Compare commits

...

4 Commits

13 changed files with 235 additions and 131 deletions

View File

@ -168,6 +168,7 @@
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681A299249AD62D0085E54E /* LargeImageContentView.swift */; };
D681E4D3246E2AFF0053414F /* MuteConversationActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D2246E2AFF0053414F /* MuteConversationActivity.swift */; };
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D4246E2BC30053414F /* UnmuteConversationActivity.swift */; };
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
@ -471,6 +472,7 @@
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; };
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
D681A299249AD62D0085E54E /* LargeImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageContentView.swift; sourceTree = "<group>"; };
D681E4D2246E2AFF0053414F /* MuteConversationActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteConversationActivity.swift; sourceTree = "<group>"; };
D681E4D4246E2BC30053414F /* UnmuteConversationActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnmuteConversationActivity.swift; sourceTree = "<group>"; };
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
@ -942,6 +944,7 @@
D646C954213B364600269FB5 /* Transitions */,
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */,
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */,
D681A299249AD62D0085E54E /* LargeImageContentView.swift */,
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */,
);
path = "Large Image";
@ -1770,6 +1773,7 @@
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */,
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */,
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */,

View File

@ -41,14 +41,18 @@ class Preferences: Codable, ObservableObject {
self.showRepliesInProfiles = try container.decode(Bool.self, forKey: .showRepliesInProfiles)
self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle)
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility)
self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts)
self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions)
self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode)
self.mentionReblogger = try container.decode(Bool.self, forKey: .mentionReblogger)
self.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia)
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
self.inAppSafariAutomaticReaderMode = try container.decode(Bool.self, forKey: .inAppSafariAutomaticReaderMode)
@ -67,14 +71,18 @@ class Preferences: Codable, ObservableObject {
try container.encode(showRepliesInProfiles, forKey: .showRepliesInProfiles)
try container.encode(avatarStyle, forKey: .avatarStyle)
try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts)
try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions)
try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode)
try container.encode(mentionReblogger, forKey: .mentionReblogger)
try container.encode(blurAllMedia, forKey: .blurAllMedia)
try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs)
try container.encode(openLinksInApps, forKey: .openLinksInApps)
try container.encode(useInAppSafari, forKey: .useInAppSafari)
try container.encode(inAppSafariAutomaticReaderMode, forKey: .inAppSafariAutomaticReaderMode)
@ -86,29 +94,35 @@ class Preferences: Codable, ObservableObject {
try container.encode(statusContentType, forKey: .statusContentType)
}
// MARK: - Appearance
// MARK: Appearance
@Published var theme = UIUserInterfaceStyle.unspecified
@Published var showRepliesInProfiles = false
@Published var avatarStyle = AvatarStyle.roundRect
@Published var hideCustomEmojiInUsernames = false
@Published var showIsStatusReplyIcon = false
@Published var alwaysShowStatusVisibilityIcon = false
// MARK: - Behavior
// MARK: Composing
@Published var defaultPostVisibility = Status.Visibility.public
@Published var automaticallySaveDrafts = true
@Published var requireAttachmentDescriptions = false
@Published var contentWarningCopyMode = ContentWarningCopyMode.asIs
@Published var mentionReblogger = false
// MARK: Media
@Published var blurAllMedia = false
@Published var automaticallyPlayGifs = true
// MARK: Behavior
@Published var openLinksInApps = true
@Published var useInAppSafari = true
@Published var inAppSafariAutomaticReaderMode = false
// MARK: - Digital Wellness
// MARK: Digital Wellness
@Published var showFavoriteAndReblogCounts = true
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
// MARK: - Advanced
// MARK: Advanced
@Published var silentActions: [String: Permission] = [:]
@Published var statusContentType: StatusContentType = .plain
@ -117,14 +131,18 @@ class Preferences: Codable, ObservableObject {
case showRepliesInProfiles
case avatarStyle
case hideCustomEmojiInUsernames
case showIsStatusReplyIcon
case alwaysShowStatusVisibilityIcon
case defaultPostVisibility
case automaticallySaveDrafts
case requireAttachmentDescriptions
case contentWarningCopyMode
case mentionReblogger
case blurAllMedia
case automaticallyPlayGifs
case openLinksInApps
case useInAppSafari
case inAppSafariAutomaticReaderMode

View File

@ -28,8 +28,8 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
var animationSourceView: UIImageView? { sourceViews[currentIndex] }
var animationImage: UIImage? {
if let page = pages[currentIndex] as? LoadingLargeImageViewController,
let image = page.largeImageVC?.image {
if let page = pages[currentIndex] as? LargeImageAnimatableViewController,
let image = page.animationImage {
return image
} else {
return animationSourceView?.image
@ -65,18 +65,29 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
self.sourceViews = WeakArray(sourceViews)
self.startIndex = startIndex
self.pages = attachments.map {
switch $0.kind {
self.pages = attachments.enumerated().map { (index, attachment) in
switch attachment.kind {
case .image:
let vc = LoadingLargeImageViewController(attachment: $0)
let vc = LoadingLargeImageViewController(attachment: attachment)
vc.shrinkGestureEnabled = false
return vc
case .video, .audio:
let vc = AVPlayerViewController()
vc.player = AVPlayer(url: $0.url)
vc.player = AVPlayer(url: attachment.url)
return vc
case .gifv:
return GifvAttachmentViewController(attachment: $0)
// Passing the source view to the LargeImageGifvContentView is a crappy workaround for not
// having the video size directly inside the content view. This will break when there
// are more than 4 attachments and there is a gifv at index >= 3 (the More... button will show
// in place of the fourth attachment, so there aren't source views for the attachments at index >= 3).
// Really, what should happen is the LargeImageGifvContentView should get the size of the video from
// the AVFoundation instead of the source view.
// This isn't a priority as only Mastodon converts gifs to gifvs, and Mastodon (in its default configuration,
// I don't know about forks) doesn't allow more than four attachments, meaning there will always be a source view.
let gifvContentView = LargeImageGifvContentView(attachment: attachment, source: sourceViews[index]!)
let vc = LargeImageViewController(contentView: gifvContentView, description: attachment.description, sourceView: nil)
vc.shrinkGestureEnabled = false
return vc
default:
fatalError()
}

View File

@ -0,0 +1,84 @@
//
// LargeImageContentView.swift
// Tusker
//
// Created by Shadowfacts on 6/17/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import Gifu
import Pachyderm
import AVFoundation
protocol LargeImageContentView {
var animationImage: UIImage? { get }
var animationGifData: Data? { get }
var activityItemsForSharing: [Any] { get }
}
class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentView {
lazy var animator: Animator? = {
return Animator(withDelegate: self)
}()
var animationImage: UIImage? { image! }
let animationGifData: Data?
var activityItemsForSharing: [Any] {
[image!]
}
init(image: UIImage, gifData: Data?) {
self.animationGifData = gifData
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) {
updateImageIfNeeded()
}
}
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?
[]
}
private let asset: AVURLAsset
// The content view needs to supply an intrinsicContentSize for the LargeImageViewController to handle layout/scrolling/zooming correctly
override var intrinsicContentSize: CGSize {
// This is a really sucky workaround for the fact that in the content view, we don't have access to the size of the underlying video.
// There's probably some way of getting this from the AVPlayer/AVAsset directly
animationImage?.size ?? CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
}
init(attachment: Attachment, source: UIImageView) {
precondition(attachment.kind == .gifv)
self.asset = AVURLAsset(url: attachment.url)
super.init(asset: asset, gravity: .resizeAspect)
self.animationImage = source.image
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -7,22 +7,17 @@
//
import UIKit
import Gifu
class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController {
typealias ContentView = UIView & LargeImageContentView
weak var animationSourceView: UIImageView?
var animationImage: UIImage? { image ?? animationSourceView?.image }
var animationGifData: Data? { gifData }
var animationImage: UIImage? { contentView.animationImage }
var animationGifData: Data? { contentView.animationGifData }
var dismissInteractionController: LargeImageInteractionController?
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var imageView: GIFImageView!
@IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewTrailingConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint!
@IBOutlet weak var imageViewBottomConstraint: NSLayoutConstraint!
@IBOutlet weak var topControlsView: UIView!
@IBOutlet weak var topControlsHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var shareButton: UIButton!
@ -35,8 +30,10 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
@IBOutlet weak var bottomControlsView: UIView!
@IBOutlet weak var descriptionLabel: UILabel!
var image: UIImage?
var gifData: Data?
var contentView: ContentView
var contentViewLeadingConstraint: NSLayoutConstraint!
var contentViewTopConstraint: NSLayoutConstraint!
var imageDescription: String?
var initialControlsVisible = true
@ -57,11 +54,12 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
return !controlsVisible
}
init(image: UIImage, description: String?, sourceView: UIImageView?) {
self.image = image
init(contentView: ContentView, description: String?, sourceView: UIImageView?) {
self.imageDescription = description
self.animationSourceView = sourceView
self.contentView = contentView
super.init(nibName: "LargeImageViewController", bundle: nil)
modalPresentationStyle = .fullScreen
@ -74,15 +72,19 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
override func viewDidLoad() {
super.viewDidLoad()
setControlsVisible(initialControlsVisible, animated: false)
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor)
NSLayoutConstraint.activate([
contentViewLeadingConstraint,
contentViewTopConstraint,
])
imageView.image = image
if let gifData = gifData {
imageView.animate(withGIFData: gifData)
}
setControlsVisible(initialControlsVisible, animated: false)
shareButton.isEnabled = !contentView.activityItemsForSharing.isEmpty
scrollView.delegate = self
imageView.bounds = CGRect(origin: .zero, size: imageView.image!.size)
if let imageDescription = imageDescription {
descriptionLabel.text = imageDescription
@ -100,15 +102,15 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
view.addGestureRecognizer(doubleTap)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// todo: does this need to be in viewDidLayoutSubviews?
// limit the image height to the safe area height, so the image doesn't overlap the top controls
// while zoomed all the way out
let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom
let heightScale = maxHeight / imageView.bounds.height
let widthScale = view.bounds.width / imageView.bounds.width
let heightScale = maxHeight / contentView.intrinsicContentSize.height
let widthScale = view.bounds.width / contentView.intrinsicContentSize.width
let minScale = min(widthScale, heightScale)
scrollView.minimumZoomScale = minScale
scrollView.zoomScale = minScale
@ -116,6 +118,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
centerImage()
// todo: does this need to be in viewDidLayoutSubviews?
if view.safeAreaInsets.top == 44 {
// running on iPhone X style notched device
let notchWidth: CGFloat = 209
@ -147,7 +150,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
return contentView
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
@ -163,18 +166,18 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
}
func centerImage() {
let yOffset = max(0, (view.bounds.size.height - imageView.frame.height) / 2)
imageViewTopConstraint.constant = yOffset
let yOffset = max(0, (view.bounds.size.height - contentView.frame.height) / 2)
contentViewTopConstraint.constant = yOffset
let xOffset = max(0, (view.bounds.size.width - imageView.frame.width) / 2)
imageViewLeadingConstraint.constant = xOffset
let xOffset = max(0, (view.bounds.size.width - contentView.frame.width) / 2)
contentViewLeadingConstraint.constant = xOffset
}
func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect {
var zoomRect = CGRect.zero
zoomRect.size.width = imageView.frame.width / scale
zoomRect.size.height = imageView.frame.height / scale
let newCenter = scrollView.convert(center, to: imageView)
zoomRect.size.width = contentView.frame.width / scale
zoomRect.size.height = contentView.frame.height / scale
let newCenter = scrollView.convert(center, to: contentView)
zoomRect.origin.x = newCenter.x - (zoomRect.width / 2)
zoomRect.origin.y = newCenter.y - (zoomRect.height / 2)
return zoomRect
@ -225,11 +228,8 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
}
@IBAction func sharePressed(_ sender: Any) {
guard let image = image else { return }
let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil)
if let presentationController = activityVC.presentationController as? UIPopoverPresentationController {
presentationController.sourceView = shareButton
}
let activityVC = UIActivityViewController(activityItems: contentView.activityItemsForSharing, applicationActivities: nil)
activityVC.popoverPresentationController?.sourceView = shareButton
present(activityVC, animated: true)
}

View File

@ -14,9 +14,6 @@
<outlet property="closeButtonTopConstraint" destination="ImD-2H-0XK" id="DUe-b1-a2N"/>
<outlet property="closeButtonTrailingConstraint" destination="JFe-ig-3Ic" id="cWO-Rr-y3F"/>
<outlet property="descriptionLabel" destination="eo5-fc-RV8" id="vrW-RJ-y5k"/>
<outlet property="imageView" destination="qcn-1t-3sS" id="Q01-G2-y1c"/>
<outlet property="imageViewLeadingConstraint" destination="bI3-V8-M70" id="nIe-xI-E9u"/>
<outlet property="imageViewTopConstraint" destination="tfL-hp-2I2" id="EDV-RO-pTe"/>
<outlet property="scrollView" destination="Skj-xq-AgQ" id="TFb-zF-m1b"/>
<outlet property="shareButton" destination="vhp-0u-Q0S" id="JZS-K9-4w9"/>
<outlet property="shareButtonLeadingConstraint" destination="MJx-2r-p0k" id="Dn5-Eg-Pid"/>
@ -31,19 +28,9 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" ambiguous="YES" minimumZoomScale="0.25" maximumZoomScale="2" translatesAutoresizingMaskIntoConstraints="NO" id="Skj-xq-AgQ">
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" minimumZoomScale="0.25" maximumZoomScale="2" translatesAutoresizingMaskIntoConstraints="NO" id="Skj-xq-AgQ">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews>
<imageView contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qcn-1t-3sS" customClass="GIFImageView" customModule="Gifu">
<rect key="frame" x="0.0" y="-10" width="375" height="647"/>
<gestureRecognizers/>
</imageView>
</subviews>
<gestureRecognizers/>
<constraints>
<constraint firstItem="qcn-1t-3sS" firstAttribute="leading" secondItem="Skj-xq-AgQ" secondAttribute="leading" id="bI3-V8-M70"/>
<constraint firstItem="qcn-1t-3sS" firstAttribute="top" secondItem="Skj-xq-AgQ" secondAttribute="top" id="tfL-hp-2I2"/>
</constraints>
</scrollView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kHo-B9-R7a">
<rect key="frame" x="0.0" y="0.0" width="375" height="36"/>

View File

@ -36,8 +36,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
var shrinkGestureEnabled = true
weak var animationSourceView: UIImageView?
var animationImage: UIImage? { largeImageVC?.image ?? animationSourceView?.image }
var animationGifData: Data? { largeImageVC?.gifData }
var animationImage: UIImage? { largeImageVC?.animationImage ?? animationSourceView?.image }
var animationGifData: Data? { largeImageVC?.animationGifData }
var dismissInteractionController: LargeImageInteractionController?
override var prefersStatusBarHidden: Bool {
@ -108,12 +108,12 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
func createLargeImage(data: Data) {
guard let image = UIImage(data: data) else { return }
largeImageVC = LargeImageViewController(image: image, description: imageDescription, sourceView: animationSourceView)
let gifData = url.pathExtension == "gif" ? data : nil
let imageView = LargeImageImageContentView(image: image, gifData: gifData)
largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView)
largeImageVC!.initialControlsVisible = initialControlsVisible
largeImageVC!.shrinkGestureEnabled = false
if url.pathExtension == "gif" {
largeImageVC!.gifData = data
}
embedChild(largeImageVC!)
}

View File

@ -39,6 +39,12 @@ struct AppearancePrefsView : View {
Toggle(isOn: $preferences.hideCustomEmojiInUsernames) {
Text("Hide Custom Emoji in Usernames")
}
Toggle(isOn: $preferences.showIsStatusReplyIcon) {
Text("Show Status Reply Icons")
}
Toggle(isOn: $preferences.alwaysShowStatusVisibilityIcon) {
Text("Always Show Status Visibility Icons")
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle(Text("Appearance"))

View File

@ -36,14 +36,6 @@ protocol TuskerNavigationDelegate: class {
func reply(to statusID: String, mentioningAcct: String?)
func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController
func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController
func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIImageView)
func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView)
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController
func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView)
@ -151,27 +143,6 @@ extension TuskerNavigationDelegate where Self: UIViewController {
present(vc, animated: true)
}
func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController {
let vc = LargeImageViewController(image: image, description: description, sourceView: sourceView)
vc.transitioningDelegate = self
return vc
}
func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController {
let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceView: sourceView)
vc.transitioningDelegate = self
vc.gifData = gifData
return vc
}
func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIImageView) {
present(largeImage(image, description: description, sourceView: sourceView), animated: true)
}
func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView) {
present(largeImage(gifData: gifData, description: description, sourceView: sourceView), animated: true)
}
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController {
let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description)
vc.animationSourceView = sourceView

View File

@ -19,8 +19,8 @@ class GifvAttachmentView: UIView {
layer as! AVPlayerLayer
}
private let item: AVPlayerItem
private let player: AVPlayer
let item: AVPlayerItem
let player: AVPlayer
init(asset: AVAsset, gravity: AVLayerVideoGravity) {
item = AVPlayerItem(asset: asset)

View File

@ -128,9 +128,6 @@ class BaseStatusTableViewCell: UITableViewCell {
updateUI(account: account)
updateUIForPreferences(account: account)
visibilityImageView.image = UIImage(systemName: status.visibility.unfilledImageName)
visibilityImageView.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "status visibility indicator accessibility label"), status.visibility.displayName)
attachmentsView.updateUI(status: status)
attachmentsView.isAccessibilityElement = status.attachments.count > 0
attachmentsView.accessibilityLabel = String(format: NSLocalizedString("%d attachments", comment: "status attachments count accessibility label"), status.attachments.count)
@ -154,8 +151,8 @@ class BaseStatusTableViewCell: UITableViewCell {
reblogDisabled = status.visibility == .direct || (status.visibility == .private && status.account.id != mastodonController.account.id)
}
reblogButton.isEnabled = !reblogDisabled
let reblogButtonImage = reblogDisabled ? UIImage(systemName: status.visibility.imageName) : UIImage(systemName: "repeat")
reblogButton.setImage(reblogButtonImage, for: .normal)
updateStatusIconsForPreferences(status)
if state.unknown {
collapsible = !status.spoilerText.isEmpty
@ -207,8 +204,11 @@ class BaseStatusTableViewCell: UITableViewCell {
}
@objc func preferencesChanged() {
guard let mastodonController = mastodonController, let account = mastodonController.persistentContainer.account(for: accountID) else { return }
guard let mastodonController = mastodonController,
let account = mastodonController.persistentContainer.account(for: accountID),
let status = mastodonController.persistentContainer.status(for: statusID) else { return }
updateUIForPreferences(account: account)
updateStatusIconsForPreferences(status)
}
func updateUIForPreferences(account: AccountMO) {
@ -217,6 +217,21 @@ class BaseStatusTableViewCell: UITableViewCell {
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false)
}
func updateStatusIconsForPreferences(_ status: StatusMO) {
visibilityImageView.isHidden = !Preferences.shared.alwaysShowStatusVisibilityIcon
if Preferences.shared.alwaysShowStatusVisibilityIcon {
visibilityImageView.image = UIImage(systemName: status.visibility.unfilledImageName)
visibilityImageView.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "status visibility indicator accessibility label"), status.visibility.displayName)
}
let reblogButtonImage: UIImage
if Preferences.shared.alwaysShowStatusVisibilityIcon || reblogButton.isEnabled {
reblogButtonImage = UIImage(systemName: "repeat")!
} else {
reblogButtonImage = UIImage(systemName: status.visibility.imageName)!
}
reblogButton.setImage(reblogButtonImage, for: .normal)
}
override func prepareForReuse() {
super.prepareForReuse()

View File

@ -89,12 +89,11 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
timestampLabel.isHidden = pinned
pinImageView.isHidden = !pinned
}
replyImageView.isHidden = !showReplyIndicator || status.inReplyToID == nil
}
@objc override func preferencesChanged() {
super.preferencesChanged()
if let rebloggerID = rebloggerID,
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
updateRebloggerLabel(reblogger: reblogger)
@ -111,6 +110,12 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
}
}
override func updateStatusIconsForPreferences(_ status: StatusMO) {
super.updateStatusIconsForPreferences(status)
replyImageView.isHidden = !Preferences.shared.showIsStatusReplyIcon || !showReplyIndicator || status.inReplyToID == nil
}
func updateTimestamp() {
// if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated
// so we bail out immediately, since there's nothing to update

View File

@ -151,36 +151,39 @@
</stackView>
</subviews>
</stackView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="globe" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="LRh-Cc-1br">
<rect key="frame" x="31" y="55" width="19" height="20"/>
<color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="22" id="3Mk-NN-6fY"/>
</constraints>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/>
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bubble.left.and.bubble.right" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KdQ-Zn-IhD">
<rect key="frame" x="0.0" y="55" width="25.5" height="21.5"/>
<color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" label="Is a reply"/>
<constraints>
<constraint firstAttribute="height" constant="22" id="x0C-Qo-YVA"/>
</constraints>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
<rect key="frame" x="0.5" y="54" width="49.5" height="22"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bubble.left.and.bubble.right" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KdQ-Zn-IhD">
<rect key="frame" x="0.0" y="1" width="25.5" height="21.5"/>
<color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" label="Is a reply"/>
<constraints>
<constraint firstAttribute="height" constant="22" id="x0C-Qo-YVA"/>
</constraints>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/>
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="globe" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="LRh-Cc-1br">
<rect key="frame" x="30.5" y="1" width="19" height="20"/>
<color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="22" id="3Mk-NN-6fY"/>
</constraints>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/>
</imageView>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="QMP-j2-HLn" secondAttribute="trailing" constant="8" id="0Tm-v7-Ts4"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="8" id="2Ao-Gj-fY3"/>
<constraint firstItem="oie-wK-IpU" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="7Mp-WS-FhY"/>
<constraint firstItem="QMP-j2-HLn" firstAttribute="top" secondItem="ve3-Y1-NQH" secondAttribute="top" id="PC4-Bi-QXm"/>
<constraint firstItem="LRh-Cc-1br" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="QE5-0f-q1a"/>
<constraint firstItem="KdQ-Zn-IhD" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="R2V-fr-WCN"/>
<constraint firstItem="oie-wK-IpU" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="QKi-ny-jOJ"/>
<constraint firstItem="gIY-Wp-RSk" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="top" id="fEd-wN-kuQ"/>
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="oie-wK-IpU" secondAttribute="trailing" constant="8" id="fqd-p6-oGe"/>
<constraint firstAttribute="trailingMargin" secondItem="gIY-Wp-RSk" secondAttribute="trailing" id="hKk-kO-wFT"/>
<constraint firstAttribute="bottom" secondItem="gIY-Wp-RSk" secondAttribute="bottom" id="kRU-Ct-CIg"/>
<constraint firstItem="KdQ-Zn-IhD" firstAttribute="bottom" relation="lessThanOrEqual" secondItem="ve3-Y1-NQH" secondAttribute="bottom" id="rp8-N9-Iid"/>
<constraint firstItem="KdQ-Zn-IhD" firstAttribute="leading" secondItem="QMP-j2-HLn" secondAttribute="leading" id="uJd-Cz-AG3"/>
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="LRh-Cc-1br" secondAttribute="trailing" constant="8" id="zFc-5l-916"/>
<constraint firstItem="QMP-j2-HLn" firstAttribute="leading" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="zeW-tQ-uJl"/>
</constraints>
</view>