feat: 页面暂调
This commit is contained in:
parent
0ef80ca913
commit
963c87a9b9
@ -1,5 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftData
|
import SwiftData
|
||||||
|
import AVKit
|
||||||
|
|
||||||
// MARK: - 自定义过渡动画
|
// MARK: - 自定义过渡动画
|
||||||
extension AnyTransition {
|
extension AnyTransition {
|
||||||
@ -12,6 +13,70 @@ extension AnyTransition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Video Player View
|
||||||
|
struct VideoPlayerView: View {
|
||||||
|
let player: AVPlayer
|
||||||
|
@Binding var isFullscreen: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
VideoPlayer(player: player)
|
||||||
|
.frame(width: 300, height: 200)
|
||||||
|
.cornerRadius(12)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 全屏按钮
|
||||||
|
Button(action: {
|
||||||
|
isFullscreen = true
|
||||||
|
player.play()
|
||||||
|
}) {
|
||||||
|
Image(systemName: "arrow.up.left.and.arrow.down.right")
|
||||||
|
.font(.system(size: 20))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(8)
|
||||||
|
.background(Color.black.opacity(0.6))
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
}
|
||||||
|
.frame(width: 300, height: 200)
|
||||||
|
.onTapGesture {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
.fullScreenCover(isPresented: $isFullscreen) {
|
||||||
|
ZStack(alignment: .topLeading) {
|
||||||
|
// 全屏视频播放器
|
||||||
|
VideoPlayer(player: player)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
.onAppear { player.play() }
|
||||||
|
|
||||||
|
// 关闭按钮
|
||||||
|
Button(action: {
|
||||||
|
isFullscreen = false
|
||||||
|
player.pause()
|
||||||
|
}) {
|
||||||
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding()
|
||||||
|
.background(Color.black.opacity(0.4))
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
.padding(.top, 50)
|
||||||
|
.padding(.leading, 20)
|
||||||
|
}
|
||||||
|
.background(Color.black.edgesIgnoringSafeArea(.all))
|
||||||
|
.statusBar(hidden: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 主视图
|
// MARK: - 主视图
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
// MARK: - 状态属性
|
// MARK: - 状态属性
|
||||||
@ -20,6 +85,9 @@ struct ContentView: View {
|
|||||||
@State private var contentOffset: CGFloat = 0 // 内容偏移量
|
@State private var contentOffset: CGFloat = 0 // 内容偏移量
|
||||||
@State private var showLogin = false
|
@State private var showLogin = false
|
||||||
@State private var animateGradient = false
|
@State private var animateGradient = false
|
||||||
|
@State private var showLottieAnimation = true // 控制Lottie动画显示
|
||||||
|
@State private var showVideoPlayer = false // 控制视频播放器显示
|
||||||
|
@State private var isVideoFullscreen = false // 控制视频全屏状态
|
||||||
|
|
||||||
let timer = Timer.publish(every: 0.02, on: .main, in: .common).autoconnect()
|
let timer = Timer.publish(every: 0.02, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
@ -29,6 +97,19 @@ struct ContentView: View {
|
|||||||
// 查询数据 - 简单查询
|
// 查询数据 - 简单查询
|
||||||
@Query private var login: [Login]
|
@Query private var login: [Login]
|
||||||
|
|
||||||
|
// 视频播放器
|
||||||
|
private let player: AVPlayer?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// 使用远程视频URL
|
||||||
|
if let videoURL = URL(string: "https://cdn.fairclip.cn/files/7342843896868769793/飞书20250617-144935.mp4") {
|
||||||
|
self.player = AVPlayer(url: videoURL)
|
||||||
|
} else {
|
||||||
|
self.player = nil
|
||||||
|
print("Error: Invalid video URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 主体视图
|
// MARK: - 主体视图
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
@ -112,10 +193,29 @@ struct ContentView: View {
|
|||||||
y: UIScreen.main.bounds.height * 0.325)
|
y: UIScreen.main.bounds.height * 0.325)
|
||||||
|
|
||||||
// 2. Lottie动画层
|
// 2. Lottie动画层
|
||||||
LottieView(name: "loading", loopMode: .loop)
|
if showLottieAnimation {
|
||||||
.frame(width: 200, height: 200)
|
LottieView(name: "loading", loopMode: .loop)
|
||||||
.position(x: UIScreen.main.bounds.width / 2,
|
.frame(width: 200, height: 200)
|
||||||
y: UIScreen.main.bounds.height * 0.325)
|
.position(x: UIScreen.main.bounds.width / 2,
|
||||||
|
y: UIScreen.main.bounds.height * 0.325)
|
||||||
|
.onAppear {
|
||||||
|
// 5秒后隐藏Lottie动画并显示视频
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
|
||||||
|
withAnimation {
|
||||||
|
showLottieAnimation = false
|
||||||
|
showVideoPlayer = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 视频播放器
|
||||||
|
if showVideoPlayer, let player = player {
|
||||||
|
VideoPlayerView(player: player, isFullscreen: $isVideoFullscreen)
|
||||||
|
} else if showVideoPlayer {
|
||||||
|
Text("Video not found")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(
|
.frame(
|
||||||
maxWidth: .infinity,
|
maxWidth: .infinity,
|
||||||
|
|||||||
46
wake/Models/MediaType.swift
Normal file
46
wake/Models/MediaType.swift
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import AVKit
|
||||||
|
|
||||||
|
/// Represents different types of media that can be displayed or processed
|
||||||
|
public enum MediaType: Equatable, Hashable {
|
||||||
|
case image(UIImage)
|
||||||
|
case video(URL, UIImage?) // URL is the video URL, UIImage is the thumbnail
|
||||||
|
|
||||||
|
public var thumbnail: UIImage? {
|
||||||
|
switch self {
|
||||||
|
case .image(let image):
|
||||||
|
return image
|
||||||
|
case .video(_, let thumbnail):
|
||||||
|
return thumbnail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isVideo: Bool {
|
||||||
|
if case .video = self {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func == (lhs: MediaType, rhs: MediaType) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.image(let lhsImage), .image(let rhsImage)):
|
||||||
|
return lhsImage.pngData() == rhsImage.pngData()
|
||||||
|
case (.video(let lhsURL, _), .video(let rhsURL, _)):
|
||||||
|
return lhsURL == rhsURL
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
switch self {
|
||||||
|
case .image(let image):
|
||||||
|
hasher.combine("image")
|
||||||
|
hasher.combine(image.pngData())
|
||||||
|
case .video(let url, _):
|
||||||
|
hasher.combine("video")
|
||||||
|
hasher.combine(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ import Foundation
|
|||||||
/// API 配置信息
|
/// API 配置信息
|
||||||
public enum APIConfig {
|
public enum APIConfig {
|
||||||
/// API 基础 URL
|
/// API 基础 URL
|
||||||
public static let baseURL = "https://api.memorywake.com/api/v1"
|
public static let baseURL = "https://api-dev.memorywake.com:31274/api/v1"
|
||||||
|
|
||||||
/// 认证 token - 从 Keychain 中获取
|
/// 认证 token - 从 Keychain 中获取
|
||||||
public static var authToken: String {
|
public static var authToken: String {
|
||||||
|
|||||||
@ -6,7 +6,8 @@ enum AppRoute: Hashable {
|
|||||||
case feedbackView
|
case feedbackView
|
||||||
case feedbackDetail(type: FeedbackView.FeedbackType)
|
case feedbackDetail(type: FeedbackView.FeedbackType)
|
||||||
case mediaUpload
|
case mediaUpload
|
||||||
// Add other routes here as needed
|
case blindBox
|
||||||
|
case blindOutcome(media: MediaType)
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var view: some View {
|
var view: some View {
|
||||||
@ -19,6 +20,10 @@ enum AppRoute: Hashable {
|
|||||||
FeedbackDetailView(feedbackType: type)
|
FeedbackDetailView(feedbackType: type)
|
||||||
case .mediaUpload:
|
case .mediaUpload:
|
||||||
MediaUploadView()
|
MediaUploadView()
|
||||||
|
case .blindBox:
|
||||||
|
BlindBoxView()
|
||||||
|
case .blindOutcome(let media):
|
||||||
|
BlindOutcomeView(media: media)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
86
wake/View/Blind/BlindBox.swift
Normal file
86
wake/View/Blind/BlindBox.swift
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
import AVKit
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static let navigateToMediaViewer = Notification.Name("navigateToMediaViewer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 主视图
|
||||||
|
struct BlindBoxView: View {
|
||||||
|
@State private var showLottieAnimation = true // 控制Lottie动画显示
|
||||||
|
|
||||||
|
// MARK: - 主体视图
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
// 全局背景颜色背景色
|
||||||
|
Color.themeTextWhiteSecondary.ignoresSafeArea()
|
||||||
|
|
||||||
|
// 主内容区域
|
||||||
|
VStack {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
// 标题
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Hi! Click And")
|
||||||
|
Text("Open Your First Box~")
|
||||||
|
}
|
||||||
|
.font(Typography.font(for: .smallLargeTitle))
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.foregroundColor(Color.themeTextMessageMain)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.horizontal)
|
||||||
|
// 盲盒
|
||||||
|
// 添加SVG背景图片
|
||||||
|
ZStack {
|
||||||
|
// 1. 背景SVG
|
||||||
|
SVGImage(svgName: "BlindBg")
|
||||||
|
.frame(
|
||||||
|
width: UIScreen.main.bounds.width * 1.8,
|
||||||
|
height: UIScreen.main.bounds.height * 0.85
|
||||||
|
)
|
||||||
|
.position(x: UIScreen.main.bounds.width / 2,
|
||||||
|
y: UIScreen.main.bounds.height * 0.325)
|
||||||
|
|
||||||
|
LottieView(name: "loading", loopMode: .loop)
|
||||||
|
.frame(width: 200, height: 200)
|
||||||
|
.position(x: UIScreen.main.bounds.width / 2,
|
||||||
|
y: UIScreen.main.bounds.height * 0.325)
|
||||||
|
.onAppear {
|
||||||
|
// Load image from URL asynchronously
|
||||||
|
if let url = URL(string: "https://cdn.fairclip.cn/files/7348219809961742336/c5ca6151-91d3-483e-b7e7-c37f2cb69dc0.png") {
|
||||||
|
URLSession.shared.dataTask(with: url) { data, response, error in
|
||||||
|
if let data = data, let image = UIImage(data: data) {
|
||||||
|
let media = MediaType.image(image)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
// Present the outcome view modally
|
||||||
|
let outcomeView = BlindOutcomeView(media: media)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
|
let window = windowScene.windows.first {
|
||||||
|
window.rootViewController?.present(UIHostingController(rootView: outcomeView), animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(
|
||||||
|
maxWidth: .infinity,
|
||||||
|
maxHeight: UIScreen.main.bounds.height * 0.65
|
||||||
|
)
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(Color.themeTextWhiteSecondary)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// MARK: - 预览
|
||||||
|
#Preview {
|
||||||
|
BlindBoxView()
|
||||||
|
}
|
||||||
304
wake/View/Blind/BlindOutCome.swift
Normal file
304
wake/View/Blind/BlindOutCome.swift
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,39 +3,6 @@ import PhotosUI
|
|||||||
import os.log
|
import os.log
|
||||||
import AVKit
|
import AVKit
|
||||||
|
|
||||||
/// 媒体类型
|
|
||||||
public enum MediaType: Equatable {
|
|
||||||
case image(UIImage)
|
|
||||||
case video(URL, UIImage?) // URL 是视频地址,UIImage 是视频缩略图
|
|
||||||
|
|
||||||
public var thumbnail: UIImage? {
|
|
||||||
switch self {
|
|
||||||
case .image(let image):
|
|
||||||
return image
|
|
||||||
case .video(_, let thumbnail):
|
|
||||||
return thumbnail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var isVideo: Bool {
|
|
||||||
if case .video = self {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func == (lhs: MediaType, rhs: MediaType) -> Bool {
|
|
||||||
switch (lhs, rhs) {
|
|
||||||
case (.image(let lhsImage), .image(let rhsImage)):
|
|
||||||
return lhsImage.pngData() == rhsImage.pngData()
|
|
||||||
case (.video(let lhsURL, _), .video(let rhsURL, _)):
|
|
||||||
return lhsURL == rhsURL
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MediaTypeFilter {
|
enum MediaTypeFilter {
|
||||||
case imagesOnly
|
case imagesOnly
|
||||||
case videosOnly
|
case videosOnly
|
||||||
|
|||||||
@ -142,7 +142,6 @@ struct UserInfo: View {
|
|||||||
// Continue Button
|
// Continue Button
|
||||||
Button(action: {
|
Button(action: {
|
||||||
if showUsername {
|
if showUsername {
|
||||||
router.navigate(to: .avatarBox)
|
|
||||||
let parameters: [String: Any] = [
|
let parameters: [String: Any] = [
|
||||||
"username": userName,
|
"username": userName,
|
||||||
"avatar_file_id": uploadedFileId ?? ""
|
"avatar_file_id": uploadedFileId ?? ""
|
||||||
@ -161,7 +160,7 @@ struct UserInfo: View {
|
|||||||
self.userName = userData.username
|
self.userName = userData.username
|
||||||
}
|
}
|
||||||
// Navigate using router
|
// Navigate using router
|
||||||
router.navigate(to: .avatarBox)
|
router.navigate(to: .blindBox)
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
print("❌ 用户信息更新失败: \(error.localizedDescription)")
|
print("❌ 用户信息更新失败: \(error.localizedDescription)")
|
||||||
|
|||||||
@ -46,7 +46,9 @@ struct WakeApp: App {
|
|||||||
// 根据登录状态显示不同视图
|
// 根据登录状态显示不同视图
|
||||||
if authState.isAuthenticated {
|
if authState.isAuthenticated {
|
||||||
// 已登录:显示userInfo页面
|
// 已登录:显示userInfo页面
|
||||||
ContentView()
|
// ContentView()
|
||||||
|
// .environmentObject(authState)
|
||||||
|
UserInfo()
|
||||||
.environmentObject(authState)
|
.environmentObject(authState)
|
||||||
} else {
|
} else {
|
||||||
// 未登录:显示登录界面
|
// 未登录:显示登录界面
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user