diff --git a/Model/FavoriteItem.swift b/Model/FavoriteItem.swift index 55063f8b..ee126b02 100644 --- a/Model/FavoriteItem.swift +++ b/Model/FavoriteItem.swift @@ -9,6 +9,7 @@ struct FavoriteItem: Codable, Equatable, Identifiable, Defaults.Serializable { case channel(String, String) case playlist(String) case channelPlaylist(String, String) + case searchQuery(String, String, String, String) var label: String { switch self { @@ -24,6 +25,19 @@ struct FavoriteItem: Codable, Equatable, Identifiable, Defaults.Serializable { return name case let .channelPlaylist(_, name): return name + case let .searchQuery(text, date, duration, order): + var label = "Search: \"\(text)\"" + if !date.isEmpty, let date = SearchQuery.Date(rawValue: date), date != .any { + label += " from \(date == .today ? date.name : " this \(date.name)")" + } + if !order.isEmpty, let order = SearchQuery.SortOrder(rawValue: order), order != .relevance { + label += " by \(order.name)" + } + if !duration.isEmpty { + label += " (\(duration))" + } + + return label default: return "" } diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 39162c4f..fc4c3b71 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -36,7 +36,10 @@ extension Defaults.Keys { static let sponsorBlockCategories = Key>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories)) static let favorites = Key<[FavoriteItem]>("favorites", default: [ - .init(section: .trending("US", nil)) + .init(section: .trending("US", nil)), + .init(section: .searchQuery("World Discoveries", "", "", "")), + .init(section: .searchQuery("Full Body Workout", "", "", "")), + .init(section: .searchQuery("Apple Pie Recipes", "", "", "")) ]) static let channelOnThumbnail = Key("channelOnThumbnail", default: true) diff --git a/Shared/Favorites/FavoriteItemView.swift b/Shared/Favorites/FavoriteItemView.swift index 8c847cc9..73ce9574 100644 --- a/Shared/Favorites/FavoriteItemView.swift +++ b/Shared/Favorites/FavoriteItemView.swift @@ -105,6 +105,14 @@ struct FavoriteItemView: View { case let .playlist(id): return accounts.api.playlist(id) + + case let .searchQuery(text, date, duration, order): + return accounts.api.search(.init( + query: text, + sortBy: SearchQuery.SortOrder(rawValue: order) ?? .uploadDate, + date: SearchQuery.Date(rawValue: date), + duration: SearchQuery.Duration(rawValue: duration) + )) } return nil diff --git a/Shared/Favorites/FavoriteResourceObserver.swift b/Shared/Favorites/FavoriteResourceObserver.swift index 6b93c008..384943b0 100644 --- a/Shared/Favorites/FavoriteResourceObserver.swift +++ b/Shared/Favorites/FavoriteResourceObserver.swift @@ -2,21 +2,19 @@ import Foundation import Siesta final class FavoriteResourceObserver: ObservableObject, ResourceObserver { - @Published var videos = [Video]() + @Published var contentItems = [ContentItem]() func resourceChanged(_ resource: Resource, event _: ResourceEvent) { if let videos: [Video] = resource.typedContent() { - self.videos = videos + contentItems = videos.map { ContentItem(video: $0) } } else if let channel: Channel = resource.typedContent() { - videos = channel.videos + contentItems = channel.videos.map { ContentItem(video: $0) } } else if let playlist: ChannelPlaylist = resource.typedContent() { - videos = playlist.videos + contentItems = playlist.videos.map { ContentItem(video: $0) } } else if let playlist: Playlist = resource.typedContent() { - videos = playlist.videos + contentItems = playlist.videos.map { ContentItem(video: $0) } + } else if let items: [ContentItem] = resource.typedContent() { + contentItems = items } } - - var contentItems: [ContentItem] { - videos.map { ContentItem(video: $0) } - } } diff --git a/Shared/Views/SearchView.swift b/Shared/Views/SearchView.swift index cfc7eccd..03096c03 100644 --- a/Shared/Views/SearchView.swift +++ b/Shared/Views/SearchView.swift @@ -17,6 +17,8 @@ struct SearchView: View { @State private var recentsDebounce = Debounce() #endif + @State private var favoriteItem: FavoriteItem? + @Environment(\.navigationStyle) private var navigationStyle @EnvironmentObject private var accounts @@ -42,8 +44,17 @@ struct SearchView: View { } else { #if os(tvOS) ScrollView(.vertical, showsIndicators: false) { - if accounts.app.supportsSearchFilters { - filtersHorizontalStack + HStack(spacing: 0) { + if accounts.app.supportsSearchFilters { + filtersHorizontalStack + } + + if let favoriteItem = favoriteItem { + FavoriteButton(item: favoriteItem) + .id(favoriteItem.id) + .labelStyle(.iconOnly) + .font(.system(size: 25)) + } } HorizontalCells(items: items) @@ -68,6 +79,13 @@ struct SearchView: View { .toolbar { #if !os(tvOS) ToolbarItemGroup(placement: toolbarPlacement) { + #if os(macOS) + if let favoriteItem = favoriteItem { + FavoriteButton(item: favoriteItem) + .id(favoriteItem.id) + } + #endif + if accounts.app.supportsSearchFilters { Section { #if os(macOS) @@ -84,9 +102,20 @@ struct SearchView: View { #endif } .transaction { t in t.animation = .none } + } + #if os(iOS) Spacer() + if let favoriteItem = favoriteItem { + FavoriteButton(item: favoriteItem) + .id(favoriteItem.id) + } + + Spacer() + #endif + + if accounts.app.supportsSearchFilters { filtersMenu } } @@ -96,6 +125,7 @@ struct SearchView: View { if query != nil { state.queryText = query!.query state.resetQuery(query!) + updateFavoriteItem() } if !videos.isEmpty { @@ -120,7 +150,10 @@ struct SearchView: View { recentsDebounce.invalidate() searchDebounce.debouncing(2) { - state.changeQuery { query in query.query = newQuery } + state.changeQuery { query in + query.query = newQuery + updateFavoriteItem() + } } recentsDebounce.debouncing(10) { @@ -131,15 +164,25 @@ struct SearchView: View { .onSubmit(of: .search) { state.changeQuery { query in query.query = state.queryText } recents.addQuery(state.queryText) + updateFavoriteItem() } .onChange(of: searchSortOrder) { order in - state.changeQuery { query in query.sortBy = order } + state.changeQuery { query in + query.sortBy = order + updateFavoriteItem() + } } .onChange(of: searchDate) { date in - state.changeQuery { query in query.date = date } + state.changeQuery { query in + query.date = date + updateFavoriteItem() + } } .onChange(of: searchDuration) { duration in - state.changeQuery { query in query.duration = duration } + state.changeQuery { query in + query.duration = duration + updateFavoriteItem() + } } #if !os(tvOS) .navigationTitle("Search") @@ -192,6 +235,7 @@ struct SearchView: View { Button(item.title) { state.queryText = item.title state.changeQuery { query in query.query = item.title } + updateFavoriteItem() } #if os(iOS) .swipeActions(edge: .trailing) { @@ -312,21 +356,21 @@ struct SearchView: View { .foregroundColor(.secondary) searchSortOrderButton } - .frame(maxWidth: .infinity, alignment: .trailing) + .frame(maxWidth: 300, alignment: .trailing) HStack(spacing: 30) { Text("Duration") .foregroundColor(.secondary) searchDurationButton } - .frame(maxWidth: .infinity) + .frame(maxWidth: 300) HStack(spacing: 30) { Text("Date") .foregroundColor(.secondary) searchDateButton } - .frame(maxWidth: .infinity, alignment: .leading) + .frame(maxWidth: 300, alignment: .leading) } .font(.system(size: 30)) } @@ -348,8 +392,16 @@ struct SearchView: View { .foregroundColor(filtersActive ? .accentColor : .secondary) .transaction { t in t.animation = .none } } - #endif + + private func updateFavoriteItem() { + favoriteItem = FavoriteItem(section: .searchQuery( + state.query.query, + state.query.date?.rawValue ?? "", + state.query.duration?.rawValue ?? "", + state.query.sortBy.rawValue + )) + } } struct SearchView_Previews: PreviewProvider { diff --git a/tvOS/EditFavorites.swift b/tvOS/EditFavorites.swift index 05d05aa6..fab5d14d 100644 --- a/tvOS/EditFavorites.swift +++ b/tvOS/EditFavorites.swift @@ -64,7 +64,7 @@ struct EditFavorites: View { .padding(.trailing, 40) HStack { - Text("Add more Channels and Playlists to your Favorites using button") + Text("Add Channels, Playlists and Searches to Favorites using") Button {} label: { Label("Add to Favorites", systemImage: "heart") .labelStyle(.iconOnly)