mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Album rail in Gallery
This commit is contained in:
parent
fd424f3892
commit
84879b991d
|
@ -450,6 +450,7 @@
|
||||||
4C858A52212DC5E1001B45D3 /* UIImage+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C858A51212DC5E1001B45D3 /* UIImage+OWS.swift */; };
|
4C858A52212DC5E1001B45D3 /* UIImage+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C858A51212DC5E1001B45D3 /* UIImage+OWS.swift */; };
|
||||||
4C948FF72146EB4800349F0D /* BlockListCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C948FF62146EB4800349F0D /* BlockListCache.swift */; };
|
4C948FF72146EB4800349F0D /* BlockListCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C948FF62146EB4800349F0D /* BlockListCache.swift */; };
|
||||||
4C9CA25D217E676900607C63 /* ZXingObjC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C9CA25C217E676900607C63 /* ZXingObjC.framework */; };
|
4C9CA25D217E676900607C63 /* ZXingObjC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C9CA25C217E676900607C63 /* ZXingObjC.framework */; };
|
||||||
|
4CA46F4A219C78050038ABDE /* GalleryRailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA46F49219C78050038ABDE /* GalleryRailView.swift */; };
|
||||||
4CA5F793211E1F06008C2708 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5F792211E1F06008C2708 /* Toast.swift */; };
|
4CA5F793211E1F06008C2708 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5F792211E1F06008C2708 /* Toast.swift */; };
|
||||||
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */; };
|
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */; };
|
||||||
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; };
|
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; };
|
||||||
|
@ -1137,7 +1138,6 @@
|
||||||
4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = "<group>"; };
|
4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = "<group>"; };
|
||||||
4C1885CF218D0EA800B67051 /* ImagePickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerController.swift; sourceTree = "<group>"; };
|
4C1885CF218D0EA800B67051 /* ImagePickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerController.swift; sourceTree = "<group>"; };
|
||||||
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoGridViewCell.swift; sourceTree = "<group>"; };
|
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoGridViewCell.swift; sourceTree = "<group>"; };
|
||||||
4C1D233C218B96A000A0598F /* typing-animation.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = "typing-animation.gif"; path = "../../../../../Downloads/typing-animation.gif"; sourceTree = "<group>"; };
|
|
||||||
4C1D2333218B692800A0598F /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = translations/ko.lproj/Localizable.strings; sourceTree = "<group>"; };
|
4C1D2333218B692800A0598F /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = translations/ko.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
4C1D2334218B6A1100A0598F /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = translations/az.lproj/Localizable.strings; sourceTree = "<group>"; };
|
4C1D2334218B6A1100A0598F /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = translations/az.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
4C1D2335218B6A7600A0598F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = translations/el.lproj/Localizable.strings; sourceTree = "<group>"; };
|
4C1D2335218B6A7600A0598F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = translations/el.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
@ -1159,6 +1159,7 @@
|
||||||
4C858A51212DC5E1001B45D3 /* UIImage+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+OWS.swift"; sourceTree = "<group>"; };
|
4C858A51212DC5E1001B45D3 /* UIImage+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+OWS.swift"; sourceTree = "<group>"; };
|
||||||
4C948FF62146EB4800349F0D /* BlockListCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListCache.swift; sourceTree = "<group>"; };
|
4C948FF62146EB4800349F0D /* BlockListCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListCache.swift; sourceTree = "<group>"; };
|
||||||
4C9CA25C217E676900607C63 /* ZXingObjC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ZXingObjC.framework; path = ThirdParty/Carthage/Build/iOS/ZXingObjC.framework; sourceTree = "<group>"; };
|
4C9CA25C217E676900607C63 /* ZXingObjC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ZXingObjC.framework; path = ThirdParty/Carthage/Build/iOS/ZXingObjC.framework; sourceTree = "<group>"; };
|
||||||
|
4CA46F49219C78050038ABDE /* GalleryRailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryRailView.swift; sourceTree = "<group>"; };
|
||||||
4CA5F792211E1F06008C2708 /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = "<group>"; };
|
4CA5F792211E1F06008C2708 /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = "<group>"; };
|
||||||
4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = "<group>"; };
|
4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = "<group>"; };
|
||||||
4CB93DC12180FF07004B9764 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProximityMonitoringManager.swift; sourceTree = "<group>"; };
|
4CB93DC12180FF07004B9764 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProximityMonitoringManager.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2269,6 +2270,7 @@
|
||||||
4CA5F792211E1F06008C2708 /* Toast.swift */,
|
4CA5F792211E1F06008C2708 /* Toast.swift */,
|
||||||
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
|
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
|
||||||
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */,
|
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */,
|
||||||
|
4CA46F49219C78050038ABDE /* GalleryRailView.swift */,
|
||||||
);
|
);
|
||||||
name = Views;
|
name = Views;
|
||||||
path = views;
|
path = views;
|
||||||
|
@ -3448,6 +3450,7 @@
|
||||||
45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */,
|
45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */,
|
||||||
45F659821E1BE77000444429 /* NonCallKitCallUIAdaptee.swift in Sources */,
|
45F659821E1BE77000444429 /* NonCallKitCallUIAdaptee.swift in Sources */,
|
||||||
45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */,
|
45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */,
|
||||||
|
4CA46F4A219C78050038ABDE /* GalleryRailView.swift in Sources */,
|
||||||
34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */,
|
34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */,
|
||||||
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
|
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
|
||||||
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
|
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
|
||||||
|
|
|
@ -8,17 +8,41 @@ public enum GalleryDirection {
|
||||||
case before, after, around
|
case before, after, around
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MediaGalleryAlbum {
|
||||||
|
private(set) var items: [MediaGalleryItem]
|
||||||
|
|
||||||
|
init(items: [MediaGalleryItem]) {
|
||||||
|
self.items = items
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(item: MediaGalleryItem) {
|
||||||
|
guard !items.contains(item) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
items.append(item)
|
||||||
|
items.sort { (lhs, rhs) -> Bool in
|
||||||
|
return lhs.albumIndex < rhs.albumIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class MediaGalleryItem: Equatable, Hashable {
|
public class MediaGalleryItem: Equatable, Hashable {
|
||||||
let message: TSMessage
|
let message: TSMessage
|
||||||
let attachmentStream: TSAttachmentStream
|
let attachmentStream: TSAttachmentStream
|
||||||
let galleryDate: GalleryDate
|
let galleryDate: GalleryDate
|
||||||
let captionForDisplay: String?
|
let captionForDisplay: String?
|
||||||
|
let albumIndex: Int
|
||||||
|
var album: MediaGalleryAlbum?
|
||||||
|
let orderingKey: MediaGalleryItemOrderingKey
|
||||||
|
|
||||||
init(message: TSMessage, attachmentStream: TSAttachmentStream) {
|
init(message: TSMessage, attachmentStream: TSAttachmentStream) {
|
||||||
self.message = message
|
self.message = message
|
||||||
self.attachmentStream = attachmentStream
|
self.attachmentStream = attachmentStream
|
||||||
self.captionForDisplay = attachmentStream.caption?.filterForDisplay
|
self.captionForDisplay = attachmentStream.caption?.filterForDisplay
|
||||||
self.galleryDate = GalleryDate(message: message)
|
self.galleryDate = GalleryDate(message: message)
|
||||||
|
self.albumIndex = message.attachmentIds.index(of: attachmentStream.uniqueId!)
|
||||||
|
self.orderingKey = MediaGalleryItemOrderingKey(messageSortKey: message.timestampForSorting(), attachmentSortKey: albumIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isVideo: Bool {
|
var isVideo: Bool {
|
||||||
|
@ -33,6 +57,10 @@ public class MediaGalleryItem: Equatable, Hashable {
|
||||||
return attachmentStream.isImage
|
return attachmentStream.isImage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var imageSize: CGSize {
|
||||||
|
return attachmentStream.imageSize()
|
||||||
|
}
|
||||||
|
|
||||||
public typealias AsyncThumbnailBlock = (UIImage) -> Void
|
public typealias AsyncThumbnailBlock = (UIImage) -> Void
|
||||||
func thumbnailImage(async:@escaping AsyncThumbnailBlock) -> UIImage? {
|
func thumbnailImage(async:@escaping AsyncThumbnailBlock) -> UIImage? {
|
||||||
return attachmentStream.thumbnailImageSmall(success: async, failure: {})
|
return attachmentStream.thumbnailImageSmall(success: async, failure: {})
|
||||||
|
@ -49,6 +77,29 @@ public class MediaGalleryItem: Equatable, Hashable {
|
||||||
public var hashValue: Int {
|
public var hashValue: Int {
|
||||||
return attachmentStream.uniqueId?.hashValue ?? attachmentStream.hashValue
|
return attachmentStream.uniqueId?.hashValue ?? attachmentStream.hashValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Sorting
|
||||||
|
|
||||||
|
struct MediaGalleryItemOrderingKey: Comparable {
|
||||||
|
let messageSortKey: UInt64
|
||||||
|
let attachmentSortKey: Int
|
||||||
|
|
||||||
|
// MARK: Comparable
|
||||||
|
|
||||||
|
static func < (lhs: MediaGalleryItem.MediaGalleryItemOrderingKey, rhs: MediaGalleryItem.MediaGalleryItemOrderingKey) -> Bool {
|
||||||
|
if lhs.messageSortKey < rhs.messageSortKey {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if lhs.messageSortKey == rhs.messageSortKey {
|
||||||
|
if lhs.attachmentSortKey < rhs.attachmentSortKey {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct GalleryDate: Hashable, Comparable, Equatable {
|
public struct GalleryDate: Hashable, Comparable, Equatable {
|
||||||
|
@ -648,7 +699,27 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return MediaGalleryItem(message: message, attachmentStream: attachmentStream)
|
let galleryItem = MediaGalleryItem(message: message, attachmentStream: attachmentStream)
|
||||||
|
galleryItem.album = getAlbum(item: galleryItem)
|
||||||
|
|
||||||
|
return galleryItem
|
||||||
|
}
|
||||||
|
|
||||||
|
var galleryAlbums: [String: MediaGalleryAlbum] = [:]
|
||||||
|
func getAlbum(item: MediaGalleryItem) -> MediaGalleryAlbum? {
|
||||||
|
guard let albumMessageId = item.attachmentStream.albumMessageId else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let existingAlbum = galleryAlbums[albumMessageId] else {
|
||||||
|
let newAlbum = MediaGalleryAlbum(items: [item])
|
||||||
|
galleryAlbums[albumMessageId] = newAlbum
|
||||||
|
|
||||||
|
return newAlbum
|
||||||
|
}
|
||||||
|
|
||||||
|
existingAlbum.add(item: item)
|
||||||
|
return existingAlbum
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range instead of indexSet since it's contiguous?
|
// Range instead of indexSet since it's contiguous?
|
||||||
|
@ -760,13 +831,13 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
|
||||||
|
|
||||||
Bench(title: "sorting gallery items") {
|
Bench(title: "sorting gallery items") {
|
||||||
galleryItems.sort { lhs, rhs -> Bool in
|
galleryItems.sort { lhs, rhs -> Bool in
|
||||||
return lhs.message.timestampForSorting() < rhs.message.timestampForSorting()
|
return lhs.orderingKey < rhs.orderingKey
|
||||||
}
|
}
|
||||||
sectionDates.sort()
|
sectionDates.sort()
|
||||||
|
|
||||||
for (date, galleryItems) in sections {
|
for (date, galleryItems) in sections {
|
||||||
sortedSections[date] = galleryItems.sorted { lhs, rhs -> Bool in
|
sortedSections[date] = galleryItems.sorted { lhs, rhs -> Bool in
|
||||||
return lhs.message.timestampForSorting() < rhs.message.timestampForSorting()
|
return lhs.orderingKey < rhs.orderingKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import PromiseKit
|
||||||
|
|
||||||
// Objc wrapper for the MediaGalleryItem struct
|
// Objc wrapper for the MediaGalleryItem struct
|
||||||
@objc
|
@objc
|
||||||
|
@ -53,10 +54,11 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.updateTitle(item: item)
|
updateTitle(item: item)
|
||||||
self.updateCaption(item: item)
|
updateCaption(item: item)
|
||||||
self.setViewControllers([galleryPage], direction: direction, animated: isAnimated)
|
setViewControllers([galleryPage], direction: direction, animated: isAnimated)
|
||||||
self.updateFooterBarButtonItems(isPlayingVideo: false)
|
updateFooterBarButtonItems(isPlayingVideo: false)
|
||||||
|
updateMediaRail()
|
||||||
}
|
}
|
||||||
|
|
||||||
private let uiDatabaseConnection: YapDatabaseConnection
|
private let uiDatabaseConnection: YapDatabaseConnection
|
||||||
|
@ -108,6 +110,26 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
||||||
var currentCaptionView: CaptionView!
|
var currentCaptionView: CaptionView!
|
||||||
var pendingCaptionView: CaptionView!
|
var pendingCaptionView: CaptionView!
|
||||||
|
|
||||||
|
// MARK: ImageRail
|
||||||
|
|
||||||
|
var galleryRailView: GalleryRailView!
|
||||||
|
|
||||||
|
private func makeClearToolbar() -> UIToolbar {
|
||||||
|
let toolbar = UIToolbar()
|
||||||
|
|
||||||
|
toolbar.backgroundColor = UIColor.clear
|
||||||
|
|
||||||
|
// Making a toolbar transparent requires setting an empty uiimage
|
||||||
|
toolbar.setBackgroundImage(UIImage(), forToolbarPosition: .any, barMetrics: .default)
|
||||||
|
|
||||||
|
// hide 1px top-border
|
||||||
|
toolbar.clipsToBounds = true
|
||||||
|
|
||||||
|
return toolbar
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK:
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -120,8 +142,6 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
||||||
|
|
||||||
self.navigationItem.titleView = portraitHeaderView
|
self.navigationItem.titleView = portraitHeaderView
|
||||||
|
|
||||||
self.updateTitle()
|
|
||||||
|
|
||||||
if showAllMediaButton {
|
if showAllMediaButton {
|
||||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: MediaStrings.allMedia, style: .plain, target: self, action: #selector(didPressAllMediaButton))
|
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: MediaStrings.allMedia, style: .plain, target: self, action: #selector(didPressAllMediaButton))
|
||||||
}
|
}
|
||||||
|
@ -151,15 +171,9 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
|
|
||||||
let kFooterHeight: CGFloat = 44
|
|
||||||
|
|
||||||
view.backgroundColor = Theme.backgroundColor
|
view.backgroundColor = Theme.backgroundColor
|
||||||
|
|
||||||
let footerBar = UIToolbar()
|
|
||||||
self.footerBar = footerBar
|
|
||||||
|
|
||||||
let captionViewsContainer = UIView()
|
let captionViewsContainer = UIView()
|
||||||
|
|
||||||
captionViewsContainer.setContentHuggingHigh()
|
captionViewsContainer.setContentHuggingHigh()
|
||||||
captionViewsContainer.setCompressionResistanceHigh()
|
captionViewsContainer.setCompressionResistanceHigh()
|
||||||
|
|
||||||
|
@ -177,9 +191,24 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
||||||
pendingCaptionView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top)
|
pendingCaptionView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top)
|
||||||
pendingCaptionView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual)
|
pendingCaptionView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual)
|
||||||
|
|
||||||
|
let galleryRailView = GalleryRailView()
|
||||||
|
galleryRailView.delegate = self
|
||||||
|
galleryRailView.autoSetDimension(.height, toSize: 60)
|
||||||
|
self.galleryRailView = galleryRailView
|
||||||
|
|
||||||
|
let footerBar = self.makeClearToolbar()
|
||||||
|
self.footerBar = footerBar
|
||||||
|
|
||||||
let bottomContainer = UIView()
|
let bottomContainer = UIView()
|
||||||
self.bottomContainer = bottomContainer
|
self.bottomContainer = bottomContainer
|
||||||
let bottomStack = UIStackView(arrangedSubviews: [captionViewsContainer, footerBar])
|
|
||||||
|
let toolbarStack = UIStackView(arrangedSubviews: [galleryRailView, footerBar])
|
||||||
|
toolbarStack.axis = .vertical
|
||||||
|
let toolbarBarBlurView = UIVisualEffectView(effect: Theme.barBlurEffect)
|
||||||
|
toolbarStack.insertSubview(toolbarBarBlurView, at: 0)
|
||||||
|
toolbarBarBlurView.autoPinEdgesToSuperviewEdges()
|
||||||
|
|
||||||
|
let bottomStack = UIStackView(arrangedSubviews: [captionViewsContainer, toolbarStack])
|
||||||
bottomStack.axis = .vertical
|
bottomStack.axis = .vertical
|
||||||
bottomContainer.addSubview(bottomStack)
|
bottomContainer.addSubview(bottomStack)
|
||||||
bottomStack.autoPinEdgesToSuperviewEdges()
|
bottomStack.autoPinEdgesToSuperviewEdges()
|
||||||
|
@ -187,12 +216,15 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
||||||
self.videoPlayBarButton = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(didPressPlayBarButton))
|
self.videoPlayBarButton = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(didPressPlayBarButton))
|
||||||
self.videoPauseBarButton = UIBarButtonItem(barButtonSystemItem: .pause, target: self, action: #selector(didPressPauseBarButton))
|
self.videoPauseBarButton = UIBarButtonItem(barButtonSystemItem: .pause, target: self, action: #selector(didPressPauseBarButton))
|
||||||
|
|
||||||
self.updateFooterBarButtonItems(isPlayingVideo: true)
|
|
||||||
self.view.addSubview(bottomContainer)
|
self.view.addSubview(bottomContainer)
|
||||||
bottomContainer.autoPinWidthToSuperview()
|
bottomContainer.autoPinWidthToSuperview()
|
||||||
bottomContainer.autoPinEdge(toSuperviewEdge: .bottom)
|
bottomContainer.autoPinEdge(toSuperviewEdge: .bottom)
|
||||||
footerBar.autoPin(toBottomLayoutGuideOf: self, withInset: 0)
|
footerBar.autoPin(toBottomLayoutGuideOf: self, withInset: 0)
|
||||||
footerBar.autoSetDimension(.height, toSize: kFooterHeight)
|
footerBar.autoSetDimension(.height, toSize: 44)
|
||||||
|
|
||||||
|
updateTitle()
|
||||||
|
updateMediaRail()
|
||||||
|
updateFooterBarButtonItems(isPlayingVideo: true)
|
||||||
|
|
||||||
// Gestures
|
// Gestures
|
||||||
|
|
||||||
|
@ -309,6 +341,15 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
||||||
self.footerBar.setItems(toolbarItems, animated: false)
|
self.footerBar.setItems(toolbarItems, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateMediaRail() {
|
||||||
|
guard let currentItem = self.currentItem else {
|
||||||
|
owsFailDebug("currentItem was unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryRailView.configure(itemProvider: currentItem.album, focusedItem: currentItem)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Actions
|
// MARK: Actions
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
@ -484,6 +525,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTitle()
|
updateTitle()
|
||||||
|
updateMediaRail()
|
||||||
previousPage.zoomOut(animated: false)
|
previousPage.zoomOut(animated: false)
|
||||||
previousPage.stopAnyVideo()
|
previousPage.stopAnyVideo()
|
||||||
updateFooterBarButtonItems(isPlayingVideo: false)
|
updateFooterBarButtonItems(isPlayingVideo: false)
|
||||||
|
@ -747,6 +789,19 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MediaPageViewController: GalleryRailViewDelegate {
|
||||||
|
func galleryRailView(_ galleryRailView: GalleryRailView, didTapItem imageRailItem: GalleryRailItem) {
|
||||||
|
guard let targetItem = imageRailItem as? MediaGalleryItem else {
|
||||||
|
owsFailDebug("unexpected imageRailItem: \(imageRailItem)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let direction: NavigationDirection = currentItem.albumIndex < targetItem.albumIndex ? .forward : .reverse
|
||||||
|
|
||||||
|
self.setCurrentItem(targetItem, direction: direction, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CaptionView: UIView {
|
class CaptionView: UIView {
|
||||||
|
|
||||||
var text: String? {
|
var text: String? {
|
||||||
|
|
274
Signal/src/views/GalleryRailView.swift
Normal file
274
Signal/src/views/GalleryRailView.swift
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import PromiseKit
|
||||||
|
|
||||||
|
protocol GalleryRailItemProvider: class {
|
||||||
|
var railItems: [GalleryRailItem] { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol GalleryRailItem: class {
|
||||||
|
func getRailImage() -> Guarantee<UIImage>
|
||||||
|
var aspectRatio: CGFloat { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CGSize {
|
||||||
|
var aspectRatio: CGFloat {
|
||||||
|
guard self.height > 0 else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.width / self.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MediaGalleryItem: GalleryRailItem {
|
||||||
|
var aspectRatio: CGFloat {
|
||||||
|
return self.imageSize.aspectRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRailImage() -> Guarantee<UIImage> {
|
||||||
|
let (guarantee, fulfill) = Guarantee<UIImage>.pending()
|
||||||
|
if let image = self.thumbnailImage(async: { fulfill($0) }) {
|
||||||
|
fulfill(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
return guarantee
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MediaGalleryAlbum: GalleryRailItemProvider {
|
||||||
|
var railItems: [GalleryRailItem] {
|
||||||
|
return self.items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol GalleryRailCellViewDelegate: class {
|
||||||
|
func didTapGalleryRailCellView(_ galleryRailCellView: GalleryRailCellView)
|
||||||
|
}
|
||||||
|
|
||||||
|
class GalleryRailCellView: UIView {
|
||||||
|
|
||||||
|
weak var delegate: GalleryRailCellViewDelegate?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
layoutMargins = .zero
|
||||||
|
self.clipsToBounds = true
|
||||||
|
adjustAspectRatio(isSelected: isSelected)
|
||||||
|
addSubview(imageView)
|
||||||
|
imageView.autoPinEdgesToSuperviewMargins()
|
||||||
|
|
||||||
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap(sender:)))
|
||||||
|
addGestureRecognizer(tapGesture)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Actions
|
||||||
|
|
||||||
|
@objc
|
||||||
|
func didTap(sender: UITapGestureRecognizer) {
|
||||||
|
self.delegate?.didTapGalleryRailCellView(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK:
|
||||||
|
|
||||||
|
var item: GalleryRailItem?
|
||||||
|
|
||||||
|
func configure(item: GalleryRailItem, delegate: GalleryRailCellViewDelegate) {
|
||||||
|
self.item = item
|
||||||
|
self.delegate = delegate
|
||||||
|
|
||||||
|
item.getRailImage().done { image in
|
||||||
|
guard self.item === item else { return }
|
||||||
|
|
||||||
|
self.imageView.image = image
|
||||||
|
}.retainUntilComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Selected
|
||||||
|
|
||||||
|
private(set) var isSelected: Bool = false
|
||||||
|
|
||||||
|
func setIsSelected(_ isSelected: Bool) {
|
||||||
|
self.isSelected = isSelected
|
||||||
|
adjustAspectRatio(isSelected: isSelected)
|
||||||
|
if isSelected {
|
||||||
|
self.layoutMargins = UIEdgeInsets(top: 0, left: 3, bottom: 0, right: 3)
|
||||||
|
} else {
|
||||||
|
self.layoutMargins = .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Subview Helpers
|
||||||
|
|
||||||
|
var aspectRatioConstraint: NSLayoutConstraint?
|
||||||
|
func adjustAspectRatio(isSelected: Bool) {
|
||||||
|
if let oldConstraint = aspectRatioConstraint {
|
||||||
|
NSLayoutConstraint.deactivate([oldConstraint])
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSelected, let itemAspectRatio = item?.aspectRatio {
|
||||||
|
aspectRatioConstraint = imageView.autoPin(toAspectRatio: itemAspectRatio)
|
||||||
|
} else {
|
||||||
|
// Portrait mode AR by default
|
||||||
|
let kDefaultAspectRatio: CGFloat = 9.0 / 16.0
|
||||||
|
aspectRatioConstraint = imageView.autoPin(toAspectRatio: kDefaultAspectRatio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let imageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol GalleryRailViewDelegate: class {
|
||||||
|
func galleryRailView(_ galleryRailView: GalleryRailView, didTapItem imageRailItem: GalleryRailItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
class GalleryRailView: UIView, GalleryRailCellViewDelegate {
|
||||||
|
|
||||||
|
weak var delegate: GalleryRailViewDelegate?
|
||||||
|
|
||||||
|
// MARK: Initializers
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
addSubview(scrollView)
|
||||||
|
scrollView.layoutMargins = .zero
|
||||||
|
scrollView.autoPinEdgesToSuperviewMargins()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Public
|
||||||
|
|
||||||
|
public func configure(itemProvider: GalleryRailItemProvider?, focusedItem: GalleryRailItem?) {
|
||||||
|
let animationDuration: TimeInterval = 0.2
|
||||||
|
|
||||||
|
guard let itemProvider = itemProvider else {
|
||||||
|
UIView.animate(withDuration: animationDuration) {
|
||||||
|
self.isHidden = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let areRailItemsIdentical = { (lhs: [GalleryRailItem], rhs: [GalleryRailItem]) -> Bool in
|
||||||
|
guard lhs.count == rhs.count else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (index, element) in lhs.enumerated() {
|
||||||
|
guard element === rhs[index] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemProvider === self.itemProvider, areRailItemsIdentical(itemProvider.railItems, self.cellViewItems) {
|
||||||
|
UIView.animate(withDuration: animationDuration) {
|
||||||
|
self.updateFocusedItem(focusedItem)
|
||||||
|
self.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.itemProvider = itemProvider
|
||||||
|
scrollView.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
|
||||||
|
guard itemProvider.railItems.count > 1 else {
|
||||||
|
UIView.animate(withDuration: animationDuration) {
|
||||||
|
self.isHidden = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UIView.animate(withDuration: animationDuration) {
|
||||||
|
self.isHidden = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let cellViews = buildCellViews(items: itemProvider.railItems)
|
||||||
|
self.cellViews = cellViews
|
||||||
|
let stackView = UIStackView(arrangedSubviews: cellViews)
|
||||||
|
stackView.axis = .horizontal
|
||||||
|
stackView.spacing = 4
|
||||||
|
|
||||||
|
scrollView.addSubview(stackView)
|
||||||
|
stackView.autoPinEdgesToSuperviewEdges()
|
||||||
|
stackView.autoMatch(.height, to: .height, of: scrollView)
|
||||||
|
|
||||||
|
updateFocusedItem(focusedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: GalleryRailCellViewDelegate
|
||||||
|
|
||||||
|
func didTapGalleryRailCellView(_ galleryRailCellView: GalleryRailCellView) {
|
||||||
|
guard let item = galleryRailCellView.item else {
|
||||||
|
owsFailDebug("item was unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate?.galleryRailView(self, didTapItem: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Subview Helpers
|
||||||
|
|
||||||
|
private var itemProvider: GalleryRailItemProvider?
|
||||||
|
|
||||||
|
private let scrollView: UIScrollView = {
|
||||||
|
let scrollView = UIScrollView()
|
||||||
|
scrollView.isScrollEnabled = true
|
||||||
|
return scrollView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private func buildCellViews(items: [GalleryRailItem]) -> [GalleryRailCellView] {
|
||||||
|
return items.map { item in
|
||||||
|
let cellView = GalleryRailCellView()
|
||||||
|
cellView.configure(item: item, delegate: self)
|
||||||
|
return cellView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cellViews: [GalleryRailCellView] = []
|
||||||
|
var cellViewItems: [GalleryRailItem] {
|
||||||
|
get { return cellViews.compactMap { $0.item } }
|
||||||
|
}
|
||||||
|
func updateFocusedItem(_ focusedItem: GalleryRailItem?) {
|
||||||
|
var selectedCellView: GalleryRailCellView?
|
||||||
|
cellViews.forEach { cellView in
|
||||||
|
if cellView.item === focusedItem {
|
||||||
|
assert(selectedCellView == nil)
|
||||||
|
selectedCellView = cellView
|
||||||
|
cellView.setIsSelected(true)
|
||||||
|
} else {
|
||||||
|
cellView.setIsSelected(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.layoutIfNeeded()
|
||||||
|
guard let selectedCell = selectedCellView else {
|
||||||
|
owsFailDebug("selectedCell was unexpectedly nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let cellViewCenter = selectedCell.superview!.convert(selectedCell.center, to: scrollView)
|
||||||
|
let additionalInset = scrollView.center.x - cellViewCenter.x
|
||||||
|
|
||||||
|
var inset = scrollView.contentInset
|
||||||
|
inset.left = additionalInset
|
||||||
|
scrollView.contentInset = inset
|
||||||
|
|
||||||
|
var offset = scrollView.contentOffset
|
||||||
|
offset.x = -additionalInset
|
||||||
|
scrollView.contentOffset = offset
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue