Add Grayscale Images preference

This commit is contained in:
Shadowfacts 2020-11-01 13:59:58 -05:00
parent 89b35fab6d
commit eb4e6e32f7
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
21 changed files with 592 additions and 136 deletions

View File

@ -76,6 +76,7 @@ public class Client {
return return
} }
guard let result = try? Client.decoder.decode(Result.self, from: data) else { guard let result = try? Client.decoder.decode(Result.self, from: data) else {
print(request)
completion(.failure(.invalidModel)) completion(.failure(.invalidModel))
return return
} }
@ -90,7 +91,7 @@ public class Client {
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? { func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil } guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
components.path = request.path components.path = request.path
components.queryItems = request.queryParameters.queryItems components.queryItems = request.queryParameters.isEmpty ? nil : request.queryParameters.queryItems
guard let url = components.url else { return nil } guard let url = components.url else { return nil }
var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval) var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval)
urlRequest.httpMethod = request.method.name urlRequest.httpMethod = request.method.name

View File

@ -231,6 +231,7 @@
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; }; D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; };
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; }; D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; };
D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */; }; D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */; };
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */; };
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */; }; D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */; };
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; }; D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BC874421961F73006163F1 /* Gifu.framework */; }; D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BC874421961F73006163F1 /* Gifu.framework */; };
@ -569,6 +570,7 @@
D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = "<group>"; }; D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = "<group>"; };
D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = "<group>"; }; D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = "<group>"; };
D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerSheetContainerViewController.swift; sourceTree = "<group>"; }; D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerSheetContainerViewController.swift; sourceTree = "<group>"; };
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrayscalifier.swift; sourceTree = "<group>"; };
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; }; D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; };
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; }; D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
D6BC874421961F73006163F1 /* Gifu.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Gifu.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BC874421961F73006163F1 /* Gifu.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Gifu.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -1385,6 +1387,7 @@
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */, D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */, D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */, D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
D67B506B250B28FF00FAECFB /* Vendor */, D67B506B250B28FF00FAECFB /* Vendor */,
D6F1F84E2193B9BE00F5FE67 /* Caching */, D6F1F84E2193B9BE00F5FE67 /* Caching */,
D6757A7A2157E00100721E32 /* XCallbackURL */, D6757A7A2157E00100721E32 /* XCallbackURL */,
@ -1842,6 +1845,7 @@
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */, D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */, D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */, D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
D6E426812532814100C02E1C /* MaybeLazyStack.swift in Sources */, D6E426812532814100C02E1C /* MaybeLazyStack.swift in Sources */,
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */, D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
D6A3BC852321F6C100FD64D5 /* AccountListTableViewController.swift in Sources */, D6A3BC852321F6C100FD64D5 /* AccountListTableViewController.swift in Sources */,

View File

@ -0,0 +1,59 @@
//
// ImageGrayscalifier.swift
// Tusker
//
// Created by Shadowfacts on 10/29/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
struct ImageGrayscalifier {
static let queue = DispatchQueue(label: "ImageGrayscalifier", qos: .default)
private static let context = CIContext()
private static let cache = NSCache<NSURL, UIImage>()
static func convert(url: URL?, data: Data) -> UIImage? {
if let url = url,
let cached = cache.object(forKey: url as NSURL) {
return cached
}
guard let source = CIImage(data: data) else {
return nil
}
return doConvert(source, url: url)
}
static func convert(url: URL?, cgImage: CGImage) -> UIImage? {
if let url = url,
let cached = cache.object(forKey: url as NSURL) {
return cached
}
return doConvert(CIImage(cgImage: cgImage), url: url)
}
private static func doConvert(_ source: CIImage, url: URL?) -> UIImage? {
guard let filter = CIFilter(name: "CIColorMonochrome") else {
return nil
}
filter.setValue(source, forKey: "inputImage")
filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor")
filter.setValue(1.0, forKey: "inputIntensity")
guard let output = filter.outputImage,
let cgImage = context.createCGImage(output, from: output.extent) else {
return nil
}
let image = UIImage(cgImage: cgImage)
if let url = url {
cache.setObject(image, forKey: url as NSURL)
}
return image
}
}

View File

