350 lines
13 KiB
Swift
350 lines
13 KiB
Swift
import SwiftUI
|
|
import AVKit
|
|
import os.log
|
|
|
|
/// A view that displays either an image or a video with fullscreen support
|
|
struct BlindOutcomeView: View {
|
|
let media: MediaType
|
|
@Environment(\.presentationMode) var presentationMode
|
|
@State private var isFullscreen = false
|
|
@State private var isPlaying = false
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
ZStack {
|
|
Color.themeTextWhiteSecondary.ignoresSafeArea()
|
|
|
|
VStack(spacing: 0) {
|
|
// Custom navigation header
|
|
HStack {
|
|
Button(action: {
|
|
// Pop two view controllers to go back two levels
|
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
let window = windowScene.windows.first,
|
|
let rootVC = window.rootViewController as? UINavigationController {
|
|
let viewControllers = rootVC.viewControllers
|
|
if viewControllers.count > 2 {
|
|
let destinationVC = viewControllers[viewControllers.count - 3]
|
|
rootVC.popToViewController(destinationVC, animated: true)
|
|
} else {
|
|
presentationMode.wrappedValue.dismiss()
|
|
}
|
|
} else {
|
|
presentationMode.wrappedValue.dismiss()
|
|
}
|
|
}) {
|
|
Image(systemName: "chevron.left")
|
|
.font(.headline)
|
|
.foregroundColor(Color.themeTextMessageMain)
|
|
}
|
|
Spacer()
|
|
Text("Blind Box")
|
|
.font(.headline)
|
|
.foregroundColor(Color.themeTextMessageMain)
|
|
Spacer()
|
|
// Invisible spacer to balance the layout
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "chevron.left")
|
|
.opacity(0)
|
|
Text("Back")
|
|
.opacity(0)
|
|
}
|
|
.padding(.trailing, 8)
|
|
}
|
|
.padding(.vertical, 8)
|
|
.background(Color.themeTextWhiteSecondary)
|
|
.overlay(
|
|
Rectangle()
|
|
.frame(height: 1)
|
|
.foregroundColor(Color.gray.opacity(0.3)),
|
|
alignment: .bottom
|
|
)
|
|
Spacer()
|
|
// Media content
|
|
ZStack {
|
|
switch media {
|
|
case .image(let uiImage):
|
|
Image(uiImage: uiImage)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.onTapGesture {
|
|
withAnimation {
|
|
isFullscreen.toggle()
|
|
}
|
|
}
|
|
|
|
case .video(let url, _):
|
|
// Create an AVPlayer with the video URL
|
|
let player = AVPlayer(url: url)
|
|
VideoPlayer(player: player)
|
|
.onAppear {
|
|
player.play()
|
|
isPlaying = true
|
|
}
|
|
.onDisappear {
|
|
player.pause()
|
|
isPlaying = false
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: UIScreen.main.bounds.height * 0.4) // Takes 40% of screen height
|
|
.background(Color.black)
|
|
.padding() // Add some space below navigation bar
|
|
Spacer()
|
|
// Button below media
|
|
VStack(spacing: 16) {
|
|
Button(action: {
|
|
// Navigate to the desired view
|
|
// Replace with your navigation logic
|
|
// print("Button tapped!")
|
|
Router.shared.navigate(to: .mediaUpload)
|
|
}) {
|
|
Text("Go to Next View")
|
|
.font(.headline)
|
|
.foregroundColor(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(Color.blue)
|
|
.cornerRadius(10)
|
|
}
|
|
.padding(.horizontal, 40)
|
|
.padding(.top, 30)
|
|
|
|
Spacer() // Push everything to the top
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
.navigationBarBackButtonHidden(true)
|
|
.statusBar(hidden: isFullscreen)
|
|
.fullScreenCover(isPresented: $isFullscreen) {
|
|
if case .video(let url, _) = media {
|
|
let player = AVPlayer(url: url)
|
|
FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: player)
|
|
} else {
|
|
FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Fullscreen Media View
|
|
private struct FullscreenMediaView: View {
|
|
let media: MediaType
|
|
@Binding var isPresented: Bool
|
|
@Binding var isPlaying: Bool
|
|
@State private var showControls = true
|
|
let player: AVPlayer?
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.black.edgesIgnoringSafeArea(.all)
|
|
|
|
// Media content
|
|
ZStack {
|
|
switch media {
|
|
case .image(let uiImage):
|
|
Image(uiImage: uiImage)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.onTapGesture {
|
|
withAnimation {
|
|
showControls.toggle()
|
|
}
|
|
}
|
|
|
|
case .video(_, _):
|
|
if let player = player {
|
|
VideoPlayer(player: player)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.onTapGesture {
|
|
withAnimation {
|
|
showControls.toggle()
|
|
}
|
|
}
|
|
.overlay(
|
|
showControls ? VideoControls(
|
|
isPlaying: $isPlaying,
|
|
player: player,
|
|
onClose: { isPresented = false }
|
|
) : nil
|
|
)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
// Close button (always visible)
|
|
VStack {
|
|
HStack {
|
|
Button(action: { isPresented = false }) {
|
|
Image(systemName: "xmark")
|
|
.font(.title2)
|
|
.foregroundColor(.white)
|
|
.padding()
|
|
.background(Color.black.opacity(0.5))
|
|
.clipShape(Circle())
|
|
}
|
|
.padding()
|
|
Spacer()
|
|
}
|
|
Spacer()
|
|
}
|
|
}
|
|
.onAppear {
|
|
if case .video = media {
|
|
player?.play()
|
|
isPlaying = true
|
|
}
|
|
}
|
|
.onDisappear {
|
|
if case .video = media {
|
|
player?.pause()
|
|
isPlaying = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Video Controls
|
|
private struct VideoControls: View {
|
|
@Binding var isPlaying: Bool
|
|
let player: AVPlayer
|
|
let onClose: () -> Void
|
|
@State private var currentTime: Double = 0
|
|
@State private var duration: Double = 0
|
|
@State private var isSeeking = false
|
|
|
|
private let timeFormatter: DateComponentsFormatter = {
|
|
let formatter = DateComponentsFormatter()
|
|
formatter.allowedUnits = [.minute, .second]
|
|
formatter.zeroFormattingBehavior = .pad
|
|
formatter.unitsStyle = .positional
|
|
return formatter
|
|
}()
|
|
|
|
var body: some View {
|
|
VStack {
|
|
Spacer()
|
|
|
|
HStack(spacing: 20) {
|
|
// Play/Pause button
|
|
Button(action: togglePlayPause) {
|
|
Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill")
|
|
.font(.system(size: 30))
|
|
.foregroundColor(.white)
|
|
}
|
|
|
|
// Time slider
|
|
VStack {
|
|
Slider(
|
|
value: $currentTime,
|
|
in: 0...max(duration, 1),
|
|
onEditingChanged: { editing in
|
|
isSeeking = editing
|
|
if !editing {
|
|
let targetTime = CMTime(seconds: currentTime, preferredTimescale: 1)
|
|
player.seek(to: targetTime)
|
|
}
|
|
}
|
|
)
|
|
.accentColor(.white)
|
|
|
|
HStack {
|
|
Text(timeString(from: currentTime))
|
|
.font(.caption)
|
|
.foregroundColor(.white)
|
|
|
|
Spacer()
|
|
|
|
Text(timeString(from: duration))
|
|
.font(.caption)
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
|
|
// Fullscreen button
|
|
Button(action: onClose) {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.system(size: 20))
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
LinearGradient(
|
|
gradient: Gradient(colors: [Color.clear, Color.black.opacity(0.7)]),
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
)
|
|
}
|
|
.onAppear(perform: setupPlayerObservers)
|
|
.onDisappear(perform: removePlayerObservers)
|
|
}
|
|
|
|
private func togglePlayPause() {
|
|
if isPlaying {
|
|
player.pause()
|
|
} else {
|
|
player.play()
|
|
}
|
|
isPlaying.toggle()
|
|
}
|
|
|
|
private func timeString(from seconds: Double) -> String {
|
|
guard !seconds.isNaN && !seconds.isInfinite else { return "--:--" }
|
|
return timeFormatter.string(from: seconds) ?? "--:--"
|
|
}
|
|
|
|
private func setupPlayerObservers() {
|
|
// Add time observer to update slider
|
|
let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
_ = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [self] time in
|
|
guard !isSeeking else { return }
|
|
currentTime = time.seconds
|
|
|
|
// Update duration if needed
|
|
if let duration = player.currentItem?.duration.seconds, duration > 0 {
|
|
self.duration = duration
|
|
}
|
|
}
|
|
|
|
// Add observer for when the video ends
|
|
NotificationCenter.default.addObserver(
|
|
forName: .AVPlayerItemDidPlayToEndTime,
|
|
object: player.currentItem,
|
|
queue: .main
|
|
) { [self] _ in
|
|
// Loop the video
|
|
player.seek(to: .zero)
|
|
player.play()
|
|
isPlaying = true
|
|
}
|
|
}
|
|
|
|
private func removePlayerObservers() {
|
|
// Remove all observers when the view disappears
|
|
NotificationCenter.default.removeObserver(
|
|
self,
|
|
name: .AVPlayerItemDidPlayToEndTime,
|
|
object: player.currentItem
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
struct BlindOutcomeView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
// Preview with image
|
|
BlindOutcomeView(media: .image(UIImage(systemName: "photo")!))
|
|
|
|
// Preview with video
|
|
if let url = URL(string: "https://example.com/sample.mp4") {
|
|
BlindOutcomeView(media: .video(url, nil))
|
|
}
|
|
}
|
|
}
|