Features: - VOD library with movie grouping and version detection - TV show library with season/episode organization - TMDB integration for trending shows and recently aired episodes - Recent releases section with TMDB release date sorting - Watch history tracking with continue watching - Playlist caching (12-hour TTL) for offline support - M3U playlist parsing with XStream API support - Authentication with credential storage Technical: - SwiftUI for tvOS - Actor-based services for thread safety - Persistent caching for playlists, TMDB data, and watch history - KSPlayer integration for video playback Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
167 lines
4.8 KiB
Swift
167 lines
4.8 KiB
Swift
import KSPlayer
|
|
import SwiftUI
|
|
|
|
struct ContentView: View {
|
|
#if !os(tvOS)
|
|
@Environment(\.openWindow) private var openWindow
|
|
#endif
|
|
@EnvironmentObject
|
|
private var appModel: APPModel
|
|
private var initialView: some View {
|
|
#if os(macOS)
|
|
NavigationSplitView {
|
|
List(selection: $appModel.tabSelected) {
|
|
link(to: .Home)
|
|
link(to: .Favorite)
|
|
link(to: .Files)
|
|
}
|
|
} detail: {
|
|
appModel.tabSelected.destination(appModel: appModel)
|
|
}
|
|
#else
|
|
TabView(selection: $appModel.tabSelected) {
|
|
tab(to: .Home)
|
|
tab(to: .Favorite)
|
|
tab(to: .Files)
|
|
tab(to: .Setting)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
var body: some View {
|
|
initialView
|
|
.preferredColorScheme(.dark)
|
|
.background(Color.black)
|
|
.sheet(isPresented: $appModel.openURLImport) {
|
|
URLImportView()
|
|
}
|
|
.onChange(of: appModel.openURL) { url in
|
|
if let url {
|
|
#if !os(tvOS)
|
|
openWindow(value: url)
|
|
#endif
|
|
appModel.openURL = nil
|
|
}
|
|
}
|
|
.onChange(of: appModel.openPlayModel) { model in
|
|
if let model {
|
|
#if !os(tvOS)
|
|
openWindow(value: model)
|
|
#endif
|
|
appModel.openPlayModel = nil
|
|
}
|
|
}
|
|
#if !os(tvOS)
|
|
.onDrop(of: ["public.url", "public.file-url"], isTargeted: nil) { items -> Bool in
|
|
guard let item = items.first, let identifier = item.registeredTypeIdentifiers.first else {
|
|
return false
|
|
}
|
|
item.loadItem(forTypeIdentifier: identifier, options: nil) { urlData, _ in
|
|
if let urlData = urlData as? Data {
|
|
let url = NSURL(absoluteURLWithDataRepresentation: urlData, relativeTo: nil) as URL
|
|
DispatchQueue.main.async {
|
|
appModel.open(url: url)
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
.fileImporter(isPresented: $appModel.openFileImport, allowedContentTypes: [.movie, .audio, .data]) { result in
|
|
guard let url = try? result.get() else {
|
|
return
|
|
}
|
|
if url.startAccessingSecurityScopedResource() {
|
|
appModel.open(url: url)
|
|
}
|
|
}
|
|
#endif
|
|
.onOpenURL { url in
|
|
KSLog("onOpenURL")
|
|
appModel.open(url: url)
|
|
}
|
|
}
|
|
|
|
func link(to item: TabBarItem) -> some View {
|
|
item.lable.tag(item)
|
|
}
|
|
|
|
func tab(to item: TabBarItem) -> some View {
|
|
Group {
|
|
if item == .Home {
|
|
NavigationStack(path: $appModel.path) {
|
|
item.destination(appModel: appModel)
|
|
}
|
|
} else {
|
|
NavigationStack {
|
|
item.destination(appModel: appModel)
|
|
}
|
|
}
|
|
}
|
|
.tabItem {
|
|
item.lable.tag(item)
|
|
}.tag(item)
|
|
}
|
|
}
|
|
|
|
enum TabBarItem: Int {
|
|
case Home
|
|
case Favorite
|
|
case Files
|
|
case Setting
|
|
var lable: Label<Text, Image> {
|
|
switch self {
|
|
case .Home:
|
|
return Label("Home", systemImage: "house.fill")
|
|
case .Favorite:
|
|
return Label("Favorite", systemImage: "star.fill")
|
|
case .Files:
|
|
return Label("Files", systemImage: "folder.fill.badge.gearshape")
|
|
case .Setting:
|
|
return Label("Setting", systemImage: "gear")
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
@ViewBuilder
|
|
func destination(appModel: APPModel) -> some View {
|
|
switch self {
|
|
case .Home:
|
|
HomeView(m3uURL: appModel.activeM3UModel?.m3uURL)
|
|
.navigationPlay()
|
|
case .Favorite:
|
|
FavoriteView()
|
|
.navigationPlay()
|
|
case .Files:
|
|
FilesView()
|
|
case .Setting:
|
|
SettingView()
|
|
}
|
|
}
|
|
}
|
|
|
|
public extension View {
|
|
@MainActor
|
|
@ViewBuilder
|
|
func navigationPlay() -> some View {
|
|
navigationDestination(for: URL.self) { url in
|
|
KSVideoPlayerView(url: url)
|
|
#if !os(macOS)
|
|
.toolbar(.hidden, for: .tabBar)
|
|
#endif
|
|
}
|
|
.navigationDestination(for: MovieModel.self) { model in
|
|
model.view
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension MovieModel {
|
|
@MainActor
|
|
var view: some View {
|
|
KSVideoPlayerView(model: self)
|
|
#if !os(macOS)
|
|
.toolbar(.hidden, for: .tabBar)
|
|
#endif
|
|
}
|
|
}
|