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.join" = "Join";
|
||||||
"add-identity.request-invite" = "Request an invite";
|
"add-identity.request-invite" = "Request an invite";
|
||||||
"add-identity.unable-to-connect-to-instance" = "Unable to connect to instance";
|
"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.sensitive-content" = "Sensitive content";
|
||||||
"attachment.media-hidden" = "Media hidden";
|
"attachment.media-hidden" = "Media hidden";
|
||||||
"bookmarks" = "Bookmarks";
|
"bookmarks" = "Bookmarks";
|
||||||
|
|
|
@ -22,8 +22,8 @@ public struct Attachment: Codable, Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Focus: Codable, Hashable {
|
public struct Focus: Codable, Hashable {
|
||||||
public let x: Double
|
public var x: Double
|
||||||
public let y: Double
|
public var y: Double
|
||||||
}
|
}
|
||||||
|
|
||||||
public let original: Info?
|
public let original: Info?
|
||||||
|
@ -60,3 +60,7 @@ public extension Attachment {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension Attachment.Meta.Focus {
|
||||||
|
static let `default` = Self(x: 0, y: 0)
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Mastodon
|
||||||
|
|
||||||
public enum AttachmentEndpoint {
|
public enum AttachmentEndpoint {
|
||||||
case create(data: Data, mimeType: String, description: String?, focus: Attachment.Meta.Focus?)
|
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 {
|
extension AttachmentEndpoint: Endpoint {
|
||||||
|
@ -19,6 +20,8 @@ extension AttachmentEndpoint: Endpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case .create:
|
case .create:
|
||||||
return []
|
return []
|
||||||
|
case let .update(id, _, _):
|
||||||
|
return [id]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +40,18 @@ extension AttachmentEndpoint: Endpoint {
|
||||||
params["focus"] = .string("\(focus.x),\(focus.y)")
|
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
|
return params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +60,8 @@ extension AttachmentEndpoint: Endpoint {
|
||||||
switch self {
|
switch self {
|
||||||
case .create:
|
case .create:
|
||||||
return .post
|
return .post
|
||||||
|
case .update:
|
||||||
|
return .put
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,14 @@
|
||||||
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; };
|
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B1B29253818F3008F964B /* MediaPreferencesView.swift */; };
|
||||||
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; };
|
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */; };
|
||||||
D04F9E8E259E9C950081B0C9 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D04F9E8D259E9C950081B0C9 /* ViewModels */; };
|
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 */; };
|
D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; };
|
||||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; };
|
||||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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; };
|
D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -442,7 +454,9 @@
|
||||||
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
||||||
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
||||||
D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */,
|
D08E52602579D2E100FA2C5F /* DomainBlocksView.swift */,
|
||||||
|
D05936E825AA3F3D00754FDF /* EditAttachmentView.swift */,
|
||||||
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
||||||
|
D05936DD25A937EC00754FDF /* EditThumbnailView.swift */,
|
||||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||||
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */,
|
||||||
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */,
|
||||||
|
@ -482,6 +496,7 @@
|
||||||
D0C7D43024F76169001EBDBB /* View Controllers */ = {
|
D0C7D43024F76169001EBDBB /* View Controllers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D05936CE25A8D79800754FDF /* EditAttachmentViewController.swift */,
|
||||||
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */,
|
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */,
|
||||||
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */,
|
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */,
|
||||||
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
|
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
|
||||||
|
@ -522,6 +537,7 @@
|
||||||
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
|
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */,
|
||||||
D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */,
|
D08E512025786A6600FA2C5F /* UIButton+Extensions.swift */,
|
||||||
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
|
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */,
|
||||||
|
D05936F325AA66A600754FDF /* UIView+Extensions.swift */,
|
||||||
D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */,
|
D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */,
|
||||||
D0030981250C6C8500EACB32 /* URL+Extensions.swift */,
|
D0030981250C6C8500EACB32 /* URL+Extensions.swift */,
|
||||||
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */,
|
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */,
|
||||||
|
@ -760,6 +776,7 @@
|
||||||
D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */,
|
D08E52EE257D757100FA2C5F /* CompositionView.swift in Sources */,
|
||||||
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
||||||
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
||||||
|
D05936DE25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
||||||
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
||||||
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */,
|
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */,
|
||||||
D0E7AD3925870B13005F5E2D /* UIVIewController+Extensions.swift in Sources */,
|
D0E7AD3925870B13005F5E2D /* UIVIewController+Extensions.swift in Sources */,
|
||||||
|
@ -789,6 +806,7 @@
|
||||||
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
|
||||||
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
||||||
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
||||||
|
D05936CF25A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
||||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||||
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
|
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
|
||||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
||||||
|
@ -803,6 +821,8 @@
|
||||||
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
|
||||||
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
|
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
|
||||||
D08E512125786A6600FA2C5F /* UIButton+Extensions.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 */,
|
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
||||||
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
|
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
|
||||||
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
|
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
|
||||||
|
@ -847,20 +867,24 @@
|
||||||
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */,
|
D08E52A6257C61C000FA2C5F /* ShareExtensionNavigationViewController.swift in Sources */,
|
||||||
D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */,
|
D08E52D2257C811200FA2C5F /* ShareExtensionError+Extensions.swift in Sources */,
|
||||||
D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
|
D0E9F9AB258450B300EF503D /* CompositionInputAccessoryView.swift in Sources */,
|
||||||
|
D05936D025A8D79800754FDF /* EditAttachmentViewController.swift in Sources */,
|
||||||
D038273C259EA38F00056E0F /* NewStatusView.swift in Sources */,
|
D038273C259EA38F00056E0F /* NewStatusView.swift in Sources */,
|
||||||
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */,
|
D08E52EF257D757100FA2C5F /* CompositionView.swift in Sources */,
|
||||||
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
D0CE9F88258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
||||||
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
|
D08E52C7257C7AEE00FA2C5F /* ShareErrorViewController.swift in Sources */,
|
||||||
D015B14425A812F6006D88A8 /* PlayerCache.swift in Sources */,
|
D015B14425A812F6006D88A8 /* PlayerCache.swift in Sources */,
|
||||||
|
D05936F525AA66A600754FDF /* UIView+Extensions.swift in Sources */,
|
||||||
D015B13F25A812EC006D88A8 /* PlayerView.swift in Sources */,
|
D015B13F25A812EC006D88A8 /* PlayerView.swift in Sources */,
|
||||||
D015B13A25A812E6006D88A8 /* AttachmentView.swift in Sources */,
|
D015B13A25A812E6006D88A8 /* AttachmentView.swift in Sources */,
|
||||||
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
|
D08E52F8257D78BE00FA2C5F /* ViewConstants.swift in Sources */,
|
||||||
D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */,
|
D036EBC2259FE2AD00EC1CFC /* UIVIewController+Extensions.swift in Sources */,
|
||||||
D015B13525A812DD006D88A8 /* AttachmentsView.swift in Sources */,
|
D015B13525A812DD006D88A8 /* AttachmentsView.swift in Sources */,
|
||||||
|
D05936EA25AA3F3D00754FDF /* EditAttachmentView.swift in Sources */,
|
||||||
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
|
D0FCC106259C4E62000B67DF /* NewStatusViewController.swift in Sources */,
|
||||||
D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */,
|
D036EBBD259FE2A100EC1CFC /* Array+Extensions.swift in Sources */,
|
||||||
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
|
D036EBB3259FE28800EC1CFC /* UIColor+Extensions.swift in Sources */,
|
||||||
D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */,
|
D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */,
|
||||||
|
D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -212,6 +212,12 @@ public extension IdentityService {
|
||||||
progress: progress)
|
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> {
|
func post(statusComponents: StatusComponents) -> AnyPublisher<Status.Id, Error> {
|
||||||
mastodonAPIClient.request(StatusEndpoint.post(statusComponents)).map(\.id).eraseToAnyPublisher()
|
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 Combine
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
import UIKit
|
import SwiftUI
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
|
||||||
|
@ -110,6 +110,10 @@ private extension NewStatusViewController {
|
||||||
#if !IS_SHARE_EXTENSION
|
#if !IS_SHARE_EXTENSION
|
||||||
presentCamera(compositionViewModel: compositionViewModel)
|
presentCamera(compositionViewModel: compositionViewModel)
|
||||||
#endif
|
#endif
|
||||||
|
case let .editAttachment(attachmentViewModel, compositionViewModel):
|
||||||
|
presentAttachmentEditor(
|
||||||
|
attachmentViewModel: attachmentViewModel,
|
||||||
|
compositionViewModel: compositionViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,6 +280,15 @@ private extension NewStatusViewController {
|
||||||
}
|
}
|
||||||
#endif
|
#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 {
|
func changeIdentityButton(identification: Identification) -> UIButton {
|
||||||
let changeIdentityButton = UIButton()
|
let changeIdentityButton = UIButton()
|
||||||
let downsampled = KingfisherOptionsInfo.downsampled(
|
let downsampled = KingfisherOptionsInfo.downsampled(
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import Network
|
import Network
|
||||||
|
|
||||||
public final class AttachmentViewModel: ObservableObject {
|
public final class AttachmentViewModel: ObservableObject {
|
||||||
public let attachment: Attachment
|
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 identification: Identification
|
||||||
private let status: Status?
|
private let status: Status?
|
||||||
|
@ -14,6 +18,11 @@ public final class AttachmentViewModel: ObservableObject {
|
||||||
self.attachment = attachment
|
self.attachment = attachment
|
||||||
self.identification = identification
|
self.identification = identification
|
||||||
self.status = status
|
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 {
|
private extension AttachmentViewModel {
|
||||||
|
static let descriptionMaxCharacters = 1500
|
||||||
|
|
||||||
static var wifiMonitor: NWPathMonitor = {
|
static var wifiMonitor: NWPathMonitor = {
|
||||||
let monitor = NWPathMonitor(requiredInterfaceType: .wifi)
|
let monitor = NWPathMonitor(requiredInterfaceType: .wifi)
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,12 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
@Published public private(set) var remainingCharacters = CompositionViewModel.maxCharacters
|
@Published public private(set) var remainingCharacters = CompositionViewModel.maxCharacters
|
||||||
public let canRemoveAttachments = true
|
public let canRemoveAttachments = true
|
||||||
|
|
||||||
|
private let eventsSubject: PassthroughSubject<Event, Never>
|
||||||
private var attachmentUploadCancellable: AnyCancellable?
|
private var attachmentUploadCancellable: AnyCancellable?
|
||||||
|
|
||||||
init() {
|
init(eventsSubject: PassthroughSubject<Event, Never>) {
|
||||||
|
self.eventsSubject = eventsSubject
|
||||||
|
|
||||||
$text.map { !$0.isEmpty }
|
$text.map { !$0.isEmpty }
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.combineLatest($attachmentViewModels.map { !$0.isEmpty })
|
.combineLatest($attachmentViewModels.map { !$0.isEmpty })
|
||||||
|
@ -45,7 +48,7 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
}
|
}
|
||||||
|
|
||||||
public func attachmentSelected(viewModel: AttachmentViewModel) {
|
public func attachmentSelected(viewModel: AttachmentViewModel) {
|
||||||
|
eventsSubject.send(.editAttachment(viewModel, self))
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeAttachment(viewModel: AttachmentViewModel) {
|
public func removeAttachment(viewModel: AttachmentViewModel) {
|
||||||
|
@ -56,14 +59,13 @@ public final class CompositionViewModel: AttachmentsRenderingViewModel, Observab
|
||||||
public extension CompositionViewModel {
|
public extension CompositionViewModel {
|
||||||
static let maxCharacters = 500
|
static let maxCharacters = 500
|
||||||
|
|
||||||
typealias Id = UUID
|
|
||||||
|
|
||||||
enum Event {
|
enum Event {
|
||||||
case insertAfter(CompositionViewModel)
|
case editAttachment(AttachmentViewModel, CompositionViewModel)
|
||||||
case presentMediaPicker(CompositionViewModel)
|
case updateAttachment(AnyPublisher<Never, Error>)
|
||||||
case error(Error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typealias Id = UUID
|
||||||
|
|
||||||
func components(inReplyToId: Status.Id?, visibility: Status.Visibility) -> StatusComponents {
|
func components(inReplyToId: Status.Id?, visibility: Status.Visibility) -> StatusComponents {
|
||||||
StatusComponents(
|
StatusComponents(
|
||||||
inReplyToId: inReplyToId,
|
inReplyToId: inReplyToId,
|
||||||
|
@ -76,6 +78,23 @@ public extension CompositionViewModel {
|
||||||
func cancelUpload() {
|
func cancelUpload() {
|
||||||
attachmentUploadCancellable?.cancel()
|
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 {
|
extension CompositionViewModel {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ServiceLayer
|
||||||
|
|
||||||
public final class NewStatusViewModel: ObservableObject {
|
public final class NewStatusViewModel: ObservableObject {
|
||||||
@Published public var visibility: Status.Visibility
|
@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 identification: Identification
|
||||||
@Published public private(set) var authenticatedIdentities = [Identity]()
|
@Published public private(set) var authenticatedIdentities = [Identity]()
|
||||||
@Published public var canPost = false
|
@Published public var canPost = false
|
||||||
|
@ -19,7 +19,7 @@ public final class NewStatusViewModel: ObservableObject {
|
||||||
private let allIdentitiesService: AllIdentitiesService
|
private let allIdentitiesService: AllIdentitiesService
|
||||||
private let environment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
private let eventsSubject = PassthroughSubject<Event, Never>()
|
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>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
public init(allIdentitiesService: AllIdentitiesService,
|
public init(allIdentitiesService: AllIdentitiesService,
|
||||||
|
@ -28,6 +28,7 @@ public final class NewStatusViewModel: ObservableObject {
|
||||||
self.allIdentitiesService = allIdentitiesService
|
self.allIdentitiesService = allIdentitiesService
|
||||||
self.identification = identification
|
self.identification = identification
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
|
compositionViewModels = [CompositionViewModel(eventsSubject: compositionEventsSubject)]
|
||||||
events = eventsSubject.eraseToAnyPublisher()
|
events = eventsSubject.eraseToAnyPublisher()
|
||||||
visibility = identification.identity.preferences.postingDefaultVisibility
|
visibility = identification.identity.preferences.postingDefaultVisibility
|
||||||
allIdentitiesService.authenticatedIdentitiesPublisher()
|
allIdentitiesService.authenticatedIdentitiesPublisher()
|
||||||
|
@ -39,6 +40,9 @@ public final class NewStatusViewModel: ObservableObject {
|
||||||
.combineLatest($postingState)
|
.combineLatest($postingState)
|
||||||
.map { $0 && $1 == .composing }
|
.map { $0 && $1 == .composing }
|
||||||
.assign(to: &$canPost)
|
.assign(to: &$canPost)
|
||||||
|
compositionEventsSubject
|
||||||
|
.sink { [weak self] in self?.handle(event: $0) }
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +50,7 @@ public extension NewStatusViewModel {
|
||||||
enum Event {
|
enum Event {
|
||||||
case presentMediaPicker(CompositionViewModel)
|
case presentMediaPicker(CompositionViewModel)
|
||||||
case presentCamera(CompositionViewModel)
|
case presentCamera(CompositionViewModel)
|
||||||
|
case editAttachment(AttachmentViewModel, CompositionViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PostingState {
|
enum PostingState {
|
||||||
|
@ -85,7 +90,7 @@ public extension NewStatusViewModel {
|
||||||
guard let index = compositionViewModels.firstIndex(where: { $0 === after })
|
guard let index = compositionViewModels.firstIndex(where: { $0 === after })
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
let newViewModel = CompositionViewModel()
|
let newViewModel = CompositionViewModel(eventsSubject: compositionEventsSubject)
|
||||||
|
|
||||||
newViewModel.contentWarning = after.contentWarning
|
newViewModel.contentWarning = after.contentWarning
|
||||||
newViewModel.displayContentWarning = after.displayContentWarning
|
newViewModel.displayContentWarning = after.displayContentWarning
|
||||||
|
@ -109,6 +114,14 @@ public extension NewStatusViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private 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?) {
|
func post(viewModel: CompositionViewModel, inReplyToId: Status.Id?) {
|
||||||
postingState = .posting
|
postingState = .posting
|
||||||
identification.service.post(statusComponents: viewModel.components(
|
identification.service.post(statusComponents: viewModel.components(
|
||||||
|
|
|
@ -47,29 +47,13 @@ final class AttachmentView: UIView {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
if let focus = viewModel.attachment.meta?.focus {
|
if let focus = viewModel.attachment.meta?.focus {
|
||||||
let viewsAndSizes: [(UIView, CGSize?)] = [
|
let viewsAndMediaSizes: [(UIView, CGSize?)] = [
|
||||||
(imageView, imageView.image?.size),
|
(imageView, imageView.image?.size),
|
||||||
(playerView, playerView.player?.currentItem?.presentationSize)]
|
(playerView, playerView.player?.currentItem?.presentationSize)]
|
||||||
for (view, size) in viewsAndSizes {
|
for (view, mediaSize) in viewsAndMediaSizes {
|
||||||
guard let size = size else { continue }
|
guard let size = mediaSize else { continue }
|
||||||
|
|
||||||
let aspectRatio = size.width / size.height
|
view.setContentsRect(focus: focus, mediaSize: size)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
.clipShape(Circle())
|
||||||
.frame(width: .newStatusButtonDimension,
|
.frame(width: .newStatusButtonDimension,
|
||||||
height: .newStatusButtonDimension)
|
height: .newStatusButtonDimension)
|
||||||
.shadow(radius: .newStatusButtonShadowRadius)
|
.shadow(radius: .defaultShadowRadius)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ extension CGFloat {
|
||||||
static let minimumButtonDimension: Self = 44
|
static let minimumButtonDimension: Self = 44
|
||||||
static let barButtonItemDimension: Self = 28
|
static let barButtonItemDimension: Self = 28
|
||||||
static let newStatusButtonDimension: CGFloat = 54
|
static let newStatusButtonDimension: CGFloat = 54
|
||||||
static let newStatusButtonShadowRadius: CGFloat = 2
|
static let defaultShadowRadius: CGFloat = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CGRect {
|
extension CGRect {
|
||||||
|
|
Loading…
Reference in New Issue