Merge pull request #54 from tootsuite/feature/gifVideoSupport

chore: the play interrupts event could be sent with the notification
This commit is contained in:
sxiaojian88 2021-03-15 15:09:44 +08:00 committed by GitHub
commit a75f2e1889
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 482 additions and 243 deletions

View File

@ -25,7 +25,7 @@
2D206B7225F5D27F00143C56 /* AudioContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7125F5D27F00143C56 /* AudioContainerView.swift */; };
2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B7F25F5F45E00143C56 /* UIImage.swift */; };
2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; };
2D206B8C25F6015000143C56 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlayer.swift */; };
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */; };
2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; };
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; };
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; };
@ -60,6 +60,7 @@
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; };
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; };
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; };
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; };
2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; };
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */; };
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; };
@ -93,7 +94,7 @@
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; };
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; };
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; };
5DF1057925F88A1D00D6C0D4 /* MosaicPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */; };
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */; };
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.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 */; };
@ -269,7 +270,7 @@
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContainerView.swift; sourceTree = "<group>"; };
2D206B7F25F5F45E00143C56 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
2D206B8B25F6015000143C56 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.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>"; };
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
@ -301,6 +302,7 @@
2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = "<group>"; };
2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = "<group>"; };
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = "<group>"; };
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningOverlayView.swift; sourceTree = "<group>"; };
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Toot.swift"; sourceTree = "<group>"; };
2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = "<group>"; };
@ -337,7 +339,7 @@
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.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>"; };
5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MosaicPlayerView.swift; sourceTree = "<group>"; };
5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerContainerView.swift; sourceTree = "<group>"; };
5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBlockingView.swift; sourceTree = "<group>"; };
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; 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>"; };
@ -576,6 +578,7 @@
isa = PBXGroup;
children = (
2D152A8B25C295CC009AA50C /* StatusView.swift */,
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */,
);
path = Content;
sourceTree = "<group>";
@ -659,7 +662,7 @@
DB45FB0425CA87B4005A8AC7 /* APIService */,
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
2D206B8B25F6015000143C56 /* AudioPlayer.swift */,
2D206B8B25F6015000143C56 /* AudioPlaybackService.swift */,
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
);
@ -1178,7 +1181,7 @@
children = (
DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */,
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */,
5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */,
5DF1057825F88A1D00D6C0D4 /* PlayerContainerView.swift */,
5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */,
);
path = Container;
@ -1569,7 +1572,7 @@
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
2D206B8C25F6015000143C56 /* AudioPlayer.swift in Sources */,
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
@ -1632,6 +1635,7 @@
2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */,
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
@ -1707,7 +1711,7 @@
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
5DF1057925F88A1D00D6C0D4 /* MosaicPlayerView.swift in Sources */,
5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */,
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -37,7 +37,11 @@ extension StatusSection {
StatusSection.configure(
cell: cell,
dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID, statusItemAttribute: attribute
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
timestampUpdatePublisher: timestampUpdatePublisher,
toot: timelineIndex.toot,
requestUserID: timelineIndex.userID,
statusItemAttribute: attribute
)
}
cell.delegate = statusTableViewCellDelegate
@ -52,7 +56,11 @@ extension StatusSection {
StatusSection.configure(
cell: cell,
dependency: dependency,
readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID, statusItemAttribute: attribute
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
timestampUpdatePublisher: timestampUpdatePublisher,
toot: toot,
requestUserID: requestUserID,
statusItemAttribute: attribute
)
}
cell.delegate = statusTableViewCellDelegate
@ -162,13 +170,13 @@ extension StatusSection {
}
cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty
let isStatusSensitive = statusItemAttribute.isStatusSensitive
cell.statusView.statusMosaicImageViewContainer.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil
cell.statusView.statusMosaicImageViewContainer.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
// set audio
if let audioAttachment = mediaAttachments.filter({ $0.type == .audio }).first {
cell.statusView.audioView.isHidden = false
AudioContainerViewModel.configure(cell: cell, audioAttachment: audioAttachment, videoPlaybackService: dependency.context.videoPlaybackService)
AudioContainerViewModel.configure(cell: cell, audioAttachment: audioAttachment, audioService: dependency.context.audioPlaybackService)
} else {
cell.statusView.audioView.isHidden = true
}
@ -185,12 +193,16 @@ extension StatusSection {
return CGSize(width: maxWidth, height: maxWidth * scale)
}()
cell.statusView.playerContainerView.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil
cell.statusView.playerContainerView.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
cell.statusView.playerContainerView.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment)
{
let parent = cell.delegate?.parent()
let mosaicPlayerView = cell.statusView.mosaicPlayerView
let playerViewController = mosaicPlayerView.setupPlayer(
let playerContainerView = cell.statusView.playerContainerView
let playerViewController = playerContainerView.setupPlayer(
aspectRatio: videoPlayerViewModel.videoSize,
maxSize: playerViewMaxSize,
parent: parent
@ -198,13 +210,12 @@ extension StatusSection {
playerViewController.delegate = cell.delegate?.playerViewControllerDelegate
playerViewController.player = videoPlayerViewModel.player
playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif
mosaicPlayerView.gifIndicatorLabel.isHidden = videoPlayerViewModel.videoKind != .gif
mosaicPlayerView.isHidden = false
playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind)
playerContainerView.isHidden = false
} else {
cell.statusView.mosaicPlayerView.playerViewController.player?.pause()
cell.statusView.mosaicPlayerView.playerViewController.player = nil
cell.statusView.playerContainerView.playerViewController.player?.pause()
cell.statusView.playerContainerView.playerViewController.player = nil
}
// set poll
let poll = (toot.reblog ?? toot).poll

View File

@ -37,6 +37,7 @@ internal enum Asset {
internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled")
internal static let highlight = ColorAsset(name: "Colors/Background/Poll/highlight")
}
internal static let mediaTypeIndicotor = ColorAsset(name: "Colors/Background/mediaTypeIndicotor")
internal static let onboardingBackground = ColorAsset(name: "Colors/Background/onboarding.background")
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")

View File

@ -46,7 +46,16 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
}
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) {
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
statusTableViewCell(cell, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
}
func statusTableViewCell(_ cell: StatusTableViewCell, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
contentWarningOverlayView.isUserInteractionEnabled = false
statusTableViewCell(cell, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
}
func statusTableViewCell(_ cell: StatusTableViewCell, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
guard let diffableDataSource = self.tableViewDiffableDataSource else { return }
guard let item = item(for: cell, indexPath: nil) else { return }
@ -58,12 +67,12 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
default:
return
}
contentWarningOverlayView.isUserInteractionEnabled = false
var snapshot = diffableDataSource.snapshot()
snapshot.reloadItems([item])
UIView.animate(withDuration: 0.33) {
cell.statusView.statusMosaicImageViewContainer.blurVisualEffectView.effect = nil
cell.statusView.statusMosaicImageViewContainer.vibrancyVisualEffectView.alpha = 0.0
contentWarningOverlayView.blurVisualEffectView.effect = nil
contentWarningOverlayView.vibrancyVisualEffectView.alpha = 0.0
} completion: { _ in
diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil)
}

View File

@ -87,10 +87,14 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
.sink { [weak self] toot in
guard let self = self else { return }
guard let media = (toot?.mediaAttachments ?? Set()).first else { return }
guard let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) else { return }
DispatchQueue.main.async {
videoPlayerViewModel.didEndDisplaying()
if let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) {
DispatchQueue.main.async {
videoPlayerViewModel.didEndDisplaying()
}
}
if let currentAudioAttachment = self.context.audioPlaybackService.attachment, let _ = toot?.mediaAttachments?.contains(currentAudioAttachment) {
self.context.audioPlaybackService.pause()
}
}
.store(in: &disposeBag)

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.600",
"blue" : "0",
"green" : "0",
"red" : "0"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -141,7 +141,8 @@ extension HomeTimelineViewController {
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
context.videoPlaybackService.viewDidDisappear(from: self)
context.audioPlaybackService.viewDidDisappear(from: self)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
@ -236,7 +237,10 @@ extension HomeTimelineViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
handleTableView(tableView, willDisplay: cell, forRowAt: indexPath)
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
handleTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
}
}
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate

