Edit attachments
This commit is contained in:
parent
11f43c3df5
commit
032e187681
|
@ -0,0 +1,28 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import Mastodon
|
||||
import UIKit
|
||||
|
||||
extension UIView {
|
||||
private static let defaultContentsRectSize = CGSize(width: 1, height: 1)
|
||||
|
||||
func setContentsRect(focus: Attachment.Meta.Focus, mediaSize: CGSize) {
|
||||
let aspectRatio = mediaSize.width / mediaSize.height
|
||||
let viewAspectRatio = bounds.width / bounds.height
|
||||
var origin = CGPoint.zero
|
||||
|
||||
if viewAspectRatio > aspectRatio {
|
||||
let mediaProportionalHeight = mediaSize.height * bounds.width / mediaSize.width
|
||||
let maxPan = (mediaProportionalHeight - bounds.height) / (2 * mediaProportionalHeight)
|
||||
|
||||
origin.y = CGFloat(-focus.y) * maxPan
|
||||
} else {
|
||||
let mediaProportionalWidth = mediaSize.width * bounds.height / mediaSize.height
|
||||
let maxPan = (mediaProportionalWidth - bounds.width) / (2 * mediaProportionalWidth)
|
||||
|
||||
origin.x = CGFloat(focus.x) * maxPan
|
||||
}
|
||||
|
||||
layer.contentsRect = CGRect(origin: origin, size: Self.defaultContentsRectSize)
|
||||
}
|
||||
}
|
|
@ -31,6 +31,11 @@
|
|||
"add-identity.join" = "Join";
|
||||
"add-identity.request-invite" = "Request an invite";
|
||||
"add-identity.unable-to-connect-to-instance" = "Unable to connect to instance";
|
||||
"attachment.edit.description" = "Describe for the visually impaired";
|
||||
"attachment.edit.description.audio" = "Describe for people with hearing loss";
|
||||
"attachment.edit.description.video" = "Describe for people with hearing loss or visual impairment";
|
||||
"attachment.edit.title" = "Edit media";
|
||||
"attachment.edit.thumbnail.prompt" = "Drag the circle on the preview to choose the focal point which will always be in view on all thumbnails";
|
||||
"attachment.sensitive-content" = "Sensitive content";
|
||||
"attachment.media-hidden" = "Media hidden";
|
||||
"bookmarks" = "Bookmarks";
|
||||
|
|
|
@ -22,8 +22,8 @@ public struct Attachment: Codable, Hashable {
|
|||
}
|
||||
|
||||
public struct Focus: Codable, Hashable {
|
||||
public let x: Double
|
||||
public let y: Double
|
||||
public var x: Double
|
||||
public var y: Double
|
||||
}
|
||||
|
||||
public let original: Info?
|
||||
|
@ -60,3 +60,7 @@ public extension Attachment {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public extension Attachment.Meta.Focus {
|
||||
static let `default` = Self(x: 0, y: 0)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import Mastodon
|
|||
|
||||
public enum AttachmentEndpoint {
|
||||
case create(data: Data, mimeType: String, description: String?, focus: Attachment.Meta.Focus?)
|
||||
case update(id: Attachment.Id, description: String?, focus: Attachment.Meta.Focus?)
|
||||
}
|
||||
|
||||
extension AttachmentEndpoint: Endpoint {
|
||||
|
@ -19,6 +20,8 @@ extension AttachmentEndpoint: Endpoint {
|
|||
switch self {
|
||||
case .create:
|
||||
return []
|
||||
case let .update(id, _, _):
|
||||
return [id]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,6 +40,18 @@ extension AttachmentEndpoint: Endpoint {
|
|||
params["focus"] = .string("\(focus.x),\(focus.y)")
|
||||
}
|
||||
|
||||
return params
|
||||
case let .update(_, description, focus):
|
||||
var params = [String: MultipartFormValue]()
|
||||
|
||||
if let description = description {
|
||||
params["description"] = .string(description)
|
||||
}
|
||||
|
||||
if let focus = focus {
|
||||
params["focus"] = .string("\(focus.x),\(focus.y)")
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +60,8 @@ extension AttachmentEndpoint: Endpoint {
|
|||
switch self {
|
||||
case .create:
|
||||
return .post
|
||||
case .update:
|
||||
return .put
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,14 @@
|
|||
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; };
|
||||
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; };
|
||||
D04F9E8E259E9C950081B0C9 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D04F9E8D259E9C950081B0C9 /* ViewModels */; };
|
||||
D05936CF25A8D79800754FDF /* EditAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */; };
|
||||
D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */; };
|
||||
D05936DE25A937EC00754FDF /* EditThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936DD25A937EC00754FDF /* EditThumbnailView.swift */; };
|
||||
D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936DD25A937EC00754FDF /* EditThumbnailView.swift */; };
|
||||
D05936E925AA3F3D00754FDF /* EditAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936E825AA3F3D00754FDF /* EditAttachmentView.swift */; };
|
||||
D05936EA25AA3F3D00754FDF /* EditAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936E825AA3F3D00754FDF /* EditAttachmentView.swift */; };
|
||||
D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936F325AA66A600754FDF /* UIView+Extensions.swift */; };
|
||||
D05936F525AA66A600754FDF /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05936F325AA66A600754FDF /* UIView+Extensions.swift */; };
|
||||
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; };
|
||||
|
@ -181,6 +189,10 @@
|
|||
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreferencesView.swift; sourceTree = "<group>"; };
|
||||
D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupAndSyncingPreferencesView.swift; sourceTree = "<group>"; };
|
||||
D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAttachmentViewController.swift; sourceTree = "<group>"; };
|
||||
D05936DD25A937EC00754FDF /* EditThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditThumbnailView.swift; sourceTree = "<group>"; };
|
||||
D05936E825AA3F3D00754FDF /* EditAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAttachmentView.swift; sourceTree = "<group>"; };
|
||||
D05936F325AA66A600754FDF /* UIView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = "<group>"; };
|
||||
D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = "<group>"; };
|
||||
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -442,7 +454,9 @@
|
|||
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
||||
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
||||
D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */,
|
||||
D05936E825AA3F3D00754FDF /* EditAttachmentView.swift */,
|
||||
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
||||
D05936DD25A937EC00754FDF /* EditThumbnailView.swift */,
|
||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
||||
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
||||
|
@ -482,6 +496,7 @@
|
|||
D0C7D43024F76169001EBDBB /* View Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */,
|
||||
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */,
|
||||
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */,
|
||||
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
|
||||
|
@ -522,6 +537,7 @@
|
|||
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
|
||||
D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */,
|
||||
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
|
||||
D05936F325AA66A600754FDF /* UIView+Extensions.swift */,
|
||||
D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */,
|
||||
D0030981250C6C8500EACB32 /* URL+Extensions.swift */,
|
||||
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */,
|
||||
|
@ -760,6 +776,7 @@
|
|||
D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */,
|
||||
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
||||
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
||||
D05936DE25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
||||
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
||||
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */,
|
||||
D0E7AD3925870B13005F5E2D /* UIVIewController+Extensions.swift in Sources */,
|
||||
|
@ -789,6 +806,7 @@
|
|||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
||||
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
||||
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
||||
D05936CF25A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
|
||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
||||
|
@ -803,6 +821,8 @@
|
|||
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
||||
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
|
||||
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */,
|
||||
D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
||||
D05936E925AA3F3D00754FDF /* EditAttachmentView.swift in Sources */,
|
||||
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
||||
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
|
||||
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
|
||||
|
@ -847,20 +867,24 @@
|
|||
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */,
|
||||
D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */,
|
||||
D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
|
||||
D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
||||
D038273C259EA38F00056E0F /* NewStatusView.swift in Sources */,
|
||||
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */,
|
||||
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
||||
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
|
||||
D015B14425A812F6006D88A8 /* PlayerCache.swift in Sources */,
|
||||
D05936F525AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
||||
D015B13F25A812EC006D88A8 /* PlayerView.swift in Sources */,
|
||||
D015B13A25A812E6006D88A8 /* AttachmentView.swift in Sources */,
|
||||
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
|
||||
D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */,
|
||||
D015B13525A812DD006D88A8 /* AttachmentsView.swift in Sources */,
|
||||
D05936EA25AA3F3D00754FDF /* EditAttachmentView.swift in Sources */,
|
||||
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
|
||||
D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */,
|
||||
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
|
||||
D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */,
|
||||
D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -212,6 +212,12 @@ public extension IdentityService {
|
|||
progress: progress)
|
||||
}
|
||||
|
||||
func updateAttachment(id: Attachment.Id,
|
||||
description: String,
|
||||
focus: Attachment.Meta.Focus) -> AnyPublisher<Attachment, Error> {
|
||||
mastodonAPIClient.request(AttachmentEndpoint.update(id: id, description: description, focus: focus))
|
||||
}
|
||||
|
||||
func post(statusComponents: StatusComponents) -> AnyPublisher<Status.Id, Error> {
|
||||
mastodonAPIClient.request(StatusEndpoint.post(statusComponents)).map(\.id).eraseToAnyPublisher()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import UIKit
|
||||
import ViewModels
|
||||
|
||||
final class EditAttachmentViewController: UIViewController {
|
||||
private let viewModel: AttachmentViewModel
|
||||
private let parentViewModel: CompositionViewModel
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(viewModel: AttachmentViewModel, parentViewModel: CompositionViewModel) {
|
||||
self.viewModel = viewModel
|
||||
self.parentViewModel = parentViewModel
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
let editThumbnailView = EditThumbnailView(viewModel: viewModel)
|
||||
|
||||
view.addSubview(editThumbnailView)
|
||||
editThumbnailView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let stackView = UIStackView()
|
||||
|
||||
view.addSubview(stackView)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = .defaultSpacing
|
||||
|
||||
let describeLabel = UILabel()
|
||||
|
||||
stackView.addArrangedSubview(describeLabel)
|
||||
describeLabel.adjustsFontForContentSizeCategory = true
|
||||
describeLabel.font = .preferredFont(forTextStyle: .headline)
|
||||
describeLabel.numberOfLines = 0
|
||||
describeLabel.textAlignment = .center
|
||||
|
||||
switch viewModel.attachment.type {
|
||||
case .audio:
|
||||
describeLabel.text = NSLocalizedString("attachment.edit.description.audio", comment: "")
|
||||
case .video:
|
||||
describeLabel.text = NSLocalizedString("attachment.edit.description.video", comment: "")
|
||||
default:
|
||||
describeLabel.text = NSLocalizedString("attachment.edit.description", comment: "")
|
||||
}
|
||||
|
||||
let textView = UITextView()
|
||||
|
||||
stackView.addArrangedSubview(textView)
|
||||
textView.adjustsFontForContentSizeCategory = true
|
||||
textView.font = .preferredFont(forTextStyle: .body)
|
||||
textView.layer.borderWidth = .hairline
|
||||
textView.layer.borderColor = UIColor.separator.cgColor
|
||||
textView.layer.cornerRadius = .defaultCornerRadius
|
||||
textView.delegate = self
|
||||
textView.text = viewModel.editingDescription
|
||||
|
||||
let remainingCharactersLabel = UILabel()
|
||||
|
||||
stackView.addArrangedSubview(remainingCharactersLabel)
|
||||
remainingCharactersLabel.adjustsFontForContentSizeCategory = true
|
||||
remainingCharactersLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
remainingCharactersLabel.text = "1500"
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
|
||||
stackView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: .defaultSpacing),
|
||||
editThumbnailView.leadingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: .defaultSpacing),
|
||||
stackView.bottomAnchor.constraint(
|
||||
equalTo: view.layoutMarginsGuide.bottomAnchor,
|
||||
constant: -.defaultSpacing),
|
||||
editThumbnailView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
editThumbnailView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
editThumbnailView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
||||
editThumbnailView.widthAnchor.constraint(equalTo: stackView.widthAnchor, multiplier: 3 / 2)
|
||||
])
|
||||
|
||||
viewModel.$descriptionRemainingCharacters
|
||||
.sink {
|
||||
remainingCharactersLabel.text = String($0)
|
||||
remainingCharactersLabel.textColor = $0 < 0 ? .systemRed : .label
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
textView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override func didMove(toParent parent: UIViewController?) {
|
||||
super.didMove(toParent: parent)
|
||||
|
||||
let cancelButton = UIBarButtonItem(
|
||||
systemItem: .cancel,
|
||||
primaryAction: UIAction { [weak self] _ in
|
||||
self?.presentingViewController?.dismiss(animated: true)
|
||||
})
|
||||
let doneButton = UIBarButtonItem(
|
||||
systemItem: .done,
|
||||
primaryAction: UIAction { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.parentViewModel.update(attachmentViewModel: self.viewModel)
|
||||
self.presentingViewController?.dismiss(animated: true)
|
||||
})
|
||||
|
||||
parent?.navigationItem.leftBarButtonItem = cancelButton
|
||||
parent?.navigationItem.rightBarButtonItem = doneButton
|
||||
parent?.navigationItem.title = NSLocalizedString("attachment.edit.title", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
extension EditAttachmentViewController: UITextViewDelegate {
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
viewModel.editingDescription = textView.text
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import AVFoundation
|
|||
import Combine
|
||||
import Kingfisher
|
||||
import PhotosUI
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import ViewModels
|
||||
|
||||
|
@ -110,6 +110,10 @@ private extension NewStatusViewController {
|
|||
#if !IS_SHARE_EXTENSION
|
||||
presentCamera(compositionViewModel: compositionViewModel)
|
||||
#endif
|
||||
case let .editAttachment(attachmentViewModel, compositionViewModel):
|
||||
presentAttachmentEditor(
|
||||
attachmentViewModel: attachmentViewModel,
|
||||
compositionViewModel: compositionViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -276,6 +280,15 @@ private extension NewStatusViewController {
|
|||
}
|
||||
#endif
|
||||
|
||||
func presentAttachmentEditor(attachmentViewModel: AttachmentViewModel, compositionViewModel: CompositionViewModel) {
|
||||
let editAttachmentsView = EditAttachmentView { (attachmentViewModel, compositionViewModel) }
|
||||
let editAttachmentViewController = UIHostingController(rootView: editAttachmentsView)
|
||||
let navigationController = UINavigationController(rootViewController: editAttachmentViewController)
|
||||
|
||||
navigationController.modalPresentationStyle = .overFullScreen
|
||||
present(navigationController, animated: true)
|
||||
}
|
||||
|
||||
func changeIdentityButton(identification: Identification) -> UIButton {
|
||||
let changeIdentityButton = UIButton()
|
||||
let downsampled = KingfisherOptionsInfo.downsampled(
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import Mastodon
|
||||
import Network
|
||||
|
||||
public final class AttachmentViewModel: ObservableObject {
|
||||
public let attachment: Attachment
|
||||
@Published public var editingDescription: String
|
||||
@Published public var editingFocus: Attachment.Meta.Focus
|
||||
@Published public private(set) var descriptionRemainingCharacters = AttachmentViewModel.descriptionMaxCharacters
|
||||
|
||||
private let identification: Identification
|
||||
private let status: Status?
|
||||
|
@ -14,6 +18,11 @@ public final class AttachmentViewModel: ObservableObject {
|
|||
self.attachment = attachment
|
||||
self.identification = identification
|
||||
self.status = status
|
||||
editingDescription = attachment.description ?? ""
|
||||
editingFocus = attachment.meta?.focus ?? .default
|
||||
$editingDescription
|
||||
.map { Self.descriptionMaxCharacters - $0.count }
|
||||
.assign(to: &$descriptionRemainingCharacters)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,7 +46,21 @@ public extension AttachmentViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
extension AttachmentViewModel {
|
||||
func updated() -> AnyPublisher<AttachmentViewModel, Error> {
|
||||
identification.service.updateAttachment(id: attachment.id, description: editingDescription, focus: editingFocus)
|
||||
.compactMap { [weak self] in
|
||||
guard let self = self else { return nil }
|
||||
|
||||
return AttachmentViewModel(attachment: $0, identification: self.identification, status: self.status)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
private extension AttachmentViewModel {
|
||||
static let descriptionMaxCharacters = 1500
|
||||
|
||||
static var wifiMonitor: NWPathMonitor = {
|
||||
let monitor = NWPathMonitor(requiredInterfaceType: .wifi)
|
||||
|
||||
|
|
|
@ -19,9 +19,12 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
|||
@Published public private(set) var remainingCharacters = CompositionViewModel.maxCharacters
|
||||
public let canRemoveAttachments = true
|
||||
|
||||
private let eventsSubject: PassthroughSubject<Event, Never>
|
||||
private var attachmentUploadCancellable: AnyCancellable?
|
||||
|
||||
init() {
|
||||
init(eventsSubject: PassthroughSubject<Event, Never>) {
|
||||
self.eventsSubject = eventsSubject
|
||||
|
||||
$text.map { !$0.isEmpty }
|
||||
.removeDuplicates()
|
||||
.combineLatest($attachmentViewModels.map { !$0.isEmpty })
|
||||
|
@ -45,7 +48,7 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
|||
}
|
||||
|
||||
public func attachmentSelected(viewModel: AttachmentViewModel) {
|
||||
|
||||
eventsSubject.send(.editAttachment(viewModel, self))
|
||||
}
|
||||
|
||||
public func removeAttachment(viewModel: AttachmentViewModel) {
|
||||
|
@ -56,14 +59,13 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
|||
public extension CompositionViewModel {
|
||||
static let maxCharacters = 500
|
||||
|
||||
typealias Id = UUID
|
||||
|
||||
enum Event {
|
||||
case insertAfter(CompositionViewModel)
|
||||
case presentMediaPicker(CompositionViewModel)
|
||||
case error(Error)
|
||||
case editAttachment(AttachmentViewModel, CompositionViewModel)
|
||||
case updateAttachment(AnyPublisher<Never, Error>)
|
||||
}
|
||||
|
||||
typealias Id = UUID
|
||||
|
||||
func components(inReplyToId: Status.Id?, visibility: Status.Visibility) -> StatusComponents {
|
||||
StatusComponents(
|
||||
inReplyToId: inReplyToId,
|
||||
|
@ -76,6 +78,23 @@ public extension CompositionViewModel {
|
|||
func cancelUpload() {
|
||||
attachmentUploadCancellable?.cancel()
|
||||
}
|
||||
|
||||
func update(attachmentViewModel: AttachmentViewModel) {
|
||||
let publisher = attachmentViewModel.updated()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents(receiveOutput: { [weak self] updatedAttachmentViewModel in
|
||||
guard let self = self,
|
||||
let index = self.attachmentViewModels.firstIndex(
|
||||
where: { $0.attachment.id == updatedAttachmentViewModel.attachment.id })
|
||||
else { return }
|
||||
|
||||
self.attachmentViewModels[index] = updatedAttachmentViewModel
|
||||
})
|
||||
.ignoreOutput()
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
eventsSubject.send(.updateAttachment(publisher))
|
||||
}
|
||||
}
|
||||
|
||||
extension CompositionViewModel {
|
||||
|
|
|
@ -7,7 +7,7 @@ import ServiceLayer
|
|||
|
||||
public final class NewStatusViewModel: ObservableObject {
|
||||
@Published public var visibility: Status.Visibility
|
||||
@Published public private(set) var compositionViewModels = [CompositionViewModel()]
|
||||
@Published public private(set) var compositionViewModels: [CompositionViewModel]
|
||||
@Published public private(set) var identification: Identification
|
||||
@Published public private(set) var authenticatedIdentities = [Identity]()
|
||||
@Published public var canPost = false
|
||||
|
@ -19,7 +19,7 @@ public final class NewStatusViewModel: ObservableObject {
|
|||
private let allIdentitiesService: AllIdentitiesService
|
||||
private let environment: AppEnvironment
|
||||
private let eventsSubject = PassthroughSubject<Event, Never>()
|
||||
private let itemEventsSubject = PassthroughSubject<CompositionViewModel.Event, Never>()
|
||||
private let compositionEventsSubject = PassthroughSubject<CompositionViewModel.Event, Never>()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
public init(allIdentitiesService: AllIdentitiesService,
|
||||
|
@ -28,6 +28,7 @@ public final class NewStatusViewModel: ObservableObject {
|
|||
self.allIdentitiesService = allIdentitiesService
|
||||
self.identification = identification
|
||||
self.environment = environment
|
||||
compositionViewModels = [CompositionViewModel(eventsSubject: compositionEventsSubject)]
|
||||
events = eventsSubject.eraseToAnyPublisher()
|
||||
visibility = identification.identity.preferences.postingDefaultVisibility
|
||||
allIdentitiesService.authenticatedIdentitiesPublisher()
|
||||
|
@ -39,6 +40,9 @@ public final class NewStatusViewModel: ObservableObject {
|
|||
.combineLatest($postingState)
|
||||
.map { $0 && $1 == .composing }
|
||||
.assign(to: &$canPost)
|
||||
compositionEventsSubject
|
||||
.sink { [weak self] in self?.handle(event: $0) }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +50,7 @@ public extension NewStatusViewModel {
|
|||
enum Event {
|
||||
case presentMediaPicker(CompositionViewModel)
|
||||
case presentCamera(CompositionViewModel)
|
||||
case editAttachment(AttachmentViewModel, CompositionViewModel)
|
||||
}
|
||||
|
||||
enum PostingState {
|
||||
|
@ -85,7 +90,7 @@ public extension NewStatusViewModel {
|
|||
guard let index = compositionViewModels.firstIndex(where: { $0 === after })
|
||||
else { return }
|
||||
|
||||
let newViewModel = CompositionViewModel()
|
||||
let newViewModel = CompositionViewModel(eventsSubject: compositionEventsSubject)
|
||||
|
||||
newViewModel.contentWarning = after.contentWarning
|
||||
newViewModel.displayContentWarning = after.displayContentWarning
|
||||
|
@ -109,6 +114,14 @@ public extension NewStatusViewModel {
|
|||
}
|
||||
|
||||
private extension NewStatusViewModel {
|
||||
func handle(event: CompositionViewModel.Event) {
|
||||
switch event {
|
||||
case let .editAttachment(attachmentViewModel, compositionViewModel):
|
||||
eventsSubject.send(.editAttachment(attachmentViewModel, compositionViewModel))
|
||||
case let .updateAttachment(publisher):
|
||||
publisher.assignErrorsToAlertItem(to: \.alertItem, on: self).sink { _ in }.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
func post(viewModel: CompositionViewModel, inReplyToId: Status.Id?) {
|
||||
postingState = .posting
|
||||
identification.service.post(statusComponents: viewModel.components(
|
||||
|
|
|
@ -47,29 +47,13 @@ final class AttachmentView: UIView {
|
|||
super.layoutSubviews()
|
||||
|
||||
if let focus = viewModel.attachment.meta?.focus {
|
||||
let viewsAndSizes: [(UIView, CGSize?)] = [
|
||||
let viewsAndMediaSizes: [(UIView, CGSize?)] = [
|
||||
(imageView, imageView.image?.size),
|
||||
(playerView, playerView.player?.currentItem?.presentationSize)]
|
||||
for (view, size) in viewsAndSizes {
|
||||
guard let size = size else { continue }
|
||||
for (view, mediaSize) in viewsAndMediaSizes {
|
||||
guard let size = mediaSize else { continue }
|
||||
|
||||
let aspectRatio = size.width / size.height
|
||||
let viewAspectRatio = view.frame.width / view.frame.height
|
||||
var origin = CGPoint.zero
|
||||
|
||||
if viewAspectRatio > aspectRatio {
|
||||
let mediaProportionalHeight = size.height * view.frame.width / size.width
|
||||
let maxPan = (mediaProportionalHeight - view.frame.height) / (2 * mediaProportionalHeight)
|
||||
|
||||
origin.y = CGFloat(-focus.y) * maxPan
|
||||
} else {
|
||||
let mediaProportionalWidth = size.width * view.frame.height / size.height
|
||||
let maxPan = (mediaProportionalWidth - view.frame.width) / (2 * mediaProportionalWidth)
|
||||
|
||||
origin.x = CGFloat(focus.x) * maxPan
|
||||
}
|
||||
|
||||
view.layer.contentsRect = .init(origin: origin, size: CGRect.defaultContentsRect.size)
|
||||
view.setContentsRect(focus: focus, mediaSize: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import SwiftUI
|
||||
import ViewModels
|
||||
|
||||
struct EditAttachmentView: UIViewControllerRepresentable {
|
||||
let viewModelsClosure: () -> (AttachmentViewModel, CompositionViewModel)
|
||||
|
||||
func makeUIViewController(context: Context) -> EditAttachmentViewController {
|
||||
let (attachmentViewModel, compositionViewModel) = viewModelsClosure()
|
||||
|
||||
return EditAttachmentViewController(viewModel: attachmentViewModel, parentViewModel: compositionViewModel)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: EditAttachmentViewController, context: Context) {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import Kingfisher
|
||||
import UIKit
|
||||
import ViewModels
|
||||
|
||||
final class EditThumbnailView: UIView {
|
||||
let playerView = PlayerView()
|
||||
let imageView = UIImageView()
|
||||
let previewImageView = UIImageView()
|
||||
let promptBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
||||
let thumbnailPromptLabel = UILabel()
|
||||
|
||||
private let viewModel: AttachmentViewModel
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private lazy var circleView: UIVisualEffectView = {
|
||||
let blurEffect = UIBlurEffect(style: .systemUltraThinMaterial)
|
||||
let circleView = UIVisualEffectView(effect: blurEffect)
|
||||
let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect))
|
||||
let scopeImageView = UIImageView(
|
||||
image: UIImage(systemName: "scope",
|
||||
withConfiguration: UIImage.SymbolConfiguration(scale: .medium)))
|
||||
|
||||
circleView.translatesAutoresizingMaskIntoConstraints = false
|
||||
vibrancyView.translatesAutoresizingMaskIntoConstraints = false
|
||||
scopeImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
vibrancyView.contentView.addSubview(scopeImageView)
|
||||
circleView.contentView.addSubview(vibrancyView)
|
||||
circleView.layer.cornerRadius = .minimumButtonDimension / 2
|
||||
circleView.clipsToBounds = true
|
||||
scopeImageView.contentMode = .scaleAspectFit
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
scopeImageView.centerXAnchor.constraint(equalTo: circleView.contentView.centerXAnchor),
|
||||
scopeImageView.centerYAnchor.constraint(equalTo: circleView.contentView.centerYAnchor),
|
||||
vibrancyView.leadingAnchor.constraint(equalTo: circleView.leadingAnchor),
|
||||
vibrancyView.topAnchor.constraint(equalTo: circleView.topAnchor),
|
||||
vibrancyView.trailingAnchor.constraint(equalTo: circleView.trailingAnchor),
|
||||
vibrancyView.bottomAnchor.constraint(equalTo: circleView.bottomAnchor),
|
||||
circleView.trailingAnchor.constraint(
|
||||
equalTo: scopeImageView.trailingAnchor, constant: .compactSpacing),
|
||||
circleView.bottomAnchor.constraint(
|
||||
equalTo: scopeImageView.bottomAnchor, constant: .compactSpacing),
|
||||
scopeImageView.topAnchor.constraint(
|
||||
equalTo: circleView.topAnchor, constant: .compactSpacing),
|
||||
scopeImageView.leadingAnchor.constraint(
|
||||
equalTo: circleView.leadingAnchor, constant: .compactSpacing),
|
||||
circleView.widthAnchor.constraint(equalToConstant: .minimumButtonDimension),
|
||||
circleView.heightAnchor.constraint(equalToConstant: .minimumButtonDimension)
|
||||
])
|
||||
|
||||
return circleView
|
||||
}()
|
||||
|
||||
init(viewModel: AttachmentViewModel) {
|
||||
self.viewModel = viewModel
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
initialSetup()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
guard let touch = touches.first else { return }
|
||||
|
||||
if promptBackgroundView.effect != nil {
|
||||
UIView.animate(withDuration: .defaultAnimationDuration) {
|
||||
self.promptBackgroundView.effect = nil
|
||||
self.thumbnailPromptLabel.alpha = 0
|
||||
}
|
||||
}
|
||||
|
||||
let location = touch.location(in: self)
|
||||
|
||||
viewModel.editingFocus.x = Double(max(min(((location.x - (bounds.width / 2)) / (bounds.width / 2)), 1), -1))
|
||||
viewModel.editingFocus.y = Double(max(min((-location.y / (bounds.height / 2)) + 1, 1), -1))
|
||||
}
|
||||
}
|
||||
|
||||
private extension EditThumbnailView {
|
||||
// swiftlint:disable:next function_body_length
|
||||
func initialSetup() {
|
||||
backgroundColor = .secondarySystemBackground
|
||||
|
||||
addSubview(imageView)
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.kf.indicatorType = .activity
|
||||
|
||||
addSubview(playerView)
|
||||
playerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
addSubview(circleView)
|
||||
|
||||
let circleViewCenterXConstraint = circleView.centerXAnchor.constraint(equalTo: centerXAnchor)
|
||||
let circleViewCenterYConstraint = circleView.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||
|
||||
addSubview(promptBackgroundView)
|
||||
promptBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
if viewModel.editingFocus != .default {
|
||||
promptBackgroundView.effect = nil
|
||||
}
|
||||
|
||||
promptBackgroundView.contentView.addSubview(thumbnailPromptLabel)
|
||||
thumbnailPromptLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
thumbnailPromptLabel.adjustsFontForContentSizeCategory = true
|
||||
thumbnailPromptLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||
thumbnailPromptLabel.numberOfLines = 0
|
||||
thumbnailPromptLabel.textAlignment = .center
|
||||
thumbnailPromptLabel.text = NSLocalizedString("attachment.edit.thumbnail.prompt", comment: "")
|
||||
|
||||
if viewModel.editingFocus != .default {
|
||||
thumbnailPromptLabel.alpha = 0
|
||||
}
|
||||
|
||||
let previewImageContainerView = UIView()
|
||||
|
||||
addSubview(previewImageContainerView)
|
||||
previewImageContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
previewImageContainerView.layer.cornerRadius = .defaultCornerRadius
|
||||
previewImageContainerView.layer.shadowOffset = .zero
|
||||
previewImageContainerView.layer.shadowRadius = .defaultShadowRadius
|
||||
previewImageContainerView.layer.shadowOpacity = 0.25
|
||||
|
||||
previewImageContainerView.addSubview(previewImageView)
|
||||
previewImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
previewImageView.contentMode = .scaleAspectFill
|
||||
previewImageView.clipsToBounds = true
|
||||
previewImageView.layer.cornerRadius = .defaultCornerRadius
|
||||
previewImageView.kf.setImage(with: viewModel.attachment.previewUrl)
|
||||
|
||||
switch viewModel.attachment.type {
|
||||
case .image:
|
||||
playerView.isHidden = true
|
||||
imageView.kf.setImage(
|
||||
with: viewModel.attachment.previewUrl,
|
||||
options: [.onlyFromCache],
|
||||
completionHandler: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if case .success = $0 {
|
||||
self.imageView.kf.indicatorType = .none
|
||||
}
|
||||
|
||||
self.imageView.kf.setImage(
|
||||
with: self.viewModel.attachment.url,
|
||||
options: [.keepCurrentImageWhileLoading])
|
||||
})
|
||||
case .gifv:
|
||||
imageView.isHidden = true
|
||||
let player = PlayerCache.shared.player(url: viewModel.attachment.url)
|
||||
|
||||
player.isMuted = true
|
||||
|
||||
playerView.player = player
|
||||
player.play()
|
||||
default: break
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
imageView.topAnchor.constraint(equalTo: topAnchor),
|
||||
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
playerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
playerView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
playerView.topAnchor.constraint(equalTo: topAnchor),
|
||||
playerView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
circleViewCenterXConstraint,
|
||||
circleViewCenterYConstraint,
|
||||
promptBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
promptBackgroundView.topAnchor.constraint(equalTo: topAnchor),
|
||||
promptBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
thumbnailPromptLabel.leadingAnchor.constraint(
|
||||
equalTo: promptBackgroundView.layoutMarginsGuide.leadingAnchor),
|
||||
thumbnailPromptLabel.topAnchor.constraint(equalTo: promptBackgroundView.layoutMarginsGuide.topAnchor),
|
||||
thumbnailPromptLabel.trailingAnchor.constraint(
|
||||
equalTo: promptBackgroundView.layoutMarginsGuide.trailingAnchor),
|
||||
thumbnailPromptLabel.bottomAnchor.constraint(equalTo: promptBackgroundView.layoutMarginsGuide.bottomAnchor),
|
||||
previewImageView.leadingAnchor.constraint(equalTo: previewImageContainerView.leadingAnchor),
|
||||
previewImageView.topAnchor.constraint(equalTo: previewImageContainerView.topAnchor),
|
||||
previewImageView.trailingAnchor.constraint(equalTo: previewImageContainerView.trailingAnchor),
|
||||
previewImageView.bottomAnchor.constraint(equalTo: previewImageContainerView.bottomAnchor),
|
||||
previewImageContainerView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||
previewImageContainerView.bottomAnchor.constraint(
|
||||
equalTo: layoutMarginsGuide.bottomAnchor,
|
||||
constant: -.defaultSpacing),
|
||||
previewImageContainerView.widthAnchor.constraint(
|
||||
equalTo: previewImageContainerView.heightAnchor,
|
||||
multiplier: 16 / 9),
|
||||
previewImageContainerView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1 / 8)
|
||||
])
|
||||
|
||||
viewModel.$editingFocus
|
||||
.receive(on: DispatchQueue.main) // punt to next run loop to allow initial layout to happen
|
||||
.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
circleViewCenterXConstraint.constant = CGFloat($0.x) * self.bounds.width / 2
|
||||
circleViewCenterYConstraint.constant = -CGFloat($0.y) * self.bounds.height / 2
|
||||
|
||||
guard let mediaSize = self.previewImageView.image?.size else { return }
|
||||
|
||||
self.previewImageView.setContentsRect(focus: $0, mediaSize: mediaSize)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
|
@ -181,7 +181,7 @@ private extension TabNavigationView {
|
|||
.clipShape(Circle())
|
||||
.frame(width: .newStatusButtonDimension,
|
||||
height: .newStatusButtonDimension)
|
||||
.shadow(radius: .newStatusButtonShadowRadius)
|
||||
.shadow(radius: .defaultShadowRadius)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ extension CGFloat {
|
|||
static let minimumButtonDimension: Self = 44
|
||||
static let barButtonItemDimension: Self = 28
|
||||
static let newStatusButtonDimension: CGFloat = 54
|
||||
static let newStatusButtonShadowRadius: CGFloat = 2
|
||||
static let defaultShadowRadius: CGFloat = 2
|
||||
}
|
||||
|
||||
extension CGRect {
|
||||
|
|
Loading…
Reference in New Issue