From 5c86feccb96462be1026663558045832bd8cc918 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 21 Nov 2024 19:28:55 -0500 Subject: [PATCH] Move content VCs to GalleryVC package --- Packages/GalleryVC/Package.swift | 4 + ...FallbackGalleryContentViewController.swift | 22 ++-- .../ImageGalleryContentViewController.swift | 69 +++-------- .../LoadingGalleryContentViewController.swift | 27 +++-- .../VideoControlsViewController.swift | 70 ++++++----- .../VideoGalleryContentViewController.swift | 110 ++++++------------ .../Content}/VideoOverlayViewController.swift | 4 +- Tusker.xcodeproj/project.pbxproj | 32 ++--- ...bleImageGalleryContentViewController.swift | 60 ++++++++++ ...bleVideoGalleryContentViewController.swift | 83 +++++++++++++ .../Gallery/ImageGalleryDataSource.swift | 4 +- .../StatusAttachmentsGalleryDataSource.swift | 10 +- Tusker/Views/Attachments/AttachmentView.swift | 4 +- 13 files changed, 281 insertions(+), 218 deletions(-) rename {Tusker/Screens/Gallery => Packages/GalleryVC/Sources/GalleryVC/Content}/FallbackGalleryContentViewController.swift (79%) rename {Tusker/Screens/Gallery => Packages/GalleryVC/Sources/GalleryVC/Content}/ImageGalleryContentViewController.swift (59%) rename {Tusker/Screens/Gallery => Packages/GalleryVC/Sources/GalleryVC/Content}/LoadingGalleryContentViewController.swift (79%) rename {Tusker/Screens/Gallery => Packages/GalleryVC/Sources/GalleryVC/Content}/VideoControlsViewController.swift (90%) rename {Tusker/Screens/Gallery => Packages/GalleryVC/Sources/GalleryVC/Content}/VideoGalleryContentViewController.swift (64%) rename {Tusker/Screens/Gallery => Packages/GalleryVC/Sources/GalleryVC/Content}/VideoOverlayViewController.swift (99%) create mode 100644 Tusker/Screens/Gallery/GrayscalableImageGalleryContentViewController.swift create mode 100644 Tusker/Screens/Gallery/GrayscalableVideoGalleryContentViewController.swift diff --git a/Packages/GalleryVC/Package.swift b/Packages/GalleryVC/Package.swift index 544564072..158fd4c8d 100644 --- a/Packages/GalleryVC/Package.swift +++ b/Packages/GalleryVC/Package.swift @@ -14,11 +14,15 @@ let package = Package( name: "GalleryVC", targets: ["GalleryVC"]), ], + dependencies: [ + .package(path: "../TuskerComponents"), + ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( name: "GalleryVC", + dependencies: ["TuskerComponents"], swiftSettings: [ .swiftLanguageMode(.v5) ]), diff --git a/Tusker/Screens/Gallery/FallbackGalleryContentViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/Content/FallbackGalleryContentViewController.swift similarity index 79% rename from Tusker/Screens/Gallery/FallbackGalleryContentViewController.swift rename to Packages/GalleryVC/Sources/GalleryVC/Content/FallbackGalleryContentViewController.swift index b3b959e24..12d28da88 100644 --- a/Tusker/Screens/Gallery/FallbackGalleryContentViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/Content/FallbackGalleryContentViewController.swift @@ -1,15 +1,13 @@ // // FallbackGalleryContentViewController.swift -// Tusker +// GalleryVC // // Created by Shadowfacts on 3/18/24. // Copyright © 2024 Shadowfacts. All rights reserved. // import UIKit -import GalleryVC import QuickLook -import Pachyderm private class FallbackGalleryContentViewController: QLPreviewController { private let previewItem = GalleryPreviewItem() @@ -52,39 +50,39 @@ extension FallbackGalleryContentViewController: QLPreviewControllerDataSource { } } -class FallbackGalleryNavigationController: UINavigationController, GalleryContentViewController { - init(url: URL) { +public class FallbackGalleryNavigationController: UINavigationController, GalleryContentViewController { + public init(url: URL) { super.init(nibName: nil, bundle: nil) self.viewControllers = [FallbackGalleryContentViewController(url: url)] } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() container?.disableGalleryScrollAndZoom() } - required init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: GalleryContentViewController - weak var container: (any GalleryVC.GalleryContentViewControllerContainer)? + public weak var container: (any GalleryContentViewControllerContainer)? - var contentSize: CGSize { + public var contentSize: CGSize { .zero } - var activityItemsForSharing: [Any] { + public var activityItemsForSharing: [Any] { [] } - var caption: String? { + public var caption: String? { nil } - var canAnimateFromSourceView: Bool { + public var canAnimateFromSourceView: Bool { false } } diff --git a/Tusker/Screens/Gallery/ImageGalleryContentViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/Content/ImageGalleryContentViewController.swift similarity index 59% rename from Tusker/Screens/Gallery/ImageGalleryContentViewController.swift rename to Packages/GalleryVC/Sources/GalleryVC/Content/ImageGalleryContentViewController.swift index c7a5b2e04..37191dbcb 100644 --- a/Tusker/Screens/Gallery/ImageGalleryContentViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/Content/ImageGalleryContentViewController.swift @@ -1,22 +1,22 @@ // // ImageGalleryContentViewController.swift -// Tusker +// GalleryVC // // Created by Shadowfacts on 3/17/24. // Copyright © 2024 Shadowfacts. All rights reserved. // import UIKit -import GalleryVC -import Pachyderm import TuskerComponents @preconcurrency import VisionKit -class ImageGalleryContentViewController: UIViewController, GalleryContentViewController { - let url: URL - let caption: String? - let originalData: Data? - let image: UIImage +open class ImageGalleryContentViewController: UIViewController, GalleryContentViewController { + public let caption: String? + public var image: UIImage { + didSet { + imageView?.image = image + } + } let gifController: GIFController? private var imageView: GIFImageView! @@ -27,12 +27,8 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon @available(iOS 16.0, macCatalyst 17.0, *) private var analysisInteraction: ImageAnalysisInteraction? { _analysisInteraction as? ImageAnalysisInteraction } - private var isGrayscale = false - - init(url: URL, caption: String?, originalData: Data?, image: UIImage, gifController: GIFController?) { - self.url = url + public init(image: UIImage, caption: String?, gifController: GIFController?) { self.caption = caption - self.originalData = originalData self.image = image self.gifController = gifController @@ -41,21 +37,14 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon preferredContentSize = image.size } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() - isGrayscale = Preferences.shared.grayscaleImages - let maybeGrayscaleImage = if isGrayscale { - ImageGrayscalifier.convert(url: url, image: image) ?? image - } else { - image - } - - imageView = GIFImageView(image: maybeGrayscaleImage) + imageView = GIFImageView(image: image) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFill imageView.isUserInteractionEnabled = true @@ -86,11 +75,9 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon } } } - - NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) } - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if let gifController { @@ -98,37 +85,19 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon } } - @objc private func preferencesChanged() { - if isGrayscale != Preferences.shared.grayscaleImages { - isGrayscale = Preferences.shared.grayscaleImages - let image = if isGrayscale { - ImageGrayscalifier.convert(url: url, image: image) - } else { - image - } - if let image { - imageView.image = image - } - } - } - // MARK: GalleryContentViewController - weak var container: (any GalleryVC.GalleryContentViewControllerContainer)? + public weak var container: (any GalleryContentViewControllerContainer)? - var contentSize: CGSize { + public var contentSize: CGSize { image.size } - var activityItemsForSharing: [Any] { - if let data = originalData ?? image.pngData() { - return [ImageActivityItemSource(data: data, url: url, image: image)] - } else { - return [] - } + open var activityItemsForSharing: [Any] { + return [image] } - func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { + public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { if #available(iOS 16.0, macCatalyst 17.0, *), let analysisInteraction { analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated) @@ -138,7 +107,7 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon @available(iOS 16.0, macCatalyst 17.0, *) extension ImageGalleryContentViewController: ImageAnalysisInteractionDelegate { - func interaction(_ interaction: ImageAnalysisInteraction, shouldBeginAt point: CGPoint, for interactionType: ImageAnalysisInteraction.InteractionTypes) -> Bool { + public func interaction(_ interaction: ImageAnalysisInteraction, shouldBeginAt point: CGPoint, for interactionType: ImageAnalysisInteraction.InteractionTypes) -> Bool { return container?.galleryControlsVisible ?? true } } diff --git a/Tusker/Screens/Gallery/LoadingGalleryContentViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/Content/LoadingGalleryContentViewController.swift similarity index 79% rename from Tusker/Screens/Gallery/LoadingGalleryContentViewController.swift rename to Packages/GalleryVC/Sources/GalleryVC/Content/LoadingGalleryContentViewController.swift index 77199efe8..48509d645 100644 --- a/Tusker/Screens/Gallery/LoadingGalleryContentViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/Content/LoadingGalleryContentViewController.swift @@ -7,43 +7,42 @@ // import UIKit -import GalleryVC -class LoadingGalleryContentViewController: UIViewController, GalleryContentViewController { +public class LoadingGalleryContentViewController: UIViewController, GalleryContentViewController { private let fallbackCaption: String? private let provider: () async -> (any GalleryContentViewController)? private var wrapped: (any GalleryContentViewController)! - weak var container: GalleryContentViewControllerContainer? + public weak var container: GalleryContentViewControllerContainer? - var contentSize: CGSize { + public var contentSize: CGSize { wrapped?.contentSize ?? .zero } - var activityItemsForSharing: [Any] { + public var activityItemsForSharing: [Any] { wrapped?.activityItemsForSharing ?? [] } - var caption: String? { + public var caption: String? { wrapped?.caption ?? fallbackCaption } - var canAnimateFromSourceView: Bool { + public var canAnimateFromSourceView: Bool { wrapped?.canAnimateFromSourceView ?? true } - init(caption: String?, provider: @escaping () async -> (any GalleryContentViewController)?) { + public init(caption: String?, provider: @escaping () async -> (any GalleryContentViewController)?) { self.fallbackCaption = caption self.provider = provider super.init(nibName: nil, bundle: nil) } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() container?.setGalleryContentLoading(true) @@ -81,7 +80,7 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC let label = UILabel() label.text = "Error Loading" - label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)! + label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .title1).withSymbolicTraits(.traitBold)!, size: 0) label.textColor = .secondaryLabel label.adjustsFontForContentSizeCategory = true @@ -102,15 +101,15 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC ]) } - func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { + public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { wrapped?.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction) } - func galleryContentDidAppear() { + public func galleryContentDidAppear() { wrapped?.galleryContentDidAppear() } - func galleryContentWillDisappear() { + public func galleryContentWillDisappear() { wrapped?.galleryContentWillDisappear() } diff --git a/Tusker/Screens/Gallery/VideoControlsViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/Content/VideoControlsViewController.swift similarity index 90% rename from Tusker/Screens/Gallery/VideoControlsViewController.swift rename to Packages/GalleryVC/Sources/GalleryVC/Content/VideoControlsViewController.swift index f8748981f..f19f13601 100644 --- a/Tusker/Screens/Gallery/VideoControlsViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/Content/VideoControlsViewController.swift @@ -1,6 +1,6 @@ // // VideoControlsViewController.swift -// Tusker +// GalleryVC // // Created by Shadowfacts on 3/21/24. // Copyright © 2024 Shadowfacts. All rights reserved. @@ -19,27 +19,35 @@ class VideoControlsViewController: UIViewController { private let player: AVPlayer - private lazy var muteButton = MuteButton().configure { - $0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside) - $0.setMuted(false, animated: false) - } + private lazy var muteButton: MuteButton = { + let button = MuteButton() + button.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside) + button.setMuted(false, animated: false) + return button + }() - private let timestampLabel = UILabel().configure { - $0.text = "0:00" - $0.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular)) - } + private let timestampLabel: UILabel = { + let label = UILabel() + label.text = "0:00" + label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular)) + return label + }() - private lazy var scrubbingControl = VideoScrubbingControl().configure { - $0.heightAnchor.constraint(equalToConstant: 44).isActive = true - $0.addTarget(self, action: #selector(scrubbingStarted), for: .editingDidBegin) - $0.addTarget(self, action: #selector(scrubbingChanged), for: .editingChanged) - $0.addTarget(self, action: #selector(scrubbingEnded), for: .editingDidEnd) - } + private lazy var scrubbingControl: VideoScrubbingControl = { + let control = VideoScrubbingControl() + control.heightAnchor.constraint(equalToConstant: 44).isActive = true + control.addTarget(self, action: #selector(scrubbingStarted), for: .editingDidBegin) + control.addTarget(self, action: #selector(scrubbingChanged), for: .editingChanged) + control.addTarget(self, action: #selector(scrubbingEnded), for: .editingDidEnd) + return control + }() - private let timeRemainingLabel = UILabel().configure { - $0.text = "-0:00" - $0.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular)) - } + private let timeRemainingLabel: UILabel = { + let label = UILabel() + label.text = "-0:00" + label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular)) + return label + }() private lazy var optionsButton = MenuButton { [unowned self] in let imageName: String @@ -70,17 +78,19 @@ class VideoControlsViewController: UIViewController { return UIMenu(children: [speedMenu]) } - private lazy var hStack = UIStackView(arrangedSubviews: [ - muteButton, - timestampLabel, - scrubbingControl, - timeRemainingLabel, - optionsButton, - ]).configure { - $0.axis = .horizontal - $0.spacing = 8 - $0.alignment = .center - } + private lazy var hStack: UIStackView = { + let stack = UIStackView(arrangedSubviews: [ + muteButton, + timestampLabel, + scrubbingControl, + timeRemainingLabel, + optionsButton, + ]) + stack.axis = .horizontal + stack.spacing = 8 + stack.alignment = .center + return stack + }() private var timestampObserverToken: Any? private var scrubberObserverToken: Any? diff --git a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/Content/VideoGalleryContentViewController.swift similarity index 64% rename from Tusker/Screens/Gallery/VideoGalleryContentViewController.swift rename to Packages/GalleryVC/Sources/GalleryVC/Content/VideoGalleryContentViewController.swift index 756fae558..09f2751dc 100644 --- a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/Content/VideoGalleryContentViewController.swift @@ -1,68 +1,52 @@ // // VideoGalleryContentViewController.swift -// Tusker +// GalleryVC // // Created by Shadowfacts on 3/19/24. // Copyright © 2024 Shadowfacts. All rights reserved. // import UIKit -import GalleryVC import AVFoundation import CoreImage -class VideoGalleryContentViewController: UIViewController, GalleryContentViewController { - private let url: URL - let caption: String? - private var item: AVPlayerItem - let player: AVPlayer - - private var isGrayscale: Bool +open class VideoGalleryContentViewController: UIViewController, GalleryContentViewController { + public let url: URL + public let caption: String? + public private(set) var item: AVPlayerItem + public let player: AVPlayer private var presentationSizeObservation: NSKeyValueObservation? private var statusObservation: NSKeyValueObservation? private var rateObservation: NSKeyValueObservation? - private var isFirstAppearance = true private var hideControlsWorkItem: DispatchWorkItem? - private var audioSessionToken: AudioSessionCoordinator.Token? - init(url: URL, caption: String?) { + public init(url: URL, caption: String?) { self.url = url self.caption = caption - self.isGrayscale = Preferences.shared.grayscaleImages - let asset = AVAsset(url: url) - self.item = VideoGalleryContentViewController.createItem(asset: asset) + self.item = Self.createItem(asset: asset) self.player = AVPlayer(playerItem: item) super.init(nibName: nil, bundle: nil) } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private static func createItem(asset: AVAsset) -> AVPlayerItem { - let item = AVPlayerItem(asset: asset) - if Preferences.shared.grayscaleImages { - #if os(visionOS) - #warning("Use async AVVideoComposition CIFilter initializer") - #else - let filter = CIFilter(name: "CIColorMonochrome")! - filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor") - filter.setValue(1.0, forKey: "inputIntensity") - - item.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in - filter.setValue(request.sourceImage, forKey: "inputImage") - request.finish(with: filter.outputImage!, context: nil) - }) - #endif - } - return item + open class func createItem(asset: AVAsset) -> AVPlayerItem { + return AVPlayerItem(asset: asset) } - override func viewDidLoad() { + public func replaceCurrentItem(with item: AVPlayerItem) { + self.item = item + player.replaceCurrentItem(with: item) + updateItemObservations() + } + + public override func viewDidLoad() { super.viewDidLoad() container?.setGalleryContentLoading(true) @@ -87,19 +71,17 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon scheduleControlsHide() } }) - - NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) } private func updateItemObservations() { presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in - MainActor.runUnsafely { + MainActor.assumeIsolated { self.preferredContentSize = item.presentationSize self.container?.galleryContentChanged() } }) statusObservation = item.observe(\.status, changeHandler: { [unowned self] item, _ in - MainActor.runUnsafely { + MainActor.assumeIsolated { if item.status == .readyToPlay { self.container?.setGalleryContentLoading(false) self.statusObservation = nil @@ -120,7 +102,7 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon let label = UILabel() label.text = "Error Loading" - label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)! + label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .title1).withSymbolicTraits(.traitBold)!, size: 0) label.textColor = .secondaryLabel label.adjustsFontForContentSizeCategory = true @@ -148,22 +130,9 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon ]) } - @objc private func preferencesChanged() { - if isGrayscale != Preferences.shared.grayscaleImages { - let isPlaying = player.rate > 0 - isGrayscale = Preferences.shared.grayscaleImages - item = VideoGalleryContentViewController.createItem(asset: item.asset) - player.replaceCurrentItem(with: item) - updateItemObservations() - if isPlaying { - player.play() - } - } - } - private func scheduleControlsHide() { hideControlsWorkItem = DispatchWorkItem { [weak self] in - MainActor.runUnsafely { + MainActor.assumeIsolated { guard let self, let container = self.container, container.galleryControlsVisible else { @@ -177,24 +146,25 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon // MARK: GalleryContentViewController - weak var container: (any GalleryVC.GalleryContentViewControllerContainer)? + public weak var container: (any GalleryVC.GalleryContentViewControllerContainer)? - var contentSize: CGSize { + public var contentSize: CGSize { item.presentationSize } - var activityItemsForSharing: [Any] { - [VideoActivityItemSource(asset: item.asset, url: url)] + open var activityItemsForSharing: [Any] { +// [VideoActivityItemSource(asset: item.asset, url: url)] + [] } private lazy var overlayVC = VideoOverlayViewController(player: player) - var contentOverlayAccessoryViewController: UIViewController? { + public var contentOverlayAccessoryViewController: UIViewController? { overlayVC } - private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player) + public private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player) - func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { + public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { overlayVC.setVisible(visible) if !visible { @@ -205,25 +175,11 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon } } - func galleryContentDidAppear() { - let wasFirstAppearance = isFirstAppearance - isFirstAppearance = false - - audioSessionToken = AudioSessionCoordinator.shared.beginPlayback(mode: .video) { - if wasFirstAppearance { - DispatchQueue.main.async { - self.player.play() - } - } - } + open func galleryContentDidAppear() { } - func galleryContentWillDisappear() { + open func galleryContentWillDisappear() { player.pause() - - if let audioSessionToken { - AudioSessionCoordinator.shared.endPlayback(token: audioSessionToken) - } } } @@ -253,7 +209,7 @@ private class PlayerView: UIView { playerLayer.videoGravity = .resizeAspect presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] _, _ in - MainActor.runUnsafely { + MainActor.assumeIsolated { self.invalidateIntrinsicContentSize() } }) diff --git a/Tusker/Screens/Gallery/VideoOverlayViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/Content/VideoOverlayViewController.swift similarity index 99% rename from Tusker/Screens/Gallery/VideoOverlayViewController.swift rename to Packages/GalleryVC/Sources/GalleryVC/Content/VideoOverlayViewController.swift index 40515704b..2877621d8 100644 --- a/Tusker/Screens/Gallery/VideoOverlayViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/Content/VideoOverlayViewController.swift @@ -1,6 +1,6 @@ // // VideoOverlayViewController.swift -// Tusker +// GalleryVC // // Created by Shadowfacts on 3/26/24. // Copyright © 2024 Shadowfacts. All rights reserved. @@ -79,7 +79,7 @@ class VideoOverlayViewController: UIViewController { ]) rateObservation = player.observe(\.rate, changeHandler: { player, _ in - MainActor.runUnsafely { + MainActor.assumeIsolated { playPauseButton.image = player.rate > 0 ? VideoOverlayViewController.pauseImage : VideoOverlayViewController.playImage } }) diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 61b01eff3..3e451d7c4 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -203,20 +203,16 @@ D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */; }; D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */; }; D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */; }; - D69261232BB3AEFB0023152C /* VideoOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */; }; D69261272BB3BA610023152C /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261262BB3BA610023152C /* Box.swift */; }; D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */ = {isa = PBXBuildFile; productRef = D6934F2B2BA7AD32002B1C8D /* GalleryVC */; }; D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */; }; - D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */; }; - D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */; }; + D6934F302BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2F2BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift */; }; D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */; }; D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */; }; D6934F382BA8E2B7002B1C8D /* GifvController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F372BA8E2B7002B1C8D /* GifvController.swift */; }; - D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */; }; - D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */; }; + D6934F3C2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3B2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift */; }; D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */; }; D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; }; - D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */; }; D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; }; D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; }; D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; }; @@ -637,19 +633,15 @@ D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNoContentCollectionViewCell.swift; sourceTree = ""; }; D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderMovedOverlayView.swift; sourceTree = ""; }; D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityHandlingContext.swift; sourceTree = ""; }; - D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoOverlayViewController.swift; sourceTree = ""; }; D69261262BB3BA610023152C /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = ""; }; D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsGalleryDataSource.swift; sourceTree = ""; }; - D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryContentViewController.swift; sourceTree = ""; }; - D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingGalleryContentViewController.swift; sourceTree = ""; }; + D6934F2F2BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrayscalableImageGalleryContentViewController.swift; sourceTree = ""; }; D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryDataSource.swift; sourceTree = ""; }; D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvGalleryContentViewController.swift; sourceTree = ""; }; D6934F372BA8E2B7002B1C8D /* GifvController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvController.swift; sourceTree = ""; }; - D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackGalleryContentViewController.swift; sourceTree = ""; }; - D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoGalleryContentViewController.swift; sourceTree = ""; }; + D6934F3B2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrayscalableVideoGalleryContentViewController.swift; sourceTree = ""; }; D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageActivityItemSource.swift; sourceTree = ""; }; D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = ""; }; - D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.swift; sourceTree = ""; }; D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = ""; }; D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = ""; }; D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = ""; }; @@ -899,13 +891,9 @@ children = ( D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */, D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */, - D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */, - D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */, + D6934F2F2BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift */, D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */, - D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */, - D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */, - D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */, - D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */, + D6934F3B2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift */, ); path = Gallery; sourceTree = ""; @@ -2122,7 +2110,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */, 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */, D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */, @@ -2169,8 +2156,7 @@ D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */, D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */, D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */, - D69261232BB3AEFB0023152C /* VideoOverlayViewController.swift in Sources */, - D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */, + D6934F3C2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift in Sources */, D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */, D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, D698F46B2BD079F00054DB14 /* AnnouncementListRow.swift in Sources */, @@ -2190,7 +2176,6 @@ D6D94955298963A900C59229 /* Colors.swift in Sources */, D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */, D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */, - D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */, D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */, D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */, D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */, @@ -2218,7 +2203,6 @@ D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */, D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */, D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */, - D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */, D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */, D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */, @@ -2341,7 +2325,7 @@ D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */, - D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */, + D6934F302BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift in Sources */, D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */, D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */, D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */, diff --git a/Tusker/Screens/Gallery/GrayscalableImageGalleryContentViewController.swift b/Tusker/Screens/Gallery/GrayscalableImageGalleryContentViewController.swift new file mode 100644 index 000000000..f4e2ca737 --- /dev/null +++ b/Tusker/Screens/Gallery/GrayscalableImageGalleryContentViewController.swift @@ -0,0 +1,60 @@ +// +// GrayscalableImageGalleryContentViewController.swift +// Tusker +// +// Created by Shadowfacts on 11/21/24. +// Copyright © 2024 Shadowfacts. All rights reserved. +// + +import UIKit +import TuskerComponents +import GalleryVC + +class GrayscalableImageGalleryContentViewController: GalleryVC.ImageGalleryContentViewController { + private let url: URL + private let originalImage: UIImage + private let originalData: Data? + private var isGrayscale = false + + init(url: URL, caption: String?, originalData: Data?, image: UIImage, gifController: GIFController?) { + self.url = url + self.originalImage = image + self.originalData = originalData + + super.init(image: image, caption: caption, gifController: gifController) + + isGrayscale = Preferences.shared.grayscaleImages + if isGrayscale { + self.image = ImageGrayscalifier.convert(url: url, image: image) ?? image + } + + NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func preferencesChanged() { + if isGrayscale != Preferences.shared.grayscaleImages { + isGrayscale = Preferences.shared.grayscaleImages + let image = if isGrayscale { + ImageGrayscalifier.convert(url: url, image: originalImage) + } else { + originalImage + } + if let image { + self.image = image + } + } + } + + override var activityItemsForSharing: [Any] { + if let data = originalData ?? image.pngData() { + return [ImageActivityItemSource(data: data, url: url, image: image)] + } else { + return [] + } + } + +} diff --git a/Tusker/Screens/Gallery/GrayscalableVideoGalleryContentViewController.swift b/Tusker/Screens/Gallery/GrayscalableVideoGalleryContentViewController.swift new file mode 100644 index 000000000..2478501e7 --- /dev/null +++ b/Tusker/Screens/Gallery/GrayscalableVideoGalleryContentViewController.swift @@ -0,0 +1,83 @@ +// +// GrayscalableVideoGalleryContentViewController.swift +// Tusker +// +// Created by Shadowfacts on 11/21/24. +// Copyright © 2024 Shadowfacts. All rights reserved. +// + +import UIKit +import GalleryVC +import AVFoundation + +class GrayscalableVideoGalleryContentViewController: GalleryVC.VideoGalleryContentViewController { + private var audioSessionToken: AudioSessionCoordinator.Token? + private var isGrayscale: Bool + private var isFirstAppearance = true + + override init(url: URL, caption: String?) { + self.isGrayscale = Preferences.shared.grayscaleImages + + super.init(url: url, caption: caption) + + NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override class func createItem(asset: AVAsset) -> AVPlayerItem { + let item = AVPlayerItem(asset: asset) + if Preferences.shared.grayscaleImages { + #if os(visionOS) + #warning("Use async AVVideoComposition CIFilter initializer") + #else + let filter = CIFilter(name: "CIColorMonochrome")! + filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor") + filter.setValue(1.0, forKey: "inputIntensity") + + item.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in + filter.setValue(request.sourceImage, forKey: "inputImage") + request.finish(with: filter.outputImage!, context: nil) + }) + #endif + } + return item + } + + @objc private func preferencesChanged() { + if isGrayscale != Preferences.shared.grayscaleImages { + let isPlaying = player.rate > 0 + isGrayscale = Preferences.shared.grayscaleImages + replaceCurrentItem(with: Self.createItem(asset: item.asset)) + if isPlaying { + player.play() + } + } + } + + override func galleryContentDidAppear() { + super.galleryContentDidAppear() + + let wasFirstAppearance = isFirstAppearance + isFirstAppearance = false + + audioSessionToken = AudioSessionCoordinator.shared.beginPlayback(mode: .video) { + if wasFirstAppearance { + DispatchQueue.main.async { + self.player.play() + } + } + } + } + + override func galleryContentWillDisappear() { + super.galleryContentWillDisappear() + + if let audioSessionToken { + AudioSessionCoordinator.shared.endPlayback(token: audioSessionToken) + } + } + +} diff --git a/Tusker/Screens/Gallery/ImageGalleryDataSource.swift b/Tusker/Screens/Gallery/ImageGalleryDataSource.swift index 832fa5bf7..a2a87d1c0 100644 --- a/Tusker/Screens/Gallery/ImageGalleryDataSource.swift +++ b/Tusker/Screens/Gallery/ImageGalleryDataSource.swift @@ -34,7 +34,7 @@ class ImageGalleryDataSource: GalleryDataSource { } else { nil } - return ImageGalleryContentViewController( + return GrayscalableImageGalleryContentViewController( url: url, caption: nil, originalData: entry.data, @@ -52,7 +52,7 @@ class ImageGalleryDataSource: GalleryDataSource { } else { nil } - return ImageGalleryContentViewController( + return GrayscalableImageGalleryContentViewController( url: self.url, caption: nil, originalData: data, diff --git a/Tusker/Screens/Gallery/StatusAttachmentsGalleryDataSource.swift b/Tusker/Screens/Gallery/StatusAttachmentsGalleryDataSource.swift index 1d4230905..d738dc87e 100644 --- a/Tusker/Screens/Gallery/StatusAttachmentsGalleryDataSource.swift +++ b/Tusker/Screens/Gallery/StatusAttachmentsGalleryDataSource.swift @@ -33,7 +33,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource { case .image: if let view = attachmentView(for: attachment), let image = view.attachmentImage { - return ImageGalleryContentViewController( + return GrayscalableImageGalleryContentViewController( url: attachment.url, caption: attachment.description, originalData: view.originalData, @@ -49,7 +49,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource { } else { nil } - return ImageGalleryContentViewController( + return GrayscalableImageGalleryContentViewController( url: attachment.url, caption: attachment.description, originalData: entry.data, @@ -68,7 +68,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource { } else { nil } - return ImageGalleryContentViewController( + return GrayscalableImageGalleryContentViewController( url: attachment.url, caption: attachment.description, originalData: data, @@ -91,10 +91,10 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource { } return GifvGalleryContentViewController(controller: controller, url: attachment.url, caption: attachment.description) case .video: - return VideoGalleryContentViewController(url: attachment.url, caption: attachment.description) + return GrayscalableVideoGalleryContentViewController(url: attachment.url, caption: attachment.description) case .audio: // TODO: use separate content VC with audio visualization? - return VideoGalleryContentViewController(url: attachment.url, caption: attachment.description) + return GrayscalableVideoGalleryContentViewController(url: attachment.url, caption: attachment.description) case .unknown: return LoadingGalleryContentViewController(caption: nil) { do { diff --git a/Tusker/Views/Attachments/AttachmentView.swift b/Tusker/Views/Attachments/AttachmentView.swift index 5617d3fc3..0a7edb4ff 100644 --- a/Tusker/Views/Attachments/AttachmentView.swift +++ b/Tusker/Views/Attachments/AttachmentView.swift @@ -467,12 +467,12 @@ extension AttachmentView: UIContextMenuInteractionDelegate { return UIContextMenuConfiguration { [unowned self] () -> UIViewController? in if self.attachment.kind == .image, let image { - return ImageGalleryContentViewController(url: self.attachment.url, caption: nil, originalData: nil, image: image, gifController: self.gifController) + return GrayscalableImageGalleryContentViewController(url: self.attachment.url, caption: nil, originalData: nil, image: image, gifController: self.gifController) } else if self.attachment.kind == .gifv, let gifvView { return GifvGalleryContentViewController(controller: gifvView.controller, url: self.attachment.url, caption: nil) } else if self.attachment.kind == .video || self.attachment.kind == .audio { - let vc = VideoGalleryContentViewController(url: self.attachment.url, caption: nil) + let vc = GrayscalableVideoGalleryContentViewController(url: self.attachment.url, caption: nil) vc.player.isMuted = true return vc } else {