196 lines
7.7 KiB
Swift
196 lines
7.7 KiB
Swift
#if os(iOS)
|
|
import SwiftUI
|
|
|
|
/// 直播频道全屏覆盖式选择器 - 替代 macOS 上的 390pt 抽屉
|
|
/// 采用左侧分组列表 + 右侧频道列表的双栏布局
|
|
struct ChannelOverlayView: View {
|
|
let channelGroups: [LiveChannelGroup]
|
|
@Binding var selectedGroupIndex: Int
|
|
let currentChannels: [LiveChannelItem]
|
|
let currentChannel: LiveChannelItem?
|
|
let onSelectGroup: (Int) -> Void
|
|
let onSelectChannel: (LiveChannelItem) -> Void
|
|
let onDismiss: () -> Void
|
|
|
|
@State private var dragOffset: CGFloat = 0
|
|
|
|
/// 计算左侧分组列表宽度(屏幕宽度的 32%)
|
|
/// - Parameter screenWidth: 屏幕宽度
|
|
/// - Returns: 分组列表宽度,范围在 [screenWidth * 0.30, screenWidth * 0.35]
|
|
static func computeGroupWidth(screenWidth: CGFloat) -> CGFloat {
|
|
return screenWidth * 0.32
|
|
}
|
|
|
|
var body: some View {
|
|
GeometryReader { geometry in
|
|
ZStack {
|
|
// Dimmed background - tap to dismiss
|
|
Color.black.opacity(0.5)
|
|
.ignoresSafeArea()
|
|
.onTapGesture {
|
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.85)) {
|
|
onDismiss()
|
|
}
|
|
}
|
|
|
|
// Content panel
|
|
VStack(spacing: 0) {
|
|
// Handle bar for drag-to-dismiss
|
|
handleBar
|
|
|
|
// Two-column layout
|
|
HStack(spacing: 0) {
|
|
groupList
|
|
.frame(width: Self.computeGroupWidth(screenWidth: geometry.size.width))
|
|
|
|
Divider()
|
|
.overlay(Color.white.opacity(0.1))
|
|
|
|
channelList
|
|
}
|
|
}
|
|
.frame(maxHeight: geometry.size.height * 0.85)
|
|
.background(.ultraThinMaterial)
|
|
.clipShape(RoundedRectangle(cornerRadius: 20))
|
|
.offset(y: max(dragOffset, 0))
|
|
.gesture(dismissDragGesture)
|
|
.padding(.top, geometry.safeAreaInsets.top + 40)
|
|
}
|
|
}
|
|
.transition(.move(edge: .bottom).combined(with: .opacity))
|
|
.animation(.spring(response: 0.3, dampingFraction: 0.85), value: dragOffset)
|
|
}
|
|
|
|
// MARK: - Handle Bar
|
|
|
|
private var handleBar: some View {
|
|
VStack(spacing: 8) {
|
|
Capsule()
|
|
.fill(Color.white.opacity(0.4))
|
|
.frame(width: 36, height: 5)
|
|
.padding(.top, 10)
|
|
|
|
Text("频道列表")
|
|
.font(.system(size: 15, weight: .semibold))
|
|
.foregroundColor(.white.opacity(0.9))
|
|
.padding(.bottom, 8)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.background(Color.white.opacity(0.05))
|
|
}
|
|
|
|
// MARK: - Group List (Left Column)
|
|
|
|
private var groupList: some View {
|
|
ScrollView {
|
|
LazyVStack(spacing: 0) {
|
|
if channelGroups.isEmpty {
|
|
Text("暂无分组")
|
|
.font(.system(size: 14))
|
|
.foregroundColor(.white.opacity(0.5))
|
|
.padding(.vertical, 20)
|
|
.frame(maxWidth: .infinity)
|
|
} else {
|
|
ForEach(Array(channelGroups.enumerated()), id: \.offset) { index, group in
|
|
Button {
|
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.85)) {
|
|
onSelectGroup(index)
|
|
}
|
|
} label: {
|
|
Text(group.groupName)
|
|
.font(.system(size: 14, weight: selectedGroupIndex == index ? .bold : .medium))
|
|
.foregroundColor(selectedGroupIndex == index ? .orange : .white.opacity(0.8))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal, 14)
|
|
.frame(minHeight: 48)
|
|
.background(
|
|
selectedGroupIndex == index
|
|
? Color.white.opacity(0.1)
|
|
: Color.clear
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Channel List (Right Column)
|
|
|
|
private var channelList: some View {
|
|
ScrollView {
|
|
LazyVStack(spacing: 0) {
|
|
if currentChannels.isEmpty {
|
|
Text("暂无频道")
|
|
.font(.system(size: 14))
|
|
.foregroundColor(.white.opacity(0.5))
|
|
.padding(.vertical, 20)
|
|
.frame(maxWidth: .infinity)
|
|
} else {
|
|
ForEach(Array(currentChannels.enumerated()), id: \.offset) { _, channel in
|
|
Button {
|
|
HapticManager.shared.mediumImpact()
|
|
onSelectChannel(channel)
|
|
} label: {
|
|
HStack {
|
|
Text(channel.channelName)
|
|
.font(.system(size: 14, weight: currentChannel?.channelName == channel.channelName ? .bold : .medium))
|
|
.foregroundColor(currentChannel?.channelName == channel.channelName ? .orange : .white.opacity(0.8))
|
|
|
|
Spacer()
|
|
|
|
if channel.sourceNum > 1 {
|
|
Text("\(channel.sourceNum)")
|
|
.font(.system(size: 10))
|
|
.foregroundColor(.white.opacity(0.3))
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(Capsule().stroke(Color.white.opacity(0.2), lineWidth: 0.5))
|
|
}
|
|
|
|
if currentChannel?.channelName == channel.channelName {
|
|
Image(systemName: "speaker.wave.2.fill")
|
|
.font(.system(size: 10))
|
|
.foregroundColor(.orange)
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.frame(minHeight: 48)
|
|
.background(
|
|
currentChannel?.channelName == channel.channelName
|
|
? Color.orange.opacity(0.15)
|
|
: Color.clear
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Drag to Dismiss Gesture
|
|
|
|
private var dismissDragGesture: some Gesture {
|
|
DragGesture()
|
|
.onChanged { value in
|
|
dragOffset = value.translation.height
|
|
}
|
|
.onEnded { value in
|
|
if value.translation.height > 120 {
|
|
// Dismiss if dragged down far enough
|
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.85)) {
|
|
onDismiss()
|
|
}
|
|
} else {
|
|
// Snap back
|
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.85)) {
|
|
dragOffset = 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|