mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
226 lines
7.3 KiB
Swift
226 lines
7.3 KiB
Swift
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import SwiftUI
|
|
|
|
public struct SessionCarouselView_SwiftUI: View {
|
|
@Binding var index: Int
|
|
let isOutgoing: Bool
|
|
var contentInfos: [Attachment]
|
|
let numberOfPages: Int
|
|
|
|
public init(index: Binding<Int>, isOutgoing: Bool, contentInfos: [Attachment]) {
|
|
self._index = index
|
|
self.isOutgoing = isOutgoing
|
|
self.contentInfos = contentInfos
|
|
self.numberOfPages = contentInfos.count
|
|
|
|
let first = self.contentInfos.first!
|
|
let last = self.contentInfos.last!
|
|
self.contentInfos.append(first)
|
|
self.contentInfos.insert(last, at: 0)
|
|
}
|
|
|
|
public var body: some View {
|
|
HStack(spacing: 0) {
|
|
ArrowView(index: $index, numberOfPages: numberOfPages, type: .decrement)
|
|
.zIndex(1)
|
|
|
|
PageView(index: $index, numberOfPages: self.numberOfPages) {
|
|
ForEach(self.contentInfos) { attachment in
|
|
MediaView_SwiftUI(
|
|
attachment: attachment,
|
|
isOutgoing: self.isOutgoing,
|
|
shouldSupressControls: true,
|
|
cornerRadius: 0
|
|
)
|
|
}
|
|
}
|
|
.aspectRatio(1, contentMode: .fit)
|
|
|
|
ArrowView(index: $index, numberOfPages: numberOfPages, type: .increment)
|
|
.zIndex(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ArrowView: View {
|
|
@Binding var index: Int
|
|
let numberOfPages: Int
|
|
let maxIndex: Int
|
|
let type: ArrowType
|
|
|
|
enum ArrowType {
|
|
case increment
|
|
case decrement
|
|
}
|
|
|
|
init(index: Binding<Int>, numberOfPages: Int, type: ArrowType) {
|
|
self._index = index
|
|
self.numberOfPages = numberOfPages
|
|
self.maxIndex = numberOfPages + 1
|
|
self.type = type
|
|
}
|
|
|
|
var body: some View {
|
|
let imageName = self.type == .decrement ? "chevron.left" : "chevron.right"
|
|
Button {
|
|
if self.type == .decrement {
|
|
decrement()
|
|
} else {
|
|
increment()
|
|
}
|
|
} label: {
|
|
Image(systemName: imageName)
|
|
.font(.system(size: 20))
|
|
.foregroundColor(themeColor: .textPrimary)
|
|
.frame(width: 30, height: 30)
|
|
}
|
|
}
|
|
|
|
func decrement() {
|
|
withAnimation(.easeOut) {
|
|
self.index -= 1
|
|
}
|
|
|
|
if self.index == 0 {
|
|
self.index = self.maxIndex - 1
|
|
}
|
|
}
|
|
|
|
func increment() {
|
|
withAnimation(.easeOut) {
|
|
self.index += 1
|
|
}
|
|
|
|
if self.index == self.maxIndex {
|
|
self.index = 1
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PageView<Content>: View where Content: View {
|
|
@Binding var index: Int
|
|
let numberOfPages: Int
|
|
let maxIndex: Int
|
|
let content: () -> Content
|
|
|
|
@State private var offset = CGFloat.zero
|
|
@State private var dragging = false
|
|
|
|
init(index: Binding<Int>, numberOfPages: Int, @ViewBuilder content: @escaping () -> Content) {
|
|
self._index = index
|
|
self.numberOfPages = numberOfPages
|
|
self.content = content
|
|
self.maxIndex = numberOfPages + 1
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack(alignment: .bottom) {
|
|
GeometryReader { geometry in
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 0) {
|
|
self.content()
|
|
.frame(width: geometry.size.width, height: geometry.size.height)
|
|
}
|
|
}
|
|
.content.offset(x: self.offset(in: geometry), y: 0)
|
|
.frame(width: geometry.size.width, alignment: .leading)
|
|
.clipShape(RoundedRectangle(cornerRadius: 15))
|
|
.gesture(
|
|
DragGesture(coordinateSpace: .local)
|
|
.onChanged { value in
|
|
self.dragging = true
|
|
self.offset = -CGFloat(self.index) * geometry.size.width + value.translation.width
|
|
}
|
|
.onEnded { value in
|
|
let predictedEndOffset = -CGFloat(self.index) * geometry.size.width + value.predictedEndTranslation.width
|
|
let predictedIndex = Int(round(predictedEndOffset / -geometry.size.width))
|
|
self.index = self.clampedIndex(from: predictedIndex)
|
|
withAnimation(.easeOut(duration: 0.2)) {
|
|
self.dragging = false
|
|
}
|
|
// FIXME: This is a workaround for withAnimation() not having completion callback
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
switch self.index {
|
|
case 0: self.index = self.maxIndex - 1
|
|
case self.maxIndex: self.index = 1
|
|
default: break
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
.clipped()
|
|
|
|
PageControl(index: $index, maxIndex: numberOfPages - 1)
|
|
.padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 0))
|
|
}
|
|
}
|
|
|
|
func offset(in geometry: GeometryProxy) -> CGFloat {
|
|
if self.dragging {
|
|
return max(min(self.offset, 0), -CGFloat(self.maxIndex) * geometry.size.width)
|
|
} else {
|
|
return -CGFloat(self.index) * geometry.size.width
|
|
}
|
|
}
|
|
|
|
func clampedIndex(from predictedIndex: Int) -> Int {
|
|
let newIndex = min(max(predictedIndex, self.index - 1), self.index + 1)
|
|
guard newIndex >= 0 else { return 0 }
|
|
guard newIndex <= maxIndex else { return maxIndex }
|
|
return newIndex
|
|
}
|
|
}
|
|
|
|
struct PageControl: View {
|
|
@Binding var index: Int
|
|
let maxIndex: Int
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Capsule()
|
|
.foregroundColor(.init(white: 0, opacity: 0.4))
|
|
HStack(spacing: 4) {
|
|
ForEach(0...maxIndex, id: \.self) { index in
|
|
Circle()
|
|
.fill(index == ((self.index - 1) % (self.maxIndex + 1)) ? Color.white : Color.gray)
|
|
.frame(width: 7, height: 7)
|
|
}
|
|
}
|
|
.padding(6)
|
|
}
|
|
.fixedSize(horizontal: true, vertical: true)
|
|
.frame(
|
|
maxWidth: .infinity,
|
|
maxHeight: 19
|
|
)
|
|
}
|
|
}
|
|
|
|
struct SessionCarouselView_SwiftUI_Previews: PreviewProvider {
|
|
@State static var index = 1
|
|
static var previews: some View {
|
|
ZStack {
|
|
Color.black.ignoresSafeArea()
|
|
|
|
SessionCarouselView_SwiftUI(
|
|
index: $index,
|
|
isOutgoing: true,
|
|
contentInfos: [
|
|
Attachment(
|
|
variant: .standard,
|
|
contentType: "jpeg",
|
|
byteCount: 100
|
|
),
|
|
Attachment(
|
|
variant: .standard,
|
|
contentType: "jpeg",
|
|
byteCount: 100
|
|
)
|
|
]
|
|
)
|
|
}
|
|
}
|
|
}
|