wake-ios/wake/Features/BlindBox/View/BlindOutCome.swift

393 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
struct BlindOutcomeView: View {
let media: MediaType
let time: String?
let description: String?
let isMember: Bool
// Removed presentationMode; use Router.shared.pop() for back navigation
@State private var isFullscreen = false
@State private var isPlaying = false
@State private var showControls = true
@State private var showIPListModal = false
@State private var player: AVPlayer?
init(media: MediaType, time: String? = nil, description: String? = nil, isMember: Bool = false) {
self.media = media
self.time = time
self.description = description
self.isMember = isMember
}
var body: some View {
ZStack {
Color.themeTextWhiteSecondary.ignoresSafeArea()
VStack(spacing: 0) {
//
HStack {
Button(action: {
Router.shared.pop()
}) {
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
VStack(spacing: 16) {
ZStack {
RoundedRectangle(cornerRadius: 12)
.fill(Color.white)
.shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2)
VStack(spacing: 0) {
switch media {
case .image(let uiImage):
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.cornerRadius(10)
.padding(4)
.onTapGesture {
withAnimation {
isFullscreen.toggle()
}
}
case .video(let url, _):
VideoPlayerView(url: url, isPlaying: $isPlaying, player: $player)
.frame(width: UIScreen.main.bounds.width - 40)
.background(Color.clear)
.cornerRadius(10)
.clipped()
.onAppear {
isPlaying = true
}
.onDisappear {
isPlaying = false
player?.pause()
}
.onTapGesture {
withAnimation {
showControls.toggle()
}
}
.fullScreenCover(isPresented: $isFullscreen) {
FullscreenMediaView(media: media, isPresented: $isFullscreen, isPlaying: $isPlaying, player: player)
}
}
if let description = description, !description.isEmpty {
VStack(alignment: .leading, spacing: 2) {
Text("Description")
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(.themeTextMessageMain)
Text(description)
.font(.system(size: 12))
.foregroundColor(Color.themeTextMessageMain)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal, 12)
.padding(.bottom, 12)
}
}
.padding(.top, 8)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.padding(.bottom, 20)
}
.padding(.horizontal)
Spacer()
// Button at bottom
VStack {
Spacer()
Button(action: {
if case .video = media {
withAnimation {
showIPListModal = true
}
} else {
Router.shared.navigate(to: .feedbackView)
}
}) {
Text("Continue")
.font(.headline)
.foregroundColor(.themeTextMessageMain)
.frame(maxWidth: .infinity)
.padding()
.background(Color.themePrimary)
.cornerRadius(26)
}
.padding(.horizontal)
}
.padding(.bottom, 20)
}
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.statusBar(hidden: isFullscreen)
.overlay(
JoinModal(isPresented: $showIPListModal)
)
.onDisappear {
player?.pause()
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
private let player: AVPlayer?
init(media: MediaType, isPresented: Binding<Bool>, isPlaying: Binding<Bool>, player: AVPlayer?) {
self.media = media
self._isPresented = isPresented
self._isPlaying = isPlaying
self.player = player
}
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
ZStack {
switch media {
case .image(let uiImage):
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture {
withAnimation {
showControls.toggle()
}
}
case .video(_, _):
if let player = player {
CustomVideoPlayer(player: player)
.onAppear {
player.play()
isPlaying = true
}
.onDisappear {
player.pause()
isPlaying = false
}
}
}
}
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()
}
}
.onDisappear {
player?.pause()
}
}
}
// MARK: - Video Player View
struct VideoPlayerView: UIViewRepresentable {
let url: URL
@Binding var isPlaying: Bool
@Binding var player: AVPlayer?
func makeUIView(context: Context) -> PlayerView {
let view = PlayerView()
let player = view.setupPlayer(url: url)
self.player = player
return view
}
func updateUIView(_ uiView: PlayerView, context: Context) {
if isPlaying {
uiView.play()
} else {
uiView.pause()
}
}
}
// MARK: - Custom Video Player
@available(iOS 14.0, *)
struct CustomVideoPlayer: UIViewControllerRepresentable {
let player: AVPlayer
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
controller.videoGravity = .resizeAspect
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
uiViewController.player = player
}
}
// MARK: - Player View
class PlayerView: UIView {
private var player: AVPlayer?
private var playerLayer: AVPlayerLayer?
private var playerItem: AVPlayerItem?
private var playerItemObserver: NSKeyValueObservation?
@discardableResult
func setupPlayer(url: URL) -> AVPlayer {
cleanup()
let asset = AVAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
self.playerItem = playerItem
player = AVPlayer(playerItem: playerItem)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.videoGravity = .resizeAspect
layer.addSublayer(playerLayer)
self.playerLayer = playerLayer
playerLayer.frame = bounds
NotificationCenter.default.addObserver(
self,
selector: #selector(playerItemDidReachEnd),
name: .AVPlayerItemDidPlayToEndTime,
object: playerItem
)
return player!
}
func play() {
player?.play()
}
func pause() {
player?.pause()
}
private func cleanup() {
if let playerItem = playerItem {
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: playerItem)
}
player?.pause()
player?.replaceCurrentItem(with: nil)
player = nil
playerLayer?.removeFromSuperlayer()
playerLayer = nil
playerItem?.cancelPendingSeeks()
playerItem?.asset.cancelLoading()
playerItem = nil
}
@objc private func playerItemDidReachEnd() {
player?.seek(to: .zero)
player?.play()
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer?.frame = bounds
}
deinit {
cleanup()
}
}
#if DEBUG
// MARK: - Previews
struct BlindOutcomeView_Previews: PreviewProvider {
private static func coloredImage(_ color: UIColor, size: CGSize = CGSize(width: 300, height: 300)) -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = 2
let renderer = UIGraphicsImageRenderer(size: size, format: format)
return renderer.image { ctx in
color.setFill()
ctx.fill(CGRect(origin: .zero, size: size))
}
}
static var previews: some View {
Group {
// 1
BlindOutcomeView(
media: .image(coloredImage(.systemPink)),
time: "00:23",
description: "这是一段示例描述,用于在预览中验证样式与布局。",
isMember: false
)
.previewDisplayName("Image • With Description • Guest")
// 2
BlindOutcomeView(
media: .image(coloredImage(.systemTeal)),
time: nil,
description: nil,
isMember: true
)
.previewDisplayName("Image • Minimal • Member")
}
}
}
#endif