mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
204
Yattee/Services/Navigation/NavigationDestination.swift
Normal file
204
Yattee/Services/Navigation/NavigationDestination.swift
Normal file
@@ -0,0 +1,204 @@
|
||||
//
|
||||
// NavigationDestination.swift
|
||||
// Yattee
|
||||
//
|
||||
// Navigation destinations for the app.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// Source for video navigation - either a loaded video or just an ID to fetch.
|
||||
enum VideoSource: Hashable, Sendable {
|
||||
case id(VideoID)
|
||||
case loaded(Video)
|
||||
}
|
||||
|
||||
/// Navigation destinations for the app.
|
||||
enum NavigationDestination: Hashable {
|
||||
/// Video info page - can be initialized with either a loaded video or just an ID.
|
||||
case video(VideoSource, queueContext: VideoQueueContext? = nil)
|
||||
case channel(String, ContentSource)
|
||||
case playlist(PlaylistSource)
|
||||
case continueWatching
|
||||
case downloads
|
||||
case downloadsStorage
|
||||
case subscriptionsFeed
|
||||
case settings
|
||||
case playlists
|
||||
case bookmarks
|
||||
case history
|
||||
case manageChannels
|
||||
case search(String)
|
||||
/// External video URL to be extracted via Yattee Server.
|
||||
case externalVideo(URL)
|
||||
/// External channel URL to be extracted via Yattee Server.
|
||||
case externalChannel(URL)
|
||||
/// Direct media URL (mp4, m3u8, etc.) to play without extraction.
|
||||
case directMedia(URL)
|
||||
/// Media sources list.
|
||||
case mediaSources
|
||||
/// Browse a specific media source by ID (for sidebar navigation).
|
||||
case mediaSource(UUID)
|
||||
/// Browse a specific media source at a path.
|
||||
case mediaBrowser(MediaSource, path: String, showOnlyPlayable: Bool = false)
|
||||
/// Browse a specific instance (Popular/Trending).
|
||||
case instanceBrowse(Instance, initialTab: InstanceBrowseView.BrowseTab? = nil)
|
||||
/// Import subscriptions from an instance.
|
||||
case importSubscriptions(instance: Instance)
|
||||
/// Import playlists from an instance.
|
||||
case importPlaylists(instance: Instance)
|
||||
}
|
||||
|
||||
extension NavigationDestination {
|
||||
/// Returns the transition ID for zoom navigation animations, if applicable.
|
||||
///
|
||||
/// Used to connect source views (NavigationLinks) with destination views
|
||||
/// for smooth zoom transitions. Returns nil for destinations that don't
|
||||
/// support zoom transitions.
|
||||
var transitionID: AnyHashable? {
|
||||
switch self {
|
||||
case .video(let source, _):
|
||||
switch source {
|
||||
case .id(let videoID): return videoID
|
||||
case .loaded(let video): return video.id
|
||||
}
|
||||
case .channel(let channelID, _):
|
||||
return channelID
|
||||
case .playlist(let source):
|
||||
return source.transitionID
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func view() -> some View {
|
||||
switch self {
|
||||
case .video(let source, let queueContext):
|
||||
switch source {
|
||||
case .id(let videoID):
|
||||
VideoInfoView(videoID: videoID)
|
||||
.videoQueueContext(queueContext)
|
||||
case .loaded(let video):
|
||||
VideoInfoView(video: video)
|
||||
.videoQueueContext(queueContext)
|
||||
}
|
||||
case .channel(let channelID, let source):
|
||||
ChannelView(channelID: channelID, source: source)
|
||||
case .playlist(let source):
|
||||
UnifiedPlaylistDetailView(source: source)
|
||||
case .continueWatching:
|
||||
ContinueWatchingView()
|
||||
case .downloads:
|
||||
#if os(tvOS)
|
||||
ContentUnavailableView {
|
||||
Label(String(localized: "home.downloads.title"), systemImage: "arrow.down.circle")
|
||||
} description: {
|
||||
Text(String(localized: "home.downloads.notAvailable"))
|
||||
}
|
||||
#else
|
||||
DownloadsView()
|
||||
#endif
|
||||
case .downloadsStorage:
|
||||
#if os(tvOS)
|
||||
ContentUnavailableView {
|
||||
Label(String(localized: "settings.downloads.storage.title"), systemImage: "arrow.down.circle")
|
||||
} description: {
|
||||
Text(String(localized: "home.downloads.notAvailable"))
|
||||
}
|
||||
#else
|
||||
DownloadsStorageView()
|
||||
#endif
|
||||
case .subscriptionsFeed:
|
||||
SubscriptionsView()
|
||||
case .settings:
|
||||
SettingsView()
|
||||
case .playlists:
|
||||
PlaylistsListView()
|
||||
case .bookmarks:
|
||||
BookmarksListView()
|
||||
case .history:
|
||||
HistoryListView()
|
||||
case .manageChannels:
|
||||
ManageChannelsView()
|
||||
case .search(let query):
|
||||
SearchView(initialQuery: query)
|
||||
case .externalVideo(let url):
|
||||
ExternalVideoView(url: url)
|
||||
case .directMedia(let url):
|
||||
// Direct media URLs are typically handled inline by OpenLinkSheet,
|
||||
// but if navigated to directly, show video info with the created video
|
||||
VideoInfoView(video: DirectMediaHelper.createVideo(from: url))
|
||||
case .externalChannel(let url):
|
||||
// Use unified ChannelView with external channel URL
|
||||
ChannelView(
|
||||
channelID: url.absoluteString,
|
||||
source: .extracted(extractor: "external", originalURL: url),
|
||||
channelURL: url
|
||||
)
|
||||
case .mediaSources:
|
||||
MediaSourcesView()
|
||||
case .mediaSource(let id):
|
||||
MediaSourceByIDView(sourceID: id)
|
||||
case .mediaBrowser(let source, let path, let showOnlyPlayable):
|
||||
MediaBrowserView(source: source, path: path, showOnlyPlayable: showOnlyPlayable)
|
||||
case .instanceBrowse(let instance, let initialTab):
|
||||
InstanceBrowseView(instance: instance, initialTab: initialTab)
|
||||
case .importSubscriptions(let instance):
|
||||
ImportSubscriptionsView(instance: instance)
|
||||
case .importPlaylists(let instance):
|
||||
ImportPlaylistsView(instance: instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Media Source by ID View
|
||||
|
||||
/// Helper view that looks up a MediaSource by ID and shows MediaBrowserView.
|
||||
private struct MediaSourceByIDView: View {
|
||||
let sourceID: UUID
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
|
||||
var body: some View {
|
||||
if let source = appEnvironment?.mediaSourcesManager.source(byID: sourceID) {
|
||||
MediaBrowserView(source: source, path: "/")
|
||||
} else {
|
||||
ContentUnavailableView {
|
||||
Label(String(localized: "navigation.sourceNotFound"), systemImage: "externaldrive.badge.exclamationmark")
|
||||
} description: {
|
||||
Text(String(localized: "navigation.sourceNotFound.description"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Navigation Destination Modifier
|
||||
|
||||
/// A view modifier that registers navigation destination handlers for all app destinations.
|
||||
/// Apply this to views that contain NavigationLink(value: NavigationDestination) to ensure
|
||||
/// the navigation stack can resolve all destination types.
|
||||
///
|
||||
/// Also applies zoom navigation transitions for supported destinations (video, channel, playlist)
|
||||
/// when a zoom transition namespace is available in the environment.
|
||||
struct NavigationDestinationHandlerModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.navigationDestination(for: NavigationDestination.self) { destination in
|
||||
if let transitionID = destination.transitionID {
|
||||
destination.view()
|
||||
.zoomTransitionDestination(id: transitionID)
|
||||
} else {
|
||||
destination.view()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Adds navigation destination handlers for all app navigation destinations.
|
||||
/// Use this on views within a NavigationStack that contain NavigationLink(value:).
|
||||
func withNavigationDestinations() -> some View {
|
||||
modifier(NavigationDestinationHandlerModifier())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user