From 8df452752aa19812fdcc9a6b3271df50ca215e08 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 1 Nov 2021 22:56:18 +0100 Subject: [PATCH] Managing Favorites sections --- Fixtures/View+Fixtures.swift | 1 + Model/Applications/PipedAPI.swift | 5 ++ Model/Applications/VideosAPI.swift | 2 + Model/FavoriteItem.swift | 39 +++++++++ Model/FavoritesModel.swift | 77 ++++++++++++++++++ Model/NavigationModel.swift | 6 +- Model/Player/PlayerModel.swift | 9 ++- Model/Player/PlayerQueue.swift | 2 +- Model/Player/PlayerQueueItemBridge.swift | 4 +- Model/Search/SearchQuery.swift | 6 +- Model/TrendingCategory.swift | 12 ++- Pearvidious.xcodeproj/project.pbxproj | 86 +++++++++++++------- Shared/Defaults.swift | 4 + Shared/Favorites/DropFavorite.swift | 35 ++++++++ Shared/Favorites/DropFavoriteOutside.swift | 11 +++ Shared/Favorites/FavoriteItemView.swift | 93 +++++++++++++++++++++ Shared/Favorites/FavoritesView.swift | 91 +++++++++++++++++++++ Shared/Navigation/AppTabNavigation.swift | 8 +- Shared/Navigation/Sidebar.swift | 6 +- Shared/Player/VideoPlayerView.swift | 13 ++- Shared/Playlists/PlaylistsView.swift | 7 ++ Shared/Settings/ServicesSettings.swift | 4 +- Shared/Trending/TrendingView.swift | 51 +++++++++--- Shared/Videos/VideoCell.swift | 24 +++--- Shared/Views/ChannelPlaylistView.swift | 21 ++--- Shared/Views/ChannelVideosView.swift | 11 +-- Shared/Views/FavoriteButton.swift | 25 ++++++ Shared/Views/PlaylistVideosView.swift | 5 ++ Shared/Views/PopularView.swift | 5 ++ Shared/Views/SubscriptionsView.swift | 5 ++ Shared/Watch Now/WatchNowSection.swift | 28 ------- Shared/Watch Now/WatchNowSectionBody.swift | 21 ----- Shared/Watch Now/WatchNowView.swift | 51 ------------ tvOS/EditFavorites.swift | 94 ++++++++++++++++++++++ tvOS/TVNavigationView.swift | 6 +- 35 files changed, 665 insertions(+), 203 deletions(-) create mode 100644 Model/FavoriteItem.swift create mode 100644 Model/FavoritesModel.swift create mode 100644 Shared/Favorites/DropFavorite.swift create mode 100644 Shared/Favorites/DropFavoriteOutside.swift create mode 100644 Shared/Favorites/FavoriteItemView.swift create mode 100644 Shared/Favorites/FavoritesView.swift create mode 100644 Shared/Views/FavoriteButton.swift delete mode 100644 Shared/Watch Now/WatchNowSection.swift delete mode 100644 Shared/Watch Now/WatchNowSectionBody.swift delete mode 100644 Shared/Watch Now/WatchNowView.swift create mode 100644 tvOS/EditFavorites.swift diff --git a/Fixtures/View+Fixtures.swift b/Fixtures/View+Fixtures.swift index d1fac91e..38fa11d9 100644 --- a/Fixtures/View+Fixtures.swift +++ b/Fixtures/View+Fixtures.swift @@ -14,6 +14,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier { .environmentObject(RecentsModel()) .environmentObject(SearchModel()) .environmentObject(subscriptions) + .environmentObject(ThumbnailsModel()) } private var invidious: InvidiousAPI { diff --git a/Model/Applications/PipedAPI.swift b/Model/Applications/PipedAPI.swift index 9e7ac9db..fe63ad21 100644 --- a/Model/Applications/PipedAPI.swift +++ b/Model/Applications/PipedAPI.swift @@ -60,6 +60,10 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { resource(baseURL: account.url, path: "channel/\(id)") } + func channelVideos(_ id: String) -> Resource { + channel(id) + } + func channelPlaylist(_ id: String) -> Resource? { resource(baseURL: account.url, path: "playlists/\(id)") } @@ -94,6 +98,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI { func channelSubscription(_: String) -> Resource? { nil } + func playlist(_: String) -> Resource? { nil } func playlistVideo(_: String, _: String) -> Resource? { nil } func playlistVideos(_: String) -> Resource? { nil } diff --git a/Model/Applications/VideosAPI.swift b/Model/Applications/VideosAPI.swift index 54eb3600..ed3dcd55 100644 --- a/Model/Applications/VideosAPI.swift +++ b/Model/Applications/VideosAPI.swift @@ -6,6 +6,7 @@ protocol VideosAPI { var signedIn: Bool { get } func channel(_ id: String) -> Resource + func channelVideos(_ id: String) -> Resource func trending(country: Country, category: TrendingCategory?) -> Resource func search(_ query: SearchQuery) -> Resource func searchSuggestions(query: String) -> Resource @@ -20,6 +21,7 @@ protocol VideosAPI { func channelSubscription(_ id: String) -> Resource? + func playlist(_ id: String) -> Resource? func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource? func playlistVideos(_ id: String) -> Resource? diff --git a/Model/FavoriteItem.swift b/Model/FavoriteItem.swift new file mode 100644 index 00000000..187d2c23 --- /dev/null +++ b/Model/FavoriteItem.swift @@ -0,0 +1,39 @@ +import Defaults +import Foundation + +struct FavoriteItem: Codable, Equatable, Identifiable, Defaults.Serializable { + enum Section: Codable, Equatable, Defaults.Serializable { + case subscriptions + case popular + case trending(String, String?) + case channel(String, String) + case playlist(String) + case channelPlaylist(String, String) + + var label: String { + switch self { + case .subscriptions: + return "Subscriptions" + case .popular: + return "Popular" + case let .trending(country, category): + let trendingCountry = Country(rawValue: country)! + let trendingCategory = category.isNil ? nil : TrendingCategory(rawValue: category!)! + return "\(trendingCountry.flag) \(trendingCategory?.name ?? "")" + case let .channel(_, name): + return name + case let .channelPlaylist(_, name): + return name + default: + return "" + } + } + } + + static func == (lhs: FavoriteItem, rhs: FavoriteItem) -> Bool { + lhs.section == rhs.section + } + + var id = UUID().uuidString + var section: Section +} diff --git a/Model/FavoritesModel.swift b/Model/FavoritesModel.swift new file mode 100644 index 00000000..7a78f2f5 --- /dev/null +++ b/Model/FavoritesModel.swift @@ -0,0 +1,77 @@ +import Defaults +import Foundation + +struct FavoritesModel { + static let shared = FavoritesModel() + + @Default(.favorites) var all + + func contains(_ item: FavoriteItem) -> Bool { + all.contains { $0 == item } + } + + func toggle(_ item: FavoriteItem) { + contains(item) ? remove(item) : add(item) + } + + func add(_ item: FavoriteItem) { + all.append(item) + } + + func remove(_ item: FavoriteItem) { + if let index = all.firstIndex(where: { $0 == item }) { + all.remove(at: index) + } + } + + func canMoveUp(_ item: FavoriteItem) -> Bool { + if let index = all.firstIndex(where: { $0 == item }) { + return index > all.startIndex + } + + return false + } + + func canMoveDown(_ item: FavoriteItem) -> Bool { + if let index = all.firstIndex(where: { $0 == item }) { + return index < all.endIndex - 1 + } + + return false + } + + func moveUp(_ item: FavoriteItem) { + guard canMoveUp(item) else { + return + } + + if let from = all.firstIndex(where: { $0 == item }) { + all.move( + fromOffsets: IndexSet(integer: from), + toOffset: from - 1 + ) + } + } + + func moveDown(_ item: FavoriteItem) { + guard canMoveDown(item) else { + return + } + + if let from = all.firstIndex(where: { $0 == item }) { + all.move( + fromOffsets: IndexSet(integer: from), + toOffset: from + 2 + ) + } + } + + func addableItems() -> [FavoriteItem] { + let allItems = [ + FavoriteItem(section: .subscriptions), + FavoriteItem(section: .popular) + ] + + return allItems.filter { item in !all.contains { $0.section == item.section } } + } +} diff --git a/Model/NavigationModel.swift b/Model/NavigationModel.swift index 4e61a27c..782a2a6a 100644 --- a/Model/NavigationModel.swift +++ b/Model/NavigationModel.swift @@ -3,7 +3,7 @@ import SwiftUI final class NavigationModel: ObservableObject { enum TabSelection: Hashable { - case watchNow + case favorites case subscriptions case popular case trending @@ -23,7 +23,7 @@ final class NavigationModel: ObservableObject { } } - @Published var tabSelection: TabSelection! = .watchNow + @Published var tabSelection: TabSelection! = .favorites @Published var presentingAddToPlaylist = false @Published var videoToAddToPlaylist: Video! @@ -44,7 +44,7 @@ final class NavigationModel: ObservableObject { var tabSelectionBinding: Binding { Binding( get: { - self.tabSelection ?? .watchNow + self.tabSelection ?? .favorites }, set: { newValue in self.tabSelection = newValue diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 659c00bf..a6730922 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -168,8 +168,10 @@ final class PlayerModel: ObservableObject { try? AVAudioSession.sharedInstance().setActive(true) #endif - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in - self?.play() + if self.isAutoplaying(playerItem!) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in + self?.play() + } } } @@ -440,7 +442,8 @@ final class PlayerModel: ObservableObject { MPMediaItemPropertyTitle: currentItem.video.title as AnyObject, MPMediaItemPropertyArtist: currentItem.video.author as AnyObject, MPMediaItemPropertyArtwork: currentArtwork as AnyObject, - MPMediaItemPropertyPlaybackDuration: Int(currentItem.videoDuration ?? 0) as AnyObject, + MPMediaItemPropertyPlaybackDuration: (currentItem.video.live ? nil : Int(currentItem.videoDuration ?? 0)) as AnyObject, + MPNowPlayingInfoPropertyIsLiveStream: currentItem.video.live as AnyObject, MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime().seconds as AnyObject, MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject, MPMediaItemPropertyMediaType: MPMediaType.anyVideo.rawValue as AnyObject diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index f51032bf..17e1e721 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -106,7 +106,7 @@ extension PlayerModel { } func isAutoplaying(_ item: AVPlayerItem) -> Bool { - player.currentItem == item + player.currentItem == item && presentingPlayer } @discardableResult func enqueueVideo( diff --git a/Model/Player/PlayerQueueItemBridge.swift b/Model/Player/PlayerQueueItemBridge.swift index ab43a49a..3f89d05a 100644 --- a/Model/Player/PlayerQueueItemBridge.swift +++ b/Model/Player/PlayerQueueItemBridge.swift @@ -11,8 +11,6 @@ struct PlayerQueueItemBridge: Defaults.Bridge { return nil } - let videoID = value.videoID.isEmpty ? value.video!.videoID : value.videoID - var playbackTime = "" if let time = value.playbackTime { if time.seconds.isFinite { @@ -28,7 +26,7 @@ struct PlayerQueueItemBridge: Defaults.Bridge { } return [ - "videoID": videoID, + "videoID": value.videoID, "playbackTime": playbackTime, "videoDuration": videoDuration ] diff --git a/Model/Search/SearchQuery.swift b/Model/Search/SearchQuery.swift index 003803ae..7cd0273d 100644 --- a/Model/Search/SearchQuery.swift +++ b/Model/Search/SearchQuery.swift @@ -2,7 +2,7 @@ import Defaults import Foundation final class SearchQuery: ObservableObject { - enum Date: String, CaseIterable, Identifiable, DefaultsSerializable { + enum Date: String, CaseIterable, Identifiable { case any, hour, today, week, month, year var id: SearchQuery.Date.RawValue { @@ -14,7 +14,7 @@ final class SearchQuery: ObservableObject { } } - enum Duration: String, CaseIterable, Identifiable, DefaultsSerializable { + enum Duration: String, CaseIterable, Identifiable { case any, short, long var id: SearchQuery.Duration.RawValue { @@ -26,7 +26,7 @@ final class SearchQuery: ObservableObject { } } - enum SortOrder: String, CaseIterable, Identifiable, DefaultsSerializable { + enum SortOrder: String, CaseIterable, Identifiable { case relevance, rating, uploadDate, viewCount var id: SearchQuery.SortOrder.RawValue { diff --git a/Model/TrendingCategory.swift b/Model/TrendingCategory.swift index b022b108..91a7dc09 100644 --- a/Model/TrendingCategory.swift +++ b/Model/TrendingCategory.swift @@ -3,11 +3,19 @@ import Defaults enum TrendingCategory: String, CaseIterable, Identifiable, Defaults.Serializable { case `default`, music, gaming, movies - var id: TrendingCategory.RawValue { + var id: RawValue { rawValue } - var name: String { + var title: RawValue { rawValue.capitalized } + + var name: String { + id == "default" ? "Trending" : title + } + + var controlLabel: String { + id == "default" ? "All" : title + } } diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index 8e1d56e9..cb3fff05 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -144,6 +144,15 @@ 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; 375168D82700FDB9008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; }; 3758638A2721B0A9000CB14E /* ChannelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743B86727216D3600261544 /* ChannelCell.swift */; }; + 37599F30272B42810087F250 /* FavoriteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F2F272B42810087F250 /* FavoriteItem.swift */; }; + 37599F31272B42810087F250 /* FavoriteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F2F272B42810087F250 /* FavoriteItem.swift */; }; + 37599F32272B42810087F250 /* FavoriteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F2F272B42810087F250 /* FavoriteItem.swift */; }; + 37599F34272B44000087F250 /* FavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F33272B44000087F250 /* FavoritesModel.swift */; }; + 37599F35272B44000087F250 /* FavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F33272B44000087F250 /* FavoritesModel.swift */; }; + 37599F36272B44000087F250 /* FavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F33272B44000087F250 /* FavoritesModel.swift */; }; + 37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F37272B4D740087F250 /* FavoriteButton.swift */; }; + 37599F39272B4D740087F250 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F37272B4D740087F250 /* FavoriteButton.swift */; }; + 37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F37272B4D740087F250 /* FavoriteButton.swift */; }; 375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; }; 375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; }; 375DFB5A26F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; }; @@ -201,12 +210,9 @@ 3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784B23A272894DA00B09468 /* ShareSheet.swift */; }; 3784B23D2728B85300B09468 /* ShareButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784B23C2728B85300B09468 /* ShareButton.swift */; }; 3784B23E2728B85300B09468 /* ShareButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784B23C2728B85300B09468 /* ShareButton.swift */; }; - 3788AC2726F6840700F6BAA9 /* WatchNowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */; }; - 3788AC2826F6840700F6BAA9 /* WatchNowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */; }; - 3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */; }; - 3788AC2B26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */; }; - 3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */; }; - 3788AC2D26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */; }; + 3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */; }; + 3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */; }; + 3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */; }; 378E50FB26FE8B9F00F49626 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FA26FE8B9F00F49626 /* Instance.swift */; }; 378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FA26FE8B9F00F49626 /* Instance.swift */; }; 378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378E50FA26FE8B9F00F49626 /* Instance.swift */; }; @@ -241,9 +247,9 @@ 37A9965A26D6F8CA006E3224 /* HorizontalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */; }; 37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */; }; 37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */; }; - 37A9965E26D6F9B9006E3224 /* WatchNowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965D26D6F9B9006E3224 /* WatchNowView.swift */; }; - 37A9965F26D6F9B9006E3224 /* WatchNowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965D26D6F9B9006E3224 /* WatchNowView.swift */; }; - 37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965D26D6F9B9006E3224 /* WatchNowView.swift */; }; + 37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965D26D6F9B9006E3224 /* FavoritesView.swift */; }; + 37A9965F26D6F9B9006E3224 /* FavoritesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965D26D6F9B9006E3224 /* FavoritesView.swift */; }; + 37A9966026D6F9B9006E3224 /* FavoritesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A9965D26D6F9B9006E3224 /* FavoritesView.swift */; }; 37AAF27E26737323007FC770 /* PopularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27D26737323007FC770 /* PopularView.swift */; }; 37AAF28026737550007FC770 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; }; 37AAF29026740715007FC770 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF28F26740715007FC770 /* Channel.swift */; }; @@ -311,6 +317,10 @@ 37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */; }; 37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD926A214630092E2DB /* PlayerViewController.swift */; }; 37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* Player.swift */; }; + 37BF661C27308859008CCFB0 /* DropFavorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661B27308859008CCFB0 /* DropFavorite.swift */; }; + 37BF661D27308859008CCFB0 /* DropFavorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661B27308859008CCFB0 /* DropFavorite.swift */; }; + 37BF661F27308884008CCFB0 /* DropFavoriteOutside.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661E27308884008CCFB0 /* DropFavoriteOutside.swift */; }; + 37BF662027308884008CCFB0 /* DropFavoriteOutside.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661E27308884008CCFB0 /* DropFavoriteOutside.swift */; }; 37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069772725962F00F7F6CB /* ScreenSaverManager.swift */; }; 37C0697A2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */; }; 37C0697B2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */; }; @@ -411,6 +421,7 @@ 37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */; }; 37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */; }; 37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */; }; + 37FAE000272ED58000330459 /* EditFavorites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FADFFF272ED58000330459 /* EditFavorites.swift */; }; 37FB28412721B22200A57617 /* ContentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB28402721B22200A57617 /* ContentItem.swift */; }; 37FB28422721B22200A57617 /* ContentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB28402721B22200A57617 /* ContentItem.swift */; }; 37FB28432721B22200A57617 /* ContentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB28402721B22200A57617 /* ContentItem.swift */; }; @@ -538,6 +549,9 @@ 374C0542272496E4009BDDBE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = macOS/AppDelegate.swift; sourceTree = SOURCE_ROOT; }; 374C0544272496FD009BDDBE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 375168D52700FAFF008F96A6 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = ""; }; + 37599F2F272B42810087F250 /* FavoriteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItem.swift; sourceTree = ""; }; + 37599F33272B44000087F250 /* FavoritesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesModel.swift; sourceTree = ""; }; + 37599F37272B4D740087F250 /* FavoriteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteButton.swift; sourceTree = ""; }; 375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = ""; }; 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = ""; }; 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsubscribeAlertModifier.swift; sourceTree = ""; }; @@ -555,8 +569,7 @@ 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = ""; }; 3784B23A272894DA00B09468 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = ""; }; 3784B23C2728B85300B09468 /* ShareButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareButton.swift; sourceTree = ""; }; - 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSection.swift; sourceTree = ""; }; - 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSectionBody.swift; sourceTree = ""; }; + 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItemView.swift; sourceTree = ""; }; 378E50FA26FE8B9F00F49626 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = ""; }; 378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsMenuView.swift; sourceTree = ""; }; 37977582268922F600DD52A8 /* InvidiousAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvidiousAPI.swift; sourceTree = ""; }; @@ -575,7 +588,7 @@ 37A3B16D27255E7F000FB5EE /* Open in Yattee.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Open in Yattee.entitlements"; sourceTree = ""; }; 37A3B1792725735F000FB5EE /* Open in Yattee (iOS).appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Open in Yattee (iOS).appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalCells.swift; sourceTree = ""; }; - 37A9965D26D6F9B9006E3224 /* WatchNowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowView.swift; sourceTree = ""; }; + 37A9965D26D6F9B9006E3224 /* FavoritesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesView.swift; sourceTree = ""; }; 37AAF27D26737323007FC770 /* PopularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularView.swift; sourceTree = ""; }; 37AAF27F26737550007FC770 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; 37AAF28F26740715007FC770 /* Channel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = ""; }; @@ -603,6 +616,8 @@ 37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; 37BE0BD926A214630092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; 37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; + 37BF661B27308859008CCFB0 /* DropFavorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropFavorite.swift; sourceTree = ""; }; + 37BF661E27308884008CCFB0 /* DropFavoriteOutside.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropFavoriteOutside.swift; sourceTree = ""; }; 37C069772725962F00F7F6CB /* ScreenSaverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverManager.swift; sourceTree = ""; }; 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItemBridge.swift; sourceTree = ""; }; 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMTime+DefaultTimescale.swift"; sourceTree = ""; }; @@ -649,6 +664,7 @@ 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = ""; }; 37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = ""; }; 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = ""; }; + 37FADFFF272ED58000330459 /* EditFavorites.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFavorites.swift; sourceTree = ""; }; 37FB28402721B22200A57617 /* ContentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentItem.swift; sourceTree = ""; }; 37FB285D272225E800A57617 /* ContentItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentItemView.swift; sourceTree = ""; }; 37FD43DB270470B70073EE42 /* InstancesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesSettings.swift; sourceTree = ""; }; @@ -814,6 +830,7 @@ 37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */, 37FB285D272225E800A57617 /* ContentItemView.swift */, 3748186D26A769D60084E870 /* DetailBadge.swift */, + 37599F37272B4D740087F250 /* FavoriteButton.swift */, 37152EE926EFEB95004FB96D /* LazyView.swift */, 37E70926271CDDAE00D34DDE /* OpenSettingsButton.swift */, 37E2EEAA270656EC00170416 /* PlayerControlsView.swift */, @@ -922,14 +939,15 @@ name = Frameworks; sourceTree = ""; }; - 3788AC2126F683AB00F6BAA9 /* Watch Now */ = { + 3788AC2126F683AB00F6BAA9 /* Favorites */ = { isa = PBXGroup; children = ( - 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */, - 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */, - 37A9965D26D6F9B9006E3224 /* WatchNowView.swift */, + 37BF661E27308884008CCFB0 /* DropFavoriteOutside.swift */, + 37BF661B27308859008CCFB0 /* DropFavorite.swift */, + 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */, + 37A9965D26D6F9B9006E3224 /* FavoritesView.swift */, ); - path = "Watch Now"; + path = Favorites; sourceTree = ""; }; 37992DC826CC50CD003D4C27 /* iOS */ = { @@ -1022,6 +1040,7 @@ 37D4B0C12671614700C925CA /* Shared */ = { isa = PBXGroup; children = ( + 3788AC2126F683AB00F6BAA9 /* Favorites */, 37D526E12720B49200ED2F5E /* Gestures */, 3761AC0526F0F96100AA496F /* Modifiers */, 371AAE2326CEB9E800901972 /* Navigation */, @@ -1031,7 +1050,6 @@ 371AAE2526CEBF0B00901972 /* Trending */, 371AAE2726CEBF4700901972 /* Videos */, 371AAE2826CEC7D900901972 /* Views */, - 3788AC2126F683AB00F6BAA9 /* Watch Now */, 375168D52700FAFF008F96A6 /* Debounce.swift */, 372915E52687E3B900F5A35B /* Defaults.swift */, 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */, @@ -1082,6 +1100,7 @@ isa = PBXGroup; children = ( 37666BA927023AF000F869E5 /* AccountSelectionView.swift */, + 37FADFFF272ED58000330459 /* EditFavorites.swift */, 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */, 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */, 37D4B15E267164AF00C925CA /* Assets.xcassets */, @@ -1123,6 +1142,8 @@ 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */, 3705B181267B4E4900704544 /* TrendingCategory.swift */, 37D4B19626717E1500C925CA /* Video.swift */, + 37599F2F272B42810087F250 /* FavoriteItem.swift */, + 37599F33272B44000087F250 /* FavoritesModel.swift */, ); path = Model; sourceTree = ""; @@ -1627,6 +1648,7 @@ 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */, 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, + 37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */, 37CB12792724C76D00213B45 /* VideoURLParser.swift in Sources */, 3784B23D2728B85300B09468 /* ShareButton.swift in Sources */, 37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, @@ -1653,11 +1675,12 @@ 37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, 37BE0BD326A1D4780092E2DB /* Player.swift in Sources */, - 37A9965E26D6F9B9006E3224 /* WatchNowView.swift in Sources */, + 37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */, 37CEE4C12677B697005A1EFE /* Stream.swift in Sources */, 37F4AE7226828F0900BD60EA /* VerticalCells.swift in Sources */, 376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 3748186626A7627F0084E870 /* Video+Fixtures.swift in Sources */, + 37599F34272B44000087F250 /* FavoritesModel.swift in Sources */, 37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */, 377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */, 37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */, @@ -1669,15 +1692,16 @@ 37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */, 376578892685471400D4EA09 /* Playlist.swift in Sources */, 373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */, - 3788AC2B26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */, 3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */, 373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, + 37BF661F27308884008CCFB0 /* DropFavoriteOutside.swift in Sources */, 3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */, 37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */, 377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */, 37C3A251272366440087A57A /* ChannelPlaylistView.swift in Sources */, 37E2EEAB270656EC00170416 /* PlayerControlsView.swift in Sources */, 374C053527242D9F009BDDBE /* ServicesSettings.swift in Sources */, + 37BF661C27308859008CCFB0 /* DropFavorite.swift in Sources */, 376A33E42720CB35000C1D6B /* Account.swift in Sources */, 37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */, 37AAF29026740715007FC770 /* Channel.swift in Sources */, @@ -1711,13 +1735,14 @@ 37D526DE2720AC4400ED2F5E /* VideosAPI.swift in Sources */, 37484C2526FC83E000287258 /* InstanceForm.swift in Sources */, 37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */, - 3788AC2726F6840700F6BAA9 /* WatchNowSection.swift in Sources */, + 3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, 375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */, 37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */, 37141673267A8E10006CA35D /* Country.swift in Sources */, 3748186E26A769D60084E870 /* DetailBadge.swift in Sources */, 37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */, + 37599F30272B42810087F250 /* FavoriteItem.swift in Sources */, 373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, 37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */, 372915E62687E3B900F5A35B /* Defaults.swift in Sources */, @@ -1751,8 +1776,8 @@ 37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */, 37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */, 3743CA53270F284F00E4D32B /* View+Borders.swift in Sources */, + 37599F39272B4D740087F250 /* FavoriteButton.swift in Sources */, 37C3A24627235DA70087A57A /* ChannelPlaylist.swift in Sources */, - 3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */, 37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 374C0540272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, 374C053627242D9F009BDDBE /* ServicesSettings.swift in Sources */, @@ -1781,7 +1806,8 @@ 37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */, 37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 37EAD870267B9ED100D9E01B /* Segment.swift in Sources */, - 3788AC2826F6840700F6BAA9 /* WatchNowSection.swift in Sources */, + 3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, + 37599F35272B44000087F250 /* FavoritesModel.swift in Sources */, 37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 37FFC441272734C3009FFD26 /* Throttle.swift in Sources */, 37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */, @@ -1796,12 +1822,14 @@ 37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */, 3765788A2685471400D4EA09 /* Playlist.swift in Sources */, 37E2EEAC270656EC00170416 /* PlayerControlsView.swift in Sources */, + 37BF662027308884008CCFB0 /* DropFavoriteOutside.swift in Sources */, 37E70924271CD43000D34DDE /* WelcomeScreen.swift in Sources */, 374C0543272496E4009BDDBE /* AppDelegate.swift in Sources */, 373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */, 37AAF29126740715007FC770 /* Channel.swift in Sources */, 376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */, 37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */, + 37BF661D27308859008CCFB0 /* DropFavorite.swift in Sources */, 3748186F26A769D60084E870 /* DetailBadge.swift in Sources */, 372915E72687E3B900F5A35B /* Defaults.swift in Sources */, 37C3A242272359900087A57A /* Double+Format.swift in Sources */, @@ -1815,7 +1843,7 @@ 37B81B0326D2CAE700675966 /* PlaybackBar.swift in Sources */, 376A33E52720CB35000C1D6B /* Account.swift in Sources */, 376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, - 37A9965F26D6F9B9006E3224 /* WatchNowView.swift in Sources */, + 37A9965F26D6F9B9006E3224 /* FavoritesView.swift in Sources */, 37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */, 37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */, 37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */, @@ -1842,6 +1870,7 @@ 37CC3F46270CE30600608308 /* PlayerQueueItem.swift in Sources */, 3700155C271B0D4D0049C794 /* PipedAPI.swift in Sources */, 37D4B19826717E1500C925CA /* Video.swift in Sources */, + 37599F31272B42810087F250 /* FavoriteItem.swift in Sources */, 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */, 37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */, 37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */, @@ -1891,7 +1920,6 @@ buildActionMask = 2147483647; files = ( 37AAF28026737550007FC770 /* SearchView.swift in Sources */, - 3788AC2D26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */, 37EAD871267B9ED100D9E01B /* Segment.swift in Sources */, 37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */, 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, @@ -1911,7 +1939,7 @@ 376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */, 37141671267A8ACC006CA35D /* TrendingView.swift in Sources */, 37C3A24727235DA70087A57A /* ChannelPlaylist.swift in Sources */, - 3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */, + 3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, 37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */, 37E70925271CD43000D34DDE /* WelcomeScreen.swift in Sources */, 37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, @@ -1940,11 +1968,12 @@ 37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, 37AAF27E26737323007FC770 /* PopularView.swift in Sources */, 3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, - 37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */, + 37A9966026D6F9B9006E3224 /* FavoritesView.swift in Sources */, 37001565271B1F250049C794 /* AccountsModel.swift in Sources */, 374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, 37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */, 376A33E62720CB35000C1D6B /* Account.swift in Sources */, + 37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */, 37484C1F26FC83A400287258 /* InstancesSettings.swift in Sources */, 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */, 376578932685490700D4EA09 /* PlaylistsView.swift in Sources */, @@ -1976,6 +2005,8 @@ 373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */, 37C3A24B27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 37FD43E52704847C0073EE42 /* View+Fixtures.swift in Sources */, + 37FAE000272ED58000330459 /* EditFavorites.swift in Sources */, + 37599F32272B42810087F250 /* FavoriteItem.swift in Sources */, 37141675267A8E10006CA35D /* Country.swift in Sources */, 37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */, 37484C2726FC83E000287258 /* InstanceForm.swift in Sources */, @@ -1984,6 +2015,7 @@ 378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */, 37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */, 37D526E02720AC4400ED2F5E /* VideosAPI.swift in Sources */, + 37599F36272B44000087F250 /* FavoritesModel.swift in Sources */, 3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */, 3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, 37C3A24F272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 99c8f595..a98ef65b 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -35,6 +35,10 @@ extension Defaults.Keys { static let sponsorBlockInstance = Key("sponsorBlockInstance", default: "https://sponsor.ajay.app") static let sponsorBlockCategories = Key>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories)) + static let favorites = Key<[FavoriteItem]>("favorites", default: [ + .init(section: .trending("US", nil)) + ]) + static let quality = Key("quality", default: .hd720pFirstThenBest) static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: []) diff --git a/Shared/Favorites/DropFavorite.swift b/Shared/Favorites/DropFavorite.swift new file mode 100644 index 00000000..568b2343 --- /dev/null +++ b/Shared/Favorites/DropFavorite.swift @@ -0,0 +1,35 @@ +import Foundation +import SwiftUI + +struct DropFavorite: DropDelegate { + let item: FavoriteItem + @Binding var favorites: [FavoriteItem] + @Binding var current: FavoriteItem? + + func dropEntered(info _: DropInfo) { + guard item != current else { + return + } + + let from = favorites.firstIndex(of: current!)! + let to = favorites.firstIndex(of: item)! + + guard favorites[to].id != current!.id else { + return + } + + favorites.move( + fromOffsets: IndexSet(integer: from), + toOffset: to > from ? to + 1 : to + ) + } + + func dropUpdated(info _: DropInfo) -> DropProposal? { + DropProposal(operation: .move) + } + + func performDrop(info _: DropInfo) -> Bool { + current = nil + return true + } +} diff --git a/Shared/Favorites/DropFavoriteOutside.swift b/Shared/Favorites/DropFavoriteOutside.swift new file mode 100644 index 00000000..9204d5e1 --- /dev/null +++ b/Shared/Favorites/DropFavoriteOutside.swift @@ -0,0 +1,11 @@ +import Foundation +import SwiftUI + +struct DropFavoriteOutside: DropDelegate { + @Binding var current: FavoriteItem? + + func performDrop(info _: DropInfo) -> Bool { + current = nil + return true + } +} diff --git a/Shared/Favorites/FavoriteItemView.swift b/Shared/Favorites/FavoriteItemView.swift new file mode 100644 index 00000000..97be4117 --- /dev/null +++ b/Shared/Favorites/FavoriteItemView.swift @@ -0,0 +1,93 @@ +import Defaults +import Siesta +import SwiftUI +import UniformTypeIdentifiers + +final class FavoriteResourceObserver: ObservableObject, ResourceObserver { + @Published var videos = [Video]() + + func resourceChanged(_ resource: Resource, event _: ResourceEvent) { + if let videos: [Video] = resource.typedContent() { + self.videos = videos + } else if let channel: Channel = resource.typedContent() { + videos = channel.videos + } else if let playlist: ChannelPlaylist = resource.typedContent() { + videos = playlist.videos + } else if let playlist: Playlist = resource.typedContent() { + videos = playlist.videos + } + } +} + +struct FavoriteItemView: View { + let item: FavoriteItem + let resource: Resource? + + @StateObject private var store = FavoriteResourceObserver() + + @Binding private var favorites: [FavoriteItem] + @Binding private var dragging: FavoriteItem? + + @EnvironmentObject private var playlistsModel + + init( + item: FavoriteItem, + resource: Resource?, + favorites: Binding<[FavoriteItem]>, + dragging: Binding + ) { + self.item = item + self.resource = resource + _favorites = favorites + _dragging = dragging + } + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text(label) + .font(.title3.bold()) + .foregroundColor(.secondary) + + .contextMenu { + Button { + FavoritesModel.shared.remove(item) + } label: { + Label("Remove from Favorites", systemImage: "trash") + } + } + .contentShape(Rectangle()) + #if os(tvOS) + .padding(.leading, 40) + #else + .padding(.leading, 15) + #endif + + HorizontalCells(items: store.videos.map { ContentItem(video: $0) }) + } + + .contentShape(Rectangle()) + .opacity(dragging?.id == item.id ? 0.5 : 1) + .onAppear { + resource?.addObserver(store) + resource?.loadIfNeeded() + } + #if !os(tvOS) + .onDrag { + dragging = item + return NSItemProvider(object: item.id as NSString) + } + .onDrop( + of: [UTType.text], + delegate: DropFavorite(item: item, favorites: $favorites, current: $dragging) + ) + #endif + } + + var label: String { + if case let .playlist(id) = item.section { + return playlistsModel.find(id: id)?.title ?? "Unknown Playlist" + } + + return item.section.label + } +} diff --git a/Shared/Favorites/FavoritesView.swift b/Shared/Favorites/FavoritesView.swift new file mode 100644 index 00000000..9aca3279 --- /dev/null +++ b/Shared/Favorites/FavoritesView.swift @@ -0,0 +1,91 @@ +import Defaults +import Siesta +import SwiftUI +import UniformTypeIdentifiers + +struct FavoritesView: View { + @EnvironmentObject private var accounts + @EnvironmentObject private var playlists + + @State private var dragging: FavoriteItem? + @State private var presentingEditFavorites = false + + @Default(.favorites) private var favorites + + var body: some View { + PlayerControlsView { + ScrollView(.vertical, showsIndicators: false) { + if !accounts.current.isNil { + VStack(alignment: .leading, spacing: 0) { + ForEach(favorites) { item in + VStack { + if let resource = resource(item) { + FavoriteItemView(item: item, resource: resource, favorites: $favorites, dragging: $dragging) + } + } + } + } + + #if os(tvOS) + Button { + presentingEditFavorites = true + } label: { + Text("Edit Favorites...") + } + #endif + } + } + #if os(tvOS) + .sheet(isPresented: $presentingEditFavorites) { + EditFavorites() + } + .edgesIgnoringSafeArea(.horizontal) + #else + .onDrop(of: [UTType.text], delegate: DropFavoriteOutside(current: $dragging)) + .navigationTitle("Favorites") + #endif + #if os(macOS) + .background() + .frame(minWidth: 360) + #endif + } + } + + func resource(_ item: FavoriteItem) -> Resource? { + switch item.section { + case .subscriptions: + if accounts.app.supportsSubscriptions { + return accounts.api.feed + } + + case .popular: + if accounts.app.supportsPopular { + return accounts.api.popular + } + + case let .trending(country, category): + let trendingCountry = Country(rawValue: country)! + let trendingCategory = category.isNil ? nil : TrendingCategory(rawValue: category!)! + + return accounts.api.trending(country: trendingCountry, category: trendingCategory) + + case let .channel(id, _): + return accounts.api.channelVideos(id) + + case let .channelPlaylist(id, _): + return accounts.api.channelPlaylist(id) + + case let .playlist(id): + return accounts.api.playlist(id) + } + + return nil + } +} + +struct Favorites_Previews: PreviewProvider { + static var previews: some View { + FavoritesView() + .injectFixtureEnvironmentObjects() + } +} diff --git a/Shared/Navigation/AppTabNavigation.swift b/Shared/Navigation/AppTabNavigation.swift index 67a20439..b169335b 100644 --- a/Shared/Navigation/AppTabNavigation.swift +++ b/Shared/Navigation/AppTabNavigation.swift @@ -11,14 +11,14 @@ struct AppTabNavigation: View { var body: some View { TabView(selection: navigation.tabSelectionBinding) { NavigationView { - LazyView(WatchNowView()) + LazyView(FavoritesView()) .toolbar { toolbarContent } } .tabItem { - Label("Watch Now", systemImage: "play.circle") - .accessibility(label: Text("Subscriptions")) + Label("Favorites", systemImage: "heart") + .accessibility(label: Text("Favorites")) } - .tag(TabSelection.watchNow) + .tag(TabSelection.favorites) if accounts.app.supportsSubscriptions { NavigationView { diff --git a/Shared/Navigation/Sidebar.swift b/Shared/Navigation/Sidebar.swift index 3d0b8ed2..472550da 100644 --- a/Shared/Navigation/Sidebar.swift +++ b/Shared/Navigation/Sidebar.swift @@ -28,9 +28,9 @@ struct Sidebar: View { var mainNavigationLinks: some View { Section("Videos") { - NavigationLink(destination: LazyView(WatchNowView()), tag: TabSelection.watchNow, selection: $navigation.tabSelection) { - Label("Watch Now", systemImage: "play.circle") - .accessibility(label: Text("Watch Now")) + NavigationLink(destination: LazyView(FavoritesView()), tag: TabSelection.favorites, selection: $navigation.tabSelection) { + Label("Favorites", systemImage: "heart") + .accessibility(label: Text("Favorites")) } if accounts.app.supportsSubscriptions && accounts.signedIn { NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: $navigation.tabSelection) { diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 607b771c..6e61c521 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -13,13 +13,16 @@ struct VideoPlayerView: View { #endif } - @State private var playerSize: CGSize = .zero @State private var fullScreen = false #if os(iOS) @Environment(\.dismiss) private var dismiss @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) private var verticalSizeClass + + private var idiom: UIUserInterfaceIdiom { + UIDevice.current.userInterfaceIdiom + } #endif @EnvironmentObject private var player @@ -75,12 +78,6 @@ struct VideoPlayerView: View { #endif .background(.black) - .onAppear { - self.playerSize = geometry.size - } - .onChange(of: geometry.size) { size in - self.playerSize = size - } Group { #if os(iOS) @@ -134,7 +131,7 @@ struct VideoPlayerView: View { #if os(iOS) var sidebarQueue: Bool { - horizontalSizeClass == .regular && playerSize.width > 750 + horizontalSizeClass == .regular && idiom == .pad } var sidebarQueueBinding: Binding { diff --git a/Shared/Playlists/PlaylistsView.swift b/Shared/Playlists/PlaylistsView.swift index cb43f085..1d3d9a6c 100644 --- a/Shared/Playlists/PlaylistsView.swift +++ b/Shared/Playlists/PlaylistsView.swift @@ -75,6 +75,8 @@ struct PlaylistsView: View { editPlaylistButton } #endif + FavoriteButton(item: FavoriteItem(section: .playlist(selectedPlaylistID))) + newPlaylistButton } @@ -139,6 +141,11 @@ struct PlaylistsView: View { editPlaylistButton } + if let playlist = currentPlaylist { + FavoriteButton(item: FavoriteItem(section: .playlist(playlist.id))) + .labelStyle(.iconOnly) + } + Spacer() newPlaylistButton diff --git a/Shared/Settings/ServicesSettings.swift b/Shared/Settings/ServicesSettings.swift index ec23d035..7f622baf 100644 --- a/Shared/Settings/ServicesSettings.swift +++ b/Shared/Settings/ServicesSettings.swift @@ -95,6 +95,8 @@ struct ServicesSettings: View { struct ServicesSettings_Previews: PreviewProvider { static var previews: some View { - ServicesSettings() + VStack { + ServicesSettings() + } } } diff --git a/Shared/Trending/TrendingView.swift b/Shared/Trending/TrendingView.swift index fd51e402..ecddeaa5 100644 --- a/Shared/Trending/TrendingView.swift +++ b/Shared/Trending/TrendingView.swift @@ -11,9 +11,11 @@ struct TrendingView: View { @State private var presentingCountrySelection = false + @State private var favoriteItem: FavoriteItem? + @EnvironmentObject private var accounts - var popular: [ContentItem] { + var trending: [ContentItem] { ContentItem.array(of: store.collection) } @@ -36,12 +38,12 @@ struct TrendingView: View { VStack(alignment: .center, spacing: 0) { #if os(tvOS) toolbar - HorizontalCells(items: popular) + HorizontalCells(items: trending) .padding(.top, 40) Spacer() #else - VerticalCells(items: popular) + VerticalCells(items: trending) #endif } } @@ -62,6 +64,11 @@ struct TrendingView: View { .toolbar { #if os(macOS) ToolbarItemGroup { + if let favoriteItem = favoriteItem { + FavoriteButton(item: favoriteItem) + .id(favoriteItem.id) + } + if accounts.app.supportsTrendingCategories { categoryButton } @@ -70,8 +77,8 @@ struct TrendingView: View { #elseif os(iOS) ToolbarItemGroup(placement: .bottomBar) { Group { - if accounts.app.supportsTrendingCategories { - HStack { + HStack { + if accounts.app.supportsTrendingCategories { Text("Category") .foregroundColor(.secondary) @@ -80,7 +87,14 @@ struct TrendingView: View { // force redraw of the view when it changes .id(UUID()) } - } else { + } + + Spacer() + + if let favoriteItem = favoriteItem { + FavoriteButton(item: favoriteItem) + .id(favoriteItem.id) + Spacer() } @@ -96,6 +110,7 @@ struct TrendingView: View { } .onChange(of: resource) { _ in resource.load() + updateFavoriteItem() } .onAppear { if videos.isEmpty { @@ -104,10 +119,12 @@ struct TrendingView: View { } else { store.replace(videos) } + + updateFavoriteItem() } } - var toolbar: some View { + private var toolbar: some View { HStack { if accounts.app.supportsTrendingCategories { HStack { @@ -128,17 +145,25 @@ struct TrendingView: View { countryButton } + + #if os(tvOS) + if let favoriteItem = favoriteItem { + FavoriteButton(item: favoriteItem) + .id(favoriteItem.id) + .labelStyle(.iconOnly) + } + #endif } } - var categoryButton: some View { + private var categoryButton: some View { #if os(tvOS) Button(category.name) { self.category = category.next() } .contextMenu { ForEach(TrendingCategory.allCases) { category in - Button(category.name) { self.category = category } + Button(category.controlLabel) { self.category = category } } Button("Cancel", role: .cancel) {} @@ -147,13 +172,13 @@ struct TrendingView: View { #else Picker("Category", selection: $category) { ForEach(TrendingCategory.allCases) { category in - Text(category.name).tag(category) + Text(category.controlLabel).tag(category) } } #endif } - var countryButton: some View { + private var countryButton: some View { Button(action: { presentingCountrySelection.toggle() resource.removeObservers(ownedBy: store) @@ -161,6 +186,10 @@ struct TrendingView: View { Text("\(country.flag) \(country.id)") } } + + private func updateFavoriteItem() { + favoriteItem = FavoriteItem(section: .trending(country.rawValue, category.rawValue)) + } } struct TrendingView_Previews: PreviewProvider { diff --git a/Shared/Videos/VideoCell.swift b/Shared/Videos/VideoCell.swift index 9d94e494..9250648a 100644 --- a/Shared/Videos/VideoCell.swift +++ b/Shared/Videos/VideoCell.swift @@ -143,24 +143,18 @@ struct VideoCell: View { #endif .padding(.bottom, 4) - Group { - if additionalDetailsAvailable { - HStack(spacing: 8) { - if let date = video.publishedDate { - Image(systemName: "calendar") - Text(date) - } + HStack(spacing: 8) { + if let date = video.publishedDate { + Image(systemName: "calendar") + Text(date) + } - if video.views > 0 { - Image(systemName: "eye") - Text(video.viewsCount!) - } - } - .foregroundColor(.secondary) - } else { - Spacer() + if video.views > 0 { + Image(systemName: "eye") + Text(video.viewsCount!) } } + .foregroundColor(.secondary) .frame(minHeight: 30, alignment: .top) #if os(tvOS) .padding(.bottom, 10) diff --git a/Shared/Views/ChannelPlaylistView.swift b/Shared/Views/ChannelPlaylistView.swift index d3eb6495..ef2b90eb 100644 --- a/Shared/Views/ChannelPlaylistView.swift +++ b/Shared/Views/ChannelPlaylistView.swift @@ -40,9 +40,16 @@ struct ChannelPlaylistView: View { var content: some View { VStack(alignment: .leading) { #if os(tvOS) - Text(playlist.title) - .font(.title2) - .frame(alignment: .leading) + HStack { + Text(playlist.title) + .font(.title2) + .frame(alignment: .leading) + + Spacer() + + FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title))) + .labelStyle(.iconOnly) + } #endif VerticalCells(items: items) } @@ -66,12 +73,8 @@ struct ChannelPlaylistView: View { ) } - ToolbarItem(placement: .cancellationAction) { - if inNavigationView { - Button("Done") { - dismiss() - } - } + ToolbarItem { + FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title))) } } .navigationTitle(playlist.title) diff --git a/Shared/Views/ChannelVideosView.swift b/Shared/Views/ChannelVideosView.swift index 53bc6e58..e4c85730 100644 --- a/Shared/Views/ChannelVideosView.swift +++ b/Shared/Views/ChannelVideosView.swift @@ -51,6 +51,9 @@ struct ChannelVideosView: View { Spacer() + FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name))) + .labelStyle(.iconOnly) + if let subscribers = store.item?.subscriptionsString { Text("**\(subscribers)** subscribers") .foregroundColor(.secondary) @@ -87,14 +90,8 @@ struct ChannelVideosView: View { .opacity(store.item?.subscriptionsString != nil ? 1 : 0) subscriptionToggleButton - } - } - ToolbarItem(placement: .cancellationAction) { - if inNavigationView { - Button("Done") { - dismiss() - } + FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name))) } } } diff --git a/Shared/Views/FavoriteButton.swift b/Shared/Views/FavoriteButton.swift new file mode 100644 index 00000000..71af228e --- /dev/null +++ b/Shared/Views/FavoriteButton.swift @@ -0,0 +1,25 @@ +import Foundation +import SwiftUI + +struct FavoriteButton: View { + let item: FavoriteItem + let favorites = FavoritesModel.shared + + @State private var isFavorite = false + + var body: some View { + Button { + favorites.toggle(item) + isFavorite.toggle() + } label: { + if isFavorite { + Label("Remove from Favorites", systemImage: "heart.fill") + } else { + Label("Add to Favorites", systemImage: "heart") + } + } + .onAppear { + isFavorite = favorites.contains(item) + } + } +} diff --git a/Shared/Views/PlaylistVideosView.swift b/Shared/Views/PlaylistVideosView.swift index a4de858e..b5178bdc 100644 --- a/Shared/Views/PlaylistVideosView.swift +++ b/Shared/Views/PlaylistVideosView.swift @@ -19,5 +19,10 @@ struct PlaylistVideosView: View { .navigationTitle("\(playlist.title) Playlist") #endif } + .toolbar { + ToolbarItem { + FavoriteButton(item: FavoriteItem(section: .playlist(playlist.id))) + } + } } } diff --git a/Shared/Views/PopularView.swift b/Shared/Views/PopularView.swift index 946840b0..f5296497 100644 --- a/Shared/Views/PopularView.swift +++ b/Shared/Views/PopularView.swift @@ -25,5 +25,10 @@ struct PopularView: View { .navigationTitle("Popular") #endif } + .toolbar { + ToolbarItem(placement: .automatic) { + FavoriteButton(item: FavoriteItem(section: .popular)) + } + } } } diff --git a/Shared/Views/SubscriptionsView.swift b/Shared/Views/SubscriptionsView.swift index e023fdac..f58efb64 100644 --- a/Shared/Views/SubscriptionsView.swift +++ b/Shared/Views/SubscriptionsView.swift @@ -26,6 +26,11 @@ struct SubscriptionsView: View { } } } + .toolbar { + ToolbarItem(placement: .automatic) { + FavoriteButton(item: FavoriteItem(section: .subscriptions)) + } + } .refreshable { loadResources(force: true) } diff --git a/Shared/Watch Now/WatchNowSection.swift b/Shared/Watch Now/WatchNowSection.swift deleted file mode 100644 index c2286221..00000000 --- a/Shared/Watch Now/WatchNowSection.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Defaults -import Siesta -import SwiftUI - -struct WatchNowSection: View { - let resource: Resource? - let label: String - - @StateObject private var store = Store<[Video]>() - - @EnvironmentObject private var accounts - - init(resource: Resource?, label: String) { - self.resource = resource - self.label = label - } - - var body: some View { - WatchNowSectionBody(label: label, videos: store.collection) - .onAppear { - resource?.addObserver(store) - resource?.loadIfNeeded() - } - .onChange(of: accounts.current) { _ in - resource?.load() - } - } -} diff --git a/Shared/Watch Now/WatchNowSectionBody.swift b/Shared/Watch Now/WatchNowSectionBody.swift deleted file mode 100644 index c3515204..00000000 --- a/Shared/Watch Now/WatchNowSectionBody.swift +++ /dev/null @@ -1,21 +0,0 @@ -import SwiftUI - -struct WatchNowSectionBody: View { - let label: String - let videos: [Video] - - var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(label) - .font(.title3.bold()) - .foregroundColor(.secondary) - #if os(tvOS) - .padding(.leading, 40) - #else - .padding(.leading, 15) - #endif - - HorizontalCells(items: ContentItem.array(of: videos)) - } - } -} diff --git a/Shared/Watch Now/WatchNowView.swift b/Shared/Watch Now/WatchNowView.swift deleted file mode 100644 index 9c093e52..00000000 --- a/Shared/Watch Now/WatchNowView.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Defaults -import Siesta -import SwiftUI - -struct WatchNowView: View { - @EnvironmentObject private var accounts - - var body: some View { - PlayerControlsView { - ScrollView(.vertical, showsIndicators: false) { - if !accounts.current.isNil { - VStack(alignment: .leading, spacing: 0) { - if accounts.api.signedIn { - WatchNowSection(resource: accounts.api.feed, label: "Subscriptions") - } - if accounts.app.supportsPopular { - WatchNowSection(resource: accounts.api.popular, label: "Popular") - } - WatchNowSection(resource: accounts.api.trending(country: .pl, category: .default), label: "Trending") - if accounts.app.supportsTrendingCategories { - WatchNowSection(resource: accounts.api.trending(country: .pl, category: .movies), label: "Movies") - WatchNowSection(resource: accounts.api.trending(country: .pl, category: .music), label: "Music") - } - -// TODO: adding sections to view -// =================== -// WatchNowPlaylistSection(id: "IVPLmRFYLGYZpq61SpujNw3EKbzzGNvoDmH") -// WatchNowSection(resource: api.channelVideos("UCBJycsmduvYEL83R_U4JriQ"), label: "MKBHD") - } - } - } - .id(UUID()) - #if os(tvOS) - .edgesIgnoringSafeArea(.horizontal) - #else - .navigationTitle("Watch Now") - #endif - #if os(macOS) - .background() - .frame(minWidth: 360) - #endif - } - } -} - -struct WatchNowView_Previews: PreviewProvider { - static var previews: some View { - WatchNowView() - .injectFixtureEnvironmentObjects() - } -} diff --git a/tvOS/EditFavorites.swift b/tvOS/EditFavorites.swift new file mode 100644 index 00000000..14987316 --- /dev/null +++ b/tvOS/EditFavorites.swift @@ -0,0 +1,94 @@ +import Defaults +import SwiftUI + +struct EditFavorites: View { + @EnvironmentObject private var playlistsModel + + private var model = FavoritesModel.shared + + @Default(.favorites) private var favorites + + var body: some View { + VStack { + ScrollView { + Text("Edit Favorites") + .font(.system(size: 40)) + .fontWeight(.bold) + .foregroundColor(.secondary) + + ForEach(favorites) { item in + HStack { + Text(label(item)) + + Spacer() + HStack(spacing: 30) { + Button { + model.moveUp(item) + } label: { + Image(systemName: "arrow.up") + } + + Button { + model.moveDown(item) + } label: { + Image(systemName: "arrow.down") + } + + Button { + model.remove(item) + } label: { + Image(systemName: "trash") + } + } + } + } + .padding(.trailing, 40) + + Divider() + .padding(20) + + ForEach(model.addableItems()) { item in + HStack { + Text(label(item)) + + Spacer() + + Button { + model.add(item) + } label: { + Label("Add to Favorites", systemImage: "heart") + } + } + } + .padding(.trailing, 40) + + HStack { + Text("Add more Channels and Playlists to your Favorites using button") + Button {} label: { + Label("Add to Favorites", systemImage: "heart") + .labelStyle(.iconOnly) + } + .disabled(true) + } + .foregroundColor(.secondary) + .padding(.top, 80) + } + .frame(width: 1000, alignment: .leading) + } + } + + func label(_ item: FavoriteItem) -> String { + if case let .playlist(id) = item.section { + return playlistsModel.find(id: id)?.title ?? "Unknown Playlist" + } + + return item.section.label + } +} + +struct EditFavorites_Previews: PreviewProvider { + static var previews: some View { + EditFavorites() + .injectFixtureEnvironmentObjects() + } +} diff --git a/tvOS/TVNavigationView.swift b/tvOS/TVNavigationView.swift index 3c109536..049b60e6 100644 --- a/tvOS/TVNavigationView.swift +++ b/tvOS/TVNavigationView.swift @@ -10,9 +10,9 @@ struct TVNavigationView: View { var body: some View { TabView(selection: navigation.tabSelectionBinding) { - WatchNowView() - .tabItem { Text("Watch Now") } - .tag(TabSelection.watchNow) + FavoritesView() + .tabItem { Text("Favorites") } + .tag(TabSelection.favorites) if accounts.app.supportsSubscriptions { SubscriptionsView()