View File

@ -81,6 +81,11 @@ extension PublicTimelineViewController {
)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
context.videoPlaybackService.viewDidDisappear(from: self)
context.audioPlaybackService.viewDidDisappear(from: self)
}
}
// MARK: - UIScrollViewDelegate
@ -114,8 +119,11 @@ extension PublicTimelineViewController: UITableViewDelegate {
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
handleTableView(tableView, willDisplay: cell, forRowAt: indexPath)
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
handleTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }

View File

@ -15,15 +15,12 @@ protocol MosaicImageViewContainerPresentable: class {
protocol MosaicImageViewContainerDelegate: class {
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView)
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
}
final class MosaicImageViewContainer: UIView {
static let cornerRadius: CGFloat = 4
static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial)
weak var delegate: MosaicImageViewContainerDelegate?
let container = UIStackView()
@ -37,14 +34,10 @@ final class MosaicImageViewContainer: UIView {
}
}
}
let blurVisualEffectView = UIVisualEffectView(effect: MosaicImageViewContainer.blurVisualEffect)
let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: MosaicImageViewContainer.blurVisualEffect))
let contentWarningLabel: UILabel = {
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15))
label.text = L10n.Common.Controls.Status.mediaContentWarning
label.textAlignment = .center
return label
let contentWarningOverlayView: ContentWarningOverlayView = {
let contentWarningOverlayView = ContentWarningOverlayView()
return contentWarningOverlayView
}()
private var containerHeightLayoutConstraint: NSLayoutConstraint!
@ -61,9 +54,16 @@ final class MosaicImageViewContainer: UIView {
}
extension MosaicImageViewContainer: ContentWarningOverlayViewDelegate {
func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) {
self.delegate?.mosaicImageViewContainer(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
}
}
extension MosaicImageViewContainer {
private func _init() {
contentWarningOverlayView.delegate = self
container.translatesAutoresizingMaskIntoConstraints = false
container.axis = .horizontal
container.distribution = .fillEqually
@ -77,32 +77,13 @@ extension MosaicImageViewContainer {
containerHeightLayoutConstraint
])
// add blur visual effect view in the setup method
blurVisualEffectView.layer.masksToBounds = true
blurVisualEffectView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius
blurVisualEffectView.layer.cornerCurve = .continuous
vibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
blurVisualEffectView.contentView.addSubview(vibrancyVisualEffectView)
addSubview(contentWarningOverlayView)
NSLayoutConstraint.activate([
vibrancyVisualEffectView.topAnchor.constraint(equalTo: blurVisualEffectView.topAnchor),
vibrancyVisualEffectView.leadingAnchor.constraint(equalTo: blurVisualEffectView.leadingAnchor),
vibrancyVisualEffectView.trailingAnchor.constraint(equalTo: blurVisualEffectView.trailingAnchor),
vibrancyVisualEffectView.bottomAnchor.constraint(equalTo: blurVisualEffectView.bottomAnchor),
contentWarningOverlayView.topAnchor.constraint(equalTo: container.topAnchor),
contentWarningOverlayView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
contentWarningOverlayView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
contentWarningOverlayView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
])
contentWarningLabel.translatesAutoresizingMaskIntoConstraints = false
vibrancyVisualEffectView.contentView.addSubview(contentWarningLabel)
NSLayoutConstraint.activate([
contentWarningLabel.leadingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.leadingAnchor),
contentWarningLabel.trailingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.trailingAnchor),
contentWarningLabel.centerYAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.centerYAnchor),
])
blurVisualEffectView.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer
tapGesture.addTarget(self, action: #selector(MosaicImageViewContainer.visualEffectViewTapGestureRecognizerHandler(_:)))
blurVisualEffectView.addGestureRecognizer(tapGesture)
}
}
@ -117,9 +98,9 @@ extension MosaicImageViewContainer {
container.subviews.forEach { subview in
subview.removeFromSuperview()
}
blurVisualEffectView.removeFromSuperview()
blurVisualEffectView.effect = MosaicImageViewContainer.blurVisualEffect
vibrancyVisualEffectView.alpha = 1.0
contentWarningOverlayView.removeFromSuperview()
contentWarningOverlayView.blurVisualEffectView.effect = ContentWarningOverlayView.blurVisualEffect
contentWarningOverlayView.vibrancyVisualEffectView.alpha = 1.0
imageViews = []
container.spacing = 1
@ -140,7 +121,7 @@ extension MosaicImageViewContainer {
let imageView = UIImageView()
imageViews.append(imageView)
imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius
imageView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius
imageView.layer.cornerCurve = .continuous
imageView.contentMode = .scaleAspectFill
@ -155,13 +136,12 @@ extension MosaicImageViewContainer {
containerHeightLayoutConstraint.constant = floor(rect.height)
containerHeightLayoutConstraint.isActive = true
blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
addSubview(blurVisualEffectView)
addSubview(contentWarningOverlayView)
NSLayoutConstraint.activate([
blurVisualEffectView.topAnchor.constraint(equalTo: imageView.topAnchor),
blurVisualEffectView.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
blurVisualEffectView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
blurVisualEffectView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
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 imageView
@ -193,7 +173,7 @@ extension MosaicImageViewContainer {
self.imageViews.append(contentsOf: imageViews)
imageViews.forEach { imageView in
imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = MosaicImageViewContainer.cornerRadius
imageView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius
imageView.layer.cornerCurve = .continuous
imageView.contentMode = .scaleAspectFill
}
@ -242,13 +222,12 @@ extension MosaicImageViewContainer {
}
}
blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
addSubview(blurVisualEffectView)
addSubview(contentWarningOverlayView)
NSLayoutConstraint.activate([
blurVisualEffectView.topAnchor.constraint(equalTo: container.topAnchor),
blurVisualEffectView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
blurVisualEffectView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
blurVisualEffectView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
contentWarningOverlayView.topAnchor.constraint(equalTo: container.topAnchor),
contentWarningOverlayView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
contentWarningOverlayView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
contentWarningOverlayView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
])
return imageViews
@ -260,7 +239,7 @@ 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, didTapContentWarningVisualEffectView: blurVisualEffectView)
delegate?.mosaicImageViewContainer(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
}
@objc private func photoTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {

View File

@ -1,121 +0,0 @@
//
// MosaicPlayerView.swift
// Mastodon
//
// Created by xiaojian sun on 2021/3/10.
//
import AVKit
import UIKit
final class MosaicPlayerView: UIView {
static let cornerRadius: CGFloat = 8
private let container = UIView()
private let touchBlockingView = TouchBlockingView()
private var containerHeightLayoutConstraint: NSLayoutConstraint!
let playerViewController = AVPlayerViewController()
let gifIndicatorLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .heavy)
label.text = "GIF"
label.textColor = .white
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension MosaicPlayerView {
private func _init() {
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,
])
addSubview(gifIndicatorLabel)
gifIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
gifIndicatorLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 4),
gifIndicatorLabel.trailingAnchor.constraint(equalTo: trailingAnchor)
])
// will not influence full-screen playback
playerViewController.view.layer.masksToBounds = true
playerViewController.view.layer.cornerRadius = MosaicPlayerView.cornerRadius
playerViewController.view.layer.cornerCurve = .continuous
}
}
extension MosaicPlayerView {
func reset() {
// note: set playerViewController.player pause() and nil in data source configuration process make reloadData not break playing
gifIndicatorLabel.removeFromSuperview()
playerViewController.willMove(toParent: nil)
playerViewController.view.removeFromSuperview()
playerViewController.removeFromParent()
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)
)
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: floor(rect.width)).priority(.required - 1),
])
containerHeightLayoutConstraint.constant = floor(rect.height)
containerHeightLayoutConstraint.isActive = true
gifIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false
touchBlockingView.addSubview(gifIndicatorLabel)
NSLayoutConstraint.activate([
touchBlockingView.trailingAnchor.constraint(equalTo: gifIndicatorLabel.trailingAnchor, constant: 8),
touchBlockingView.bottomAnchor.constraint(equalTo: gifIndicatorLabel.bottomAnchor, constant: 8),
])
return playerViewController
}
}

View File

@ -0,0 +1,185 @@
//
// PlayerContainerView.swift
// Mastodon
//
// Created by xiaojian sun on 2021/3/10.
//
import AVKit
import UIKit
protocol PlayerContainerViewDelegate: class {
func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
}
final class PlayerContainerView: UIView {
static let cornerRadius: CGFloat = 8
private let container = UIView()
private let touchBlockingView = TouchBlockingView()
private var containerHeightLayoutConstraint: NSLayoutConstraint!
let contentWarningOverlayView: ContentWarningOverlayView = {
let contentWarningOverlayView = ContentWarningOverlayView()
return contentWarningOverlayView
}()
let playerViewController = AVPlayerViewController()
let mediaTypeIndicotorLabel: UILabel = {
let label = UILabel()
label.textColor = .white
label.textAlignment = .right
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let mediaTypeIndicotorView: UIView = {
let view = UIView()
view.backgroundColor = Asset.Colors.Background.mediaTypeIndicotor.color
view.translatesAutoresizingMaskIntoConstraints = false
let rect = CGRect(x: 0, y: 0, width: 47, height: 50)
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft], cornerRadii: CGSize(width: 50, height: 50))
let maskLayer = CAShapeLayer()
maskLayer.frame = rect
maskLayer.path = path.cgPath
view.layer.mask = maskLayer
return view
}()
weak var delegate: PlayerContainerViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension PlayerContainerView {
private func _init() {
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
addSubview(contentWarningOverlayView)
NSLayoutConstraint.activate([
contentWarningOverlayView.topAnchor.constraint(equalTo: topAnchor),
contentWarningOverlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentWarningOverlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
contentWarningOverlayView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
contentWarningOverlayView.delegate = self
// mediaType
addSubview(mediaTypeIndicotorView)
NSLayoutConstraint.activate([
mediaTypeIndicotorView.bottomAnchor.constraint(equalTo: bottomAnchor),
mediaTypeIndicotorView.trailingAnchor.constraint(equalTo: trailingAnchor),
mediaTypeIndicotorView.heightAnchor.constraint(equalToConstant: 25),
mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: 47)
])
mediaTypeIndicotorView.addSubview(mediaTypeIndicotorLabel)
NSLayoutConstraint.activate([
mediaTypeIndicotorLabel.topAnchor.constraint(equalTo: mediaTypeIndicotorView.topAnchor),
mediaTypeIndicotorLabel.leadingAnchor.constraint(equalTo: mediaTypeIndicotorView.leadingAnchor),
mediaTypeIndicotorLabel.bottomAnchor.constraint(equalTo: mediaTypeIndicotorView.bottomAnchor),
mediaTypeIndicotorView.trailingAnchor.constraint(equalTo: mediaTypeIndicotorLabel.trailingAnchor, constant: 8)
])
}
}
// MARK: - ContentWarningOverlayViewDelegate
extension PlayerContainerView: ContentWarningOverlayViewDelegate {
func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) {
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()
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)
)
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: floor(rect.width)).priority(.required - 1),
])
containerHeightLayoutConstraint.constant = floor(rect.height)
containerHeightLayoutConstraint.isActive = true
bringSubviewToFront(mediaTypeIndicotorView)
return playerViewController
}
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:
mediaTypeIndicotorLabel.font = roundedFont(weight: .heavy, fontSize: fontSize)
mediaTypeIndicotorLabel.text = "GIF"
case .video:
let configuration = UIImage.SymbolConfiguration(font: roundedFont(weight: .regular, fontSize: fontSize))
let image = UIImage(systemName: "video.fill", withConfiguration: configuration)!
let attachment = NSTextAttachment()
attachment.image = image.withTintColor(.white)
mediaTypeIndicotorLabel.attributedText = NSAttributedString(attachment: attachment)
}
}
}

