Compare commits
3 Commits
10a3cbbe9c
...
9768097488
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 9768097488 | |
Shadowfacts | f5e9f71586 | |
Shadowfacts | 9f8b14d180 |
|
@ -1,6 +1,3 @@
|
|||
[submodule "Gifu"]
|
||||
path = Gifu
|
||||
url = git://github.com/kaishin/Gifu.git
|
||||
[submodule "Embassy"]
|
||||
path = Embassy
|
||||
url = https://github.com/envoy/Embassy.git
|
||||
|
|
1
Gifu
1
Gifu
|
@ -1 +0,0 @@
|
|||
Subproject commit 9b1a6461aa3b5f66cb0ed3a50c5523db0b4fb007
|
|
@ -13,12 +13,12 @@ public struct CharacterCounter {
|
|||
static let linkDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||
static let mention = try! NSRegularExpression(pattern: "(@[a-z0-9_]+)(?:@[a-z0-9\\-\\.]+[a-z0-9]+)?", options: .caseInsensitive)
|
||||
|
||||
public static func count(text: String) -> Int {
|
||||
public static func count(text: String, for instance: Instance? = nil) -> Int {
|
||||
let mentionsRemoved = removeMentions(in: text)
|
||||
var count = mentionsRemoved.count
|
||||
for match in linkDetector.matches(in: mentionsRemoved, options: [], range: NSRange(location: 0, length: mentionsRemoved.utf16.count)) {
|
||||
count -= match.range.length
|
||||
count += 23 // Mastodon link length
|
||||
count += instance?.configuration?.statuses.charactersReservedPerURL ?? 23 // default Mastodon link length
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
|
|
@ -274,8 +274,6 @@
|
|||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */; };
|
||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F432560390300F6E31D /* MenuController.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 */; };
|
||||
D6BC874621961F73006163F1 /* Gifu.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BC874421961F73006163F1 /* Gifu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */; };
|
||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */; };
|
||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */; };
|
||||
|
@ -309,6 +307,7 @@
|
|||
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; };
|
||||
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
||||
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
|
||||
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
|
||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
|
||||
D6DEA0DE268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DEA0DC268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift */; };
|
||||
|
@ -406,7 +405,6 @@
|
|||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
D61099C12144B0CC00432DC2 /* Pachyderm.framework in Embed Frameworks */,
|
||||
D6BC874621961F73006163F1 /* Gifu.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -681,7 +679,6 @@
|
|||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableViewController.swift; sourceTree = "<group>"; };
|
||||
D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.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; };
|
||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedTableViewController.swift; sourceTree = "<group>"; };
|
||||
D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPageViewController.swift; sourceTree = "<group>"; };
|
||||
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellnessPrefsView.swift; sourceTree = "<group>"; };
|
||||
|
@ -721,6 +718,7 @@
|
|||
D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerUITests.swift; sourceTree = "<group>"; };
|
||||
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
|
||||
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
|
||||
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
||||
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
|
||||
D6DEA0DC268400C300FE896A /* ConfirmLoadMoreTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreTableViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -780,7 +778,6 @@
|
|||
D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */,
|
||||
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */,
|
||||
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
|
||||
D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */,
|
||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -1480,6 +1477,7 @@
|
|||
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
||||
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */,
|
||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
||||
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */,
|
||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
|
||||
D620483323D3801D008A63EF /* LinkTextView.swift */,
|
||||
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
|
||||
|
@ -1546,7 +1544,6 @@
|
|||
D6D4DDC3212518A000E1C4BB = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6BC874421961F73006163F1 /* Gifu.framework */,
|
||||
D61099AC2144B0CC00432DC2 /* Pachyderm */,
|
||||
D61099B92144B0CC00432DC2 /* PachydermTests */,
|
||||
D6D4DDCE212518A000E1C4BB /* Tusker */,
|
||||
|
@ -2191,6 +2188,7 @@
|
|||
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
||||
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */,
|
||||
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */,
|
||||
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */,
|
||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
||||
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
|
||||
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
|
||||
|
|
|
@ -7,9 +7,6 @@
|
|||
<FileRef
|
||||
location = "group:BlankSlate.xcappdata">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Gifu/Gifu.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Embassy/Embassy.xcodeproj">
|
||||
</FileRef>
|
||||
|
|
|
@ -17,6 +17,7 @@ struct ImageGrayscalifier {
|
|||
static func convertIfNecessary(url: URL?, image: UIImage) -> UIImage? {
|
||||
if Preferences.shared.grayscaleImages,
|
||||
let source = image.cgImage {
|
||||
// todo: should this return the original image if conversion fails?
|
||||
return convert(url: url, cgImage: source)
|
||||
} else {
|
||||
return image
|
||||
|
|
|
@ -8,14 +8,15 @@
|
|||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import Gifu
|
||||
|
||||
class AttachmentPreviewViewController: UIViewController {
|
||||
|
||||
let attachment: Attachment
|
||||
private let attachment: Attachment
|
||||
private let sourceView: AttachmentView
|
||||
|
||||
init(attachment: Attachment) {
|
||||
self.attachment = attachment
|
||||
init(sourceView: AttachmentView) {
|
||||
self.attachment = sourceView.attachment
|
||||
self.sourceView = sourceView
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
@ -30,7 +31,9 @@ class AttachmentPreviewViewController: UIViewController {
|
|||
let imageView: UIImageView
|
||||
if attachment.url.pathExtension == "gif" {
|
||||
let gifView = GIFImageView(image: image)
|
||||
gifView.animate(withGIFData: data)
|
||||
let controller = sourceView.gifController ?? GIFController(gifData: data)
|
||||
controller.attach(to: gifView)
|
||||
controller.startAnimating()
|
||||
imageView = gifView
|
||||
} else {
|
||||
imageView = UIImageView(image: image)
|
||||
|
|
|
@ -41,14 +41,6 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
|||
return animationSourceView?.image
|
||||
}
|
||||
}
|
||||
var animationGifData: Data? {
|
||||
let attachment = attachments[currentIndex]
|
||||
if attachment.url.pathExtension == "gif" {
|
||||
return ImageCache.attachments.getData(attachment.url)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var dismissInteractionController: LargeImageInteractionController?
|
||||
|
||||
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||
|
|
|
@ -30,7 +30,7 @@ struct ComposeView: View {
|
|||
var charactersRemaining: Int {
|
||||
let limit = mastodonController.instance?.maxStatusCharacters ?? 500
|
||||
let cwCount = draft.contentWarningEnabled ? draft.contentWarning.count : 0
|
||||
return limit - (cwCount + CharacterCounter.count(text: draft.text))
|
||||
return limit - (cwCount + CharacterCounter.count(text: draft.text, for: mastodonController.instance))
|
||||
}
|
||||
|
||||
var requiresAttachmentDescriptions: Bool {
|
||||
|
|
|
@ -7,24 +7,18 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Gifu
|
||||
import Pachyderm
|
||||
import AVFoundation
|
||||
|
||||
protocol LargeImageContentView: UIView {
|
||||
var animationImage: UIImage? { get }
|
||||
var animationGifData: Data? { get }
|
||||
var activityItemsForSharing: [Any] { get }
|
||||
func grayscaleStateChanged()
|
||||
}
|
||||
|
||||
class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentView {
|
||||
lazy var animator: Animator? = {
|
||||
return Animator(withDelegate: self)
|
||||
}()
|
||||
class LargeImageImageContentView: GIFImageView, LargeImageContentView {
|
||||
|
||||
var animationImage: UIImage? { image! }
|
||||
let animationGifData: Data?
|
||||
|
||||
var activityItemsForSharing: [Any] {
|
||||
[image!]
|
||||
|
@ -32,34 +26,18 @@ class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentV
|
|||
|
||||
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?) {
|
||||
self.animationGifData = gifData
|
||||
|
||||
init(image: UIImage) {
|
||||
super.init(image: image)
|
||||
|
||||
contentMode = .scaleAspectFit
|
||||
|
||||
if let data = gifData {
|
||||
self.animate(withGIFData: data)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func display(_ layer: CALayer) {
|
||||
super.display(layer)
|
||||
|
||||
updateImageIfNeeded()
|
||||
}
|
||||
|
||||
func grayscaleStateChanged() {
|
||||
guard let data = sourceData else {
|
||||
return
|
||||
|
@ -78,9 +56,35 @@ class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentV
|
|||
}
|
||||
}
|
||||
|
||||
class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
||||
var animationImage: UIImage? { image }
|
||||
|
||||
var activityItemsForSharing: [Any] {
|
||||
// todo: should gifs share the data?
|
||||
[image].compactMap { $0 }
|
||||
}
|
||||
|
||||
init(gifController: GIFController) {
|
||||
super.init(image: gifController.lastFrame?.image)
|
||||
|
||||
contentMode = .scaleAspectFit
|
||||
|
||||
gifController.attach(to: self)
|
||||
// todo: doing this in the init feels wrong
|
||||
gifController.startAnimating()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func grayscaleStateChanged() {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
|
||||
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
||||
private(set) var animationImage: UIImage?
|
||||
var animationGifData: Data? { nil }
|
||||
var activityItemsForSharing: [Any] {
|
||||
// todo: what should we share for gifvs?
|
||||
// some SO posts indicate that just sharing a URL to the video should work, but that may need to be a local URL?
|
||||
|
|
|
@ -13,7 +13,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
weak var animationSourceView: UIImageView?
|
||||
var largeImageController: LargeImageViewController? { self }
|
||||
var animationImage: UIImage? { contentView.animationImage }
|
||||
var animationGifData: Data? { contentView.animationGifData }
|
||||
var dismissInteractionController: LargeImageInteractionController?
|
||||
|
||||
@IBOutlet weak var scrollView: UIScrollView!
|
||||
|
|
|
@ -40,7 +40,6 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
weak var animationSourceView: UIImageView?
|
||||
var largeImageController: LargeImageViewController? { largeImageVC }
|
||||
var animationImage: UIImage? { largeImageVC?.animationImage ?? animationSourceView?.image }
|
||||
var animationGifData: Data? { largeImageVC?.animationGifData }
|
||||
var dismissInteractionController: LargeImageInteractionController?
|
||||
|
||||
var isInteractivelyAnimatingDismissal: Bool = false {
|
||||
|
@ -93,6 +92,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
|
||||
// always load full resolution from disk for large image, in case the cache is scaled
|
||||
if let entry = cache.get(url, loadOriginal: true) {
|
||||
// todo: if load original is true, is there any way entry.data could be nil?
|
||||
// feels like the data param of createLargeImage shouldn't be optional
|
||||
createLargeImage(data: entry.data, image: entry.image, url: url)
|
||||
} else {
|
||||
createPreview()
|
||||
|
@ -126,19 +127,31 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
guard !loaded else { return }
|
||||
loaded = true
|
||||
|
||||
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
||||
let gifData = url.pathExtension == "gif" ? data : nil
|
||||
createLargeImage(image: transformedImage, gifData: gifData)
|
||||
}
|
||||
}
|
||||
let content: LargeImageContentView
|
||||
|
||||
private func createLargeImage(image: UIImage, gifData: Data?) {
|
||||
let imageView = LargeImageImageContentView(image: image, gifData: gifData)
|
||||
|
||||
if let existing = largeImageVC {
|
||||
existing.contentView = imageView
|
||||
// todo: p sure grayscaling gifs has never worked
|
||||
if url.pathExtension == "gif", let data = data {
|
||||
// todo: pulling the gif controller out of the source view feels icky
|
||||
// is it possible for the source view's gif controller to have different data than we just got?
|
||||
// should this be a property set by the animation controller instead?
|
||||
let gifController = (animationSourceView as? GIFImageView)?.gifController ?? GIFController(gifData: data)
|
||||
content = LargeImageGifContentView(gifController: gifController)
|
||||
} else {
|
||||
largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView)
|
||||
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
||||
content = LargeImageImageContentView(image: transformedImage)
|
||||
} else {
|
||||
content = LargeImageImageContentView(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
setContent(content)
|
||||
}
|
||||
|
||||
private func setContent(_ content: LargeImageContentView) {
|
||||
if let existing = largeImageVC {
|
||||
existing.contentView = content
|
||||
} else {
|
||||
largeImageVC = LargeImageViewController(contentView: content, description: imageDescription, sourceView: animationSourceView)
|
||||
largeImageVC!.initialControlsVisible = initialControlsVisible
|
||||
largeImageVC!.shrinkGestureEnabled = false
|
||||
embedChild(largeImageVC!)
|
||||
|
@ -154,7 +167,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: source) {
|
||||
image = grayscale
|
||||
}
|
||||
self.createLargeImage(image: image, gifData: nil)
|
||||
setContent(LargeImageImageContentView(image: image))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,13 +7,11 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Gifu
|
||||
|
||||
protocol LargeImageAnimatableViewController: UIViewController {
|
||||
var animationSourceView: UIImageView? { get }
|
||||
var largeImageController: LargeImageViewController? { get }
|
||||
var animationImage: UIImage? { get }
|
||||
var animationGifData: Data? { get }
|
||||
var dismissInteractionController: LargeImageInteractionController? { get }
|
||||
var isInteractivelyAnimatingDismissal: Bool { get set }
|
||||
}
|
||||
|
@ -87,8 +85,8 @@ class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTra
|
|||
|
||||
let imageView = GIFImageView(frame: sourceFrame)
|
||||
imageView.image = image
|
||||
if let gifData = toVC.animationGifData {
|
||||
imageView.animate(withGIFData: gifData)
|
||||
if let gifController = (sourceView as? GIFImageView)?.gifController {
|
||||
gifController.attach(to: imageView)
|
||||
}
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.layer.cornerRadius = sourceView.layer.cornerRadius
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Gifu
|
||||
|
||||
class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
|
||||
|
@ -57,8 +56,10 @@ class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTra
|
|||
|
||||
let imageView = GIFImageView(frame: originalFrame)
|
||||
imageView.image = image
|
||||
if let gifData = fromVC.animationGifData {
|
||||
imageView.animate(withGIFData: gifData)
|
||||
if let gifController = (sourceView as? GIFImageView)?.gifController {
|
||||
gifController.attach(to: imageView)
|
||||
// todo: this might not be necessary, the large image content view should have started it
|
||||
gifController.startAnimating()
|
||||
}
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.layer.cornerRadius = 0
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import Gifu
|
||||
import AVFoundation
|
||||
|
||||
protocol AttachmentViewDelegate: AnyObject {
|
||||
|
@ -16,7 +15,7 @@ protocol AttachmentViewDelegate: AnyObject {
|
|||
func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
|
||||
}
|
||||
|
||||
class AttachmentView: UIImageView, GIFAnimatable {
|
||||
class AttachmentView: GIFImageView {
|
||||
|
||||
weak var delegate: AttachmentViewDelegate?
|
||||
|
||||
|
@ -30,22 +29,12 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
|||
private var attachmentRequest: ImageCache.Request?
|
||||
private var source: Source?
|
||||
|
||||
var gifData: Data? {
|
||||
switch source {
|
||||
case let .gifData(_, data):
|
||||
return data
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
private var autoplayGifs: Bool {
|
||||
Preferences.shared.automaticallyPlayGifs && !ProcessInfo.processInfo.isLowPowerModeEnabled
|
||||
}
|
||||
|
||||
private var isGrayscale = false
|
||||
|
||||
public lazy var animator: Animator? = Animator(withDelegate: self)
|
||||
|
||||
init(attachment: Attachment, index: Int, expectedSize: CGSize) {
|
||||
super.init(image: nil)
|
||||
commonInit()
|
||||
|
@ -94,11 +83,13 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
|||
// NSProcessInfoPowerStateDidChange is sometimes fired on a background thread
|
||||
DispatchQueue.main.async {
|
||||
if self.attachment.kind == .image,
|
||||
let gifData = self.gifData {
|
||||
let gifController = self.gifController {
|
||||
if self.autoplayGifs && !self.isAnimatingGIF {
|
||||
self.animate(withGIFData: gifData)
|
||||
gifController.attach(to: self)
|
||||
gifController.startAnimating()
|
||||
} else if !self.autoplayGifs && self.isAnimatingGIF {
|
||||
self.stopAnimatingGIF()
|
||||
// detach instead of stopping so that any other attached gif views keep animating
|
||||
self.detachGIFController()
|
||||
}
|
||||
} else if self.attachment.kind == .gifv,
|
||||
let gifvView = self.gifvView {
|
||||
|
@ -169,13 +160,18 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
|||
}
|
||||
if self.attachment.url.pathExtension == "gif" {
|
||||
self.source = .gifData(attachmentURL, data)
|
||||
if self.autoplayGifs {
|
||||
let controller = GIFController(gifData: data)
|
||||
DispatchQueue.main.async {
|
||||
self.animate(withGIFData: data)
|
||||
controller.attach(to: self)
|
||||
if self.autoplayGifs {
|
||||
controller.startAnimating()
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if !self.autoplayGifs {
|
||||
self.displayImage()
|
||||
}
|
||||
|
||||
} else {
|
||||
self.source = .imageData(attachmentURL, data)
|
||||
self.displayImage()
|
||||
|
@ -293,12 +289,6 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
|||
}
|
||||
}
|
||||
|
||||
override func display(_ layer: CALayer) {
|
||||
super.display(layer)
|
||||
|
||||
updateImageIfNeeded()
|
||||
}
|
||||
|
||||
func showGallery() {
|
||||
if let delegate = delegate,
|
||||
let gallery = delegate.attachmentViewGallery(startingAt: index) {
|
||||
|
@ -331,7 +321,7 @@ extension AttachmentView: UIContextMenuInteractionDelegate {
|
|||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
|
||||
if self.attachment.kind == .image {
|
||||
return AttachmentPreviewViewController(attachment: self.attachment)
|
||||
return AttachmentPreviewViewController(sourceView: self)
|
||||
} else if self.attachment.kind == .gifv {
|
||||
let vc = GifvAttachmentViewController(attachment: self.attachment)
|
||||
vc.preferredContentSize = self.image?.size ?? .zero
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
//
|
||||
// GIFImageView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 11/11/21.
|
||||
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class GIFImageView: UIImageView {
|
||||
|
||||
fileprivate(set) var gifController: GIFController? = nil
|
||||
var isAnimatingGIF: Bool { gifController?.state == .playing }
|
||||
|
||||
/// Detaches the current GIF controller from this view.
|
||||
/// If this view is the GIF controller's only one, it will stop itself.
|
||||
func detachGIFController() {
|
||||
gifController?.detach(from: self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// A `GIFController` controls the animation of one or more `GIFImageView`s.
|
||||
class GIFController {
|
||||
|
||||
// GIFImageView strongly holds the controller so that when the last view detaches, the controller is freed
|
||||
private var imageViews = WeakArray<GIFImageView>()
|
||||
|
||||
private(set) var gifData: Data
|
||||
private(set) var state: State = .stopped
|
||||
private(set) var lastFrame: (image: UIImage, index: Int)? = nil
|
||||
|
||||
init(gifData: Data) {
|
||||
self.gifData = gifData
|
||||
}
|
||||
|
||||
/// Attaches another view to this controller, letting it play back alongside the others.
|
||||
/// Immediately brings it into sync with the others, setting the last frame if there was one.
|
||||
func attach(to view: GIFImageView) {
|
||||
imageViews.append(view)
|
||||
view.gifController = self
|
||||
|
||||
if let lastFrame = lastFrame {
|
||||
view.image = lastFrame.image
|
||||
}
|
||||
}
|
||||
|
||||
/// Detaches the given view from this controller.
|
||||
/// If no views attached views remain, the last strong reference to this controller is nilled out
|
||||
/// and image animation will stop at the next CGAnimateImageDataWithBlock callback.
|
||||
func detach(from view: GIFImageView) {
|
||||
// todo: does === work the way i want here
|
||||
imageViews.removeAll(where: { $0 === view })
|
||||
view.gifController = nil
|
||||
}
|
||||
|
||||
func startAnimating() {
|
||||
guard state.shouldStop else { return }
|
||||
|
||||
state = .playing
|
||||
|
||||
CGAnimateImageDataWithBlock(gifData as CFData, nil) { [weak self] (frameIndex, cgImage, stop) in
|
||||
guard let self = self else {
|
||||
stop.pointee = true
|
||||
return
|
||||
}
|
||||
let image = UIImage(cgImage: cgImage)
|
||||
self.lastFrame = (image, frameIndex)
|
||||
for case let .some(view) in self.imageViews {
|
||||
view.image = image
|
||||
}
|
||||
stop.pointee = self.state.shouldStop
|
||||
}
|
||||
}
|
||||
|
||||
func stopAnimating() {
|
||||
guard state == .playing else { return }
|
||||
|
||||
state = .stopping
|
||||
}
|
||||
|
||||
enum State: Equatable {
|
||||
case stopped, playing, stopping
|
||||
|
||||
var shouldStop: Bool {
|
||||
self == .stopped || self == .stopping
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue