增加注解

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
+33
View File
@@ -2,9 +2,13 @@ import Foundation
import SwiftUI
struct PlaybackQualityOption: Identifiable, Hashable {
///
static let autoIdentifier = "auto"
/// 使 auto id
let id: String
/// UI 1080p / 720p /
let name: String
///
let url: String
var isAuto: Bool {
@@ -19,23 +23,39 @@ struct PlaybackQualityOption: Identifiable, Hashable {
/// ViewModel
@MainActor
class DetailViewModel: ObservableObject {
///
@Published var vodInfo: VodInfo?
///
@Published var isLoading = false
///
@Published var errorMessage: String?
/// 线
@Published var selectedFlag: String = ""
///
@Published var selectedEpisodeIndex: Int = 0
///
@Published var isPlaying = false
///
@Published var playUrl: String?
///
@Published var resumeSeconds: Double = 0
///
@Published var qualityOptions: [PlaybackQualityOption] = []
/// id
@Published var selectedQualityId: String = PlaybackQualityOption.autoIdentifier
/// UI
private var realtimeProgressSeconds: Double = 0
///
private let sourceService = SourceService.shared
private let network = NetworkManager.shared
///
private var qualityBaseEpisodeURL: String = ""
/// key URL
private var qualityOptionCache: [String: [PlaybackQualityOption]] = [:]
///
private var qualityResolveTask: Task<Void, Never>?
///
private var qualityResolveToken = UUID()
///
@@ -105,6 +125,7 @@ class DetailViewModel: ObservableObject {
realtimeProgressSeconds = 0
if let episode = vodInfo?.currentEpisode {
// URL
let shouldResetQuality = qualityBaseEpisodeURL != episode.url
updateQualityOptions(for: episode.url, resetSelection: shouldResetQuality)
playUrl = selectedPlayableURL(fallback: episode.url)
@@ -144,6 +165,7 @@ class DetailViewModel: ObservableObject {
selectedQualityId = option.id
guard isPlaying else { return }
// 使使
let targetURL = option.url.isEmpty ? qualityBaseEpisodeURL : option.url
guard !targetURL.isEmpty, playUrl != targetURL else { return }
@@ -208,6 +230,7 @@ class DetailViewModel: ObservableObject {
}
private func selectedPlayableURL(fallback: String) -> String {
// URL使退
let selected = qualityOptions.first(where: { $0.id == selectedQualityId })?.url
if let selected, !selected.isEmpty {
return selected
@@ -215,6 +238,7 @@ class DetailViewModel: ObservableObject {
return fallback
}
///
private func resetQualityState() {
qualityResolveTask?.cancel()
qualityResolveTask = nil
@@ -231,6 +255,7 @@ class DetailViewModel: ObservableObject {
return
}
//
qualityResolveTask?.cancel()
qualityResolveTask = nil
@@ -245,6 +270,7 @@ class DetailViewModel: ObservableObject {
qualityOptions = [autoOption]
if let cached = qualityOptionCache[trimmedEpisodeURL] {
//
qualityOptions = cached
if resetSelection || !cached.contains(where: { $0.id == selectedQualityId }) {
selectedQualityId = PlaybackQualityOption.autoIdentifier
@@ -277,12 +303,14 @@ class DetailViewModel: ObservableObject {
}
}
/// HLS
private func resolveQualityOptions(for episodeURL: String) async -> [PlaybackQualityOption] {
guard let url = URL(string: episodeURL), Self.looksLikeHLSURL(url) else { return [] }
guard let playlist = try? await network.getString(from: episodeURL) else { return [] }
return Self.parseMasterPlaylist(playlist, masterURL: url)
}
/// HLS
private struct HLSVariant {
let url: String
let name: String?
@@ -290,6 +318,7 @@ class DetailViewModel: ObservableObject {
let bandwidth: Int?
}
/// URL HLS
private static func looksLikeHLSURL(_ url: URL) -> Bool {
let lowercased = url.absoluteString.lowercased()
if lowercased.contains(".m3u8") { return true }
@@ -297,6 +326,8 @@ class DetailViewModel: ObservableObject {
return ext == "m3u8" || ext == "m3u"
}
/// HLS
/// 2
private static func parseMasterPlaylist(_ content: String, masterURL: URL) -> [PlaybackQualityOption] {
guard content.localizedCaseInsensitiveContains("#EXT-X-STREAM-INF") else { return [] }
@@ -407,6 +438,7 @@ class DetailViewModel: ObservableObject {
return merged
}
/// `EXT-X-STREAM-INF`
private static func parseAttributeMap(_ raw: String) -> [String: String] {
var result: [String: String] = [:]
let pairs = splitAttributes(raw)
@@ -426,6 +458,7 @@ class DetailViewModel: ObservableObject {
return result
}
///
private static func splitAttributes(_ raw: String) -> [String] {
var parts: [String] = []
var buffer = ""
+14 -1
View File
@@ -4,15 +4,24 @@ import SwiftUI
/// ViewModel
@MainActor
class HomeViewModel: ObservableObject {
///
@Published var sorts: [MovieSort.SortData] = []
///
@Published var selectedSort: MovieSort.SortData?
///
@Published var homeVideos: [Movie.Video] = []
///
@Published var categoryVideos: [Movie.Video] = []
///
@Published var isLoading = false
///
@Published var currentPage = 1
///
@Published var hasMore = true
///
@Published var errorMessage: String?
/// 访
private let sourceService = SourceService.shared
///
@@ -24,7 +33,7 @@ class HomeViewModel: ObservableObject {
do {
let result = try await sourceService.getSort(sourceBean: source)
// ""
// UI Android
var allSorts = [MovieSort.SortData.home()]
allSorts.append(contentsOf: result.sorts)
@@ -43,6 +52,7 @@ class HomeViewModel: ObservableObject {
///
func selectSort(_ sort: MovieSort.SortData) {
//
selectedSort = sort
errorMessage = nil
categoryVideos = []
@@ -62,6 +72,7 @@ class HomeViewModel: ObservableObject {
private func loadCategoryVideos(page: Int, sort: MovieSort.SortData) async {
guard sort.id != "home" else { return }
guard let source = ApiConfig.shared.homeSourceBean else { return }
//
guard !isLoading else { return }
isLoading = true
@@ -78,6 +89,7 @@ class HomeViewModel: ObservableObject {
} else {
categoryVideos.append(contentsOf: videos)
}
//
currentPage = page
hasMore = !videos.isEmpty
} catch {
@@ -105,6 +117,7 @@ class HomeViewModel: ObservableObject {
///
func refresh() async {
//
currentPage = 1
hasMore = true
categoryVideos = []
+11 -1
View File
@@ -4,12 +4,19 @@ import SwiftUI
/// ViewModel
@MainActor
class LiveViewModel: ObservableObject {
///
@Published var channelGroups: [LiveChannelGroup] = []
///
@Published var selectedGroupIndex: Int = 0
///
@Published var selectedChannelIndex: Int = 0
///
@Published var currentChannel: LiveChannelItem?
///
@Published var epgList: [Epginfo] = []
/// 便 EPG
@Published var isLoading = false
/// TV
@Published var showChannelList = false
///
@@ -67,6 +74,7 @@ class LiveViewModel: ObservableObject {
/// 线
func switchSource() {
// `currentChannel` mutating @Published
currentChannel?.nextSource()
}
@@ -78,13 +86,15 @@ class LiveViewModel: ObservableObject {
/// EPG
private func loadEPG(for channel: LiveChannelItem) {
// EPG -
// / ID EPG
//
epgList = []
}
}
// 访
extension Collection {
/// `nil`
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
+13 -1
View File
@@ -4,14 +4,21 @@ import SwiftUI
/// ViewModel
@MainActor
class SearchViewModel: ObservableObject {
///
@Published var keyword: String = ""
///
@Published var results: [Movie.Video] = []
///
@Published var isSearching = false
///
@Published var searchHistory: [String] = []
///
@Published var errorMessage: String?
///
private let sourceService = SourceService.shared
///
init() {
loadSearchHistory()
}
@@ -25,9 +32,10 @@ class SearchViewModel: ObservableObject {
errorMessage = nil
results = []
//
//
addToHistory(trimmed)
//
let videos = await sourceService.searchAll(keyword: trimmed)
self.results = videos
@@ -57,10 +65,12 @@ class SearchViewModel: ObservableObject {
// MARK: -
///
private func loadSearchHistory() {
searchHistory = UserDefaults.standard.stringArray(forKey: HawkConfig.SEARCH_HISTORY) ?? []
}
/// 20
private func addToHistory(_ keyword: String) {
searchHistory.removeAll { $0 == keyword }
searchHistory.insert(keyword, at: 0)
@@ -70,11 +80,13 @@ class SearchViewModel: ObservableObject {
UserDefaults.standard.set(searchHistory, forKey: HawkConfig.SEARCH_HISTORY)
}
///
func clearHistory() {
searchHistory = []
UserDefaults.standard.removeObject(forKey: HawkConfig.SEARCH_HISTORY)
}
///
func removeFromHistory(_ keyword: String) {
searchHistory.removeAll { $0 == keyword }
UserDefaults.standard.set(searchHistory, forKey: HawkConfig.SEARCH_HISTORY)
+38
View File
@@ -4,7 +4,9 @@ import SwiftUI
/// ViewModel
@MainActor
class SettingsViewModel: ObservableObject {
///
struct PendingMultiRepoSelection: Identifiable {
///
enum Target {
case vod
case live
@@ -18,30 +20,54 @@ class SettingsViewModel: ObservableObject {
}
let id = UUID()
///
let target: Target
/// live
let sourceUrl: String
///
let options: [ApiConfig.MultiRepoOption]
}
///
@Published var vodApiUrl: String = ""
///
@Published var liveApiUrl: String = ""
///
@Published var isLoadingConfig = false
///
@Published var configError: String?
/// UI /
@Published var configSuccess = false
/// nil
@Published var pendingMultiRepoSelection: PendingMultiRepoSelection?
/// API
@Published var apiHistory: [String] = []
///
@Published var vodPlayerEngine: PlayerEngine = .system
///
@Published var livePlayerEngine: PlayerEngine = .system
///
@Published var decodeMode: VideoDecodeMode = .auto
/// VLC
@Published var vlcBufferMode: VLCBufferMode = .defaultMode
/// /退
@Published var playTimeStep: Int = 10
///
@Published var cacheSizeString: String = "0 KB"
///
let playTimeStepOptions: [Int] = [5, 10, 15, 30, 60]
///
let playerEngineOptions: [PlayerEngine] = PlayerEngine.availableEngines
///
let decodeModeOptions: [VideoDecodeMode] = VideoDecodeMode.allCases
/// VLC
let vlcBufferModeOptions: [VLCBufferMode] = VLCBufferMode.allCases
///
/// 1)
/// 2)
/// 3) /
init() {
let defaults = UserDefaults.standard
let savedVod = defaults.string(forKey: HawkConfig.API_URL) ?? ""
@@ -99,6 +125,7 @@ class SettingsViewModel: ObservableObject {
do {
let resolvedLive = trimmedLive.isEmpty ? trimmedVod : trimmedLive
//
if let pending = try await detectPendingMultiRepoSelection(
vodUrl: trimmedVod,
liveUrl: resolvedLive
@@ -109,6 +136,7 @@ class SettingsViewModel: ObservableObject {
}
try await ApiConfig.shared.loadConfigs(vodApiUrl: trimmedVod, liveApiUrl: resolvedLive)
// live
UserDefaults.standard.set(trimmedVod, forKey: HawkConfig.API_URL)
UserDefaults.standard.set(trimmedLive, forKey: HawkConfig.LIVE_API_URL)
vodApiUrl = trimmedVod
@@ -123,6 +151,7 @@ class SettingsViewModel: ObservableObject {
isLoadingConfig = false
}
///
func selectPendingMultiRepoOption(_ option: ApiConfig.MultiRepoOption) async {
guard let pending = pendingMultiRepoSelection else { return }
let normalizedSource = ApiConfig.normalizeConfigUrl(pending.sourceUrl)
@@ -130,6 +159,7 @@ class SettingsViewModel: ObservableObject {
switch pending.target {
case .vod:
let normalizedLive = ApiConfig.normalizeConfigUrl(liveApiUrl)
// live vod
let shouldSyncLive = !liveApiUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
&& normalizedLive == normalizedSource
vodApiUrl = option.url
@@ -144,11 +174,14 @@ class SettingsViewModel: ObservableObject {
await loadConfig()
}
///
func cancelPendingMultiRepoSelection() {
pendingMultiRepoSelection = nil
isLoadingConfig = false
}
///
/// - Returns: `nil`
private func detectPendingMultiRepoSelection(
vodUrl: String,
liveUrl: String
@@ -186,10 +219,12 @@ class SettingsViewModel: ObservableObject {
// MARK: - API
/// API
private func loadApiHistory() {
apiHistory = UserDefaults.standard.stringArray(forKey: "api_history") ?? []
}
/// 10
private func addToApiHistory(_ url: String) {
apiHistory.removeAll { $0 == url }
apiHistory.insert(url, at: 0)
@@ -199,6 +234,7 @@ class SettingsViewModel: ObservableObject {
UserDefaults.standard.set(apiHistory, forKey: "api_history")
}
/// API
func removeApiHistory(_ url: String) {
apiHistory.removeAll { $0 == url }
UserDefaults.standard.set(apiHistory, forKey: "api_history")
@@ -247,12 +283,14 @@ class SettingsViewModel: ObservableObject {
UserDefaults.standard.set(mode.rawValue, forKey: HawkConfig.PLAY_VLC_BUFFER_MODE)
}
/// +
private func refreshCacheSize() {
let sharedDisk = URLCache.shared.currentDiskUsage
let imageDisk = ImageLoader.shared.cacheUsage.disk
cacheSizeString = Self.formatSize(bytes: sharedDisk + imageDisk)
}
///
private static func formatSize(bytes: Int) -> String {
let size = max(0, bytes)
if size < 1024 * 1024 {