View File

@ -0,0 +1,92 @@
//
// ContentWarningOverlayView.swift
// Mastodon
//
// Created by sxiaojian on 2021/3/11.
//
import os.log
import Foundation
import UIKit
protocol ContentWarningOverlayViewDelegate: class {
func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView)
}
class ContentWarningOverlayView: UIView {
static let cornerRadius: CGFloat = 4
static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial)
let blurVisualEffectView = UIVisualEffectView(effect: ContentWarningOverlayView.blurVisualEffect)
let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: ContentWarningOverlayView.blurVisualEffect))
let contentWarningLabel: UILabel = {
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15))
label.text = L10n.Common.Controls.Status.mediaContentWarning
label.textAlignment = .center
return label
}()
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
weak var delegate: ContentWarningOverlayViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension ContentWarningOverlayView {
private func _init() {
backgroundColor = .clear
translatesAutoresizingMaskIntoConstraints = false
// add blur visual effect view in the setup method
blurVisualEffectView.layer.masksToBounds = true
blurVisualEffectView.layer.cornerRadius = ContentWarningOverlayView.cornerRadius
blurVisualEffectView.layer.cornerCurve = .continuous
vibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
blurVisualEffectView.contentView.addSubview(vibrancyVisualEffectView)
NSLayoutConstraint.activate([
vibrancyVisualEffectView.topAnchor.constraint(equalTo: blurVisualEffectView.topAnchor),
vibrancyVisualEffectView.leadingAnchor.constraint(equalTo: blurVisualEffectView.leadingAnchor),
vibrancyVisualEffectView.trailingAnchor.constraint(equalTo: blurVisualEffectView.trailingAnchor),
vibrancyVisualEffectView.bottomAnchor.constraint(equalTo: blurVisualEffectView.bottomAnchor),
])
contentWarningLabel.translatesAutoresizingMaskIntoConstraints = false
vibrancyVisualEffectView.contentView.addSubview(contentWarningLabel)
NSLayoutConstraint.activate([
contentWarningLabel.leadingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.leadingAnchor),
contentWarningLabel.trailingAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.layoutMarginsGuide.trailingAnchor),
contentWarningLabel.centerYAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.centerYAnchor),
])
blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
addSubview(blurVisualEffectView)
NSLayoutConstraint.activate([
blurVisualEffectView.topAnchor.constraint(equalTo: topAnchor),
blurVisualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor),
blurVisualEffectView.trailingAnchor.constraint(equalTo: trailingAnchor),
blurVisualEffectView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
tapGestureRecognizer.addTarget(self, action: #selector(ContentWarningOverlayView.tapGestureRecognizerHandler(_:)))
addGestureRecognizer(tapGestureRecognizer)
}
}
extension ContentWarningOverlayView {
@objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
delegate?.contentWarningOverlayViewDidPressed(self)
}
}

View File

@ -13,6 +13,7 @@ import AlamofireImage
protocol StatusViewDelegate: class {
func statusView(_ statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton)
}
@ -156,7 +157,7 @@ final class StatusView: UIView {
return imageView
}()
let mosaicPlayerView = MosaicPlayerView()
let playerContainerView = PlayerContainerView()
let audioView: AudioContainerView = {
let audioView = AudioContainerView()
@ -353,7 +354,7 @@ extension StatusView {
audioView.heightAnchor.constraint(equalToConstant: 44)
])
// video gif
statusContainerStackView.addArrangedSubview(mosaicPlayerView)
statusContainerStackView.addArrangedSubview(playerContainerView)
// action toolbar container
containerStackView.addArrangedSubview(actionToolbarContainer)
@ -364,12 +365,14 @@ extension StatusView {
pollTableView.isHidden = true
pollStatusStackView.isHidden = true
audioView.isHidden = true
mosaicPlayerView.isHidden = true
playerContainerView.isHidden = true
contentWarningBlurContentImageView.isHidden = true
statusContentWarningContainerStackView.isHidden = true
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false
playerContainerView.delegate = self
contentWarningActionButton.addTarget(self, action: #selector(StatusView.contentWarningActionButtonPressed(_:)), for: .touchUpInside)
pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside)
}
@ -420,6 +423,13 @@ extension StatusView {
}
// MARK: - PlayerContainerViewDelegate
extension StatusView: PlayerContainerViewDelegate {
func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
delegate?.statusView(self, playerContainerView: playerContainerView, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
}
}
// MARK: - AvatarConfigurableView
extension StatusView: AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize }

View File

@ -22,8 +22,9 @@ protocol StatusTableViewCellDelegate: class {
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView)
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
func statusTableViewCell(_ cell: StatusTableViewCell, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, pollVoteButtonPressed button: UIButton)
@ -54,8 +55,8 @@ final class StatusTableViewCell: UITableViewCell {
statusView.isStatusTextSensitive = false
statusView.cleanUpContentWarning()
statusView.pollTableView.dataSource = nil
statusView.mosaicPlayerView.reset()
statusView.mosaicPlayerView.isHidden = true
statusView.playerContainerView.reset()
statusView.playerContainerView.isHidden = true
disposeBag.removeAll()
observations.removeAll()
}
@ -197,6 +198,10 @@ extension StatusTableViewCell: StatusViewDelegate {
delegate?.statusTableViewCell(self, statusView: statusView, contentWarningActionButtonPressed: button)
}
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
delegate?.statusTableViewCell(self, playerContainerView: playerContainerView, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
}
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
delegate?.statusTableViewCell(self, statusView: statusView, pollVoteButtonPressed: button)
}
@ -210,8 +215,8 @@ extension StatusTableViewCell: MosaicImageViewContainerDelegate {
delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, didTapImageView: imageView, atIndex: index)
}
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView) {
delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, didTapContentWarningVisualEffectView: visualEffectView)
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
delegate?.statusTableViewCell(self, mosaicImageViewContainer: mosaicImageViewContainer, contentWarningOverlayViewDidPressed: contentWarningOverlayView)
}
}

