diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 22ddb86f..75881005 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -87,18 +87,15 @@ D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A542263634100095BD04 /* PollOptionCheckboxView.swift */; }; D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */; }; D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */; }; - D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */; }; D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */; }; D626493923C0FD0000612E6E /* AllPhotosTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */; }; D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493A23C1000300612E6E /* AlbumTableViewCell.swift */; }; D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493B23C1000300612E6E /* AlbumTableViewCell.xib */; }; - D626493F23C101C500612E6E /* AlbumAssetCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493E23C101C500612E6E /* AlbumAssetCollectionViewController.swift */; }; D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943123A5466600D38C68 /* SelectableTableViewCell.swift */; }; D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */; }; D627944723A6AC9300D38C68 /* BasicTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */; }; D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */; }; D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */; }; - D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6285B5221EA708700FE4B39 /* StatusFormat.swift */; }; D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */; }; D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; }; D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; }; @@ -117,7 +114,6 @@ D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */; }; D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */; }; D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63D8DF32850FE7A008D95E1 /* ViewTags.swift */; }; - D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; }; D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; }; D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */; }; D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0824B0291E00F5412E /* MyProfileViewController.swift */; }; @@ -131,7 +127,6 @@ D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; }; D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; }; D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; }; - D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */; }; D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */; }; D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */; }; D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; }; @@ -252,9 +247,6 @@ D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */; }; D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */; }; D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D6B0026D29B5248800C70BE2 /* UserAccounts */; }; - D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A123BD2C0600A066FA /* AssetPickerViewController.swift */; }; - D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A323BD2C8100A066FA /* AssetCollectionsListViewController.swift */; }; - D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */; }; D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; }; D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; }; D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */; }; @@ -481,18 +473,15 @@ D623A542263634100095BD04 /* PollOptionCheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionCheckboxView.swift; sourceTree = ""; }; D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableTableViewCell.swift; sourceTree = ""; }; D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableViewCell.swift; sourceTree = ""; }; - D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentData.swift; sourceTree = ""; }; D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPhotosTableViewCell.swift; sourceTree = ""; }; D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AllPhotosTableViewCell.xib; sourceTree = ""; }; D626493A23C1000300612E6E /* AlbumTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumTableViewCell.swift; sourceTree = ""; }; D626493B23C1000300612E6E /* AlbumTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AlbumTableViewCell.xib; sourceTree = ""; }; - D626493E23C101C500612E6E /* AlbumAssetCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumAssetCollectionViewController.swift; sourceTree = ""; }; D627943123A5466600D38C68 /* SelectableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTableViewCell.swift; sourceTree = ""; }; D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigableTableViewCell.swift; sourceTree = ""; }; D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BasicTableViewCell.xib; sourceTree = ""; }; D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = ""; }; D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.swift; sourceTree = ""; }; - D6285B5221EA708700FE4B39 /* StatusFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFormat.swift; sourceTree = ""; }; D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LargeImageViewController.xib; sourceTree = ""; }; D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = ""; }; D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = ""; }; @@ -510,7 +499,6 @@ D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Top.swift"; sourceTree = ""; }; D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarTappableViewController.swift; sourceTree = ""; }; D63D8DF32850FE7A008D95E1 /* ViewTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTags.swift; sourceTree = ""; }; - D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = ""; }; D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = ""; }; D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = ""; }; D6412B0824B0291E00F5412E /* MyProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileViewController.swift; sourceTree = ""; }; @@ -524,7 +512,6 @@ D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = ""; }; D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = ""; }; - D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPreviewViewController.swift; sourceTree = ""; }; D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationTableViewCell.swift; sourceTree = ""; }; D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FollowRequestNotificationTableViewCell.xib; sourceTree = ""; }; D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; @@ -648,9 +635,6 @@ D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = ""; }; D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = ""; }; D6B0026C29B5245400C70BE2 /* UserAccounts */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = UserAccounts; path = Packages/UserAccounts; sourceTree = ""; }; - D6B053A123BD2C0600A066FA /* AssetPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerViewController.swift; sourceTree = ""; }; - D6B053A323BD2C8100A066FA /* AssetCollectionsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionsListViewController.swift; sourceTree = ""; }; - D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewController.swift; sourceTree = ""; }; D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = ""; }; D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = ""; }; D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OppositeCollapseKeywordsView.swift; sourceTree = ""; }; @@ -836,9 +820,6 @@ D61959D2241E846D00A37B8E /* Models */ = { isa = PBXGroup; children = ( - D6285B5221EA708700FE4B39 /* StatusFormat.swift */, - D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */, - D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */, D61F75AE293AF50C00C0B37F /* EditedFilter.swift */, D65B4B532971F71D00DABDFB /* EditedReport.swift */, D600891A29848289005B4D00 /* PinnedTimeline.swift */, @@ -989,7 +970,6 @@ children = ( D65B4B89297879DE00DABDFB /* Account Follows */, D6A3BC822321F69400FD64D5 /* Account List */, - D6B053A023BD2BED00A066FA /* Asset Picker */, 0411610522B457290030A9B7 /* Attachment Gallery */, D641C787213DD862004B4513 /* Compose */, D641C785213DD83B004B4513 /* Conversation */, @@ -1362,18 +1342,6 @@ path = Activities; sourceTree = ""; }; - D6B053A023BD2BED00A066FA /* Asset Picker */ = { - isa = PBXGroup; - children = ( - D6B053A123BD2C0600A066FA /* AssetPickerViewController.swift */, - D6B053A323BD2C8100A066FA /* AssetCollectionsListViewController.swift */, - D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */, - D626493E23C101C500612E6E /* AlbumAssetCollectionViewController.swift */, - D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */, - ); - path = "Asset Picker"; - sourceTree = ""; - }; D6BC9DD8232D8BCA002CA326 /* Search */ = { isa = PBXGroup; children = ( @@ -1910,9 +1878,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */, D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */, - D6285B5321EA708700FE4B39 /* StatusFormat.swift in Sources */, D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */, 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */, @@ -2002,7 +1968,6 @@ D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */, D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */, D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */, - D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */, D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */, D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */, D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */, @@ -2040,7 +2005,6 @@ D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */, D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */, D65B4B5E2973040D00DABDFB /* ReportAddStatusView.swift in Sources */, - D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */, D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */, D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */, D6B936712829F72900237D0E /* NSManagedObjectContext+Helpers.swift in Sources */, @@ -2052,7 +2016,6 @@ D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */, D65B4B6429771EFF00DABDFB /* ConversationViewController.swift in Sources */, D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */, - D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */, D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */, D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */, D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */, @@ -2078,7 +2041,6 @@ D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */, D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */, D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */, - D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */, D6F6A550291F058600F496A8 /* CreateListService.swift in Sources */, D601FA5B29787AB100A8E8B5 /* AccountFollowsListViewController.swift in Sources */, D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */, @@ -2176,7 +2138,6 @@ D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */, D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */, D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */, - D626493F23C101C500612E6E /* AlbumAssetCollectionViewController.swift in Sources */, D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */, D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */, D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */, @@ -2192,7 +2153,6 @@ D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */, D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */, D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */, - D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */, D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */, D65B4B562971F98300DABDFB /* ReportView.swift in Sources */, D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */, diff --git a/Tusker/Models/CompositionAttachment.swift b/Tusker/Models/CompositionAttachment.swift deleted file mode 100644 index 2c51302e..00000000 --- a/Tusker/Models/CompositionAttachment.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// CompositionAttachment.swift -// Tusker -// -// Created by Shadowfacts on 3/14/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import Foundation -import UIKit -import UniformTypeIdentifiers - -final class CompositionAttachment: NSObject, Codable, ObservableObject { - static let typeIdentifier = "space.vaccor.Tusker.composition-attachment" - - let id: UUID - @Published var data: CompositionAttachmentData - @Published var attachmentDescription: String - - init(data: CompositionAttachmentData, description: String = "") { - self.id = UUID() - self.data = data - self.attachmentDescription = description - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - self.id = try container.decode(UUID.self, forKey: .id) - self.data = try container.decode(CompositionAttachmentData.self, forKey: .data) - self.attachmentDescription = try container.decode(String.self, forKey: .attachmentDescription) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(id, forKey: .id) - try container.encode(data, forKey: .data) - try container.encode(attachmentDescription, forKey: .attachmentDescription) - } - - static func ==(lhs: CompositionAttachment, rhs: CompositionAttachment) -> Bool { - return lhs.id == rhs.id - } - - enum CodingKeys: String, CodingKey { - case id - case data - case attachmentDescription - } -} - -extension CompositionAttachment: Identifiable {} - -private let imageType = UTType.image.identifier -private let mp4Type = UTType.mpeg4Movie.identifier -private let quickTimeType = UTType.quickTimeMovie.identifier -private let dataType = UTType.data.identifier -private let gifType = UTType.gif.identifier - -extension CompositionAttachment: NSItemProviderWriting { - static var writableTypeIdentifiersForItemProvider: [String] { - [typeIdentifier] - } - - func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? { - if typeIdentifier == CompositionAttachment.typeIdentifier { - do { - completionHandler(try PropertyListEncoder().encode(self), nil) - } catch { - completionHandler(nil, error) - } - } else { - completionHandler(nil, ItemProviderError.incompatibleTypeIdentifier) - } - return nil - } - - enum ItemProviderError: Error { - case incompatibleTypeIdentifier - - var localizedDescription: String { - switch self { - case .incompatibleTypeIdentifier: - return "Cannot provide data for given type" - } - } - } -} - -extension CompositionAttachment: NSItemProviderReading { - static var readableTypeIdentifiersForItemProvider: [String] { - // todo: is there a better way of handling movies than manually adding all possible UTI types? - // just using kUTTypeMovie doesn't work, because we need the actually type in order to get the file extension - // without the file extension, getting the thumbnail and exporting the video for attachment upload fails - [typeIdentifier] + UIImage.readableTypeIdentifiersForItemProvider + [mp4Type, quickTimeType] + NSURL.readableTypeIdentifiersForItemProvider - } - - static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> CompositionAttachment { - if typeIdentifier == CompositionAttachment.typeIdentifier { - return try PropertyListDecoder().decode(CompositionAttachment.self, from: data) - } else if typeIdentifier == gifType { - return CompositionAttachment(data: .gif(data)) - } else if UIImage.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let image = try? UIImage.object(withItemProviderData: data, typeIdentifier: typeIdentifier) { - return CompositionAttachment(data: .image(image)) - } else if let type = UTType(typeIdentifier), type == .mpeg4Movie || type == .quickTimeMovie { - let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) - let temporaryFileName = ProcessInfo().globallyUniqueString - let fileExt = type.preferredFilenameExtension! - let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(temporaryFileName).appendingPathExtension(fileExt) - try data.write(to: temporaryFileURL) - return CompositionAttachment(data: .video(temporaryFileURL)) - } else if NSURL.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let url = try? NSURL.object(withItemProviderData: data, typeIdentifier: typeIdentifier) as URL { - return CompositionAttachment(data: .video(url)) - } else { - throw ItemProviderError.incompatibleTypeIdentifier - } - } -} diff --git a/Tusker/Models/CompositionAttachmentData.swift b/Tusker/Models/CompositionAttachmentData.swift deleted file mode 100644 index 112b974a..00000000 --- a/Tusker/Models/CompositionAttachmentData.swift +++ /dev/null @@ -1,259 +0,0 @@ -// -// CompositionAttachmentData.swift -// Tusker -// -// Created by Shadowfacts on 1/1/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import UIKit -import Photos -import UniformTypeIdentifiers -import PencilKit -import InstanceFeatures - -enum CompositionAttachmentData { - case asset(PHAsset) - case image(UIImage) - case video(URL) - case drawing(PKDrawing) - case gif(Data) - - var type: AttachmentType { - switch self { - case let .asset(asset): - return asset.attachmentType! - case .image(_): - return .image - case .video(_): - return .video - case .drawing(_): - return .image - case .gif(_): - return .image - } - } - - var isAsset: Bool { - switch self { - case .asset(_): - return true - default: - return false - } - } - - var canSaveToDraft: Bool { - switch self { - case .video(_): - return false - default: - return true - } - } - - func getData(features: InstanceFeatures, skipAllConversion: Bool = false, completion: @escaping (Result<(Data, UTType), Error>) -> Void) { - switch self { - case let .image(image): - // Export as JPEG instead of PNG, otherweise photos straight from the camera are too large - // for Mastodon in its default configuration (max of 10MB). - // The quality of 0.8 was chosen completely arbitrarily, it may need to be tuned in the future. - completion(.success((image.jpegData(compressionQuality: 0.8)!, .jpeg))) - case let .asset(asset): - if asset.mediaType == .image { - let options = PHImageRequestOptions() - options.version = .current - options.deliveryMode = .highQualityFormat - options.resizeMode = .none - options.isNetworkAccessAllowed = true - PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (data, dataUTI, orientation, info) in - guard var data = data, let dataUTI = dataUTI else { - completion(.failure(.missingData)) - return - } - - guard !skipAllConversion else { - completion(.success((data, UTType(dataUTI)!))) - return - } - - let utType: UTType - let image = CIImage(data: data)! - let needsColorSpaceConversion = features.needsWideColorGamutHack && image.colorSpace?.name != CGColorSpace.sRGB - - // neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG - // they also do a bad job converting wide color gamut images (they seem to just drop the profile, letting the wide-gamut values be reinterprete as sRGB) - // if that changes in the future, we'll need to pass the InstanceFeatures in here somehow and gate the conversion - if needsColorSpaceConversion || dataUTI == "public.heic" { - let context = CIContext() - let colorSpace = needsColorSpaceConversion || image.colorSpace != nil ? CGColorSpace(name: CGColorSpace.sRGB)! : image.colorSpace! - if dataUTI == "public.png" { - data = context.pngRepresentation(of: image, format: .ARGB8, colorSpace: colorSpace)! - utType = .png - } else { - data = context.jpegRepresentation(of: image, colorSpace: colorSpace)! - utType = .jpeg - } - } else { - utType = UTType(dataUTI)! - } - - completion(.success((data, utType))) - } - } else if asset.mediaType == .video { - let options = PHVideoRequestOptions() - options.deliveryMode = .automatic - options.isNetworkAccessAllowed = true - options.version = .current - PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in - if let exportSession = exportSession { - CompositionAttachmentData.exportVideoData(session: exportSession, completion: completion) - } else if let error = info?[PHImageErrorKey] as? Error { - completion(.failure(.videoExport(error))) - } else { - completion(.failure(.noVideoExportSession)) - } - } - } else { - fatalError("assetType must be either image or video") - } - case let .video(url): - let asset = AVURLAsset(url: url) - guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else { - completion(.failure(.noVideoExportSession)) - return - } - CompositionAttachmentData.exportVideoData(session: session, completion: completion) - - case let .drawing(drawing): - let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1) - completion(.success((image.pngData()!, .png))) - case let .gif(data): - completion(.success((data, .gif))) - } - } - - private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Result<(Data, UTType), Error>) -> Void) { - session.outputFileType = .mp4 - session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4") - session.exportAsynchronously { - guard session.status == .completed else { - completion(.failure(.videoExport(session.error!))) - return - } - do { - let data = try Data(contentsOf: session.outputURL!) - completion(.success((data, .mpeg4Movie))) - } catch { - completion(.failure(.videoExport(error))) - } - } - } - - enum AttachmentType { - case image, video - } - - enum Error: Swift.Error, LocalizedError { - case missingData - case videoExport(Swift.Error) - case noVideoExportSession - - var localizedDescription: String { - switch self { - case .missingData: - return "Missing Data" - case .videoExport(let error): - return "Exporting video: \(error)" - case .noVideoExportSession: - return "Couldn't create video export session" - } - } - } -} - -extension PHAsset { - var attachmentType: CompositionAttachmentData.AttachmentType? { - switch self.mediaType { - case .image: - return .image - case .video: - return .video - default: - return nil - } - } -} - -extension CompositionAttachmentData: Codable { - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - switch self { - case let .asset(asset): - try container.encode("asset", forKey: .type) - try container.encode(asset.localIdentifier, forKey: .assetIdentifier) - case let .image(image): - try container.encode("image", forKey: .type) - try container.encode(image.pngData()!, forKey: .imageData) - case .video(_): - throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: [], debugDescription: "video CompositionAttachments cannot be encoded")) - case let .drawing(drawing): - try container.encode("drawing", forKey: .type) - let drawingData = drawing.dataRepresentation() - try container.encode(drawingData, forKey: .drawing) - case .gif(_): - throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: [], debugDescription: "gif CompositionAttachments cannot be encoded")) - } - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - switch try container.decode(String.self, forKey: .type) { - case "asset": - let identifier = try container.decode(String.self, forKey: .assetIdentifier) - guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject else { - throw DecodingError.dataCorruptedError(forKey: .assetIdentifier, in: container, debugDescription: "Could not fetch asset with local identifier") - } - self = .asset(asset) - case "image": - guard let image = UIImage(data: try container.decode(Data.self, forKey: .imageData)) else { - throw DecodingError.dataCorruptedError(forKey: .imageData, in: container, debugDescription: "Could not decode UIImage from image data") - } - self = .image(image) - case "drawing": - let drawingData = try container.decode(Data.self, forKey: .drawing) - let drawing = try PKDrawing(data: drawingData) - self = .drawing(drawing) - default: - throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of image, asset, or drawing") - } - } - - enum CodingKeys: CodingKey { - case type - case imageData - /// The local identifier of the PHAsset for this attachment - case assetIdentifier - /// The PKDrawing object for this attachment. - case drawing - } -} - -extension CompositionAttachmentData: Equatable { - static func ==(lhs: CompositionAttachmentData, rhs: CompositionAttachmentData) -> Bool { - switch (lhs, rhs) { - case let (.asset(a), .asset(b)): - return a.localIdentifier == b.localIdentifier - case let (.image(a), .image(b)): - return a == b - case let (.video(a), .video(b)): - return a == b - case let (.drawing(a), .drawing(b)): - return a == b - default: - return false - } - } -} diff --git a/Tusker/Models/StatusFormat.swift b/Tusker/Models/StatusFormat.swift deleted file mode 100644 index 1f78df13..00000000 --- a/Tusker/Models/StatusFormat.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// StatusFormat.swift -// Tusker -// -// Created by Shadowfacts on 1/12/19. -// Copyright © 2019 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm - -enum StatusFormat: Int, CaseIterable { - case bold, italics, strikethrough, code - - var insertionResult: FormatInsertionResult? { - switch Preferences.shared.statusContentType { - case .plain: - return nil - case .markdown: - return Markdown.format(self) - case .html: - return HTML.format(self) - } - } - - var imageName: String? { - switch self { - case .italics: - return "italic" - case .bold: - return "bold" - case .strikethrough: - return "strikethrough" - default: - return nil - } - } - - var title: (String, [NSAttributedString.Key: Any])? { - if self == .code { - return ("", [.font: UIFont(name: "Menlo", size: 17)!]) - } else { - return nil - } - } - - var accessibilityLabel: String { - switch self { - case .italics: - return NSLocalizedString("Italics", comment: "italics text format accessibility label") - case .bold: - return NSLocalizedString("Bold", comment: "bold text format accessibility label") - case .strikethrough: - return NSLocalizedString("Strikethrough", comment: "strikethrough text format accessibility label") - case .code: - return NSLocalizedString("Code", comment: "code text format accessibility label") - } - } -} - -typealias FormatInsertionResult = (prefix: String, suffix: String, insertionPoint: Int) - -protocol FormatType { - static func format(_ format: StatusFormat) -> FormatInsertionResult -} - -extension StatusFormat { - struct Markdown: FormatType { - static var formats: [StatusFormat: String] = [ - .italics: "_", - .bold: "**", - .strikethrough: "~~", - .code: "`" - ] - - static func format(_ format: StatusFormat) -> FormatInsertionResult { - let str = formats[format]! - return (str, str, str.count) - } - } - - struct HTML: FormatType { - static var tags: [StatusFormat: String] = [ - .italics: "em", - .bold: "strong", - .strikethrough: "del", - .code: "code" - ] - - static func format(_ format: StatusFormat) -> FormatInsertionResult { - let tag = tags[format]! - return ("<\(tag)>", "", tag.count + 2) - } - } -} diff --git a/Tusker/Screens/Asset Picker/AlbumAssetCollectionViewController.swift b/Tusker/Screens/Asset Picker/AlbumAssetCollectionViewController.swift deleted file mode 100644 index 751a56b9..00000000 --- a/Tusker/Screens/Asset Picker/AlbumAssetCollectionViewController.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// AlbumAssetCollectionViewController.swift -// Tusker -// -// Created by Shadowfacts on 1/4/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import UIKit -import Photos - -class AlbumAssetCollectionViewController: AssetCollectionViewController { - - let collection: PHAssetCollection - - init(collection: PHAssetCollection) { - self.collection = collection - - super.init() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func fetchAssets(with options: PHFetchOptions) -> PHFetchResult { - return PHAsset.fetchAssets(in: collection, options: options) - } - -} diff --git a/Tusker/Screens/Asset Picker/AssetCollectionViewController.swift b/Tusker/Screens/Asset Picker/AssetCollectionViewController.swift deleted file mode 100644 index def706ca..00000000 --- a/Tusker/Screens/Asset Picker/AssetCollectionViewController.swift +++ /dev/null @@ -1,269 +0,0 @@ -// -// AssetCollectionViewController.swift -// Tusker -// -// Created by Shadowfacts on 1/1/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import UIKit -import Photos - -private let reuseIdentifier = "assetCell" -private let cameraReuseIdentifier = "showCameraCell" - -protocol AssetCollectionViewControllerDelegate: AnyObject { - func shouldSelectAsset(_ asset: PHAsset) -> Bool - func didSelectAssets(_ assets: [PHAsset]) - func captureFromCamera() -} - -class AssetCollectionViewController: UIViewController, UICollectionViewDelegate { - - weak var delegate: AssetCollectionViewControllerDelegate? - - private var collectionView: UICollectionView! - private var dataSource: UICollectionViewDiffableDataSource! - - private var thumbnailSize: CGSize! - - private let imageManager = PHCachingImageManager() - private var fetchResult: PHFetchResult! - - var selectedAssets: [PHAsset] { - return collectionView.indexPathsForSelectedItems?.compactMap { (indexPath) in - guard case let .asset(asset) = dataSource.itemIdentifier(for: indexPath) else { return nil } - return asset - } ?? [] - } - - init() { - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalWidth(1/3)) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1/3)) - let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 3) - group.interItemSpacing = .fixed(4) - let section = NSCollectionLayoutSection(group: group) - let layout = UICollectionViewCompositionalLayout(section: section) - collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) - collectionView.delegate = self - view.addSubview(collectionView) - - // use the safe area layout guide instead of letting it automatically use the safe area insets - // because otherwise, when presented in a popover with the arrow on the left or right side, - // the collection view content will be cut off by the width of the arrow because the popover - // doesn't respect safe area insets - collectionView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor), - view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor), - // top ignores safe area because when presented in the sheet container, it simplifies the top content offset - view.topAnchor.constraint(equalTo: collectionView.topAnchor), - // bottom ignores safe area because we want cells to underflow bottom of the screen on notched iPhones - view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor), - ]) - view.backgroundColor = .appBackground - collectionView.backgroundColor = .appBackground - - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed)) - - collectionView.alwaysBounceVertical = true - collectionView.allowsMultipleSelection = true - collectionView.allowsSelection = true - collectionView.allowsFocus = true - - collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier) - - let controlCell = UICollectionView.CellRegistration { cell, indexPath, itemIdentifier in - switch itemIdentifier { - case .showCamera: - cell.imageView.image = UIImage(systemName: "camera") - cell.label.text = "Take a Photo" - case .changeLimitedSelection: - cell.imageView.image = UIImage(systemName: "photo.on.rectangle.angled") - cell.label.text = "Select More Photos" - case .asset(_): - break - } - } - - dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in - switch item { - case .showCamera, .changeLimitedSelection: - return collectionView.dequeueConfiguredReusableCell(using: controlCell, for: indexPath, item: item) - case let .asset(asset): - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AssetCollectionViewCell - - cell.updateUI(asset: asset) - self.imageManager.requestImage(for: asset, targetSize: self.thumbnailSize, contentMode: .aspectFill, options: nil) { (image, _) in - guard let image = image else { return } - DispatchQueue.main.async { - guard cell.assetIdentifier == asset.localIdentifier else { return } - cell.thumbnailImage = image - } - } - - return cell - } - }) - - updateItemsSelectedCount() - - if let singleFingerPanGesture = collectionView.gestureRecognizers?.first(where: { - $0.name == "multi-select.singleFingerPanGesture" - }), - let interactivePopGesture = navigationController?.interactivePopGestureRecognizer { - singleFingerPanGesture.require(toFail: interactivePopGesture) - } - - PHPhotoLibrary.shared().register(self) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - let scale = UIScreen.main.scale - let cellWidth = view.bounds.width / 3 - thumbnailSize = CGSize(width: cellWidth * scale, height: cellWidth * scale) - - loadAssets() - } - - private func loadAssets() { - var items = [Item.showCamera] - - switch PHPhotoLibrary.authorizationStatus(for: .readWrite) { - case .notDetermined: - PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_) in - DispatchQueue.main.async { - self.loadAssets() - } - } - return - - case .restricted, .denied: - // todo: better UI for this - return - - case .authorized: - break - - case .limited: - items.append(.changeLimitedSelection) - break - - @unknown default: - // who knows, just try anyways - break - } - - let options = PHFetchOptions() - options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] - fetchResult = fetchAssets(with: options) - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.assets]) - fetchResult.enumerateObjects { (asset, _, _) in - items.append(.asset(asset)) - } - snapshot.appendItems(items) - dataSource.apply(snapshot, animatingDifferences: false) - } - - open func fetchAssets(with options: PHFetchOptions) -> PHFetchResult { - return PHAsset.fetchAssets(with: options) - } - - func updateItemsSelectedCount() { - let selected = collectionView.indexPathsForSelectedItems?.count ?? 0 - - navigationItem.title = "\(selected) selected" - } - - // MARK: UICollectionViewDelegate - - func collectionView(_ collectionView: UICollectionView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool { - return true - } - - func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { - guard let item = dataSource.itemIdentifier(for: indexPath) else { return false } - if let delegate = delegate, - case let .asset(asset) = item { - return delegate.shouldSelectAsset(asset) - } - return true - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let item = dataSource.itemIdentifier(for: indexPath) else { return } - switch item { - case .showCamera: - collectionView.deselectItem(at: indexPath, animated: false) - delegate?.captureFromCamera() - case .changeLimitedSelection: - // todo: change observer - PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: self) - case .asset(_): - updateItemsSelectedCount() - } - } - - func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { - updateItemsSelectedCount() - } - - func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { - guard case let .asset(asset) = dataSource.itemIdentifier(for: indexPath) else { return nil } - return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in - return AssetPreviewViewController(asset: asset) - }, actionProvider: nil) - } - - func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { - if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?, - let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell { - let parameters = UIPreviewParameters() - parameters.backgroundColor = .black - return UITargetedPreview(view: cell.imageView, parameters: parameters) - } else { - return nil - } - } - - // MARK: - Interaction - - @objc func donePressed() { - delegate?.didSelectAssets(selectedAssets) - dismiss(animated: true) - } - -} - -extension AssetCollectionViewController { - enum Section: Hashable { - case assets - } - enum Item: Hashable { - case showCamera - case changeLimitedSelection - case asset(PHAsset) - } -} - -extension AssetCollectionViewController: PHPhotoLibraryChangeObserver { - func photoLibraryDidChange(_ changeInstance: PHChange) { - DispatchQueue.main.async { - self.loadAssets() - } - } -} diff --git a/Tusker/Screens/Asset Picker/AssetCollectionsListViewController.swift b/Tusker/Screens/Asset Picker/AssetCollectionsListViewController.swift deleted file mode 100644 index ba0bbd7a..00000000 --- a/Tusker/Screens/Asset Picker/AssetCollectionsListViewController.swift +++ /dev/null @@ -1,158 +0,0 @@ -// -// AssetCollectionsListViewController.swift -// Tusker -// -// Created by Shadowfacts on 1/1/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import UIKit -import Photos - -class AssetCollectionsListViewController: UITableViewController { - - weak var assetCollectionDelegate: AssetCollectionViewControllerDelegate? - - var dataSource: DataSource! - - init() { - super.init(style: .plain) - - title = NSLocalizedString("Collections", comment: "asset collections list title") - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelPressed)) - - tableView.register(UINib(nibName: "AllPhotosTableViewCell", bundle: .main), forCellReuseIdentifier: "allPhotosCell") - tableView.register(UINib(nibName: "AlbumTableViewCell", bundle: .main), forCellReuseIdentifier: "albumCell") - - tableView.allowsFocus = true - tableView.backgroundColor = .appGroupedBackground - - dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in - switch item { - case .cameraRoll: - return tableView.dequeueReusableCell(withIdentifier: "allPhotosCell", for: indexPath) - - case let .album(collection): - let cell = tableView.dequeueReusableCell(withIdentifier: "albumCell", for: indexPath) as! AlbumTableViewCell - cell.updateUI(album: collection) - return cell - } - }) - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.system, .albums, .sharedAlbums, .smartAlbums]) - snapshot.appendItems([.cameraRoll], toSection: .system) - - let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .any, options: nil) - var smartAlbumItems = [Item]() - smartAlbums.enumerateObjects { (collection, _, _) in - guard collection.assetCollectionSubtype != .smartAlbumAllHidden else { - return - } - smartAlbumItems.append(.album(collection)) - } - // sort these manually, using PHFetchOptions.sortDescriptors seems like it just doesn't work with fetchAssetCollections - smartAlbumItems.sort(by: { $0.title < $1.title }) - snapshot.appendItems(smartAlbumItems, toSection: .smartAlbums) - - let albums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: nil) - var albumItems = [Item]() - var sharedItems = [Item]() - albums.enumerateObjects { (collection, _, _) in - if collection.estimatedAssetCount > 0 { - if collection.assetCollectionSubtype == .albumCloudShared { - sharedItems.append(.album(collection)) - } else { - albumItems.append(.album(collection)) - } - } - } - albumItems.sort(by: { $0.title < $1.title }) - sharedItems.sort(by: { $0.title < $1.title }) - snapshot.appendItems(albumItems, toSection: .albums) - snapshot.appendItems(sharedItems, toSection: .sharedAlbums) - - dataSource.apply(snapshot, animatingDifferences: false) - } - - // MARK: - Table view delegate - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - switch dataSource.itemIdentifier(for: indexPath) { - case nil: - return - - case .cameraRoll: - let assetCollection = AssetCollectionViewController() - assetCollection.delegate = assetCollectionDelegate - show(assetCollection, sender: self) - - case let .album(collection): - let assetCollection = AlbumAssetCollectionViewController(collection: collection) - assetCollection.delegate = assetCollectionDelegate - show(assetCollection, sender: self) - } - } - - // MARK: - Interaction - - @objc func cancelPressed() { - dismiss(animated: true) - } - -} - -extension AssetCollectionsListViewController { - enum Section { - case system - case albums - case sharedAlbums - case smartAlbums - } - enum Item: Hashable { - case cameraRoll - case album(PHAssetCollection) - - func hash(into hasher: inout Hasher) { - switch self { - case .cameraRoll: - hasher.combine("cameraRoll") - case let .album(collection): - hasher.combine("album") - hasher.combine(collection.localIdentifier) - } - } - - var title: String { - switch self { - case .cameraRoll: - return "All Photos" - case .album(let collection): - return collection.localizedTitle ?? "" - } - } - } - class DataSource: UITableViewDiffableDataSource { - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - switch sectionIdentifier(for: section) { - case .albums: - return NSLocalizedString("Albums", comment: "albums section title") - case .sharedAlbums: - return NSLocalizedString("Shared Albums", comment: "shared albums section title") - case .smartAlbums: - return NSLocalizedString("Smart Albums", comment: "smart albums section title") - default: - return nil - } - } - } -} diff --git a/Tusker/Screens/Asset Picker/AssetPickerViewController.swift b/Tusker/Screens/Asset Picker/AssetPickerViewController.swift deleted file mode 100644 index 91ceacb7..00000000 --- a/Tusker/Screens/Asset Picker/AssetPickerViewController.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// AssetPickerViewController.swift -// Tusker -// -// Created by Shadowfacts on 1/1/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import UIKit -import Photos - -protocol AssetPickerViewControllerDelegate: AnyObject { - func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool - func assetPicker(_ assetPicker: AssetPickerViewController, didSelectAttachments attachments: [CompositionAttachmentData]) -} - -class AssetPickerViewController: UINavigationController { - - weak var assetPickerDelegate: AssetPickerViewControllerDelegate? - - var currentCollectionSelectedAssets: [CompositionAttachmentData] { - if let vc = visibleViewController as? AssetCollectionViewController { - return vc.selectedAssets.map { .asset($0) } - } else { - return [] - } - } - - init() { - super.init(navigationBarClass: nil, toolbarClass: nil) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - let assetCollectionsList = AssetCollectionsListViewController() - assetCollectionsList.assetCollectionDelegate = self - let assetCollection = AssetCollectionViewController() - assetCollection.delegate = self - setViewControllers([assetCollectionsList, assetCollection], animated: false) - } - - func presentImagePicker(animated: Bool) { - let imagePicker = UIImagePickerController() - imagePicker.delegate = self - imagePicker.sourceType = .camera - imagePicker.mediaTypes = UIImagePickerController.availableMediaTypes(for: .camera)! - self.present(imagePicker, animated: true) - } - -} - -extension AssetPickerViewController: AssetCollectionViewControllerDelegate { - func shouldSelectAsset(_ asset: PHAsset) -> Bool { - guard let delegate = assetPickerDelegate else { return true } - guard let type = asset.attachmentType else { return false } - return delegate.assetPicker(self, shouldAllowAssetOfType: type) - } - func didSelectAssets(_ assets: [PHAsset]) { - assetPickerDelegate?.assetPicker(self, didSelectAttachments: assets.map { .asset($0) }) - } - func captureFromCamera() { - presentImagePicker(animated: true) - } -} - -extension AssetPickerViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - let attachment: CompositionAttachmentData - if let image = info[.originalImage] as? UIImage { - attachment = .image(image) - } else if let url = info[.mediaURL] as? URL { - attachment = .video(url) - } else { - return - } - - if assetPickerDelegate?.assetPicker(self, shouldAllowAssetOfType: attachment.type) ?? true { - assetPickerDelegate?.assetPicker(self, didSelectAttachments: [attachment]) - // dismiss image picker - dismiss(animated: true) { - // dismiss asset picker - self.dismiss(animated: true) - } - } else { - dismiss(animated: false) { - self.presentImagePicker(animated: false) - } - } - } -} diff --git a/Tusker/Screens/Asset Picker/AssetPreviewViewController.swift b/Tusker/Screens/Asset Picker/AssetPreviewViewController.swift deleted file mode 100644 index 0583ecf0..00000000 --- a/Tusker/Screens/Asset Picker/AssetPreviewViewController.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// AssetPreviewViewController.swift -// Tusker -// -// Created by Shadowfacts on 1/4/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import UIKit -import Photos -import PhotosUI -import AVKit - -class AssetPreviewViewController: UIViewController { - - let asset: PHAsset - - init(asset: PHAsset) { - self.asset = asset - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .black - - switch asset.mediaType { - case .image: - if asset.mediaSubtypes.contains(.photoLive) { - showLivePhoto(asset) - } else { - showAssetImage(asset) - } - case .video: - showAssetVideo(asset) - default: - fatalError("asset mediaType must be image or video") - } - } - - func showImage(_ image: UIImage) { - let imageView = UIImageView(image: image) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFit - view.addSubview(imageView) - NSLayoutConstraint.activate([ - imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - imageView.topAnchor.constraint(equalTo: view.topAnchor), - imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) - preferredContentSize = image.size - } - - func showAssetImage(_ asset: PHAsset) { - let options = PHImageRequestOptions() - options.version = .current - options.deliveryMode = .opportunistic - options.resizeMode = .none - options.isNetworkAccessAllowed = true - PHImageManager.default().requestImage(for: asset, targetSize: view.bounds.size, contentMode: .aspectFit, options: options) { (image, _) in - DispatchQueue.main.async { - self.showImage(image!) - } - } - } - - func showLivePhoto(_ asset: PHAsset) { - let options = PHLivePhotoRequestOptions() - options.deliveryMode = .opportunistic - options.version = .current - options.isNetworkAccessAllowed = true - PHImageManager.default().requestLivePhoto(for: asset, targetSize: view.bounds.size, contentMode: .aspectFit, options: options) { (livePhoto, _) in - guard let livePhoto = livePhoto else { - fatalError("failed to get live photo") - } - DispatchQueue.main.async { - let livePhotoView = PHLivePhotoView() - livePhotoView.livePhoto = livePhoto - livePhotoView.isMuted = true - livePhotoView.startPlayback(with: .full) - livePhotoView.translatesAutoresizingMaskIntoConstraints = false - livePhotoView.contentMode = .scaleAspectFit - self.view.addSubview(livePhotoView) - NSLayoutConstraint.activate([ - livePhotoView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), - livePhotoView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), - livePhotoView.topAnchor.constraint(equalTo: self.view.topAnchor), - livePhotoView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor) - ]) - self.preferredContentSize = livePhoto.size - } - } - } - - func showVideo(asset: AVAsset) { - let playerController = AVPlayerViewController() - let item = AVPlayerItem(asset: asset) - let player = AVPlayer(playerItem: item) - player.isMuted = true - player.play() - playerController.player = player - self.embedChild(playerController) - self.preferredContentSize = item.presentationSize - } - - func showAssetVideo(_ asset: PHAsset) { - let options = PHVideoRequestOptions() - options.deliveryMode = .automatic - options.isNetworkAccessAllowed = true - options.version = .current - PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { (avAsset, _, _) in - guard let avAsset = avAsset else { - fatalError("failed to get AVAsset") - } - DispatchQueue.main.async { - self.showVideo(asset: avAsset) - } - } - } - -}