Compare commits
No commits in common. "22fe1e8ab16ce9e4e34936af02cd5fde7d79d518" and "f89d2c1cca0463cf34feef4f8eac3cabf8ebf1ff" have entirely different histories.
22fe1e8ab1
...
f89d2c1cca
|
@ -33,13 +33,8 @@ public struct Endpoint: ExpressibleByStringInterpolation, CustomStringConvertibl
|
|||
switch $0 {
|
||||
case .literal(let s):
|
||||
return s
|
||||
#if DEBUG
|
||||
case .interpolated(let s):
|
||||
return s
|
||||
#else
|
||||
case .interpolated(_):
|
||||
return "<redacted>"
|
||||
#endif
|
||||
}
|
||||
}.joined(separator: "")
|
||||
}
|
||||
|
|
|
@ -87,15 +87,18 @@
|
|||
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 */; };
|
||||
|
@ -114,6 +117,7 @@
|
|||
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 */; };
|
||||
|
@ -127,6 +131,7 @@
|
|||
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 */; };
|
||||
|
@ -247,6 +252,9 @@
|
|||
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 */; };
|
||||
|
@ -473,15 +481,18 @@
|
|||
D623A542263634100095BD04 /* PollOptionCheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionCheckboxView.swift; sourceTree = "<group>"; };
|
||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D6262C9928D01C4B00390C1F /* LoadingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentData.swift; sourceTree = "<group>"; };
|
||||
D626493623C0FD0000612E6E /* AllPhotosTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllPhotosTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D626493723C0FD0000612E6E /* AllPhotosTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AllPhotosTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D626493A23C1000300612E6E /* AlbumTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D626493B23C1000300612E6E /* AlbumTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AlbumTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D626493E23C101C500612E6E /* AlbumAssetCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumAssetCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigableTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BasicTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = "<group>"; };
|
||||
D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.swift; sourceTree = "<group>"; };
|
||||
D6285B5221EA708700FE4B39 /* StatusFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFormat.swift; sourceTree = "<group>"; };
|
||||
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LargeImageViewController.xib; sourceTree = "<group>"; };
|
||||
D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
|
||||
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
|
||||
|
@ -499,6 +510,7 @@
|
|||
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Top.swift"; sourceTree = "<group>"; };
|
||||
D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarTappableViewController.swift; sourceTree = "<group>"; };
|
||||
D63D8DF32850FE7A008D95E1 /* ViewTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTags.swift; sourceTree = "<group>"; };
|
||||
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = "<group>"; };
|
||||
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -512,6 +524,7 @@
|
|||
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
||||
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
||||
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
||||
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPreviewViewController.swift; sourceTree = "<group>"; };
|
||||
D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FollowRequestNotificationTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -635,6 +648,9 @@
|
|||
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
|
||||
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
|
||||
D6B0026C29B5245400C70BE2 /* UserAccounts */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = UserAccounts; path = Packages/UserAccounts; sourceTree = "<group>"; };
|
||||
D6B053A123BD2C0600A066FA /* AssetPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerViewController.swift; sourceTree = "<group>"; };
|
||||
D6B053A323BD2C8100A066FA /* AssetCollectionsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionsListViewController.swift; sourceTree = "<group>"; };
|
||||
D6B053A523BD2D0C00A066FA /* AssetCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OppositeCollapseKeywordsView.swift; sourceTree = "<group>"; };
|
||||
|
@ -820,6 +836,9 @@
|
|||
D61959D2241E846D00A37B8E /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6285B5221EA708700FE4B39 /* StatusFormat.swift */,
|
||||
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */,
|
||||
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */,
|
||||
D61F75AE293AF50C00C0B37F /* EditedFilter.swift */,
|
||||
D65B4B532971F71D00DABDFB /* EditedReport.swift */,
|
||||
D600891A29848289005B4D00 /* PinnedTimeline.swift */,
|
||||
|
@ -970,13 +989,14 @@
|
|||
children = (
|
||||
D65B4B89297879DE00DABDFB /* Account Follows */,
|
||||
D6A3BC822321F69400FD64D5 /* Account List */,
|
||||
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
||||
0411610522B457290030A9B7 /* Attachment Gallery */,
|
||||
D641C787213DD862004B4513 /* Compose */,
|
||||
D641C785213DD83B004B4513 /* Conversation */,
|
||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||
D61F759729384D4200C0B37F /* Customize Timelines */,
|
||||
D627943C23A5635D00D38C68 /* Explore */,
|
||||
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
||||
D61F759729384D4200C0B37F /* Customize Timelines */,
|
||||
D641C788213DD86D004B4513 /* Large Image */,
|
||||
D627944B23A9A02400D38C68 /* Lists */,
|
||||
D627944823A6AD5100D38C68 /* Local Predicate Statuses List */,
|
||||
|
@ -1342,6 +1362,18 @@
|
|||
path = Activities;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
D6BC9DD8232D8BCA002CA326 /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1878,7 +1910,9 @@
|
|||
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 */,
|
||||
|
@ -1968,6 +2002,7 @@
|
|||
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 */,
|
||||
|
@ -2005,6 +2040,7 @@
|
|||
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 */,
|
||||
|
@ -2016,6 +2052,7 @@
|
|||
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 */,
|
||||
|
@ -2041,6 +2078,7 @@
|
|||
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 */,
|
||||
|
@ -2138,6 +2176,7 @@
|
|||
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 */,
|
||||
|
@ -2153,6 +2192,7 @@
|
|||
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 */,
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
//
|
||||
// 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)>", tag.count + 2)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// 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<PHAsset> {
|
||||
return PHAsset.fetchAssets(in: collection, options: options)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,269 @@
|
|||
//
|
||||
// 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<Section, Item>!
|
||||
|
||||
private var thumbnailSize: CGSize!
|
||||
|
||||
private let imageManager = PHCachingImageManager()
|
||||
private var fetchResult: PHFetchResult<PHAsset>!
|
||||
|
||||
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<AssetPickerControlCollectionViewCell, Item> { 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<Section, Item>(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<Section, Item>()
|
||||
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<PHAsset> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
//
|
||||
// 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<Section, Item>()
|
||||
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<Section, Item> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -32,7 +32,6 @@ struct AddHashtagPinnedTimelineView: View {
|
|||
@State private var searchTask: Task<Void, Never>?
|
||||
@State private var isSearching = false
|
||||
@State private var searchResults: [String] = []
|
||||
@State private var trendingTags: [String] = []
|
||||
|
||||
private var savedAndFollowedHashtags: [String] {
|
||||
var tags = Set<String>()
|
||||
|
@ -43,19 +42,17 @@ struct AddHashtagPinnedTimelineView: View {
|
|||
for followed in mastodonController.followedHashtags {
|
||||
tags.insert(followed.name)
|
||||
}
|
||||
return tags.sorted(using: SemiCaseSensitiveComparator())
|
||||
return Array(tags).sorted(using: SemiCaseSensitiveComparator())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
list
|
||||
.appGroupedListBackground(container: AddHashtagPinnedTimelineRepresentable.UIViewControllerType.self)
|
||||
.listStyle(.grouped)
|
||||
.appGroupedListBackground(container: AddHashtagPinnedTimelineRepresentable.UIViewControllerType.self)
|
||||
.navigationTitle("Add Hashtag")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.searchable(text: $viewModel.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Search for hashtags"))
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.never)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
|
@ -74,45 +71,35 @@ struct AddHashtagPinnedTimelineView: View {
|
|||
try? await updateSearchResults()
|
||||
}
|
||||
})
|
||||
.task {
|
||||
await fetchTrendingTags()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var list: some View {
|
||||
List {
|
||||
if !viewModel.searchQuery.isEmpty {
|
||||
Section {
|
||||
let list = List {
|
||||
Section {
|
||||
if viewModel.searchQuery.isEmpty {
|
||||
forEachTag(savedAndFollowedHashtags)
|
||||
} else {
|
||||
forEachTag(searchResults)
|
||||
}
|
||||
} header: {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.opacity(isSearching ? 1 : 0)
|
||||
.animation(.linear(duration: 0.1), value: isSearching)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.listRowBackground(EmptyView())
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
forEachTag(searchResults)
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
}
|
||||
.listStyle(.grouped)
|
||||
|
||||
if !savedAndFollowedHashtags.isEmpty {
|
||||
Section {
|
||||
forEachTag(savedAndFollowedHashtags)
|
||||
} header: {
|
||||
Text("Saved and Followed Hashtags")
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
}
|
||||
|
||||
if !trendingTags.isEmpty {
|
||||
Section {
|
||||
forEachTag(trendingTags)
|
||||
} header: {
|
||||
Text("Trending Hashtags")
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
}
|
||||
if #available(iOS 16.0, *) {
|
||||
list
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appGroupedBackground)
|
||||
} else {
|
||||
list
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,14 +126,6 @@ struct AddHashtagPinnedTimelineView: View {
|
|||
searchResults = results.hashtags.map(\.name)
|
||||
isSearching = false
|
||||
}
|
||||
|
||||
private func fetchTrendingTags() async {
|
||||
guard mastodonController.instanceFeatures.trends else { return }
|
||||
let req = Client.getTrendingHashtags()
|
||||
if let (results, _) = try? await mastodonController.run(req) {
|
||||
trendingTags = results.map(\.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchViewModel: ObservableObject {
|
||||
|
|
|
@ -89,7 +89,7 @@ struct CustomizeTimelinesList: View {
|
|||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.appGroupedListBackground(container: UIHostingController<CustomizeTimelinesView>.self)
|
||||
.appGroupedListBackground(container: UIHostingController<CustomizeTimelinesList>.self)
|
||||
.navigationTitle(Text("Customize Timelines"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
|
|
@ -165,21 +165,16 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
|||
}
|
||||
}
|
||||
|
||||
private func validateNotifications(_ notifications: [Pachyderm.Notification]) -> [Pachyderm.Notification] {
|
||||
return notifications.compactMap { notif in
|
||||
if notif.status == nil && (notif.kind == .mention || notif.kind == .reblog || notif.kind == .favourite) {
|
||||
let crumb = Breadcrumb(level: .fatal, category: "notifications")
|
||||
crumb.data = [
|
||||
"id": notif.id,
|
||||
"type": notif.kind.rawValue,
|
||||
"created_at": notif.createdAt.formatted(.iso8601),
|
||||
"account": notif.account.id,
|
||||
]
|
||||
SentrySDK.addBreadcrumb(crumb)
|
||||
return nil
|
||||
} else {
|
||||
return notif
|
||||
}
|
||||
private func validateNotifications(_ notifications: [Pachyderm.Notification]) {
|
||||
for notif in notifications where notif.status == nil && (notif.kind == .mention || notif.kind == .reblog || notif.kind == .favourite) {
|
||||
let crumb = Breadcrumb(level: .fatal, category: "notifications")
|
||||
crumb.data = [
|
||||
"id": notif.id,
|
||||
"type": notif.kind.rawValue,
|
||||
"created_at": notif.createdAt.formatted(.iso8601),
|
||||
"account": notif.account.id,
|
||||
]
|
||||
SentrySDK.addBreadcrumb(crumb)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -190,7 +185,7 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
|||
completion(.failure(.client(error)))
|
||||
|
||||
case let .success(notifications, _):
|
||||
let notifications = self.validateNotifications(notifications)
|
||||
self.validateNotifications(notifications)
|
||||
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
||||
|
||||
if !notifications.isEmpty {
|
||||
|
@ -220,7 +215,7 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
|||
completion(.failure(.client(error)))
|
||||
|
||||
case let .success(newNotifications, _):
|
||||
let newNotifications = self.validateNotifications(newNotifications)
|
||||
self.validateNotifications(newNotifications)
|
||||
if !newNotifications.isEmpty {
|
||||
self.older = .before(id: newNotifications.last!.id, count: nil)
|
||||
}
|
||||
|
@ -251,7 +246,7 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
|
|||
completion(.failure(.client(error)))
|
||||
|
||||
case let .success(newNotifications, _):
|
||||
let newNotifications = self.validateNotifications(newNotifications)
|
||||
self.validateNotifications(newNotifications)
|
||||
guard !newNotifications.isEmpty else {
|
||||
completion(.failure(.allCaughtUp))
|
||||
return
|
||||
|
|
|
@ -45,9 +45,7 @@ struct AboutView: View {
|
|||
.appGroupedListRowBackground()
|
||||
|
||||
Section {
|
||||
Link(destination: URL(string: "https://vaccor.space/tusker")!) {
|
||||
Label("Website", systemImage: "safari")
|
||||
}
|
||||
Link("Website", destination: URL(string: "https://vaccor.space/tusker")!)
|
||||
Button {
|
||||
if MFMailComposeViewController.canSendMail() {
|
||||
Task {
|
||||
|
@ -58,7 +56,7 @@ struct AboutView: View {
|
|||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Label("Get Support", systemImage: "envelope")
|
||||
Text("Get Support")
|
||||
Spacer()
|
||||
if isGettingLogData {
|
||||
ProgressView()
|
||||
|
@ -67,14 +65,9 @@ struct AboutView: View {
|
|||
}
|
||||
}
|
||||
.disabled(isGettingLogData)
|
||||
Link(destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker")!) {
|
||||
Label("Source Code", systemImage: "curlybraces")
|
||||
}
|
||||
Link(destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker/issues")!) {
|
||||
Label("Issue Tracker", systemImage: "checklist")
|
||||
}
|
||||
Link("Source Code", destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker")!)
|
||||
Link("Issue Tracker", destination: URL(string: "https://git.shadowfacts.net/shadowfacts/Tusker/issues")!)
|
||||
}
|
||||
.labelStyle(AboutLinksLabelStyle())
|
||||
.appGroupedListRowBackground()
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
|
@ -173,15 +166,6 @@ private struct MailSheet: UIViewControllerRepresentable {
|
|||
}
|
||||
}
|
||||
|
||||
private struct AboutLinksLabelStyle: LabelStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack(alignment: .lastTextBaseline, spacing: 8) {
|
||||
configuration.icon
|
||||
configuration.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AboutView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AboutView()
|
||||
|
|
|
@ -18,10 +18,6 @@ class ProfileNoContentCollectionViewCell: UICollectionViewListCell {
|
|||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
var config = UIBackgroundConfiguration.listPlainCell()
|
||||
config.backgroundColor = .appBackground
|
||||
backgroundConfiguration = config
|
||||
|
||||
let title = UILabel()
|
||||
title.text = "There's nothing here"
|
||||
title.adjustsFontForContentSizeCategory = true
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
import Sentry
|
||||
import OSLog
|
||||
|
||||
struct ToastConfiguration {
|
||||
var systemImageName: String?
|
||||
|
@ -90,8 +89,6 @@ fileprivate extension Pachyderm.Client.Error {
|
|||
}
|
||||
}
|
||||
|
||||
private let toastErrorLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ToastError")
|
||||
|
||||
private func captureError(_ error: Client.Error, title: String) {
|
||||
let event = Event(error: error)
|
||||
event.message = SentryMessage(formatted: "\(title): \(error)")
|
||||
|
@ -126,6 +123,4 @@ private func captureError(_ error: Client.Error, title: String) {
|
|||
return
|
||||
}
|
||||
SentrySDK.capture(event: event)
|
||||
|
||||
toastErrorLogger.error("\(title, privacy: .public): \(error), \(event.tags!.debugDescription, privacy: .public)")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue