forked from zelo72/mastodon-ios
feat: add video player for audio/video kind media
This commit is contained in:
parent
1789e6eb86
commit
582843f54a
|
@ -27,9 +27,7 @@
|
||||||
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
||||||
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
|
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; };
|
||||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
|
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; };
|
||||||
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; };
|
|
||||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
|
||||||
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */; };
|
|
||||||
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; };
|
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; };
|
||||||
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */; };
|
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */; };
|
||||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; };
|
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; };
|
||||||
|
@ -80,7 +78,6 @@
|
||||||
2D9DB96B263A91D1007C1D71 /* APIService+DomainBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB96A263A91D1007C1D71 /* APIService+DomainBlock.swift */; };
|
2D9DB96B263A91D1007C1D71 /* APIService+DomainBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB96A263A91D1007C1D71 /* APIService+DomainBlock.swift */; };
|
||||||
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA504682601ADE7008F4E6C /* SawToothView.swift */; };
|
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA504682601ADE7008F4E6C /* SawToothView.swift */; };
|
||||||
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA6054625F716A2006356F9 /* PlaybackState.swift */; };
|
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA6054625F716A2006356F9 /* PlaybackState.swift */; };
|
||||||
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA6055025F74407006356F9 /* AudioContainerViewModel.swift */; };
|
|
||||||
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; };
|
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; };
|
||||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
|
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; };
|
||||||
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */; };
|
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */; };
|
||||||
|
@ -111,11 +108,7 @@
|
||||||
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; };
|
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; };
|
||||||
5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; };
|
5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; };
|
||||||
5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */; };
|
5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */; };
|
||||||
5DF1054125F886D400D6C0D4 /* VideoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* VideoPlaybackService.swift */; };
|
|
||||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; };
|
|
||||||
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; };
|
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; };
|
||||||
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */; };
|
|
||||||
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */; };
|
|
||||||
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; };
|
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; };
|
||||||
5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; };
|
5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; };
|
||||||
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
|
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
|
||||||
|
@ -262,7 +255,6 @@
|
||||||
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; };
|
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; };
|
||||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; };
|
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; };
|
||||||
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */; };
|
DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */; };
|
||||||
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; };
|
|
||||||
DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0962269ED06300D62E92 /* SearchResultViewController.swift */; };
|
DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0962269ED06300D62E92 /* SearchResultViewController.swift */; };
|
||||||
DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */; };
|
DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0965269ED52200D62E92 /* SearchResultViewModel.swift */; };
|
||||||
DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */; };
|
DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F0967269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift */; };
|
||||||
|
@ -455,8 +447,6 @@
|
||||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
|
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
|
||||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
|
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; };
|
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; };
|
||||||
DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */; };
|
|
||||||
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */; };
|
|
||||||
DB9D7C21269824B80054B3DF /* APIService+Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D7C20269824B80054B3DF /* APIService+Filter.swift */; };
|
DB9D7C21269824B80054B3DF /* APIService+Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D7C20269824B80054B3DF /* APIService+Filter.swift */; };
|
||||||
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; };
|
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; };
|
||||||
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9F58EB26EF435000E7BBE9 /* AccountViewController.swift */; };
|
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9F58EB26EF435000E7BBE9 /* AccountViewController.swift */; };
|
||||||
|
@ -489,6 +479,9 @@
|
||||||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
|
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; };
|
||||||
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
|
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
|
||||||
DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
|
DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; };
|
||||||
|
DBB45B5627B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5527B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift */; };
|
||||||
|
DBB45B5927B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5827B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift */; };
|
||||||
|
DBB45B5B27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */; };
|
||||||
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
|
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
|
||||||
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
|
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
|
||||||
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; };
|
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; };
|
||||||
|
@ -723,9 +716,7 @@
|
||||||
1D6D967E77A5357E2C6110D9 /* Pods-Mastodon.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - debug.xcconfig"; sourceTree = "<group>"; };
|
1D6D967E77A5357E2C6110D9 /* Pods-Mastodon.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
|
2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = "<group>"; };
|
||||||
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
|
2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = "<group>"; };
|
||||||
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; };
|
|
||||||
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
|
||||||
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackService.swift; sourceTree = "<group>"; };
|
|
||||||
2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; };
|
2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; };
|
||||||
2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Gesture.swift"; sourceTree = "<group>"; };
|
2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Gesture.swift"; sourceTree = "<group>"; };
|
||||||
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -773,7 +764,6 @@
|
||||||
2D9DB96A263A91D1007C1D71 /* APIService+DomainBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+DomainBlock.swift"; sourceTree = "<group>"; };
|
2D9DB96A263A91D1007C1D71 /* APIService+DomainBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+DomainBlock.swift"; sourceTree = "<group>"; };
|
||||||
2DA504682601ADE7008F4E6C /* SawToothView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SawToothView.swift; sourceTree = "<group>"; };
|
2DA504682601ADE7008F4E6C /* SawToothView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SawToothView.swift; sourceTree = "<group>"; };
|
||||||
2DA6054625F716A2006356F9 /* PlaybackState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackState.swift; sourceTree = "<group>"; };
|
2DA6054625F716A2006356F9 /* PlaybackState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackState.swift; sourceTree = "<group>"; };
|
||||||
2DA6055025F74407006356F9 /* AudioContainerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerViewModel.swift; sourceTree = "<group>"; };
|
|
||||||
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
|
||||||
|
@ -812,11 +802,7 @@
|
||||||
5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = "<group>"; };
|
5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = "<group>"; };
|
||||||
5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = "<group>"; };
|
5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = "<group>"; };
|
||||||
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = "<group>"; };
|
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = "<group>"; };
|
||||||
5DF1054025F886D400D6C0D4 /* VideoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlaybackService.swift; sourceTree = "<group>"; };
|
|
||||||
5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
|
||||||
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = "<group>"; };
|
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = "<group>"; };
|
||||||
5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerContainerView.swift; sourceTree = "<group>"; };
|
|
||||||
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = "<group>"; };
|
|
||||||
6130CBE4B26E3C976ACC1688 /* Pods-ShareActionExtension.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareActionExtension.asdk - debug.xcconfig"; path = "Target Support Files/Pods-ShareActionExtension/Pods-ShareActionExtension.asdk - debug.xcconfig"; sourceTree = "<group>"; };
|
6130CBE4B26E3C976ACC1688 /* Pods-ShareActionExtension.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareActionExtension.asdk - debug.xcconfig"; path = "Target Support Files/Pods-ShareActionExtension/Pods-ShareActionExtension.asdk - debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = "<group>"; };
|
75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
77EE917BC055E6621C0452B6 /* Pods-ShareActionExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareActionExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareActionExtension/Pods-ShareActionExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
77EE917BC055E6621C0452B6 /* Pods-ShareActionExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareActionExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareActionExtension/Pods-ShareActionExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
@ -989,7 +975,6 @@
|
||||||
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = "<group>"; };
|
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = "<group>"; };
|
||||||
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = "<group>"; };
|
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = "<group>"; };
|
||||||
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CustomEmoji.swift"; sourceTree = "<group>"; };
|
DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CustomEmoji.swift"; sourceTree = "<group>"; };
|
||||||
DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlayerContainerView+MediaTypeIndicotorView.swift"; sourceTree = "<group>"; };
|
|
||||||
DB4B777F26CA4EFA00B087B3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Intents.strings; sourceTree = "<group>"; };
|
DB4B777F26CA4EFA00B087B3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Intents.strings; sourceTree = "<group>"; };
|
||||||
DB4B778226CA4EFA00B087B3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
DB4B778226CA4EFA00B087B3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
DB4B778326CA4EFA00B087B3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Intents.stringsdict; sourceTree = "<group>"; };
|
DB4B778326CA4EFA00B087B3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Intents.stringsdict; sourceTree = "<group>"; };
|
||||||
|
@ -1196,8 +1181,6 @@
|
||||||
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
||||||
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
|
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
|
||||||
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewContainer.swift; sourceTree = "<group>"; };
|
|
||||||
DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewModel.swift; sourceTree = "<group>"; };
|
|
||||||
DB9D7C20269824B80054B3DF /* APIService+Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Filter.swift"; sourceTree = "<group>"; };
|
DB9D7C20269824B80054B3DF /* APIService+Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Filter.swift"; sourceTree = "<group>"; };
|
||||||
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = "<group>"; };
|
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = "<group>"; };
|
||||||
DB9F58EB26EF435000E7BBE9 /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = "<group>"; };
|
DB9F58EB26EF435000E7BBE9 /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1239,6 +1222,9 @@
|
||||||
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; };
|
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; };
|
||||||
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
|
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
|
||||||
DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = "<group>"; };
|
DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = "<group>"; };
|
||||||
|
DBB45B5527B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DBB45B5827B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewTransitionViewController.swift; sourceTree = "<group>"; };
|
||||||
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
|
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
|
||||||
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
|
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
|
||||||
DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = "<group>"; };
|
DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1658,9 +1644,7 @@
|
||||||
DB9A489B26036E19008B817C /* MastodonAttachmentService */,
|
DB9A489B26036E19008B817C /* MastodonAttachmentService */,
|
||||||
DBBC24BD26A5441A00398BB9 /* ThemeService */,
|
DBBC24BD26A5441A00398BB9 /* ThemeService */,
|
||||||
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
||||||
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */,
|
|
||||||
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
|
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
|
||||||
5DF1054025F886D400D6C0D4 /* VideoPlaybackService.swift */,
|
|
||||||
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */,
|
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */,
|
||||||
2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */,
|
2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */,
|
||||||
DB4924E126312AB200E9DB22 /* NotificationService.swift */,
|
DB4924E126312AB200E9DB22 /* NotificationService.swift */,
|
||||||
|
@ -1681,7 +1665,6 @@
|
||||||
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */,
|
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */,
|
||||||
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */,
|
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */,
|
||||||
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
|
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
|
||||||
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */,
|
|
||||||
DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */,
|
DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */,
|
||||||
DB1D84372657B275000346B3 /* SegmentedControlNavigateable.swift */,
|
DB1D84372657B275000346B3 /* SegmentedControlNavigateable.swift */,
|
||||||
DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */,
|
DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */,
|
||||||
|
@ -1727,7 +1710,6 @@
|
||||||
children = (
|
children = (
|
||||||
2DA504672601ADBA008F4E6C /* Decoration */,
|
2DA504672601ADBA008F4E6C /* Decoration */,
|
||||||
2D42FF8325C82245004A627A /* Button */,
|
2D42FF8325C82245004A627A /* Button */,
|
||||||
DB9D6C1325E4F97A0051B173 /* Container */,
|
|
||||||
DBA9B90325F1D4420012E7B6 /* Control */,
|
DBA9B90325F1D4420012E7B6 /* Control */,
|
||||||
2D152A8A25C295B8009AA50C /* Content */,
|
2D152A8A25C295B8009AA50C /* Content */,
|
||||||
DB1D187125EF5BBD003F1F23 /* TableView */,
|
DB1D187125EF5BBD003F1F23 /* TableView */,
|
||||||
|
@ -2350,6 +2332,8 @@
|
||||||
DB6180DE263919350018D199 /* MediaPreview */ = {
|
DB6180DE263919350018D199 /* MediaPreview */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
DBB45B5727B39FCC002DC5A7 /* Video */,
|
||||||
|
DB6180F026391CAB0018D199 /* Image */,
|
||||||
DB6180E1263919780018D199 /* Paging */,
|
DB6180E1263919780018D199 /* Paging */,
|
||||||
DB6180DC263918E30018D199 /* MediaPreviewViewController.swift */,
|
DB6180DC263918E30018D199 /* MediaPreviewViewController.swift */,
|
||||||
DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */,
|
DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */,
|
||||||
|
@ -2360,7 +2344,6 @@
|
||||||
DB6180E1263919780018D199 /* Paging */ = {
|
DB6180E1263919780018D199 /* Paging */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB6180F026391CAB0018D199 /* Image */,
|
|
||||||
DB6180DF2639194B0018D199 /* MediaPreviewPagingViewController.swift */,
|
DB6180DF2639194B0018D199 /* MediaPreviewPagingViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Paging;
|
path = Paging;
|
||||||
|
@ -2384,6 +2367,7 @@
|
||||||
DB6180EA26391C140018D199 /* MediaPreviewTransitionItem.swift */,
|
DB6180EA26391C140018D199 /* MediaPreviewTransitionItem.swift */,
|
||||||
DB6180F526391D580018D199 /* MediaPreviewableViewController.swift */,
|
DB6180F526391D580018D199 /* MediaPreviewableViewController.swift */,
|
||||||
DB6180F726391D660018D199 /* MediaPreviewingViewController.swift */,
|
DB6180F726391D660018D199 /* MediaPreviewingViewController.swift */,
|
||||||
|
DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */,
|
||||||
);
|
);
|
||||||
path = MediaPreview;
|
path = MediaPreview;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2842,23 +2826,9 @@
|
||||||
path = Profile;
|
path = Profile;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
DB9D6C1325E4F97A0051B173 /* Container */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */,
|
|
||||||
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */,
|
|
||||||
5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */,
|
|
||||||
DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */,
|
|
||||||
);
|
|
||||||
path = Container;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
DB9D6C2025E502C60051B173 /* ViewModel */ = {
|
DB9D6C2025E502C60051B173 /* ViewModel */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */,
|
|
||||||
2DA6055025F74407006356F9 /* AudioContainerViewModel.swift */,
|
|
||||||
5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */,
|
|
||||||
DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */,
|
DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */,
|
||||||
);
|
);
|
||||||
path = ViewModel;
|
path = ViewModel;
|
||||||
|
@ -2929,6 +2899,15 @@
|
||||||
path = View;
|
path = View;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DBB45B5727B39FCC002DC5A7 /* Video */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DBB45B5527B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift */,
|
||||||
|
DBB45B5827B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift */,
|
||||||
|
);
|
||||||
|
path = Video;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DBB525132611EBB1002F1F29 /* Segmented */ = {
|
DBB525132611EBB1002F1F29 /* Segmented */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -3787,7 +3766,6 @@
|
||||||
DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */,
|
DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */,
|
||||||
DB1D843426579931000346B3 /* TableViewControllerNavigateable.swift in Sources */,
|
DB1D843426579931000346B3 /* TableViewControllerNavigateable.swift in Sources */,
|
||||||
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
||||||
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
|
|
||||||
DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */,
|
DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */,
|
||||||
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
||||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
||||||
|
@ -3799,7 +3777,6 @@
|
||||||
DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */,
|
DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */,
|
||||||
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */,
|
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */,
|
||||||
DB5B7298273112C800081888 /* FollowingListViewModel.swift in Sources */,
|
DB5B7298273112C800081888 /* FollowingListViewModel.swift in Sources */,
|
||||||
5DF1054125F886D400D6C0D4 /* VideoPlaybackService.swift in Sources */,
|
|
||||||
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */,
|
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */,
|
||||||
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
|
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
|
||||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
||||||
|
@ -3934,9 +3911,7 @@
|
||||||
2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */,
|
2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */,
|
||||||
DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */,
|
DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */,
|
||||||
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
||||||
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */,
|
|
||||||
DBBC24C126A5443100398BB9 /* SystemTheme.swift in Sources */,
|
DBBC24C126A5443100398BB9 /* SystemTheme.swift in Sources */,
|
||||||
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */,
|
|
||||||
DBE3CE01261D623D00430CC6 /* FavoriteViewModel+State.swift in Sources */,
|
DBE3CE01261D623D00430CC6 /* FavoriteViewModel+State.swift in Sources */,
|
||||||
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */,
|
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */,
|
||||||
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||||
|
@ -3974,7 +3949,6 @@
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||||
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
|
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
|
||||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
||||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
|
||||||
DB73BF43271192BB00781945 /* InstanceService.swift in Sources */,
|
DB73BF43271192BB00781945 /* InstanceService.swift in Sources */,
|
||||||
DB67D08427312970006A36CF /* APIService+Following.swift in Sources */,
|
DB67D08427312970006A36CF /* APIService+Following.swift in Sources */,
|
||||||
DB025B78278D606A002F581E /* StatusItem.swift in Sources */,
|
DB025B78278D606A002F581E /* StatusItem.swift in Sources */,
|
||||||
|
@ -3991,7 +3965,6 @@
|
||||||
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
|
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
|
||||||
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */,
|
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */,
|
||||||
5B24BBDB262DB14800A9381B /* ReportViewModel+Diffable.swift in Sources */,
|
5B24BBDB262DB14800A9381B /* ReportViewModel+Diffable.swift in Sources */,
|
||||||
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
|
||||||
DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */,
|
DB4F0968269ED8AD00D62E92 /* SearchHistoryTableHeaderView.swift in Sources */,
|
||||||
0FB3D2FE25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift in Sources */,
|
0FB3D2FE25E4CB6400AAD544 /* OnboardingHeadlineTableViewCell.swift in Sources */,
|
||||||
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */,
|
||||||
|
@ -4068,6 +4041,7 @@
|
||||||
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||||
DBF1D257269DBAC600C1C08A /* SearchDetailViewModel.swift in Sources */,
|
DBF1D257269DBAC600C1C08A /* SearchDetailViewModel.swift in Sources */,
|
||||||
DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */,
|
DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */,
|
||||||
|
DBB45B5927B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift in Sources */,
|
||||||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
||||||
DB0FCB76279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift in Sources */,
|
DB0FCB76279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift in Sources */,
|
||||||
DB0FCB7027951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift in Sources */,
|
DB0FCB7027951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift in Sources */,
|
||||||
|
@ -4118,7 +4092,6 @@
|
||||||
DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */,
|
DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */,
|
||||||
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
||||||
DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */,
|
DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */,
|
||||||
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
|
||||||
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */,
|
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */,
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||||
DB71C7CB271D5A0300BE3819 /* LineChartView.swift in Sources */,
|
DB71C7CB271D5A0300BE3819 /* LineChartView.swift in Sources */,
|
||||||
|
@ -4148,7 +4121,6 @@
|
||||||
DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */,
|
DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */,
|
||||||
DB0FCB822796AC78006C02E2 /* UserTimelineViewController+DataSourceProvider.swift in Sources */,
|
DB0FCB822796AC78006C02E2 /* UserTimelineViewController+DataSourceProvider.swift in Sources */,
|
||||||
DB63F773279A87DC00455B82 /* Notification+Property.swift in Sources */,
|
DB63F773279A87DC00455B82 /* Notification+Property.swift in Sources */,
|
||||||
DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */,
|
|
||||||
DBCBCC0D2680B908000F5B51 /* HomeTimelinePreference.swift in Sources */,
|
DBCBCC0D2680B908000F5B51 /* HomeTimelinePreference.swift in Sources */,
|
||||||
DB0EF72E26FDB24F00347686 /* SidebarListContentView.swift in Sources */,
|
DB0EF72E26FDB24F00347686 /* SidebarListContentView.swift in Sources */,
|
||||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */,
|
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */,
|
||||||
|
@ -4170,7 +4142,6 @@
|
||||||
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */,
|
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */,
|
||||||
DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */,
|
DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */,
|
||||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||||
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
|
|
||||||
DB73BF45271195AC00781945 /* APIService+CoreData+Instance.swift in Sources */,
|
DB73BF45271195AC00781945 /* APIService+CoreData+Instance.swift in Sources */,
|
||||||
DB336F21278D6D960031E64B /* MastodonEmoji.swift in Sources */,
|
DB336F21278D6D960031E64B /* MastodonEmoji.swift in Sources */,
|
||||||
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */,
|
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */,
|
||||||
|
@ -4200,6 +4171,7 @@
|
||||||
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */,
|
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */,
|
||||||
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
|
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
|
||||||
DB98EB6B27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift in Sources */,
|
DB98EB6B27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift in Sources */,
|
||||||
|
DBB45B5B27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift in Sources */,
|
||||||
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */,
|
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */,
|
||||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||||
DB4932B926F31AD300EF46D4 /* BadgeButton.swift in Sources */,
|
DB4932B926F31AD300EF46D4 /* BadgeButton.swift in Sources */,
|
||||||
|
@ -4211,6 +4183,7 @@
|
||||||
DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */,
|
DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */,
|
||||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
||||||
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
|
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
|
||||||
|
DBB45B5627B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift in Sources */,
|
||||||
DBBC24C026A5443100398BB9 /* MastodonTheme.swift in Sources */,
|
DBBC24C026A5443100398BB9 /* MastodonTheme.swift in Sources */,
|
||||||
DB0FCB8027968F70006C02E2 /* MastodonStatusThreadViewModel.swift in Sources */,
|
DB0FCB8027968F70006C02E2 /* MastodonStatusThreadViewModel.swift in Sources */,
|
||||||
DB0FCB6E27950E6B006C02E2 /* MastodonMention.swift in Sources */,
|
DB0FCB6E27950E6B006C02E2 /* MastodonMention.swift in Sources */,
|
||||||
|
@ -4257,7 +4230,6 @@
|
||||||
DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */,
|
DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */,
|
||||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
||||||
DB0FCB882796BDA9006C02E2 /* SearchItem.swift in Sources */,
|
DB0FCB882796BDA9006C02E2 /* SearchItem.swift in Sources */,
|
||||||
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
|
|
||||||
DB336F3D278D80040031E64B /* FeedFetchedResultsController.swift in Sources */,
|
DB336F3D278D80040031E64B /* FeedFetchedResultsController.swift in Sources */,
|
||||||
DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */,
|
DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */,
|
||||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<key>AppShared.xcscheme_^#shared#^_</key>
|
<key>AppShared.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>25</integer>
|
<integer>20</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>27</integer>
|
<integer>19</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>26</integer>
|
<integer>18</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
//
|
|
||||||
// NeedsDependency+AVPlayerViewControllerDelegate.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by xiaojian sun on 2021/3/10.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import AVKit
|
|
||||||
|
|
||||||
extension NeedsDependency where Self: AVPlayerViewControllerDelegate {
|
|
||||||
|
|
||||||
func handlePlayerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
|
||||||
context.videoPlaybackService.playerViewModel(for: playerViewController)?.isFullScreenPresentationing = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlePlayerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
|
||||||
context.videoPlaybackService.playerViewModel(for: playerViewController)?.isFullScreenPresentationing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -122,9 +122,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & MediaPrev
|
||||||
let needsToggleMediaSensitive: Bool = try await managedObjectContext.perform {
|
let needsToggleMediaSensitive: Bool = try await managedObjectContext.perform {
|
||||||
guard let _status = status.object(in: managedObjectContext) else { return false }
|
guard let _status = status.object(in: managedObjectContext) else { return false }
|
||||||
let status = _status.reblog ?? _status
|
let status = _status.reblog ?? _status
|
||||||
guard status.sensitive else { return false }
|
return status.isMediaSensitiveToggled ? !status.sensitive : status.sensitive
|
||||||
guard status.isMediaSensitiveToggled else { return true }
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard !needsToggleMediaSensitive else {
|
guard !needsToggleMediaSensitive else {
|
||||||
|
@ -407,5 +405,29 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
|
||||||
)
|
)
|
||||||
} // end Task
|
} // end Task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tableViewCell(
|
||||||
|
_ cell: UITableViewCell,
|
||||||
|
statusView: StatusView,
|
||||||
|
mediaGridContainerView: MediaGridContainerView,
|
||||||
|
mediaSensitiveButtonDidPressed button: UIButton
|
||||||
|
) {
|
||||||
|
Task {
|
||||||
|
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
|
||||||
|
guard let item = await item(from: source) else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard case let .status(status) = item else {
|
||||||
|
assertionFailure("only works for status data provider")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try await DataSourceFacade.responseToToggleMediaSensitiveAction(
|
||||||
|
dependency: self,
|
||||||
|
status: status
|
||||||
|
)
|
||||||
|
} // end Task
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -163,3 +163,21 @@ extension MediaPreviewImageViewController {
|
||||||
case share
|
case share
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - MediaPreviewTransitionViewController
|
||||||
|
extension MediaPreviewImageViewController: MediaPreviewTransitionViewController {
|
||||||
|
var mediaPreviewTransitionContext: MediaPreviewTransitionContext? {
|
||||||
|
let imageView = previewImageView.imageView
|
||||||
|
let _snapshot: UIView? = imageView.snapshotView(afterScreenUpdates: false)
|
||||||
|
|
||||||
|
guard let snapshot = _snapshot else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return MediaPreviewTransitionContext(
|
||||||
|
transitionView: imageView,
|
||||||
|
snapshot: snapshot,
|
||||||
|
snapshotTransitioning: snapshot
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -105,7 +105,7 @@ extension MediaPreviewViewController {
|
||||||
.sink { [weak self] index in
|
.sink { [weak self] index in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
switch self.viewModel.transitionItem.source {
|
switch self.viewModel.transitionItem.source {
|
||||||
case .attachment(_):
|
case .attachment:
|
||||||
break
|
break
|
||||||
case .attachments(let mediaGridContainerView):
|
case .attachments(let mediaGridContainerView):
|
||||||
UIView.animate(withDuration: 0.3) {
|
UIView.animate(withDuration: 0.3) {
|
||||||
|
@ -117,6 +117,24 @@ extension MediaPreviewViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
viewModel.$currentPage
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] index in
|
||||||
|
guard let self = self else { return }
|
||||||
|
switch self.viewModel.item {
|
||||||
|
case .attachment(let previewContext):
|
||||||
|
let needsHideCloseButton: Bool = {
|
||||||
|
guard index < previewContext.attachments.count else { return false }
|
||||||
|
let attachment = previewContext.attachments[index]
|
||||||
|
return attachment.kind == .video // not hide buttno for audio
|
||||||
|
}()
|
||||||
|
self.closeButtonBackground.isHidden = needsHideCloseButton
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -147,6 +165,10 @@ extension MediaPreviewViewController: MediaPreviewingViewController {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: dismissible %s", ((#file as NSString).lastPathComponent), #line, #function, dismissible ? "true" : "false")
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: dismissible %s", ((#file as NSString).lastPathComponent), #line, #function, dismissible ? "true" : "false")
|
||||||
return dismissible
|
return dismissible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let _ = pagingViewController.currentViewController as? MediaPreviewVideoViewController {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: dismissible false", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: dismissible false", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -38,17 +38,42 @@ final class MediaPreviewViewModel: NSObject {
|
||||||
case .attachment(let previewContext):
|
case .attachment(let previewContext):
|
||||||
currentPage = previewContext.initialIndex
|
currentPage = previewContext.initialIndex
|
||||||
for (i, attachment) in previewContext.attachments.enumerated() {
|
for (i, attachment) in previewContext.attachments.enumerated() {
|
||||||
let viewController = MediaPreviewImageViewController()
|
switch attachment.kind {
|
||||||
let viewModel = MediaPreviewImageViewModel(
|
case .image:
|
||||||
context: context,
|
let viewController = MediaPreviewImageViewController()
|
||||||
item: .remote(.init(
|
let viewModel = MediaPreviewImageViewModel(
|
||||||
assetURL: attachment.assetURL.flatMap { URL(string: $0) },
|
context: context,
|
||||||
thumbnail: previewContext.thumbnail(at: i),
|
item: .remote(.init(
|
||||||
altText: attachment.altDescription
|
assetURL: attachment.assetURL.flatMap { URL(string: $0) },
|
||||||
))
|
thumbnail: previewContext.thumbnail(at: i),
|
||||||
)
|
altText: attachment.altDescription
|
||||||
viewController.viewModel = viewModel
|
))
|
||||||
viewControllers.append(viewController)
|
)
|
||||||
|
viewController.viewModel = viewModel
|
||||||
|
viewControllers.append(viewController)
|
||||||
|
case .gifv:
|
||||||
|
let viewController = MediaPreviewVideoViewController()
|
||||||
|
let viewModel = MediaPreviewVideoViewModel(
|
||||||
|
context: context,
|
||||||
|
item: .gif(.init(
|
||||||
|
assetURL: attachment.assetURL.flatMap { URL(string: $0) },
|
||||||
|
previewURL: attachment.previewURL.flatMap { URL(string: $0) }
|
||||||
|
))
|
||||||
|
)
|
||||||
|
viewController.viewModel = viewModel
|
||||||
|
viewControllers.append(viewController)
|
||||||
|
case .video, .audio:
|
||||||
|
let viewController = MediaPreviewVideoViewController()
|
||||||
|
let viewModel = MediaPreviewVideoViewModel(
|
||||||
|
context: context,
|
||||||
|
item: .video(.init(
|
||||||
|
assetURL: attachment.assetURL.flatMap { URL(string: $0) },
|
||||||
|
previewURL: attachment.previewURL.flatMap { URL(string: $0) }
|
||||||
|
))
|
||||||
|
)
|
||||||
|
viewController.viewModel = viewModel
|
||||||
|
viewControllers.append(viewController)
|
||||||
|
} // end switch attachment.kind { … }
|
||||||
} // end for … in …
|
} // end for … in …
|
||||||
case .profileAvatar(let previewContext):
|
case .profileAvatar(let previewContext):
|
||||||
let viewController = MediaPreviewImageViewController()
|
let viewController = MediaPreviewImageViewController()
|
||||||
|
@ -75,68 +100,13 @@ final class MediaPreviewViewModel: NSObject {
|
||||||
viewController.viewModel = viewModel
|
viewController.viewModel = viewModel
|
||||||
viewControllers.append(viewController)
|
viewControllers.append(viewController)
|
||||||
} // end switch
|
} // end switch
|
||||||
// let status = managedObjectContext.object(with: meta.statusObjectID) as! Status
|
|
||||||
// for (entity, image) in zip(status.attachments, meta.preloadThumbnailImages) {
|
|
||||||
// let thumbnail: UIImage? = image.flatMap { $0.size != CGSize(width: 1, height: 1) ? $0 : nil }
|
|
||||||
// switch entity.kind {
|
|
||||||
// case .image:
|
|
||||||
// guard let url = URL(string: entity.assetURL ?? "") else { continue }
|
|
||||||
// let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: url, thumbnail: thumbnail, altText: entity.altDescription)
|
|
||||||
// let mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
|
|
||||||
// let mediaPreviewImageViewController = MediaPreviewImageViewController()
|
|
||||||
// mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
|
|
||||||
// viewControllers.append(mediaPreviewImageViewController)
|
|
||||||
// default:
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
self.viewControllers = viewControllers
|
self.viewControllers = viewControllers
|
||||||
self.currentPage = currentPage
|
self.currentPage = currentPage
|
||||||
self.transitionItem = transitionItem
|
self.transitionItem = transitionItem
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
// init(context: AppContext, meta: ProfileBannerImagePreviewMeta, pushTransitionItem: MediaPreviewTransitionItem) {
|
|
||||||
// self.context = context
|
|
||||||
// self.item = .profileBanner(meta)
|
|
||||||
// var viewControllers: [UIViewController] = []
|
|
||||||
// let managedObjectContext = self.context.managedObjectContext
|
|
||||||
// managedObjectContext.performAndWait {
|
|
||||||
// let account = managedObjectContext.object(with: meta.accountObjectID) as! MastodonUser
|
|
||||||
// let avatarURL = account.headerImageURLWithFallback(domain: account.domain)
|
|
||||||
// let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: avatarURL, thumbnail: meta.preloadThumbnailImage, altText: nil)
|
|
||||||
// let mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
|
|
||||||
// let mediaPreviewImageViewController = MediaPreviewImageViewController()
|
|
||||||
// mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
|
|
||||||
// viewControllers.append(mediaPreviewImageViewController)
|
|
||||||
// }
|
|
||||||
// self.viewControllers = viewControllers
|
|
||||||
// self.currentPage = CurrentValueSubject(0)
|
|
||||||
// self.transitionItem = pushTransitionItem
|
|
||||||
// super.init()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// init(context: AppContext, meta: ProfileAvatarImagePreviewMeta, pushTransitionItem: MediaPreviewTransitionItem) {
|
|
||||||
// self.context = context
|
|
||||||
// self.item = .profileAvatar(meta)
|
|
||||||
// var viewControllers: [UIViewController] = []
|
|
||||||
// let managedObjectContext = self.context.managedObjectContext
|
|
||||||
// managedObjectContext.performAndWait {
|
|
||||||
// let account = managedObjectContext.object(with: meta.accountObjectID) as! MastodonUser
|
|
||||||
// let avatarURL = account.avatarImageURLWithFallback(domain: account.domain)
|
|
||||||
// let meta = MediaPreviewImageViewModel.RemoteImagePreviewMeta(url: avatarURL, thumbnail: meta.preloadThumbnailImage, altText: nil)
|
|
||||||
// let mediaPreviewImageModel = MediaPreviewImageViewModel(meta: meta)
|
|
||||||
// let mediaPreviewImageViewController = MediaPreviewImageViewController()
|
|
||||||
// mediaPreviewImageViewController.viewModel = mediaPreviewImageModel
|
|
||||||
// viewControllers.append(mediaPreviewImageViewController)
|
|
||||||
// }
|
|
||||||
// self.viewControllers = viewControllers
|
|
||||||
// self.currentPage = CurrentValueSubject(0)
|
|
||||||
// self.transitionItem = pushTransitionItem
|
|
||||||
// super.init()
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MediaPreviewViewModel {
|
extension MediaPreviewViewModel {
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
//
|
||||||
|
// MediaPreviewVideoViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-2-9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import AVKit
|
||||||
|
import Combine
|
||||||
|
import func AVFoundation.AVMakeRect
|
||||||
|
|
||||||
|
final class MediaPreviewVideoViewController: UIViewController {
|
||||||
|
|
||||||
|
let logger = Logger(subsystem: "MediaPreviewVideoViewController", category: "ViewController")
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var viewModel: MediaPreviewVideoViewModel!
|
||||||
|
|
||||||
|
let playerViewController = AVPlayerViewController()
|
||||||
|
// var pictureInPictureController: AVPictureInPictureController?
|
||||||
|
|
||||||
|
let previewImageView = UIImageView()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
playerViewController.player?.pause()
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.ambient)
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MediaPreviewVideoViewController {
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
addChild(playerViewController)
|
||||||
|
playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(playerViewController.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
playerViewController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||||
|
playerViewController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||||
|
playerViewController.view.widthAnchor.constraint(equalTo: view.widthAnchor),
|
||||||
|
playerViewController.view.heightAnchor.constraint(equalTo: view.heightAnchor),
|
||||||
|
])
|
||||||
|
playerViewController.didMove(toParent: self)
|
||||||
|
|
||||||
|
if let contentOverlayView = playerViewController.contentOverlayView {
|
||||||
|
previewImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentOverlayView.addSubview(previewImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
previewImageView.topAnchor.constraint(equalTo: contentOverlayView.topAnchor),
|
||||||
|
previewImageView.leadingAnchor.constraint(equalTo: contentOverlayView.leadingAnchor),
|
||||||
|
previewImageView.trailingAnchor.constraint(equalTo: contentOverlayView.trailingAnchor),
|
||||||
|
previewImageView.bottomAnchor.constraint(equalTo: contentOverlayView.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
playerViewController.delegate = self
|
||||||
|
playerViewController.view.backgroundColor = .clear
|
||||||
|
playerViewController.player = viewModel.player
|
||||||
|
playerViewController.allowsPictureInPicturePlayback = true
|
||||||
|
|
||||||
|
switch viewModel.item {
|
||||||
|
case .video:
|
||||||
|
break
|
||||||
|
case .gif:
|
||||||
|
playerViewController.showsPlaybackControls = false
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.player?.play()
|
||||||
|
viewModel.playbackState = .playing
|
||||||
|
|
||||||
|
if let previewURL = viewModel.item.previewURL {
|
||||||
|
previewImageView.contentMode = .scaleAspectFit
|
||||||
|
previewImageView.af.setImage(
|
||||||
|
withURL: previewURL,
|
||||||
|
placeholderImage: .placeholder(color: .systemFill)
|
||||||
|
)
|
||||||
|
|
||||||
|
playerViewController.publisher(for: \.isReadyForDisplay)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] isReadyForDisplay in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.previewImageView.isHidden = isReadyForDisplay
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ShareActivityProvider
|
||||||
|
//extension MediaPreviewVideoViewController: ShareActivityProvider {
|
||||||
|
// var activities: [Any] {
|
||||||
|
// return []
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var applicationActivities: [UIActivity] {
|
||||||
|
// switch viewModel.item {
|
||||||
|
// case .gif(let mediaContext):
|
||||||
|
// guard let url = mediaContext.assetURL else { return [] }
|
||||||
|
// return [
|
||||||
|
// SavePhotoActivity(context: viewModel.context, url: url, resourceType: .video)
|
||||||
|
// ]
|
||||||
|
// default:
|
||||||
|
// return []
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
// MARK: - AVPlayerViewControllerDelegate
|
||||||
|
extension MediaPreviewVideoViewController: AVPlayerViewControllerDelegate {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - MediaPreviewTransitionViewController
|
||||||
|
extension MediaPreviewVideoViewController: MediaPreviewTransitionViewController {
|
||||||
|
var mediaPreviewTransitionContext: MediaPreviewTransitionContext? {
|
||||||
|
guard let playerView = playerViewController.view else { return nil }
|
||||||
|
let _currentFrame: UIImage? = {
|
||||||
|
guard let player = playerViewController.player else { return nil }
|
||||||
|
guard let asset = player.currentItem?.asset else { return nil }
|
||||||
|
let assetImageGenerator = AVAssetImageGenerator(asset: asset)
|
||||||
|
assetImageGenerator.appliesPreferredTrackTransform = true // fix orientation
|
||||||
|
do {
|
||||||
|
let cgImage = try assetImageGenerator.copyCGImage(at: player.currentTime(), actualTime: nil)
|
||||||
|
let image = UIImage(cgImage: cgImage)
|
||||||
|
return image
|
||||||
|
} catch {
|
||||||
|
return previewImageView.image
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
let _snapshot: UIView? = {
|
||||||
|
guard let currentFrame = _currentFrame else { return nil }
|
||||||
|
let size = AVMakeRect(aspectRatio: currentFrame.size, insideRect: view.frame).size
|
||||||
|
let imageView = UIImageView(frame: CGRect(origin: .zero, size: size))
|
||||||
|
imageView.image = currentFrame
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
guard let snapshot = _snapshot else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return MediaPreviewTransitionContext(
|
||||||
|
transitionView: playerView,
|
||||||
|
snapshot: snapshot,
|
||||||
|
snapshotTransitioning: snapshot
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
//
|
||||||
|
// MediaPreviewVideoViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022-2-9.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import AVKit
|
||||||
|
import Combine
|
||||||
|
import AlamofireImage
|
||||||
|
|
||||||
|
final class MediaPreviewVideoViewModel {
|
||||||
|
|
||||||
|
let logger = Logger(subsystem: "MediaPreviewVideoViewModel", category: "ViewModel")
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// input
|
||||||
|
let context: AppContext
|
||||||
|
let item: Item
|
||||||
|
|
||||||
|
// output
|
||||||
|
public private(set) var player: AVPlayer?
|
||||||
|
private var playerLooper: AVPlayerLooper?
|
||||||
|
@Published var playbackState = PlaybackState.unknown
|
||||||
|
|
||||||
|
init(context: AppContext, item: Item) {
|
||||||
|
self.context = context
|
||||||
|
self.item = item
|
||||||
|
// end init
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .video(let mediaContext):
|
||||||
|
guard let assertURL = mediaContext.assetURL else { return }
|
||||||
|
let playerItem = AVPlayerItem(url: assertURL)
|
||||||
|
let _player = AVPlayer(playerItem: playerItem)
|
||||||
|
self.player = _player
|
||||||
|
|
||||||
|
case .gif(let mediaContext):
|
||||||
|
guard let assertURL = mediaContext.assetURL else { return }
|
||||||
|
let playerItem = AVPlayerItem(url: assertURL)
|
||||||
|
let _player = AVQueuePlayer(playerItem: playerItem)
|
||||||
|
_player.isMuted = true
|
||||||
|
self.player = _player
|
||||||
|
if let templateItem = _player.items().first {
|
||||||
|
let _playerLooper = AVPlayerLooper(player: _player, templateItem: templateItem)
|
||||||
|
self.playerLooper = _playerLooper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let player = player else {
|
||||||
|
assertionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup player state observer
|
||||||
|
$playbackState
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] status in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): player state: \(status.description)")
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case .unknown, .buffering, .readyToPlay:
|
||||||
|
break
|
||||||
|
case .playing:
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.playback)
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
case .paused, .stopped, .failed:
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.ambient) // set to ambient to allow mixed (needed for GIFV)
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
player.publisher(for: \.status, options: [.initial, .new])
|
||||||
|
.sink(receiveValue: { [weak self] status in
|
||||||
|
guard let self = self else { return }
|
||||||
|
switch status {
|
||||||
|
case .failed:
|
||||||
|
self.playbackState = .failed
|
||||||
|
case .readyToPlay:
|
||||||
|
self.playbackState = .readyToPlay
|
||||||
|
case .unknown:
|
||||||
|
self.playbackState = .unknown
|
||||||
|
@unknown default:
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: nil)
|
||||||
|
.sink { [weak self] notification in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let playerItem = notification.object as? AVPlayerItem,
|
||||||
|
let urlAsset = playerItem.asset as? AVURLAsset
|
||||||
|
else { return }
|
||||||
|
print(urlAsset.url)
|
||||||
|
guard urlAsset.url == item.assetURL else { return }
|
||||||
|
self.playbackState = .stopped
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MediaPreviewVideoViewModel {
|
||||||
|
|
||||||
|
enum Item {
|
||||||
|
case video(RemoteVideoContext)
|
||||||
|
case gif(RemoteGIFContext)
|
||||||
|
|
||||||
|
var previewURL: URL? {
|
||||||
|
switch self {
|
||||||
|
case .video(let mediaContext): return mediaContext.previewURL
|
||||||
|
case .gif(let mediaContext): return mediaContext.previewURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var assetURL: URL? {
|
||||||
|
switch self {
|
||||||
|
case .video(let mediaContext): return mediaContext.assetURL
|
||||||
|
case .gif(let mediaContext): return mediaContext.assetURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RemoteVideoContext {
|
||||||
|
let assetURL: URL?
|
||||||
|
let previewURL: URL?
|
||||||
|
// let thumbnail: UIImage?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RemoteGIFContext {
|
||||||
|
let assetURL: URL?
|
||||||
|
let previewURL: URL?
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,131 +0,0 @@
|
||||||
//
|
|
||||||
// AudioViewContainer.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by sxiaojian on 2021/3/8.
|
|
||||||
//
|
|
||||||
|
|
||||||
import CoreDataStack
|
|
||||||
import os.log
|
|
||||||
import UIKit
|
|
||||||
import MastodonAsset
|
|
||||||
import MastodonLocalization
|
|
||||||
|
|
||||||
final class AudioContainerView: UIView {
|
|
||||||
static let cornerRadius: CGFloat = 22
|
|
||||||
|
|
||||||
let container: UIStackView = {
|
|
||||||
let stackView = UIStackView()
|
|
||||||
stackView.axis = .horizontal
|
|
||||||
stackView.distribution = .fill
|
|
||||||
stackView.alignment = .center
|
|
||||||
stackView.spacing = 11
|
|
||||||
stackView.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
|
|
||||||
stackView.isLayoutMarginsRelativeArrangement = true
|
|
||||||
stackView.layer.cornerRadius = AudioContainerView.cornerRadius
|
|
||||||
stackView.clipsToBounds = true
|
|
||||||
stackView.backgroundColor = Asset.Colors.brandBlue.color
|
|
||||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return stackView
|
|
||||||
}()
|
|
||||||
|
|
||||||
let playButtonBackgroundView: UIView = {
|
|
||||||
let view = UIView()
|
|
||||||
view.layer.cornerRadius = 16
|
|
||||||
view.clipsToBounds = true
|
|
||||||
view.backgroundColor = Asset.Colors.brandBlue.color
|
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return view
|
|
||||||
}()
|
|
||||||
|
|
||||||
let playButton: UIButton = {
|
|
||||||
let button = HighlightDimmableButton(type: .custom)
|
|
||||||
let image = UIImage(systemName: "play.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 32, weight: .bold))!
|
|
||||||
button.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal)
|
|
||||||
|
|
||||||
let pauseImage = UIImage(systemName: "pause.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 32, weight: .bold))!
|
|
||||||
button.setImage(pauseImage.withRenderingMode(.alwaysTemplate), for: .selected)
|
|
||||||
|
|
||||||
button.tintColor = .white
|
|
||||||
button.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
button.isEnabled = true
|
|
||||||
return button
|
|
||||||
}()
|
|
||||||
|
|
||||||
let slider: UISlider = {
|
|
||||||
let slider = UISlider()
|
|
||||||
slider.isContinuous = true
|
|
||||||
slider.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
slider.minimumTrackTintColor = Asset.Colors.Slider.track.color
|
|
||||||
slider.maximumTrackTintColor = Asset.Colors.Slider.track.color
|
|
||||||
if let image = UIImage.placeholder(size: CGSize(width: 22, height: 22), color: .white).withRoundedCorners(radius: 11) {
|
|
||||||
slider.setThumbImage(image, for: .normal)
|
|
||||||
}
|
|
||||||
return slider
|
|
||||||
}()
|
|
||||||
|
|
||||||
let timeLabel: UILabel = {
|
|
||||||
let label = UILabel()
|
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
label.font = .systemFont(ofSize: 13, weight: .regular)
|
|
||||||
label.textColor = .white
|
|
||||||
label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .right : .left
|
|
||||||
return label
|
|
||||||
}()
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AudioContainerView {
|
|
||||||
private func _init() {
|
|
||||||
addSubview(container)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
container.topAnchor.constraint(equalTo: topAnchor),
|
|
||||||
container.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
|
||||||
bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
// checkmark
|
|
||||||
playButtonBackgroundView.addSubview(playButton)
|
|
||||||
container.addArrangedSubview(playButtonBackgroundView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
playButton.centerXAnchor.constraint(equalTo: playButtonBackgroundView.centerXAnchor),
|
|
||||||
playButton.centerYAnchor.constraint(equalTo: playButtonBackgroundView.centerYAnchor),
|
|
||||||
playButtonBackgroundView.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1),
|
|
||||||
playButtonBackgroundView.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1),
|
|
||||||
])
|
|
||||||
|
|
||||||
container.addArrangedSubview(slider)
|
|
||||||
|
|
||||||
container.addArrangedSubview(timeLabel)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
timeLabel.widthAnchor.constraint(equalToConstant: 40).priority(.required - 1),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if canImport(SwiftUI) && DEBUG
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct AudioContainerView_Previews: PreviewProvider {
|
|
||||||
|
|
||||||
static var previews: some View {
|
|
||||||
UIViewPreview(width: 375) {
|
|
||||||
AudioContainerView()
|
|
||||||
}
|
|
||||||
.previewLayout(.fixed(width: 375, height: 100))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
|
@ -1,497 +0,0 @@
|
||||||
//
|
|
||||||
// MosaicImageViewContainer.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by Cirno MainasuK on 2021-2-23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import os.log
|
|
||||||
import func AVFoundation.AVMakeRect
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
protocol MosaicImageViewContainerPresentable: AnyObject {
|
|
||||||
var mosaicImageViewContainer: MosaicImageViewContainer { get }
|
|
||||||
var isRevealing: Bool { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol MosaicImageViewContainerDelegate: AnyObject {
|
|
||||||
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
|
||||||
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
|
||||||
}
|
|
||||||
|
|
||||||
final class MosaicImageViewContainer: UIView {
|
|
||||||
|
|
||||||
weak var delegate: MosaicImageViewContainerDelegate?
|
|
||||||
|
|
||||||
let container = UIStackView()
|
|
||||||
private(set) lazy var imageViews: [UIImageView] = {
|
|
||||||
(0..<4).map { _ -> UIImageView in
|
|
||||||
let imageView = UIImageView()
|
|
||||||
imageView.isUserInteractionEnabled = true
|
|
||||||
let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer
|
|
||||||
tapGesture.addTarget(self, action: #selector(MosaicImageViewContainer.photoTapGestureRecognizerHandler(_:)))
|
|
||||||
imageView.addGestureRecognizer(tapGesture)
|
|
||||||
imageView.isAccessibilityElement = true
|
|
||||||
imageView.backgroundColor = .systemFill
|
|
||||||
return imageView
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
let blurhashOverlayImageViews: [UIImageView] = {
|
|
||||||
(0..<4).map { _ in UIImageView() }
|
|
||||||
}()
|
|
||||||
|
|
||||||
let contentWarningOverlayView: ContentWarningOverlayView = {
|
|
||||||
let contentWarningOverlayView = ContentWarningOverlayView()
|
|
||||||
contentWarningOverlayView.configure(style: .media)
|
|
||||||
return contentWarningOverlayView
|
|
||||||
}()
|
|
||||||
|
|
||||||
private var containerHeightLayoutConstraint: NSLayoutConstraint!
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MosaicImageViewContainer: ContentWarningOverlayViewDelegate {
|
|
||||||
func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) {
|
|
||||||
self.delegate?.mosaicImageViewContainer(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MosaicImageViewContainer {
|
|
||||||
|
|
||||||
private func _init() {
|
|
||||||
// accessibility
|
|
||||||
accessibilityIgnoresInvertColors = true
|
|
||||||
|
|
||||||
container.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
container.axis = .horizontal
|
|
||||||
container.distribution = .fillEqually
|
|
||||||
addSubview(container)
|
|
||||||
containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
container.topAnchor.constraint(equalTo: topAnchor),
|
|
||||||
container.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
|
||||||
bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
|
||||||
containerHeightLayoutConstraint
|
|
||||||
])
|
|
||||||
|
|
||||||
contentWarningOverlayView.delegate = self
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MosaicImageViewContainer {
|
|
||||||
|
|
||||||
func resetImageTask() {
|
|
||||||
imageViews.forEach { imageView in
|
|
||||||
imageView.af.cancelImageRequest()
|
|
||||||
imageView.image = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func reset() {
|
|
||||||
resetImageTask()
|
|
||||||
|
|
||||||
container.arrangedSubviews.forEach { subview in
|
|
||||||
container.removeArrangedSubview(subview)
|
|
||||||
subview.removeFromSuperview()
|
|
||||||
}
|
|
||||||
container.subviews.forEach { subview in
|
|
||||||
subview.removeFromSuperview()
|
|
||||||
}
|
|
||||||
imageViews.forEach { imageView in
|
|
||||||
imageView.constraints.forEach { imageView.removeConstraint($0) }
|
|
||||||
imageView.removeFromSuperview()
|
|
||||||
imageView.layer.maskedCorners = [
|
|
||||||
.layerMinXMinYCorner, .layerMaxXMinYCorner,
|
|
||||||
.layerMinXMaxYCorner, .layerMaxXMaxYCorner
|
|
||||||
]
|
|
||||||
imageView.image = nil
|
|
||||||
}
|
|
||||||
blurhashOverlayImageViews.forEach { imageView in
|
|
||||||
imageView.constraints.forEach { imageView.removeConstraint($0) }
|
|
||||||
imageView.removeFromSuperview()
|
|
||||||
imageView.layer.maskedCorners = [
|
|
||||||
.layerMinXMinYCorner, .layerMaxXMinYCorner,
|
|
||||||
.layerMinXMaxYCorner, .layerMaxXMaxYCorner
|
|
||||||
]
|
|
||||||
imageView.image = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
contentWarningOverlayView.removeFromSuperview()
|
|
||||||
contentWarningOverlayView.blurVisualEffectView.effect = ContentWarningOverlayView.blurVisualEffect
|
|
||||||
contentWarningOverlayView.vibrancyVisualEffectView.alpha = 1.0
|
|
||||||
contentWarningOverlayView.isUserInteractionEnabled = true
|
|
||||||
|
|
||||||
container.spacing = UIView.separatorLineHeight(of: self) * 2 // 2px
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ConfigurableMosaic {
|
|
||||||
let imageView: UIImageView
|
|
||||||
let blurhashOverlayImageView: UIImageView
|
|
||||||
let imageViewSize: CGSize
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupImageView(aspectRatio: CGSize, maxSize: CGSize) -> ConfigurableMosaic {
|
|
||||||
reset()
|
|
||||||
|
|
||||||
let contentView = UIView()
|
|
||||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
container.addArrangedSubview(contentView)
|
|
||||||
|
|
||||||
let imageViewSize: CGSize = {
|
|
||||||
let rect = AVMakeRect(
|
|
||||||
aspectRatio: aspectRatio,
|
|
||||||
insideRect: CGRect(origin: .zero, size: maxSize)
|
|
||||||
).integral
|
|
||||||
return rect.size
|
|
||||||
}()
|
|
||||||
let imageViewFrame = CGRect(origin: .zero, size: imageViewSize)
|
|
||||||
|
|
||||||
let imageView = imageViews[0]
|
|
||||||
imageView.layer.masksToBounds = true
|
|
||||||
imageView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius
|
|
||||||
imageView.layer.cornerCurve = .continuous
|
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
|
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
contentView.addSubview(imageView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
||||||
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
||||||
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
|
||||||
imageView.widthAnchor.constraint(equalToConstant: imageViewFrame.width).priority(.required - 1),
|
|
||||||
])
|
|
||||||
containerHeightLayoutConstraint.constant = imageViewFrame.height
|
|
||||||
containerHeightLayoutConstraint.isActive = true
|
|
||||||
|
|
||||||
let blurhashOverlayImageView = blurhashOverlayImageViews[0]
|
|
||||||
blurhashOverlayImageView.layer.masksToBounds = true
|
|
||||||
blurhashOverlayImageView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius
|
|
||||||
blurhashOverlayImageView.layer.cornerCurve = .continuous
|
|
||||||
blurhashOverlayImageView.contentMode = .scaleAspectFill
|
|
||||||
blurhashOverlayImageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
contentView.addSubview(blurhashOverlayImageView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
blurhashOverlayImageView.topAnchor.constraint(equalTo: imageView.topAnchor),
|
|
||||||
blurhashOverlayImageView.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
|
|
||||||
blurhashOverlayImageView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
|
|
||||||
blurhashOverlayImageView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(contentWarningOverlayView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
contentWarningOverlayView.topAnchor.constraint(equalTo: imageView.topAnchor),
|
|
||||||
contentWarningOverlayView.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
|
|
||||||
contentWarningOverlayView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
|
|
||||||
contentWarningOverlayView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
return ConfigurableMosaic(
|
|
||||||
imageView: imageView,
|
|
||||||
blurhashOverlayImageView: blurhashOverlayImageView,
|
|
||||||
imageViewSize: imageViewSize
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupImageViews(count: Int, maxSize: CGSize) -> [ConfigurableMosaic] {
|
|
||||||
reset()
|
|
||||||
let count = min(4, max(0, count))
|
|
||||||
guard count > 1 else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
let maxHeight = maxSize.height
|
|
||||||
let spacing: CGFloat = 1
|
|
||||||
|
|
||||||
containerHeightLayoutConstraint.constant = maxHeight
|
|
||||||
containerHeightLayoutConstraint.isActive = true
|
|
||||||
|
|
||||||
let contentLeftStackView = UIStackView()
|
|
||||||
let contentRightStackView = UIStackView()
|
|
||||||
[contentLeftStackView, contentRightStackView].forEach { stackView in
|
|
||||||
stackView.axis = .vertical
|
|
||||||
stackView.distribution = .fillEqually
|
|
||||||
stackView.spacing = spacing
|
|
||||||
}
|
|
||||||
container.addArrangedSubview(contentLeftStackView)
|
|
||||||
container.addArrangedSubview(contentRightStackView)
|
|
||||||
|
|
||||||
let imageViews: [UIImageView] = (0..<count).map { i in self.imageViews[i] }
|
|
||||||
let blurhashOverlayImageViews: [UIImageView] = (0..<count).map { i in self.blurhashOverlayImageViews[i] }
|
|
||||||
|
|
||||||
imageViews.forEach { imageView in
|
|
||||||
imageView.layer.masksToBounds = true
|
|
||||||
imageView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius
|
|
||||||
imageView.layer.cornerCurve = .continuous
|
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
}
|
|
||||||
blurhashOverlayImageViews.forEach { imageView in
|
|
||||||
imageView.layer.masksToBounds = true
|
|
||||||
imageView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius
|
|
||||||
imageView.layer.cornerCurve = .continuous
|
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
}
|
|
||||||
if count == 2 {
|
|
||||||
contentLeftStackView.addArrangedSubview(imageViews[0])
|
|
||||||
contentRightStackView.addArrangedSubview(imageViews[1])
|
|
||||||
switch UIApplication.shared.userInterfaceLayoutDirection {
|
|
||||||
case .rightToLeft:
|
|
||||||
imageViews[1].layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
|
||||||
imageViews[0].layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
|
||||||
|
|
||||||
blurhashOverlayImageViews[1].layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
|
||||||
blurhashOverlayImageViews[0].layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
|
||||||
|
|
||||||
default:
|
|
||||||
imageViews[0].layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
|
||||||
imageViews[1].layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
|
||||||
|
|
||||||
blurhashOverlayImageViews[0].layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
|
||||||
blurhashOverlayImageViews[1].layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if count == 3 {
|
|
||||||
contentLeftStackView.addArrangedSubview(imageViews[0])
|
|
||||||
contentRightStackView.addArrangedSubview(imageViews[1])
|
|
||||||
contentRightStackView.addArrangedSubview(imageViews[2])
|
|
||||||
switch UIApplication.shared.userInterfaceLayoutDirection {
|
|
||||||
case .rightToLeft:
|
|
||||||
imageViews[0].layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
|
||||||
imageViews[1].layer.maskedCorners = [.layerMinXMinYCorner]
|
|
||||||
imageViews[2].layer.maskedCorners = [.layerMinXMaxYCorner]
|
|
||||||
|
|
||||||
blurhashOverlayImageViews[0].layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
|
||||||
blurhashOverlayImageViews[1].layer.maskedCorners = [.layerMinXMinYCorner]
|
|
||||||
blurhashOverlayImageViews[2].layer.maskedCorners = [.layerMinXMaxYCorner]
|
|
||||||
default:
|
|
||||||
imageViews[0].layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
|
||||||
imageViews[1].layer.maskedCorners = [.layerMaxXMinYCorner]
|
|
||||||
imageViews[2].layer.maskedCorners = [.layerMaxXMaxYCorner]
|
|
||||||
|
|
||||||
blurhashOverlayImageViews[0].layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
|
||||||
blurhashOverlayImageViews[1].layer.maskedCorners = [.layerMaxXMinYCorner]
|
|
||||||
blurhashOverlayImageViews[2].layer.maskedCorners = [.layerMaxXMaxYCorner]
|
|
||||||
}
|
|
||||||
} else if count == 4 {
|
|
||||||
contentLeftStackView.addArrangedSubview(imageViews[0])
|
|
||||||
contentRightStackView.addArrangedSubview(imageViews[1])
|
|
||||||
contentLeftStackView.addArrangedSubview(imageViews[2])
|
|
||||||
contentRightStackView.addArrangedSubview(imageViews[3])
|
|
||||||
switch UIApplication.shared.userInterfaceLayoutDirection {
|
|
||||||
case .rightToLeft:
|
|
||||||
imageViews[0].layer.maskedCorners = [.layerMaxXMinYCorner]
|
|
||||||
imageViews[1].layer.maskedCorners = [.layerMinXMinYCorner]
|
|
||||||
imageViews[2].layer.maskedCorners = [.layerMaxXMaxYCorner]
|
|
||||||
imageViews[3].layer.maskedCorners = [.layerMinXMaxYCorner]
|
|
||||||
|
|
||||||
blurhashOverlayImageViews[0].layer.maskedCorners = [.layerMaxXMinYCorner]
|
|
||||||
blurhashOverlayImageViews[1].layer.maskedCorners = [.layerMinXMinYCorner]
|
|
||||||
blurhashOverlayImageViews[2].layer.maskedCorners = [.layerMaxXMaxYCorner]
|
|
||||||
blurhashOverlayImageViews[3].layer.maskedCorners = [.layerMinXMaxYCorner]
|
|
||||||
default:
|
|
||||||
imageViews[0].layer.maskedCorners = [.layerMinXMinYCorner]
|
|
||||||
imageViews[1].layer.maskedCorners = [.layerMaxXMinYCorner]
|
|
||||||
imageViews[2].layer.maskedCorners = [.layerMinXMaxYCorner]
|
|
||||||
imageViews[3].layer.maskedCorners = [.layerMaxXMaxYCorner]
|
|
||||||
|
|
||||||
blurhashOverlayImageViews[0].layer.maskedCorners = [.layerMinXMinYCorner]
|
|
||||||
blurhashOverlayImageViews[1].layer.maskedCorners = [.layerMaxXMinYCorner]
|
|
||||||
blurhashOverlayImageViews[2].layer.maskedCorners = [.layerMinXMaxYCorner]
|
|
||||||
blurhashOverlayImageViews[3].layer.maskedCorners = [.layerMaxXMaxYCorner]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (imageView, blurhashOverlayImageView) in zip(imageViews, blurhashOverlayImageViews) {
|
|
||||||
blurhashOverlayImageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
container.addSubview(blurhashOverlayImageView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
blurhashOverlayImageView.topAnchor.constraint(equalTo: imageView.topAnchor),
|
|
||||||
blurhashOverlayImageView.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
|
|
||||||
blurhashOverlayImageView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
|
|
||||||
blurhashOverlayImageView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(contentWarningOverlayView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
contentWarningOverlayView.topAnchor.constraint(equalTo: container.topAnchor),
|
|
||||||
contentWarningOverlayView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
|
||||||
contentWarningOverlayView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
|
||||||
contentWarningOverlayView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
var mosaics: [ConfigurableMosaic] = []
|
|
||||||
for (i, (imageView, blurhashOverlayImageView)) in zip(imageViews, blurhashOverlayImageViews).enumerated() {
|
|
||||||
let imageViewSize: CGSize = {
|
|
||||||
switch (i, count) {
|
|
||||||
case (_, 4):
|
|
||||||
return CGSize(width: maxSize.width * 0.5 - spacing, height: maxSize.height * 0.5 - spacing)
|
|
||||||
case (i, 3):
|
|
||||||
let width = maxSize.width * 0.5 - spacing
|
|
||||||
if i == 0 {
|
|
||||||
return CGSize(width: width, height: maxSize.height)
|
|
||||||
} else {
|
|
||||||
return CGSize(width: width, height: maxSize.height * 0.5 - spacing)
|
|
||||||
}
|
|
||||||
case (_, 2):
|
|
||||||
let width = maxSize.width * 0.5 - spacing
|
|
||||||
return CGSize(width: width, height: maxSize.height)
|
|
||||||
default:
|
|
||||||
assertionFailure()
|
|
||||||
return maxSize
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
imageView.frame.size = imageViewSize
|
|
||||||
let mosaic = ConfigurableMosaic(
|
|
||||||
imageView: imageView,
|
|
||||||
blurhashOverlayImageView: blurhashOverlayImageView,
|
|
||||||
imageViewSize: imageViewSize
|
|
||||||
)
|
|
||||||
mosaics.append(mosaic)
|
|
||||||
}
|
|
||||||
return mosaics
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: refactor blurhash image and preview image
|
|
||||||
extension MosaicImageViewContainer {
|
|
||||||
|
|
||||||
func setImageViews(alpha: CGFloat) {
|
|
||||||
// blurhashOverlayImageViews.forEach { $0.alpha = alpha }
|
|
||||||
imageViews.forEach { $0.alpha = alpha }
|
|
||||||
}
|
|
||||||
|
|
||||||
func setImageView(alpha: CGFloat, index: Int) {
|
|
||||||
// if index < blurhashOverlayImageViews.count {
|
|
||||||
// blurhashOverlayImageViews[index].alpha = alpha
|
|
||||||
// }
|
|
||||||
if index < imageViews.count {
|
|
||||||
imageViews[index].alpha = alpha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func thumbnail(at index: Int) -> UIImage? {
|
|
||||||
guard blurhashOverlayImageViews.count == imageViews.count else { return nil }
|
|
||||||
let tuples = Array(zip(blurhashOverlayImageViews, imageViews))
|
|
||||||
guard index < tuples.count else { return nil }
|
|
||||||
let tuple = tuples[index]
|
|
||||||
return tuple.1.image ?? tuple.0.image
|
|
||||||
}
|
|
||||||
|
|
||||||
func thumbnails() -> [UIImage?] {
|
|
||||||
guard blurhashOverlayImageViews.count == imageViews.count else { return [] }
|
|
||||||
let tuples = Array(zip(blurhashOverlayImageViews, imageViews))
|
|
||||||
return tuples.map { blurhashOverlayImageView, imageView -> UIImage? in
|
|
||||||
return imageView.image ?? blurhashOverlayImageView.image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MosaicImageViewContainer {
|
|
||||||
|
|
||||||
@objc private func visualEffectViewTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
||||||
delegate?.mosaicImageViewContainer(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func photoTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
|
||||||
guard let imageView = sender.view as? UIImageView else { return }
|
|
||||||
guard let index = imageViews.firstIndex(of: imageView) else { return }
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: tap photo at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, index)
|
|
||||||
delegate?.mosaicImageViewContainer(self, didTapImageView: imageView, atIndex: index)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DEBUG && canImport(SwiftUI)
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct MosaicImageView_Previews: PreviewProvider {
|
|
||||||
|
|
||||||
static var images: [UIImage] {
|
|
||||||
return ["bradley-dunn", "mrdongok", "lucas-ludwig", "markus-spiske"]
|
|
||||||
.map { UIImage(named: $0)! }
|
|
||||||
}
|
|
||||||
|
|
||||||
static var previews: some View {
|
|
||||||
Group {
|
|
||||||
UIViewPreview(width: 375) {
|
|
||||||
let view = MosaicImageViewContainer()
|
|
||||||
let image = images[3]
|
|
||||||
let mosaic = view.setupImageView(
|
|
||||||
aspectRatio: image.size,
|
|
||||||
maxSize: CGSize(width: 375, height: 400)
|
|
||||||
)
|
|
||||||
mosaic.imageView.image = image
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
.previewLayout(.fixed(width: 375, height: 400))
|
|
||||||
.previewDisplayName("Portrait - one image")
|
|
||||||
UIViewPreview(width: 375) {
|
|
||||||
let view = MosaicImageViewContainer()
|
|
||||||
let image = images[1]
|
|
||||||
let mosaic = view.setupImageView(
|
|
||||||
aspectRatio: image.size,
|
|
||||||
maxSize: CGSize(width: 375, height: 400)
|
|
||||||
)
|
|
||||||
mosaic.imageView.layer.masksToBounds = true
|
|
||||||
mosaic.imageView.layer.cornerRadius = 8
|
|
||||||
mosaic.imageView.contentMode = .scaleAspectFill
|
|
||||||
mosaic.imageView.image = image
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
.previewLayout(.fixed(width: 375, height: 400))
|
|
||||||
.previewDisplayName("Landscape - one image")
|
|
||||||
UIViewPreview(width: 375) {
|
|
||||||
let view = MosaicImageViewContainer()
|
|
||||||
let images = self.images.prefix(2)
|
|
||||||
let mosaics = view.setupImageViews(count: images.count, maxSize: CGSize(width: 375, height: 162))
|
|
||||||
for (i, mosaic) in mosaics.enumerated() {
|
|
||||||
mosaic.imageView.image = images[i]
|
|
||||||
}
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
.previewLayout(.fixed(width: 375, height: 200))
|
|
||||||
.previewDisplayName("two image")
|
|
||||||
UIViewPreview(width: 375) {
|
|
||||||
let view = MosaicImageViewContainer()
|
|
||||||
let images = self.images.prefix(3)
|
|
||||||
let mosaics = view.setupImageViews(count: images.count, maxSize: CGSize(width: 375, height: 162))
|
|
||||||
for (i, mosaic) in mosaics.enumerated() {
|
|
||||||
mosaic.imageView.image = images[i]
|
|
||||||
}
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
.previewLayout(.fixed(width: 375, height: 200))
|
|
||||||
.previewDisplayName("three image")
|
|
||||||
UIViewPreview(width: 375) {
|
|
||||||
let view = MosaicImageViewContainer()
|
|
||||||
let images = self.images.prefix(4)
|
|
||||||
let mosaics = view.setupImageViews(count: images.count, maxSize: CGSize(width: 375, height: 162))
|
|
||||||
for (i, mosaic) in mosaics.enumerated() {
|
|
||||||
mosaic.imageView.image = images[i]
|
|
||||||
}
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
.previewLayout(.fixed(width: 375, height: 200))
|
|
||||||
.previewDisplayName("four image")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
#endif
|
|
|
@ -1,125 +0,0 @@
|
||||||
//
|
|
||||||
// PlayerContainerView+MediaTypeIndicotorView.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by MainasuK Cirno on 2021-3-15.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import MastodonAsset
|
|
||||||
import MastodonLocalization
|
|
||||||
|
|
||||||
extension PlayerContainerView {
|
|
||||||
|
|
||||||
final class MediaTypeIndicatorView: UIView {
|
|
||||||
|
|
||||||
static let indicatorViewSize = CGSize(width: 47, height: 25)
|
|
||||||
|
|
||||||
let maskLayer = CAShapeLayer()
|
|
||||||
|
|
||||||
let label: UILabel = {
|
|
||||||
let label = UILabel()
|
|
||||||
label.textColor = .white
|
|
||||||
label.textAlignment = .right
|
|
||||||
label.adjustsFontSizeToFitWidth = true
|
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return label
|
|
||||||
}()
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
|
|
||||||
let path = UIBezierPath()
|
|
||||||
path.move(to: CGPoint(x: bounds.width, y: bounds.height))
|
|
||||||
path.addLine(to: CGPoint(x: bounds.width, y: 0))
|
|
||||||
path.addLine(to: CGPoint(x: bounds.width * 0.5, y: 0))
|
|
||||||
path.addCurve(
|
|
||||||
to: CGPoint(x: 0, y: bounds.height),
|
|
||||||
controlPoint1: CGPoint(x: bounds.width * 0.2, y: 0),
|
|
||||||
controlPoint2: CGPoint(x: 0, y: bounds.height * 0.3)
|
|
||||||
)
|
|
||||||
path.close()
|
|
||||||
|
|
||||||
maskLayer.frame = bounds
|
|
||||||
maskLayer.path = path.cgPath
|
|
||||||
layer.mask = maskLayer
|
|
||||||
|
|
||||||
layer.cornerRadius = PlayerContainerView.cornerRadius
|
|
||||||
layer.maskedCorners = [.layerMaxXMaxYCorner]
|
|
||||||
layer.cornerCurve = .continuous
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PlayerContainerView.MediaTypeIndicatorView {
|
|
||||||
|
|
||||||
private func _init() {
|
|
||||||
backgroundColor = Asset.Colors.mediaTypeIndicotor.color
|
|
||||||
layoutMargins = UIEdgeInsets(top: 3, left: 13, bottom: 0, right: 6)
|
|
||||||
|
|
||||||
addSubview(label)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
|
||||||
label.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
|
||||||
label.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
|
||||||
label.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func roundedFont(weight: UIFont.Weight,fontSize: CGFloat) -> UIFont {
|
|
||||||
let systemFont = UIFont.systemFont(ofSize: fontSize, weight: weight)
|
|
||||||
guard let descriptor = systemFont.fontDescriptor.withDesign(.rounded) else { return systemFont }
|
|
||||||
let roundedFont = UIFont(descriptor: descriptor, size: fontSize)
|
|
||||||
return roundedFont
|
|
||||||
}
|
|
||||||
|
|
||||||
func setMediaKind(kind: VideoPlayerViewModel.Kind) {
|
|
||||||
let fontSize: CGFloat = 18
|
|
||||||
|
|
||||||
switch kind {
|
|
||||||
case .gif:
|
|
||||||
label.font = PlayerContainerView.MediaTypeIndicatorView.roundedFont(weight: .heavy, fontSize: fontSize)
|
|
||||||
label.text = "GIF"
|
|
||||||
case .video:
|
|
||||||
label.text = " "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#if canImport(SwiftUI) && DEBUG
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct PlayerContainerViewMediaTypeIndicatorView_Previews: PreviewProvider {
|
|
||||||
|
|
||||||
static var previews: some View {
|
|
||||||
Group {
|
|
||||||
UIViewPreview(width: 47) {
|
|
||||||
let view = PlayerContainerView.MediaTypeIndicatorView()
|
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
view.heightAnchor.constraint(equalToConstant: 25),
|
|
||||||
view.widthAnchor.constraint(equalToConstant: 47),
|
|
||||||
])
|
|
||||||
view.setMediaKind(kind: .gif)
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
.previewLayout(.fixed(width: 47, height: 25))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
|
@ -1,179 +0,0 @@
|
||||||
//
|
|
||||||
// PlayerContainerView.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by xiaojian sun on 2021/3/10.
|
|
||||||
//
|
|
||||||
|
|
||||||
import os.log
|
|
||||||
import AVKit
|
|
||||||
import UIKit
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
protocol PlayerContainerViewDelegate: AnyObject {
|
|
||||||
func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
|
||||||
}
|
|
||||||
|
|
||||||
final class PlayerContainerView: UIView {
|
|
||||||
static let cornerRadius: CGFloat = ContentWarningOverlayView.cornerRadius
|
|
||||||
|
|
||||||
private let container = UIView()
|
|
||||||
private let touchBlockingView = TouchBlockingView()
|
|
||||||
private var containerHeightLayoutConstraint: NSLayoutConstraint!
|
|
||||||
|
|
||||||
let contentWarningOverlayView: ContentWarningOverlayView = {
|
|
||||||
let contentWarningOverlayView = ContentWarningOverlayView()
|
|
||||||
contentWarningOverlayView.update(cornerRadius: PlayerContainerView.cornerRadius)
|
|
||||||
return contentWarningOverlayView
|
|
||||||
}()
|
|
||||||
|
|
||||||
let playerViewController = AVPlayerViewController()
|
|
||||||
|
|
||||||
let blurhashOverlayImageView = UIImageView()
|
|
||||||
let mediaTypeIndicatorView = MediaTypeIndicatorView()
|
|
||||||
|
|
||||||
weak var delegate: PlayerContainerViewDelegate?
|
|
||||||
|
|
||||||
private var isReadyForDisplayObservation: NSKeyValueObservation?
|
|
||||||
let isReadyForDisplay = CurrentValueSubject<Bool, Never>(false)
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
_init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PlayerContainerView {
|
|
||||||
private func _init() {
|
|
||||||
// accessibility
|
|
||||||
accessibilityIgnoresInvertColors = true
|
|
||||||
|
|
||||||
container.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(container)
|
|
||||||
containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
container.topAnchor.constraint(equalTo: topAnchor),
|
|
||||||
container.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
|
||||||
bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
|
||||||
containerHeightLayoutConstraint,
|
|
||||||
])
|
|
||||||
|
|
||||||
// will not influence full-screen playback
|
|
||||||
playerViewController.view.layer.masksToBounds = true
|
|
||||||
playerViewController.view.layer.cornerRadius = PlayerContainerView.cornerRadius
|
|
||||||
playerViewController.view.layer.cornerCurve = .continuous
|
|
||||||
|
|
||||||
blurhashOverlayImageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
playerViewController.contentOverlayView!.addSubview(blurhashOverlayImageView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
blurhashOverlayImageView.topAnchor.constraint(equalTo: playerViewController.contentOverlayView!.topAnchor),
|
|
||||||
blurhashOverlayImageView.leadingAnchor.constraint(equalTo: playerViewController.contentOverlayView!.leadingAnchor),
|
|
||||||
blurhashOverlayImageView.trailingAnchor.constraint(equalTo: playerViewController.contentOverlayView!.trailingAnchor),
|
|
||||||
blurhashOverlayImageView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
// mediaType
|
|
||||||
mediaTypeIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
playerViewController.contentOverlayView!.addSubview(mediaTypeIndicatorView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
mediaTypeIndicatorView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor),
|
|
||||||
mediaTypeIndicatorView.rightAnchor.constraint(equalTo: playerViewController.contentOverlayView!.rightAnchor),
|
|
||||||
mediaTypeIndicatorView.heightAnchor.constraint(equalToConstant: MediaTypeIndicatorView.indicatorViewSize.height).priority(.required - 1),
|
|
||||||
mediaTypeIndicatorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicatorView.indicatorViewSize.width).priority(.required - 1),
|
|
||||||
])
|
|
||||||
|
|
||||||
isReadyForDisplayObservation = playerViewController.observe(\.isReadyForDisplay, options: [.initial, .new]) { [weak self] playerViewController, _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: isReadyForDisplay: %s", (#file as NSString).lastPathComponent, #line, #function, playerViewController.isReadyForDisplay.description)
|
|
||||||
self.isReadyForDisplay.value = playerViewController.isReadyForDisplay
|
|
||||||
}
|
|
||||||
|
|
||||||
contentWarningOverlayView.delegate = self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - ContentWarningOverlayViewDelegate
|
|
||||||
extension PlayerContainerView: ContentWarningOverlayViewDelegate {
|
|
||||||
func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
|
||||||
delegate?.playerContainerView(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PlayerContainerView {
|
|
||||||
func reset() {
|
|
||||||
// note: set playerViewController.player pause() and nil in data source configuration process make reloadData not break playing
|
|
||||||
|
|
||||||
playerViewController.willMove(toParent: nil)
|
|
||||||
playerViewController.view.removeFromSuperview()
|
|
||||||
playerViewController.removeFromParent()
|
|
||||||
|
|
||||||
blurhashOverlayImageView.image = nil
|
|
||||||
|
|
||||||
container.subviews.forEach { subview in
|
|
||||||
subview.removeFromSuperview()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupPlayer(aspectRatio: CGSize, maxSize: CGSize, parent: UIViewController?) -> AVPlayerViewController {
|
|
||||||
reset()
|
|
||||||
|
|
||||||
touchBlockingView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
container.addSubview(touchBlockingView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
touchBlockingView.topAnchor.constraint(equalTo: container.topAnchor),
|
|
||||||
touchBlockingView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
|
||||||
touchBlockingView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
let rect = AVMakeRect(
|
|
||||||
aspectRatio: aspectRatio,
|
|
||||||
insideRect: CGRect(origin: .zero, size: maxSize)
|
|
||||||
).integral
|
|
||||||
|
|
||||||
parent?.addChild(playerViewController)
|
|
||||||
playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
touchBlockingView.addSubview(playerViewController.view)
|
|
||||||
parent.flatMap { playerViewController.didMove(toParent: $0) }
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
playerViewController.view.topAnchor.constraint(equalTo: touchBlockingView.topAnchor),
|
|
||||||
playerViewController.view.leadingAnchor.constraint(equalTo: touchBlockingView.leadingAnchor),
|
|
||||||
playerViewController.view.trailingAnchor.constraint(equalTo: touchBlockingView.trailingAnchor),
|
|
||||||
playerViewController.view.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor),
|
|
||||||
touchBlockingView.widthAnchor.constraint(equalToConstant: rect.width).priority(.required - 1),
|
|
||||||
])
|
|
||||||
containerHeightLayoutConstraint.constant = rect.height
|
|
||||||
containerHeightLayoutConstraint.isActive = true
|
|
||||||
|
|
||||||
playerViewController.view.frame.size = rect.size
|
|
||||||
|
|
||||||
contentWarningOverlayView.removeFromSuperview()
|
|
||||||
contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(contentWarningOverlayView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
contentWarningOverlayView.topAnchor.constraint(equalTo: touchBlockingView.topAnchor),
|
|
||||||
contentWarningOverlayView.leadingAnchor.constraint(equalTo: touchBlockingView.leadingAnchor),
|
|
||||||
contentWarningOverlayView.trailingAnchor.constraint(equalTo: touchBlockingView.trailingAnchor),
|
|
||||||
contentWarningOverlayView.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor)
|
|
||||||
])
|
|
||||||
|
|
||||||
bringSubviewToFront(mediaTypeIndicatorView)
|
|
||||||
|
|
||||||
return playerViewController
|
|
||||||
}
|
|
||||||
|
|
||||||
func setMediaKind(kind: VideoPlayerViewModel.Kind) {
|
|
||||||
mediaTypeIndicatorView.setMediaKind(kind: kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setMediaIndicator(isHidden: Bool) {
|
|
||||||
mediaTypeIndicatorView.alpha = isHidden ? 0 : 1
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -9,9 +9,10 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import MastodonUI
|
import MastodonUI
|
||||||
|
import AlamofireImage
|
||||||
|
|
||||||
extension MediaView {
|
extension MediaView {
|
||||||
public static func configuration(status: Status) -> AnyPublisher<[MediaView.Configuration], Never> {
|
public static func configuration(status: Status) -> [MediaView.Configuration] {
|
||||||
func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo {
|
func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo {
|
||||||
MediaView.Configuration.VideoInfo(
|
MediaView.Configuration.VideoInfo(
|
||||||
aspectRadio: attachment.size,
|
aspectRadio: attachment.size,
|
||||||
|
@ -22,59 +23,72 @@ extension MediaView {
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = status.reblog ?? status
|
let status = status.reblog ?? status
|
||||||
return status.publisher(for: \.attachments)
|
let attachments = status.attachments
|
||||||
.map { attachments -> [MediaView.Configuration] in
|
let configurations = attachments.map { attachment -> MediaView.Configuration in
|
||||||
return attachments.map { attachment -> MediaView.Configuration in
|
let configuration: MediaView.Configuration = {
|
||||||
let configuration: MediaView.Configuration = {
|
switch attachment.kind {
|
||||||
switch attachment.kind {
|
case .image:
|
||||||
case .image:
|
let info = MediaView.Configuration.ImageInfo(
|
||||||
let info = MediaView.Configuration.ImageInfo(
|
aspectRadio: attachment.size,
|
||||||
aspectRadio: attachment.size,
|
assetURL: attachment.assetURL
|
||||||
assetURL: attachment.assetURL
|
)
|
||||||
)
|
return .init(
|
||||||
return .init(
|
info: .image(info: info),
|
||||||
info: .image(info: info),
|
blurhash: attachment.blurhash
|
||||||
blurhash: attachment.blurhash
|
)
|
||||||
)
|
case .video:
|
||||||
case .video:
|
let info = videoInfo(from: attachment)
|
||||||
let info = videoInfo(from: attachment)
|
return .init(
|
||||||
return .init(
|
info: .video(info: info),
|
||||||
info: .video(info: info),
|
blurhash: attachment.blurhash
|
||||||
blurhash: attachment.blurhash
|
)
|
||||||
)
|
case .gifv:
|
||||||
case .gifv:
|
let info = videoInfo(from: attachment)
|
||||||
let info = videoInfo(from: attachment)
|
return .init(
|
||||||
return .init(
|
info: .gif(info: info),
|
||||||
info: .gif(info: info),
|
blurhash: attachment.blurhash
|
||||||
blurhash: attachment.blurhash
|
)
|
||||||
)
|
case .audio:
|
||||||
case .audio:
|
let info = videoInfo(from: attachment)
|
||||||
// TODO:
|
return .init(
|
||||||
let info = videoInfo(from: attachment)
|
info: .video(info: info),
|
||||||
return .init(
|
blurhash: attachment.blurhash
|
||||||
info: .video(info: info),
|
)
|
||||||
blurhash: attachment.blurhash
|
} // end switch
|
||||||
)
|
}()
|
||||||
} // end switch
|
|
||||||
}()
|
if let previewURL = configuration.previewURL,
|
||||||
|
let url = URL(string: previewURL)
|
||||||
if let assetURL = configuration.assetURL,
|
{
|
||||||
let blurhash = configuration.blurhash
|
let placeholder = UIImage.placeholder(color: .systemGray6)
|
||||||
{
|
let request = URLRequest(url: url)
|
||||||
AppContext.shared.blurhashImageCacheService.image(
|
ImageDownloader.default.download(request) { response in
|
||||||
blurhash: blurhash,
|
switch response.result {
|
||||||
size: configuration.aspectRadio,
|
case .success(let image):
|
||||||
url: assetURL
|
configuration.previewImage = image
|
||||||
)
|
case .failure(let error):
|
||||||
.assign(to: \.blurhashImage, on: configuration)
|
configuration.previewImage = placeholder
|
||||||
.store(in: &configuration.blurhashImageDisposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration.isReveal = status.sensitive ? status.isMediaSensitiveToggled : true
|
|
||||||
|
|
||||||
return configuration
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
|
||||||
|
if let assetURL = configuration.assetURL,
|
||||||
|
let blurhash = configuration.blurhash
|
||||||
|
{
|
||||||
|
AppContext.shared.blurhashImageCacheService.image(
|
||||||
|
blurhash: blurhash,
|
||||||
|
size: configuration.aspectRadio,
|
||||||
|
url: assetURL
|
||||||
|
)
|
||||||
|
.assign(to: \.blurhashImage, on: configuration)
|
||||||
|
.store(in: &configuration.blurhashImageDisposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration.isReveal = status.sensitive ? status.isMediaSensitiveToggled : true
|
||||||
|
|
||||||
|
return configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
return configurations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -272,9 +272,10 @@ extension StatusView {
|
||||||
|
|
||||||
viewModel.isMediaSensitive = status.sensitive && !status.attachments.isEmpty // some servers set media sensitive even empty attachments
|
viewModel.isMediaSensitive = status.sensitive && !status.attachments.isEmpty // some servers set media sensitive even empty attachments
|
||||||
|
|
||||||
MediaView.configuration(status: status)
|
let configurations = MediaView.configuration(status: status)
|
||||||
.assign(to: \.mediaViewConfigurations, on: viewModel)
|
if viewModel.mediaViewConfigurations != configurations {
|
||||||
.store(in: &disposeBag)
|
viewModel.mediaViewConfigurations = configurations
|
||||||
|
}
|
||||||
|
|
||||||
status.publisher(for: \.isMediaSensitiveToggled)
|
status.publisher(for: \.isMediaSensitiveToggled)
|
||||||
.assign(to: \.isMediaSensitiveToggled, on: viewModel)
|
.assign(to: \.isMediaSensitiveToggled, on: viewModel)
|
||||||
|
|
|
@ -33,6 +33,7 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate {
|
||||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action)
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action)
|
||||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
||||||
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
||||||
|
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
||||||
// sourcery:end
|
// sourcery:end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,5 +81,9 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell {
|
||||||
func statusView(_ statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView) {
|
func statusView(_ statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView) {
|
||||||
delegate?.tableViewCell(self, statusView: statusView, spoilerBannerViewDidPressed: bannerView)
|
delegate?.tableViewCell(self, statusView: statusView, spoilerBannerViewDidPressed: bannerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) {
|
||||||
|
delegate?.tableViewCell(self, statusView: statusView, mediaGridContainerView: mediaGridContainerView, mediaSensitiveButtonDidPressed: button)
|
||||||
|
}
|
||||||
// sourcery:end
|
// sourcery:end
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
//
|
|
||||||
// AudioContainerViewModel.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by sxiaojian on 2021/3/9.
|
|
||||||
//
|
|
||||||
|
|
||||||
import CoreDataStack
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class AudioContainerViewModel {
|
|
||||||
|
|
||||||
// static func configure(
|
|
||||||
// cell: StatusCell,
|
|
||||||
// audioAttachment: Attachment,
|
|
||||||
// audioService: AudioPlaybackService
|
|
||||||
// ) {
|
|
||||||
// guard let duration = audioAttachment.meta?.original?.duration else { return }
|
|
||||||
// let audioView = cell.statusView.audioView
|
|
||||||
// audioView.timeLabel.text = duration.asString(style: .positional)
|
|
||||||
//
|
|
||||||
// audioView.playButton.publisher(for: .touchUpInside)
|
|
||||||
// .sink { [weak audioService] _ in
|
|
||||||
// guard let audioService = audioService else { return }
|
|
||||||
// if audioAttachment === audioService.attachment {
|
|
||||||
// if audioService.isPlaying() {
|
|
||||||
// audioService.pause()
|
|
||||||
// } else {
|
|
||||||
// audioService.resume()
|
|
||||||
// }
|
|
||||||
// if audioService.currentTimeSubject.value == 0 {
|
|
||||||
// audioService.playAudio(audioAttachment: audioAttachment)
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// audioService.playAudio(audioAttachment: audioAttachment)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .store(in: &cell.disposeBag)
|
|
||||||
// audioView.slider.maximumValue = Float(duration)
|
|
||||||
// audioView.slider.publisher(for: .valueChanged)
|
|
||||||
// .sink { [weak audioService] slider in
|
|
||||||
// guard let audioService = audioService else { return }
|
|
||||||
// let slider = slider as! UISlider
|
|
||||||
// let time = TimeInterval(slider.value)
|
|
||||||
// audioService.seekToTime(time: time)
|
|
||||||
// }
|
|
||||||
// .store(in: &cell.disposeBag)
|
|
||||||
// observePlayer(cell: cell, audioAttachment: audioAttachment, audioService: audioService)
|
|
||||||
// if audioAttachment != audioService.attachment {
|
|
||||||
// configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: .stopped)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// static func observePlayer(
|
|
||||||
// cell: StatusCell,
|
|
||||||
// audioAttachment: Attachment,
|
|
||||||
// audioService: AudioPlaybackService
|
|
||||||
// ) {
|
|
||||||
// let audioView = cell.statusView.audioView
|
|
||||||
// var lastCurrentTimeSubject: TimeInterval?
|
|
||||||
// audioService.currentTimeSubject
|
|
||||||
// .throttle(for: 0.008, scheduler: DispatchQueue.main, latest: true)
|
|
||||||
// .compactMap { [weak audioService] time -> TimeInterval? in
|
|
||||||
// defer {
|
|
||||||
// lastCurrentTimeSubject = time
|
|
||||||
// }
|
|
||||||
// guard audioAttachment === audioService?.attachment else { return nil }
|
|
||||||
// // guard let duration = audioAttachment.meta?.original?.duration else { return nil }
|
|
||||||
//
|
|
||||||
// if let lastCurrentTimeSubject = lastCurrentTimeSubject, time != 0.0 {
|
|
||||||
// guard abs(time - lastCurrentTimeSubject) < 0.5 else { return nil } // debounce
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// guard !audioView.slider.isTracking else { return nil }
|
|
||||||
// return TimeInterval(time)
|
|
||||||
// }
|
|
||||||
// .sink(receiveValue: { time in
|
|
||||||
// audioView.timeLabel.text = time.asString(style: .positional)
|
|
||||||
// audioView.slider.setValue(Float(time), animated: true)
|
|
||||||
// })
|
|
||||||
// .store(in: &cell.disposeBag)
|
|
||||||
// audioService.playbackState
|
|
||||||
// .receive(on: DispatchQueue.main)
|
|
||||||
// .sink(receiveValue: { playbackState in
|
|
||||||
// if audioAttachment === audioService.attachment {
|
|
||||||
// configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: playbackState)
|
|
||||||
// } else {
|
|
||||||
// configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: .stopped)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .store(in: &cell.disposeBag)
|
|
||||||
// }
|
|
||||||
|
|
||||||
static func configureAudioView(
|
|
||||||
audioView: AudioContainerView,
|
|
||||||
audioAttachment: MastodonAttachment,
|
|
||||||
playbackState: PlaybackState
|
|
||||||
) {
|
|
||||||
fatalError()
|
|
||||||
// switch playbackState {
|
|
||||||
// case .stopped:
|
|
||||||
// audioView.playButton.isSelected = false
|
|
||||||
// audioView.slider.isUserInteractionEnabled = false
|
|
||||||
// audioView.slider.setValue(0, animated: false)
|
|
||||||
// case .paused:
|
|
||||||
// audioView.playButton.isSelected = false
|
|
||||||
// audioView.slider.isUserInteractionEnabled = true
|
|
||||||
// case .playing, .readyToPlay:
|
|
||||||
// audioView.playButton.isSelected = true
|
|
||||||
// audioView.slider.isUserInteractionEnabled = true
|
|
||||||
// default:
|
|
||||||
// assertionFailure()
|
|
||||||
// }
|
|
||||||
// guard let duration = audioAttachment.meta?.original?.duration else { return }
|
|
||||||
// audioView.timeLabel.text = duration.asString(style: .positional)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
//
|
|
||||||
// MosaicImageViewModel.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by Cirno MainasuK on 2021-2-23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Combine
|
|
||||||
import CoreDataStack
|
|
||||||
|
|
||||||
//struct MosaicImageViewModel {
|
|
||||||
//
|
|
||||||
// let metas: [MosaicMeta]
|
|
||||||
//
|
|
||||||
// init(mediaAttachments: [Attachment]) {
|
|
||||||
// var metas: [MosaicMeta] = []
|
|
||||||
// for element in mediaAttachments where element.type == .image {
|
|
||||||
// guard let meta = element.meta,
|
|
||||||
// let width = meta.original?.width,
|
|
||||||
// let height = meta.original?.height,
|
|
||||||
// let url = URL(string: element.url) else {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
// let mosaicMeta = MosaicMeta(
|
|
||||||
// previewURL: element.previewURL.flatMap { URL(string: $0) },
|
|
||||||
// url: url,
|
|
||||||
// size: CGSize(width: width, height: height),
|
|
||||||
// blurhash: element.blurhash,
|
|
||||||
// altText: element.descriptionString
|
|
||||||
// )
|
|
||||||
// metas.append(mosaicMeta)
|
|
||||||
// }
|
|
||||||
// self.metas = metas
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//struct MosaicMeta {
|
|
||||||
// static let edgeMaxLength: CGFloat = 20
|
|
||||||
//
|
|
||||||
// let previewURL: URL?
|
|
||||||
// let url: URL
|
|
||||||
// let size: CGSize
|
|
||||||
// let blurhash: String?
|
|
||||||
// let altText: String?
|
|
||||||
//
|
|
||||||
// func blurhashImagePublisher() -> AnyPublisher<UIImage?, Never> {
|
|
||||||
// guard let blurhash = blurhash else {
|
|
||||||
// return Just(nil).eraseToAnyPublisher()
|
|
||||||
// }
|
|
||||||
// return AppContext.shared.blurhashImageCacheService.image(blurhash: blurhash, size: size, url: url)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//}
|
|
|
@ -1,193 +0,0 @@
|
||||||
//
|
|
||||||
// VideoPlayerViewModel.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by xiaojian sun on 2021/3/10.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AVKit
|
|
||||||
import Combine
|
|
||||||
import CoreDataStack
|
|
||||||
import os.log
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
final class VideoPlayerViewModel {
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "org.joinmastodon.app.video-playback-service.appWillPlayVideo")
|
|
||||||
// input
|
|
||||||
let previewImageURL: URL?
|
|
||||||
let videoURL: URL
|
|
||||||
let videoSize: CGSize
|
|
||||||
let videoKind: Kind
|
|
||||||
|
|
||||||
var isTransitioning = false
|
|
||||||
var isFullScreenPresentationing = false
|
|
||||||
var isPlayingWhenEndDisplaying = false
|
|
||||||
|
|
||||||
// prevent player state flick when tableView reload
|
|
||||||
private typealias Play = Bool
|
|
||||||
private let debouncePlayingState = PassthroughSubject<Play, Never>()
|
|
||||||
|
|
||||||
private var updateDate = Date()
|
|
||||||
|
|
||||||
// output
|
|
||||||
let player: AVPlayer
|
|
||||||
private(set) var looper: AVPlayerLooper? // works with AVQueuePlayer (iOS 10+)
|
|
||||||
|
|
||||||
private var timeControlStatusObservation: NSKeyValueObservation?
|
|
||||||
let timeControlStatus = CurrentValueSubject<AVPlayer.TimeControlStatus, Never>(.paused)
|
|
||||||
let playbackState = CurrentValueSubject<PlaybackState, Never>(PlaybackState.unknown)
|
|
||||||
|
|
||||||
init(previewImageURL: URL?, videoURL: URL, videoSize: CGSize, videoKind: VideoPlayerViewModel.Kind) {
|
|
||||||
self.previewImageURL = previewImageURL
|
|
||||||
self.videoURL = videoURL
|
|
||||||
self.videoSize = videoSize
|
|
||||||
self.videoKind = videoKind
|
|
||||||
|
|
||||||
let playerItem = AVPlayerItem(url: videoURL)
|
|
||||||
let player = videoKind == .gif ? AVQueuePlayer(playerItem: playerItem) : AVPlayer(playerItem: playerItem)
|
|
||||||
player.isMuted = true
|
|
||||||
self.player = player
|
|
||||||
|
|
||||||
if videoKind == .gif {
|
|
||||||
setupLooper()
|
|
||||||
}
|
|
||||||
|
|
||||||
timeControlStatusObservation = player.observe(\.timeControlStatus, options: [.initial, .new]) { [weak self] player, _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: player state: %s", (#file as NSString).lastPathComponent, #line, #function, player.timeControlStatus.debugDescription)
|
|
||||||
self.timeControlStatus.value = player.timeControlStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
player.publisher(for: \.status, options: [.initial, .new])
|
|
||||||
.sink(receiveValue: { [weak self] status in
|
|
||||||
guard let self = self else { return }
|
|
||||||
switch status {
|
|
||||||
case .failed:
|
|
||||||
self.playbackState.value = .failed
|
|
||||||
case .readyToPlay:
|
|
||||||
self.playbackState.value = .readyToPlay
|
|
||||||
case .unknown:
|
|
||||||
self.playbackState.value = .unknown
|
|
||||||
@unknown default:
|
|
||||||
assertionFailure()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
|
||||||
timeControlStatus
|
|
||||||
.sink { [weak self] timeControlStatus in
|
|
||||||
guard let self = self else { return }
|
|
||||||
|
|
||||||
// emit playing event
|
|
||||||
if timeControlStatus == .playing {
|
|
||||||
NotificationCenter.default.post(name: VideoPlayerViewModel.appWillPlayVideoNotification, object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch timeControlStatus {
|
|
||||||
case .paused:
|
|
||||||
self.playbackState.value = .paused
|
|
||||||
case .waitingToPlayAtSpecifiedRate:
|
|
||||||
self.playbackState.value = .buffering
|
|
||||||
case .playing:
|
|
||||||
self.playbackState.value = .playing
|
|
||||||
@unknown default:
|
|
||||||
assertionFailure()
|
|
||||||
self.playbackState.value = .unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
|
||||||
debouncePlayingState
|
|
||||||
.debounce(for: 0.3, scheduler: DispatchQueue.main)
|
|
||||||
.sink { [weak self] isPlay in
|
|
||||||
guard let self = self else { return }
|
|
||||||
isPlay ? self.play() : self.pause()
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
|
||||||
let sessionName = videoKind == .gif ? "GIF" : "Video"
|
|
||||||
playbackState
|
|
||||||
.receive(on: RunLoop.main)
|
|
||||||
.sink { [weak self] status in
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: %s status: %s", ((#file as NSString).lastPathComponent), #line, #function, sessionName, status.description)
|
|
||||||
guard let self = self else { return }
|
|
||||||
|
|
||||||
// only update audio session for video
|
|
||||||
guard self.videoKind == .video else { return }
|
|
||||||
switch status {
|
|
||||||
case .unknown, .buffering, .readyToPlay:
|
|
||||||
break
|
|
||||||
case .playing:
|
|
||||||
try? AVAudioSession.sharedInstance().setCategory(.playback)
|
|
||||||
try? AVAudioSession.sharedInstance().setActive(true)
|
|
||||||
case .paused, .stopped, .failed:
|
|
||||||
try? AVAudioSession.sharedInstance().setCategory(.ambient) // set to ambient to allow mixed (needed for GIFV)
|
|
||||||
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
timeControlStatusObservation = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VideoPlayerViewModel {
|
|
||||||
enum Kind {
|
|
||||||
case gif
|
|
||||||
case video
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VideoPlayerViewModel {
|
|
||||||
func setupLooper() {
|
|
||||||
guard looper == nil, let queuePlayer = player as? AVQueuePlayer else { return }
|
|
||||||
guard let templateItem = queuePlayer.items().first else { return }
|
|
||||||
looper = AVPlayerLooper(player: queuePlayer, templateItem: templateItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
func play() {
|
|
||||||
player.play()
|
|
||||||
updateDate = Date()
|
|
||||||
}
|
|
||||||
|
|
||||||
func pause() {
|
|
||||||
player.pause()
|
|
||||||
updateDate = Date()
|
|
||||||
}
|
|
||||||
|
|
||||||
func willDisplay() {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: url: %s", (#file as NSString).lastPathComponent, #line, #function, videoURL.debugDescription)
|
|
||||||
|
|
||||||
switch videoKind {
|
|
||||||
case .gif:
|
|
||||||
play() // always auto play GIF
|
|
||||||
case .video:
|
|
||||||
guard isPlayingWhenEndDisplaying else { return }
|
|
||||||
// mute before resume
|
|
||||||
if updateDate.timeIntervalSinceNow < -3 {
|
|
||||||
player.isMuted = true
|
|
||||||
}
|
|
||||||
debouncePlayingState.send(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDate = Date()
|
|
||||||
}
|
|
||||||
|
|
||||||
func didEndDisplaying() {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: url: %s", (#file as NSString).lastPathComponent, #line, #function, videoURL.debugDescription)
|
|
||||||
|
|
||||||
isPlayingWhenEndDisplaying = timeControlStatus.value != .paused
|
|
||||||
switch videoKind {
|
|
||||||
case .gif:
|
|
||||||
pause() // always pause GIF immediately
|
|
||||||
case .video:
|
|
||||||
debouncePlayingState.send(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDate = Date()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -78,7 +78,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
||||||
return imageView
|
return imageView
|
||||||
}()
|
}()
|
||||||
transitionItem.targetFrame = transitionTargetFrame
|
transitionItem.targetFrame = transitionTargetFrame
|
||||||
transitionItem.imageView = transitionImageView
|
transitionItem.transitionView = transitionImageView
|
||||||
transitionContext.containerView.addSubview(transitionImageView)
|
transitionContext.containerView.addSubview(transitionImageView)
|
||||||
|
|
||||||
toVC.closeButtonBackground.alpha = 0
|
toVC.closeButtonBackground.alpha = 0
|
||||||
|
@ -109,122 +109,166 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
||||||
return animator
|
return animator
|
||||||
}
|
}
|
||||||
|
|
||||||
private func popTransition(using transitionContext: UIViewControllerContextTransitioning, curve: UIView.AnimationCurve = .easeInOut) -> UIViewPropertyAnimator {
|
@discardableResult
|
||||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? MediaPreviewViewController,
|
private func popTransition(
|
||||||
let fromView = transitionContext.view(forKey: .from),
|
using transitionContext: UIViewControllerContextTransitioning,
|
||||||
let mediaPreviewImageViewController = fromVC.pagingViewController.currentViewController as? MediaPreviewImageViewController,
|
curve: UIView.AnimationCurve = .easeInOut
|
||||||
let index = fromVC.pagingViewController.currentIndex else {
|
) -> UIViewPropertyAnimator {
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert view hierarchy not change
|
|
||||||
let toVC = transitionItem.previewableViewController
|
|
||||||
let targetFrame = toVC.sourceFrame(transitionItem: transitionItem, index: index)
|
|
||||||
|
|
||||||
let imageView = mediaPreviewImageViewController.previewImageView.imageView
|
|
||||||
let _snapshot: UIView? = {
|
|
||||||
transitionItem.snapshotRaw = imageView
|
|
||||||
let snapshot = imageView.snapshotView(afterScreenUpdates: false)
|
|
||||||
snapshot?.clipsToBounds = true
|
|
||||||
snapshot?.contentMode = .scaleAspectFill
|
|
||||||
return snapshot
|
|
||||||
}()
|
|
||||||
guard let snapshot = _snapshot else {
|
|
||||||
transitionContext.completeTransition(false)
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
let transitionMaskView = UIView(frame: transitionContext.containerView.bounds)
|
|
||||||
transitionMaskView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
||||||
transitionContext.containerView.addSubview(transitionMaskView)
|
|
||||||
|
|
||||||
let maskLayer = CAShapeLayer()
|
|
||||||
maskLayer.frame = transitionMaskView.bounds
|
|
||||||
let maskLayerFromPath = UIBezierPath(rect: maskLayer.bounds).cgPath
|
|
||||||
maskLayer.path = maskLayerFromPath
|
|
||||||
transitionMaskView.layer.mask = maskLayer
|
|
||||||
|
|
||||||
transitionMaskView.addSubview(snapshot)
|
|
||||||
snapshot.center = transitionMaskView.center
|
|
||||||
fromVC.view.bringSubviewToFront(fromVC.closeButtonBackground)
|
|
||||||
|
|
||||||
transitionItem.imageView = imageView
|
|
||||||
transitionItem.snapshotTransitioning = snapshot
|
|
||||||
transitionItem.initialFrame = snapshot.frame
|
|
||||||
transitionItem.targetFrame = targetFrame
|
|
||||||
|
|
||||||
// disable interaction
|
|
||||||
fromVC.pagingViewController.isUserInteractionEnabled = false
|
|
||||||
|
|
||||||
let animator = popInteractiveTransitionAnimator
|
let animator = popInteractiveTransitionAnimator
|
||||||
|
|
||||||
self.transitionItem.snapshotRaw?.alpha = 0.0
|
|
||||||
|
|
||||||
var needsMaskWithAnimation = true
|
animator.addCompletion { position in
|
||||||
let maskLayerToRect: CGRect? = {
|
transitionContext.completeTransition(position == .end)
|
||||||
guard case .attachments = transitionItem.source else { return nil }
|
|
||||||
guard let navigationBar = toVC.navigationController?.navigationBar, let navigationBarSuperView = navigationBar.superview else { return nil }
|
|
||||||
let navigationBarFrameInWindow = navigationBarSuperView.convert(navigationBar.frame, to: nil)
|
|
||||||
|
|
||||||
// crop rect top edge
|
|
||||||
var rect = transitionMaskView.frame
|
|
||||||
let _toViewFrameInWindow = toVC.view.superview.flatMap { $0.convert(toVC.view.frame, to: nil) }
|
|
||||||
if let toViewFrameInWindow = _toViewFrameInWindow, toViewFrameInWindow.minY > navigationBarFrameInWindow.maxY {
|
|
||||||
rect.origin.y = toViewFrameInWindow.minY
|
|
||||||
} else {
|
|
||||||
rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline
|
|
||||||
}
|
|
||||||
|
|
||||||
if rect.minY < snapshot.frame.minY {
|
|
||||||
needsMaskWithAnimation = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return rect
|
|
||||||
}()
|
|
||||||
let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath
|
|
||||||
let maskLayerToFinalRect: CGRect? = {
|
|
||||||
guard case .attachments = transitionItem.source else { return nil }
|
|
||||||
var rect = maskLayerToRect ?? transitionMaskView.frame
|
|
||||||
// clip tabBar when bar visible
|
|
||||||
guard let tabBarController = toVC.tabBarController,
|
|
||||||
!tabBarController.tabBar.isHidden,
|
|
||||||
let tabBarSuperView = tabBarController.tabBar.superview
|
|
||||||
else { return rect }
|
|
||||||
let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil)
|
|
||||||
let offset = rect.maxY - tabBarFrameInWindow.minY
|
|
||||||
guard offset > 0 else { return rect }
|
|
||||||
rect.size.height -= offset
|
|
||||||
return rect
|
|
||||||
}()
|
|
||||||
let maskLayerToFinalPath = maskLayerToFinalRect.flatMap { UIBezierPath(rect: $0) }?.cgPath
|
|
||||||
|
|
||||||
if !needsMaskWithAnimation, let maskLayerToPath = maskLayerToPath {
|
|
||||||
maskLayer.path = maskLayerToPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
animator.addAnimations {
|
guard let fromVC = transitionContext.viewController(forKey: .from) as? MediaPreviewViewController,
|
||||||
if let targetFrame = targetFrame {
|
let index = fromVC.pagingViewController.currentIndex,
|
||||||
self.transitionItem.snapshotTransitioning?.frame = targetFrame
|
let fromView = transitionContext.view(forKey: .from),
|
||||||
} else {
|
let mediaPreviewTransitionViewController = fromVC.pagingViewController.currentViewController as? MediaPreviewTransitionViewController,
|
||||||
fromView.alpha = 0
|
let mediaPreviewTransitionContext = mediaPreviewTransitionViewController.mediaPreviewTransitionContext
|
||||||
|
else {
|
||||||
|
animator.addAnimations {
|
||||||
|
self.transitionItem.source.updateAppearance(position: .end, index: nil)
|
||||||
}
|
}
|
||||||
self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 }
|
return animator
|
||||||
|
}
|
||||||
|
|
||||||
|
// update close button
|
||||||
|
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
|
||||||
fromVC.closeButtonBackground.alpha = 0
|
fromVC.closeButtonBackground.alpha = 0
|
||||||
fromVC.visualEffectView.effect = nil
|
}
|
||||||
if let maskLayerToFinalPath = maskLayerToFinalPath {
|
animator.addCompletion { position in
|
||||||
maskLayer.path = maskLayerToFinalPath
|
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
|
||||||
|
fromVC.closeButtonBackground.alpha = position == .end ? 0 : 1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update view controller
|
||||||
|
fromVC.pagingViewController.isUserInteractionEnabled = false
|
||||||
|
animator.addCompletion { position in
|
||||||
|
fromVC.pagingViewController.isUserInteractionEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// update background
|
||||||
|
let blurEffect = fromVC.visualEffectView.effect
|
||||||
|
animator.addAnimations {
|
||||||
|
fromVC.visualEffectView.effect = nil
|
||||||
if UIAccessibility.isReduceTransparencyEnabled {
|
if UIAccessibility.isReduceTransparencyEnabled {
|
||||||
fromVC.visualEffectView.alpha = 0
|
fromVC.visualEffectView.alpha = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
animator.addCompletion { position in
|
animator.addCompletion { position in
|
||||||
self.transitionItem.snapshotTransitioning?.removeFromSuperview()
|
fromVC.visualEffectView.effect = position == .end ? nil : blurEffect
|
||||||
self.transitionItem.source.updateAppearance(position: position, index: nil)
|
if UIAccessibility.isReduceTransparencyEnabled {
|
||||||
transitionContext.completeTransition(position == .end)
|
fromVC.visualEffectView.alpha = position == .end ? 0 : 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update transition item source
|
||||||
|
animator.addCompletion { position in
|
||||||
|
if position == .end {
|
||||||
|
// reset appearance
|
||||||
|
self.transitionItem.source.updateAppearance(position: position, index: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update transitioning snapshot
|
||||||
|
let transitionMaskView = UIView(frame: transitionContext.containerView.bounds)
|
||||||
|
transitionMaskView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
transitionContext.containerView.addSubview(transitionMaskView)
|
||||||
|
transitionItem.interactiveTransitionMaskView = transitionMaskView
|
||||||
|
|
||||||
|
animator.addCompletion { position in
|
||||||
|
transitionMaskView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
let transitionMaskViewTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||||
|
transitionMaskViewTapGestureRecognizer.addTarget(self, action: #selector(MediaHostToMediaPreviewViewControllerAnimatedTransitioning.transitionMaskViewTapGestureRecognizerHandler(_:)))
|
||||||
|
transitionMaskView.addGestureRecognizer(transitionMaskViewTapGestureRecognizer)
|
||||||
|
|
||||||
|
let maskLayer = CAShapeLayer()
|
||||||
|
maskLayer.frame = transitionMaskView.bounds
|
||||||
|
maskLayer.path = UIBezierPath(rect: maskLayer.bounds).cgPath
|
||||||
|
transitionMaskView.layer.mask = maskLayer
|
||||||
|
transitionItem.interactiveTransitionMaskLayer = maskLayer
|
||||||
|
|
||||||
|
// attach transitioning snapshot
|
||||||
|
mediaPreviewTransitionContext.snapshot.center = transitionMaskView.center
|
||||||
|
mediaPreviewTransitionContext.snapshot.contentMode = .scaleAspectFill
|
||||||
|
mediaPreviewTransitionContext.snapshot.clipsToBounds = true
|
||||||
|
transitionMaskView.addSubview(mediaPreviewTransitionContext.snapshot)
|
||||||
|
fromVC.view.bringSubviewToFront(fromVC.closeButtonBackground)
|
||||||
|
|
||||||
|
transitionItem.transitionView = mediaPreviewTransitionContext.transitionView
|
||||||
|
transitionItem.snapshotTransitioning = mediaPreviewTransitionContext.snapshot
|
||||||
|
transitionItem.initialFrame = mediaPreviewTransitionContext.snapshot.frame
|
||||||
|
|
||||||
|
// assert view hierarchy not change
|
||||||
|
let toVC = transitionItem.previewableViewController
|
||||||
|
let targetFrame = toVC.sourceFrame(transitionItem: transitionItem, index: index)
|
||||||
|
transitionItem.targetFrame = targetFrame
|
||||||
|
|
||||||
|
animator.addAnimations {
|
||||||
|
self.transitionItem.snapshotTransitioning?.layer.cornerRadius = self.transitionItem.sourceImageViewCornerRadius ?? 0
|
||||||
|
}
|
||||||
|
animator.addCompletion { position in
|
||||||
|
self.transitionItem.snapshotTransitioning?.layer.cornerRadius = position == .end ? 0 : (self.transitionItem.sourceImageViewCornerRadius ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isInteractive {
|
||||||
|
animator.addAnimations {
|
||||||
|
if let targetFrame = targetFrame {
|
||||||
|
self.transitionItem.snapshotTransitioning?.frame = targetFrame
|
||||||
|
} else {
|
||||||
|
fromView.alpha = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate transition mask
|
||||||
|
let maskLayerToRect: CGRect? = {
|
||||||
|
guard case .attachments = transitionItem.source else { return nil }
|
||||||
|
guard let navigationBar = toVC.navigationController?.navigationBar, let navigationBarSuperView = navigationBar.superview else { return nil }
|
||||||
|
let navigationBarFrameInWindow = navigationBarSuperView.convert(navigationBar.frame, to: nil)
|
||||||
|
|
||||||
|
// crop rect top edge
|
||||||
|
var rect = transitionMaskView.frame
|
||||||
|
let _toViewFrameInWindow = toVC.view.superview.flatMap { $0.convert(toVC.view.frame, to: nil) }
|
||||||
|
if let toViewFrameInWindow = _toViewFrameInWindow, toViewFrameInWindow.minY > navigationBarFrameInWindow.maxY {
|
||||||
|
rect.origin.y = toViewFrameInWindow.minY
|
||||||
|
} else {
|
||||||
|
rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline
|
||||||
|
}
|
||||||
|
|
||||||
|
return rect
|
||||||
|
}()
|
||||||
|
let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath
|
||||||
|
let maskLayerToFinalRect: CGRect? = {
|
||||||
|
guard case .attachments = transitionItem.source else { return nil }
|
||||||
|
var rect = maskLayerToRect ?? transitionMaskView.frame
|
||||||
|
// clip tabBar when bar visible
|
||||||
|
guard let tabBarController = toVC.tabBarController,
|
||||||
|
!tabBarController.tabBar.isHidden,
|
||||||
|
let tabBarSuperView = tabBarController.tabBar.superview
|
||||||
|
else { return rect }
|
||||||
|
let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil)
|
||||||
|
let offset = rect.maxY - tabBarFrameInWindow.minY
|
||||||
|
guard offset > 0 else { return rect }
|
||||||
|
rect.size.height -= offset
|
||||||
|
return rect
|
||||||
|
}()
|
||||||
|
let maskLayerToFinalPath = maskLayerToFinalRect.flatMap { UIBezierPath(rect: $0) }?.cgPath
|
||||||
|
|
||||||
|
if let maskLayerToPath = maskLayerToPath {
|
||||||
|
maskLayer.path = maskLayerToPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaPreviewTransitionContext.transitionView.isHidden = true
|
||||||
|
animator.addCompletion { position in
|
||||||
|
self.transitionItem.transitionView?.isHidden = position == .end
|
||||||
|
self.transitionItem.snapshotRaw?.alpha = position == .start ? 1.0 : 0.0
|
||||||
|
self.transitionItem.snapshotTransitioning?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
return animator
|
return animator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,100 +292,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func popInteractiveTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
private func popInteractiveTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? MediaPreviewViewController,
|
popTransition(using: transitionContext)
|
||||||
let _ = transitionContext.view(forKey: .from),
|
|
||||||
let mediaPreviewImageViewController = fromVC.pagingViewController.currentViewController as? MediaPreviewImageViewController,
|
|
||||||
let index = fromVC.pagingViewController.currentIndex else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert view hierarchy not change
|
|
||||||
let toVC = transitionItem.previewableViewController
|
|
||||||
let targetFrame = toVC.sourceFrame(transitionItem: transitionItem, index: index)
|
|
||||||
|
|
||||||
let imageView = mediaPreviewImageViewController.previewImageView.imageView
|
|
||||||
let _snapshot: UIView? = {
|
|
||||||
transitionItem.snapshotRaw = imageView
|
|
||||||
let snapshot = imageView.snapshotView(afterScreenUpdates: false)
|
|
||||||
snapshot?.clipsToBounds = true
|
|
||||||
snapshot?.contentMode = .scaleAspectFill
|
|
||||||
return snapshot
|
|
||||||
}()
|
|
||||||
guard let snapshot = _snapshot else {
|
|
||||||
transitionContext.completeTransition(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let transitionMaskView = UIView(frame: transitionContext.containerView.bounds)
|
|
||||||
transitionMaskView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
||||||
transitionContext.containerView.addSubview(transitionMaskView)
|
|
||||||
transitionItem.interactiveTransitionMaskView = transitionMaskView
|
|
||||||
|
|
||||||
let transitionMaskViewTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
|
||||||
transitionMaskViewTapGestureRecognizer.addTarget(self, action: #selector(MediaHostToMediaPreviewViewControllerAnimatedTransitioning.transitionMaskViewTapGestureRecognizerHandler(_:)))
|
|
||||||
transitionMaskView.addGestureRecognizer(transitionMaskViewTapGestureRecognizer)
|
|
||||||
|
|
||||||
let maskLayer = CAShapeLayer()
|
|
||||||
maskLayer.frame = transitionMaskView.bounds
|
|
||||||
maskLayer.path = UIBezierPath(rect: maskLayer.bounds).cgPath
|
|
||||||
transitionMaskView.layer.mask = maskLayer
|
|
||||||
transitionItem.interactiveTransitionMaskLayer = maskLayer
|
|
||||||
|
|
||||||
transitionMaskView.addSubview(snapshot)
|
|
||||||
snapshot.center = transitionMaskView.center
|
|
||||||
fromVC.view.bringSubviewToFront(fromVC.closeButtonBackground)
|
|
||||||
|
|
||||||
transitionItem.imageView = imageView
|
|
||||||
transitionItem.snapshotTransitioning = snapshot
|
|
||||||
transitionItem.initialFrame = snapshot.frame
|
|
||||||
transitionItem.targetFrame = targetFrame ?? snapshot.frame
|
|
||||||
|
|
||||||
// disable interaction
|
|
||||||
fromVC.pagingViewController.isUserInteractionEnabled = false
|
|
||||||
|
|
||||||
let animator = popInteractiveTransitionAnimator
|
|
||||||
|
|
||||||
let blurEffect = fromVC.visualEffectView.effect
|
|
||||||
self.transitionItem.snapshotRaw?.alpha = 0.0
|
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
|
|
||||||
fromVC.closeButtonBackground.alpha = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
animator.addAnimations {
|
|
||||||
switch self.transitionItem.source {
|
|
||||||
case .profileBanner:
|
|
||||||
self.transitionItem.snapshotTransitioning?.alpha = 0.4
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fromVC.visualEffectView.effect = nil
|
|
||||||
self.transitionItem.sourceImageViewCornerRadius.flatMap { self.transitionItem.snapshotTransitioning?.layer.cornerRadius = $0 }
|
|
||||||
if UIAccessibility.isReduceTransparencyEnabled {
|
|
||||||
fromVC.visualEffectView.alpha = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
animator.addCompletion { position in
|
|
||||||
fromVC.pagingViewController.isUserInteractionEnabled = true
|
|
||||||
fromVC.closeButtonBackground.alpha = position == .end ? 0 : 1
|
|
||||||
self.transitionItem.imageView?.isHidden = position == .end
|
|
||||||
self.transitionItem.snapshotRaw?.alpha = position == .start ? 1.0 : 0.0
|
|
||||||
self.transitionItem.snapshotTransitioning?.removeFromSuperview()
|
|
||||||
if position == .end {
|
|
||||||
// reset appearance
|
|
||||||
self.transitionItem.source.updateAppearance(position: position, index: nil)
|
|
||||||
}
|
|
||||||
fromVC.visualEffectView.effect = position == .end ? nil : blurEffect
|
|
||||||
transitionMaskView.removeFromSuperview()
|
|
||||||
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
|
|
||||||
fromVC.closeButtonBackground.alpha = position == .end ? 0 : 1
|
|
||||||
}
|
|
||||||
if UIAccessibility.isReduceTransparencyEnabled {
|
|
||||||
fromVC.visualEffectView.alpha = position == .end ? 0 : 1
|
|
||||||
}
|
|
||||||
transitionContext.completeTransition(position == .end)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -380,7 +331,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
||||||
popInteractiveTransitionAnimator.fractionComplete = percent
|
popInteractiveTransitionAnimator.fractionComplete = percent
|
||||||
transitionContext.updateInteractiveTransition(percent)
|
transitionContext.updateInteractiveTransition(percent)
|
||||||
updateTransitionItemPosition(of: translation)
|
updateTransitionItemPosition(of: translation)
|
||||||
|
|
||||||
// Reset translation to zero
|
// Reset translation to zero
|
||||||
sender.setTranslation(CGPoint.zero, in: transitionContext.containerView)
|
sender.setTranslation(CGPoint.zero, in: transitionContext.containerView)
|
||||||
case .ended, .cancelled:
|
case .ended, .cancelled:
|
||||||
|
@ -399,7 +350,9 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func convert(_ velocity: CGPoint, for item: MediaPreviewTransitionItem?) -> CGVector {
|
private func convert(_ velocity: CGPoint, for item: MediaPreviewTransitionItem?) -> CGVector {
|
||||||
guard let currentFrame = item?.imageView?.frame, let targetFrame = item?.targetFrame else {
|
guard let currentFrame = item?.transitionView?.frame,
|
||||||
|
let targetFrame = item?.targetFrame
|
||||||
|
else {
|
||||||
return CGVector.zero
|
return CGVector.zero
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,25 +16,28 @@ class MediaPreviewTransitionItem: Identifiable {
|
||||||
var previewableViewController: MediaPreviewableViewController
|
var previewableViewController: MediaPreviewableViewController
|
||||||
|
|
||||||
// source
|
// source
|
||||||
// value maybe invalid when preview paging
|
|
||||||
var image: UIImage?
|
var image: UIImage?
|
||||||
var aspectRatio: CGSize?
|
var aspectRatio: CGSize?
|
||||||
var initialFrame: CGRect? = nil
|
var initialFrame: CGRect? = nil
|
||||||
var sourceImageView: UIImageView?
|
var sourceImageView: UIImageView?
|
||||||
var sourceImageViewCornerRadius: CGFloat?
|
var sourceImageViewCornerRadius: CGFloat?
|
||||||
|
|
||||||
// target
|
// target
|
||||||
var targetFrame: CGRect? = nil
|
var targetFrame: CGRect? = nil
|
||||||
|
|
||||||
// transitioning
|
// transitioning
|
||||||
var imageView: UIImageView?
|
var transitionView: UIView?
|
||||||
var snapshotRaw: UIView?
|
var snapshotRaw: UIView?
|
||||||
var snapshotTransitioning: UIView?
|
var snapshotTransitioning: UIView?
|
||||||
var touchOffset: CGVector = CGVector.zero
|
var touchOffset: CGVector = CGVector.zero
|
||||||
var interactiveTransitionMaskView: UIView?
|
var interactiveTransitionMaskView: UIView?
|
||||||
var interactiveTransitionMaskLayer: CAShapeLayer?
|
var interactiveTransitionMaskLayer: CAShapeLayer?
|
||||||
|
|
||||||
init(id: UUID = UUID(), source: Source, previewableViewController: MediaPreviewableViewController) {
|
init(
|
||||||
|
id: UUID = UUID(),
|
||||||
|
source: Source,
|
||||||
|
previewableViewController: MediaPreviewableViewController
|
||||||
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.source = source
|
self.source = source
|
||||||
self.previewableViewController = previewableViewController
|
self.previewableViewController = previewableViewController
|
||||||
|
@ -56,7 +59,7 @@ extension MediaPreviewTransitionItem {
|
||||||
mediaView.alpha = alpha
|
mediaView.alpha = alpha
|
||||||
case .attachments(let mediaGridContainerView):
|
case .attachments(let mediaGridContainerView):
|
||||||
if let index = index {
|
if let index = index {
|
||||||
mediaGridContainerView.setAlpha(0, index: index)
|
mediaGridContainerView.setAlpha(alpha, index: index)
|
||||||
} else {
|
} else {
|
||||||
mediaGridContainerView.setAlpha(alpha)
|
mediaGridContainerView.setAlpha(alpha)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// MediaPreviewTransitionViewController.swift
|
||||||
|
// TwidereX
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2021-12-8.
|
||||||
|
// Copyright © 2021 Twidere. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol MediaPreviewTransitionViewController: UIViewController {
|
||||||
|
var mediaPreviewTransitionContext: MediaPreviewTransitionContext? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct MediaPreviewTransitionContext {
|
||||||
|
let transitionView: UIView
|
||||||
|
let snapshot: UIView
|
||||||
|
let snapshotTransitioning: UIView
|
||||||
|
}
|
|
@ -1,150 +0,0 @@
|
||||||
//
|
|
||||||
// AudioPlayer.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by sxiaojian on 2021/3/8.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AVFoundation
|
|
||||||
import Combine
|
|
||||||
import CoreDataStack
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import os.log
|
|
||||||
|
|
||||||
final class AudioPlaybackService: NSObject {
|
|
||||||
|
|
||||||
static let appWillPlayAudioNotification = NSNotification.Name(rawValue: "org.joinmastodon.app.audio-playback-service.appWillPlayAudio")
|
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
var player = AVPlayer()
|
|
||||||
var timeObserver: Any?
|
|
||||||
var statusObserver: Any?
|
|
||||||
var attachment: MastodonAttachment?
|
|
||||||
|
|
||||||
let playbackState = CurrentValueSubject<PlaybackState, Never>(PlaybackState.unknown)
|
|
||||||
|
|
||||||
let currentTimeSubject = CurrentValueSubject<TimeInterval, Never>(0)
|
|
||||||
|
|
||||||
override init() {
|
|
||||||
super.init()
|
|
||||||
addObserver()
|
|
||||||
|
|
||||||
playbackState
|
|
||||||
.receive(on: RunLoop.main)
|
|
||||||
.sink { status in
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: audio status: %s", ((#file as NSString).lastPathComponent), #line, #function, status.description)
|
|
||||||
switch status {
|
|
||||||
case .unknown, .buffering, .readyToPlay:
|
|
||||||
break
|
|
||||||
case .playing:
|
|
||||||
try? AVAudioSession.sharedInstance().setCategory(.playback)
|
|
||||||
try? AVAudioSession.sharedInstance().setActive(true)
|
|
||||||
case .paused, .stopped, .failed:
|
|
||||||
try? AVAudioSession.sharedInstance().setCategory(.ambient) // set to ambient to allow mixed (needed for GIFV)
|
|
||||||
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AudioPlaybackService {
|
|
||||||
func playAudio(audioAttachment: MastodonAttachment) {
|
|
||||||
guard let assetURL = audioAttachment.assetURL,
|
|
||||||
let url = URL(string: assetURL) else
|
|
||||||
{ return }
|
|
||||||
|
|
||||||
notifyWillPlayAudioNotification()
|
|
||||||
if audioAttachment == attachment {
|
|
||||||
if self.playbackState.value == .stopped {
|
|
||||||
self.seekToTime(time: .zero)
|
|
||||||
}
|
|
||||||
player.play()
|
|
||||||
self.playbackState.value = .playing
|
|
||||||
return
|
|
||||||
}
|
|
||||||
player.pause()
|
|
||||||
let playerItem = AVPlayerItem(url: url)
|
|
||||||
player.replaceCurrentItem(with: playerItem)
|
|
||||||
attachment = audioAttachment
|
|
||||||
player.play()
|
|
||||||
playbackState.value = .playing
|
|
||||||
}
|
|
||||||
|
|
||||||
func addObserver() {
|
|
||||||
NotificationCenter.default.publisher(for: VideoPlayerViewModel.appWillPlayVideoNotification)
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.pauseIfNeed()
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
|
||||||
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: CMTimeScale(NSEC_PER_SEC)), queue: DispatchQueue.main, using: { [weak self] time in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.currentTimeSubject.value = time.seconds
|
|
||||||
})
|
|
||||||
player.publisher(for: \.status, options: [.initial, .new])
|
|
||||||
.sink(receiveValue: { [weak self] status in
|
|
||||||
guard let self = self else { return }
|
|
||||||
switch status {
|
|
||||||
case .failed:
|
|
||||||
self.playbackState.value = .failed
|
|
||||||
case .readyToPlay:
|
|
||||||
self.playbackState.value = .readyToPlay
|
|
||||||
case .unknown:
|
|
||||||
self.playbackState.value = .unknown
|
|
||||||
@unknown default:
|
|
||||||
assertionFailure()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: nil)
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.player.seek(to: .zero)
|
|
||||||
self.playbackState.value = .stopped
|
|
||||||
self.currentTimeSubject.value = 0
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
NotificationCenter.default.publisher(for: AVAudioSession.interruptionNotification, object: nil)
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.pause()
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func notifyWillPlayAudioNotification() {
|
|
||||||
NotificationCenter.default.post(name: AudioPlaybackService.appWillPlayAudioNotification, object: nil)
|
|
||||||
}
|
|
||||||
func isPlaying() -> Bool {
|
|
||||||
return playbackState.value == .readyToPlay || playbackState.value == .playing
|
|
||||||
}
|
|
||||||
func resume() {
|
|
||||||
notifyWillPlayAudioNotification()
|
|
||||||
player.play()
|
|
||||||
playbackState.value = .playing
|
|
||||||
}
|
|
||||||
|
|
||||||
func pause() {
|
|
||||||
player.pause()
|
|
||||||
playbackState.value = .paused
|
|
||||||
}
|
|
||||||
func pauseIfNeed() {
|
|
||||||
if isPlaying() {
|
|
||||||
pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func seekToTime(time: TimeInterval) {
|
|
||||||
player.seek(to: CMTimeMake(value:Int64(time), timescale: 1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AudioPlaybackService {
|
|
||||||
func viewDidDisappear(from viewController: UIViewController?) {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
|
||||||
pause()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
//
|
|
||||||
// ViedeoPlaybackService.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by xiaojian sun on 2021/3/10.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AVKit
|
|
||||||
import Combine
|
|
||||||
import CoreDataStack
|
|
||||||
import Foundation
|
|
||||||
import os.log
|
|
||||||
|
|
||||||
final class VideoPlaybackService {
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.VideoPlaybackService.working-queue")
|
|
||||||
private(set) var viewPlayerViewModelDict: [URL: VideoPlayerViewModel] = [:]
|
|
||||||
|
|
||||||
// only for video kind
|
|
||||||
weak var latestPlayingVideoPlayerViewModel: VideoPlayerViewModel?
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VideoPlaybackService {
|
|
||||||
private func playerViewModel(_ playerViewModel: VideoPlayerViewModel, didUpdateTimeControlStatus: AVPlayer.TimeControlStatus) {
|
|
||||||
switch playerViewModel.videoKind {
|
|
||||||
case .gif:
|
|
||||||
// do nothing
|
|
||||||
return
|
|
||||||
case .video:
|
|
||||||
if playerViewModel.timeControlStatus.value != .paused {
|
|
||||||
latestPlayingVideoPlayerViewModel = playerViewModel
|
|
||||||
|
|
||||||
// pause other player
|
|
||||||
for viewModel in viewPlayerViewModelDict.values {
|
|
||||||
guard viewModel.timeControlStatus.value != .paused else { continue }
|
|
||||||
guard viewModel !== playerViewModel else { continue }
|
|
||||||
viewModel.pause()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if latestPlayingVideoPlayerViewModel === playerViewModel {
|
|
||||||
latestPlayingVideoPlayerViewModel = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VideoPlaybackService {
|
|
||||||
func dequeueVideoPlayerViewModel(for media: MastodonAttachment) -> VideoPlayerViewModel? {
|
|
||||||
// Core Data entity not thread-safe. Save attribute before enter working queue
|
|
||||||
guard let assetURL = media.assetURL,
|
|
||||||
let url = URL(string: assetURL),
|
|
||||||
media.kind == .gifv || media.kind == .video
|
|
||||||
else { return nil }
|
|
||||||
|
|
||||||
let previewImageURL = media.previewURL.flatMap { URL(string: $0) }
|
|
||||||
let videoKind: VideoPlayerViewModel.Kind = media.kind == .gifv ? .gif : .video
|
|
||||||
|
|
||||||
var _viewModel: VideoPlayerViewModel?
|
|
||||||
workingQueue.sync {
|
|
||||||
if let viewModel = viewPlayerViewModelDict[url] {
|
|
||||||
_viewModel = viewModel
|
|
||||||
} else {
|
|
||||||
let viewModel = VideoPlayerViewModel(
|
|
||||||
previewImageURL: previewImageURL,
|
|
||||||
videoURL: url,
|
|
||||||
videoSize: media.size,
|
|
||||||
videoKind: videoKind
|
|
||||||
)
|
|
||||||
viewPlayerViewModelDict[url] = viewModel
|
|
||||||
setupListener(for: viewModel)
|
|
||||||
_viewModel = viewModel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _viewModel
|
|
||||||
}
|
|
||||||
|
|
||||||
func playerViewModel(for playerViewController: AVPlayerViewController) -> VideoPlayerViewModel? {
|
|
||||||
guard let url = (playerViewController.player?.currentItem?.asset as? AVURLAsset)?.url else { return nil }
|
|
||||||
return viewPlayerViewModelDict[url]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupListener(for viewModel: VideoPlayerViewModel) {
|
|
||||||
viewModel.timeControlStatus
|
|
||||||
.sink { [weak self] timeControlStatus in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.playerViewModel(viewModel, didUpdateTimeControlStatus: timeControlStatus)
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
|
||||||
NotificationCenter.default.publisher(for: AudioPlaybackService.appWillPlayAudioNotification)
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.pauseWhenPlayAudio()
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VideoPlaybackService {
|
|
||||||
func markTransitioning(for status: Status) {
|
|
||||||
// TODO:
|
|
||||||
// guard let videoAttachment = status.mediaAttachments?.filter({ $0.type == .gifv || $0.type == .video }).first else { return }
|
|
||||||
// guard let videoPlayerViewModel = dequeueVideoPlayerViewModel(for: videoAttachment) else { return }
|
|
||||||
// videoPlayerViewModel.isTransitioning = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func viewDidDisappear(from viewController: UIViewController?) {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
|
||||||
|
|
||||||
// note: do not retain view controller
|
|
||||||
// pause all player when view disappear exclude full screen player and other transitioning scene
|
|
||||||
for viewModel in viewPlayerViewModelDict.values {
|
|
||||||
guard !viewModel.isTransitioning else {
|
|
||||||
viewModel.isTransitioning = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
guard !viewModel.isFullScreenPresentationing else {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: isFullScreenPresentationing", (#file as NSString).lastPathComponent, #line, #function)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
guard viewModel.videoKind == .video else { continue }
|
|
||||||
viewModel.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func pauseWhenPlayAudio() {
|
|
||||||
for viewModel in viewPlayerViewModelDict.values {
|
|
||||||
guard !viewModel.isTransitioning else {
|
|
||||||
viewModel.isTransitioning = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
guard !viewModel.isFullScreenPresentationing else {
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: isFullScreenPresentationing", (#file as NSString).lastPathComponent, #line, #function)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
viewModel.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,8 +25,6 @@ class AppContext: ObservableObject {
|
||||||
let apiService: APIService
|
let apiService: APIService
|
||||||
let authenticationService: AuthenticationService
|
let authenticationService: AuthenticationService
|
||||||
let emojiService: EmojiService
|
let emojiService: EmojiService
|
||||||
let audioPlaybackService = AudioPlaybackService()
|
|
||||||
let videoPlaybackService = VideoPlaybackService()
|
|
||||||
let statusPublishService = StatusPublishService()
|
let statusPublishService = StatusPublishService()
|
||||||
let notificationService: NotificationService
|
let notificationService: NotificationService
|
||||||
let settingService: SettingService
|
let settingService: SettingService
|
||||||
|
|
|
@ -24,6 +24,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
// configure appearance
|
// configure appearance
|
||||||
ThemeService.shared.apply(theme: ThemeService.shared.currentTheme.value)
|
ThemeService.shared.apply(theme: ThemeService.shared.currentTheme.value)
|
||||||
|
|
||||||
|
// configure AudioSession
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(.ambient)
|
||||||
|
|
||||||
// Update app version info. See: `Settings.bundle`
|
// Update app version info. See: `Settings.bundle`
|
||||||
UserDefaults.standard.setValue(UIApplication.appVersion(), forKey: "Mastodon.appVersion")
|
UserDefaults.standard.setValue(UIApplication.appVersion(), forKey: "Mastodon.appVersion")
|
||||||
UserDefaults.standard.setValue(UIApplication.appBuild(), forKey: "Mastodon.appBundle")
|
UserDefaults.standard.setValue(UIApplication.appBuild(), forKey: "Mastodon.appBundle")
|
||||||
|
|
|
@ -113,7 +113,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
// Called as the scene transitions from the foreground to the background.
|
// Called as the scene transitions from the foreground to the background.
|
||||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||||
// to restore the scene back to its current state.
|
// to restore the scene back to its current state.
|
||||||
AppContext.shared.audioPlaybackService.pauseIfNeed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
//
|
||||||
|
// AudioViewContainer.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/3/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreDataStack
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import MastodonAsset
|
||||||
|
import MastodonLocalization
|
||||||
|
|
||||||
|
//public final class AudioContainerView: UIView {
|
||||||
|
// static let cornerRadius: CGFloat = 22
|
||||||
|
//
|
||||||
|
// let container: UIStackView = {
|
||||||
|
// let stackView = UIStackView()
|
||||||
|
// stackView.axis = .horizontal
|
||||||
|
// stackView.distribution = .fill
|
||||||
|
// stackView.alignment = .center
|
||||||
|
// stackView.spacing = 11
|
||||||
|
// stackView.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
|
||||||
|
// stackView.isLayoutMarginsRelativeArrangement = true
|
||||||
|
// stackView.layer.cornerRadius = AudioContainerView.cornerRadius
|
||||||
|
// stackView.clipsToBounds = true
|
||||||
|
// stackView.backgroundColor = Asset.Colors.brandBlue.color
|
||||||
|
// stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
// return stackView
|
||||||
|
// }()
|
||||||
|
//
|
||||||
|
// let playButtonBackgroundView: UIView = {
|
||||||
|
// let view = UIView()
|
||||||
|
// view.layer.cornerRadius = 16
|
||||||
|
// view.clipsToBounds = true
|
||||||
|
// view.backgroundColor = Asset.Colors.brandBlue.color
|
||||||
|
// view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
// return view
|
||||||
|
// }()
|
||||||
|
//
|
||||||
|
// let playButton: UIButton = {
|
||||||
|
// let button = HighlightDimmableButton(type: .custom)
|
||||||
|
// let image = UIImage(systemName: "play.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 32, weight: .bold))!
|
||||||
|
// button.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal)
|
||||||
|
//
|
||||||
|
// let pauseImage = UIImage(systemName: "pause.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 32, weight: .bold))!
|
||||||
|
// button.setImage(pauseImage.withRenderingMode(.alwaysTemplate), for: .selected)
|
||||||
|
//
|
||||||
|
// button.tintColor = .white
|
||||||
|
// button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
// button.isEnabled = true
|
||||||
|
// return button
|
||||||
|
// }()
|
||||||
|
//
|
||||||
|
// let slider: UISlider = {
|
||||||
|
// let slider = UISlider()
|
||||||
|
// slider.isContinuous = true
|
||||||
|
// slider.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
// slider.minimumTrackTintColor = Asset.Colors.Slider.track.color
|
||||||
|
// slider.maximumTrackTintColor = Asset.Colors.Slider.track.color
|
||||||
|
// if let image = UIImage.placeholder(size: CGSize(width: 22, height: 22), color: .white).withRoundedCorners(radius: 11) {
|
||||||
|
// slider.setThumbImage(image, for: .normal)
|
||||||
|
// }
|
||||||
|
// return slider
|
||||||
|
// }()
|
||||||
|
//
|
||||||
|
// let timeLabel: UILabel = {
|
||||||
|
// let label = UILabel()
|
||||||
|
// label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
// label.font = .systemFont(ofSize: 13, weight: .regular)
|
||||||
|
// label.textColor = .white
|
||||||
|
// label.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .right : .left
|
||||||
|
// return label
|
||||||
|
// }()
|
||||||
|
//
|
||||||
|
// override init(frame: CGRect) {
|
||||||
|
// super.init(frame: frame)
|
||||||
|
// _init()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// required init?(coder: NSCoder) {
|
||||||
|
// super.init(coder: coder)
|
||||||
|
// _init()
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//extension AudioContainerView {
|
||||||
|
// private func _init() {
|
||||||
|
// addSubview(container)
|
||||||
|
// NSLayoutConstraint.activate([
|
||||||
|
// container.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
// container.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
// trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||||
|
// bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||||
|
// ])
|
||||||
|
//
|
||||||
|
// // checkmark
|
||||||
|
// playButtonBackgroundView.addSubview(playButton)
|
||||||
|
// container.addArrangedSubview(playButtonBackgroundView)
|
||||||
|
// NSLayoutConstraint.activate([
|
||||||
|
// playButton.centerXAnchor.constraint(equalTo: playButtonBackgroundView.centerXAnchor),
|
||||||
|
// playButton.centerYAnchor.constraint(equalTo: playButtonBackgroundView.centerYAnchor),
|
||||||
|
// playButtonBackgroundView.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1),
|
||||||
|
// playButtonBackgroundView.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1),
|
||||||
|
// ])
|
||||||
|
//
|
||||||
|
// container.addArrangedSubview(slider)
|
||||||
|
//
|
||||||
|
// container.addArrangedSubview(timeLabel)
|
||||||
|
// NSLayoutConstraint.activate([
|
||||||
|
// timeLabel.widthAnchor.constraint(equalToConstant: 40).priority(.required - 1),
|
||||||
|
// ])
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//extension AudioContainerView {
|
||||||
|
// public struct Configuration: Hashable {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//#if canImport(SwiftUI) && DEBUG
|
||||||
|
//import SwiftUI
|
||||||
|
//
|
||||||
|
//struct AudioContainerView_Previews: PreviewProvider {
|
||||||
|
//
|
||||||
|
// static var previews: some View {
|
||||||
|
// UIViewPreview(width: 375) {
|
||||||
|
// AudioContainerView()
|
||||||
|
// }
|
||||||
|
// .previewLayout(.fixed(width: 375, height: 100))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//}
|
||||||
|
//#endif
|
||||||
|
//
|
|
@ -12,47 +12,18 @@ extension MediaGridContainerView {
|
||||||
public class ViewModel {
|
public class ViewModel {
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
|
||||||
@Published public var isSensitiveToggleButtonDisplay: Bool = false
|
@Published public var isSensitiveToggleButtonDisplay: Bool = false
|
||||||
@Published public var isContentWarningOverlayDisplay: Bool? = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MediaGridContainerView.ViewModel {
|
extension MediaGridContainerView.ViewModel {
|
||||||
|
|
||||||
func resetContentWarningOverlay() {
|
|
||||||
isContentWarningOverlayDisplay = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func bind(view: MediaGridContainerView) {
|
func bind(view: MediaGridContainerView) {
|
||||||
$isSensitiveToggleButtonDisplay
|
$isSensitiveToggleButtonDisplay
|
||||||
.sink { isDisplay in
|
.sink { isDisplay in
|
||||||
view.sensitiveToggleButtonBlurVisualEffectView.isHidden = !isDisplay
|
view.sensitiveToggleButtonBlurVisualEffectView.isHidden = !isDisplay
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
$isContentWarningOverlayDisplay
|
|
||||||
.sink { isDisplay in
|
|
||||||
assert(Thread.isMainThread)
|
|
||||||
guard let isDisplay = isDisplay else { return }
|
|
||||||
let withAnimation = self.isContentWarningOverlayDisplay != nil
|
|
||||||
view.configureOverlayDisplay(isDisplay: isDisplay, animated: withAnimation)
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MediaGridContainerView {
|
|
||||||
func configureOverlayDisplay(isDisplay: Bool, animated: Bool) {
|
|
||||||
if animated {
|
|
||||||
UIView.animate(withDuration: 0.33, delay: 0, options: .curveEaseInOut) {
|
|
||||||
self.contentWarningOverlayView.blurVisualEffectView.alpha = isDisplay ? 1 : 0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentWarningOverlayView.blurVisualEffectView.alpha = isDisplay ? 1 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
contentWarningOverlayView.isUserInteractionEnabled = isDisplay
|
|
||||||
contentWarningOverlayView.tapGestureRecognizer.isEnabled = isDisplay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,11 +12,12 @@ import func AVFoundation.AVMakeRect
|
||||||
|
|
||||||
public protocol MediaGridContainerViewDelegate: AnyObject {
|
public protocol MediaGridContainerViewDelegate: AnyObject {
|
||||||
func mediaGridContainerView(_ container: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int)
|
func mediaGridContainerView(_ container: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int)
|
||||||
func mediaGridContainerView(_ container: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView)
|
func mediaGridContainerView(_ container: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class MediaGridContainerView: UIView {
|
public final class MediaGridContainerView: UIView {
|
||||||
|
|
||||||
|
static let sensitiveToggleButtonSize = CGSize(width: 34, height: 34)
|
||||||
public static let maxCount = 9
|
public static let maxCount = 9
|
||||||
|
|
||||||
let logger = Logger(subsystem: "MediaGridContainerView", category: "UI")
|
let logger = Logger(subsystem: "MediaGridContainerView", category: "UI")
|
||||||
|
@ -51,25 +52,19 @@ public final class MediaGridContainerView: UIView {
|
||||||
let sensitiveToggleButtonBlurVisualEffectView: UIVisualEffectView = {
|
let sensitiveToggleButtonBlurVisualEffectView: UIVisualEffectView = {
|
||||||
let visualEffectView = UIVisualEffectView(effect: ContentWarningOverlayView.blurVisualEffect)
|
let visualEffectView = UIVisualEffectView(effect: ContentWarningOverlayView.blurVisualEffect)
|
||||||
visualEffectView.layer.masksToBounds = true
|
visualEffectView.layer.masksToBounds = true
|
||||||
visualEffectView.layer.cornerRadius = 6
|
visualEffectView.layer.cornerRadius = MediaGridContainerView.sensitiveToggleButtonSize.width / 2
|
||||||
visualEffectView.layer.cornerCurve = .continuous
|
visualEffectView.layer.cornerCurve = .continuous
|
||||||
return visualEffectView
|
return visualEffectView
|
||||||
}()
|
}()
|
||||||
let sensitiveToggleButtonVibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: ContentWarningOverlayView.blurVisualEffect))
|
let sensitiveToggleButtonVibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: ContentWarningOverlayView.blurVisualEffect))
|
||||||
let sensitiveToggleButton: HitTestExpandedButton = {
|
let sensitiveToggleButton: HitTestExpandedButton = {
|
||||||
let button = HitTestExpandedButton(type: .system)
|
let button = HitTestExpandedButton(type: .system)
|
||||||
|
button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
|
||||||
|
button.imageView?.contentMode = .scaleAspectFit
|
||||||
button.setImage(UIImage(systemName: "eye.slash.fill"), for: .normal)
|
button.setImage(UIImage(systemName: "eye.slash.fill"), for: .normal)
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
public let contentWarningOverlayView: ContentWarningOverlayView = {
|
|
||||||
let overlay = ContentWarningOverlayView()
|
|
||||||
overlay.layer.masksToBounds = true
|
|
||||||
overlay.layer.cornerRadius = MediaView.cornerRadius
|
|
||||||
overlay.layer.cornerCurve = .continuous
|
|
||||||
return overlay
|
|
||||||
}()
|
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
_init()
|
_init()
|
||||||
|
@ -85,7 +80,6 @@ public final class MediaGridContainerView: UIView {
|
||||||
extension MediaGridContainerView {
|
extension MediaGridContainerView {
|
||||||
private func _init() {
|
private func _init() {
|
||||||
sensitiveToggleButton.addTarget(self, action: #selector(MediaGridContainerView.sensitiveToggleButtonDidPressed(_:)), for: .touchUpInside)
|
sensitiveToggleButton.addTarget(self, action: #selector(MediaGridContainerView.sensitiveToggleButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
contentWarningOverlayView.delegate = self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +93,7 @@ extension MediaGridContainerView {
|
||||||
|
|
||||||
@objc private func sensitiveToggleButtonDidPressed(_ sender: UIButton) {
|
@objc private func sensitiveToggleButtonDidPressed(_ sender: UIButton) {
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
|
||||||
delegate?.mediaGridContainerView(self, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView)
|
delegate?.mediaGridContainerView(self, mediaSensitiveButtonDidPressed: sender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,9 +107,6 @@ extension MediaGridContainerView {
|
||||||
|
|
||||||
layoutSensitiveToggleButton()
|
layoutSensitiveToggleButton()
|
||||||
bringSubviewToFront(sensitiveToggleButtonBlurVisualEffectView)
|
bringSubviewToFront(sensitiveToggleButtonBlurVisualEffectView)
|
||||||
|
|
||||||
layoutContentOverlayView(on: mediaView)
|
|
||||||
bringSubviewToFront(contentWarningOverlayView)
|
|
||||||
|
|
||||||
return mediaView
|
return mediaView
|
||||||
}
|
}
|
||||||
|
@ -128,9 +119,6 @@ extension MediaGridContainerView {
|
||||||
|
|
||||||
layoutSensitiveToggleButton()
|
layoutSensitiveToggleButton()
|
||||||
bringSubviewToFront(sensitiveToggleButtonBlurVisualEffectView)
|
bringSubviewToFront(sensitiveToggleButtonBlurVisualEffectView)
|
||||||
|
|
||||||
layoutContentOverlayView(on: self)
|
|
||||||
bringSubviewToFront(contentWarningOverlayView)
|
|
||||||
|
|
||||||
return mediaViews
|
return mediaViews
|
||||||
}
|
}
|
||||||
|
@ -156,8 +144,8 @@ extension MediaGridContainerView {
|
||||||
sensitiveToggleButtonBlurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
sensitiveToggleButtonBlurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(sensitiveToggleButtonBlurVisualEffectView)
|
addSubview(sensitiveToggleButtonBlurVisualEffectView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
sensitiveToggleButtonBlurVisualEffectView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
sensitiveToggleButtonBlurVisualEffectView.topAnchor.constraint(equalTo: topAnchor, constant: 16),
|
||||||
sensitiveToggleButtonBlurVisualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
trailingAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.trailingAnchor, constant: 16),
|
||||||
])
|
])
|
||||||
|
|
||||||
sensitiveToggleButtonVibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
sensitiveToggleButtonVibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -172,21 +160,12 @@ extension MediaGridContainerView {
|
||||||
sensitiveToggleButton.translatesAutoresizingMaskIntoConstraints = false
|
sensitiveToggleButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
sensitiveToggleButtonVibrancyVisualEffectView.contentView.addSubview(sensitiveToggleButton)
|
sensitiveToggleButtonVibrancyVisualEffectView.contentView.addSubview(sensitiveToggleButton)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
sensitiveToggleButton.topAnchor.constraint(equalTo: sensitiveToggleButtonVibrancyVisualEffectView.contentView.topAnchor, constant: 4),
|
sensitiveToggleButton.topAnchor.constraint(equalTo: sensitiveToggleButtonVibrancyVisualEffectView.contentView.topAnchor),
|
||||||
sensitiveToggleButton.leadingAnchor.constraint(equalTo: sensitiveToggleButtonVibrancyVisualEffectView.contentView.leadingAnchor, constant: 4),
|
sensitiveToggleButton.leadingAnchor.constraint(equalTo: sensitiveToggleButtonVibrancyVisualEffectView.contentView.leadingAnchor),
|
||||||
sensitiveToggleButtonVibrancyVisualEffectView.contentView.trailingAnchor.constraint(equalTo: sensitiveToggleButton.trailingAnchor, constant: 4),
|
sensitiveToggleButtonVibrancyVisualEffectView.contentView.trailingAnchor.constraint(equalTo: sensitiveToggleButton.trailingAnchor),
|
||||||
sensitiveToggleButtonVibrancyVisualEffectView.contentView.bottomAnchor.constraint(equalTo: sensitiveToggleButton.bottomAnchor, constant: 4),
|
sensitiveToggleButtonVibrancyVisualEffectView.contentView.bottomAnchor.constraint(equalTo: sensitiveToggleButton.bottomAnchor),
|
||||||
])
|
sensitiveToggleButton.widthAnchor.constraint(equalToConstant: MediaGridContainerView.sensitiveToggleButtonSize.width).priority(.required - 1),
|
||||||
}
|
sensitiveToggleButton.heightAnchor.constraint(equalToConstant: MediaGridContainerView.sensitiveToggleButtonSize.height).priority(.required - 1),
|
||||||
|
|
||||||
private func layoutContentOverlayView(on view: UIView) {
|
|
||||||
contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(contentWarningOverlayView) // should add to container
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
contentWarningOverlayView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
||||||
contentWarningOverlayView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
||||||
contentWarningOverlayView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
||||||
contentWarningOverlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,10 +307,3 @@ extension MediaGridContainerView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - ContentWarningOverlayViewDelegate
|
|
||||||
extension MediaGridContainerView: ContentWarningOverlayViewDelegate {
|
|
||||||
public func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) {
|
|
||||||
delegate?.mediaGridContainerView(self, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,10 +14,13 @@ import Photos
|
||||||
extension MediaView {
|
extension MediaView {
|
||||||
public class Configuration: Hashable {
|
public class Configuration: Hashable {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
public let info: Info
|
public let info: Info
|
||||||
public let blurhash: String?
|
public let blurhash: String?
|
||||||
|
|
||||||
@Published public var isReveal = true
|
@Published public var isReveal = true
|
||||||
|
@Published public var previewImage: UIImage?
|
||||||
@Published public var blurhashImage: UIImage?
|
@Published public var blurhashImage: UIImage?
|
||||||
public var blurhashImageDisposeBag = Set<AnyCancellable>()
|
public var blurhashImageDisposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@ -37,6 +40,17 @@ extension MediaView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var previewURL: String? {
|
||||||
|
switch info {
|
||||||
|
case .image(let info):
|
||||||
|
return info.assetURL
|
||||||
|
case .gif(let info):
|
||||||
|
return info.previewURL
|
||||||
|
case .video(let info):
|
||||||
|
return info.previewURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public var assetURL: String? {
|
public var assetURL: String? {
|
||||||
switch info {
|
switch info {
|
||||||
case .image(let info):
|
case .image(let info):
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import AVKit
|
import AVKit
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
import AlamofireImage
|
||||||
|
|
||||||
public final class MediaView: UIView {
|
public final class MediaView: UIView {
|
||||||
|
|
||||||
|
@ -46,9 +47,17 @@ public final class MediaView: UIView {
|
||||||
let playerViewController = AVPlayerViewController()
|
let playerViewController = AVPlayerViewController()
|
||||||
playerViewController.view.layer.masksToBounds = true
|
playerViewController.view.layer.masksToBounds = true
|
||||||
playerViewController.view.isUserInteractionEnabled = false
|
playerViewController.view.isUserInteractionEnabled = false
|
||||||
|
playerViewController.videoGravity = .resizeAspectFill
|
||||||
|
playerViewController.updatesNowPlayingInfoCenter = false
|
||||||
return playerViewController
|
return playerViewController
|
||||||
}()
|
}()
|
||||||
private var playerLooper: AVPlayerLooper?
|
private var playerLooper: AVPlayerLooper?
|
||||||
|
private(set) lazy var playbackImageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.image = UIImage(systemName: "play.circle.fill")
|
||||||
|
imageView.tintColor = .white
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
private(set) lazy var indicatorBlurEffectView: UIVisualEffectView = {
|
private(set) lazy var indicatorBlurEffectView: UIVisualEffectView = {
|
||||||
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial))
|
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial))
|
||||||
|
@ -60,12 +69,12 @@ public final class MediaView: UIView {
|
||||||
private(set) lazy var indicatorVibrancyEffectView = UIVisualEffectView(
|
private(set) lazy var indicatorVibrancyEffectView = UIVisualEffectView(
|
||||||
effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .systemUltraThinMaterial))
|
effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .systemUltraThinMaterial))
|
||||||
)
|
)
|
||||||
// private(set) lazy var playerIndicatorLabel: UILabel = {
|
private(set) lazy var playerIndicatorLabel: UILabel = {
|
||||||
// let label = UILabel()
|
let label = UILabel()
|
||||||
// label.font = .preferredFont(forTextStyle: .caption1)
|
label.font = .preferredFont(forTextStyle: .caption1)
|
||||||
// label.textColor = .secondaryLabel
|
label.textColor = .secondaryLabel
|
||||||
// return label
|
return label
|
||||||
// }()
|
}()
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
@ -83,11 +92,11 @@ extension MediaView {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public func thumbnail() async -> UIImage? {
|
public func thumbnail() async -> UIImage? {
|
||||||
return imageView.image
|
return imageView.image ?? configuration?.previewImage
|
||||||
}
|
}
|
||||||
|
|
||||||
public func thumbnail() -> UIImage? {
|
public func thumbnail() -> UIImage? {
|
||||||
return imageView.image
|
return imageView.image ?? configuration?.previewImage
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -104,40 +113,21 @@ extension MediaView {
|
||||||
|
|
||||||
switch configuration.info {
|
switch configuration.info {
|
||||||
case .image(let info):
|
case .image(let info):
|
||||||
configure(image: info)
|
layoutImage()
|
||||||
|
bindImage(configuration: configuration, info: info)
|
||||||
case .gif(let info):
|
case .gif(let info):
|
||||||
configure(gif: info)
|
layoutGIF()
|
||||||
|
bindGIF(configuration: configuration, info: info)
|
||||||
case .video(let info):
|
case .video(let info):
|
||||||
configure(video: info)
|
layoutVideo()
|
||||||
|
bindVideo(configuration: configuration, info: info)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let blurhash = configuration.blurhash {
|
layoutBlurhash()
|
||||||
configure(blurhash: blurhash)
|
bindBlurhash(configuration: configuration)
|
||||||
|
|
||||||
configuration.$blurhashImage
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.assign(to: \.image, on: blurhashImageView)
|
|
||||||
.store(in: &_disposeBag)
|
|
||||||
|
|
||||||
blurhashImageView.alpha = configuration.isReveal ? 0 : 1
|
|
||||||
}
|
|
||||||
|
|
||||||
configuration.$isReveal
|
|
||||||
.dropFirst()
|
|
||||||
.removeDuplicates()
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] isReveal in
|
|
||||||
guard let self = self else { return }
|
|
||||||
let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut)
|
|
||||||
animator.addAnimations {
|
|
||||||
self.blurhashImageView.alpha = isReveal ? 0 : 1
|
|
||||||
}
|
|
||||||
animator.startAnimation()
|
|
||||||
}
|
|
||||||
.store(in: &_disposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configure(image info: Configuration.ImageInfo) {
|
private func layoutImage() {
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
container.addSubview(imageView)
|
container.addSubview(imageView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -146,20 +136,24 @@ extension MediaView {
|
||||||
imageView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
imageView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||||
imageView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
imageView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
}
|
||||||
let placeholder = UIImage.placeholder(color: .systemGray6)
|
|
||||||
guard let urlString = info.assetURL,
|
private func bindImage(configuration: Configuration, info: Configuration.ImageInfo) {
|
||||||
let url = URL(string: urlString) else {
|
Publishers.CombineLatest3(
|
||||||
imageView.image = placeholder
|
configuration.$isReveal,
|
||||||
return
|
configuration.$previewImage,
|
||||||
}
|
configuration.$blurhashImage
|
||||||
imageView.af.setImage(
|
|
||||||
withURL: url,
|
|
||||||
placeholderImage: placeholder
|
|
||||||
)
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] isReveal, previewImage, blurhashImage in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let image = isReveal ? previewImage : blurhashImage
|
||||||
|
self.imageView.image = image
|
||||||
|
}
|
||||||
|
.store(in: &configuration.disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configure(gif info: Configuration.VideoInfo) {
|
private func layoutGIF() {
|
||||||
// use view controller as View here
|
// use view controller as View here
|
||||||
playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
container.addSubview(playerViewController.view)
|
container.addSubview(playerViewController.view)
|
||||||
|
@ -170,18 +164,11 @@ extension MediaView {
|
||||||
playerViewController.view.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
playerViewController.view.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
assert(playerViewController.contentOverlayView != nil)
|
setupIndicatorViewHierarchy()
|
||||||
if let contentOverlayView = playerViewController.contentOverlayView {
|
playerIndicatorLabel.attributedText = NSAttributedString(string: "GIF")
|
||||||
indicatorBlurEffectView.translatesAutoresizingMaskIntoConstraints = false
|
}
|
||||||
contentOverlayView.addSubview(indicatorBlurEffectView)
|
|
||||||
NSLayoutConstraint.activate([
|
private func bindGIF(configuration: Configuration, info: Configuration.VideoInfo) {
|
||||||
contentOverlayView.trailingAnchor.constraint(equalTo: indicatorBlurEffectView.trailingAnchor, constant: 11),
|
|
||||||
contentOverlayView.bottomAnchor.constraint(equalTo: indicatorBlurEffectView.bottomAnchor, constant: 8),
|
|
||||||
])
|
|
||||||
setupIndicatorViewHierarchy()
|
|
||||||
}
|
|
||||||
// playerIndicatorLabel.attributedText = NSAttributedString(AttributedString("GIF"))
|
|
||||||
|
|
||||||
guard let player = setupGIFPlayer(info: info) else { return }
|
guard let player = setupGIFPlayer(info: info) else { return }
|
||||||
setupPlayerLooper(player: player)
|
setupPlayerLooper(player: player)
|
||||||
playerViewController.player = player
|
playerViewController.player = player
|
||||||
|
@ -191,20 +178,33 @@ extension MediaView {
|
||||||
player.play()
|
player.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configure(video info: Configuration.VideoInfo) {
|
private func layoutVideo() {
|
||||||
|
layoutImage()
|
||||||
|
|
||||||
|
playbackImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.addSubview(playbackImageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
playbackImageView.centerXAnchor.constraint(equalTo: container.centerXAnchor),
|
||||||
|
playbackImageView.centerYAnchor.constraint(equalTo: container.centerYAnchor),
|
||||||
|
playbackImageView.widthAnchor.constraint(equalToConstant: 88).priority(.required - 1),
|
||||||
|
playbackImageView.heightAnchor.constraint(equalToConstant: 88).priority(.required - 1),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindVideo(configuration: Configuration, info: Configuration.VideoInfo) {
|
||||||
let imageInfo = Configuration.ImageInfo(
|
let imageInfo = Configuration.ImageInfo(
|
||||||
aspectRadio: info.aspectRadio,
|
aspectRadio: info.aspectRadio,
|
||||||
assetURL: info.previewURL
|
assetURL: info.previewURL
|
||||||
)
|
)
|
||||||
configure(image: imageInfo)
|
bindImage(configuration: configuration, info: imageInfo)
|
||||||
|
|
||||||
indicatorBlurEffectView.translatesAutoresizingMaskIntoConstraints = false
|
// indicatorBlurEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
imageView.addSubview(indicatorBlurEffectView)
|
// imageView.addSubview(indicatorBlurEffectView)
|
||||||
NSLayoutConstraint.activate([
|
// NSLayoutConstraint.activate([
|
||||||
imageView.trailingAnchor.constraint(equalTo: indicatorBlurEffectView.trailingAnchor, constant: 11),
|
// imageView.trailingAnchor.constraint(equalTo: indicatorBlurEffectView.trailingAnchor, constant: 11),
|
||||||
imageView.bottomAnchor.constraint(equalTo: indicatorBlurEffectView.bottomAnchor, constant: 8),
|
// imageView.bottomAnchor.constraint(equalTo: indicatorBlurEffectView.bottomAnchor, constant: 8),
|
||||||
])
|
// ])
|
||||||
setupIndicatorViewHierarchy()
|
// setupIndicatorViewHierarchy()
|
||||||
|
|
||||||
// playerIndicatorLabel.attributedText = {
|
// playerIndicatorLabel.attributedText = {
|
||||||
// let imageAttachment = NSTextAttachment(image: UIImage(systemName: "play.fill")!)
|
// let imageAttachment = NSTextAttachment(image: UIImage(systemName: "play.fill")!)
|
||||||
|
@ -221,10 +221,9 @@ extension MediaView {
|
||||||
// attributedString.foregroundColor = .secondaryLabel
|
// attributedString.foregroundColor = .secondaryLabel
|
||||||
// return NSAttributedString(attributedString)
|
// return NSAttributedString(attributedString)
|
||||||
// }()
|
// }()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configure(blurhash: String) {
|
private func layoutBlurhash() {
|
||||||
blurhashImageView.translatesAutoresizingMaskIntoConstraints = false
|
blurhashImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
container.addSubview(blurhashImageView)
|
container.addSubview(blurhashImageView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -233,8 +232,28 @@ extension MediaView {
|
||||||
blurhashImageView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
blurhashImageView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||||
blurhashImageView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
blurhashImageView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindBlurhash(configuration: Configuration) {
|
||||||
|
configuration.$blurhashImage
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.assign(to: \.image, on: blurhashImageView)
|
||||||
|
.store(in: &_disposeBag)
|
||||||
|
blurhashImageView.alpha = configuration.isReveal ? 0 : 1
|
||||||
|
|
||||||
blurhashImageView.backgroundColor = .systemGray
|
configuration.$isReveal
|
||||||
|
.dropFirst()
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] isReveal in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut)
|
||||||
|
animator.addAnimations {
|
||||||
|
self.blurhashImageView.alpha = isReveal ? 0 : 1
|
||||||
|
}
|
||||||
|
animator.startAnimation()
|
||||||
|
}
|
||||||
|
.store(in: &_disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func prepareForReuse() {
|
public func prepareForReuse() {
|
||||||
|
@ -305,29 +324,39 @@ extension MediaView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupIndicatorViewHierarchy() {
|
private func setupIndicatorViewHierarchy() {
|
||||||
// let blurEffectView = indicatorBlurEffectView
|
let blurEffectView = indicatorBlurEffectView
|
||||||
// let vibrancyEffectView = indicatorVibrancyEffectView
|
let vibrancyEffectView = indicatorVibrancyEffectView
|
||||||
//
|
|
||||||
// if vibrancyEffectView.superview == nil {
|
assert(playerViewController.contentOverlayView != nil)
|
||||||
// vibrancyEffectView.translatesAutoresizingMaskIntoConstraints = false
|
if let contentOverlayView = playerViewController.contentOverlayView {
|
||||||
// blurEffectView.contentView.addSubview(vibrancyEffectView)
|
blurEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
// NSLayoutConstraint.activate([
|
contentOverlayView.addSubview(indicatorBlurEffectView)
|
||||||
// vibrancyEffectView.topAnchor.constraint(equalTo: blurEffectView.contentView.topAnchor),
|
NSLayoutConstraint.activate([
|
||||||
// vibrancyEffectView.leadingAnchor.constraint(equalTo: blurEffectView.contentView.leadingAnchor),
|
contentOverlayView.trailingAnchor.constraint(equalTo: blurEffectView.trailingAnchor, constant: 16),
|
||||||
// vibrancyEffectView.trailingAnchor.constraint(equalTo: blurEffectView.contentView.trailingAnchor),
|
contentOverlayView.bottomAnchor.constraint(equalTo: blurEffectView.bottomAnchor, constant: 8),
|
||||||
// vibrancyEffectView.bottomAnchor.constraint(equalTo: blurEffectView.contentView.bottomAnchor),
|
])
|
||||||
// ])
|
}
|
||||||
// }
|
|
||||||
//
|
if vibrancyEffectView.superview == nil {
|
||||||
// if playerIndicatorLabel.superview == nil {
|
vibrancyEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
// playerIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false
|
blurEffectView.contentView.addSubview(vibrancyEffectView)
|
||||||
// vibrancyEffectView.contentView.addSubview(playerIndicatorLabel)
|
NSLayoutConstraint.activate([
|
||||||
// NSLayoutConstraint.activate([
|
vibrancyEffectView.topAnchor.constraint(equalTo: blurEffectView.contentView.topAnchor),
|
||||||
// playerIndicatorLabel.topAnchor.constraint(equalTo: vibrancyEffectView.contentView.topAnchor),
|
vibrancyEffectView.leadingAnchor.constraint(equalTo: blurEffectView.contentView.leadingAnchor),
|
||||||
// playerIndicatorLabel.leadingAnchor.constraint(equalTo: vibrancyEffectView.contentView.leadingAnchor, constant: 3),
|
vibrancyEffectView.trailingAnchor.constraint(equalTo: blurEffectView.contentView.trailingAnchor),
|
||||||
// vibrancyEffectView.contentView.trailingAnchor.constraint(equalTo: playerIndicatorLabel.trailingAnchor, constant: 3),
|
vibrancyEffectView.bottomAnchor.constraint(equalTo: blurEffectView.contentView.bottomAnchor),
|
||||||
// playerIndicatorLabel.bottomAnchor.constraint(equalTo: vibrancyEffectView.contentView.bottomAnchor),
|
])
|
||||||
// ])
|
}
|
||||||
// }
|
|
||||||
|
if playerIndicatorLabel.superview == nil {
|
||||||
|
playerIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
vibrancyEffectView.contentView.addSubview(playerIndicatorLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
playerIndicatorLabel.topAnchor.constraint(equalTo: vibrancyEffectView.contentView.topAnchor),
|
||||||
|
playerIndicatorLabel.leadingAnchor.constraint(equalTo: vibrancyEffectView.contentView.leadingAnchor, constant: 3),
|
||||||
|
vibrancyEffectView.contentView.trailingAnchor.constraint(equalTo: playerIndicatorLabel.trailingAnchor, constant: 3),
|
||||||
|
playerIndicatorLabel.bottomAnchor.constraint(equalTo: vibrancyEffectView.contentView.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -388,6 +388,10 @@ extension NotificationView: StatusViewDelegate {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) {
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - MastodonMenuDelegate
|
// MARK: - MastodonMenuDelegate
|
||||||
|
|
|
@ -53,6 +53,9 @@ extension StatusView {
|
||||||
// Media
|
// Media
|
||||||
@Published public var mediaViewConfigurations: [MediaView.Configuration] = []
|
@Published public var mediaViewConfigurations: [MediaView.Configuration] = []
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
@Published public var audioConfigurations: [MediaView.Configuration] = []
|
||||||
|
|
||||||
// Poll
|
// Poll
|
||||||
@Published public var pollItems: [PollItem] = []
|
@Published public var pollItems: [PollItem] = []
|
||||||
@Published public var isVotable: Bool = false
|
@Published public var isVotable: Bool = false
|
||||||
|
@ -121,9 +124,9 @@ extension StatusView {
|
||||||
isMediaSensitive = false
|
isMediaSensitive = false
|
||||||
isMediaSensitiveToggled = false
|
isMediaSensitiveToggled = false
|
||||||
|
|
||||||
isSensitive = false
|
// isSensitive = false
|
||||||
isContentReveal = false
|
// isContentReveal = false
|
||||||
isMediaReveal = false
|
// isMediaReveal = false
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -154,7 +157,8 @@ extension StatusView {
|
||||||
$isMediaSensitive,
|
$isMediaSensitive,
|
||||||
$isMediaSensitiveToggled
|
$isMediaSensitiveToggled
|
||||||
)
|
)
|
||||||
.map { $0 ? $1 : true }
|
.map { $1 ? !$0 : $0 }
|
||||||
|
.map { !$0 }
|
||||||
.assign(to: &$isMediaReveal)
|
.assign(to: &$isMediaReveal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -375,6 +379,8 @@ extension StatusView.ViewModel {
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): configure media")
|
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): configure media")
|
||||||
|
|
||||||
|
statusView.mediaGridContainerView.prepareForReuse()
|
||||||
|
|
||||||
let maxSize = CGSize(
|
let maxSize = CGSize(
|
||||||
width: statusView.contentMaxLayoutWidth,
|
width: statusView.contentMaxLayoutWidth,
|
||||||
height: 9999 // fulfill the width
|
height: 9999 // fulfill the width
|
||||||
|
@ -419,18 +425,11 @@ extension StatusView.ViewModel {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// FIXME:
|
$isMediaReveal
|
||||||
statusView.mediaGridContainerView.viewModel.isContentWarningOverlayDisplay = false
|
.sink { isMediaReveal in
|
||||||
// $isMediaReveal
|
statusView.mediaGridContainerView.viewModel.isSensitiveToggleButtonDisplay = isMediaReveal
|
||||||
// .sink { isMediaReveal in
|
}
|
||||||
// statusView.mediaGridContainerView.viewModel.isContentWarningOverlayDisplay = isMediaReveal
|
.store(in: &disposeBag)
|
||||||
// }
|
|
||||||
// .store(in: &disposeBag)
|
|
||||||
// $isMediaSensitiveSwitchable
|
|
||||||
// .sink { isMediaSensitiveSwitchable in
|
|
||||||
// statusView.mediaGridContainerView.viewModel.isSensitiveToggleButtonDisplay = isMediaSensitiveSwitchable
|
|
||||||
// }
|
|
||||||
// .store(in: &disposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func bindPoll(statusView: StatusView) {
|
private func bindPoll(statusView: StatusView) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ public protocol StatusViewDelegate: AnyObject {
|
||||||
func statusView(_ statusView: StatusView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action)
|
func statusView(_ statusView: StatusView, menuButton button: UIButton, didSelectAction action: MastodonMenu.Action)
|
||||||
func statusView(_ statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
func statusView(_ statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
|
||||||
func statusView(_ statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
func statusView(_ statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
|
||||||
|
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
|
||||||
// func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
// func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
// func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
// func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||||
}
|
}
|
||||||
|
@ -212,7 +213,6 @@ public final class StatusView: UIView {
|
||||||
viewModel.prepareForReuse()
|
viewModel.prepareForReuse()
|
||||||
|
|
||||||
avatarButton.avatarImageView.cancelTask()
|
avatarButton.avatarImageView.cancelTask()
|
||||||
mediaGridContainerView.prepareForReuse()
|
|
||||||
if var snapshot = pollTableViewDiffableDataSource?.snapshot() {
|
if var snapshot = pollTableViewDiffableDataSource?.snapshot() {
|
||||||
snapshot.deleteAllItems()
|
snapshot.deleteAllItems()
|
||||||
if #available(iOS 15.0, *) {
|
if #available(iOS 15.0, *) {
|
||||||
|
@ -407,7 +407,7 @@ extension StatusView.Style {
|
||||||
statusView.authorContainerView.alignment = category > .accessibilityLarge ? .leading : .center
|
statusView.authorContainerView.alignment = category > .accessibilityLarge ? .leading : .center
|
||||||
}
|
}
|
||||||
.store(in: &statusView._disposeBag)
|
.store(in: &statusView._disposeBag)
|
||||||
|
|
||||||
// avatarButton
|
// avatarButton
|
||||||
let authorAvatarButtonSize = CGSize(width: 46, height: 46)
|
let authorAvatarButtonSize = CGSize(width: 46, height: 46)
|
||||||
statusView.avatarButton.size = authorAvatarButtonSize
|
statusView.avatarButton.size = authorAvatarButtonSize
|
||||||
|
@ -420,25 +420,29 @@ extension StatusView.Style {
|
||||||
])
|
])
|
||||||
statusView.avatarButton.setContentHuggingPriority(.required - 1, for: .vertical)
|
statusView.avatarButton.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||||
statusView.avatarButton.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
statusView.avatarButton.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||||
|
|
||||||
// authrMetaContainer: V - [ authorPrimaryMetaContainer | authorSecondaryMetaContainer ]
|
// authrMetaContainer: V - [ authorPrimaryMetaContainer | authorSecondaryMetaContainer ]
|
||||||
let authorMetaContainer = UIStackView()
|
let authorMetaContainer = UIStackView()
|
||||||
authorMetaContainer.axis = .vertical
|
authorMetaContainer.axis = .vertical
|
||||||
authorMetaContainer.spacing = 4
|
authorMetaContainer.spacing = 4
|
||||||
statusView.authorContainerView.addArrangedSubview(authorMetaContainer)
|
statusView.authorContainerView.addArrangedSubview(authorMetaContainer)
|
||||||
|
|
||||||
// authorPrimaryMetaContainer: H - [ authorNameLabel | (padding) | menuButton ]
|
// authorPrimaryMetaContainer: H - [ authorNameLabel | (padding) | menuButton ]
|
||||||
let authorPrimaryMetaContainer = UIStackView()
|
let authorPrimaryMetaContainer = UIStackView()
|
||||||
authorPrimaryMetaContainer.axis = .horizontal
|
authorPrimaryMetaContainer.axis = .horizontal
|
||||||
|
authorPrimaryMetaContainer.spacing = 10
|
||||||
authorMetaContainer.addArrangedSubview(authorPrimaryMetaContainer)
|
authorMetaContainer.addArrangedSubview(authorPrimaryMetaContainer)
|
||||||
|
|
||||||
// authorNameLabel
|
// authorNameLabel
|
||||||
authorPrimaryMetaContainer.addArrangedSubview(statusView.authorNameLabel)
|
authorPrimaryMetaContainer.addArrangedSubview(statusView.authorNameLabel)
|
||||||
|
statusView.authorNameLabel.setContentHuggingPriority(.required - 10, for: .horizontal)
|
||||||
|
statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal)
|
||||||
authorPrimaryMetaContainer.addArrangedSubview(UIView())
|
authorPrimaryMetaContainer.addArrangedSubview(UIView())
|
||||||
// menuButton
|
// menuButton
|
||||||
authorPrimaryMetaContainer.addArrangedSubview(statusView.menuButton)
|
authorPrimaryMetaContainer.addArrangedSubview(statusView.menuButton)
|
||||||
statusView.menuButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
statusView.menuButton.setContentHuggingPriority(.required - 2, for: .horizontal)
|
||||||
|
statusView.menuButton.setContentCompressionResistancePriority(.required - 2, for: .horizontal)
|
||||||
|
|
||||||
// authorSecondaryMetaContainer: H - [ authorUsername | usernameTrialingDotLabel | dateLabel | (padding) ]
|
// authorSecondaryMetaContainer: H - [ authorUsername | usernameTrialingDotLabel | dateLabel | (padding) ]
|
||||||
let authorSecondaryMetaContainer = UIStackView()
|
let authorSecondaryMetaContainer = UIStackView()
|
||||||
authorSecondaryMetaContainer.axis = .horizontal
|
authorSecondaryMetaContainer.axis = .horizontal
|
||||||
|
@ -455,22 +459,22 @@ extension StatusView.Style {
|
||||||
statusView.dateLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
|
statusView.dateLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
|
||||||
statusView.dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
statusView.dateLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||||
authorSecondaryMetaContainer.addArrangedSubview(UIView())
|
authorSecondaryMetaContainer.addArrangedSubview(UIView())
|
||||||
|
|
||||||
// content container: V - [ contentMetaText ]
|
// content container: V - [ contentMetaText ]
|
||||||
statusView.contentContainer.axis = .vertical
|
statusView.contentContainer.axis = .vertical
|
||||||
statusView.contentContainer.spacing = 12
|
statusView.contentContainer.spacing = 12
|
||||||
statusView.contentContainer.distribution = .fill
|
statusView.contentContainer.distribution = .fill
|
||||||
statusView.contentContainer.alignment = .top
|
statusView.contentContainer.alignment = .top
|
||||||
|
|
||||||
statusView.contentContainer.preservesSuperviewLayoutMargins = true
|
statusView.contentContainer.preservesSuperviewLayoutMargins = true
|
||||||
statusView.contentContainer.isLayoutMarginsRelativeArrangement = true
|
statusView.contentContainer.isLayoutMarginsRelativeArrangement = true
|
||||||
statusView.containerStackView.addArrangedSubview(statusView.contentContainer)
|
statusView.containerStackView.addArrangedSubview(statusView.contentContainer)
|
||||||
statusView.contentContainer.setContentHuggingPriority(.required - 1, for: .vertical)
|
statusView.contentContainer.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||||
statusView.contentContainer.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
statusView.contentContainer.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||||
|
|
||||||
// status content
|
// status content
|
||||||
statusView.contentContainer.addArrangedSubview(statusView.contentMetaText.textView)
|
statusView.contentContainer.addArrangedSubview(statusView.contentMetaText.textView)
|
||||||
|
|
||||||
statusView.spoilerOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
statusView.spoilerOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
statusView.containerStackView.addSubview(statusView.spoilerOverlayView)
|
statusView.containerStackView.addSubview(statusView.spoilerOverlayView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -479,10 +483,10 @@ extension StatusView.Style {
|
||||||
statusView.contentContainer.trailingAnchor.constraint(equalTo: statusView.spoilerOverlayView.trailingAnchor),
|
statusView.contentContainer.trailingAnchor.constraint(equalTo: statusView.spoilerOverlayView.trailingAnchor),
|
||||||
statusView.contentContainer.bottomAnchor.constraint(equalTo: statusView.spoilerOverlayView.bottomAnchor),
|
statusView.contentContainer.bottomAnchor.constraint(equalTo: statusView.spoilerOverlayView.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
// media container: V - [ mediaGridContainerView ]
|
// media container: V - [ mediaGridContainerView ]
|
||||||
statusView.containerStackView.addArrangedSubview(statusView.mediaContainerView)
|
statusView.containerStackView.addArrangedSubview(statusView.mediaContainerView)
|
||||||
|
|
||||||
statusView.mediaGridContainerView.translatesAutoresizingMaskIntoConstraints = false
|
statusView.mediaGridContainerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
statusView.mediaContainerView.addSubview(statusView.mediaGridContainerView)
|
statusView.mediaContainerView.addSubview(statusView.mediaGridContainerView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -491,20 +495,20 @@ extension StatusView.Style {
|
||||||
statusView.mediaGridContainerView.trailingAnchor.constraint(equalTo: statusView.mediaContainerView.trailingAnchor),
|
statusView.mediaGridContainerView.trailingAnchor.constraint(equalTo: statusView.mediaContainerView.trailingAnchor),
|
||||||
statusView.mediaGridContainerView.bottomAnchor.constraint(equalTo: statusView.mediaContainerView.bottomAnchor),
|
statusView.mediaGridContainerView.bottomAnchor.constraint(equalTo: statusView.mediaContainerView.bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
// pollContainerView: V - [ pollTableView | pollStatusStackView ]
|
// pollContainerView: V - [ pollTableView | pollStatusStackView ]
|
||||||
statusView.pollContainerView.axis = .vertical
|
statusView.pollContainerView.axis = .vertical
|
||||||
statusView.pollContainerView.preservesSuperviewLayoutMargins = true
|
statusView.pollContainerView.preservesSuperviewLayoutMargins = true
|
||||||
statusView.pollContainerView.isLayoutMarginsRelativeArrangement = true
|
statusView.pollContainerView.isLayoutMarginsRelativeArrangement = true
|
||||||
statusView.containerStackView.addArrangedSubview(statusView.pollContainerView)
|
statusView.containerStackView.addArrangedSubview(statusView.pollContainerView)
|
||||||
|
|
||||||
// pollTableView
|
// pollTableView
|
||||||
statusView.pollContainerView.addArrangedSubview(statusView.pollTableView)
|
statusView.pollContainerView.addArrangedSubview(statusView.pollTableView)
|
||||||
|
|
||||||
// pollStatusStackView
|
// pollStatusStackView
|
||||||
statusView.pollStatusStackView.axis = .horizontal
|
statusView.pollStatusStackView.axis = .horizontal
|
||||||
statusView.pollContainerView.addArrangedSubview(statusView.pollStatusStackView)
|
statusView.pollContainerView.addArrangedSubview(statusView.pollStatusStackView)
|
||||||
|
|
||||||
statusView.pollStatusStackView.addArrangedSubview(statusView.pollVoteCountLabel)
|
statusView.pollStatusStackView.addArrangedSubview(statusView.pollVoteCountLabel)
|
||||||
statusView.pollStatusStackView.addArrangedSubview(statusView.pollStatusDotLabel)
|
statusView.pollStatusStackView.addArrangedSubview(statusView.pollStatusDotLabel)
|
||||||
statusView.pollStatusStackView.addArrangedSubview(statusView.pollCountdownLabel)
|
statusView.pollStatusStackView.addArrangedSubview(statusView.pollCountdownLabel)
|
||||||
|
@ -514,14 +518,14 @@ extension StatusView.Style {
|
||||||
statusView.pollStatusDotLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
statusView.pollStatusDotLabel.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
|
||||||
statusView.pollCountdownLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
statusView.pollCountdownLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||||
statusView.pollVoteButton.setContentHuggingPriority(.defaultHigh + 3, for: .horizontal)
|
statusView.pollVoteButton.setContentHuggingPriority(.defaultHigh + 3, for: .horizontal)
|
||||||
|
|
||||||
// statusVisibilityView
|
// statusVisibilityView
|
||||||
statusView.statusVisibilityView.preservesSuperviewLayoutMargins = true
|
statusView.statusVisibilityView.preservesSuperviewLayoutMargins = true
|
||||||
statusView.containerStackView.addArrangedSubview(statusView.statusVisibilityView)
|
statusView.containerStackView.addArrangedSubview(statusView.statusVisibilityView)
|
||||||
|
|
||||||
statusView.spoilerBannerView.preservesSuperviewLayoutMargins = true
|
statusView.spoilerBannerView.preservesSuperviewLayoutMargins = true
|
||||||
statusView.containerStackView.addArrangedSubview(statusView.spoilerBannerView)
|
statusView.containerStackView.addArrangedSubview(statusView.spoilerBannerView)
|
||||||
|
|
||||||
// action toolbar
|
// action toolbar
|
||||||
statusView.actionToolbarContainer.configure(for: .inline)
|
statusView.actionToolbarContainer.configure(for: .inline)
|
||||||
statusView.actionToolbarContainer.preservesSuperviewLayoutMargins = true
|
statusView.actionToolbarContainer.preservesSuperviewLayoutMargins = true
|
||||||
|
@ -728,8 +732,8 @@ extension StatusView: MediaGridContainerViewDelegate {
|
||||||
delegate?.statusView(self, mediaGridContainerView: container, mediaView: mediaView, didSelectMediaViewAt: index)
|
delegate?.statusView(self, mediaGridContainerView: container, mediaView: mediaView, didSelectMediaViewAt: index)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func mediaGridContainerView(_ container: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) {
|
public func mediaGridContainerView(_ container: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) {
|
||||||
fatalError()
|
delegate?.statusView(self, mediaGridContainerView: container, mediaSensitiveButtonDidPressed: button)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue