wake-ios/wake/View/Blind/BlindOutCome.swift
2025-08-29 15:27:02 +08:00

362 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
@State private var showIPListModal = false
var body: some View {
NavigationView {
ZStack {
Color.themeTextWhiteSecondary.ignoresSafeArea()
VStack(spacing: 0) {
//
HStack {
Button(action: {
//
presentationMode.wrappedValue.dismiss()
}) {
HStack(spacing: 4) {
Image(systemName: "chevron.left")
.font(.headline)
}
.foregroundColor(Color.themeTextMessageMain)
}
.padding(.leading, 16)
Spacer()
Text("Blind Box")
.font(.headline)
.foregroundColor(Color.themeTextMessageMain)
Spacer()
//
HStack(spacing: 4) {
Image(systemName: "chevron.left")
.opacity(0)
}
.padding(.trailing, 16)
}
.padding(.vertical, 12)
.background(Color.themeTextWhiteSecondary)
.zIndex(1) //
Spacer()
.frame(height: 30)
// Media content
GeometryReader { geometry in
ZStack {
//
RoundedRectangle(cornerRadius: 12)
.fill(Color.white)
.shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2)
switch media {
case .image(let uiImage):
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.cornerRadius(10)
.padding(4)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.onTapGesture {
withAnimation {
isFullscreen.toggle()
}
}
case .video(let url, _):
// Create an AVPlayer with the video URL
let player = AVPlayer(url: url)
VideoPlayer(player: player)
.cornerRadius(10)
.padding(4)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.onAppear {
player.play()
isPlaying = true
}
.onDisappear {
player.pause()
isPlaying = false
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.padding(.bottom, 20)
}
.frame(height: UIScreen.main.bounds.height / 2)
.padding(.horizontal)
Spacer()
// Button below media
VStack(spacing: 16) {
Button(action: {
// video
if case .video = media {
withAnimation {
showIPListModal = true
}
} else {
Router.shared.navigate(to: .mediaUpload)
}
}) {
Text("Continue")
.font(.headline)
.foregroundColor(.themeTextMessageMain)
.frame(maxWidth: .infinity)
.padding()
.background(Color.themePrimary)
.cornerRadius(26)
}
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.navigationBarHidden(true) //
.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)
}
}
.overlay(
JoinModal(isPresented: $showIPListModal)
)
}
.navigationViewStyle(StackNavigationViewStyle()) // iPad
.navigationBarHidden(true) //
}
}
// 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))
}
}
}