@ -64,6 +64,7 @@ class Preferences: Codable, ObservableObject {
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts) self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType) self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
self.grayscaleImages = try container.decode(Bool.self, forKey: .grayscaleImages)
self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions) self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions)
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType) self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
@ -95,6 +96,7 @@ class Preferences: Codable, ObservableObject {
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts) try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType) try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
try container.encode(grayscaleImages, forKey: .grayscaleImages)
try container.encode(silentActions, forKey: .silentActions) try container.encode(silentActions, forKey: .silentActions)
try container.encode(statusContentType, forKey: .statusContentType) try container.encode(statusContentType, forKey: .statusContentType)
@ -128,12 +130,13 @@ class Preferences: Codable, ObservableObject {
// MARK: Digital Wellness // MARK: Digital Wellness
@Published var showFavoriteAndReblogCounts = true @Published var showFavoriteAndReblogCounts = true
@Published var defaultNotificationsMode = NotificationsMode.allNotifications @Published var defaultNotificationsMode = NotificationsMode.allNotifications
@Published var grayscaleImages = false
// MARK: Advanced // MARK: Advanced
@Published var silentActions: [String: Permission] = [:] @Published var silentActions: [String: Permission] = [:]
@Published var statusContentType: StatusContentType = .plain @Published var statusContentType: StatusContentType = .plain
enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case theme case theme
case avatarStyle case avatarStyle
case hideCustomEmojiInUsernames case hideCustomEmojiInUsernames
@ -157,6 +160,7 @@ class Preferences: Codable, ObservableObject {
case showFavoriteAndReblogCounts case showFavoriteAndReblogCounts
case defaultNotificationsType case defaultNotificationsType
case grayscaleImages
case silentActions case silentActions
case statusContentType case statusContentType

View File

@ -11,10 +11,11 @@ import Gifu
import Pachyderm import Pachyderm
import AVFoundation import AVFoundation
protocol LargeImageContentView { protocol LargeImageContentView: UIView {
var animationImage: UIImage? { get } var animationImage: UIImage? { get }
var animationGifData: Data? { get } var animationGifData: Data? { get }
var activityItemsForSharing: [Any] { get } var activityItemsForSharing: [Any] { get }
func grayscaleStateChanged()
} }
class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentView { class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentView {
@ -29,6 +30,14 @@ class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentV
[image!] [image!]
} }
private var sourceData: Data?
convenience init(sourceData data: Data, isGif: Bool) {
self.init(image: UIImage(data: data)!, gifData: isGif ? data : nil)
self.sourceData = data
}
init(image: UIImage, gifData: Data?) { init(image: UIImage, gifData: Data?) {
self.animationGifData = gifData self.animationGifData = gifData
@ -50,6 +59,23 @@ class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentV
updateImageIfNeeded() updateImageIfNeeded()
} }
func grayscaleStateChanged() {
guard let data = sourceData else {
return
}
let image: UIImage?
if Preferences.shared.grayscaleImages {
image = ImageGrayscalifier.convert(url: nil, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
self.image = image
}
}
} }
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView { class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
@ -85,4 +111,8 @@ class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
func grayscaleStateChanged() {
// no-op, GifvAttachmentView observes the grayscale state itself
}
} }

View File

