增加注解

This commit is contained in:
JinJiangHuang
2026-03-01 22:23:38 +08:00
parent 99a8cc799b
commit ee8c71dcd4
23 changed files with 374 additions and 24 deletions
+7
View File
@@ -1,8 +1,13 @@
import SwiftUI
///
///
struct EmptyStateView: View {
/// SF Symbol
let icon: String
///
let title: String
///
var message: String? = nil
var body: some View {
@@ -23,11 +28,13 @@ struct EmptyStateView: View {
}
.padding(.bottom, 8)
//
Text(title)
.font(.title3.bold())
.foregroundColor(.white.opacity(0.9))
.tracking(1)
//
if let message = message {
Text(message)
.font(.callout)
+11
View File
@@ -2,15 +2,21 @@ import SwiftUI
/// - Android HomeActivity TabView
struct ContentView: View {
/// 使
private enum ApiInputTarget {
case vod
case live
}
///
@EnvironmentObject var appState: AppState
/// ViewModel
@StateObject private var settingsVM = SettingsViewModel()
///
@State private var selectedTab = 0
/// `appState.isConfigLoaded`
@State private var showSetup = false
///
@State private var setupInputTarget: ApiInputTarget = .vod
var body: some View {
@@ -29,6 +35,7 @@ struct ContentView: View {
let savedVodUrl = defaults.string(forKey: HawkConfig.API_URL) ?? ""
let savedLiveUrl = defaults.string(forKey: HawkConfig.LIVE_API_URL) ?? ""
if !savedVodUrl.isEmpty {
//
Task {
await appState.loadConfig(vodUrl: savedVodUrl, liveUrl: savedLiveUrl)
}
@@ -38,6 +45,7 @@ struct ContentView: View {
@ViewBuilder
private var multiRepoSelectionOverlay: some View {
//
if let pending = settingsVM.pendingMultiRepoSelection {
SelectionModal(
title: "选择\(pending.target.title)仓库",
@@ -62,6 +70,7 @@ struct ContentView: View {
// MARK: -
/// iOS 使 TabViewmacOS 使 NavigationSplitView
private var mainTabView: some View {
#if os(iOS)
TabView(selection: $selectedTab) {
@@ -130,6 +139,7 @@ struct ContentView: View {
// MARK: -
///
private var setupView: some View {
ZStack {
//
@@ -346,6 +356,7 @@ struct ContentView: View {
#if os(iOS)
UIPasteboard.general.string
#else
// macOS NSPasteboard
NSPasteboard.general.string(forType: .string)
#endif
}
+2 -1
View File
@@ -1 +1,2 @@
// This file has been moved to tvbox/Utils/Extensions.swift to ensure compatibility across targets.
// `tvbox/Utils/Extensions.swift`
//
+8
View File
@@ -2,17 +2,24 @@ import SwiftUI
/// - Android SeriesAdapter
struct EpisodeListView: View {
/// 线
let episodes: [VodInfo.Episode]
///
let selectedIndex: Int
///
let onSelect: (Int) -> Void
/// 50
@State private var currentGroup = 0
///
private let groupSize = 50
/// 1 0
private var groupCount: Int {
max(1, (episodes.count + groupSize - 1) / groupSize)
}
///
private var currentEpisodes: [VodInfo.Episode] {
let start = currentGroup * groupSize
let end = min(start + groupSize, episodes.count)
@@ -62,6 +69,7 @@ struct EpisodeListView: View {
GridItem(.fixed(44)),
GridItem(.fixed(44))
], spacing: 10) {
// 使 + 便
ForEach(Array(currentEpisodes.enumerated()), id: \.offset) { index, episode in
let actualIndex = currentGroup * groupSize + index
Button {
+10
View File
@@ -3,15 +3,19 @@ import SwiftData
/// - Android CollectActivity
struct FavoritesView: View {
/// /
@Query(sort: \VodCollect.updateTime, order: .reverse)
private var favorites: [VodCollect]
/// SwiftData
@Environment(\.modelContext) private var modelContext
#if os(iOS)
/// iOS
private let columns = [
GridItem(.adaptive(minimum: 120, maximum: 160), spacing: 12)
]
#else
/// macOS
private let columns = [
GridItem(.adaptive(minimum: 140, maximum: 180), spacing: 16)
]
@@ -25,6 +29,7 @@ struct FavoritesView: View {
} else {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {
//
ForEach(favorites) { item in
NavigationLink(value: movieVideo(from: item)) {
favoriteCard(item)
@@ -50,12 +55,14 @@ struct FavoritesView: View {
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
// Movie.Video /
.navigationDestination(for: Movie.Video.self) { video in
DetailView(video: video)
}
}
}
///
private var emptyState: some View {
EmptyStateView(
icon: "heart.text.square",
@@ -65,6 +72,8 @@ struct FavoritesView: View {
.padding(40)
}
///
///
private func favoriteCard(_ item: VodCollect) -> some View {
VStack(alignment: .leading, spacing: 6) {
CachedAsyncImage(url: URL.posterURL(from: item.vodPic)) { image in
@@ -83,6 +92,7 @@ struct FavoritesView: View {
}
}
///
private func movieVideo(from item: VodCollect) -> Movie.Video {
Movie.Video(id: item.vodId, name: item.vodName, pic: item.vodPic, sourceKey: item.sourceKey)
}
+11
View File
@@ -3,15 +3,19 @@ import SwiftData
/// - Android HistoryActivity
struct HistoryView: View {
///
@Query(sort: \VodRecord.updateTime, order: .reverse)
private var records: [VodRecord]
/// SwiftData
@Environment(\.modelContext) private var modelContext
#if os(iOS)
/// iOS
private let columns = [
GridItem(.adaptive(minimum: 120, maximum: 160), spacing: 12)
]
#else
/// macOS
private let columns = [
GridItem(.adaptive(minimum: 140, maximum: 180), spacing: 16)
]
@@ -25,6 +29,7 @@ struct HistoryView: View {
} else {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {
//
ForEach(records) { item in
NavigationLink(value: movieVideo(from: item)) {
recordCard(item)
@@ -53,6 +58,7 @@ struct HistoryView: View {
.toolbar {
if !records.isEmpty {
ToolbarItem(placement: .automatic) {
// 使
Button {
Task {
CacheStore.shared.clearHistory(context: modelContext)
@@ -64,12 +70,14 @@ struct HistoryView: View {
}
}
}
// //
.navigationDestination(for: Movie.Video.self) { video in
DetailView(video: video)
}
}
}
///
private var emptyState: some View {
EmptyStateView(
icon: "clock.arrow.circlepath",
@@ -79,6 +87,8 @@ struct HistoryView: View {
.padding(40)
}
///
/// 便
private func recordCard(_ item: VodRecord) -> some View {
VStack(alignment: .leading, spacing: 6) {
ZStack(alignment: .bottomLeading) {
@@ -115,6 +125,7 @@ struct HistoryView: View {
}
}
///
private func movieVideo(from item: VodRecord) -> Movie.Video {
Movie.Video(id: item.vodId, name: item.vodName, pic: item.vodPic, sourceKey: item.sourceKey)
}
+4
View File
@@ -2,7 +2,9 @@ import SwiftUI
///
struct VodCardView: View {
///
let video: Movie.Video
/// macOS
@State private var isHovered = false
var body: some View {
@@ -44,6 +46,7 @@ struct VodCardView: View {
.padding(8)
}
}
//
.scaleEffect(isHovered ? 1.05 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: isHovered)
.onHover { hovering in
@@ -67,6 +70,7 @@ struct VodCardView: View {
}
}
///
private var placeholderImage: some View {
RoundedRectangle(cornerRadius: AppTheme.cardRadius)
.fill(Color.white.opacity(0.05))
+27
View File
@@ -6,24 +6,43 @@ import AppKit
/// - Android LivePlayActivity
struct LiveView: View {
///
@StateObject private var viewModel = LiveViewModel()
/// macOS
@EnvironmentObject var appState: AppState
/// 使
@State private var avPlayer: AVPlayer?
///
@AppStorage(HawkConfig.PLAY_TYPE_LIVE) private var livePlayTypeRaw = -1
///
@AppStorage(HawkConfig.PLAY_TYPE) private var legacyPlayTypeRaw = PlayerEngine.system.rawValue
///
@State private var showChannelDrawer = true
///
@State private var isWindowFullScreen = false
///
private let currentChannelInfoMaxWidth: CGFloat = 600
/// AVPlayer
@State private var itemStatusObserver: NSKeyValueObservation?
///
@State private var playbackFailedObserver: NSObjectProtocol?
///
@State private var playbackStalledObserver: NSObjectProtocol?
/// 线线
@State private var failedSourceIndices: Set<Int> = []
/// ID
@State private var trackedChannelId: String = ""
///
@State private var showCurrentChannelInfo = true
///
@State private var channelInfoTimer: Timer?
///
private let channelInfoAutoHideDelay: TimeInterval = 3.0
/// VLC
@State private var vlcInteractionToken = 0
///
/// 退使
private var selectedEngine: PlayerEngine {
let defaults = UserDefaults.standard
let rawValue: Int
@@ -80,6 +99,7 @@ struct LiveView: View {
.navigationBarTitleDisplayMode(.inline)
#endif
.onAppear {
//
viewModel.loadChannels()
wakeUpCurrentChannelInfo()
}
@@ -92,6 +112,7 @@ struct LiveView: View {
wakeUpCurrentChannelInfo()
}
.onChange(of: viewModel.currentChannel?.id) { _, _ in
// 线
resetFailureTracking(for: viewModel.currentChannel)
wakeUpCurrentChannelInfo()
}
@@ -364,6 +385,7 @@ struct LiveView: View {
}
private func reportUserActivity() {
// VLC
wakeUpCurrentChannelInfo()
vlcInteractionToken &+= 1
}
@@ -459,6 +481,7 @@ struct LiveView: View {
}
private func cleanupPlayer() {
//
if let observer = playbackFailedObserver {
NotificationCenter.default.removeObserver(observer)
playbackFailedObserver = nil
@@ -475,6 +498,7 @@ struct LiveView: View {
}
private func observePlaybackFailure(for item: AVPlayerItem) {
// KVO item
itemStatusObserver = item.observe(\.status, options: [.new]) { observedItem, _ in
if observedItem.status == .failed {
DispatchQueue.main.async {
@@ -483,6 +507,7 @@ struct LiveView: View {
}
}
//
playbackFailedObserver = NotificationCenter.default.addObserver(
forName: .AVPlayerItemFailedToPlayToEndTime,
object: item,
@@ -491,6 +516,7 @@ struct LiveView: View {
handlePlaybackFailure(trigger: "item_failed")
}
//
playbackStalledObserver = NotificationCenter.default.addObserver(
forName: .AVPlayerItemPlaybackStalled,
object: item,
@@ -526,6 +552,7 @@ struct LiveView: View {
private func switchToNextAvailableSource(totalSources: Int) -> Bool {
guard failedSourceIndices.count < totalSources else { return false }
// `totalSources` 线
for _ in 0..<totalSources {
viewModel.switchSource()
guard let nextIndex = viewModel.currentChannel?.sourceIndex else { return false }
+11
View File
@@ -2,13 +2,16 @@ import SwiftUI
/// - Android SearchActivity
struct SearchView: View {
///
@StateObject private var viewModel = SearchViewModel()
#if os(iOS)
/// iOS
private let columns = [
GridItem(.adaptive(minimum: 120, maximum: 160), spacing: 12)
]
#else
/// macOS
private let columns = [
GridItem(.adaptive(minimum: 140, maximum: 180), spacing: 16)
]
@@ -29,6 +32,7 @@ struct SearchView: View {
} else if !viewModel.results.isEmpty {
searchResults
} else if viewModel.keyword.isEmpty {
//
searchHistorySection
} else if let error = viewModel.errorMessage {
Spacer()
@@ -53,6 +57,7 @@ struct SearchView: View {
// MARK: -
///
private var searchBar: some View {
HStack(spacing: 12) {
HStack(spacing: 10) {
@@ -114,6 +119,7 @@ struct SearchView: View {
// MARK: -
///
private var searchResults: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {
@@ -134,6 +140,7 @@ struct SearchView: View {
// MARK: -
///
private var searchHistorySection: some View {
VStack(alignment: .leading, spacing: 12) {
if !viewModel.searchHistory.isEmpty {
@@ -183,13 +190,16 @@ struct SearchView: View {
///
struct FlowLayout: Layout {
///
var spacing: CGFloat = 8
///
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let result = arrangement(proposal: proposal, subviews: subviews)
return result.size
}
///
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let result = arrangement(proposal: ProposedViewSize(width: bounds.width, height: bounds.height), subviews: subviews)
for (index, position) in result.positions.enumerated() {
@@ -197,6 +207,7 @@ struct FlowLayout: Layout {
}
}
///
private func arrangement(proposal: ProposedViewSize, subviews: Subviews) -> (size: CGSize, positions: [CGPoint]) {
let maxWidth = proposal.width ?? .infinity
var positions: [CGPoint] = []