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

305 lines
10 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 {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
VStack(spacing: 0) {
// Custom navigation header
HStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "chevron.left")
.font(.title2)
.foregroundColor(.white)
.padding()
}
Spacer()
Text("Blind Box")
.font(.headline)
.foregroundColor(.white)
Spacer()
// Invisible view to balance the layout
Image(systemName: "chevron.left")
.font(.title2)
.foregroundColor(.clear)
.padding()
}
.background(Color.black)
// 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, maxHeight: .infinity)
.background(Color.black)
}
}
.navigationBarHidden(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))
}
}
}