@ -10,8 +10,6 @@ import UIKit
class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController { class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController {
typealias ContentView = UIView & LargeImageContentView
weak var animationSourceView: UIImageView? weak var animationSourceView: UIImageView?
var largeImageController: LargeImageViewController? { self } var largeImageController: LargeImageViewController? { self }
var animationImage: UIImage? { contentView.animationImage } var animationImage: UIImage? { contentView.animationImage }
@ -31,7 +29,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
@IBOutlet weak var bottomControlsView: UIView! @IBOutlet weak var bottomControlsView: UIView!
@IBOutlet weak var descriptionLabel: UILabel! @IBOutlet weak var descriptionLabel: UILabel!
var contentView: ContentView { var contentView: LargeImageContentView {
didSet { didSet {
oldValue.removeFromSuperview() oldValue.removeFromSuperview()
setupContentView() setupContentView()
@ -50,7 +48,8 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
} }
var shrinkGestureEnabled = true var shrinkGestureEnabled = true
var prevZoomScale: CGFloat? private var prevZoomScale: CGFloat?
private var isGrayscale = false
override var prefersStatusBarHidden: Bool { override var prefersStatusBarHidden: Bool {
return true return true
@ -63,7 +62,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
return !controlsVisible return !controlsVisible
} }
init(contentView: ContentView, description: String?, sourceView: UIImageView?) { init(contentView: LargeImageContentView, description: String?, sourceView: UIImageView?) {
self.imageDescription = description self.imageDescription = description
self.animationSourceView = sourceView self.animationSourceView = sourceView
@ -103,6 +102,8 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:))) let doubleTap = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:)))
doubleTap.numberOfTapsRequired = 2 doubleTap.numberOfTapsRequired = 2
view.addGestureRecognizer(doubleTap) view.addGestureRecognizer(doubleTap)
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
} }
private func setupContentView() { private func setupContentView() {
@ -147,6 +148,13 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
closeButtonTrailingConstraint.constant = offset closeButtonTrailingConstraint.constant = offset
} }
} }
@objc private func preferencesChanged() {
if isGrayscale != Preferences.shared.grayscaleImages {
isGrayscale = Preferences.shared.grayscaleImages
contentView.grayscaleStateChanged()
}
}
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) { func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
self.controlsVisible = controlsVisible self.controlsVisible = controlsVisible

View File

@ -86,7 +86,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
view.backgroundColor = .black view.backgroundColor = .black
if let data = cache.get(url) { if let data = cache.get(url) {
createLargeImage(data: data) createLargeImage(data: data, url: url)
} else { } else {
createPreview() createPreview()
@ -97,7 +97,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
self.imageRequest = nil self.imageRequest = nil
DispatchQueue.main.async { DispatchQueue.main.async {
self.loadingVC?.removeViewAndController() self.loadingVC?.removeViewAndController()
self.createLargeImage(data: data!) self.createLargeImage(data: data!, url: self.url)
} }
} }
} }
@ -115,12 +115,21 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
} }
} }
private func createLargeImage(data: Data) { private func createLargeImage(data: Data, url: URL) {
guard !loaded else { return } guard !loaded else { return }
loaded = true loaded = true
guard let image = UIImage(data: data) else { return }
let gifData = url.pathExtension == "gif" ? data : nil let image: UIImage?
createLargeImage(image: image, gifData: gifData) if Preferences.shared.grayscaleImages {
image = ImageGrayscalifier.convert(url: url, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
let gifData = url.pathExtension == "gif" ? data : nil
createLargeImage(image: image, gifData: gifData)
}
} }
private func createLargeImage(image: UIImage, gifData: Data?) { private func createLargeImage(image: UIImage, gifData: Data?) {
@ -138,8 +147,13 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
private func createPreview() { private func createPreview() {
guard !self.loaded, guard !self.loaded,
let image = animationSourceView?.image else { return } var image = animationSourceView?.image else { return }
if Preferences.shared.grayscaleImages,
let source = image.cgImage,
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: source) {
image = grayscale
}
self.createLargeImage(image: image, gifData: nil) self.createLargeImage(image: image, gifData: nil)
} }

View File

@ -9,31 +9,28 @@
import SwiftUI import SwiftUI
struct WellnessPrefsView: View { struct WellnessPrefsView: View {
@ObservedObject var preferences = Preferences.shared @ObservedObject private var preferences = Preferences.shared
var body: some View { var body: some View {
List { List {
showFavAndReblogCountSection showFavAndReblogCount
notificationsModeSection notificationsMode
grayscaleImages
} }
.insetOrGroupedListStyle() .insetOrGroupedListStyle()
.navigationBarTitle(Text("Digital Wellness")) .navigationBarTitle(Text("Digital Wellness"))
} }
var showFavAndReblogCountSection: some View { private var showFavAndReblogCount: some View {
Section(footer: showFavAndReblogCountFooter) { Section(footer: Text("Control whether total favorite and reblog counts are shown for the main post in conversations.")) {
Toggle(isOn: $preferences.showFavoriteAndReblogCounts) { Toggle(isOn: $preferences.showFavoriteAndReblogCounts) {
Text("Show Favorite and Reblog Counts") Text("Show Favorite and Reblog Counts")
} }
} }
} }
var showFavAndReblogCountFooter: some View { private var notificationsMode: some View {
Text("Control whether total favorite and reblog counts are shown for the main post in conversations.") Section(footer: Text("Choose which kinds of notifications will be shown by default in the Notifications tab.")) {
}
var notificationsModeSection: some View {
Section(footer: notificationsModeFooter) {
Picker(selection: $preferences.defaultNotificationsMode, label: Text("Default Notifications Mode")) { Picker(selection: $preferences.defaultNotificationsMode, label: Text("Default Notifications Mode")) {
ForEach(NotificationsMode.allCases, id: \.self) { type in ForEach(NotificationsMode.allCases, id: \.self) { type in
Text(type.displayName).tag(type) Text(type.displayName).tag(type)
@ -42,8 +39,12 @@ struct WellnessPrefsView: View {
} }
} }
var notificationsModeFooter: some View { private var grayscaleImages: some View {
Text("Choose which kinds of notifications will be shown by default in the Notifications tab.") Section(footer: Text("Show attachments, avatars, headers, and custom emoji in black and white.")) {
Toggle(isOn: $preferences.grayscaleImages) {
Text("Grayscale Images")
}
}
} }
} }

View File

@ -40,8 +40,21 @@ class MyProfileViewController: ProfileViewController {
} }
private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) { private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) {
_ = ImageCache.avatars.get(account.avatar, completion: { [weak self] (data) in let avatarURL = account.avatar
guard let self = self, let data = data, let image = UIImage(data: data) else { return } _ = ImageCache.avatars.get(avatarURL, completion: { [weak self] (data) in
guard let self = self, let data = data else { return }
let maybeGrayscale: UIImage?
if Preferences.shared.grayscaleImages {
maybeGrayscale = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
maybeGrayscale = UIImage(data: data)
}
guard let image = maybeGrayscale else {
return
}
DispatchQueue.main.async { DispatchQueue.main.async {
let size = CGSize(width: 30, height: 30) let size = CGSize(width: 30, height: 30)
let rect = CGRect(origin: .zero, size: size) let rect = CGRect(origin: .zero, size: size)

View File

@ -21,7 +21,8 @@ class AccountTableViewCell: UITableViewCell {
var accountID: String! var accountID: String!
var avatarRequest: ImageCache.Request? private var avatarRequest: ImageCache.Request?
private var isGrayscale = false
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -38,6 +39,10 @@ class AccountTableViewCell: UITableViewCell {
fatalError("Missing cached account \(accountID!)") fatalError("Missing cached account \(accountID!)")
} }
displayNameLabel.updateForAccountDisplayName(account: account) displayNameLabel.updateForAccountDisplayName(account: account)
if isGrayscale != Preferences.shared.grayscaleImages {
updateGrayscaleableUI(account: account)
}
} }
func updateUI(accountID: String) { func updateUI(accountID: String) {
@ -46,21 +51,37 @@ class AccountTableViewCell: UITableViewCell {
fatalError("Missing cached account \(accountID)") fatalError("Missing cached account \(accountID)")
} }
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in usernameLabel.text = "@\(account.acct)"
updateGrayscaleableUI(account: account)
updateUIForPrefrences()
}
private func updateGrayscaleableUI(account: AccountMO) {
isGrayscale = Preferences.shared.grayscaleImages
let accountID = self.accountID
let avatarURL = account.avatar
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
guard let self = self, let data = data, self.accountID == accountID else { return } guard let self = self, let data = data, self.accountID == accountID else { return }
self.avatarRequest = nil self.avatarRequest = nil
let image: UIImage?
if self.isGrayscale {
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImageView.image = UIImage(data: data) self.avatarImageView.image = image
} }
} }
usernameLabel.text = "@\(account.acct)"
let doc = try! SwiftSoup.parse(account.note) let doc = try! SwiftSoup.parse(account.note)
noteLabel.text = try! doc.text() noteLabel.text = try! doc.text()
noteLabel.setEmojis(account.emojis, identifier: account.id) noteLabel.setEmojis(account.emojis, identifier: account.id)
updateUIForPrefrences()
} }
override func prepareForReuse() { override func prepareForReuse() {

View File

@ -28,11 +28,21 @@ class AttachmentView: UIImageView, GIFAnimatable {
var expectedSize: CGSize! var expectedSize: CGSize!
private var attachmentRequest: ImageCache.Request? private var attachmentRequest: ImageCache.Request?
private var source: Source?
var gifData: Data? var gifData: Data? {
switch source {
case let .gifData(_, data):
return data
default:
return nil
}
}
private var autoplayGifs: Bool { private var autoplayGifs: Bool {
Preferences.shared.automaticallyPlayGifs && !ProcessInfo.processInfo.isLowPowerModeEnabled Preferences.shared.automaticallyPlayGifs && !ProcessInfo.processInfo.isLowPowerModeEnabled
} }
private var isGrayscale = false
public lazy var animator: Animator? = Animator(withDelegate: self) public lazy var animator: Animator? = Animator(withDelegate: self)
@ -55,19 +65,29 @@ class AttachmentView: UIImageView, GIFAnimatable {
commonInit() commonInit()
} }
func commonInit() { private func commonInit() {
contentMode = .scaleAspectFill contentMode = .scaleAspectFill
layer.masksToBounds = true layer.masksToBounds = true
isUserInteractionEnabled = true isUserInteractionEnabled = true
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imagePressed))) addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imagePressed)))
NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), name: .NSProcessInfoPowerStateDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), name: .NSProcessInfoPowerStateDidChange, object: nil)
addInteraction(UIContextMenuInteraction(delegate: self)) addInteraction(UIContextMenuInteraction(delegate: self))
} }
@objc func gifPlaybackModeChanged() { @objc private func preferencesChanged() {
gifPlaybackModeChanged()
if isGrayscale != Preferences.shared.grayscaleImages {
ImageGrayscalifier.queue.async {
self.displayImage()
}
}
}
@objc private func gifPlaybackModeChanged() {
// NSProcessInfoPowerStateDidChange is sometimes fired on a background thread // NSProcessInfoPowerStateDidChange is sometimes fired on a background thread
DispatchQueue.main.async { DispatchQueue.main.async {
if self.attachment.kind == .image, if self.attachment.kind == .image,
@ -106,13 +126,19 @@ class AttachmentView: UIImageView, GIFAnimatable {
} else { } else {
size = self.expectedSize size = self.expectedSize
} }
if let preview = UIImage(blurHash: hash, size: size) {
DispatchQueue.main.async { [weak self] in guard var preview = UIImage(blurHash: hash, size: size) else {
guard let self = self else { return } return
if self.image == nil { }
self.image = preview
} if Preferences.shared.grayscaleImages,
} let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: preview.cgImage!) {
preview = grayscale
}
DispatchQueue.main.async { [weak self] in
guard let self = self, self.image == nil else { return }
self.image = preview
} }
} }
} }
@ -132,35 +158,34 @@ class AttachmentView: UIImageView, GIFAnimatable {
} }
func loadImage() { func loadImage() {
attachmentRequest = ImageCache.attachments.get(attachment.url) { [weak self] (data) in let attachmentURL = attachment.url
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data) in
guard let self = self, let data = data else { return } guard let self = self, let data = data else { return }
self.attachmentRequest = nil self.attachmentRequest = nil
DispatchQueue.main.async { if self.attachment.url.pathExtension == "gif" {
if self.attachment.url.pathExtension == "gif" { self.source = .gifData(attachmentURL, data)
self.gifData = data if self.autoplayGifs {
if self.autoplayGifs { self.animate(withGIFData: data)
self.animate(withGIFData: data)
} else {
self.image = UIImage(data: data)
}
} else { } else {
self.image = UIImage(data: data) self.displayImage()
} }
} else {
self.source = .imageData(attachmentURL, data)
self.displayImage()
} }
} }
} }
func loadVideo() { func loadVideo() {
let attachmentURL = self.attachment.url let attachmentURL = self.attachment.url
// todo: use a single dispatch queue
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
let asset = AVURLAsset(url: attachmentURL) let asset = AVURLAsset(url: attachmentURL)
let generator = AVAssetImageGenerator(asset: asset) let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true generator.appliesPreferredTrackTransform = true
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return } guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
DispatchQueue.main.async { [weak self] in self.source = .cgImage(attachmentURL, image)
guard let self = self, self.attachment.url == attachmentURL else { return } self.displayImage()
self.image = UIImage(cgImage: image)
}
} }
let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill")) let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
@ -202,10 +227,8 @@ class AttachmentView: UIImageView, GIFAnimatable {
let generator = AVAssetImageGenerator(asset: asset) let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true generator.appliesPreferredTrackTransform = true
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return } guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
DispatchQueue.main.async { [weak self] in self.source = .cgImage(attachmentURL, image)
guard let self = self, self.attachment.url == attachmentURL else { return } self.displayImage()
self.image = UIImage(cgImage: image)
}
} }
let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill) let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
@ -223,6 +246,35 @@ class AttachmentView: UIImageView, GIFAnimatable {
]) ])
} }
private func displayImage() {
isGrayscale = Preferences.shared.grayscaleImages
let image: UIImage?
switch source {
case nil:
image = nil
case let .imageData(url, data), let .gifData(url, data):
if isGrayscale {
image = ImageGrayscalifier.convert(url: url, data: data)
} else {
image = UIImage(data: data)
}
case let .cgImage(url, cgImage):
if isGrayscale {
image = ImageGrayscalifier.convert(url: url, cgImage: cgImage)
} else {
image = UIImage(cgImage: cgImage)
}
}
DispatchQueue.main.async {
self.image = image
}
}
override func display(_ layer: CALayer) { override func display(_ layer: CALayer) {
super.display(layer) super.display(layer)
@ -242,6 +294,14 @@ class AttachmentView: UIImageView, GIFAnimatable {
} }
fileprivate extension AttachmentView {
enum Source {
case imageData(URL, Data)
case gifData(URL, Data)
case cgImage(URL, CGImage)
}
}
extension AttachmentView: UIContextMenuInteractionDelegate { extension AttachmentView: UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in

View File

@ -19,12 +19,16 @@ class GifvAttachmentView: UIView {
layer as! AVPlayerLayer layer as! AVPlayerLayer
} }
let item: AVPlayerItem private var asset: AVAsset
private(set) var item: AVPlayerItem
let player: AVPlayer let player: AVPlayer
private var isGrayscale = false
init(asset: AVAsset, gravity: AVLayerVideoGravity) { init(asset: AVAsset, gravity: AVLayerVideoGravity) {
item = AVPlayerItem(asset: asset) self.asset = asset
item = GifvAttachmentView.createItem(asset: asset)
player = AVPlayer(playerItem: item) player = AVPlayer(playerItem: item)
isGrayscale = Preferences.shared.grayscaleImages
super.init(frame: .zero) super.init(frame: .zero)
@ -33,13 +37,39 @@ class GifvAttachmentView: UIView {
player.isMuted = true player.isMuted = true
NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item) NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item)
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@objc func restartItem() { private static func createItem(asset: AVAsset) -> AVPlayerItem {
let item = AVPlayerItem(asset: asset)
if Preferences.shared.grayscaleImages {
item.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { (request) in
let filter = CIFilter(name: "CIColorMonochrome")!
filter.setValue(request.sourceImage, forKey: "inputImage")
filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor")
filter.setValue(1.0, forKey: "inputIntensity")
request.finish(with: filter.outputImage!, context: nil)
})
}
return item
}
@objc private func preferencesChanged() {
if isGrayscale != Preferences.shared.grayscaleImages {
isGrayscale = Preferences.shared.grayscaleImages
item = GifvAttachmentView.createItem(asset: asset)
player.replaceCurrentItem(with: item)
player.play()
}
}
@objc private func restartItem() {
item.seek(to: .zero) { (success) in item.seek(to: .zero) { (success) in
guard success else { return } guard success else { return }
self.player.play() self.player.play()

View File

@ -45,10 +45,18 @@ extension BaseEmojiLabel {
group.enter() group.enter()
let request = ImageCache.emojis.get(emoji.url) { (data) in let request = ImageCache.emojis.get(emoji.url) { (data) in
defer { group.leave() } defer { group.leave() }
guard let data = data, let image = UIImage(data: data) else { guard let data = data else {
return return
} }
emojiImages[emoji.shortcode] = image let image: UIImage?
if Preferences.shared.grayscaleImages {
image = ImageGrayscalifier.convert(url: emoji.url, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
emojiImages[emoji.shortcode] = image
}
} }
if let request = request { if let request = request {
emojiRequests.append(request) emojiRequests.append(request)

View File

@ -59,10 +59,18 @@ class ContentTextView: LinkTextView {
group.enter() group.enter()
_ = ImageCache.emojis.get(emoji.url) { (data) in _ = ImageCache.emojis.get(emoji.url) { (data) in
defer { group.leave() } defer { group.leave() }
guard let data = data, let image = UIImage(data: data) else { guard let data = data else {
return return
} }
emojiImages[emoji.shortcode] = image let image: UIImage?
if Preferences.shared.grayscaleImages {
image = ImageGrayscalifier.convert(url: emoji.url, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
emojiImages[emoji.shortcode] = image
}
} }
} }

View File

@ -25,8 +25,9 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
var group: NotificationGroup! var group: NotificationGroup!
var statusID: String! var statusID: String!
var avatarRequests = [String: ImageCache.Request]() private var avatarRequests = [String: ImageCache.Request]()
var updateTimestampWorkItem: DispatchWorkItem? private var updateTimestampWorkItem: DispatchWorkItem?
private var isGrayscale = false
deinit { deinit {
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
@ -44,6 +45,10 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews { for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews {
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView) imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
} }
if isGrayscale != Preferences.shared.grayscaleImages {
updateGrayscaleableUI()
}
} }
func updateUI(group: NotificationGroup) { func updateUI(group: NotificationGroup) {
@ -67,8 +72,10 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
fatalError() fatalError()
} }
isGrayscale = Preferences.shared.grayscaleImages
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) } let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
var imageViews = [UIImageView]() var imageViews = [UIImageView]()
for account in people { for account in people {
@ -76,11 +83,22 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
imageView.translatesAutoresizingMaskIntoConstraints = false imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.layer.masksToBounds = true imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
avatarRequests[account.id] = ImageCache.avatars.get(account.avatar) { [weak self] (data) in let avatarURL = account.avatar
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
guard let self = self, let data = data, self.group.id == group.id else { return } guard let self = self, let data = data, self.group.id == group.id else { return }
DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id) let image: UIImage?
imageView.image = UIImage(data: data) if self.isGrayscale {
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id)
imageView.image = image
}
} }
} }
actionAvatarStackView.addArrangedSubview(imageView) actionAvatarStackView.addArrangedSubview(imageView)
@ -104,7 +122,38 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
statusContentLabel.text = try! doc.text() statusContentLabel.text = try! doc.text()
} }
func updateTimestamp() { private func updateGrayscaleableUI() {
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
let groupID = group.id
for (index, account) in people.enumerated() {
guard actionAvatarStackView.arrangedSubviews.count > index,
let imageView = actionAvatarStackView.arrangedSubviews[index] as? UIImageView else {
continue
}
let avatarURL = account.avatar
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
guard let self = self, let data = data, self.group.id == groupID else { return }
let image: UIImage?
if self.isGrayscale {
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id)
imageView.image = image
}
}
}
}
}
private func updateTimestamp() {
guard let notification = group.notifications.first else { guard let notification = group.notifications.first else {
fatalError("Missing cached notification") fatalError("Missing cached notification")
} }

View File

@ -20,8 +20,9 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
var group: NotificationGroup! var group: NotificationGroup!
var avatarRequests = [String: ImageCache.Request]() private var avatarRequests = [String: ImageCache.Request]()
var updateTimestampWorkItem: DispatchWorkItem? private var updateTimestampWorkItem: DispatchWorkItem?
private var isGrayscale = false
deinit { deinit {
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
@ -39,6 +40,10 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
for case let imageView as UIImageView in avatarStackView.arrangedSubviews { for case let imageView as UIImageView in avatarStackView.arrangedSubviews {
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView) imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
} }
if isGrayscale != Preferences.shared.grayscaleImages {
updateGrayscaleableUI()
}
} }
func updateUI(group: NotificationGroup) { func updateUI(group: NotificationGroup) {
@ -51,17 +56,30 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
}, identifier: group.id) }, identifier: group.id)
updateTimestamp() updateTimestamp()
isGrayscale = Preferences.shared.grayscaleImages
avatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } avatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
for account in people { for account in people {
let imageView = UIImageView() let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.layer.masksToBounds = true imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
avatarRequests[account.id] = ImageCache.avatars.get(account.avatar) { [weak self] (data) in let avatarURL = account.avatar
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
guard let self = self, let data = data, self.group.id == group.id else { return } guard let self = self, let data = data, self.group.id == group.id else { return }
DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id) let image: UIImage?
imageView.image = UIImage(data: data) if Preferences.shared.grayscaleImages {
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id)
imageView.image = image
}
} }
} }
avatarStackView.addArrangedSubview(imageView) avatarStackView.addArrangedSubview(imageView)
@ -72,6 +90,39 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
} }
} }
private func updateGrayscaleableUI() {
isGrayscale = Preferences.shared.grayscaleImages
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
let groupID = group.id
for (index, account) in people.enumerated() {
guard avatarStackView.arrangedSubviews.count > index,
let imageView = avatarStackView.arrangedSubviews[index] as? UIImageView else {
continue
}
let avatarURL = account.avatar
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
guard let self = self, let data = data, self.group.id == groupID else { return }
let image: UIImage?
if self.isGrayscale {
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id)
imageView.image = image
}
}
}
}
}
func updateActionLabel(names: [NSAttributedString]) -> NSAttributedString { func updateActionLabel(names: [NSAttributedString]) -> NSAttributedString {
// todo: figure out how to localize this // todo: figure out how to localize this
let str = NSMutableAttributedString(string: "Followed by ") let str = NSMutableAttributedString(string: "Followed by ")

View File

@ -25,8 +25,9 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
var notification: Pachyderm.Notification? var notification: Pachyderm.Notification?
var account: Account! var account: Account!
var avatarRequest: ImageCache.Request? private var avatarRequest: ImageCache.Request?
var updateTimestampWorkItem: DispatchWorkItem? private var updateTimestampWorkItem: DispatchWorkItem?
private var isGrayscale = false
deinit { deinit {
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
@ -43,6 +44,11 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30 avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
if isGrayscale != Preferences.shared.grayscaleImages,
let account = self.account {
updateUI(account: account)
}
} }
func updateUI(notification: Pachyderm.Notification) { func updateUI(notification: Pachyderm.Notification) {
@ -61,16 +67,27 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
actionLabel.text = "Request to follow from \(account.displayName)" actionLabel.text = "Request to follow from \(account.displayName)"
actionLabel.setEmojis(account.emojis, identifier: account.id) actionLabel.setEmojis(account.emojis, identifier: account.id)
} }
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in let avatarURL = account.avatar
guard let self = self, self.account == account, let data = data, let image = UIImage(data: data) else { return } avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
guard let self = self, self.account == account, let data = data else { return }
self.avatarRequest = nil self.avatarRequest = nil
DispatchQueue.main.async {
self.avatarImageView.image = image let image: UIImage?
if self.isGrayscale {
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
DispatchQueue.main.async {
self.avatarImageView.image = image
}
} }
} }
} }
func updateTimestamp() { private func updateTimestamp() {
guard let notification = notification else { return } guard let notification = notification else { return }
timestampLabel.text = notification.createdAt.timeAgoString() timestampLabel.text = notification.createdAt.timeAgoString()

View File

@ -48,6 +48,8 @@ class ProfileHeaderView: UIView {
private var avatarRequest: ImageCache.Request? private var avatarRequest: ImageCache.Request?
private var headerRequest: ImageCache.Request? private var headerRequest: ImageCache.Request?
private var isGrayscale = false
private var cancellables = [AnyCancellable]() private var cancellables = [AnyCancellable]()
deinit { deinit {
@ -106,22 +108,7 @@ class ProfileHeaderView: UIView {
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in updateImages(account: account)
guard let self = self, let data = data, self.accountID == accountID else { return }
self.avatarRequest = nil
DispatchQueue.main.async {
self.avatarImageView.image = UIImage(data: data)
}
}
if let header = account.header {
headerRequest = ImageCache.headers.get(header) { [weak self] (data) in
guard let self = self, let data = data, self.accountID == accountID else { return }
self.headerRequest = nil
DispatchQueue.main.async {
self.headerImageView.image = UIImage(data: data)
}
}
}
if #available(iOS 14.0, *) { if #available(iOS 14.0, *) {
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForProfile(accountID: accountID, sourceView: moreButton)) moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForProfile(accountID: accountID, sourceView: moreButton))
@ -193,6 +180,49 @@ class ProfileHeaderView: UIView {
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView) avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
displayNameLabel.updateForAccountDisplayName(account: account) displayNameLabel.updateForAccountDisplayName(account: account)
if isGrayscale != Preferences.shared.grayscaleImages {
updateImages(account: account)
}
}
private func updateImages(account: AccountMO) {
isGrayscale = Preferences.shared.grayscaleImages
let accountID = account.id
let avatarURL = account.avatar
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
guard let self = self, let data = data, self.accountID == accountID else { return }
self.avatarRequest = nil
let image: UIImage?
if self.isGrayscale {
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
DispatchQueue.main.async {
self.avatarImageView.image = image
}
}
if let header = account.header {
headerRequest = ImageCache.headers.get(header) { [weak self] (data) in
guard let self = self, let data = data, self.accountID == accountID else { return }
self.headerRequest = nil
let image: UIImage?
if self.isGrayscale {
image = ImageGrayscalifier.convert(url: header, data: data)
} else {
image = UIImage(data: data)
}
DispatchQueue.main.async {
self.headerImageView.image = image
}
}
}
} }
// MARK: Interaction // MARK: Interaction

View File

@ -74,6 +74,8 @@ class BaseStatusTableViewCell: UITableViewCell {
private var currentPictureInPictureVideoStatusID: String? private var currentPictureInPictureVideoStatusID: String?
private var isGrayscale = false
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -142,6 +144,7 @@ class BaseStatusTableViewCell: UITableViewCell {
let account = status.account let account = status.account
self.accountID = account.id self.accountID = account.id
updateUI(account: account) updateUI(account: account)
updateGrayscaleableUI(account: account, status: status)
updateUIForPreferences(account: account, status: status) updateUIForPreferences(account: account, status: status)
cardView.card = status.card cardView.card = status.card
@ -154,8 +157,6 @@ class BaseStatusTableViewCell: UITableViewCell {
updateStatusState(status: status) updateStatusState(status: status)
contentTextView.setTextFrom(status: status)
contentWarningLabel.text = status.spoilerText contentWarningLabel.text = status.spoilerText
contentWarningLabel.isHidden = status.spoilerText.isEmpty contentWarningLabel.isHidden = status.spoilerText.isEmpty
if !contentWarningLabel.isHidden { if !contentWarningLabel.isHidden {
@ -215,12 +216,6 @@ class BaseStatusTableViewCell: UITableViewCell {
func updateUI(account: AccountMO) { func updateUI(account: AccountMO) {
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
avatarImageView.image = nil avatarImageView.image = nil
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
DispatchQueue.main.async {
guard let self = self, let data = data, self.accountID == account.id else { return }
self.avatarImageView.image = UIImage(data: data)
}
}
} }
@objc private func preferencesChanged() { @objc private func preferencesChanged() {
@ -232,10 +227,13 @@ class BaseStatusTableViewCell: UITableViewCell {
func updateUIForPreferences(account: AccountMO, status: StatusMO) { func updateUIForPreferences(account: AccountMO, status: StatusMO) {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
displayNameLabel.updateForAccountDisplayName(account: account)
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false) attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false)
updateStatusIconsForPreferences(status) updateStatusIconsForPreferences(status)
if isGrayscale != Preferences.shared.grayscaleImages {
updateGrayscaleableUI(account: account, status: status)
}
} }
func updateStatusIconsForPreferences(_ status: StatusMO) { func updateStatusIconsForPreferences(_ status: StatusMO) {
@ -253,6 +251,31 @@ class BaseStatusTableViewCell: UITableViewCell {
reblogButton.setImage(reblogButtonImage, for: .normal) reblogButton.setImage(reblogButtonImage, for: .normal)
} }
func updateGrayscaleableUI(account: AccountMO, status: StatusMO) {
isGrayscale = Preferences.shared.grayscaleImages
let avatarURL = account.avatar
let accountID = account.id
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
guard let self = self, let data = data, self.accountID == accountID else { return }
let image: UIImage?
if self.isGrayscale {
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
DispatchQueue.main.async {
self.avatarImageView.image = image
}
}
contentTextView.setTextFrom(status: status)
displayNameLabel.updateForAccountDisplayName(account: account)
}
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()

View File

@ -26,6 +26,7 @@ class StatusCardView: UIView {
private let inactiveBackgroundColor = UIColor.secondarySystemBackground private let inactiveBackgroundColor = UIColor.secondarySystemBackground
private var imageRequest: ImageCache.Request? private var imageRequest: ImageCache.Request?
private var isGrayscale = false
private var titleLabel: UILabel! private var titleLabel: UILabel!
private var descriptionLabel: UILabel! private var descriptionLabel: UILabel!
@ -108,26 +109,15 @@ class StatusCardView: UIView {
placeholderImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), placeholderImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
placeholderImageView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), placeholderImageView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
]) ])
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
} }
private func updateUI(card: Card) { private func updateUI(card: Card) {
self.imageView.image = nil self.imageView.image = nil
if let image = card.image { updateGrayscaleableUI(card: card)
placeholderImageView.isHidden = true updateUIForPreferences()
imageRequest = ImageCache.attachments.get(image, completion: { (data) in
guard let data = data, let image = UIImage(data: data) else { return }
DispatchQueue.main.async {
self.imageView.image = image
}
})
if imageRequest != nil {
loadBlurHash()
}
} else {
placeholderImageView.isHidden = false
}
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines) let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
titleLabel.text = title titleLabel.text = title
@ -138,6 +128,41 @@ class StatusCardView: UIView {
descriptionLabel.isHidden = description.isEmpty descriptionLabel.isHidden = description.isEmpty
} }
@objc private func updateUIForPreferences() {
if isGrayscale != Preferences.shared.grayscaleImages,
let card = card {
updateGrayscaleableUI(card: card)
}
}
private func updateGrayscaleableUI(card: Card) {
isGrayscale = Preferences.shared.grayscaleImages
if let imageURL = card.image {
placeholderImageView.isHidden = true
imageRequest = ImageCache.attachments.get(imageURL, completion: { (data) in
guard let data = data else { return }
let image: UIImage?
if self.isGrayscale {
image = ImageGrayscalifier.convert(url: imageURL, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
DispatchQueue.main.async {
self.imageView.image = image
}
}
})
if imageRequest != nil {
loadBlurHash()
}
} else {
placeholderImageView.isHidden = false
}
}
private func loadBlurHash() { private func loadBlurHash() {
guard let card = card, let hash = card.blurhash else { return } guard let card = card, let hash = card.blurhash else { return }

View File

@ -94,8 +94,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
pinImageView.isHidden = !pinned pinImageView.isHidden = !pinned
} }
override func updateUIForPreferences(account: AccountMO, status: StatusMO) { override func updateGrayscaleableUI(account: AccountMO, status: StatusMO) {
super.updateUIForPreferences(account: account, status: status) super.updateGrayscaleableUI(account: account, status: status)
if let rebloggerID = rebloggerID, if let rebloggerID = rebloggerID,
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) { let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {