This commit is contained in:
Jin
2026-05-14 17:50:06 +08:00
parent 63e29be346
commit fdde6c51ce
25 changed files with 2970 additions and 295 deletions
+139 -4
View File
@@ -6,6 +6,8 @@ import AppKit
/// - Android LivePlayActivity
struct LiveView: View {
/// 退iOS TabBar
var onExit: (() -> Void)? = nil
///
@StateObject private var viewModel = LiveViewModel()
/// macOS
@@ -18,6 +20,10 @@ struct LiveView: View {
@AppStorage(HawkConfig.PLAY_TYPE) private var legacyPlayTypeRaw = PlayerEngine.system.rawValue
///
@State private var showChannelDrawer = true
#if os(iOS)
/// iOS
@State private var showChannelOverlay = false
#endif
///
@State private var isWindowFullScreen = false
///
@@ -97,6 +103,9 @@ struct LiveView: View {
#endif
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
.navigationBarHidden(true)
.toolbar(.hidden, for: .navigationBar)
.toolbar(.hidden, for: .tabBar)
#endif
.onAppear {
//
@@ -170,6 +179,7 @@ struct LiveView: View {
private var overlayUI: some View {
ZStack(alignment: .leading) {
#if os(macOS)
if showChannelDrawer {
Color.black.opacity(0.22)
.ignoresSafeArea()
@@ -180,9 +190,13 @@ struct LiveView: View {
}
}
}
#endif
VStack(spacing: 0) {
HStack {
#if os(iOS)
liveBackButton
#endif
channelDrawerToggleButton
Spacer()
}
@@ -198,19 +212,44 @@ struct LiveView: View {
}
}
#if os(macOS)
if showChannelDrawer {
channelDrawer
.padding(.leading, 12)
.padding(.vertical, 20)
.transition(.move(edge: .leading).combined(with: .opacity))
}
#endif
}
.animation(.easeInOut(duration: 0.2), value: showCurrentChannelInfo)
#if os(macOS)
.animation(.easeInOut(duration: 0.2), value: showChannelDrawer)
#endif
.simultaneousGesture(
TapGesture().onEnded {
reportUserActivity()
}
)
#if os(iOS)
.fullScreenCover(isPresented: $showChannelOverlay) {
ChannelOverlayView(
channelGroups: viewModel.channelGroups,
selectedGroupIndex: $viewModel.selectedGroupIndex,
currentChannels: viewModel.currentChannels,
currentChannel: viewModel.currentChannel,
onSelectGroup: { viewModel.selectGroup($0) },
onSelectChannel: { channel in
viewModel.selectChannel(channel)
HapticManager.shared.mediumImpact()
showChannelOverlay = false
},
onDismiss: { showChannelOverlay = false }
)
.presentationBackground(.clear)
.transition(.move(edge: .bottom))
.animation(.spring(response: 0.3, dampingFraction: 0.85), value: showChannelOverlay)
}
#endif
#if os(macOS)
.onContinuousHover { phase in
switch phase {
@@ -266,20 +305,58 @@ struct LiveView: View {
.glassCard(cornerRadius: 14)
}
#if os(iOS)
private var liveBackButton: some View {
Button {
HapticManager.shared.lightImpact()
onExit?()
} label: {
Image(systemName: "chevron.left")
.font(.system(size: 16, weight: .bold))
.foregroundColor(.white.opacity(0.9))
.frame(width: 38, height: 38)
.background(Color.black.opacity(0.35))
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.white.opacity(0.15), lineWidth: 0.5)
)
}
.buttonStyle(.plain)
}
#endif
private var channelDrawerToggleButton: some View {
Button {
#if os(iOS)
HapticManager.shared.lightImpact()
showChannelOverlay = true
#else
withAnimation(.easeInOut(duration: 0.2)) {
showChannelDrawer.toggle()
}
#endif
} label: {
HStack(spacing: 8) {
#if os(iOS)
Image(systemName: "list.bullet")
.font(.system(size: 15, weight: .semibold))
Text("频道")
.font(.system(size: 14, weight: .semibold))
#else
Image(systemName: showChannelDrawer ? "sidebar.left" : "sidebar.right")
Text(showChannelDrawer ? "收起菜单" : "频道菜单")
.font(.system(size: 13, weight: .semibold))
#endif
}
.foregroundColor(.white.opacity(0.9))
#if os(iOS)
.padding(.horizontal, 14)
.padding(.vertical, 10)
#else
.padding(.horizontal, 12)
.padding(.vertical, 8)
#endif
.background(Color.black.opacity(0.35))
.clipShape(Capsule())
.overlay(
@@ -294,6 +371,61 @@ struct LiveView: View {
}
private func currentChannelInfo(_ channel: LiveChannelItem) -> some View {
#if os(iOS)
// iOS:
VStack(spacing: 12) {
HStack(spacing: 12) {
//
HStack(spacing: 8) {
Circle().fill(Color.orange).frame(width: 8, height: 8)
Text(channel.channelName)
.font(.system(size: 16, weight: .bold))
.foregroundColor(.white)
.lineLimit(1)
}
Spacer()
// 线
if channel.sourceNum > 1 {
Text("线路 \(channel.sourceIndex + 1)/\(channel.sourceNum)")
.font(.system(size: 12, weight: .medium))
.foregroundColor(.white.opacity(0.6))
}
}
//
if channel.sourceNum > 1 {
HStack(spacing: 12) {
Button {
HapticManager.shared.mediumImpact()
wakeUpCurrentChannelInfo()
resetFailureTracking(for: viewModel.currentChannel)
viewModel.switchSource()
} label: {
HStack(spacing: 6) {
Image(systemName: "shuffle")
.font(.system(size: 13))
Text("切换线路")
.font(.system(size: 13, weight: .semibold))
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 40)
.background(AppTheme.accentGradient)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.buttonStyle(.plain)
}
}
}
.padding(16)
.glassCard(cornerRadius: 14)
.frame(maxWidth: .infinity)
.padding(.horizontal, 16)
.padding(.bottom, 8)
#else
// macOS:
HStack(spacing: 15) {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 10) {
@@ -313,7 +445,6 @@ struct LiveView: View {
Spacer()
HStack(spacing: 12) {
// 线
if channel.sourceNum > 1 {
Button {
wakeUpCurrentChannelInfo()
@@ -334,7 +465,6 @@ struct LiveView: View {
.buttonStyle(.plain)
}
#if os(macOS)
Button {
wakeUpCurrentChannelInfo()
toggleWindowFullScreen()
@@ -355,7 +485,6 @@ struct LiveView: View {
)
}
.buttonStyle(.plain)
#endif
}
}
.padding(20)
@@ -363,6 +492,7 @@ struct LiveView: View {
.frame(maxWidth: currentChannelInfoMaxWidth)
.frame(maxWidth: .infinity)
.padding(20)
#endif
}
private func wakeUpCurrentChannelInfo() {
@@ -469,7 +599,12 @@ struct LiveView: View {
cleanupPlayer()
let playerItem = AVPlayerItem(url: url)
// 使 AVURLAsset HTTP CDN User-Agent
let headers: [String: String] = [
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
]
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
let playerItem = AVPlayerItem(asset: asset)
//
playerItem.preferredForwardBufferDuration = 3
playerItem.canUseNetworkResourcesForLiveStreamingWhilePaused = false