import SwiftUI import UIKit import UniformTypeIdentifiers struct RootView: View { @StateObject private var auth = AuthManager() @StateObject private var controller = PlayerController() @StateObject private var catalogVM = CatalogViewModel() @State private var activeModule: Module = .course @State private var continuousPlayback = false @State private var showImporter = false @State private var showFullscreenPlayer = false @State private var courses: [Course] = [] @State private var currentCourse: Course? @State private var courseStatusText = "正在同步" @State private var showCourseGuardToast = false enum Module { case course case train case test case mine } var body: some View { if auth.isLoggedIn { mainContent } else { AuthView(auth: auth) { Task { await loadCourses() } } } } private var mainContent: some View { ZStack { Theme.background.ignoresSafeArea() VStack(spacing: 0) { header moduleTabs switch activeModule { case .course: coursePage case .train: TrainingView( controller: controller, catalogVM: catalogVM, continuousPlayback: $continuousPlayback, onFullscreen: enterFullscreen ) case .test: TestView(controller: controller, onFullscreen: enterFullscreen) case .mine: minePage } } if showCourseGuardToast { VStack { Spacer() Text("请先选择课程") .font(.system(size: 14)) .foregroundColor(.white) .padding(.horizontal, 20) .padding(.vertical, 12) .background(Theme.overlay) .clipShape(RoundedRectangle(cornerRadius: 8)) Spacer().frame(height: 80) } } } .preferredColorScheme(.dark) .onAppear { if auth.isLoggedIn && courses.isEmpty { Task { await loadCourses() } } } .onChange(of: activeModule) { module in if module == .train { controller.setContinuousPlayback(continuousPlayback) } else { controller.setContinuousPlayback(true) } } .fileImporter( isPresented: $showImporter, allowedContentTypes: [.movie], allowsMultipleSelection: false ) { result in if case .success(let urls) = result, let url = urls.first { Task { await catalogVM.importVideo(at: url) } } } .fullScreenCover(isPresented: $showFullscreenPlayer) { FullscreenPlayerView(controller: controller) { exitFullscreen() } .onAppear(perform: enterFullscreen) } } private var header: some View { HStack(spacing: 0) { VStack(alignment: .leading, spacing: 2) { Text("八哥口语") .font(.system(size: 32, weight: .bold)) .foregroundColor(.white) Text(auth.nickname.isEmpty ? "外语跟读训练神器" : "你好,\(auth.nickname)") .font(.system(size: 15)) .foregroundColor(Theme.textMuted) } Spacer() if activeModule == .train { headerButton("导入") { showImporter = true } } headerButton("刷新") { Task { await loadCourses() } } headerButton("退出") { auth.logout() currentCourse = nil courses = [] activeModule = .course } } .padding(.horizontal, 18) .padding(.top, 8) .padding(.bottom, 10) } private func headerButton(_ label: String, action: @escaping () -> Void) -> some View { Button(action: action) { Text(label) .font(.system(size: 14, weight: .bold)) .foregroundColor(.white) .padding(.horizontal, 14) .frame(height: 40) .background(Theme.buttonBlue) .clipShape(RoundedRectangle(cornerRadius: 8)) } .buttonStyle(.plain) .padding(.leading, 8) } private var moduleTabs: some View { HStack(spacing: 0) { moduleTab("课程", module: .course) moduleTab("训练", module: .train) moduleTab("测试", module: .test) moduleTab("我的", module: .mine) } .padding(.horizontal, 16) .padding(.bottom, 8) } private func moduleTab(_ label: String, module: Module) -> some View { Button { if module != .course && currentCourse == nil { showCourseGuardToast = true DispatchQueue.main.asyncAfter(deadline: .now() + 2) { showCourseGuardToast = false } } else { activeModule = module } } label: { Text(label) .font(.system(size: 15, weight: .bold)) .foregroundColor(activeModule == module ? .white : Theme.textMuted) .frame(maxWidth: .infinity) .frame(height: 38) .background(activeModule == module ? Theme.buttonBlue : Theme.surface) .clipShape(RoundedRectangle(cornerRadius: 8)) } .buttonStyle(.plain) } // MARK: - Course Page private var coursePage: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { Text("课程首页") .font(.system(size: 26, weight: .bold)) .foregroundColor(.white) .padding(.horizontal, 16) Text("选择一门课程开始训练") .font(.system(size: 13)) .foregroundColor(Theme.textMuted) .padding(.horizontal, 16) .padding(.top, 4) Text(courseStatusText) .font(.system(size: 13)) .foregroundColor(Theme.textMuted) .padding(.horizontal, 16) .padding(.top, 8) if courses.isEmpty { Text("暂无课程") .font(.system(size: 14)) .foregroundColor(Theme.textMuted) .padding(.horizontal, 16) .padding(.top, 20) } else { LazyVStack(spacing: 10) { ForEach(courses) { course in courseCard(course) } } .padding(.horizontal, 16) .padding(.top, 8) } } .padding(.top, 12) } } private func courseCard(_ course: Course) -> some View { let isSelected = currentCourse?.videoHash == course.videoHash return Button { selectCourse(course) } label: { VStack(alignment: .leading, spacing: 6) { Text(course.title) .font(.system(size: 16, weight: .bold)) .foregroundColor(isSelected ? Theme.textDark : .white) .lineLimit(2) Text("\(course.enrolled ? "已选修" : "未选修") · \(course.sentenceCount) 句\(isSelected ? " · 当前课程" : "")") .font(.system(size: 13)) .foregroundColor(isSelected ? Theme.accentDeep : Theme.textMuted) } .frame(maxWidth: .infinity, alignment: .leading) .padding(14) .background(isSelected ? Theme.selected : Theme.surface) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(isSelected ? Theme.accent : Theme.border, lineWidth: 1) ) .clipShape(RoundedRectangle(cornerRadius: 10)) } .buttonStyle(.plain) } private func selectCourse(_ course: Course) { currentCourse = course let video = course.toVideoSummary() catalogVM.select(video) activeModule = .train } // MARK: - Mine Page private var minePage: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { Text(auth.nickname.isEmpty ? "我的学习" : auth.nickname) .font(.system(size: 26, weight: .bold)) .foregroundColor(.white) .padding(.horizontal, 16) .padding(.top, 12) if let course = currentCourse { Text("当前课程:\(course.title)") .font(.system(size: 14, weight: .bold)) .foregroundColor(.white) .padding(.horizontal, 16) } Text("此页面显示当前课程的测试记录和配音分享") .font(.system(size: 13)) .foregroundColor(Theme.textMuted) .padding(.horizontal, 16) } } } // MARK: - Load Courses private func loadCourses() async { courseStatusText = "正在同步" do { courses = try await AuthAPI.courses(token: auth.token) courseStatusText = "\(courses.count) 个课程" } catch { courseStatusText = "课程加载失败:\(error.localizedDescription)" } } // MARK: - Fullscreen private func enterFullscreen() { AppDelegate.orientationLock = UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape updateSupportedInterfaceOrientations() showFullscreenPlayer = true } private func exitFullscreen() { AppDelegate.orientationLock = UIDevice.current.userInterfaceIdiom == .pad ? .all : .portrait updateSupportedInterfaceOrientations() showFullscreenPlayer = false } private func updateSupportedInterfaceOrientations() { let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } let controllers = scenes.flatMap { $0.windows }.compactMap(\.rootViewController) for controller in controllers { if #available(iOS 16.0, *) { controller.setNeedsUpdateOfSupportedInterfaceOrientations() } else { UIViewController.attemptRotationToDeviceOrientation() } } } }