View File

@ -13,57 +13,57 @@ class AudioContainerViewModel {
static func configure(
cell: StatusTableViewCell,
audioAttachment: Attachment,
videoPlaybackService: VideoPlaybackService
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 { _ in
if audioAttachment === AudioPlayer.shared.attachment {
if AudioPlayer.shared.isPlaying() {
AudioPlayer.shared.pause()
.sink { [weak audioService] _ in
guard let audioService = audioService else { return }
if audioAttachment === audioService.attachment {
if audioService.isPlaying() {
audioService.pause()
} else {
AudioPlayer.shared.resume()
videoPlaybackService.pauseWhenPlayAudio()
audioService.resume()
}
if AudioPlayer.shared.currentTimeSubject.value == 0 {
AudioPlayer.shared.playAudio(audioAttachment: audioAttachment)
videoPlaybackService.pauseWhenPlayAudio()
if audioService.currentTimeSubject.value == 0 {
audioService.playAudio(audioAttachment: audioAttachment)
}
} else {
AudioPlayer.shared.playAudio(audioAttachment: audioAttachment)
videoPlaybackService.pauseWhenPlayAudio()
audioService.playAudio(audioAttachment: audioAttachment)
}
}
.store(in: &cell.disposeBag)
audioView.slider.publisher(for: .valueChanged)
.sink { slider in
.sink { [weak audioService] slider in
guard let audioService = audioService else { return }
let slider = slider as! UISlider
let time = Double(slider.value) * duration
AudioPlayer.shared.seekToTime(time: time)
audioService.seekToTime(time: time)
}
.store(in: &cell.disposeBag)
observePlayer(cell: cell, audioAttachment: audioAttachment)
if audioAttachment != AudioPlayer.shared.attachment {
observePlayer(cell: cell, audioAttachment: audioAttachment, audioService: audioService)
if audioAttachment != audioService.attachment {
configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: .stopped)
}
}
static func observePlayer(
cell: StatusTableViewCell,
audioAttachment: Attachment
audioAttachment: Attachment,
audioService: AudioPlaybackService
) {
let audioView = cell.statusView.audioView
var lastCurrentTimeSubject: TimeInterval?
AudioPlayer.shared.currentTimeSubject
audioService.currentTimeSubject
.throttle(for: 0.33, scheduler: DispatchQueue.main, latest: true)
.compactMap { time -> (TimeInterval, Float)? in
.compactMap { [weak audioService] time -> (TimeInterval, Float)? in
defer {
lastCurrentTimeSubject = time
}
guard audioAttachment === AudioPlayer.shared.attachment else { return nil }
guard audioAttachment === audioService?.attachment else { return nil }
guard let duration = audioAttachment.meta?.original?.duration else { return nil }
if let lastCurrentTimeSubject = lastCurrentTimeSubject, time != 0.0 {
@ -78,10 +78,10 @@ class AudioContainerViewModel {
audioView.slider.setValue(progress, animated: true)
})
.store(in: &cell.disposeBag)
AudioPlayer.shared.playbackState
audioService.playbackState
.receive(on: DispatchQueue.main)
.sink(receiveValue: { playbackState in
if audioAttachment === AudioPlayer.shared.attachment {
if audioAttachment === audioService.attachment {
configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: playbackState)
} else {
configureAudioView(audioView: audioView, audioAttachment: audioAttachment, playbackState: .stopped)

View File

@ -14,6 +14,7 @@ import UIKit
final class VideoPlayerViewModel {
var disposeBag = Set<AnyCancellable>()
static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.video-playback-service.appWillPlayVideo")
// input
let previewImageURL: URL?
let videoURL: URL
@ -63,7 +64,7 @@ final class VideoPlayerViewModel {
.sink { [weak self] timeControlStatus in
guard let _ = self else { return }
guard timeControlStatus == .playing else { return }
AudioPlayer.shared.pauseIfNeed()
NotificationCenter.default.post(name: VideoPlayerViewModel.appWillPlayVideoNotification, object: nil)
switch videoKind {
case .gif:
break

View File

@ -10,8 +10,12 @@ import Combine
import CoreDataStack
import Foundation
import UIKit
import os.log
final class AudioPlayer: NSObject {
final class AudioPlaybackService: NSObject {
static let appWillPlayAudioNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.audio-playback-service.appWillPlayAudio")
var disposeBag = Set<AnyCancellable>()
var player = AVPlayer()
@ -21,19 +25,16 @@ final class AudioPlayer: NSObject {
let session = AVAudioSession.sharedInstance()
let playbackState = CurrentValueSubject<PlaybackState, Never>(PlaybackState.unknown)
// MARK: - singleton
public static let shared = AudioPlayer()
let currentTimeSubject = CurrentValueSubject<TimeInterval, Never>(0)
private override init() {
override init() {
super.init()
addObserver()
}
}
extension AudioPlayer {
extension AudioPlaybackService {
func playAudio(audioAttachment: Attachment) {
guard let url = URL(string: audioAttachment.url) else {
return
@ -45,6 +46,7 @@ extension AudioPlayer {
return
}
notifyWillPlayAudioNotification()
if audioAttachment == attachment {
if self.playbackState.value == .stopped {
self.seekToTime(time: .zero)
@ -83,6 +85,12 @@ extension AudioPlayer {
}
}
.store(in: &disposeBag)
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 }
@ -119,10 +127,14 @@ extension AudioPlayer {
.store(in: &disposeBag)
}
func notifyWillPlayAudioNotification() {
NotificationCenter.default.post(name: AudioPlaybackService.appWillPlayAudioNotification, object: nil)
}
func isPlaying() -> Bool {
return self.playbackState.value == .readyToPlay || self.playbackState.value == .playing
return playbackState.value == .readyToPlay || playbackState.value == .playing
}
func resume() {
notifyWillPlayAudioNotification()
player.play()
playbackState.value = .playing
}
@ -140,3 +152,10 @@ extension AudioPlayer {
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()
}
}

View File

@ -90,6 +90,13 @@ extension VideoPlaybackService {
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)
}
}

View File

@ -28,6 +28,7 @@ class AppContext: ObservableObject {
private var documentStoreSubscription: AnyCancellable!
let videoPlaybackService = VideoPlaybackService()
let audioPlaybackService = AudioPlaybackService()
let overrideTraitCollection = CurrentValueSubject<UITraitCollection?, Never>(nil)