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:
53
Yattee/Models/Navigation/AppTab.swift
Normal file
53
Yattee/Models/Navigation/AppTab.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// AppTab.swift
|
||||
// Yattee
|
||||
//
|
||||
// Main app tab definitions.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum AppTab: String, CaseIterable, Identifiable {
|
||||
case home
|
||||
case subscriptions
|
||||
case search
|
||||
#if os(tvOS)
|
||||
case settings
|
||||
#endif
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .home: return String(localized: "tabs.home")
|
||||
case .subscriptions: return String(localized: "tabs.subscriptions")
|
||||
case .search: return String(localized: "tabs.search")
|
||||
#if os(tvOS)
|
||||
case .settings: return String(localized: "tabs.settings")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
var systemImage: String {
|
||||
switch self {
|
||||
case .home: return "house.fill"
|
||||
case .subscriptions: return "play.square.stack.fill"
|
||||
case .search: return "magnifyingglass"
|
||||
#if os(tvOS)
|
||||
case .settings: return "gearshape"
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// SidebarItem equivalent for UnifiedTabView navigation.
|
||||
var sidebarItem: SidebarItem {
|
||||
switch self {
|
||||
case .home: return .home
|
||||
case .subscriptions: return .subscriptionsFeed
|
||||
case .search: return .search
|
||||
#if os(tvOS)
|
||||
case .settings: return .settings
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Yattee/Models/Navigation/ChannelTab.swift
Normal file
51
Yattee/Models/Navigation/ChannelTab.swift
Normal file
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// ChannelTab.swift
|
||||
// Yattee
|
||||
//
|
||||
// Navigation tabs for channel content views.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Represents different content tabs available on a channel page.
|
||||
enum ChannelTab: String, CaseIterable, Identifiable {
|
||||
case about
|
||||
case videos
|
||||
case playlists
|
||||
case shorts
|
||||
case streams
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
/// Localized title for the tab.
|
||||
var title: String {
|
||||
switch self {
|
||||
case .about:
|
||||
return String(localized: "channel.tab.about")
|
||||
case .videos:
|
||||
return String(localized: "channel.tab.videos")
|
||||
case .playlists:
|
||||
return String(localized: "channel.tab.playlists")
|
||||
case .shorts:
|
||||
return String(localized: "channel.tab.shorts")
|
||||
case .streams:
|
||||
return String(localized: "channel.tab.streams")
|
||||
}
|
||||
}
|
||||
|
||||
/// SF Symbol name for the tab icon.
|
||||
var systemImage: String {
|
||||
switch self {
|
||||
case .about:
|
||||
return "info.circle.fill"
|
||||
case .videos:
|
||||
return "play.rectangle.fill"
|
||||
case .playlists:
|
||||
return "list.bullet.rectangle.fill"
|
||||
case .shorts:
|
||||
return "bolt.fill"
|
||||
case .streams:
|
||||
return "video.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Yattee/Models/Navigation/HomeTab.swift
Normal file
33
Yattee/Models/Navigation/HomeTab.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// HomeTab.swift
|
||||
// Yattee
|
||||
//
|
||||
// Home tab selection definitions.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Home tab selection.
|
||||
enum HomeTab: String, CaseIterable, Identifiable {
|
||||
case playlists
|
||||
case history
|
||||
case downloads
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .playlists: return String(localized: "home.playlists.title")
|
||||
case .history: return String(localized: "home.history.title")
|
||||
case .downloads: return String(localized: "home.downloads.title")
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .playlists: return "list.bullet.rectangle"
|
||||
case .history: return "clock"
|
||||
case .downloads: return "arrow.down.circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Yattee/Models/Navigation/PlayerInfoTab.swift
Normal file
20
Yattee/Models/Navigation/PlayerInfoTab.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// PlayerInfoTab.swift
|
||||
// Yattee
|
||||
//
|
||||
// Player info tab definitions.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum PlayerInfoTab: String, CaseIterable {
|
||||
case description
|
||||
case comments
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .description: return String(localized: "player.description")
|
||||
case .comments: return String(localized: "player.comments")
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Yattee/Models/Navigation/SearchResultType.swift
Normal file
27
Yattee/Models/Navigation/SearchResultType.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// SearchResultType.swift
|
||||
// Yattee
|
||||
//
|
||||
// Search result type filter definitions.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Search result type filter.
|
||||
enum SearchResultType: String, CaseIterable, Identifiable {
|
||||
case all
|
||||
case videos
|
||||
case channels
|
||||
case playlists
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all: return String(localized: "search.filter.all")
|
||||
case .videos: return String(localized: "search.filter.videos")
|
||||
case .channels: return String(localized: "search.filter.channels")
|
||||
case .playlists: return String(localized: "search.filter.playlists")
|
||||
}
|
||||
}
|
||||
}
|
||||
235
Yattee/Models/Navigation/SidebarItem.swift
Normal file
235
Yattee/Models/Navigation/SidebarItem.swift
Normal file
@@ -0,0 +1,235 @@
|
||||
//
|
||||
// SidebarItem.swift
|
||||
// Yattee
|
||||
//
|
||||
// Represents items that can appear in the sidebar navigation.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Represents all possible sidebar items for navigation.
|
||||
enum SidebarItem: Hashable, Identifiable {
|
||||
// MARK: - Fixed Navigation Items
|
||||
case home
|
||||
case search
|
||||
case sources
|
||||
case settings
|
||||
case nowPlaying
|
||||
|
||||
// MARK: - Dynamic Channel Items
|
||||
case channel(channelID: String, name: String, source: ContentSource)
|
||||
|
||||
// MARK: - Dynamic Playlist Items
|
||||
case playlist(id: UUID, title: String)
|
||||
|
||||
// MARK: - Dynamic Media Source Items
|
||||
case mediaSource(id: UUID, name: String, type: MediaSourceType)
|
||||
|
||||
// MARK: - Dynamic Instance Items
|
||||
case instance(id: UUID, name: String, type: InstanceType)
|
||||
|
||||
// MARK: - Collection Items
|
||||
case bookmarks
|
||||
case history
|
||||
case downloads
|
||||
case subscriptionsFeed
|
||||
case manageChannels
|
||||
|
||||
// MARK: - Identifiable
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .home:
|
||||
return "home"
|
||||
case .search:
|
||||
return "search"
|
||||
case .sources:
|
||||
return "sources"
|
||||
case .settings:
|
||||
return "settings"
|
||||
case .nowPlaying:
|
||||
return "now-playing"
|
||||
case .channel(let channelID, _, let source):
|
||||
return "channel-\(source.provider)-\(channelID)"
|
||||
case .playlist(let id, _):
|
||||
return "playlist-\(id.uuidString)"
|
||||
case .mediaSource(let id, _, _):
|
||||
return "mediasource-\(id.uuidString)"
|
||||
case .instance(let id, _, _):
|
||||
return "instance-\(id.uuidString)"
|
||||
case .bookmarks:
|
||||
return "bookmarks"
|
||||
case .history:
|
||||
return "history"
|
||||
case .downloads:
|
||||
return "downloads"
|
||||
case .subscriptionsFeed:
|
||||
return "subscriptions-feed"
|
||||
case .manageChannels:
|
||||
return "manage-channels"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Display Properties
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .home:
|
||||
return String(localized: "tabs.home")
|
||||
case .search:
|
||||
return String(localized: "tabs.search")
|
||||
case .sources:
|
||||
return String(localized: "tabs.sources")
|
||||
case .settings:
|
||||
return String(localized: "tabs.settings")
|
||||
case .nowPlaying:
|
||||
return String(localized: "sidebar.nowPlaying")
|
||||
case .channel(_, let name, _):
|
||||
return name
|
||||
case .playlist(_, let title):
|
||||
return title
|
||||
case .mediaSource(_, let name, _):
|
||||
return name
|
||||
case .instance(_, let name, _):
|
||||
return name
|
||||
case .bookmarks:
|
||||
return String(localized: "home.bookmarks.title")
|
||||
case .history:
|
||||
return String(localized: "home.history.title")
|
||||
case .downloads:
|
||||
return String(localized: "home.downloads.title")
|
||||
case .subscriptionsFeed:
|
||||
return String(localized: "home.subscriptions.title")
|
||||
case .manageChannels:
|
||||
return String(localized: "sidebar.manageChannels")
|
||||
}
|
||||
}
|
||||
|
||||
var systemImage: String {
|
||||
switch self {
|
||||
case .home:
|
||||
return "house.fill"
|
||||
case .search:
|
||||
return "magnifyingglass"
|
||||
case .sources:
|
||||
return "server.rack"
|
||||
case .settings:
|
||||
return "gear"
|
||||
case .nowPlaying:
|
||||
return "play.circle.fill"
|
||||
case .channel:
|
||||
return "person.circle"
|
||||
case .playlist:
|
||||
return "list.bullet.rectangle"
|
||||
case .mediaSource(_, _, let type):
|
||||
return type.systemImage
|
||||
case .instance(_, _, let type):
|
||||
return type.systemImage
|
||||
case .bookmarks:
|
||||
return "bookmark.fill"
|
||||
case .history:
|
||||
return "clock"
|
||||
case .downloads:
|
||||
return "arrow.down.circle"
|
||||
case .subscriptionsFeed:
|
||||
return "play.square.stack.fill"
|
||||
case .manageChannels:
|
||||
return "person.2"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
/// Converts this sidebar item to a NavigationDestination for pushing onto the navigation stack.
|
||||
/// Returns nil for items that are root views (home, search) which don't push.
|
||||
func navigationDestination() -> NavigationDestination? {
|
||||
switch self {
|
||||
case .home, .search, .sources, .settings, .nowPlaying:
|
||||
// These are root tabs, not push destinations
|
||||
return nil
|
||||
case .channel(let channelID, _, let source):
|
||||
return .channel(channelID, source)
|
||||
case .playlist(let id, let title):
|
||||
return .playlist(.local(id, title: title))
|
||||
case .mediaSource(let id, _, _):
|
||||
return .mediaSource(id)
|
||||
case .instance:
|
||||
// Instances are root views in the sidebar, not push destinations
|
||||
return nil
|
||||
case .bookmarks:
|
||||
return .bookmarks
|
||||
case .history:
|
||||
return .history
|
||||
case .downloads:
|
||||
return .downloads
|
||||
case .subscriptionsFeed:
|
||||
return .subscriptionsFeed
|
||||
case .manageChannels:
|
||||
return .manageChannels
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Item Categories
|
||||
|
||||
/// Whether this is a fixed navigation item (always visible).
|
||||
var isFixedNavigation: Bool {
|
||||
switch self {
|
||||
case .home, .search, .sources, .settings, .nowPlaying:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this is a dynamic channel item.
|
||||
var isChannel: Bool {
|
||||
if case .channel = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
/// Whether this is a dynamic playlist item.
|
||||
var isPlaylist: Bool {
|
||||
if case .playlist = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
/// Whether this is a dynamic media source item.
|
||||
var isMediaSource: Bool {
|
||||
if case .mediaSource = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
/// Whether this is a dynamic instance item.
|
||||
var isInstance: Bool {
|
||||
if case .instance = self { return true }
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Factory Methods
|
||||
|
||||
extension SidebarItem {
|
||||
/// Creates a SidebarItem from a Subscription.
|
||||
static func from(subscription: Subscription) -> SidebarItem {
|
||||
.channel(
|
||||
channelID: subscription.channelID,
|
||||
name: subscription.name,
|
||||
source: subscription.contentSource
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a SidebarItem from a LocalPlaylist.
|
||||
static func from(playlist: LocalPlaylist) -> SidebarItem {
|
||||
.playlist(id: playlist.id, title: playlist.title)
|
||||
}
|
||||
|
||||
/// Creates a SidebarItem from a MediaSource.
|
||||
static func from(mediaSource: MediaSource) -> SidebarItem {
|
||||
.mediaSource(id: mediaSource.id, name: mediaSource.name, type: mediaSource.type)
|
||||
}
|
||||
|
||||
/// Creates a SidebarItem from an Instance.
|
||||
static func from(instance: Instance) -> SidebarItem {
|
||||
.instance(id: instance.id, name: instance.displayName, type: instance.type)
|
||||
}
|
||||
}
|
||||
144
Yattee/Models/Navigation/SidebarMainItem.swift
Normal file
144
Yattee/Models/Navigation/SidebarMainItem.swift
Normal file
@@ -0,0 +1,144 @@
|
||||
//
|
||||
// SidebarMainItem.swift
|
||||
// Yattee
|
||||
//
|
||||
// Configurable sidebar main navigation item definitions.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Represents a configurable main navigation item in the sidebar.
|
||||
enum SidebarMainItem: String, CaseIterable, Codable, Identifiable, Sendable {
|
||||
case search
|
||||
case home
|
||||
case subscriptions
|
||||
case bookmarks
|
||||
case history
|
||||
case downloads
|
||||
case channels
|
||||
case sources
|
||||
case settings
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
/// Default order for sidebar main items.
|
||||
static var defaultOrder: [SidebarMainItem] {
|
||||
[.search, .home, .subscriptions, .bookmarks, .history, .channels, .sources, .downloads, .settings]
|
||||
}
|
||||
|
||||
/// Default visibility (all visible except subscriptions and channels).
|
||||
static var defaultVisibility: [SidebarMainItem: Bool] {
|
||||
[
|
||||
.search: true,
|
||||
.home: true,
|
||||
.subscriptions: false,
|
||||
.bookmarks: false,
|
||||
.history: false,
|
||||
.downloads: true,
|
||||
.channels: false,
|
||||
.sources: true,
|
||||
.settings: true
|
||||
]
|
||||
}
|
||||
|
||||
/// SF Symbol icon name.
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .search: "magnifyingglass"
|
||||
case .home: "house.fill"
|
||||
case .subscriptions: "play.square.stack.fill"
|
||||
case .bookmarks: "bookmark.fill"
|
||||
case .history: "clock"
|
||||
case .downloads: "arrow.down.circle"
|
||||
case .channels: "person.2"
|
||||
case .sources: "server.rack"
|
||||
case .settings: "gear"
|
||||
}
|
||||
}
|
||||
|
||||
/// Localized display title.
|
||||
var localizedTitle: String {
|
||||
switch self {
|
||||
case .search: String(localized: "sidebar.mainItem.search")
|
||||
case .home: String(localized: "sidebar.mainItem.home")
|
||||
case .subscriptions: String(localized: "sidebar.mainItem.subscriptions")
|
||||
case .bookmarks: String(localized: "sidebar.mainItem.bookmarks")
|
||||
case .history: String(localized: "sidebar.mainItem.history")
|
||||
case .downloads: String(localized: "sidebar.mainItem.downloads")
|
||||
case .channels: String(localized: "sidebar.mainItem.channels")
|
||||
case .sources: String(localized: "sidebar.mainItem.sources")
|
||||
case .settings: String(localized: "sidebar.mainItem.settings")
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this item is required and cannot be hidden.
|
||||
var isRequired: Bool {
|
||||
switch self {
|
||||
case .search, .home:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this item is available on the current platform.
|
||||
var isAvailableOnCurrentPlatform: Bool {
|
||||
switch self {
|
||||
case .downloads:
|
||||
#if os(tvOS)
|
||||
return false
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tab Value Mappings
|
||||
|
||||
/// Tab value for CompactTabView (String-based).
|
||||
/// Fixed tabs use "home" and "search", configurable tabs use TabBarItem.rawValue.
|
||||
var compactTabValue: String {
|
||||
switch self {
|
||||
case .search: return "search"
|
||||
case .home: return "home"
|
||||
case .subscriptions: return TabBarItem.subscriptions.rawValue
|
||||
case .bookmarks: return TabBarItem.bookmarks.rawValue
|
||||
case .history: return TabBarItem.history.rawValue
|
||||
case .downloads: return TabBarItem.downloads.rawValue
|
||||
case .channels: return TabBarItem.channels.rawValue
|
||||
case .sources: return TabBarItem.sources.rawValue
|
||||
case .settings: return TabBarItem.settings.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
/// SidebarItem value for UnifiedTabView.
|
||||
var sidebarItem: SidebarItem {
|
||||
switch self {
|
||||
case .search: return .search
|
||||
case .home: return .home
|
||||
case .subscriptions: return .subscriptionsFeed
|
||||
case .bookmarks: return .bookmarks
|
||||
case .history: return .history
|
||||
case .downloads: return .downloads
|
||||
case .channels: return .manageChannels
|
||||
case .sources: return .sources
|
||||
case .settings: return .settings
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize from TabBarItem (for reverse mapping).
|
||||
init?(tabBarItem: TabBarItem) {
|
||||
switch tabBarItem {
|
||||
case .subscriptions: self = .subscriptions
|
||||
case .channels: self = .channels
|
||||
case .bookmarks: self = .bookmarks
|
||||
case .playlists: return nil // No direct mapping - playlists isn't a SidebarMainItem
|
||||
case .history: self = .history
|
||||
case .downloads: self = .downloads
|
||||
case .sources: self = .sources
|
||||
case .settings: self = .settings
|
||||
}
|
||||
}
|
||||
}
|
||||
60
Yattee/Models/Navigation/TabBarItem.swift
Normal file
60
Yattee/Models/Navigation/TabBarItem.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// TabBarItem.swift
|
||||
// Yattee
|
||||
//
|
||||
// Configurable tab bar item definitions for compact size class navigation.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Represents a configurable tab bar item for compact width (iPhone, iPad small window).
|
||||
enum TabBarItem: String, CaseIterable, Codable, Identifiable, Sendable {
|
||||
case subscriptions
|
||||
case channels
|
||||
case bookmarks
|
||||
case playlists
|
||||
case history
|
||||
case downloads
|
||||
case sources
|
||||
case settings
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
/// Default order for tab bar items.
|
||||
static var defaultOrder: [TabBarItem] {
|
||||
[.subscriptions, .channels, .bookmarks, .playlists, .history, .sources, .downloads, .settings]
|
||||
}
|
||||
|
||||
/// Default visibility (only subscriptions visible by default).
|
||||
static var defaultVisibility: [TabBarItem: Bool] {
|
||||
[.subscriptions: false, .channels: false, .bookmarks: false, .playlists: false, .history: false, .downloads: true, .sources: true, .settings: false]
|
||||
}
|
||||
|
||||
/// SF Symbol icon name.
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .subscriptions: "play.square.stack.fill"
|
||||
case .channels: "person.crop.rectangle.stack.fill"
|
||||
case .bookmarks: "bookmark.fill"
|
||||
case .playlists: "list.bullet.rectangle"
|
||||
case .history: "clock"
|
||||
case .downloads: "arrow.down.circle"
|
||||
case .sources: "server.rack"
|
||||
case .settings: "gear"
|
||||
}
|
||||
}
|
||||
|
||||
/// Localized display title.
|
||||
var localizedTitle: String {
|
||||
switch self {
|
||||
case .subscriptions: String(localized: "tabBar.item.subscriptions")
|
||||
case .channels: String(localized: "tabBar.item.channels")
|
||||
case .bookmarks: String(localized: "tabBar.item.bookmarks")
|
||||
case .playlists: String(localized: "tabBar.item.playlists")
|
||||
case .history: String(localized: "tabBar.item.history")
|
||||
case .downloads: String(localized: "tabBar.item.downloads")
|
||||
case .sources: String(localized: "tabBar.item.sources")
|
||||
case .settings: String(localized: "tabBar.item.settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user