fix iOS
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user