mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-22 00:01:00 +00:00
wip
This commit is contained in:
parent
5ae1d9be9a
commit
4806c43202
11 changed files with 122 additions and 44 deletions
|
@ -9,10 +9,11 @@ final class CompositionAttachmentsDataSource: UICollectionViewDiffableDataSource
|
||||||
DispatchQueue(label: "com.metabolist.metatext.composition-attachments-data-source.update-queue")
|
DispatchQueue(label: "com.metabolist.metatext.composition-attachments-data-source.update-queue")
|
||||||
|
|
||||||
init(collectionView: UICollectionView,
|
init(collectionView: UICollectionView,
|
||||||
viewModelProvider: @escaping (IndexPath) -> CompositionAttachmentViewModel?) {
|
viewModelProvider: @escaping (IndexPath) -> (CompositionAttachmentViewModel, CompositionViewModel)) {
|
||||||
let registration = UICollectionView.CellRegistration
|
let registration = UICollectionView.CellRegistration
|
||||||
<CompositionAttachmentCollectionViewCell, CompositionAttachmentViewModel> {
|
<CompositionAttachmentCollectionViewCell, (CompositionAttachmentViewModel, CompositionViewModel)> {
|
||||||
$0.viewModel = $2
|
$0.viewModel = $2.0
|
||||||
|
$0.parentViewModel = $2.1
|
||||||
}
|
}
|
||||||
|
|
||||||
super.init(collectionView: collectionView) { collectionView, indexPath, _ in
|
super.init(collectionView: collectionView) { collectionView, indexPath, _ in
|
||||||
|
|
|
@ -38,6 +38,8 @@
|
||||||
"camera-access.description" = "Open system settings to allow camera access";
|
"camera-access.description" = "Open system settings to allow camera access";
|
||||||
"camera-access.open-system-settings" = "Open system settings";
|
"camera-access.open-system-settings" = "Open system settings";
|
||||||
"cancel" = "Cancel";
|
"cancel" = "Cancel";
|
||||||
|
"compose.attachment.uploading" = "Uploading";
|
||||||
|
"compose.attachment.remove" = "Remove";
|
||||||
"error" = "Error";
|
"error" = "Error";
|
||||||
"favorites" = "Favorites";
|
"favorites" = "Favorites";
|
||||||
"registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";
|
"registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";
|
||||||
|
|
|
@ -44,4 +44,19 @@ public struct Attachment: Codable, Hashable {
|
||||||
|
|
||||||
public extension Attachment {
|
public extension Attachment {
|
||||||
typealias Id = String
|
typealias Id = String
|
||||||
|
|
||||||
|
var aspectRatio: Double? {
|
||||||
|
if
|
||||||
|
let info = meta?.original,
|
||||||
|
let width = info.width,
|
||||||
|
let height = info.height,
|
||||||
|
width != 0,
|
||||||
|
height != 0 {
|
||||||
|
let aspectRatio = Double(width) / Double(height)
|
||||||
|
|
||||||
|
return aspectRatio.isNaN ? nil : aspectRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,21 +22,6 @@ public extension AttachmentViewModel {
|
||||||
attachment.id.appending(status.id).hashValue
|
attachment.id.appending(status.id).hashValue
|
||||||
}
|
}
|
||||||
|
|
||||||
var aspectRatio: Double? {
|
|
||||||
if
|
|
||||||
let info = attachment.meta?.original,
|
|
||||||
let width = info.width,
|
|
||||||
let height = info.height,
|
|
||||||
width != 0,
|
|
||||||
height != 0 {
|
|
||||||
let aspectRatio = Double(width) / Double(height)
|
|
||||||
|
|
||||||
return aspectRatio.isNaN ? nil : aspectRatio
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var shouldAutoplay: Bool {
|
var shouldAutoplay: Bool {
|
||||||
switch attachment.type {
|
switch attachment.type {
|
||||||
case .video:
|
case .video:
|
||||||
|
|
|
@ -46,8 +46,8 @@ public extension CompositionViewModel {
|
||||||
visibility: visibility)
|
visibility: visibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
func attachmentViewModel(indexPath: IndexPath) -> CompositionAttachmentViewModel {
|
func remove(attachmentViewModel: CompositionAttachmentViewModel) {
|
||||||
attachmentViewModels[indexPath.item]
|
attachmentViewModels.removeAll { $0 === attachmentViewModel }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import UIKit
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
final class AttachmentUploadView: UIView {
|
final class AttachmentUploadView: UIView {
|
||||||
|
let label = UILabel()
|
||||||
let progressView = UIProgressView(progressViewStyle: .default)
|
let progressView = UIProgressView(progressViewStyle: .default)
|
||||||
private var progressCancellable: AnyCancellable?
|
private var progressCancellable: AnyCancellable?
|
||||||
|
|
||||||
|
@ -24,13 +25,26 @@ final class AttachmentUploadView: UIView {
|
||||||
init() {
|
init() {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
addSubview(label)
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
label.font = .preferredFont(forTextStyle: .callout)
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.text = NSLocalizedString("compose.attachment.uploading", comment: "")
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
label.numberOfLines = 0
|
||||||
|
|
||||||
addSubview(progressView)
|
addSubview(progressView)
|
||||||
progressView.translatesAutoresizingMaskIntoConstraints = false
|
progressView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
label.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||||
|
label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
||||||
|
label.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
|
progressView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: .defaultSpacing),
|
||||||
progressView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
progressView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||||
progressView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
progressView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
progressView.centerYAnchor.constraint(equalTo: centerYAnchor)
|
progressView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,15 @@ import ViewModels
|
||||||
|
|
||||||
class CompositionAttachmentCollectionViewCell: UICollectionViewCell {
|
class CompositionAttachmentCollectionViewCell: UICollectionViewCell {
|
||||||
var viewModel: CompositionAttachmentViewModel?
|
var viewModel: CompositionAttachmentViewModel?
|
||||||
|
var parentViewModel: CompositionViewModel?
|
||||||
|
|
||||||
override func updateConfiguration(using state: UICellConfigurationState) {
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
guard let viewModel = viewModel else { return }
|
guard let viewModel = viewModel, let parentViewModel = parentViewModel else { return }
|
||||||
|
|
||||||
contentConfiguration = CompositionAttachmentContentConfiguration(viewModel: viewModel).updated(for: state)
|
contentConfiguration = CompositionAttachmentContentConfiguration(
|
||||||
|
viewModel: viewModel,
|
||||||
|
parentViewModel: parentViewModel)
|
||||||
|
.updated(for: state)
|
||||||
backgroundConfiguration = UIBackgroundConfiguration.clear().updated(for: state)
|
backgroundConfiguration = UIBackgroundConfiguration.clear().updated(for: state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import ViewModels
|
||||||
|
|
||||||
struct CompositionAttachmentContentConfiguration {
|
struct CompositionAttachmentContentConfiguration {
|
||||||
let viewModel: CompositionAttachmentViewModel
|
let viewModel: CompositionAttachmentViewModel
|
||||||
|
let parentViewModel: CompositionViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CompositionAttachmentContentConfiguration: UIContentConfiguration {
|
extension CompositionAttachmentContentConfiguration: UIContentConfiguration {
|
||||||
|
|
|
@ -6,11 +6,16 @@ import ViewModels
|
||||||
|
|
||||||
class CompositionAttachmentView: UIView {
|
class CompositionAttachmentView: UIView {
|
||||||
let imageView = UIImageView()
|
let imageView = UIImageView()
|
||||||
|
let removeButton = UIButton()
|
||||||
|
let editButton = UIButton()
|
||||||
private var compositionAttachmentConfiguration: CompositionAttachmentContentConfiguration
|
private var compositionAttachmentConfiguration: CompositionAttachmentContentConfiguration
|
||||||
|
private var aspectRatioConstraint: NSLayoutConstraint
|
||||||
|
|
||||||
init(configuration: CompositionAttachmentContentConfiguration) {
|
init(configuration: CompositionAttachmentContentConfiguration) {
|
||||||
self.compositionAttachmentConfiguration = configuration
|
self.compositionAttachmentConfiguration = configuration
|
||||||
|
|
||||||
|
aspectRatioConstraint = imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 2)
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
initialSetup()
|
initialSetup()
|
||||||
|
@ -38,22 +43,70 @@ extension CompositionAttachmentView: UIContentView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension CompositionAttachmentView {
|
private extension CompositionAttachmentView {
|
||||||
|
// swiftlint:disable:next function_body_length
|
||||||
func initialSetup() {
|
func initialSetup() {
|
||||||
|
backgroundColor = .secondarySystemBackground
|
||||||
|
layer.cornerRadius = .defaultCornerRadius
|
||||||
|
clipsToBounds = true
|
||||||
|
|
||||||
addSubview(imageView)
|
addSubview(imageView)
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
imageView.layer.cornerRadius = .defaultCornerRadius
|
imageView.kf.indicatorType = .activity
|
||||||
imageView.clipsToBounds = true
|
|
||||||
|
addSubview(removeButton)
|
||||||
|
removeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
removeButton.setImage(
|
||||||
|
UIImage(
|
||||||
|
systemName: "xmark.circle.fill",
|
||||||
|
withConfiguration: UIImage.SymbolConfiguration(scale: .large)),
|
||||||
|
for: .normal)
|
||||||
|
removeButton.showsMenuAsPrimaryAction = true
|
||||||
|
removeButton.menu = UIMenu(
|
||||||
|
children: [
|
||||||
|
UIAction(
|
||||||
|
title: NSLocalizedString("compose.attachment.remove", comment: ""),
|
||||||
|
image: UIImage(systemName: "xmark.circle.fill"),
|
||||||
|
attributes: .destructive, handler: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.compositionAttachmentConfiguration.parentViewModel.remove(
|
||||||
|
attachmentViewModel: self.compositionAttachmentConfiguration.viewModel)
|
||||||
|
})])
|
||||||
|
|
||||||
|
addSubview(editButton)
|
||||||
|
editButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
editButton.setImage(
|
||||||
|
UIImage(
|
||||||
|
systemName: "pencil.circle.fill",
|
||||||
|
withConfiguration: UIImage.SymbolConfiguration(scale: .large)),
|
||||||
|
for: .normal)
|
||||||
|
editButton.addAction(UIAction { [weak self] _ in }, for: .touchUpInside)
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
aspectRatioConstraint,
|
||||||
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
imageView.topAnchor.constraint(equalTo: topAnchor),
|
imageView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
imageView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
removeButton.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
removeButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
removeButton.heightAnchor.constraint(equalToConstant: .minimumButtonDimension),
|
||||||
|
removeButton.widthAnchor.constraint(equalToConstant: .minimumButtonDimension),
|
||||||
|
editButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
editButton.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
editButton.heightAnchor.constraint(equalToConstant: .minimumButtonDimension),
|
||||||
|
editButton.widthAnchor.constraint(equalToConstant: .minimumButtonDimension)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyCompositionAttachmentConfiguration() {
|
func applyCompositionAttachmentConfiguration() {
|
||||||
imageView.kf.setImage(with: compositionAttachmentConfiguration.viewModel.attachment.previewUrl)
|
imageView.kf.setImage(with: compositionAttachmentConfiguration.viewModel.attachment.previewUrl)
|
||||||
|
aspectRatioConstraint.isActive = false
|
||||||
|
aspectRatioConstraint = imageView.widthAnchor.constraint(
|
||||||
|
equalTo: imageView.heightAnchor,
|
||||||
|
multiplier: CGFloat(compositionAttachmentConfiguration.viewModel.attachment.aspectRatio ?? 1))
|
||||||
|
aspectRatioConstraint.priority = .justBelowMax
|
||||||
|
aspectRatioConstraint.isActive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,10 @@ final class CompositionView: UIView {
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
private lazy var attachmentsDataSource: CompositionAttachmentsDataSource = {
|
private lazy var attachmentsDataSource: CompositionAttachmentsDataSource = {
|
||||||
CompositionAttachmentsDataSource(
|
let vm = viewModel
|
||||||
collectionView: attachmentsCollectionView) { [weak self] in
|
|
||||||
self?.viewModel.attachmentViewModel(indexPath: $0)
|
return .init(collectionView: attachmentsCollectionView) {
|
||||||
|
(vm.attachmentViewModels[$0.item], vm)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -28,20 +29,25 @@ final class CompositionView: UIView {
|
||||||
self.parentViewModel = parentViewModel
|
self.parentViewModel = parentViewModel
|
||||||
|
|
||||||
let itemSize = NSCollectionLayoutSize(
|
let itemSize = NSCollectionLayoutSize(
|
||||||
widthDimension: .fractionalWidth(0.2),
|
widthDimension: .estimated(Self.attachmentCollectionViewHeight),
|
||||||
heightDimension: .fractionalHeight(1.0))
|
heightDimension: .fractionalHeight(1))
|
||||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
let groupSize = NSCollectionLayoutSize(
|
let groupSize = NSCollectionLayoutSize(
|
||||||
widthDimension: .fractionalWidth(1.0),
|
widthDimension: .estimated(Self.attachmentCollectionViewHeight),
|
||||||
heightDimension: .fractionalWidth(0.2))
|
heightDimension: .fractionalHeight(1))
|
||||||
let group = NSCollectionLayoutGroup.horizontal(
|
let group = NSCollectionLayoutGroup.horizontal(
|
||||||
layoutSize: groupSize,
|
layoutSize: groupSize,
|
||||||
subitems: [item])
|
subitems: [item])
|
||||||
|
|
||||||
group.interItemSpacing = .fixed(.defaultSpacing)
|
|
||||||
|
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
let attachmentsLayout = UICollectionViewCompositionalLayout(section: section)
|
|
||||||
|
section.interGroupSpacing = .defaultSpacing
|
||||||
|
|
||||||
|
let configuration = UICollectionViewCompositionalLayoutConfiguration()
|
||||||
|
|
||||||
|
configuration.scrollDirection = .horizontal
|
||||||
|
|
||||||
|
let attachmentsLayout = UICollectionViewCompositionalLayout(section: section, configuration: configuration)
|
||||||
|
|
||||||
attachmentsCollectionView = UICollectionView(frame: .zero, collectionViewLayout: attachmentsLayout)
|
attachmentsCollectionView = UICollectionView(frame: .zero, collectionViewLayout: attachmentsLayout)
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
@ -66,7 +72,7 @@ extension CompositionView: UITextViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension CompositionView {
|
private extension CompositionView {
|
||||||
static let attachmentUploadViewHeight: CGFloat = 100
|
static let attachmentCollectionViewHeight: CGFloat = 200
|
||||||
|
|
||||||
// swiftlint:disable:next function_body_length
|
// swiftlint:disable:next function_body_length
|
||||||
func initialSetup() {
|
func initialSetup() {
|
||||||
|
@ -114,6 +120,7 @@ private extension CompositionView {
|
||||||
|
|
||||||
stackView.addArrangedSubview(attachmentsCollectionView)
|
stackView.addArrangedSubview(attachmentsCollectionView)
|
||||||
attachmentsCollectionView.dataSource = attachmentsDataSource
|
attachmentsCollectionView.dataSource = attachmentsDataSource
|
||||||
|
attachmentsCollectionView.backgroundColor = .clear
|
||||||
|
|
||||||
stackView.addArrangedSubview(attachmentUploadView)
|
stackView.addArrangedSubview(attachmentUploadView)
|
||||||
|
|
||||||
|
@ -139,7 +146,6 @@ private extension CompositionView {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
viewModel.$attachmentViewModels
|
viewModel.$attachmentViewModels
|
||||||
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
|
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
self?.attachmentsDataSource.apply($0.map(\.attachment).snapshot())
|
self?.attachmentsDataSource.apply($0.map(\.attachment).snapshot())
|
||||||
self?.attachmentsCollectionView.isHidden = $0.isEmpty
|
self?.attachmentsCollectionView.isHidden = $0.isEmpty
|
||||||
|
@ -161,10 +167,7 @@ private extension CompositionView {
|
||||||
stackView.topAnchor.constraint(equalTo: guide.topAnchor),
|
stackView.topAnchor.constraint(equalTo: guide.topAnchor),
|
||||||
stackView.trailingAnchor.constraint(equalTo: guide.trailingAnchor),
|
stackView.trailingAnchor.constraint(equalTo: guide.trailingAnchor),
|
||||||
stackView.bottomAnchor.constraint(lessThanOrEqualTo: guide.bottomAnchor),
|
stackView.bottomAnchor.constraint(lessThanOrEqualTo: guide.bottomAnchor),
|
||||||
attachmentsCollectionView.heightAnchor.constraint(
|
attachmentsCollectionView.heightAnchor.constraint(equalToConstant: Self.attachmentCollectionViewHeight)
|
||||||
equalTo: attachmentsCollectionView.widthAnchor,
|
|
||||||
multiplier: 1 / 4),
|
|
||||||
attachmentUploadView.heightAnchor.constraint(equalToConstant: Self.attachmentUploadViewHeight)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||||
|
|
|
@ -47,7 +47,7 @@ final class StatusAttachmentsView: UIView {
|
||||||
|
|
||||||
let newAspectRatio: CGFloat
|
let newAspectRatio: CGFloat
|
||||||
|
|
||||||
if attachmentCount == 1, let aspectRatio = attachmentViewModels.first?.aspectRatio {
|
if attachmentCount == 1, let aspectRatio = attachmentViewModels.first?.attachment.aspectRatio {
|
||||||
newAspectRatio = max(CGFloat(aspectRatio), 16 / 9)
|
newAspectRatio = max(CGFloat(aspectRatio), 16 / 9)
|
||||||
} else {
|
} else {
|
||||||
newAspectRatio = 16 / 9
|
newAspectRatio = 16 / 9
|
||||||
|
|
Loading…
Reference in a new issue