Compare commits
8 Commits
5b03e0cf12
...
0b6ef6517b
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 0b6ef6517b | |
Shadowfacts | 34a01094f7 | |
Shadowfacts | 95b215c6b5 | |
Shadowfacts | e21dceb3b3 | |
Shadowfacts | 9534f19262 | |
Shadowfacts | e44ae29775 | |
Shadowfacts | a5b30c4243 | |
Shadowfacts | 479ca23e00 |
|
@ -17,6 +17,7 @@ public class Attachment: Codable {
|
||||||
public let textURL: URL?
|
public let textURL: URL?
|
||||||
public let meta: Metadata?
|
public let meta: Metadata?
|
||||||
public let description: String?
|
public let description: String?
|
||||||
|
public let blurHash: String?
|
||||||
|
|
||||||
public static func update(_ attachment: Attachment, focus: (Float, Float)?, description: String?) -> Request<Attachment> {
|
public static func update(_ attachment: Attachment, focus: (Float, Float)?, description: String?) -> Request<Attachment> {
|
||||||
return Request<Attachment>(method: .put, path: "/api/v1/media/\(attachment.id)", body: .formData([
|
return Request<Attachment>(method: .put, path: "/api/v1/media/\(attachment.id)", body: .formData([
|
||||||
|
@ -35,6 +36,7 @@ public class Attachment: Codable {
|
||||||
self.textURL = try? container.decode(URL?.self, forKey: .textURL)
|
self.textURL = try? container.decode(URL?.self, forKey: .textURL)
|
||||||
self.meta = try? container.decode(Metadata?.self, forKey: .meta)
|
self.meta = try? container.decode(Metadata?.self, forKey: .meta)
|
||||||
self.description = try? container.decode(String?.self, forKey: .description)
|
self.description = try? container.decode(String?.self, forKey: .description)
|
||||||
|
self.blurHash = try? container.decode(String?.self, forKey: .blurHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
@ -46,6 +48,7 @@ public class Attachment: Codable {
|
||||||
case textURL = "text_url"
|
case textURL = "text_url"
|
||||||
case meta
|
case meta
|
||||||
case description
|
case description
|
||||||
|
case blurHash = "blurhash"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +63,7 @@ extension Attachment {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Attachment {
|
extension Attachment {
|
||||||
public class Metadata: Codable {
|
public struct Metadata: Codable {
|
||||||
public let length: String?
|
public let length: String?
|
||||||
public let duration: Float?
|
public let duration: Float?
|
||||||
public let audioEncoding: String?
|
public let audioEncoding: String?
|
||||||
|
@ -91,7 +94,7 @@ extension Attachment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ImageMetadata: Codable {
|
public struct ImageMetadata: Codable {
|
||||||
public let width: Int?
|
public let width: Int?
|
||||||
public let height: Int?
|
public let height: Int?
|
||||||
public let size: String?
|
public let size: String?
|
||||||
|
|
|
@ -169,6 +169,7 @@
|
||||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */; };
|
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */; };
|
||||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; };
|
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */; };
|
||||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
|
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
|
||||||
|
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67B506C250B291200FAECFB /* BlurHashDecode.swift */; };
|
||||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
||||||
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
|
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
|
||||||
|
@ -494,6 +495,7 @@
|
||||||
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = "<group>"; };
|
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = "<group>"; };
|
||||||
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
|
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
|
||||||
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
|
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
|
||||||
|
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
||||||
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
||||||
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
||||||
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; };
|
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; };
|
||||||
|
@ -1119,6 +1121,14 @@
|
||||||
path = XCallbackURL;
|
path = XCallbackURL;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D67B506B250B28FF00FAECFB /* Vendor */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D67B506C250B291200FAECFB /* BlurHashDecode.swift */,
|
||||||
|
);
|
||||||
|
path = Vendor;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D67C57A721E2649B00C3118B /* Account Detail */ = {
|
D67C57A721E2649B00C3118B /* Account Detail */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1337,6 +1347,7 @@
|
||||||
D64D8CA82463B494006B0BAA /* CachedDictionary.swift */,
|
D64D8CA82463B494006B0BAA /* CachedDictionary.swift */,
|
||||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||||
|
D67B506B250B28FF00FAECFB /* Vendor */,
|
||||||
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
||||||
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
||||||
D62D241E217AA46B005076CC /* Shortcuts */,
|
D62D241E217AA46B005076CC /* Shortcuts */,
|
||||||
|
@ -1800,6 +1811,7 @@
|
||||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */,
|
||||||
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
|
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
|
||||||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
||||||
|
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
||||||
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
|
D62275A024F1677200B82A16 /* ComposeHostingController.swift in Sources */,
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
||||||
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
|
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
|
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "4637a7854de2cc5c354d46fb931d74bdbc2c043e",
|
"revision": "6b7ca9a2faad6ea990ff60b0a3ee4fdf3db59150",
|
||||||
"version": "1.7.0"
|
"version": "1.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
"repositoryURL": "https://git.shadowfacts.net/shadowfacts/SheetController.git",
|
"repositoryURL": "https://git.shadowfacts.net/shadowfacts/SheetController.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"revision": "6926446c4e15eb7f4513c4c00df9279553b330be",
|
"revision": "7ac34efeabb5b5eb08fcf3d1235dbc9ca0441662",
|
||||||
"version": null
|
"version": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -47,4 +47,15 @@ enum Cache<T> {
|
||||||
try hybrid.setObject(object, forKey: key, expiry: expiry)
|
try hybrid.setObject(object, forKey: key, expiry: expiry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removeAll() throws {
|
||||||
|
switch self {
|
||||||
|
case let .memory(memory):
|
||||||
|
memory.removeAll()
|
||||||
|
case let .disk(disk):
|
||||||
|
try disk.removeAll()
|
||||||
|
case let .hybrid(hybrid):
|
||||||
|
try hybrid.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ class ImageCache {
|
||||||
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
|
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
|
||||||
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
|
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60))
|
||||||
|
|
||||||
let cache: Cache<Data>
|
private let cache: Cache<Data>
|
||||||
|
|
||||||
private var groups = [URL: RequestGroup]()
|
private var groups = [URL: RequestGroup]()
|
||||||
|
|
||||||
|
@ -68,6 +68,10 @@ class ImageCache {
|
||||||
groups[url]?.cancelWithoutCallback()
|
groups[url]?.cancelWithoutCallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reset() throws {
|
||||||
|
try cache.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
private class RequestGroup {
|
private class RequestGroup {
|
||||||
let url: URL
|
let url: URL
|
||||||
private let onFinished: (Data?) -> Void
|
private let onFinished: (Data?) -> Void
|
||||||
|
|
|
@ -77,6 +77,7 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
||||||
case .image:
|
case .image:
|
||||||
let vc = LoadingLargeImageViewController(attachment: attachment)
|
let vc = LoadingLargeImageViewController(attachment: attachment)
|
||||||
vc.shrinkGestureEnabled = false
|
vc.shrinkGestureEnabled = false
|
||||||
|
vc.animationSourceView = sourceViews[index]
|
||||||
return vc
|
return vc
|
||||||
case .video, .audio:
|
case .video, .audio:
|
||||||
let vc = GalleryPlayerViewController()
|
let vc = GalleryPlayerViewController()
|
||||||
|
|
|
@ -31,7 +31,12 @@ 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: ContentView {
|
||||||
|
didSet {
|
||||||
|
oldValue.removeFromSuperview()
|
||||||
|
setupContentView()
|
||||||
|
}
|
||||||
|
}
|
||||||
var contentViewLeadingConstraint: NSLayoutConstraint!
|
var contentViewLeadingConstraint: NSLayoutConstraint!
|
||||||
var contentViewTopConstraint: NSLayoutConstraint!
|
var contentViewTopConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
@ -76,14 +81,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
setupContentView()
|
||||||
scrollView.addSubview(contentView)
|
|
||||||
contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
|
|
||||||
contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
contentViewLeadingConstraint,
|
|
||||||
contentViewTopConstraint,
|
|
||||||
])
|
|
||||||
|
|
||||||
setControlsVisible(initialControlsVisible, animated: false)
|
setControlsVisible(initialControlsVisible, animated: false)
|
||||||
shareButton.isEnabled = !contentView.activityItemsForSharing.isEmpty
|
shareButton.isEnabled = !contentView.activityItemsForSharing.isEmpty
|
||||||
|
@ -107,6 +105,17 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
view.addGestureRecognizer(doubleTap)
|
view.addGestureRecognizer(doubleTap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setupContentView() {
|
||||||
|
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
scrollView.addSubview(contentView)
|
||||||
|
contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
|
||||||
|
contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
contentViewLeadingConstraint,
|
||||||
|
contentViewTopConstraint,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
super.viewDidLayoutSubviews()
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
@ -124,8 +133,9 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
centerImage()
|
centerImage()
|
||||||
|
|
||||||
// todo: does this need to be in viewDidLayoutSubviews?
|
// todo: does this need to be in viewDidLayoutSubviews?
|
||||||
if view.safeAreaInsets.top == 44 {
|
// on iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max, the top safe area inset is 44pts
|
||||||
// running on iPhone X style notched device
|
// on iPhone XR, 11, the top inset is 48pts
|
||||||
|
if view.safeAreaInsets.top == 44 || view.safeAreaInsets.top == 48 {
|
||||||
let notchWidth: CGFloat = 209
|
let notchWidth: CGFloat = 209
|
||||||
let earWidth = (view.bounds.width - notchWidth) / 2
|
let earWidth = (view.bounds.width - notchWidth) / 2
|
||||||
let offset = (earWidth - shareButton.bounds.width) / 2
|
let offset = (earWidth - shareButton.bounds.width) / 2
|
||||||
|
|
|
@ -10,14 +10,16 @@ import Pachyderm
|
||||||
|
|
||||||
class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableViewController {
|
class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableViewController {
|
||||||
|
|
||||||
|
private var attachment: Attachment?
|
||||||
let url: URL
|
let url: URL
|
||||||
let cache: ImageCache
|
let cache: ImageCache
|
||||||
let imageDescription: String?
|
let imageDescription: String?
|
||||||
|
|
||||||
var largeImageVC: LargeImageViewController?
|
private(set) var loaded = false
|
||||||
var loadingVC: LoadingViewController?
|
private(set) var largeImageVC: LargeImageViewController?
|
||||||
|
private var loadingVC: LoadingViewController?
|
||||||
|
|
||||||
var imageRequest: ImageCache.Request?
|
private var imageRequest: ImageCache.Request?
|
||||||
|
|
||||||
private var initialControlsVisible: Bool = true
|
private var initialControlsVisible: Bool = true
|
||||||
var controlsVisible: Bool {
|
var controlsVisible: Bool {
|
||||||
|
@ -70,6 +72,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
|
|
||||||
convenience init(attachment: Attachment) {
|
convenience init(attachment: Attachment) {
|
||||||
self.init(url: attachment.url, cache: .attachments, imageDescription: attachment.description)
|
self.init(url: attachment.url, cache: .attachments, imageDescription: attachment.description)
|
||||||
|
self.attachment = attachment
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -85,6 +88,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
if let data = cache.get(url) {
|
if let data = cache.get(url) {
|
||||||
createLargeImage(data: data)
|
createLargeImage(data: data)
|
||||||
} else {
|
} else {
|
||||||
|
createPreview()
|
||||||
|
|
||||||
loadingVC = LoadingViewController()
|
loadingVC = LoadingViewController()
|
||||||
embedChild(loadingVC!)
|
embedChild(loadingVC!)
|
||||||
imageRequest = cache.get(url) { [weak self] (data) in
|
imageRequest = cache.get(url) { [weak self] (data) in
|
||||||
|
@ -110,15 +115,32 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createLargeImage(data: Data) {
|
private func createLargeImage(data: Data) {
|
||||||
|
guard !loaded else { return }
|
||||||
|
loaded = true
|
||||||
guard let image = UIImage(data: data) else { return }
|
guard let image = UIImage(data: data) else { return }
|
||||||
let gifData = url.pathExtension == "gif" ? data : nil
|
let gifData = url.pathExtension == "gif" ? data : nil
|
||||||
|
createLargeImage(image: image, gifData: gifData)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createLargeImage(image: UIImage, gifData: Data?) {
|
||||||
let imageView = LargeImageImageContentView(image: image, gifData: gifData)
|
let imageView = LargeImageImageContentView(image: image, gifData: gifData)
|
||||||
|
|
||||||
largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView)
|
if let existing = largeImageVC {
|
||||||
largeImageVC!.initialControlsVisible = initialControlsVisible
|
existing.contentView = imageView
|
||||||
largeImageVC!.shrinkGestureEnabled = false
|
} else {
|
||||||
embedChild(largeImageVC!)
|
largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView)
|
||||||
|
largeImageVC!.initialControlsVisible = initialControlsVisible
|
||||||
|
largeImageVC!.shrinkGestureEnabled = false
|
||||||
|
embedChild(largeImageVC!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createPreview() {
|
||||||
|
guard !self.loaded,
|
||||||
|
let image = animationSourceView?.image else { return }
|
||||||
|
|
||||||
|
self.createLargeImage(image: image, gifData: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ extension LargeImageAnimatableViewController {
|
||||||
class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
||||||
|
|
||||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||||
return 0.5
|
return 0.4
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||||
|
@ -48,7 +48,7 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
}
|
}
|
||||||
|
|
||||||
let containerView = transitionContext.containerView
|
let containerView = transitionContext.containerView
|
||||||
|
containerView.addSubview(toVC.view)
|
||||||
|
|
||||||
let finalVCFrame = transitionContext.finalFrame(for: toVC)
|
let finalVCFrame = transitionContext.finalFrame(for: toVC)
|
||||||
guard let sourceView = toVC.animationSourceView,
|
guard let sourceView = toVC.animationSourceView,
|
||||||
|
@ -85,12 +85,11 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
||||||
imageView.layer.maskedCorners = sourceView.layer.maskedCorners
|
imageView.layer.maskedCorners = sourceView.layer.maskedCorners
|
||||||
imageView.layer.masksToBounds = true
|
imageView.layer.masksToBounds = true
|
||||||
|
|
||||||
containerView.addSubview(toVC.view)
|
|
||||||
containerView.addSubview(imageView)
|
containerView.addSubview(imageView)
|
||||||
|
|
||||||
let duration = transitionDuration(using: transitionContext)
|
let duration = transitionDuration(using: transitionContext)
|
||||||
let velocity = 1 / CGFloat(duration)
|
let velocity = 1 / CGFloat(duration)
|
||||||
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: []) {
|
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.75, initialSpringVelocity: velocity, options: []) {
|
||||||
imageView.frame = finalFrame
|
imageView.frame = finalFrame
|
||||||
imageView.layer.cornerRadius = 0
|
imageView.layer.cornerRadius = 0
|
||||||
toVC.view.alpha = 1
|
toVC.view.alpha = 1
|
||||||
|
|
|
@ -46,12 +46,15 @@ struct AdvancedPrefsView : View {
|
||||||
var cachingSection: some View {
|
var cachingSection: some View {
|
||||||
Section(header: Text("Caching")) {
|
Section(header: Text("Caching")) {
|
||||||
Button(action: clearCache) {
|
Button(action: clearCache) {
|
||||||
Text("Clear Cache")
|
Text("Clear Mastodon Cache")
|
||||||
|
}.foregroundColor(.red)
|
||||||
|
Button(action: clearImageCaches) {
|
||||||
|
Text("Clear Image Caches")
|
||||||
}.foregroundColor(.red)
|
}.foregroundColor(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearCache() {
|
private func clearCache() {
|
||||||
for account in LocalData.shared.accounts {
|
for account in LocalData.shared.accounts {
|
||||||
let controller = MastodonController.getForAccount(account)
|
let controller = MastodonController.getForAccount(account)
|
||||||
let coordinator = controller.persistentContainer.persistentStoreCoordinator
|
let coordinator = controller.persistentContainer.persistentStoreCoordinator
|
||||||
|
@ -59,7 +62,22 @@ struct AdvancedPrefsView : View {
|
||||||
try! coordinator.destroyPersistentStore(at: store.url!, ofType: store.type, options: store.options)
|
try! coordinator.destroyPersistentStore(at: store.url!, ofType: store.type, options: store.options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MastodonController.resetAll()
|
resetUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clearImageCaches() {
|
||||||
|
[
|
||||||
|
ImageCache.avatars,
|
||||||
|
ImageCache.headers,
|
||||||
|
ImageCache.attachments,
|
||||||
|
ImageCache.emojis,
|
||||||
|
].forEach {
|
||||||
|
try! $0.reset()
|
||||||
|
}
|
||||||
|
resetUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetUI() {
|
||||||
let mostRecent = LocalData.shared.getMostRecentAccount()!
|
let mostRecent = LocalData.shared.getMostRecentAccount()!
|
||||||
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": mostRecent])
|
NotificationCenter.default.post(name: .activateAccount, object: nil, userInfo: ["account": mostRecent])
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
/// BlurHash reference decoder implementation.
|
||||||
|
/// From https://github.com/woltapp/blurhash/blob/b23214ddcab803fe1ec9a3e6b20558caf33a23a5/Swift/BlurHashDecode.swift
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIImage {
|
||||||
|
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
|
||||||
|
guard blurHash.count >= 6 else { return nil }
|
||||||
|
|
||||||
|
let sizeFlag = String(blurHash[0]).decode83()
|
||||||
|
let numY = (sizeFlag / 9) + 1
|
||||||
|
let numX = (sizeFlag % 9) + 1
|
||||||
|
|
||||||
|
let quantisedMaximumValue = String(blurHash[1]).decode83()
|
||||||
|
let maximumValue = Float(quantisedMaximumValue + 1) / 166
|
||||||
|
|
||||||
|
guard blurHash.count == 4 + 2 * numX * numY else { return nil }
|
||||||
|
|
||||||
|
let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in
|
||||||
|
if i == 0 {
|
||||||
|
let value = String(blurHash[2 ..< 6]).decode83()
|
||||||
|
return decodeDC(value)
|
||||||
|
} else {
|
||||||
|
let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83()
|
||||||
|
return decodeAC(value, maximumValue: maximumValue * punch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = Int(size.width)
|
||||||
|
let height = Int(size.height)
|
||||||
|
let bytesPerRow = width * 3
|
||||||
|
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
|
||||||
|
CFDataSetLength(data, bytesPerRow * height)
|
||||||
|
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }
|
||||||
|
|
||||||
|
for y in 0 ..< height {
|
||||||
|
for x in 0 ..< width {
|
||||||
|
var r: Float = 0
|
||||||
|
var g: Float = 0
|
||||||
|
var b: Float = 0
|
||||||
|
|
||||||
|
for j in 0 ..< numY {
|
||||||
|
for i in 0 ..< numX {
|
||||||
|
let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height))
|
||||||
|
let colour = colours[i + j * numX]
|
||||||
|
r += colour.0 * basis
|
||||||
|
g += colour.1 * basis
|
||||||
|
b += colour.2 * basis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let intR = UInt8(linearTosRGB(r))
|
||||||
|
let intG = UInt8(linearTosRGB(g))
|
||||||
|
let intB = UInt8(linearTosRGB(b))
|
||||||
|
|
||||||
|
pixels[3 * x + 0 + y * bytesPerRow] = intR
|
||||||
|
pixels[3 * x + 1 + y * bytesPerRow] = intG
|
||||||
|
pixels[3 * x + 2 + y * bytesPerRow] = intB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
|
||||||
|
|
||||||
|
guard let provider = CGDataProvider(data: data) else { return nil }
|
||||||
|
guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
|
||||||
|
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil }
|
||||||
|
|
||||||
|
self.init(cgImage: cgImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodeDC(_ value: Int) -> (Float, Float, Float) {
|
||||||
|
let intR = value >> 16
|
||||||
|
let intG = (value >> 8) & 255
|
||||||
|
let intB = value & 255
|
||||||
|
return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
|
||||||
|
let quantR = value / (19 * 19)
|
||||||
|
let quantG = (value / 19) % 19
|
||||||
|
let quantB = value % 19
|
||||||
|
|
||||||
|
let rgb = (
|
||||||
|
signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
|
||||||
|
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
|
||||||
|
signPow((Float(quantB) - 9) / 9, 2) * maximumValue
|
||||||
|
)
|
||||||
|
|
||||||
|
return rgb
|
||||||
|
}
|
||||||
|
|
||||||
|
private func signPow(_ value: Float, _ exp: Float) -> Float {
|
||||||
|
return copysign(pow(abs(value), exp), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func linearTosRGB(_ value: Float) -> Int {
|
||||||
|
let v = max(0, min(1, value))
|
||||||
|
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) }
|
||||||
|
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
|
||||||
|
let v = Float(Int64(value)) / 255
|
||||||
|
if v <= 0.04045 { return v / 12.92 }
|
||||||
|
else { return pow((v + 0.055) / 1.055, 2.4) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private let encodeCharacters: [String] = {
|
||||||
|
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let decodeCharacters: [String: Int] = {
|
||||||
|
var dict: [String: Int] = [:]
|
||||||
|
for (index, character) in encodeCharacters.enumerated() {
|
||||||
|
dict[character] = index
|
||||||
|
}
|
||||||
|
return dict
|
||||||
|
}()
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
func decode83() -> Int {
|
||||||
|
var value: Int = 0
|
||||||
|
for character in self {
|
||||||
|
if let digit = decodeCharacters[String(character)] {
|
||||||
|
value = value * 83 + digit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
subscript (offset: Int) -> Character {
|
||||||
|
return self[index(startIndex, offsetBy: offset)]
|
||||||
|
}
|
||||||
|
|
||||||
|
subscript (bounds: CountableClosedRange<Int>) -> Substring {
|
||||||
|
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||||
|
let end = index(startIndex, offsetBy: bounds.upperBound)
|
||||||
|
return self[start...end]
|
||||||
|
}
|
||||||
|
|
||||||
|
subscript (bounds: CountableRange<Int>) -> Substring {
|
||||||
|
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||||
|
let end = index(startIndex, offsetBy: bounds.upperBound)
|
||||||
|
return self[start..<end]
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
|
|
||||||
var attachment: Attachment!
|
var attachment: Attachment!
|
||||||
var index: Int!
|
var index: Int!
|
||||||
|
var expectedSize: CGSize!
|
||||||
|
|
||||||
private var attachmentRequest: ImageCache.Request?
|
private var attachmentRequest: ImageCache.Request?
|
||||||
|
|
||||||
|
@ -35,12 +36,13 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
|
|
||||||
public lazy var animator: Animator? = Animator(withDelegate: self)
|
public lazy var animator: Animator? = Animator(withDelegate: self)
|
||||||
|
|
||||||
init(attachment: Attachment, index: Int) {
|
init(attachment: Attachment, index: Int, expectedSize: CGSize) {
|
||||||
super.init(image: nil)
|
super.init(image: nil)
|
||||||
commonInit()
|
commonInit()
|
||||||
|
|
||||||
self.attachment = attachment
|
self.attachment = attachment
|
||||||
self.index = index
|
self.index = index
|
||||||
|
self.expectedSize = expectedSize
|
||||||
loadAttachment()
|
loadAttachment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +92,31 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
guard AttachmentsContainerView.supportedAttachmentTypes.contains(attachment.kind) else {
|
guard AttachmentsContainerView.supportedAttachmentTypes.contains(attachment.kind) else {
|
||||||
preconditionFailure("invalid attachment type")
|
preconditionFailure("invalid attachment type")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let hash = attachment.blurHash {
|
||||||
|
DispatchQueue.global(qos: .default).async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let size: CGSize
|
||||||
|
if let meta = self.attachment.meta,
|
||||||
|
let width = meta.width, let height = meta.height {
|
||||||
|
size = CGSize(width: width, height: height)
|
||||||
|
} else if let orig = self.attachment.meta?.original,
|
||||||
|
let width = orig.width, let height = orig.height {
|
||||||
|
size = CGSize(width: width, height: height)
|
||||||
|
} else {
|
||||||
|
size = self.expectedSize
|
||||||
|
}
|
||||||
|
if let preview = UIImage(blurHash: hash, size: size) {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if self.image == nil {
|
||||||
|
self.image = preview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch attachment.kind {
|
switch attachment.kind {
|
||||||
case .image:
|
case .image:
|
||||||
loadImage()
|
loadImage()
|
||||||
|
|
|
@ -65,18 +65,18 @@ class AttachmentsContainerView: UIView {
|
||||||
|
|
||||||
switch attachments.count {
|
switch attachments.count {
|
||||||
case 1:
|
case 1:
|
||||||
let attachmentView = createAttachmentView(index: 0)
|
let attachmentView = createAttachmentView(index: 0, hSize: .full, vSize: .full)
|
||||||
attachmentView.layer.cornerRadius = 5
|
attachmentView.layer.cornerRadius = 5
|
||||||
attachmentView.layer.masksToBounds = true
|
attachmentView.layer.masksToBounds = true
|
||||||
fillView(attachmentView)
|
fillView(attachmentView)
|
||||||
sendSubviewToBack(attachmentView)
|
sendSubviewToBack(attachmentView)
|
||||||
accessibilityElements.append(attachmentView)
|
accessibilityElements.append(attachmentView)
|
||||||
case 2:
|
case 2:
|
||||||
let left = createAttachmentView(index: 0)
|
let left = createAttachmentView(index: 0, hSize: .half, vSize: .full)
|
||||||
left.layer.cornerRadius = 5
|
left.layer.cornerRadius = 5
|
||||||
left.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
left.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
||||||
left.layer.masksToBounds = true
|
left.layer.masksToBounds = true
|
||||||
let right = createAttachmentView(index: 1)
|
let right = createAttachmentView(index: 1, hSize: .half, vSize: .full)
|
||||||
right.layer.cornerRadius = 5
|
right.layer.cornerRadius = 5
|
||||||
right.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
right.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
||||||
right.layer.masksToBounds = true
|
right.layer.masksToBounds = true
|
||||||
|
@ -92,15 +92,15 @@ class AttachmentsContainerView: UIView {
|
||||||
accessibilityElements.append(left)
|
accessibilityElements.append(left)
|
||||||
accessibilityElements.append(right)
|
accessibilityElements.append(right)
|
||||||
case 3:
|
case 3:
|
||||||
let left = createAttachmentView(index: 0)
|
let left = createAttachmentView(index: 0, hSize: .half, vSize: .full)
|
||||||
left.layer.cornerRadius = 5
|
left.layer.cornerRadius = 5
|
||||||
left.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
left.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
||||||
left.layer.masksToBounds = true
|
left.layer.masksToBounds = true
|
||||||
let topRight = createAttachmentView(index: 1)
|
let topRight = createAttachmentView(index: 1, hSize: .half, vSize: .half)
|
||||||
topRight.layer.cornerRadius = 5
|
topRight.layer.cornerRadius = 5
|
||||||
topRight.layer.maskedCorners = .layerMaxXMinYCorner
|
topRight.layer.maskedCorners = .layerMaxXMinYCorner
|
||||||
topRight.layer.masksToBounds = true
|
topRight.layer.masksToBounds = true
|
||||||
let bottomRight = createAttachmentView(index: 2)
|
let bottomRight = createAttachmentView(index: 2, hSize: .half, vSize: .half)
|
||||||
bottomRight.layer.cornerRadius = 5
|
bottomRight.layer.cornerRadius = 5
|
||||||
bottomRight.layer.maskedCorners = .layerMaxXMaxYCorner
|
bottomRight.layer.maskedCorners = .layerMaxXMaxYCorner
|
||||||
bottomRight.layer.masksToBounds = true
|
bottomRight.layer.masksToBounds = true
|
||||||
|
@ -121,11 +121,11 @@ class AttachmentsContainerView: UIView {
|
||||||
accessibilityElements.append(topRight)
|
accessibilityElements.append(topRight)
|
||||||
accessibilityElements.append(bottomRight)
|
accessibilityElements.append(bottomRight)
|
||||||
case 4:
|
case 4:
|
||||||
let topLeft = createAttachmentView(index: 0)
|
let topLeft = createAttachmentView(index: 0, hSize: .half, vSize: .half)
|
||||||
topLeft.layer.cornerRadius = 5
|
topLeft.layer.cornerRadius = 5
|
||||||
topLeft.layer.maskedCorners = .layerMinXMinYCorner
|
topLeft.layer.maskedCorners = .layerMinXMinYCorner
|
||||||
topLeft.layer.masksToBounds = true
|
topLeft.layer.masksToBounds = true
|
||||||
let bottomLeft = createAttachmentView(index: 2)
|
let bottomLeft = createAttachmentView(index: 2, hSize: .half, vSize: .half)
|
||||||
bottomLeft.layer.cornerRadius = 5
|
bottomLeft.layer.cornerRadius = 5
|
||||||
bottomLeft.layer.maskedCorners = .layerMinXMaxYCorner
|
bottomLeft.layer.maskedCorners = .layerMinXMaxYCorner
|
||||||
bottomLeft.layer.masksToBounds = true
|
bottomLeft.layer.masksToBounds = true
|
||||||
|
@ -133,11 +133,11 @@ class AttachmentsContainerView: UIView {
|
||||||
topLeft,
|
topLeft,
|
||||||
bottomLeft
|
bottomLeft
|
||||||
])
|
])
|
||||||
let topRight = createAttachmentView(index: 1)
|
let topRight = createAttachmentView(index: 1, hSize: .half, vSize: .half)
|
||||||
topRight.layer.cornerRadius = 5
|
topRight.layer.cornerRadius = 5
|
||||||
topRight.layer.maskedCorners = .layerMaxXMinYCorner
|
topRight.layer.maskedCorners = .layerMaxXMinYCorner
|
||||||
topRight.layer.masksToBounds = true
|
topRight.layer.masksToBounds = true
|
||||||
let bottomRight = createAttachmentView(index: 3)
|
let bottomRight = createAttachmentView(index: 3, hSize: .half, vSize: .half)
|
||||||
bottomRight.layer.cornerRadius = 5
|
bottomRight.layer.cornerRadius = 5
|
||||||
bottomRight.layer.maskedCorners = .layerMaxXMaxYCorner
|
bottomRight.layer.maskedCorners = .layerMaxXMaxYCorner
|
||||||
bottomRight.layer.masksToBounds = true
|
bottomRight.layer.masksToBounds = true
|
||||||
|
@ -177,11 +177,11 @@ class AttachmentsContainerView: UIView {
|
||||||
moreView.addSubview(moreLabel)
|
moreView.addSubview(moreLabel)
|
||||||
moreView.accessibilityLabel = moreLabel.text
|
moreView.accessibilityLabel = moreLabel.text
|
||||||
|
|
||||||
let topLeft = createAttachmentView(index: 0)
|
let topLeft = createAttachmentView(index: 0, hSize: .half, vSize: .half)
|
||||||
topLeft.layer.cornerRadius = 5
|
topLeft.layer.cornerRadius = 5
|
||||||
topLeft.layer.maskedCorners = .layerMinXMinYCorner
|
topLeft.layer.maskedCorners = .layerMinXMinYCorner
|
||||||
topLeft.layer.masksToBounds = true
|
topLeft.layer.masksToBounds = true
|
||||||
let bottomLeft = createAttachmentView(index: 2)
|
let bottomLeft = createAttachmentView(index: 2, hSize: .half, vSize: .half)
|
||||||
bottomLeft.layer.cornerRadius = 5
|
bottomLeft.layer.cornerRadius = 5
|
||||||
bottomLeft.layer.maskedCorners = .layerMinXMaxYCorner
|
bottomLeft.layer.maskedCorners = .layerMinXMaxYCorner
|
||||||
bottomLeft.layer.masksToBounds = true
|
bottomLeft.layer.masksToBounds = true
|
||||||
|
@ -189,7 +189,7 @@ class AttachmentsContainerView: UIView {
|
||||||
topLeft,
|
topLeft,
|
||||||
bottomLeft
|
bottomLeft
|
||||||
])
|
])
|
||||||
let topRight = createAttachmentView(index: 1)
|
let topRight = createAttachmentView(index: 1, hSize: .half, vSize: .half)
|
||||||
topRight.layer.cornerRadius = 5
|
topRight.layer.cornerRadius = 5
|
||||||
topRight.layer.maskedCorners = .layerMaxXMinYCorner
|
topRight.layer.maskedCorners = .layerMaxXMinYCorner
|
||||||
topRight.layer.masksToBounds = true
|
topRight.layer.masksToBounds = true
|
||||||
|
@ -225,8 +225,24 @@ class AttachmentsContainerView: UIView {
|
||||||
contentHidden = Preferences.shared.blurAllMedia || status.sensitive
|
contentHidden = Preferences.shared.blurAllMedia || status.sensitive
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createAttachmentView(index: Int) -> AttachmentView {
|
private func createAttachmentView(index: Int, hSize: RelativeSize, vSize: RelativeSize) -> AttachmentView {
|
||||||
let attachmentView = AttachmentView(attachment: attachments[index], index: index)
|
let width: CGFloat
|
||||||
|
switch hSize {
|
||||||
|
case .full:
|
||||||
|
width = bounds.width
|
||||||
|
case .half:
|
||||||
|
width = (bounds.width - 4) / 2
|
||||||
|
}
|
||||||
|
let height: CGFloat
|
||||||
|
switch vSize {
|
||||||
|
case .full:
|
||||||
|
height = bounds.height
|
||||||
|
case .half:
|
||||||
|
height = (bounds.height - 4) / 2
|
||||||
|
}
|
||||||
|
let size = CGSize(width: width, height: height)
|
||||||
|
|
||||||
|
let attachmentView = AttachmentView(attachment: attachments[index], index: index, expectedSize: size)
|
||||||
attachmentView.delegate = delegate
|
attachmentView.delegate = delegate
|
||||||
attachmentView.translatesAutoresizingMaskIntoConstraints = false
|
attachmentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
attachmentView.isAccessibilityElement = true
|
attachmentView.isAccessibilityElement = true
|
||||||
|
@ -376,20 +392,20 @@ class AttachmentsContainerView: UIView {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate extension UIView {
|
fileprivate enum RelativeSize {
|
||||||
enum RelativeSize {
|
case full, half
|
||||||
case full, half
|
|
||||||
|
|
||||||
var multiplier: CGFloat {
|
var multiplier: CGFloat {
|
||||||
switch self {
|
switch self {
|
||||||
case .full:
|
case .full:
|
||||||
return 1
|
return 1
|
||||||
case .half:
|
case .half:
|
||||||
return 0.5
|
return 0.5
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension UIView {
|
||||||
func halfWidth(spacing: CGFloat = 4) -> NSLayoutConstraint {
|
func halfWidth(spacing: CGFloat = 4) -> NSLayoutConstraint {
|
||||||
return widthAnchor.constraint(equalTo: superview!.widthAnchor, multiplier: 0.5, constant: -spacing / 2)
|
return widthAnchor.constraint(equalTo: superview!.widthAnchor, multiplier: 0.5, constant: -spacing / 2)
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,36 +104,42 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
updateTimestampWorkItem = nil
|
updateTimestampWorkItem = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addLabel(_ text: String) {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.font = .boldSystemFont(ofSize: 17)
|
||||||
|
label.text = text
|
||||||
|
self.stackView.addArrangedSubview(label)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@IBAction func rejectButtonPressed() {
|
@IBAction func rejectButtonPressed() {
|
||||||
|
acceptButton.isEnabled = false
|
||||||
|
rejectButton.isEnabled = false
|
||||||
|
|
||||||
let request = Account.rejectFollowRequest(account)
|
let request = Account.rejectFollowRequest(account)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
guard case .success(_, _) = response else { fatalError() }
|
guard case .success(_, _) = response else { fatalError() }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||||
self.actionButtonsStackView.isHidden = true
|
self.actionButtonsStackView.isHidden = true
|
||||||
let label = UILabel()
|
self.addLabel(NSLocalizedString("Rejected", comment: "rejected follow request label"))
|
||||||
label.textAlignment = .center
|
|
||||||
label.font = .boldSystemFont(ofSize: 17)
|
|
||||||
label.text = NSLocalizedString("Rejected", comment: "rejected follow request label")
|
|
||||||
self.stackView.addArrangedSubview(label)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func acceptButtonPressed() {
|
@IBAction func acceptButtonPressed() {
|
||||||
|
acceptButton.isEnabled = false
|
||||||
|
rejectButton.isEnabled = false
|
||||||
|
|
||||||
let request = Account.authorizeFollowRequest(account)
|
let request = Account.authorizeFollowRequest(account)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
guard case .success(_, _) = response else { fatalError() }
|
guard case .success(_, _) = response else { fatalError() }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||||
self.actionButtonsStackView.isHidden = true
|
self.actionButtonsStackView.isHidden = true
|
||||||
let label = UILabel()
|
self.addLabel(NSLocalizedString("Accepted", comment: "accepted follow request label"))
|
||||||
label.textAlignment = .center
|
|
||||||
label.font = .boldSystemFont(ofSize: 17)
|
|
||||||
label.text = NSLocalizedString("Accepted", comment: "accepted follow request label")
|
|
||||||
self.stackView.addArrangedSubview(label)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
|
@ -30,12 +32,12 @@
|
||||||
</constraints>
|
</constraints>
|
||||||
</imageView>
|
</imageView>
|
||||||
<view contentMode="scaleToFill" horizontalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="9WN-Ql-DDL">
|
<view contentMode="scaleToFill" horizontalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="9WN-Ql-DDL">
|
||||||
<rect key="frame" x="30" y="0.0" width="175.5" height="30"/>
|
<rect key="frame" x="30" y="0.0" width="176" height="30"/>
|
||||||
</view>
|
</view>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="2m" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Saq-P5-oVH">
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="2m" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Saq-P5-oVH">
|
||||||
<rect key="frame" x="205.5" y="0.0" width="24.5" height="30"/>
|
<rect key="frame" x="206" y="0.0" width="24" height="30"/>
|
||||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="17"/>
|
<fontDescription key="fontDescription" type="system" weight="light" pointSize="17"/>
|
||||||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
</subviews>
|
</subviews>
|
||||||
|
@ -49,19 +51,20 @@
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="20F-2n-eQx">
|
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="20F-2n-eQx">
|
||||||
<rect key="frame" x="0.0" y="58.5" width="230" height="26.5"/>
|
<rect key="frame" x="0.0" y="58.5" width="230" height="26.5"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CMQ-TI-X9k">
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="CMQ-TI-X9k">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="115" height="26.5"/>
|
<rect key="frame" x="0.0" y="0.0" width="115" height="26.5"/>
|
||||||
<state key="normal" image="checkmark.circle.fill" catalog="system">
|
<accessibility key="accessibilityConfiguration" label="Accept Request"/>
|
||||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="22"/>
|
<state key="normal" title=" Accept" image="checkmark.circle.fill" catalog="system">
|
||||||
|
<color key="titleColor" systemColor="systemBlueColor"/>
|
||||||
</state>
|
</state>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="acceptButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="hGw-3d-RNi"/>
|
<action selector="acceptButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="hGw-3d-RNi"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7MW-rY-m5l">
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7MW-rY-m5l">
|
||||||
<rect key="frame" x="115" y="0.0" width="115" height="26.5"/>
|
<rect key="frame" x="115" y="0.0" width="115" height="26.5"/>
|
||||||
<state key="normal" image="xmark.circle.fill" catalog="system">
|
<state key="normal" title=" Reject" image="xmark.circle.fill" catalog="system">
|
||||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="pointSize" pointSize="22"/>
|
<color key="titleColor" systemColor="systemBlueColor"/>
|
||||||
</state>
|
</state>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="rejectButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="EP6-Bg-3nC"/>
|
<action selector="rejectButtonPressed" destination="Pcu-ap-Xqf" eventType="touchUpInside" id="EP6-Bg-3nC"/>
|
||||||
|
@ -105,5 +108,11 @@
|
||||||
<image name="checkmark.circle.fill" catalog="system" width="128" height="121"/>
|
<image name="checkmark.circle.fill" catalog="system" width="128" height="121"/>
|
||||||
<image name="person.fill" catalog="system" width="128" height="120"/>
|
<image name="person.fill" catalog="system" width="128" height="120"/>
|
||||||
<image name="xmark.circle.fill" catalog="system" width="128" height="121"/>
|
<image name="xmark.circle.fill" catalog="system" width="128" height="121"/>
|
||||||
|
<systemColor name="secondaryLabelColor">
|
||||||
|
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</systemColor>
|
||||||
|
<systemColor name="systemBlueColor">
|
||||||
|
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</systemColor>
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
|
Loading…
Reference in New Issue