forked from shadowfacts/Tusker
parent
f2ab1778c5
commit
e9962997a6
|
@ -166,6 +166,8 @@
|
||||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
||||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
||||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; };
|
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; };
|
||||||
|
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.swift */; };
|
||||||
|
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */; };
|
||||||
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; };
|
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; };
|
||||||
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
|
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
|
||||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
|
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
|
||||||
|
@ -513,6 +515,8 @@
|
||||||
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
||||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; };
|
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; };
|
||||||
|
D6895DC128D65274006341DA /* CustomAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertController.swift; sourceTree = "<group>"; };
|
||||||
|
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmReblogStatusPreviewView.swift; sourceTree = "<group>"; };
|
||||||
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; };
|
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
|
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1253,6 +1257,7 @@
|
||||||
children = (
|
children = (
|
||||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||||
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
|
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
|
||||||
|
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */,
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */,
|
D620483523D38075008A63EF /* ContentTextView.swift */,
|
||||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||||
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
||||||
|
@ -1293,6 +1298,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
|
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
|
||||||
|
D6895DC128D65274006341DA /* CustomAlertController.swift */,
|
||||||
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
|
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
|
||||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
||||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
||||||
|
@ -1770,6 +1776,7 @@
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
||||||
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */,
|
D6D3FDE224F46A8D00FF50A5 /* ComposeUIState.swift in Sources */,
|
||||||
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */,
|
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */,
|
||||||
|
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */,
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||||
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
||||||
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
|
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
|
||||||
|
@ -1943,6 +1950,7 @@
|
||||||
D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */,
|
D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */,
|
||||||
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
||||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
||||||
|
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */,
|
||||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
||||||
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
|
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
|
||||||
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
||||||
|
|
|
@ -73,6 +73,7 @@ class ImageCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
|
func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
|
||||||
|
// todo: this should integrate with the task cancellation mechanism somehow
|
||||||
return await withCheckedContinuation { continuation in
|
return await withCheckedContinuation { continuation in
|
||||||
_ = get(url, loadOriginal: loadOriginal) { data, image in
|
_ = get(url, loadOriginal: loadOriginal) { data, image in
|
||||||
continuation.resume(returning: (data, image))
|
continuation.resume(returning: (data, image))
|
||||||
|
|
|
@ -0,0 +1,393 @@
|
||||||
|
//
|
||||||
|
// CustomAlertController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/17/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class CustomAlertController: UIViewController {
|
||||||
|
private let config: Configuration
|
||||||
|
|
||||||
|
fileprivate var blurView: UIVisualEffectView!
|
||||||
|
fileprivate var dimmingView: UIView!
|
||||||
|
fileprivate var buttonsStack: UIStackView!
|
||||||
|
fileprivate var actionsView: CustomAlertActionsView!
|
||||||
|
private var separatorHeightConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
init(config: Configuration) {
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
transitioningDelegate = self
|
||||||
|
modalPresentationStyle = .custom
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
|
||||||
|
dimmingView = UIView()
|
||||||
|
dimmingView.backgroundColor = .black.withAlphaComponent(0.2)
|
||||||
|
dimmingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(dimmingView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
dimmingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
dimmingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
dimmingView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
||||||
|
|
||||||
|
blurView.backgroundColor = .systemBackground
|
||||||
|
blurView.layer.cornerRadius = 15
|
||||||
|
blurView.layer.cornerCurve = .continuous
|
||||||
|
blurView.layer.masksToBounds = true
|
||||||
|
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(blurView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
blurView.widthAnchor.constraint(equalToConstant: 270),
|
||||||
|
blurView.heightAnchor.constraint(greaterThanOrEqualToConstant: 100),
|
||||||
|
blurView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||||
|
blurView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
let stack = UIStackView()
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.spacing = 0
|
||||||
|
stack.alignment = .fill
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
blurView.contentView.addSubview(stack)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stack.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
|
||||||
|
blurView.contentView.trailingAnchor.constraint(equalTo: stack.trailingAnchor),
|
||||||
|
stack.topAnchor.constraint(equalTo: blurView.contentView.topAnchor, constant: 16),
|
||||||
|
stack.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.text = config.title
|
||||||
|
titleLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold)!, size: 0)
|
||||||
|
titleLabel.numberOfLines = 0
|
||||||
|
titleLabel.textAlignment = .center
|
||||||
|
stack.addArrangedSubview(titleLabel)
|
||||||
|
|
||||||
|
stack.addSpacer(length: 8)
|
||||||
|
|
||||||
|
stack.addArrangedSubview(config.content)
|
||||||
|
|
||||||
|
stack.addSpacer(length: 16)
|
||||||
|
|
||||||
|
let separator = UIView()
|
||||||
|
separator.backgroundColor = .separator
|
||||||
|
stack.addArrangedSubview(separator)
|
||||||
|
separatorHeightConstraint = separator.heightAnchor.constraint(equalToConstant: 0.5)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
separator.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
||||||
|
separatorHeightConstraint,
|
||||||
|
])
|
||||||
|
|
||||||
|
actionsView = CustomAlertActionsView(config: config, dismiss: { [unowned self] in
|
||||||
|
self.dismiss(animated: true)
|
||||||
|
})
|
||||||
|
stack.addArrangedSubview(actionsView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
actionsView.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillLayoutSubviews() {
|
||||||
|
super.viewWillLayoutSubviews()
|
||||||
|
|
||||||
|
separatorHeightConstraint.constant = 1 / view.window!.screen.scale
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Configuration {
|
||||||
|
var title: String
|
||||||
|
var content: UIView
|
||||||
|
var actions: [Action]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Action {
|
||||||
|
let title: String
|
||||||
|
let style: UIAlertAction.Style
|
||||||
|
let handler: (() -> Void)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomAlertActionsView: UIControl {
|
||||||
|
private let dismiss: () -> Void
|
||||||
|
|
||||||
|
private let stack = UIStackView()
|
||||||
|
private var labels: [UIView] = []
|
||||||
|
private var labelWrappers: [UIView] = []
|
||||||
|
private var labelWrapperWidthConstraints: [NSLayoutConstraint] = []
|
||||||
|
// the actions from the config but reordered to match labelWrappers order
|
||||||
|
private var reorderedActions: [CustomAlertController.Action] = []
|
||||||
|
private var separators: [UIView] = []
|
||||||
|
private var separatorSizeConstraints: [NSLayoutConstraint] = []
|
||||||
|
|
||||||
|
private let generator = UISelectionFeedbackGenerator()
|
||||||
|
private var currentSelectedActionIndex: Int?
|
||||||
|
|
||||||
|
init(config: CustomAlertController.Configuration, dismiss: @escaping () -> Void) {
|
||||||
|
self.dismiss = dismiss
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
stack.alignment = .fill
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(stack)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
stack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
stack.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
stack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
for action in config.actions {
|
||||||
|
let labelWrapper = UIView()
|
||||||
|
labelWrapper.isAccessibilityElement = true
|
||||||
|
labelWrapper.accessibilityTraits = .button
|
||||||
|
labelWrapper.accessibilityRespondsToUserInteraction = true
|
||||||
|
labelWrapper.accessibilityLabel = action.title
|
||||||
|
|
||||||
|
let label = UILabel()
|
||||||
|
labels.append(label)
|
||||||
|
label.text = action.title
|
||||||
|
label.textColor = .tintColor
|
||||||
|
switch action.style {
|
||||||
|
case .cancel:
|
||||||
|
label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold)!, size: 0)
|
||||||
|
case .destructive:
|
||||||
|
label.textColor = .systemRed
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
labelWrapper.addSubview(label)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.leadingAnchor.constraint(greaterThanOrEqualTo: labelWrapper.leadingAnchor, constant: 4),
|
||||||
|
label.trailingAnchor.constraint(lessThanOrEqualTo: labelWrapper.trailingAnchor, constant: -4),
|
||||||
|
label.centerXAnchor.constraint(equalTo: labelWrapper.centerXAnchor),
|
||||||
|
label.centerYAnchor.constraint(equalTo: labelWrapper.centerYAnchor),
|
||||||
|
labelWrapper.heightAnchor.constraint(equalToConstant: 44),
|
||||||
|
])
|
||||||
|
|
||||||
|
if action.style == .cancel {
|
||||||
|
labelWrappers.insert(labelWrapper, at: 0)
|
||||||
|
reorderedActions.insert(action, at: 0)
|
||||||
|
} else {
|
||||||
|
labelWrappers.append(labelWrapper)
|
||||||
|
reorderedActions.append(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var first = true
|
||||||
|
for wrapper in labelWrappers {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
let separator = UIView()
|
||||||
|
separator.backgroundColor = .separator
|
||||||
|
stack.addArrangedSubview(separator)
|
||||||
|
separators.append(separator)
|
||||||
|
}
|
||||||
|
stack.addArrangedSubview(wrapper)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
updateAxis()
|
||||||
|
super.layoutSubviews()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateAxis() {
|
||||||
|
if reorderedActions.count > 2 || labels.map({ $0.intrinsicContentSize.width }).contains(where: { $0 > (bounds.width - 16) / 2 }) {
|
||||||
|
stack.axis = .vertical
|
||||||
|
NSLayoutConstraint.deactivate(labelWrapperWidthConstraints)
|
||||||
|
labelWrapperWidthConstraints = []
|
||||||
|
NSLayoutConstraint.deactivate(separatorSizeConstraints)
|
||||||
|
separatorSizeConstraints = separators.map {
|
||||||
|
$0.heightAnchor.constraint(equalToConstant: 0.5)
|
||||||
|
}
|
||||||
|
NSLayoutConstraint.activate(separatorSizeConstraints)
|
||||||
|
} else {
|
||||||
|
stack.axis = .horizontal
|
||||||
|
labelWrapperWidthConstraints = labelWrappers.map {
|
||||||
|
$0.widthAnchor.constraint(equalToConstant: (bounds.width - 0.5) / 2)
|
||||||
|
}
|
||||||
|
NSLayoutConstraint.activate(labelWrapperWidthConstraints)
|
||||||
|
NSLayoutConstraint.deactivate(separatorSizeConstraints)
|
||||||
|
separatorSizeConstraints = separators.map {
|
||||||
|
$0.widthAnchor.constraint(equalToConstant: 0.5)
|
||||||
|
}
|
||||||
|
NSLayoutConstraint.activate(separatorSizeConstraints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIControl
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
// we want this view to handle all touches inside it
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||||
|
for (index, labelWrapper) in labelWrappers.enumerated() {
|
||||||
|
if labelWrapper.point(inside: touch.location(in: labelWrapper), with: event) {
|
||||||
|
currentSelectedActionIndex = index
|
||||||
|
labelWrapper.backgroundColor = .secondarySystemFill
|
||||||
|
|
||||||
|
generator.prepare()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||||
|
for (index, labelWrapper) in labelWrappers.enumerated() {
|
||||||
|
if labelWrapper.point(inside: touch.location(in: labelWrapper), with: event) {
|
||||||
|
if index != currentSelectedActionIndex {
|
||||||
|
if let currentSelectedActionIndex {
|
||||||
|
labelWrappers[currentSelectedActionIndex].backgroundColor = nil
|
||||||
|
}
|
||||||
|
generator.selectionChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSelectedActionIndex = index
|
||||||
|
labelWrapper.backgroundColor = .secondarySystemFill
|
||||||
|
|
||||||
|
generator.prepare()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// didn't hit any button
|
||||||
|
if let currentSelectedActionIndex {
|
||||||
|
labelWrappers[currentSelectedActionIndex].backgroundColor = nil
|
||||||
|
self.currentSelectedActionIndex = nil
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||||
|
super.endTracking(touch, with: event)
|
||||||
|
|
||||||
|
if let currentSelectedActionIndex {
|
||||||
|
labelWrappers[currentSelectedActionIndex].backgroundColor = nil
|
||||||
|
reorderedActions[currentSelectedActionIndex].handler?()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func cancelTracking(with event: UIEvent?) {
|
||||||
|
super.cancelTracking(with: event)
|
||||||
|
|
||||||
|
if let currentSelectedActionIndex {
|
||||||
|
labelWrappers[currentSelectedActionIndex].backgroundColor = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CustomAlertController {
|
||||||
|
override func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||||
|
return CustomAlertPresentationAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||||
|
return CustomAlertDismissAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomAlertPresentationAnimation: NSObject, UIViewControllerAnimatedTransitioning {
|
||||||
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||||
|
return 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||||
|
guard let presenter = transitionContext.viewController(forKey: .from),
|
||||||
|
let alert = transitionContext.viewController(forKey: .to) as? CustomAlertController else {
|
||||||
|
transitionContext.completeTransition(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = transitionContext.containerView
|
||||||
|
container.addSubview(alert.view)
|
||||||
|
|
||||||
|
guard transitionContext.isAnimated else {
|
||||||
|
presenter.view.tintAdjustmentMode = .dimmed
|
||||||
|
transitionContext.completeTransition(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
alert.dimmingView.layer.opacity = 0
|
||||||
|
alert.blurView.layer.opacity = 0
|
||||||
|
alert.blurView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
|
||||||
|
|
||||||
|
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveEaseOut) {
|
||||||
|
presenter.view.tintAdjustmentMode = .dimmed
|
||||||
|
alert.dimmingView.layer.opacity = 1
|
||||||
|
alert.blurView.layer.opacity = 1
|
||||||
|
alert.blurView.transform = .identity
|
||||||
|
} completion: { _ in
|
||||||
|
transitionContext.completeTransition(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomAlertDismissAnimation: NSObject, UIViewControllerAnimatedTransitioning {
|
||||||
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||||
|
return 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||||
|
guard let presenter = transitionContext.viewController(forKey: .to),
|
||||||
|
let alert = transitionContext.viewController(forKey: .from) as? CustomAlertController else {
|
||||||
|
transitionContext.completeTransition(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard transitionContext.isAnimated else {
|
||||||
|
presenter.view.tintAdjustmentMode = .dimmed
|
||||||
|
transitionContext.completeTransition(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0) {
|
||||||
|
presenter.view.tintAdjustmentMode = .automatic
|
||||||
|
alert.view.layer.opacity = 0
|
||||||
|
} completion: { _ in
|
||||||
|
transitionContext.completeTransition(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension UIStackView {
|
||||||
|
func addSpacer(length: CGFloat) {
|
||||||
|
let spacer = UIView()
|
||||||
|
addArrangedSubview(spacer)
|
||||||
|
if axis == .vertical {
|
||||||
|
spacer.heightAnchor.constraint(equalToConstant: length).isActive = true
|
||||||
|
} else {
|
||||||
|
spacer.widthAnchor.constraint(equalToConstant: length).isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// ConfirmReblogStatusPreviewView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/17/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ConfirmReblogStatusPreviewView: UIView {
|
||||||
|
|
||||||
|
private var avatarTask: Task<Void, Error>?
|
||||||
|
|
||||||
|
init(status: StatusMO) {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
let hStack = UIStackView()
|
||||||
|
hStack.axis = .horizontal
|
||||||
|
hStack.spacing = 8
|
||||||
|
hStack.alignment = .leading
|
||||||
|
hStack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(hStack)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
hStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||||
|
hStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
||||||
|
hStack.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
hStack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
let avatarSize: CGFloat = 30
|
||||||
|
let avatarImageView = UIImageView()
|
||||||
|
avatarImageView.layer.masksToBounds = true
|
||||||
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarSize
|
||||||
|
hStack.addArrangedSubview(avatarImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarImageView.widthAnchor.constraint(equalToConstant: avatarSize),
|
||||||
|
avatarImageView.heightAnchor.constraint(equalToConstant: avatarSize),
|
||||||
|
])
|
||||||
|
if let avatar = status.account.avatar {
|
||||||
|
avatarTask = Task {
|
||||||
|
let (_, image) = await ImageCache.avatars.get(avatar)
|
||||||
|
try Task.checkCancellation()
|
||||||
|
avatarImageView.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let vStack = UIStackView()
|
||||||
|
vStack.axis = .vertical
|
||||||
|
vStack.spacing = 2
|
||||||
|
vStack.alignment = .fill
|
||||||
|
vStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
|
hStack.addArrangedSubview(vStack)
|
||||||
|
|
||||||
|
let displayNameLabel = EmojiLabel()
|
||||||
|
displayNameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .caption1).addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]]), size: 0)
|
||||||
|
displayNameLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
displayNameLabel.updateForAccountDisplayName(account: status.account)
|
||||||
|
vStack.addArrangedSubview(displayNameLabel)
|
||||||
|
|
||||||
|
let contentView = StatusContentTextView()
|
||||||
|
contentView.defaultFont = .preferredFont(forTextStyle: .caption2)
|
||||||
|
contentView.isUserInteractionEnabled = false
|
||||||
|
contentView.isScrollEnabled = false
|
||||||
|
contentView.backgroundColor = nil
|
||||||
|
contentView.textContainerInset = .zero
|
||||||
|
// remove the extra line spacing applied by StatusContentTextView because, since we're using a smaller font, the regular 2pt looks big
|
||||||
|
contentView.paragraphStyle = .default
|
||||||
|
// TODO: line limit
|
||||||
|
contentView.setTextFrom(status: status)
|
||||||
|
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
vStack.addArrangedSubview(contentView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
contentView.heightAnchor.constraint(lessThanOrEqualToConstant: 200),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
avatarTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -23,6 +23,12 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
|
|
||||||
var defaultFont: UIFont = .systemFont(ofSize: 17)
|
var defaultFont: UIFont = .systemFont(ofSize: 17)
|
||||||
var defaultColor: UIColor = .label
|
var defaultColor: UIColor = .label
|
||||||
|
var paragraphStyle: NSParagraphStyle = {
|
||||||
|
let style = NSMutableParagraphStyle()
|
||||||
|
// 2 points is enough that it doesn't make things look weirdly spaced out, but leaves a slight gap when there are multiple lines of emojis
|
||||||
|
style.lineSpacing = 2
|
||||||
|
return style
|
||||||
|
}()
|
||||||
|
|
||||||
private(set) var hasEmojis = false
|
private(set) var hasEmojis = false
|
||||||
|
|
||||||
|
@ -36,9 +42,17 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
// The preview created in the previewForHighlighting method, so that we can use the same one in previewForDismissing.
|
// The preview created in the previewForHighlighting method, so that we can use the same one in previewForDismissing.
|
||||||
private weak var currentTargetedPreview: UITargetedPreview?
|
private weak var currentTargetedPreview: UITargetedPreview?
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override init(frame: CGRect, textContainer: NSTextContainer?) {
|
||||||
super.awakeFromNib()
|
super.init(frame: frame, textContainer: textContainer)
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
commonInit()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func commonInit() {
|
||||||
delegate = self
|
delegate = self
|
||||||
|
|
||||||
// Disable layer masking, otherwise the context menu opening animation
|
// Disable layer masking, otherwise the context menu opening animation
|
||||||
|
@ -78,10 +92,7 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
||||||
mutAttrString.collapseWhitespace()
|
mutAttrString.collapseWhitespace()
|
||||||
|
|
||||||
let style = NSMutableParagraphStyle()
|
mutAttrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: mutAttrString.fullRange)
|
||||||
// 2 points is enough that it doesn't make things look weirdly spaced out, but leaves a slight gap when there are multiple lines of emojis
|
|
||||||
style.lineSpacing = 2
|
|
||||||
mutAttrString.addAttribute(.paragraphStyle, value: style, range: mutAttrString.fullRange)
|
|
||||||
|
|
||||||
self.attributedText = mutAttrString
|
self.attributedText = mutAttrString
|
||||||
}
|
}
|
||||||
|
|
|
@ -412,11 +412,14 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
// if we are about to reblog and the user has confirmation enabled
|
// if we are about to reblog and the user has confirmation enabled
|
||||||
if !reblogged,
|
if !reblogged,
|
||||||
Preferences.shared.confirmBeforeReblog {
|
Preferences.shared.confirmBeforeReblog {
|
||||||
let alert = UIAlertController(title: "Confirm Reblog", message: "Are you sure you want to reblog this post by @\(status.account.acct)?", preferredStyle: .alert)
|
let preview = ConfirmReblogStatusPreviewView(status: status)
|
||||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
let config = CustomAlertController.Configuration(title: "Are you sure you want to reblog this post?", content: preview, actions: [
|
||||||
alert.addAction(UIAlertAction(title: "Reblog", style: .default) { (_) in
|
CustomAlertController.Action(title: "Cancel", style: .cancel, handler: nil),
|
||||||
self.toggleReblogInternal()
|
CustomAlertController.Action(title: "Reblog", style: .default, handler: { [unowned self] in
|
||||||
})
|
self.toggleReblogInternal()
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
let alert = CustomAlertController(config: config)
|
||||||
delegate?.present(alert, animated: true)
|
delegate?.present(alert, animated: true)
|
||||||
} else {
|
} else {
|
||||||
toggleReblogInternal()
|
toggleReblogInternal()
|
||||||
|
|
Loading…
Reference in New Issue