diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 1653f6241..f2db77a66 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -49,7 +49,6 @@ 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8E25C8228A004A627A /* UIButton.swift */; }; 2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */; }; 2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */; }; - 2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46976325C2A71500CF4AA9 /* UIIamge.swift */; }; 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; }; 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; }; 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */; }; @@ -288,7 +287,6 @@ 2D42FF8E25C8228A004A627A /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = ""; }; 2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+State.swift"; sourceTree = ""; }; 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLayoutConstraint.swift; sourceTree = ""; }; - 2D46976325C2A71500CF4AA9 /* UIIamge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIIamge.swift; sourceTree = ""; }; 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewController.swift; sourceTree = ""; }; 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewModel.swift; sourceTree = ""; }; 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlContainableScrollViews.swift; sourceTree = ""; }; @@ -1101,7 +1099,6 @@ 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */, DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, 2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */, - 2D46976325C2A71500CF4AA9 /* UIIamge.swift */, 2DF123A625C3B0210020F248 /* ActiveLabel.swift */, DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */, 2D42FF6A25C817D2004A627A /* MastodonContent.swift */, @@ -1614,7 +1611,6 @@ 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */, 2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */, 2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */, - 2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */, diff --git a/Mastodon/Extension/UIIamge.swift b/Mastodon/Extension/UIIamge.swift deleted file mode 100644 index 4f4b350c3..000000000 --- a/Mastodon/Extension/UIIamge.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// UIIamge.swift -// Mastodon -// -// Created by sxiaojian on 2021/1/28. -// - -import UIKit -import CoreImage -import CoreImage.CIFilterBuiltins - -extension UIImage { - - static func placeholder(size: CGSize = CGSize(width: 1, height: 1), color: UIColor) -> UIImage { - let render = UIGraphicsImageRenderer(size: size) - - return render.image { (context: UIGraphicsImageRendererContext) in - context.cgContext.setFillColor(color.cgColor) - context.fill(CGRect(origin: .zero, size: size)) - } - } - -} - -// refs: https://www.hackingwithswift.com/example-code/media/how-to-read-the-average-color-of-a-uiimage-using-ciareaaverage -extension UIImage { - @available(iOS 14.0, *) - var dominantColor: UIColor? { - guard let inputImage = CIImage(image: self) else { return nil } - - let filter = CIFilter.areaAverage() - filter.inputImage = inputImage - filter.extent = inputImage.extent - guard let outputImage = filter.outputImage else { return nil } - - var bitmap = [UInt8](repeating: 0, count: 4) - let context = CIContext(options: [.workingColorSpace: kCFNull]) - context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil) - - return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255) - } -} - -extension UIImage { - func blur(radius: CGFloat) -> UIImage? { - guard let inputImage = CIImage(image: self) else { return nil } - let blurFilter = CIFilter.gaussianBlur() - blurFilter.inputImage = inputImage - blurFilter.radius = Float(radius) - guard let outputImage = blurFilter.outputImage else { return nil } - guard let cgImage = CIContext().createCGImage(outputImage, from: outputImage.extent) else { return nil } - let image = UIImage(cgImage: cgImage, scale: scale, orientation: imageOrientation) - return image - } -} diff --git a/Mastodon/Extension/UIImage.swift b/Mastodon/Extension/UIImage.swift index e821b676c..3c3c43400 100644 --- a/Mastodon/Extension/UIImage.swift +++ b/Mastodon/Extension/UIImage.swift @@ -5,31 +5,66 @@ // Created by sxiaojian on 2021/3/8. // +import CoreImage +import CoreImage.CIFilterBuiltins import UIKit extension UIImage { - class func imageWithColor(color: UIColor, size: CGSize=CGSize(width: 1, height: 1)) -> UIImage? { - UIGraphicsBeginImageContextWithOptions(size, false, 0) - color.setFill() - UIRectFill(CGRect(origin: CGPoint.zero, size: size)) - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return image - } - public func withRoundedCorners(radius: CGFloat? = nil) -> UIImage? { - let maxRadius = min(size.width, size.height) / 2 - let cornerRadius: CGFloat - if let radius = radius, radius > 0 && radius <= maxRadius { - cornerRadius = radius - } else { - cornerRadius = maxRadius + static func placeholder(size: CGSize = CGSize(width: 1, height: 1), color: UIColor) -> UIImage { + let render = UIGraphicsImageRenderer(size: size) + + return render.image { (context: UIGraphicsImageRendererContext) in + context.cgContext.setFillColor(color.cgColor) + context.fill(CGRect(origin: .zero, size: size)) } - UIGraphicsBeginImageContextWithOptions(size, false, scale) - let rect = CGRect(origin: .zero, size: size) - UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).addClip() - draw(in: rect) - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() + } +} + +// refs: https://www.hackingwithswift.com/example-code/media/how-to-read-the-average-color-of-a-uiimage-using-ciareaaverage +extension UIImage { + @available(iOS 14.0, *) + var dominantColor: UIColor? { + guard let inputImage = CIImage(image: self) else { return nil } + + let filter = CIFilter.areaAverage() + filter.inputImage = inputImage + filter.extent = inputImage.extent + guard let outputImage = filter.outputImage else { return nil } + + var bitmap = [UInt8](repeating: 0, count: 4) + let context = CIContext(options: [.workingColorSpace: kCFNull]) + context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil) + + return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255) + } +} + +extension UIImage { + func blur(radius: CGFloat) -> UIImage? { + guard let inputImage = CIImage(image: self) else { return nil } + let blurFilter = CIFilter.gaussianBlur() + blurFilter.inputImage = inputImage + blurFilter.radius = Float(radius) + guard let outputImage = blurFilter.outputImage else { return nil } + guard let cgImage = CIContext().createCGImage(outputImage, from: outputImage.extent) else { return nil } + let image = UIImage(cgImage: cgImage, scale: scale, orientation: imageOrientation) return image } } + +public extension UIImage { + func withRoundedCorners(radius: CGFloat? = nil) -> UIImage? { + let maxRadius = min(size.width, size.height) / 2 + let cornerRadius: CGFloat = { + guard let radius = radius, radius > 0 else { return maxRadius } + return min(radius, maxRadius) + }() + + let render = UIGraphicsImageRenderer(size: size) + return render.image { (_: UIGraphicsImageRendererContext) in + let rect = CGRect(origin: .zero, size: size) + UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).addClip() + draw(in: rect) + } + } +} diff --git a/Mastodon/Scene/Share/View/Container/AudioContainerView.swift b/Mastodon/Scene/Share/View/Container/AudioContainerView.swift index e20f5cca0..980e5ae87 100644 --- a/Mastodon/Scene/Share/View/Container/AudioContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/AudioContainerView.swift @@ -5,13 +5,11 @@ // Created by sxiaojian on 2021/3/8. // -import os.log import CoreDataStack +import os.log import UIKit - final class AudioContainerView: UIView { - static let cornerRadius: CGFloat = 22 let container: UIStackView = { @@ -20,7 +18,7 @@ final class AudioContainerView: UIView { stackView.distribution = .fill stackView.alignment = .center stackView.spacing = 11 - stackView.layoutMargins = UIEdgeInsets(top: 0, left: 9, bottom: 0, right: 9) + stackView.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) stackView.isLayoutMarginsRelativeArrangement = true stackView.layer.cornerRadius = AudioContainerView.cornerRadius stackView.clipsToBounds = true @@ -29,7 +27,7 @@ final class AudioContainerView: UIView { return stackView }() - let checkmarkBackgroundView: UIView = { + let playButtonBackgroundView: UIView = { let view = UIView() view.layer.cornerRadius = 16 view.clipsToBounds = true @@ -39,7 +37,7 @@ final class AudioContainerView: UIView { }() let playButton: UIButton = { - let button = UIButton(type: .custom) + 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) @@ -57,7 +55,7 @@ final class AudioContainerView: UIView { slider.translatesAutoresizingMaskIntoConstraints = false slider.minimumTrackTintColor = Asset.Colors.Slider.bar.color slider.maximumTrackTintColor = Asset.Colors.Slider.bar.color - if let image = UIImage.imageWithColor(color: .white, size: CGSize(width: 22, height: 22))?.withRoundedCorners(radius: 11) { + if let image = UIImage.placeholder(size: CGSize(width: 22, height: 22), color: .white).withRoundedCorners(radius: 11) { slider.setThumbImage(image, for: .normal) } return slider @@ -81,13 +79,10 @@ final class AudioContainerView: UIView { super.init(coder: coder) _init() } - } extension AudioContainerView { - private func _init() { - addSubview(container) NSLayoutConstraint.activate([ container.topAnchor.constraint(equalTo: topAnchor), @@ -96,14 +91,14 @@ extension AudioContainerView { bottomAnchor.constraint(equalTo: container.bottomAnchor), ]) - //checkmark - checkmarkBackgroundView.addSubview(playButton) - container.addArrangedSubview(checkmarkBackgroundView) + // checkmark + playButtonBackgroundView.addSubview(playButton) + container.addArrangedSubview(playButtonBackgroundView) NSLayoutConstraint.activate([ - playButton.centerXAnchor.constraint(equalTo: checkmarkBackgroundView.centerXAnchor), - playButton.centerYAnchor.constraint(equalTo: checkmarkBackgroundView.centerYAnchor), - checkmarkBackgroundView.heightAnchor.constraint(equalToConstant: 32), - checkmarkBackgroundView.widthAnchor.constraint(equalToConstant: 32), + playButton.centerXAnchor.constraint(equalTo: playButtonBackgroundView.centerXAnchor), + playButton.centerYAnchor.constraint(equalTo: playButtonBackgroundView.centerYAnchor), + playButtonBackgroundView.heightAnchor.constraint(equalToConstant: 32), + playButtonBackgroundView.widthAnchor.constraint(equalToConstant: 32), ]) container.addArrangedSubview(slider) @@ -113,5 +108,4 @@ extension AudioContainerView { timeLabel.widthAnchor.constraint(equalToConstant: 40), ]) } - } diff --git a/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift b/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift index 8d4e9e2a5..de250a539 100644 --- a/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift @@ -5,8 +5,8 @@ // Created by sxiaojian on 2021/3/9. // -import Foundation import CoreDataStack +import Foundation import UIKit class AudioContainerViewModel { @@ -17,10 +17,11 @@ class AudioContainerViewModel { 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 { button in - if (button.isSelected) { + .sink { _ in + let isPlaying = AudioPlayer.shared.playbackState.value == .readyToPlay || AudioPlayer.shared.playbackState.value == .playing + if isPlaying { AudioPlayer.shared.pause() } else { if audioAttachment === AudioPlayer.shared.attachment { @@ -39,15 +40,15 @@ class AudioContainerViewModel { .sink { slider in let slider = slider as! UISlider let time = Double(slider.value) * duration - AudioPlayer.shared.seekToTime(time: time) } .store(in: &cell.disposeBag) - self.observePlayer(cell:cell, audioAttachment: audioAttachment) + self.observePlayer(cell: cell, audioAttachment: audioAttachment) if audioAttachment != AudioPlayer.shared.attachment { self.resetAudioView(audioView: audioView) } } + static func observePlayer( cell: StatusTableViewCell, audioAttachment: Attachment @@ -61,16 +62,17 @@ class AudioContainerViewModel { .sink(receiveValue: { time in audioView.timeLabel.text = time.asString(style: .positional) if let duration = audioAttachment.meta?.original?.duration, !audioView.slider.isTracking { - audioView.slider.setValue(Float(time/duration), animated: true) + audioView.slider.setValue(Float(time / duration), animated: true) } }) .store(in: &cell.disposeBag) AudioPlayer.shared.playbackState .receive(on: DispatchQueue.main) .sink(receiveValue: { playbackState in - if (audioAttachment === AudioPlayer.shared.attachment) { + if audioAttachment === AudioPlayer.shared.attachment { let isPlaying = playbackState == .playing || playbackState == .readyToPlay audioView.playButton.isSelected = isPlaying + audioView.slider.isEnabled = isPlaying if playbackState == .stopped { self.resetAudioView(audioView: audioView) } @@ -80,8 +82,10 @@ class AudioContainerViewModel { }) .store(in: &cell.disposeBag) } - static func resetAudioView(audioView:AudioContainerView) { + + static func resetAudioView(audioView: AudioContainerView) { audioView.playButton.isSelected = false audioView.slider.setValue(0, animated: false) + audioView.slider.isEnabled = false } } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Attachment.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Attachment.swift index bb03ba1bd..d50d87695 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Attachment.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Attachment.swift @@ -23,7 +23,7 @@ extension Mastodon.Entity { public let id: ID public let type: Type public let url: String - public let previewURL: String? + public let previewURL: String? // could be nil when attachement is audio public let remoteURL: String? public let textURL: String?