Files
ddys-client/DDYSClient/Views/Player/VideoPlayerView.swift
2026-02-26 22:15:35 +08:00

164 lines
4.6 KiB
Swift

import SwiftUI
import AVFoundation
import AVKit
// MARK: - AVPlayerView
#if os(macOS)
import AppKit
struct NativePlayerView: NSViewRepresentable {
let player: AVPlayer
func makeNSView(context: Context) -> AVPlayerView {
let view = AVPlayerView()
view.player = player
view.controlsStyle = .floating
view.showsFullScreenToggleButton = true
return view
}
func updateNSView(_ nsView: AVPlayerView, context: Context) {
nsView.player = player
}
}
#else
import UIKit
struct NativePlayerView: UIViewControllerRepresentable {
let player: AVPlayer
func makeUIViewController(context: Context) -> AVPlayerViewController {
let vc = AVPlayerViewController()
vc.player = player
vc.allowsPictureInPicturePlayback = true
return vc
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
uiViewController.player = player
}
}
#endif
// MARK: - VideoPlayerView
struct VideoPlayerView: View {
let url: String
let title: String
let episodeName: String?
let contentId: String
let episodeId: Int
@State private var viewModel = PlayerViewModel()
#if os(iOS)
@Environment(\.dismiss) private var dismiss
#endif
var body: some View {
#if os(macOS)
macOSPlayer
#else
iOSPlayer
#endif
}
#if os(iOS)
private var iOSPlayer: some View {
ZStack {
Color.black.ignoresSafeArea()
if let player = viewModel.player {
NativePlayerView(player: player)
.ignoresSafeArea()
} else {
ProgressView()
.tint(.white)
.foregroundStyle(.white)
}
VStack {
HStack {
Button {
viewModel.stop()
dismiss()
} label: {
Image(systemName: "xmark.circle.fill")
.font(.title2)
.foregroundStyle(.white)
}
VStack(alignment: .leading) {
Text(title)
.font(.headline)
.foregroundStyle(.white)
if let episodeName {
Text(episodeName)
.font(.subheadline)
.foregroundStyle(.white.opacity(0.8))
}
}
Spacer()
Menu {
ForEach([0.5, 0.75, 1.0, 1.25, 1.5, 2.0], id: \.self) { rate in
Button {
viewModel.setRate(Float(rate))
} label: {
HStack {
Text("\(rate, specifier: "%.2g")x")
if viewModel.playbackRate == Float(rate) {
Image(systemName: "checkmark")
}
}
}
}
} label: {
Text("\(viewModel.playbackRate, specifier: "%.2g")x")
.font(.subheadline)
.foregroundStyle(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.white.opacity(0.2), in: Capsule())
}
}
.padding()
.background(.black.opacity(0.4))
Spacer()
}
}
.statusBarHidden()
.persistentSystemOverlays(.hidden)
.onAppear {
viewModel.play(url: url, contentId: contentId, episodeId: episodeId)
}
.onDisappear {
viewModel.stop()
}
}
#endif
#if os(macOS)
private var macOSPlayer: some View {
Group {
if let player = viewModel.player {
NativePlayerView(player: player)
} else {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.frame(minWidth: 640, minHeight: 400)
.onAppear {
viewModel.play(url: url, contentId: contentId, episodeId: episodeId)
}
.onDisappear {
viewModel.stop()
}
}
#endif
}