import SwiftUI /// 设置页 - 对应 Android 版 SettingActivity + ModelSettingFragment struct SettingsView: View { @StateObject private var viewModel = SettingsViewModel() @StateObject private var apiConfig = ApiConfig.shared @EnvironmentObject var appState: AppState @State private var showApiInput = false @State private var showAbout = false @State private var sourceSearchText = "" @State private var showingPicker: PickerType = .none enum PickerType { case none case player case decode case vlcBuffer case playTimeStep } var body: some View { NavigationStack { ScrollView { VStack(spacing: 24) { // API 配置 SectionCard(title: "数据源") { SettingsRow(icon: "link", title: "接口地址", value: viewModel.apiUrl.isEmpty ? "未配置" : viewModel.apiUrl) { showApiInput = true } Divider().background(Color.white.opacity(0.1)) if !apiConfig.sourceBeanList.isEmpty { NavigationLink { sourcePickerView } label: { SettingsRow(icon: "server.rack", title: "主页数据源", value: apiConfig.homeSourceBean?.name ?? "", action: nil) } } } // 播放设置 SectionCard(title: "播放设置") { SettingsRow(icon: "play.rectangle", title: "播放器", value: viewModel.playerEngine.title) { if viewModel.playerEngineOptions.count > 1 { showingPicker = .player } } Divider().background(Color.white.opacity(0.1)) SettingsRow(icon: "cpu", title: "视频解码", value: viewModel.decodeMode.title) { showingPicker = .decode } if PlayerEngine.isVLCAvailable { Divider().background(Color.white.opacity(0.1)) SettingsRow(icon: "externaldrive.badge.wifi", title: "VLC缓冲", value: viewModel.vlcBufferMode.title) { showingPicker = .vlcBuffer } } Divider().background(Color.white.opacity(0.1)) SettingsRow(icon: "forward", title: "快进步长", value: "\(viewModel.playTimeStep)秒") { showingPicker = .playTimeStep } } // 功能 SectionCard(title: "功能") { NavigationLink { HistoryView() } label: { SettingsRow(icon: "clock", title: "播放历史", value: "", action: nil) } Divider().background(Color.white.opacity(0.1)) NavigationLink { FavoritesView() } label: { SettingsRow(icon: "heart", title: "我的收藏", value: "", action: nil) } } // 缓存 SectionCard(title: "缓存") { SettingsRow(icon: "trash", title: "清除缓存", value: viewModel.cacheSizeString) { viewModel.clearCache() } } // 关于 SectionCard(title: "关于") { SettingsRow(icon: "info.circle", title: "版本", value: "1.0.0", action: nil) Divider().background(Color.white.opacity(0.1)) SettingsRow(icon: "globe", title: "站点数量", value: "\(apiConfig.sourceBeanList.count)", action: nil) Divider().background(Color.white.opacity(0.1)) SettingsRow(icon: "wand.and.stars", title: "解析数量", value: "\(apiConfig.parseBeanList.count)", action: nil) Divider().background(Color.white.opacity(0.1)) SettingsRow(icon: "tv", title: "直播分组", value: "\(apiConfig.liveChannelGroupList.count)", action: nil) } } .padding(.horizontal, 20) .padding(.vertical, 24) } .background(AppTheme.primaryGradient.ignoresSafeArea()) .navigationTitle("设置") #if os(iOS) .navigationBarTitleDisplayMode(.inline) .toolbarColorScheme(.dark, for: .navigationBar) .toolbarBackground(.hidden, for: .navigationBar) #endif .sheet(isPresented: $showApiInput) { apiInputSheet } } .overlay(pickerOverlay) } // MARK: - 选择器 Overlay @ViewBuilder private var pickerOverlay: some View { switch showingPicker { case .player: SelectionModal( title: "选择播放器", icon: "play.rectangle.fill", items: viewModel.playerEngineOptions, selectedItem: viewModel.playerEngine, itemTitle: { $0.title }, onSelect: { engine in viewModel.setPlayerEngine(engine) showingPicker = .none }, onCancel: { showingPicker = .none } ) case .decode: SelectionModal( title: "视频解码模式", icon: "cpu.fill", items: viewModel.decodeModeOptions, selectedItem: viewModel.decodeMode, itemTitle: { $0.title }, onSelect: { mode in viewModel.setDecodeMode(mode) showingPicker = .none }, onCancel: { showingPicker = .none } ) case .vlcBuffer: SelectionModal( title: "VLC 缓冲策略", icon: "externaldrive.fill", items: viewModel.vlcBufferModeOptions, selectedItem: viewModel.vlcBufferMode, itemTitle: { $0.title }, onSelect: { mode in viewModel.setVLCBufferMode(mode) showingPicker = .none }, onCancel: { showingPicker = .none } ) case .playTimeStep: SelectionModal( title: "快进步长", icon: "forward.fill", items: viewModel.playTimeStepOptions, selectedItem: viewModel.playTimeStep, itemTitle: { "\($0) 秒" }, onSelect: { step in viewModel.setPlayTimeStep(step) showingPicker = .none }, onCancel: { showingPicker = .none } ) case .none: EmptyView() } } // MARK: - API 输入弹窗 private var apiInputSheet: some View { NavigationStack { VStack(spacing: 16) { HStack { Image(systemName: "link") .foregroundColor(.secondary) TextField("请输入接口地址", text: $viewModel.apiUrl) .textFieldStyle(.plain) #if os(iOS) .autocapitalization(.none) .keyboardType(.URL) #endif } .padding() .background(Color.secondary.opacity(0.1)) .cornerRadius(10) // 粘贴按钮 HStack { Button { #if os(iOS) if let text = UIPasteboard.general.string { viewModel.apiUrl = text } #else if let text = NSPasteboard.general.string(forType: .string) { viewModel.apiUrl = text } #endif } label: { Label("粘贴", systemImage: "doc.on.clipboard") .font(.subheadline) } Spacer() } // 历史记录 if !viewModel.apiHistory.isEmpty { VStack(alignment: .leading, spacing: 8) { Text("历史记录") .font(.caption) .foregroundColor(.secondary) ForEach(viewModel.apiHistory, id: \.self) { url in HStack { Button { viewModel.apiUrl = url } label: { HStack { Image(systemName: "clock") .font(.caption) Text(url) .font(.caption) .lineLimit(1) } .foregroundColor(.secondary) } Spacer() Button { viewModel.removeApiHistory(url) } label: { Image(systemName: "xmark.circle") .font(.caption) .foregroundColor(.gray) } } } } } if let error = viewModel.configError { Text(error) .font(.caption) .foregroundColor(.red) } Spacer() } .padding() .navigationTitle("配置接口") #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .toolbar { ToolbarItem(placement: .cancellationAction) { Button("取消") { showApiInput = false } } ToolbarItem(placement: .confirmationAction) { Button { Task { await viewModel.loadConfig() if viewModel.configSuccess { await appState.loadConfig(url: viewModel.apiUrl) showApiInput = false } } } label: { if viewModel.isLoadingConfig { ProgressView() } else { Text("确认") } } .disabled(viewModel.isLoadingConfig || viewModel.apiUrl.isEmpty) } } } #if os(iOS) .presentationDetents([.medium, .large]) #endif } // MARK: - 源选择 private var filteredSources: [SourceBean] { let sources = apiConfig.sourceBeanList if sourceSearchText.isEmpty { return sources } else { return sources.filter { $0.name.localizedCaseInsensitiveContains(sourceSearchText) || $0.api.localizedCaseInsensitiveContains(sourceSearchText) } } } private var sourcePickerView: some View { VStack(spacing: 0) { // 搜索栏 HStack { Image(systemName: "magnifyingglass") .foregroundColor(.secondary) TextField("搜索数据源", text: $sourceSearchText) .textFieldStyle(.plain) if !sourceSearchText.isEmpty { Button(action: { sourceSearchText = "" }) { Image(systemName: "xmark.circle.fill") .foregroundColor(.secondary) } .buttonStyle(.plain) } } .padding(12) .glassCard(cornerRadius: 12) .padding(.horizontal, 20) .padding(.vertical, 12) ScrollView { LazyVStack(spacing: 12) { ForEach(filteredSources) { source in Button { apiConfig.setHomeSource(source) appState.currentSourceKey = source.key } label: { HStack(alignment: .center, spacing: 16) { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 8) { Text(source.name) .font(.system(size: 16, weight: .semibold)) .foregroundColor(source.isSupportedInSwift ? .white : .white.opacity(0.5)) // 类型标签 Text(source.typeDescription) .font(.system(size: 10, weight: .bold)) .foregroundColor(source.isSupportedInSwift ? .orange : .gray) .padding(.horizontal, 6) .padding(.vertical, 3) .background( Capsule().fill( source.isSupportedInSwift ? Color.orange.opacity(0.2) : Color.gray.opacity(0.2) ) ) if !source.isSupportedInSwift { Text("暂不支持") .font(.system(size: 10, weight: .medium)) .foregroundColor(.red.opacity(0.8)) .padding(.horizontal, 6) .padding(.vertical, 3) .background(Capsule().fill(Color.red.opacity(0.15))) } } Text(source.api) .font(.system(size: 12)) .foregroundColor(.white.opacity(0.5)) .lineLimit(1) } Spacer() HStack(spacing: 12) { if source.isSearchable { Image(systemName: "magnifyingglass") .font(.system(size: 14, weight: .medium)) .foregroundColor(.green.opacity(0.8)) } if source.key == apiConfig.homeSourceBean?.key { Image(systemName: "checkmark.circle.fill") .font(.system(size: 20)) .foregroundColor(.orange) } else { Circle() .strokeBorder(Color.white.opacity(0.2), lineWidth: 1) .frame(width: 20, height: 20) } } } .padding(16) .glassCard(cornerRadius: 16) .overlay( RoundedRectangle(cornerRadius: 16) .stroke( source.key == apiConfig.homeSourceBean?.key ? Color.orange.opacity(0.5) : Color.clear, lineWidth: 1 ) ) } .buttonStyle(.plain) } } .padding(.horizontal, 20) .padding(.bottom, 24) } } .background(AppTheme.primaryGradient.ignoresSafeArea()) .navigationTitle("选择数据源") #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif } } // MARK: - 辅助组件 struct SectionCard: View { let title: String @ViewBuilder let content: () -> Content var body: some View { VStack(alignment: .leading, spacing: 12) { Text(title) .font(.subheadline) .fontWeight(.bold) .foregroundColor(.white.opacity(0.6)) .padding(.leading, 8) VStack(spacing: 0) { content() } .glassCard(cornerRadius: 16) } } } struct SettingsRow: View { let icon: String let title: String let value: String let action: (() -> Void)? var body: some View { Group { if let action = action { Button(action: action) { rowContent } .buttonStyle(.plain) } else { rowContent } } } private var rowContent: some View { HStack(spacing: 16) { Image(systemName: icon) .font(.system(size: 16)) .foregroundColor(.orange) .frame(width: 24) Text(title) .font(.body) .foregroundColor(.white.opacity(0.9)) Spacer() Text(value) .font(.subheadline) .foregroundColor(.white.opacity(0.5)) .lineLimit(1) Image(systemName: "chevron.right") .font(.system(size: 12, weight: .bold)) .foregroundColor(.white.opacity(0.3)) } .padding(.horizontal, 16) .padding(.vertical, 14) .contentShape(Rectangle()) } }