From 5ef89ac9f4d2578604d3205ee284c6c5db2d7b17 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 28 Nov 2021 15:37:55 +0100 Subject: [PATCH] iOS 14/macOS Big Sur Support --- Backports/Backport.swift | 13 + Backports/Badge+Backport.swift | 11 + Backports/Tint+Backport.swift | 11 + Extensions/Color+Background.swift | 17 ++ Extensions/NSTextField+FocusRingType.swift | 8 + Extensions/String+Format.swift | 10 + Extensions/View+Borders.swift | 16 +- Fixtures/Video+Fixtures.swift | 2 +- Model/Player/PlayerModel.swift | 86 ++++--- Model/Search/SearchModel.swift | 6 + README.md | 57 ++-- .../Contents.json | 0 Shared/Favorites/FavoritesView.swift | 2 +- Shared/MenuCommands.swift | 12 +- .../Modifiers/UnsubscribeAlertModifier.swift | 26 -- Shared/Navigation/AccountsMenuView.swift | 16 +- Shared/Navigation/AppSidebarPlaylists.swift | 1 + .../Navigation/AppSidebarSubscriptions.swift | 1 - Shared/Navigation/AppTabNavigation.swift | 27 +- Shared/Navigation/Sidebar.swift | 6 +- Shared/Player/PlaybackBar.swift | 29 ++- Shared/Player/PlayerQueueView.swift | 64 +++-- Shared/Player/VideoDetails.swift | 60 +++-- Shared/Player/VideoPlayerView.swift | 10 +- Shared/Playlists/AddToPlaylistView.swift | 8 +- Shared/Playlists/PlaylistFormView.swift | 144 +++++------ Shared/Playlists/PlaylistsView.swift | 15 +- Shared/Search/SearchField.swift | 80 ++++++ Shared/Search/SearchSuggestions.swift | 77 ++++++ Shared/{Views => Search}/SearchView.swift | 198 +++++++------- Shared/Settings/AccountForm.swift | 28 +- Shared/Settings/AccountsNavigationLink.swift | 31 +++ Shared/Settings/AccountsSettings.swift | 102 -------- Shared/Settings/InstanceForm.swift | 20 +- Shared/Settings/InstanceSettings.swift | 104 ++++++++ Shared/Settings/InstancesSettings.swift | 70 ----- Shared/Settings/ServicesSettings.swift | 16 +- Shared/Settings/SettingsView.swift | 30 ++- Shared/Trending/TrendingCountry.swift | 67 ++--- Shared/Trending/TrendingView.swift | 3 +- Shared/Videos/VerticalCells.swift | 2 +- Shared/Videos/VideoCell.swift | 11 +- Shared/Views/ChannelPlaylistView.swift | 2 +- Shared/Views/ChannelVideosView.swift | 67 +++-- Shared/Views/DetailBadge.swift | 9 +- Shared/Views/OpenSettingsButton.swift | 14 +- Shared/Views/PlayerControlsView.swift | 17 +- Shared/Views/SubscriptionsView.swift | 3 - Shared/Views/VideoContextMenuView.swift | 4 +- Shared/Views/WelcomeScreen.swift | 18 +- Yattee.xcodeproj/project.pbxproj | 243 +++++++++--------- .../xcshareddata/swiftpm/Package.resolved | 6 +- .../xcschemes/Yattee (tvOS).xcscheme | 6 +- macOS/{Settings => }/InstancesSettings.swift | 72 ++++-- .../Contents.json | 2 + .../TopShelf-Wide.png | Bin 0 -> 25177 bytes .../TopShelf-Wide@2x.png | Bin 0 -> 62288 bytes 57 files changed, 1147 insertions(+), 813 deletions(-) create mode 100644 Backports/Backport.swift create mode 100644 Backports/Badge+Backport.swift create mode 100644 Backports/Tint+Backport.swift create mode 100644 Extensions/Color+Background.swift create mode 100644 Extensions/NSTextField+FocusRingType.swift create mode 100644 Extensions/String+Format.swift rename Shared/Assets.xcassets/{PlayerControlsBorderColor.colorset => ControlsBorderColor.colorset}/Contents.json (100%) delete mode 100644 Shared/Modifiers/UnsubscribeAlertModifier.swift create mode 100644 Shared/Search/SearchField.swift create mode 100644 Shared/Search/SearchSuggestions.swift rename Shared/{Views => Search}/SearchView.swift (73%) create mode 100644 Shared/Settings/AccountsNavigationLink.swift delete mode 100644 Shared/Settings/AccountsSettings.swift create mode 100644 Shared/Settings/InstanceSettings.swift delete mode 100644 Shared/Settings/InstancesSettings.swift rename macOS/{Settings => }/InstancesSettings.swift (67%) create mode 100644 tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/TopShelf-Wide.png create mode 100644 tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/TopShelf-Wide@2x.png diff --git a/Backports/Backport.swift b/Backports/Backport.swift new file mode 100644 index 00000000..e52e4c1e --- /dev/null +++ b/Backports/Backport.swift @@ -0,0 +1,13 @@ +import SwiftUI + +public struct Backport { + public let content: Content + + public init(_ content: Content) { + self.content = content + } +} + +extension View { + var backport: Backport { Backport(self) } +} diff --git a/Backports/Badge+Backport.swift b/Backports/Badge+Backport.swift new file mode 100644 index 00000000..8c617733 --- /dev/null +++ b/Backports/Badge+Backport.swift @@ -0,0 +1,11 @@ +import SwiftUI + +extension Backport where Content: View { + @ViewBuilder func badge(_ count: Text) -> some View { + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + content.badge(count) + } else { + content + } + } +} diff --git a/Backports/Tint+Backport.swift b/Backports/Tint+Backport.swift new file mode 100644 index 00000000..05683362 --- /dev/null +++ b/Backports/Tint+Backport.swift @@ -0,0 +1,11 @@ +import SwiftUI + +extension Backport where Content: View { + @ViewBuilder func tint(_ color: Color?) -> some View { + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + content.tint(color) + } else { + content.foregroundColor(color) + } + } +} diff --git a/Extensions/Color+Background.swift b/Extensions/Color+Background.swift new file mode 100644 index 00000000..e02ea81d --- /dev/null +++ b/Extensions/Color+Background.swift @@ -0,0 +1,17 @@ +import SwiftUI + +extension Color { + #if os(macOS) + static let background = Color(NSColor.windowBackgroundColor) + static let secondaryBackground = Color(NSColor.underPageBackgroundColor) + static let tertiaryBackground = Color(NSColor.controlBackgroundColor) + #elseif os(iOS) + static let background = Color(UIColor.systemBackground) + static let secondaryBackground = Color(UIColor.secondarySystemBackground) + static let tertiaryBackground = Color(UIColor.tertiarySystemBackground) + #else + static let background = Color.black + static let secondaryBackground = Color.black + static let tertiaryBackground = Color.black + #endif +} diff --git a/Extensions/NSTextField+FocusRingType.swift b/Extensions/NSTextField+FocusRingType.swift new file mode 100644 index 00000000..1fb58d54 --- /dev/null +++ b/Extensions/NSTextField+FocusRingType.swift @@ -0,0 +1,8 @@ +import AppKit + +extension NSTextField { + override open var focusRingType: NSFocusRingType { + get { .none } + set {} // swiftlint:disable:this unused_setter_value + } +} diff --git a/Extensions/String+Format.swift b/Extensions/String+Format.swift new file mode 100644 index 00000000..c35f327a --- /dev/null +++ b/Extensions/String+Format.swift @@ -0,0 +1,10 @@ +import Foundation + +extension String { + func replacingFirstOccurrence(of target: String, with replacement: String) -> String { + guard let range = range(of: target) else { + return self + } + return replacingCharacters(in: range, with: replacement) + } +} diff --git a/Extensions/View+Borders.swift b/Extensions/View+Borders.swift index 828e1150..f0cf155c 100644 --- a/Extensions/View+Borders.swift +++ b/Extensions/View+Borders.swift @@ -10,7 +10,21 @@ extension View { verticalEdgeBorder(.bottom, height: height, color: color) } + func borderLeading(width: Double, color: Color = Color(white: 0.7, opacity: 1)) -> some View { + horizontalEdgeBorder(.leading, width: width, color: color) + } + + func borderTrailing(width: Double, color: Color = Color(white: 0.7, opacity: 1)) -> some View { + horizontalEdgeBorder(.trailing, width: width, color: color) + } + private func verticalEdgeBorder(_ edge: Alignment, height: Double, color: Color) -> some View { - overlay(Rectangle().frame(width: nil, height: height, alignment: .top).foregroundColor(color), alignment: edge) + overlay(Rectangle().frame(width: nil, height: height, alignment: .top) + .foregroundColor(color), alignment: edge) + } + + private func horizontalEdgeBorder(_ edge: Alignment, width: Double, color: Color) -> some View { + overlay(Rectangle().frame(width: width, height: nil, alignment: .leading) + .foregroundColor(color), alignment: edge) } } diff --git a/Fixtures/Video+Fixtures.swift b/Fixtures/Video+Fixtures.swift index 166a7a42..09c422ea 100644 --- a/Fixtures/Video+Fixtures.swift +++ b/Fixtures/Video+Fixtures.swift @@ -24,7 +24,7 @@ extension Video { thumbnails: Thumbnail.fixturesForAllQualities(videoId: id), live: false, upcoming: false, - publishedAt: Date.now, + publishedAt: Date(), likes: 37333, dislikes: 30, keywords: ["very", "cool", "video", "msfs 2020", "757", "747", "A380", "737-900", "MOD", "Zibo", "MD80", "MD11", "Rotate", "Laminar", "787", "A350", "MSFS", "MS2020", "Microsoft Flight Simulator", "Microsoft", "Flight", "Simulator", "SIM", "World", "Ortho", "Flying", "Boeing MAX"] diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index cd256325..b906867e 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -45,6 +45,7 @@ final class PlayerModel: ObservableObject { var accounts: AccountsModel var composition = AVMutableComposition() + var loadedCompositionAssets = [AVMediaType]() private var currentArtwork: MPMediaItemArtwork? private var frequentTimeObserver: Any? @@ -147,9 +148,7 @@ final class PlayerModel: ObservableObject { logger.info("composition audio asset: \(stream.audioAsset.url)") logger.info("composition video asset: \(stream.videoAsset.url)") - Task { - await self.loadComposition(stream, of: video, preservingTime: preservingTime) - } + loadComposition(stream, of: video, preservingTime: preservingTime) } updateCurrentArtwork() @@ -228,46 +227,59 @@ final class PlayerModel: ObservableObject { _ stream: Stream, of video: Video, preservingTime: Bool = false - ) async { - await loadCompositionAsset(stream.audioAsset, type: .audio, of: video) - await loadCompositionAsset(stream.videoAsset, type: .video, of: video) - - guard streamSelection == stream else { - logger.critical("IGNORING LOADED") - return - } - - insertPlayerItem(stream, for: video, preservingTime: preservingTime) + ) { + loadedCompositionAssets = [] + loadCompositionAsset(stream.audioAsset, stream: stream, type: .audio, of: video, preservingTime: preservingTime) + loadCompositionAsset(stream.videoAsset, stream: stream, type: .video, of: video, preservingTime: preservingTime) } - private func loadCompositionAsset( + func loadCompositionAsset( _ asset: AVURLAsset, + stream: Stream, type: AVMediaType, - of video: Video - ) async { - async let assetTracks = asset.loadTracks(withMediaType: type) + of video: Video, + preservingTime: Bool = false + ) { + asset.loadValuesAsynchronously(forKeys: ["playable"]) { [weak self] in + guard let self = self else { + return + } + self.logger.info("loading \(type.rawValue) track") - logger.info("loading \(type.rawValue) track") - guard let compositionTrack = composition.addMutableTrack( - withMediaType: type, - preferredTrackID: kCMPersistentTrackID_Invalid - ) else { - logger.critical("composition \(type.rawValue) addMutableTrack FAILED") - return + let assetTracks = asset.tracks(withMediaType: type) + + guard let compositionTrack = self.composition.addMutableTrack( + withMediaType: type, + preferredTrackID: kCMPersistentTrackID_Invalid + ) else { + self.logger.critical("composition \(type.rawValue) addMutableTrack FAILED") + return + } + + guard let assetTrack = assetTracks.first else { + self.logger.critical("asset \(type.rawValue) track FAILED") + return + } + + try! compositionTrack.insertTimeRange( + CMTimeRange(start: .zero, duration: CMTime.secondsInDefaultTimescale(video.length)), + of: assetTrack, + at: .zero + ) + + self.logger.critical("\(type.rawValue) LOADED") + + guard self.streamSelection == stream else { + self.logger.critical("IGNORING LOADED") + return + } + + self.loadedCompositionAssets.append(type) + + if self.loadedCompositionAssets.count == 2 { + self.insertPlayerItem(stream, for: video, preservingTime: preservingTime) + } } - - guard let assetTrack = try? await assetTracks.first else { - logger.critical("asset \(type.rawValue) track FAILED") - return - } - - try! compositionTrack.insertTimeRange( - CMTimeRange(start: .zero, duration: CMTime.secondsInDefaultTimescale(video.length)), - of: assetTrack, - at: .zero - ) - - logger.critical("\(type.rawValue) LOADED") } private func playerItem(_ stream: Stream) -> AVPlayerItem? { diff --git a/Model/Search/SearchModel.swift b/Model/Search/SearchModel.swift index 0981f719..faf82b80 100644 --- a/Model/Search/SearchModel.swift +++ b/Model/Search/SearchModel.swift @@ -10,6 +10,8 @@ final class SearchModel: ObservableObject { @Published var queryText = "" @Published var querySuggestions = Store<[String]>() + @Published var fieldIsFocused = false + private var previousResource: Resource? private var resource: Resource! @@ -80,6 +82,10 @@ final class SearchModel: ObservableObject { private var suggestionsDebounceTimer: Timer? func loadSuggestions(_ query: String) { + guard !query.isEmpty else { + return + } + suggestionsDebounceTimer?.invalidate() suggestionsDebounceTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in diff --git a/README.md b/README.md index 2e694f47..470861c5 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ ![Yattee Banner](https://r.yattee.stream/icons/yattee-banner.png) -Video player with support for [Invidious](https://github.com/iv-org/invidious) and [Piped](https://github.com/TeamPiped/Piped) instances built for iOS 15, tvOS 15 and macOS Monterey. +Video player for [Invidious](https://github.com/iv-org/invidious) and [Piped](https://github.com/TeamPiped/Piped) instances built for iOS, tvOS and macOS. [![AGPL v3](https://shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0.en.html) -![GitHub issues](https://img.shields.io/github/issues/yattee/app) -![GitHub pull requests](https://img.shields.io/github/issues-pr/yattee/app) +[![GitHub issues](https://img.shields.io/github/issues/yattee/yattee)](https://github.com/yattee/yattee/issues) +[![GitHub pull requests](https://img.shields.io/github/issues-pr/yattee/yattee)](https://github.com/yattee/yattee/pulls) [![Matrix](https://img.shields.io/matrix/yattee:matrix.org)](https://matrix.to/#/#yattee:matrix.org) @@ -19,7 +19,7 @@ Video player with support for [Invidious](https://github.com/iv-org/invidious) a * Fullscreen playback, Picture in Picture and AirPlay support * Stream quality selection * Favorites: customizable section of channels, playlists, trending, searches and other views -* URL Scheme for integrations +* `yattee://` URL Scheme for integrations ### Availability | Feature | Invidious | Piped | @@ -38,17 +38,24 @@ Video player with support for [Invidious](https://github.com/iv-org/invidious) a ## Installation ### Requirements -Only iOS/tvOS 15 and macOS Monterey are supported. +System requirements: +* iOS 14 (or newer) +* tvOS 15 (or newer) +* macOS Big Sur (or newer) ### How to install? -#### [AltStore](https://altstore.io/) -You can sideload IPA files that you can download from Releases page. -Alternatively, if you have to access to the beta AltStore version (v1.5), you can add the following repository in `Browse > Sources` screen: +#### [AltStore](https://altstore.io/) (free) +You can sideload IPA files downloaded from the [Releases](https://github.com/yattee/yattee/releases) page to your iOS or tvOS device - check [AltStore FAQ](https://altstore.io/faq/) for more information. + +If you have to access to the beta AltStore version (v1.5, for Patreons only), you can add the following repository in `Browse > Sources` screen: `https://alt.yattee.stream` +#### Signing IPA files online (paid) +[UDID Registrations](https://www.udidregistrations.com/) provides services to sign IPA files for your devices. Refer to: ***Break free from the App Store*** section of the website for more information. + #### Manual installation -Download sources and compile them on a Mac using Xcode, install to your devices. Please note that if you are not registered in Apple Developer Program then the applications will require reinstalling every 7 days. +Download sources and compile them on a Mac using Xcode, install to your devices. Please note that if you are not registered in Apple Developer Program you will need to reinstall every 7 days. ## Integrations ### macOS @@ -63,14 +70,6 @@ With [Finicky](https://github.com/johnste/finicky) you can configure your system } ``` -### Experimental: Safari -macOS and iOS apps include Safari extension which will redirect opened YouTube tabs to the app. - -### Expermiental: Firefox -You can use [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect) extension to make the videos open in the app. In extension settings put the following URL as Invidious instance: - -`https://r.yatte.stream` - ## Screenshots ### iOS | Player | Search | Playlists | @@ -111,6 +110,30 @@ You can use [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect) * `Command+S` - Play Next * `Command+O` - Toggle Player + +## Donations + +You can support development of this app with +[Patreon](https://www.patreon.com/arekf) or cryptocurrencies: + +**Monero (XMR)** +``` +48zfKjLmnXs21PinU2ucMiUPwhiKt5d7WJKiy3ACVS28BKqSn52c1TX8L337oESHJ5TZCyGkozjfWZG11h6C46mN9n4NPrD +``` +**Bitcoin (BTC)** +``` +bc1qe24zz5a5hm0trc7glwckz93py274eycxzju3mv +``` +**Ethereum (ETH)** +``` +0xa2f81A58Ec5E550132F03615c8d91954A4E37423 +``` + +Donations will be used to cover development program access and domain renewal costs. + +## Contributing +If you're interestred in contributing, you can browse the [issues](https://github.com/yattee/yattee/issues) list or create a new one to discuss your feature idea. Every contribution is very welcome. + ## License and Liability Yattee and its components is shared on [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) license. diff --git a/Shared/Assets.xcassets/PlayerControlsBorderColor.colorset/Contents.json b/Shared/Assets.xcassets/ControlsBorderColor.colorset/Contents.json similarity index 100% rename from Shared/Assets.xcassets/PlayerControlsBorderColor.colorset/Contents.json rename to Shared/Assets.xcassets/ControlsBorderColor.colorset/Contents.json diff --git a/Shared/Favorites/FavoritesView.swift b/Shared/Favorites/FavoritesView.swift index a018a3c9..a6c5ffe7 100644 --- a/Shared/Favorites/FavoritesView.swift +++ b/Shared/Favorites/FavoritesView.swift @@ -51,7 +51,7 @@ struct FavoritesView: View { .navigationTitle("Favorites") #endif #if os(macOS) - .background() + .background(Color.tertiaryBackground) .frame(minWidth: 360) #endif } diff --git a/Shared/MenuCommands.swift b/Shared/MenuCommands.swift index 9ff90678..432d0981 100644 --- a/Shared/MenuCommands.swift +++ b/Shared/MenuCommands.swift @@ -10,7 +10,7 @@ struct MenuCommands: Commands { } private var navigationMenu: some Commands { - CommandMenu("Navigation") { + CommandGroup(before: .windowSize) { Button("Favorites") { model.navigation?.tabSelection = .favorites } @@ -19,7 +19,7 @@ struct MenuCommands: Commands { Button("Subscriptions") { model.navigation?.tabSelection = .subscriptions } - .disabled(!(model.accounts?.app.supportsSubscriptions ?? true)) + .disabled(subscriptionsDisabled) .keyboardShortcut("2") Button("Popular") { @@ -37,9 +37,17 @@ struct MenuCommands: Commands { model.navigation?.tabSelection = .search } .keyboardShortcut("f") + + Divider() } } + private var subscriptionsDisabled: Bool { + !( + (model.accounts?.app.supportsSubscriptions ?? false) && model.accounts?.signedIn ?? false + ) + } + private var playbackMenu: some Commands { CommandMenu("Playback") { Button((model.player?.isPlaying ?? true) ? "Pause" : "Play") { diff --git a/Shared/Modifiers/UnsubscribeAlertModifier.swift b/Shared/Modifiers/UnsubscribeAlertModifier.swift deleted file mode 100644 index 56068a0b..00000000 --- a/Shared/Modifiers/UnsubscribeAlertModifier.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import SwiftUI - -struct UnsubscribeAlertModifier: ViewModifier { - @EnvironmentObject private var navigation - @EnvironmentObject private var subscriptions - - func body(content: Content) -> some View { - content - .alert(unsubscribeAlertTitle, isPresented: $navigation.presentingUnsubscribeAlert) { - if let channel = navigation.channelToUnsubscribe { - Button("Unsubscribe", role: .destructive) { - subscriptions.unsubscribe(channel.id) - } - } - } - } - - var unsubscribeAlertTitle: String { - if let channel = navigation.channelToUnsubscribe { - return "Unsubscribe from \(channel.name)" - } - - return "Unknown channel" - } -} diff --git a/Shared/Navigation/AccountsMenuView.swift b/Shared/Navigation/AccountsMenuView.swift index 9afdf5c5..d7ba9986 100644 --- a/Shared/Navigation/AccountsMenuView.swift +++ b/Shared/Navigation/AccountsMenuView.swift @@ -15,13 +15,25 @@ struct AccountsMenuView: View { } } } label: { - Label(model.current?.description ?? "Select Account", systemImage: "person.crop.circle") - .labelStyle(.titleAndIcon) + if #available(iOS 15.0, macOS 12.0, *) { + label + .labelStyle(.titleAndIcon) + } else { + HStack { + Image(systemName: "person.crop.circle") + label + .labelStyle(.titleOnly) + } + } } .disabled(instances.isEmpty) .transaction { t in t.animation = .none } } + private var label: some View { + Label(model.current?.description ?? "Select Account", systemImage: "person.crop.circle") + } + private var allAccounts: [Account] { accounts + instances.map(\.anonymousAccount) } diff --git a/Shared/Navigation/AppSidebarPlaylists.swift b/Shared/Navigation/AppSidebarPlaylists.swift index 7d92ce8e..d12cb15c 100644 --- a/Shared/Navigation/AppSidebarPlaylists.swift +++ b/Shared/Navigation/AppSidebarPlaylists.swift @@ -12,6 +12,7 @@ struct AppSidebarPlaylists: View { LazyView(PlaylistVideosView(playlist)) } label: { Label(playlist.title, systemImage: AppSidebarNavigation.symbolSystemImage(playlist.title)) + .backport .badge(Text("\(playlist.videos.count)")) } .id(playlist.id) diff --git a/Shared/Navigation/AppSidebarSubscriptions.swift b/Shared/Navigation/AppSidebarSubscriptions.swift index ddbb5c4d..80063e11 100644 --- a/Shared/Navigation/AppSidebarSubscriptions.swift +++ b/Shared/Navigation/AppSidebarSubscriptions.swift @@ -18,7 +18,6 @@ struct AppSidebarSubscriptions: View { navigation.presentUnsubscribeAlert(channel) } } - .modifier(UnsubscribeAlertModifier()) .id("channel\(channel.id)") } } diff --git a/Shared/Navigation/AppTabNavigation.swift b/Shared/Navigation/AppTabNavigation.swift index 1f4e571f..2613112e 100644 --- a/Shared/Navigation/AppTabNavigation.swift +++ b/Shared/Navigation/AppTabNavigation.swift @@ -41,6 +41,8 @@ struct AppTabNavigation: View { } else { trendingNavigationView } + } else { + trendingNavigationView } } else { if accounts.app.supportsPopular { @@ -62,26 +64,7 @@ struct AppTabNavigation: View { } NavigationView { - LazyView( - SearchView() - .toolbar { toolbarContent } - .searchable(text: $search.queryText, placement: .navigationBarDrawer(displayMode: .always)) { - ForEach(search.querySuggestions.collection, id: \.self) { suggestion in - Text(suggestion) - .searchCompletion(suggestion) - } - } - .onChange(of: search.queryText) { query in - search.loadSuggestions(query) - } - .onSubmit(of: .search) { - search.changeQuery { query in - query.query = search.queryText - } - - recents.addQuery(search.queryText) - } - ) + LazyView(SearchView()) } .tabItem { Label("Search", systemImage: "magnifyingglass") @@ -129,7 +112,7 @@ struct AppTabNavigation: View { .toolbar { toolbarContent } } .tabItem { - Label("Popular", systemImage: "chart.bar") + Label("Popular", systemImage: "arrow.up.right.circle") .accessibility(label: Text("Popular")) } .tag(TabSelection.popular) @@ -141,7 +124,7 @@ struct AppTabNavigation: View { .toolbar { toolbarContent } } .tabItem { - Label("Trending", systemImage: "chart.line.uptrend.xyaxis") + Label("Trending", systemImage: "chart.bar") .accessibility(label: Text("Trending")) } .tag(TabSelection.trending) diff --git a/Shared/Navigation/Sidebar.swift b/Shared/Navigation/Sidebar.swift index add58854..3b5b7f71 100644 --- a/Shared/Navigation/Sidebar.swift +++ b/Shared/Navigation/Sidebar.swift @@ -46,7 +46,7 @@ struct Sidebar: View { } var mainNavigationLinks: some View { - Section("Videos") { + Section(header: Text("Videos")) { NavigationLink(destination: LazyView(FavoritesView()), tag: TabSelection.favorites, selection: $navigation.tabSelection) { Label("Favorites", systemImage: "heart") .accessibility(label: Text("Favorites")) @@ -60,13 +60,13 @@ struct Sidebar: View { if accounts.app.supportsPopular { NavigationLink(destination: LazyView(PopularView()), tag: TabSelection.popular, selection: $navigation.tabSelection) { - Label("Popular", systemImage: "chart.bar") + Label("Popular", systemImage: "arrow.up.right.circle") .accessibility(label: Text("Popular")) } } NavigationLink(destination: LazyView(TrendingView()), tag: TabSelection.trending, selection: $navigation.tabSelection) { - Label("Trending", systemImage: "chart.line.uptrend.xyaxis") + Label("Trending", systemImage: "chart.bar") .accessibility(label: Text("Trending")) } diff --git a/Shared/Player/PlaybackBar.swift b/Shared/Player/PlaybackBar.swift index 6bd43d0a..87c81c29 100644 --- a/Shared/Player/PlaybackBar.swift +++ b/Shared/Player/PlaybackBar.swift @@ -3,7 +3,8 @@ import Foundation import SwiftUI struct PlaybackBar: View { - @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var colorScheme + @Environment(\.presentationMode) private var presentationMode @Environment(\.inNavigationView) private var inNavigationView @EnvironmentObject private var player @@ -64,18 +65,20 @@ struct PlaybackBar: View { Spacer() } } - .alert(player.playerError?.localizedDescription ?? "", isPresented: $player.presentingErrorDetails) { - Button("OK") {} + .alert(isPresented: $player.presentingErrorDetails) { + Alert( + title: Text("Error"), + message: Text(player.playerError?.localizedDescription ?? "") + ) } - .environment(\.colorScheme, .dark) .frame(minWidth: 0, maxWidth: .infinity) .padding(4) - .background(.black) + .background(colorScheme == .dark ? Color.black : Color.white) } private var closeButton: some View { Button { - dismiss() + presentationMode.wrappedValue.dismiss() } label: { Label( "Close", @@ -105,10 +108,18 @@ struct PlaybackBar: View { return "less than a minute" } - let timeFinishAt = Date.now.addingTimeInterval(remainingSeconds) - let timeFinishAtString = timeFinishAt.formatted(date: .omitted, time: .shortened) + let timeFinishAt = Date().addingTimeInterval(remainingSeconds) - return "ends at \(timeFinishAtString)" + return "ends at \(formattedTimeFinishAt(timeFinishAt))" + } + + private func formattedTimeFinishAt(_ date: Date) -> String { + let dateFormatter = DateFormatter() + + dateFormatter.dateStyle = .none + dateFormatter.timeStyle = .short + + return dateFormatter.string(from: date) } private var rateMenu: some View { diff --git a/Shared/Player/PlayerQueueView.swift b/Shared/Player/PlayerQueueView.swift index 2110a5f5..24ebd5e5 100644 --- a/Shared/Player/PlayerQueueView.swift +++ b/Shared/Player/PlayerQueueView.swift @@ -44,16 +44,18 @@ struct PlayerQueueView: View { } ForEach(player.queue) { item in - PlayerQueueRow(item: item, fullScreen: $fullScreen) + let row = PlayerQueueRow(item: item, fullScreen: $fullScreen) .contextMenu { removeButton(item, history: false) removeAllButton(history: false) } - #if os(iOS) - .swipeActions(edge: .trailing, allowsFullSwipe: true) { + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + row.swipeActions(edge: .trailing, allowsFullSwipe: true) { removeButton(item, history: false) } - #endif + } else { + row + } } } } @@ -63,14 +65,18 @@ struct PlayerQueueView: View { if !player.history.isEmpty { Section(header: Text("Played Previously")) { ForEach(player.history) { item in - PlayerQueueRow(item: item, history: true, fullScreen: $fullScreen) + let row = PlayerQueueRow(item: item, history: true, fullScreen: $fullScreen) .contextMenu { removeButton(item, history: true) removeAllButton(history: true) } #if os(iOS) - .swipeActions(edge: .trailing, allowsFullSwipe: true) { - removeButton(item, history: true) + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + row.swipeActions(edge: .trailing, allowsFullSwipe: true) { + removeButton(item, history: true) + } + } else { + row } #endif } @@ -100,28 +106,44 @@ struct PlayerQueueView: View { } private func removeButton(_ item: PlayerQueueItem, history: Bool) -> some View { - Button(role: .destructive) { - if history { - player.removeHistory(item) - } else { - player.remove(item) + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + return Button(role: .destructive) { + removeButtonAction(item, history: history) + } label: { + Label("Remove", systemImage: "trash") + } + } else { + return Button { + removeButtonAction(item, history: history) + } label: { + Label("Remove", systemImage: "trash") } - } label: { - Label("Remove", systemImage: "trash") } } + private func removeButtonAction(_ item: PlayerQueueItem, history: Bool) { + _ = history ? player.removeHistory(item) : player.remove(item) + } + private func removeAllButton(history: Bool) -> some View { - Button(role: .destructive) { - if history { - player.removeHistoryItems() - } else { - player.removeQueueItems() + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + return Button(role: .destructive) { + removeAllButtonAction(history: history) + } label: { + Label("Remove All", systemImage: "trash.fill") + } + } else { + return Button { + removeAllButtonAction(history: history) + } label: { + Label("Remove All", systemImage: "trash.fill") } - } label: { - Label("Remove All", systemImage: "trash.fill") } } + + private func removeAllButtonAction(history: Bool) { + _ = history ? player.removeHistoryItems() : player.removeQueueItems() + } } struct PlayerQueueView_Previews: PreviewProvider { diff --git a/Shared/Player/VideoDetails.swift b/Shared/Player/VideoDetails.swift index 0940537c..f260aa4f 100644 --- a/Shared/Player/VideoDetails.swift +++ b/Shared/Player/VideoDetails.swift @@ -11,14 +11,14 @@ struct VideoDetails: View { @Binding var fullScreen: Bool @State private var subscribed = false - @State private var confirmationShown = false + @State private var presentingUnsubscribeAlert = false @State private var presentingAddToPlaylist = false @State private var presentingShareSheet = false @State private var shareURL: URL? @State private var currentPage = Page.details - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) private var presentationMode @Environment(\.inNavigationView) private var inNavigationView @EnvironmentObject private var accounts @@ -82,7 +82,7 @@ struct VideoDetails: View { if fullScreen { fullScreen = false } else { - self.dismiss() + self.presentationMode.wrappedValue.dismiss() } } } @@ -184,19 +184,26 @@ struct VideoDetails: View { Section { if subscribed { Button("Unsubscribe") { - confirmationShown = true + presentingUnsubscribeAlert = true } #if os(iOS) + .backport .tint(.gray) #endif - .confirmationDialog("Are you you want to unsubscribe from \(video!.channel.name)?", isPresented: $confirmationShown) { - Button("Unsubscribe") { - subscriptions.unsubscribe(video!.channel.id) + .alert(isPresented: $presentingUnsubscribeAlert) { + Alert( + title: Text( + "Are you you want to unsubscribe from \(video!.channel.name)?" + ), + primaryButton: .destructive(Text("Unsubscribe")) { + subscriptions.unsubscribe(video!.channel.id) - withAnimation { - subscribed.toggle() - } - } + withAnimation { + subscribed.toggle() + } + }, + secondaryButton: .cancel() + ) } } else { Button("Subscribe") { @@ -206,12 +213,12 @@ struct VideoDetails: View { subscribed.toggle() } } + .backport .tint(.blue) } } .font(.system(size: 13)) .buttonStyle(.borderless) - .buttonBorderShape(.roundedRectangle) } } } @@ -241,13 +248,13 @@ struct VideoDetails: View { Text(published) } - if let publishedAt = video.publishedAt { + if let date = video.publishedAt { if video.publishedDate != nil { Text("•") .foregroundColor(.secondary) .opacity(0.3) } - Text(publishedAt.formatted(date: .abbreviated, time: .omitted)) + Text(formattedPublishedAt(date)) } } .font(.system(size: 12)) @@ -257,6 +264,15 @@ struct VideoDetails: View { } } + func formattedPublishedAt(_ date: Date) -> String { + let dateFormatter = DateFormatter() + + dateFormatter.dateStyle = .short + dateFormatter.timeStyle = .none + + return dateFormatter.string(from: date) + } + var countsSection: some View { Group { if let video = player.currentVideo { @@ -338,11 +354,17 @@ struct VideoDetails: View { VStack(alignment: .leading, spacing: 10) { if let description = video.description { - Text(description) - .textSelection(.enabled) - .frame(maxWidth: .infinity, alignment: .leading) - .font(.caption) - .padding(.bottom, 4) + Group { + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + Text(description) + .textSelection(.enabled) + } else { + Text(description) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption) + .padding(.bottom, 4) } else { Text("No description") .foregroundColor(.secondary) diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 7bf09b5b..aded326d 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -16,8 +16,10 @@ struct VideoPlayerView: View { @State private var playerSize: CGSize = .zero @State private var fullScreen = false + @Environment(\.colorScheme) private var colorScheme + #if os(iOS) - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) private var presentationMode @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.verticalSizeClass) private var verticalSizeClass #endif @@ -82,11 +84,11 @@ struct VideoPlayerView: View { fullScreen = true } }, - down: { dismiss() } + down: { presentationMode.wrappedValue.dismiss() } ) #endif - .background(.black) + .background(Color.black) Group { #if os(iOS) @@ -98,7 +100,7 @@ struct VideoPlayerView: View { VideoDetails(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreen) #endif } - .background() + .background(colorScheme == .dark ? Color.black : Color.white) .modifier(VideoDetailsPaddingModifier(geometry: geometry, aspectRatio: player.controller?.aspectRatio, fullScreen: fullScreen)) } #endif diff --git a/Shared/Playlists/AddToPlaylistView.swift b/Shared/Playlists/AddToPlaylistView.swift index 16099acf..d557ef58 100644 --- a/Shared/Playlists/AddToPlaylistView.swift +++ b/Shared/Playlists/AddToPlaylistView.swift @@ -7,7 +7,7 @@ struct AddToPlaylistView: View { @State private var selectedPlaylistID: Playlist.ID = "" - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) private var presentationMode @EnvironmentObject private var model var body: some View { @@ -37,7 +37,7 @@ struct AddToPlaylistView: View { .padding(.vertical) #elseif os(tvOS) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .background(.thickMaterial) + .background(Color.tertiaryBackground) #else .padding(.vertical) #endif @@ -70,7 +70,7 @@ struct AddToPlaylistView: View { #if !os(tvOS) Button("Cancel") { - dismiss() + presentationMode.wrappedValue.dismiss() } .keyboardShortcut(.cancelAction) #endif @@ -155,7 +155,7 @@ struct AddToPlaylistView: View { Defaults[.lastUsedPlaylistID] = id model.addVideo(playlistID: id, videoID: video.videoID) { - dismiss() + presentationMode.wrappedValue.dismiss() } } diff --git a/Shared/Playlists/PlaylistFormView.swift b/Shared/Playlists/PlaylistFormView.swift index 6544ad25..6c628dd8 100644 --- a/Shared/Playlists/PlaylistFormView.swift +++ b/Shared/Playlists/PlaylistFormView.swift @@ -10,9 +10,7 @@ struct PlaylistFormView: View { @State private var valid = false @State private var showingDeleteConfirmation = false - @FocusState private var focused: Bool - - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) private var presentationMode @EnvironmentObject private var accounts @EnvironmentObject private var playlists @@ -22,77 +20,68 @@ struct PlaylistFormView: View { } var body: some View { - #if os(macOS) || os(iOS) - VStack(alignment: .leading) { - HStack(alignment: .center) { - Text(editing ? "Edit Playlist" : "Create Playlist") - .font(.title2.bold()) + Group { + #if os(macOS) || os(iOS) + VStack(alignment: .leading) { + HStack(alignment: .center) { + Text(editing ? "Edit Playlist" : "Create Playlist") + .font(.title2.bold()) - Spacer() + Spacer() - Button("Cancel") { - dismiss() - }.keyboardShortcut(.cancelAction) + Button("Cancel") { + presentationMode.wrappedValue.dismiss() + }.keyboardShortcut(.cancelAction) + } + .padding(.horizontal) + + Form { + TextField("Name", text: $name, onCommit: validate) + .frame(maxWidth: 450) + .padding(.leading, 10) + + visibilityFormItem + .pickerStyle(.segmented) + } + #if os(macOS) + .padding(.horizontal) + #endif + + HStack { + if editing { + deletePlaylistButton + } + + Spacer() + + Button("Save", action: submitForm) + .disabled(!valid) + .keyboardShortcut(.defaultAction) + } + .frame(minHeight: 35) + .padding(.horizontal) } - .padding(.horizontal) - Form { - TextField("Name", text: $name, onCommit: validate) - .frame(maxWidth: 450) - .padding(.leading, 10) - .focused($focused) - - visibilityFormItem - .pickerStyle(.segmented) - } - #if os(macOS) - .padding(.horizontal) + #if os(iOS) + .padding(.vertical) + #else + .frame(width: 400, height: 150) #endif - HStack { - if editing { - deletePlaylistButton - } - - Spacer() - - Button("Save", action: submitForm) - .disabled(!valid) - .keyboardShortcut(.defaultAction) - } - .frame(minHeight: 35) - .padding(.horizontal) - } - .onChange(of: name) { _ in validate() } - .onAppear(perform: initializeForm) - #if os(iOS) - .padding(.vertical) #else - .frame(width: 400, height: 150) + VStack { + Group { + header + form + } + .frame(maxWidth: 1000) + } + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) + .background(Color.tertiaryBackground) #endif - - #else - VStack { - Group { - header - form - } - .frame(maxWidth: 1000) - } - .onAppear { - guard editing else { - return - } - - self.name = self.playlist.title - self.visibility = self.playlist.visibility - - validate() - } - - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .background(.thickMaterial) - #endif + } + .onChange(of: name) { _ in validate() } + .onAppear(perform: initializeForm) } #if os(tvOS) @@ -152,16 +141,16 @@ struct PlaylistFormView: View { #endif func initializeForm() { - focused = true - guard editing else { return } - name = playlist.title - visibility = playlist.visibility + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + name = playlist.title + visibility = playlist.visibility - validate() + validate() + } } func validate() { @@ -182,7 +171,7 @@ struct PlaylistFormView: View { playlists.load(force: true) - dismiss() + presentationMode.wrappedValue.dismiss() } } @@ -194,7 +183,7 @@ struct PlaylistFormView: View { #if os(macOS) Picker("Visibility", selection: $visibility) { ForEach(Playlist.Visibility.allCases) { visibility in - Text(visibility.name) + Text(visibility.name).tag(visibility) } } #else @@ -216,9 +205,10 @@ struct PlaylistFormView: View { } var deletePlaylistButton: some View { - Button("Delete", role: .destructive) { + Button("Delete") { showingDeleteConfirmation = true - }.alert(isPresented: $showingDeleteConfirmation) { + } + .alert(isPresented: $showingDeleteConfirmation) { Alert( title: Text("Are you sure you want to delete playlist?"), message: Text("Playlist \"\(playlist.title)\" will be deleted.\nIt cannot be undone."), @@ -226,16 +216,14 @@ struct PlaylistFormView: View { secondaryButton: .cancel() ) } - #if os(macOS) .foregroundColor(.red) - #endif } func deletePlaylistAndDismiss() { accounts.api.playlist(playlist.id)?.request(.delete).onSuccess { _ in playlist = nil playlists.load(force: true) - dismiss() + presentationMode.wrappedValue.dismiss() } } } diff --git a/Shared/Playlists/PlaylistsView.swift b/Shared/Playlists/PlaylistsView.swift index 5e8b9d12..3d632d22 100644 --- a/Shared/Playlists/PlaylistsView.swift +++ b/Shared/Playlists/PlaylistsView.swift @@ -71,17 +71,20 @@ struct PlaylistsView: View { ToolbarItemGroup { #if !os(iOS) if !model.isEmpty { - selectPlaylistButton - .prefersDefaultFocus(in: focusNamespace) + if #available(macOS 12.0, *) { + selectPlaylistButton + .prefersDefaultFocus(in: focusNamespace) + } else { + selectPlaylistButton + } } if currentPlaylist != nil { editPlaylistButton } #endif - FavoriteButton(item: FavoriteItem(section: .playlist(selectedPlaylistID))) - newPlaylistButton + FavoriteButton(item: FavoriteItem(section: .playlist(selectedPlaylistID))) } #if os(iOS) @@ -99,6 +102,8 @@ struct PlaylistsView: View { Spacer() + newPlaylistButton + if currentPlaylist != nil { editPlaylistButton } @@ -168,7 +173,7 @@ struct PlaylistsView: View { } .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) #if os(macOS) - .background() + .background(Color.tertiaryBackground) #endif } diff --git a/Shared/Search/SearchField.swift b/Shared/Search/SearchField.swift new file mode 100644 index 00000000..50e39b55 --- /dev/null +++ b/Shared/Search/SearchField.swift @@ -0,0 +1,80 @@ +import SwiftUI + +struct SearchTextField: View { + @Environment(\.navigationStyle) private var navigationStyle + + @EnvironmentObject private var recents + @EnvironmentObject private var state + + var body: some View { + ZStack { + #if os(macOS) + fieldBorder + #endif + + HStack(spacing: 0) { + #if os(macOS) + Image(systemName: "magnifyingglass") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 12, height: 12) + .padding(.horizontal, 8) + .opacity(0.8) + #endif + TextField("Search...", text: $state.queryText) { + state.changeQuery { query in query.query = state.queryText } + recents.addQuery(state.queryText) + } + .onChange(of: state.queryText) { _ in + if state.query.query.compare(state.queryText, options: .caseInsensitive) == .orderedSame { + state.fieldIsFocused = true + } + } + #if os(macOS) + .textFieldStyle(.plain) + #else + .textFieldStyle(.roundedBorder) + .padding(.leading) + .padding(.trailing, 15) + #endif + if !self.state.queryText.isEmpty { + clearButton + } + } + } + .padding(.top, navigationStyle == .tab ? 10 : 0) + } + + private var fieldBorder: some View { + RoundedRectangle(cornerRadius: 5, style: .continuous) + .fill(Color.background) + .frame(width: 250, height: 32) + .overlay( + RoundedRectangle(cornerRadius: 5, style: .continuous) + .stroke( + state.fieldIsFocused ? Color.blue.opacity(0.7) : Color.gray.opacity(0.4), + lineWidth: state.fieldIsFocused ? 3 : 1 + ) + .frame(width: 250, height: 31) + ) + } + + private var clearButton: some View { + Button(action: { + self.state.queryText = "" + }) { + Image(systemName: "xmark.circle.fill") + .resizable() + .aspectRatio(contentMode: .fit) + #if os(macOS) + .frame(width: 14, height: 14) + #else + .frame(width: 18, height: 18) + #endif + .padding(.trailing, 3) + } + .buttonStyle(PlainButtonStyle()) + .padding(.trailing, 10) + .opacity(0.7) + } +} diff --git a/Shared/Search/SearchSuggestions.swift b/Shared/Search/SearchSuggestions.swift new file mode 100644 index 00000000..73a8008f --- /dev/null +++ b/Shared/Search/SearchSuggestions.swift @@ -0,0 +1,77 @@ +import SwiftUI + +struct SearchSuggestions: View { + @EnvironmentObject private var recents + @EnvironmentObject private var state + + var body: some View { + List { + Button { + state.changeQuery { query in + query.query = state.queryText + state.fieldIsFocused = false + } + + recents.addQuery(state.queryText) + } label: { + HStack(spacing: 5) { + Label(state.queryText, systemImage: "magnifyingglass") + .lineLimit(1) + } + } + #if os(macOS) + .onHover(perform: onHover(_:)) + #endif + + ForEach(visibleSuggestions, id: \.self) { suggestion in + Button { + state.queryText = suggestion + } label: { + HStack(spacing: 0) { + Label(state.queryText, systemImage: "arrow.up.left.circle") + .lineLimit(1) + .layoutPriority(2) + .foregroundColor(.secondary) + + Text(querySuffix(suggestion)) + .lineLimit(1) + .layoutPriority(1) + } + } + #if os(macOS) + .onHover(perform: onHover(_:)) + #endif + } + } + #if os(macOS) + .buttonStyle(.link) + #endif + } + + private var visibleSuggestions: [String] { + state.querySuggestions.collection.filter { + $0.compare(state.queryText, options: .caseInsensitive) != .orderedSame + } + } + + private func querySuffix(_ suggestion: String) -> String { + suggestion.replacingFirstOccurrence(of: state.queryText.lowercased(), with: "") + } + + #if os(macOS) + private func onHover(_ inside: Bool) { + if inside { + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } + } + #endif +} + +struct SearchSuggestions_Previews: PreviewProvider { + static var previews: some View { + SearchSuggestions() + .injectFixtureEnvironmentObjects() + } +} diff --git a/Shared/Views/SearchView.swift b/Shared/Search/SearchView.swift similarity index 73% rename from Shared/Views/SearchView.swift rename to Shared/Search/SearchView.swift index 03096c03..1248e92f 100644 --- a/Shared/Views/SearchView.swift +++ b/Shared/Search/SearchView.swift @@ -9,7 +9,6 @@ struct SearchView: View { @State private var searchDate = SearchQuery.Date.any @State private var searchDuration = SearchQuery.Duration.any - @State private var presentingClearConfirmation = false @State private var recentsChanged = false #if os(tvOS) @@ -31,50 +30,37 @@ struct SearchView: View { state.store.collection.sorted { $0 < $1 } } - init(_ query: SearchQuery? = nil, videos: [Video] = [Video]()) { + init(_ query: SearchQuery? = nil, videos: [Video] = []) { self.query = query self.videos = videos } var body: some View { PlayerControlsView { - VStack { - if showRecentQueries { - recentQueries - } else { - #if os(tvOS) - ScrollView(.vertical, showsIndicators: false) { - HStack(spacing: 0) { - if accounts.app.supportsSearchFilters { - filtersHorizontalStack - } + #if os(iOS) + VStack { + SearchTextField() - if let favoriteItem = favoriteItem { - FavoriteButton(item: favoriteItem) - .id(favoriteItem.id) - .labelStyle(.iconOnly) - .font(.system(size: 25)) - } - } - - HorizontalCells(items: items) - } - .edgesIgnoringSafeArea(.horizontal) - #else - VerticalCells(items: items) - #endif - - if noResults { - Text("No results") - - if searchFiltersActive { - Button("Reset search filters", action: resetFilters) - } - - Spacer() + if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty { + SearchSuggestions() + } else { + results } } - } + #else + ZStack { + results + + if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty { + HStack { + Spacer() + SearchSuggestions() + .borderLeading(width: 1, color: Color("ControlsBorderColor")) + .frame(maxWidth: 280) + } + } + } + #endif } .toolbar { #if !os(tvOS) @@ -118,6 +104,10 @@ struct SearchView: View { if accounts.app.supportsSearchFilters { filtersMenu } + + #if os(macOS) + SearchTextField() + #endif } #endif } @@ -132,10 +122,11 @@ struct SearchView: View { state.store.replace(ContentItem.array(of: videos)) } } - .searchable(text: $state.queryText, placement: searchFieldPlacement) { - ForEach(state.querySuggestions.collection, id: \.self) { suggestion in - Text(suggestion) - .searchCompletion(suggestion) + .onChange(of: state.query.query) { newQuery in + if newQuery.isEmpty { + favoriteItem = nil + } else { + updateFavoriteItem() } } .onChange(of: state.queryText) { newQuery in @@ -161,11 +152,7 @@ struct SearchView: View { } #endif } - .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 @@ -185,19 +172,55 @@ struct SearchView: View { } } #if !os(tvOS) + .ignoresSafeArea(.keyboard, edges: .bottom) .navigationTitle("Search") #endif - } - - var searchFieldPlacement: SearchFieldPlacement { #if os(iOS) - .navigationBarDrawer(displayMode: .always) - #else - .automatic + .navigationBarHidden(true) #endif } - var toolbarPlacement: ToolbarItemPlacement { + private var results: some View { + VStack { + if showRecentQueries { + recentQueries + } else { + #if os(tvOS) + ScrollView(.vertical, showsIndicators: false) { + 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) + } + .edgesIgnoringSafeArea(.horizontal) + #else + VerticalCells(items: items) + #endif + + if noResults { + Text("No results") + + if searchFiltersActive { + Button("Reset search filters", action: resetFilters) + } + + Spacer() + } + } + } + } + + private var toolbarPlacement: ToolbarItemPlacement { #if os(iOS) .bottomBar #else @@ -205,25 +228,25 @@ struct SearchView: View { #endif } - fileprivate var showRecentQueries: Bool { + private var showRecentQueries: Bool { navigationStyle == .tab && state.queryText.isEmpty } - fileprivate var filtersActive: Bool { + private var filtersActive: Bool { searchDuration != .any || searchDate != .any } - fileprivate func resetFilters() { + private func resetFilters() { searchSortOrder = .relevance searchDate = .any searchDuration = .any } - fileprivate var noResults: Bool { + private var noResults: Bool { items.isEmpty && !state.isLoading && !state.query.isEmpty } - var recentQueries: some View { + private var recentQueries: some View { VStack { List { Section(header: Text("Recents")) { @@ -237,22 +260,13 @@ struct SearchView: View { state.changeQuery { query in query.query = item.title } updateFavoriteItem() } - #if os(iOS) - .swipeActions(edge: .trailing) { - deleteButton(item) - } - #elseif os(tvOS) .contextMenu { deleteButton(item) + deleteAllButton } - #endif } } .redrawOn(change: recentsChanged) - - if !recentItems.isEmpty { - clearAllButton - } } } #if os(iOS) @@ -260,37 +274,33 @@ struct SearchView: View { #endif } - #if !os(macOS) - func deleteButton(_ item: RecentItem) -> some View { - Button(role: .destructive) { - recents.close(item) - recentsChanged.toggle() - } label: { - Label("Delete", systemImage: "trash") - } - } - #endif - - var clearAllButton: some View { - Button("Clear All", role: .destructive) { - presentingClearConfirmation = true - } - .confirmationDialog("Clear All", isPresented: $presentingClearConfirmation) { - Button("Clear All", role: .destructive) { - recents.clearQueries() - } + private func deleteButton(_ item: RecentItem) -> some View { + Button { + recents.close(item) + recentsChanged.toggle() + } label: { + Label("Delete", systemImage: "trash") } } - var searchFiltersActive: Bool { + private var deleteAllButton: some View { + Button { + recents.clearQueries() + recentsChanged.toggle() + } label: { + Label("Delete All", systemImage: "trash.fill") + } + } + + private var searchFiltersActive: Bool { searchDate != .any || searchDuration != .any } - var recentItems: [RecentItem] { + private var recentItems: [RecentItem] { Defaults[.recentlyOpened].filter { $0.type == .query }.reversed() } - var searchSortOrderPicker: some View { + private var searchSortOrderPicker: some View { Picker("Sort", selection: $searchSortOrder) { ForEach(SearchQuery.SortOrder.allCases) { sortOrder in Text(sortOrder.name).tag(sortOrder) @@ -299,7 +309,7 @@ struct SearchView: View { } #if os(tvOS) - var searchSortOrderButton: some View { + private var searchSortOrderButton: some View { Button(action: { self.searchSortOrder = self.searchSortOrder.next() }) { Text(self.searchSortOrder.name) .font(.system(size: 30)) .padding(.horizontal) @@ -315,7 +325,7 @@ struct SearchView: View { } } - var searchDateButton: some View { + private var searchDateButton: some View { Button(action: { self.searchDate = self.searchDate.next() }) { Text(self.searchDate.name) .font(.system(size: 30)) @@ -332,7 +342,7 @@ struct SearchView: View { } } - var searchDurationButton: some View { + private var searchDurationButton: some View { Button(action: { self.searchDuration = self.searchDuration.next() }) { Text(self.searchDuration.name) .font(.system(size: 30)) @@ -349,7 +359,7 @@ struct SearchView: View { } } - var filtersHorizontalStack: some View { + private var filtersHorizontalStack: some View { HStack { HStack(spacing: 30) { Text("Sort") @@ -375,7 +385,7 @@ struct SearchView: View { .font(.system(size: 30)) } #else - var filtersMenu: some View { + private var filtersMenu: some View { Menu(filtersActive ? "Filter: active" : "Filter") { Picker(selection: $searchDuration, label: Text("Duration")) { ForEach(SearchQuery.Duration.allCases) { duration in diff --git a/Shared/Settings/AccountForm.swift b/Shared/Settings/AccountForm.swift index 0815a3fe..e0d4b1fa 100644 --- a/Shared/Settings/AccountForm.swift +++ b/Shared/Settings/AccountForm.swift @@ -15,9 +15,7 @@ struct AccountForm: View { @State private var validationError: String? @State private var validationDebounce = Debounce() - @FocusState private var focused: Bool - - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) private var presentationMode var body: some View { VStack { @@ -32,7 +30,7 @@ struct AccountForm: View { .padding(.vertical) #elseif os(tvOS) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .background(.thickMaterial) + .background(Color.tertiaryBackground) #else .frame(width: 400, height: 145) #endif @@ -46,7 +44,7 @@ struct AccountForm: View { Spacer() Button("Cancel") { - dismiss() + presentationMode.wrappedValue.dismiss() } #if !os(tvOS) .keyboardShortcut(.cancelAction) @@ -68,7 +66,6 @@ struct AccountForm: View { formFields #endif } - .onAppear(perform: initializeForm) .onChange(of: username) { _ in validate() } .onChange(of: password) { _ in validate() } } @@ -76,24 +73,23 @@ struct AccountForm: View { var formFields: some View { Group { if !instance.app.accountsUsePassword { - TextField("Name", text: $name, prompt: Text("Account Name (optional)")) - .focused($focused) + TextField("Name", text: $name) } - TextField("Username", text: $username, prompt: usernamePrompt) + TextField(usernamePrompt, text: $username) if instance.app.accountsUsePassword { - SecureField("Password", text: $password, prompt: Text("Password")) + SecureField("Password", text: $password) } } } - var usernamePrompt: Text { + var usernamePrompt: String { switch instance.app { case .invidious: - return Text("SID Cookie") + return "SID Cookie" default: - return Text("Username") + return "Username" } } @@ -121,10 +117,6 @@ struct AccountForm: View { .padding(.horizontal) } - private func initializeForm() { - focused = true - } - private func validate() { isValid = false validationDebounce.invalidate() @@ -151,7 +143,7 @@ struct AccountForm: View { let account = AccountsModel.add(instance: instance, name: name, username: username, password: password) selectedAccount?.wrappedValue = account - dismiss() + presentationMode.wrappedValue.dismiss() } private var validator: AccountValidator { diff --git a/Shared/Settings/AccountsNavigationLink.swift b/Shared/Settings/AccountsNavigationLink.swift new file mode 100644 index 00000000..cdb6fdac --- /dev/null +++ b/Shared/Settings/AccountsNavigationLink.swift @@ -0,0 +1,31 @@ +import SwiftUI + +struct AccountsNavigationLink: View { + @EnvironmentObject private var accounts + var instance: Instance + + var body: some View { + NavigationLink(instance.longDescription) { + InstanceSettings(instanceID: instance.id) + } + .buttonStyle(.plain) + .contextMenu { + removeInstanceButton(instance) + } + } + + private func removeInstanceButton(_ instance: Instance) -> some View { + if #available(iOS 15.0, *) { + return Button("Remove", role: .destructive) { removeAction(instance) } + } else { + return Button("Remove") { removeAction(instance) } + } + } + + private func removeAction(_ instance: Instance) { + if accounts.current?.instance == instance { + accounts.setCurrent(nil) + } + InstancesModel.remove(instance) + } +} diff --git a/Shared/Settings/AccountsSettings.swift b/Shared/Settings/AccountsSettings.swift deleted file mode 100644 index 17542f8a..00000000 --- a/Shared/Settings/AccountsSettings.swift +++ /dev/null @@ -1,102 +0,0 @@ -import SwiftUI - -struct AccountsSettings: View { - let instanceID: Instance.ID? - - @State private var accountsChanged = false - @State private var presentingAccountForm = false - - @State private var frontendURL = "" - - @EnvironmentObject private var model - @EnvironmentObject private var instances - - var instance: Instance! { - InstancesModel.find(instanceID) - } - - var body: some View { - List { - if instance.app.hasFrontendURL { - Section(header: Text("Frontend URL")) { - TextField( - "Frontend URL", - text: $frontendURL, - prompt: Text("To enable videos, channels and playlists sharing") - ) - .onAppear { - frontendURL = instance.frontendURL ?? "" - } - .onChange(of: frontendURL) { newValue in - InstancesModel.setFrontendURL(instance, newValue) - } - .labelsHidden() - .autocapitalization(.none) - .keyboardType(.URL) - } - } - - Section(header: Text("Accounts"), footer: sectionFooter) { - if instance.app.supportsAccounts { - accounts - } else { - Text("Accounts are not supported for the application of this instance") - .foregroundColor(.secondary) - } - } - } - #if os(tvOS) - .frame(maxWidth: 1000) - #endif - - .navigationTitle(instance.description) - } - - var accounts: some View { - Group { - ForEach(InstancesModel.accounts(instanceID), id: \.self) { account in - #if os(tvOS) - Button(account.description) {} - .contextMenu { - Button("Remove", role: .destructive) { removeAccount(account) } - Button("Cancel", role: .cancel) {} - } - #else - Text(account.description) - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button("Remove", role: .destructive) { removeAccount(account) } - } - #endif - } - .redrawOn(change: accountsChanged) - - Button("Add account...") { - presentingAccountForm = true - } - } - .sheet(isPresented: $presentingAccountForm, onDismiss: { accountsChanged.toggle() }) { - AccountForm(instance: instance) - } - #if !os(tvOS) - .listStyle(.insetGrouped) - #endif - } - - private var sectionFooter: some View { - if !instance.app.supportsAccounts { - return Text("") - } - - #if os(iOS) - return Text("Swipe to remove account") - #else - return Text("Tap and hold to remove account") - .foregroundColor(.secondary) - #endif - } - - private func removeAccount(_ account: Account) { - AccountsModel.remove(account) - accountsChanged.toggle() - } -} diff --git a/Shared/Settings/InstanceForm.swift b/Shared/Settings/InstanceForm.swift index ae1c7448..4497d732 100644 --- a/Shared/Settings/InstanceForm.swift +++ b/Shared/Settings/InstanceForm.swift @@ -13,9 +13,7 @@ struct InstanceForm: View { @State private var validationError: String? @State private var validationDebounce = Debounce() - @FocusState private var nameFieldFocused: Bool - - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) private var presentationMode var body: some View { VStack(alignment: .leading) { @@ -30,12 +28,11 @@ struct InstanceForm: View { } .onChange(of: app) { _ in validate() } .onChange(of: url) { _ in validate() } - .onAppear(perform: initializeForm) #if os(iOS) .padding(.vertical) #elseif os(tvOS) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .background(.thickMaterial) + .background(Color.tertiaryBackground) #else .frame(width: 400, height: 190) #endif @@ -49,7 +46,7 @@ struct InstanceForm: View { Spacer() Button("Cancel") { - dismiss() + presentationMode.wrappedValue.dismiss() } #if !os(tvOS) .keyboardShortcut(.cancelAction) @@ -80,10 +77,9 @@ struct InstanceForm: View { } .pickerStyle(.segmented) - TextField("Name", text: $name, prompt: Text("Instance Name (optional)")) - .focused($nameFieldFocused) + TextField("Name", text: $name) - TextField("API URL", text: $url, prompt: Text("https://invidious.home.net")) + TextField("API URL", text: $url) #if !os(macOS) .autocapitalization(.none) @@ -138,10 +134,6 @@ struct InstanceForm: View { } } - func initializeForm() { - nameFieldFocused = true - } - func submitForm() { guard isValid else { return @@ -149,7 +141,7 @@ struct InstanceForm: View { savedInstanceID = InstancesModel.add(app: app, name: name, url: url).id - dismiss() + presentationMode.wrappedValue.dismiss() } } diff --git a/Shared/Settings/InstanceSettings.swift b/Shared/Settings/InstanceSettings.swift new file mode 100644 index 00000000..fd193248 --- /dev/null +++ b/Shared/Settings/InstanceSettings.swift @@ -0,0 +1,104 @@ +import SwiftUI + +struct InstanceSettings: View { + let instanceID: Instance.ID? + + @State private var accountsChanged = false + @State private var presentingAccountForm = false + + @State private var frontendURL = "" + + @EnvironmentObject private var model + @EnvironmentObject private var instances + + var instance: Instance! { + InstancesModel.find(instanceID) + } + + var body: some View { + List { + if instance.app.hasFrontendURL { + Section(header: Text("Frontend URL")) { + TextField( + "Frontend URL", + text: $frontendURL + ) + .onAppear { + frontendURL = instance.frontendURL ?? "" + } + .onChange(of: frontendURL) { newValue in + InstancesModel.setFrontendURL(instance, newValue) + } + .labelsHidden() + .autocapitalization(.none) + .keyboardType(.URL) + } + } + + Section(header: Text("Accounts"), footer: sectionFooter) { + if instance.app.supportsAccounts { + ForEach(InstancesModel.accounts(instanceID), id: \.self) { account in + #if os(tvOS) + Button(account.description) {} + .contextMenu { + Button("Remove") { removeAccount(account) } + Button("Cancel", role: .cancel) {} + } + #else + ZStack { + NavigationLink(destination: EmptyView()) { + EmptyView() + } + .disabled(true) + .hidden() + + HStack { + Text(account.description) + Spacer() + } + .contextMenu { + Button("Remove") { removeAccount(account) } + } + } + #endif + } + .redrawOn(change: accountsChanged) + + Button("Add account...") { + presentingAccountForm = true + } + .sheet(isPresented: $presentingAccountForm, onDismiss: { accountsChanged.toggle() }) { + AccountForm(instance: instance) + } + #if !os(tvOS) + .listStyle(.insetGrouped) + #endif + } else { + Text("Accounts are not supported for the application of this instance") + .foregroundColor(.secondary) + } + } + } + #if os(tvOS) + .frame(maxWidth: 1000) + #elseif os(iOS) + .listStyle(.insetGrouped) + #endif + + .navigationTitle(instance.description) + } + + private var sectionFooter: some View { + if !instance.app.supportsAccounts { + return Text("") + } + + return Text("Tap and hold to remove account") + .foregroundColor(.secondary) + } + + private func removeAccount(_ account: Account) { + AccountsModel.remove(account) + accountsChanged.toggle() + } +} diff --git a/Shared/Settings/InstancesSettings.swift b/Shared/Settings/InstancesSettings.swift deleted file mode 100644 index 83bc1528..00000000 --- a/Shared/Settings/InstancesSettings.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Defaults -import SwiftUI - -struct InstancesSettings: View { - @Default(.instances) private var instances - - @EnvironmentObject private var accounts - - @State private var selectedInstanceID: Instance.ID? - @State private var selectedAccount: Account? - - @State private var presentingInstanceForm = false - @State private var savedFormInstanceID: Instance.ID? - - var body: some View { - Group { - Section(header: SettingsHeader(text: "Instances")) { - ForEach(instances) { instance in - Group { - NavigationLink(instance.longDescription) { - AccountsSettings(instanceID: instance.id) - } - } - #if os(iOS) - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - removeInstanceButton(instance) - } - .buttonStyle(.plain) - #else - .contextMenu { - removeInstanceButton(instance) - } - #endif - } - - addInstanceButton - } - #if os(iOS) - .listStyle(.insetGrouped) - #endif - } - .sheet(isPresented: $presentingInstanceForm) { - InstanceForm(savedInstanceID: $savedFormInstanceID) - } - } - - private var addInstanceButton: some View { - Button("Add Instance...") { - presentingInstanceForm = true - } - } - - private func removeInstanceButton(_ instance: Instance) -> some View { - Button("Remove", role: .destructive) { - if accounts.current?.instance == instance { - accounts.setCurrent(nil) - } - InstancesModel.remove(instance) - } - } -} - -struct InstancesSettingsView_Previews: PreviewProvider { - static var previews: some View { - VStack { - InstancesSettings() - } - .frame(width: 400, height: 270) - } -} diff --git a/Shared/Settings/ServicesSettings.swift b/Shared/Settings/ServicesSettings.swift index 40044384..06cd1cd8 100644 --- a/Shared/Settings/ServicesSettings.swift +++ b/Shared/Settings/ServicesSettings.swift @@ -9,8 +9,7 @@ struct ServicesSettings: View { Section(header: SettingsHeader(text: "SponsorBlock API")) { TextField( "SponsorBlock API Instance", - text: $sponsorBlockInstance, - prompt: Text("SponsorBlock API URL, leave blank to disable") + text: $sponsorBlockInstance ) .labelsHidden() #if !os(macOS) @@ -21,7 +20,7 @@ struct ServicesSettings: View { Section(header: SettingsHeader(text: "Categories to Skip")) { #if os(macOS) - List(SponsorBlockAPI.categories, id: \.self) { category in + let list = List(SponsorBlockAPI.categories, id: \.self) { category in SponsorBlockCategorySelectionRow( title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown", selected: sponsorBlockCategories.contains(category) @@ -29,7 +28,16 @@ struct ServicesSettings: View { toggleCategory(category, value: value) } } - .listStyle(.inset(alternatesRowBackgrounds: true)) + + Group { + if #available(macOS 12.0, *) { + list + .listStyle(.inset(alternatesRowBackgrounds: true)) + } else { + list + .listStyle(.inset) + } + } Spacer() #else ForEach(SponsorBlockAPI.categories, id: \.self) { category in diff --git a/Shared/Settings/SettingsView.swift b/Shared/Settings/SettingsView.swift index d73bbd45..387ce99b 100644 --- a/Shared/Settings/SettingsView.swift +++ b/Shared/Settings/SettingsView.swift @@ -10,11 +10,16 @@ struct SettingsView: View { #endif #if os(iOS) - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) private var presentationMode #endif @EnvironmentObject private var accounts + @State private var presentingInstanceForm = false + @State private var savedFormInstanceID: Instance.ID? + + @Default(.instances) private var instances + var body: some View { #if os(macOS) TabView { @@ -65,8 +70,14 @@ struct SettingsView: View { } } #endif - InstancesSettings() - .environmentObject(accounts) + + Section(header: Text("Instances")) { + ForEach(instances) { instance in + AccountsNavigationLink(instance: instance) + } + addInstanceButton + } + BrowsingSettings() PlaybackSettings() ServicesSettings() @@ -76,7 +87,7 @@ struct SettingsView: View { ToolbarItem(placement: .navigationBarTrailing) { #if !os(tvOS) Button("Done") { - dismiss() + presentationMode.wrappedValue.dismiss() } .keyboardShortcut(.cancelAction) #endif @@ -87,11 +98,20 @@ struct SettingsView: View { .listStyle(.insetGrouped) #endif } + .sheet(isPresented: $presentingInstanceForm) { + InstanceForm(savedInstanceID: $savedFormInstanceID) + } #if os(tvOS) - .background(.black) + .background(Color.black) #endif #endif } + + private var addInstanceButton: some View { + Button("Add Instance...") { + presentingInstanceForm = true + } + } } struct SettingsView_Previews: PreviewProvider { diff --git a/Shared/Trending/TrendingCountry.swift b/Shared/Trending/TrendingCountry.swift index fa99a3cb..3e51835d 100644 --- a/Shared/Trending/TrendingCountry.swift +++ b/Shared/Trending/TrendingCountry.swift @@ -9,51 +9,34 @@ struct TrendingCountry: View { @State private var query: String = "" @State private var selection: Country? - @FocusState var countryIsFocused - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) private var presentationMode var body: some View { VStack { - #if os(macOS) + #if !os(tvOS) HStack { - TextField("Country", text: $query, prompt: Text(TrendingCountry.prompt)) - .focused($countryIsFocused) + if #available(iOS 15.0, macOS 12.0, *) { + TextField("Country", text: $query, prompt: Text(TrendingCountry.prompt)) + } else { + TextField(TrendingCountry.prompt, text: $query) + } Button("Done") { selectCountryAndDismiss() } .keyboardShortcut(.defaultAction) .keyboardShortcut(.cancelAction) } .padding([.horizontal, .top]) - - countriesList - #else - NavigationView { - countriesList - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - Button("Done") { selectCountryAndDismiss() } - } - } - #if os(iOS) - .navigationBarTitle("Trending Country", displayMode: .automatic) - #endif - } #endif + countriesList } - .onAppear { - countryIsFocused = true - } - .onSubmit { selectCountryAndDismiss() } - #if !os(macOS) - .searchable(text: $query, placement: searchPlacement, prompt: Text(TrendingCountry.prompt)) - #endif #if os(tvOS) - .background(.thinMaterial) + .searchable(text: $query, placement: .automatic, prompt: Text(TrendingCountry.prompt)) + .background(Color.black) #endif } var countriesList: some View { - ScrollViewReader { _ in + let list = ScrollViewReader { _ in List(store.collection, selection: $selection) { country in #if os(macOS) Text(country.name) @@ -71,29 +54,29 @@ struct TrendingCountry: View { } } - #if os(macOS) - .listStyle(.inset(alternatesRowBackgrounds: true)) - .padding(.bottom, 5) - - #endif - } - - #if !os(macOS) - var searchPlacement: SearchFieldPlacement { - #if os(iOS) - .navigationBarDrawer(displayMode: .always) + return Group { + #if os(macOS) + if #available(macOS 12.0, *) { + list + .listStyle(.inset(alternatesRowBackgrounds: true)) + } else { + list + } #else - .automatic + list #endif } - #endif + #if os(macOS) + .padding(.bottom, 5) + #endif + } func selectCountryAndDismiss(_ country: Country? = nil) { if let selected = country ?? selection { selectedCountry = selected } - dismiss() + presentationMode.wrappedValue.dismiss() } } diff --git a/Shared/Trending/TrendingView.swift b/Shared/Trending/TrendingView.swift index e562c303..a9dd405c 100644 --- a/Shared/Trending/TrendingView.swift +++ b/Shared/Trending/TrendingView.swift @@ -172,11 +172,12 @@ struct TrendingView: View { } #else - Picker("Category", selection: $category) { + Picker(category.controlLabel, selection: $category) { ForEach(TrendingCategory.allCases) { category in Text(category.controlLabel).tag(category) } } + .pickerStyle(.menu) #endif } diff --git a/Shared/Videos/VerticalCells.swift b/Shared/Videos/VerticalCells.swift index 08ff6fda..d90ff235 100644 --- a/Shared/Videos/VerticalCells.swift +++ b/Shared/Videos/VerticalCells.swift @@ -19,7 +19,7 @@ struct VerticalCells: View { } .edgesIgnoringSafeArea(.horizontal) #if os(macOS) - .background() + .background(Color.tertiaryBackground) .frame(minWidth: 360) #endif } diff --git a/Shared/Videos/VideoCell.swift b/Shared/Videos/VideoCell.swift index 63727a4d..a6a95faf 100644 --- a/Shared/Videos/VideoCell.swift +++ b/Shared/Videos/VideoCell.swift @@ -12,6 +12,7 @@ struct VideoCell: View { @Environment(\.horizontalCells) private var horizontalCells #endif + @EnvironmentObject private var accounts @EnvironmentObject private var player @EnvironmentObject private var thumbnails @@ -38,7 +39,13 @@ struct VideoCell: View { } .buttonStyle(.plain) .contentShape(RoundedRectangle(cornerRadius: 12)) - .contextMenu { VideoContextMenuView(video: video, playerNavigationLinkActive: $player.playerNavigationLinkActive) } + .contextMenu { + VideoContextMenuView( + video: video, + playerNavigationLinkActive: $player.playerNavigationLinkActive + ) + .environmentObject(accounts) + } } var content: some View { @@ -55,7 +62,7 @@ struct VideoCell: View { #endif } #if os(macOS) - .background() + .background(Color.tertiaryBackground) #endif } diff --git a/Shared/Views/ChannelPlaylistView.swift b/Shared/Views/ChannelPlaylistView.swift index fa27911c..fc16d5c5 100644 --- a/Shared/Views/ChannelPlaylistView.swift +++ b/Shared/Views/ChannelPlaylistView.swift @@ -83,7 +83,7 @@ struct ChannelPlaylistView: View { .navigationTitle(playlist.title) #else - .background(.thickMaterial) + .background(Color.tertiaryBackground) #endif } diff --git a/Shared/Views/ChannelVideosView.swift b/Shared/Views/ChannelVideosView.swift index dd52bec9..c6ae41ca 100644 --- a/Shared/Views/ChannelVideosView.swift +++ b/Shared/Views/ChannelVideosView.swift @@ -9,7 +9,7 @@ struct ChannelVideosView: View { @StateObject private var store = Store() - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) private var presentationMode @Environment(\.inNavigationView) private var inNavigationView #if os(iOS) @@ -43,7 +43,7 @@ struct ChannelVideosView: View { } var content: some View { - VStack { + let content = VStack { #if os(tvOS) HStack { Text(navigationTitle) @@ -65,40 +65,43 @@ struct ChannelVideosView: View { .frame(maxWidth: .infinity) #endif - VerticalCells(items: videos) - - #if !os(iOS) - .prefersDefaultFocus(in: focusNamespace) + #if os(iOS) + VerticalCells(items: videos) + #else + if #available(macOS 12.0, *) { + VerticalCells(items: videos) + .prefersDefaultFocus(in: focusNamespace) + } else { + VerticalCells(items: videos) + } #endif } .environment(\.inChannelView, true) - #if !os(iOS) - .focusScope(focusNamespace) - #endif + #if !os(tvOS) - .toolbar { - ToolbarItem(placement: .navigation) { - ShareButton( - contentItem: contentItem, - presentingShareSheet: $presentingShareSheet, - shareURL: $shareURL - ) - } + .toolbar { + ToolbarItem(placement: .navigation) { + ShareButton( + contentItem: contentItem, + presentingShareSheet: $presentingShareSheet, + shareURL: $shareURL + ) + } - ToolbarItem { - HStack { - Text("**\(store.item?.subscriptionsString ?? "loading")** subscribers") - .foregroundColor(.secondary) - .opacity(store.item?.subscriptionsString != nil ? 1 : 0) + ToolbarItem { + HStack { + Text("**\(store.item?.subscriptionsString ?? "loading")** subscribers") + .foregroundColor(.secondary) + .opacity(store.item?.subscriptionsString != nil ? 1 : 0) - subscriptionToggleButton + subscriptionToggleButton - FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name))) + FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name))) + } } } - } #else - .background(.thickMaterial) + .background(Color.tertiaryBackground) #endif #if os(iOS) .sheet(isPresented: $presentingShareSheet) { @@ -107,7 +110,6 @@ struct ChannelVideosView: View { } } #endif - .modifier(UnsubscribeAlertModifier()) .onAppear { if store.item.isNil { resource.addObserver(store) @@ -115,6 +117,17 @@ struct ChannelVideosView: View { } } .navigationTitle(navigationTitle) + + return Group { + if #available(macOS 12.0, *) { + content + #if !os(iOS) + .focusScope(focusNamespace) + #endif + } else { + content + } + } } private var resource: Resource { diff --git a/Shared/Views/DetailBadge.swift b/Shared/Views/DetailBadge.swift index dfc1fbba..00e64d3b 100644 --- a/Shared/Views/DetailBadge.swift +++ b/Shared/Views/DetailBadge.swift @@ -26,8 +26,13 @@ struct DetailBadge: View { struct DefaultStyleModifier: ViewModifier { func body(content: Content) -> some View { - content - .background(.thinMaterial) + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + content + .background(.thinMaterial) + } else { + content + .background(Color.background) + } } } diff --git a/Shared/Views/OpenSettingsButton.swift b/Shared/Views/OpenSettingsButton.swift index f78dfd8c..68e35c81 100644 --- a/Shared/Views/OpenSettingsButton.swift +++ b/Shared/Views/OpenSettingsButton.swift @@ -1,15 +1,15 @@ import SwiftUI struct OpenSettingsButton: View { - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) private var presentationMode #if !os(macOS) @EnvironmentObject private var navigation #endif var body: some View { - Button { - dismiss() + let button = Button { + presentationMode.wrappedValue.dismiss() #if os(macOS) NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) @@ -19,7 +19,13 @@ struct OpenSettingsButton: View { } label: { Label("Open Settings", systemImage: "gearshape.2") } - .buttonStyle(.borderedProminent) + + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + button + .buttonStyle(.borderedProminent) + } else { + button + } } } diff --git a/Shared/Views/PlayerControlsView.swift b/Shared/Views/PlayerControlsView.swift index d25ffbac..74bd1c5c 100644 --- a/Shared/Views/PlayerControlsView.swift +++ b/Shared/Views/PlayerControlsView.swift @@ -25,7 +25,7 @@ struct PlayerControlsView: View { } private var controls: some View { - HStack { + let controls = HStack { Button(action: { model.presentingPlayer.toggle() }) { @@ -92,14 +92,23 @@ struct PlayerControlsView: View { .padding(.horizontal) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 55) .padding(.vertical, 0) - .background(.ultraThinMaterial) - .borderTop(height: 0.4, color: Color("PlayerControlsBorderColor")) - .borderBottom(height: navigationStyle == .sidebar ? 0 : 0.4, color: Color("PlayerControlsBorderColor")) + .borderTop(height: 0.4, color: Color("ControlsBorderColor")) + .borderBottom(height: navigationStyle == .sidebar ? 0 : 0.4, color: Color("ControlsBorderColor")) #if !os(tvOS) .onSwipeGesture(up: { model.presentingPlayer = true }) #endif + + return Group { + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + controls + .background(Material.ultraThinMaterial) + } else { + controls + .background(Color.tertiaryBackground) + } + } } private var appVersion: String { diff --git a/Shared/Views/SubscriptionsView.swift b/Shared/Views/SubscriptionsView.swift index 454d28ab..31b5dff0 100644 --- a/Shared/Views/SubscriptionsView.swift +++ b/Shared/Views/SubscriptionsView.swift @@ -31,9 +31,6 @@ struct SubscriptionsView: View { FavoriteButton(item: FavoriteItem(section: .subscriptions)) } } - .refreshable { - loadResources(force: true) - } } fileprivate func loadResources(force: Bool = false) { diff --git a/Shared/Views/VideoContextMenuView.swift b/Shared/Views/VideoContextMenuView.swift index 8a439d3b..a85d7cd8 100644 --- a/Shared/Views/VideoContextMenuView.swift +++ b/Shared/Views/VideoContextMenuView.swift @@ -113,7 +113,7 @@ struct VideoContextMenuView: View { private var subscriptionButton: some View { Group { if subscriptions.isSubscribing(video.channel.id) { - Button(role: .destructive) { + Button { #if os(tvOS) subscriptions.unsubscribe(video.channel.id) #else @@ -143,7 +143,7 @@ struct VideoContextMenuView: View { } func removeFromPlaylistButton(playlistID: String) -> some View { - Button(role: .destructive) { + Button { playlists.removeVideo(videoIndexID: video.indexID!, playlistID: playlistID) } label: { Label("Remove from playlist", systemImage: "text.badge.minus") diff --git a/Shared/Views/WelcomeScreen.swift b/Shared/Views/WelcomeScreen.swift index 6b37a468..c392251a 100644 --- a/Shared/Views/WelcomeScreen.swift +++ b/Shared/Views/WelcomeScreen.swift @@ -2,14 +2,14 @@ import Defaults import SwiftUI struct WelcomeScreen: View { - @Environment(\.dismiss) private var dismiss + @Environment(\.presentationMode) private var presentationMode @EnvironmentObject private var accounts @Default(.accounts) private var allAccounts var body: some View { - VStack { + let welcomeScreen = VStack { Spacer() Text("Welcome") @@ -26,7 +26,7 @@ struct WelcomeScreen: View { AccountSelectionView(showHeader: false) Button { - dismiss() + presentationMode.wrappedValue.dismiss() } label: { Text("Start") } @@ -36,7 +36,7 @@ struct WelcomeScreen: View { #else AccountsMenuView() .onChange(of: accounts.current) { _ in - dismiss() + presentationMode.wrappedValue.dismiss() } #if os(macOS) .frame(maxWidth: 280) @@ -50,10 +50,16 @@ struct WelcomeScreen: View { Spacer() } - .interactiveDismissDisabled() #if os(macOS) - .frame(minWidth: 400, minHeight: 400) + .frame(minWidth: 400, minHeight: 400) #endif + + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { + welcomeScreen + .interactiveDismissDisabled() + } else { + welcomeScreen + } } } diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 07749377..f2048c5e 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -77,6 +77,8 @@ 371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; 371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; 371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; + 3722AEBC274DA396005EA4D6 /* Badge+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */; }; + 3722AEBE274DA401005EA4D6 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBD274DA401005EA4D6 /* Backport.swift */; }; 3729037E2739E47400EA99F6 /* MenuCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3729037D2739E47400EA99F6 /* MenuCommands.swift */; }; 3729037F2739E47400EA99F6 /* MenuCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3729037D2739E47400EA99F6 /* MenuCommands.swift */; }; 372915E42687E33E00F5A35B /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 372915E32687E33E00F5A35B /* Defaults */; }; @@ -110,6 +112,8 @@ 3743CA52270F284F00E4D32B /* View+Borders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA51270F284F00E4D32B /* View+Borders.swift */; }; 3743CA53270F284F00E4D32B /* View+Borders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA51270F284F00E4D32B /* View+Borders.swift */; }; 3743CA54270F284F00E4D32B /* View+Borders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA51270F284F00E4D32B /* View+Borders.swift */; }; + 374710052755291C00CE0F87 /* SearchField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374710042755291C00CE0F87 /* SearchField.swift */; }; + 374710062755291C00CE0F87 /* SearchField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374710042755291C00CE0F87 /* SearchField.swift */; }; 3748186626A7627F0084E870 /* Video+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186526A7627F0084E870 /* Video+Fixtures.swift */; }; 3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186526A7627F0084E870 /* Video+Fixtures.swift */; }; 3748186826A7627F0084E870 /* Video+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3748186526A7627F0084E870 /* Video+Fixtures.swift */; }; @@ -122,16 +126,14 @@ 37484C1926FC837400287258 /* PlaybackSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C1826FC837400287258 /* PlaybackSettings.swift */; }; 37484C1A26FC837400287258 /* PlaybackSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C1826FC837400287258 /* PlaybackSettings.swift */; }; 37484C1B26FC837400287258 /* PlaybackSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C1826FC837400287258 /* PlaybackSettings.swift */; }; - 37484C1D26FC83A400287258 /* InstancesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C1C26FC83A400287258 /* InstancesSettings.swift */; }; - 37484C1F26FC83A400287258 /* InstancesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C1C26FC83A400287258 /* InstancesSettings.swift */; }; 37484C2526FC83E000287258 /* InstanceForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2426FC83E000287258 /* InstanceForm.swift */; }; 37484C2626FC83E000287258 /* InstanceForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2426FC83E000287258 /* InstanceForm.swift */; }; 37484C2726FC83E000287258 /* InstanceForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2426FC83E000287258 /* InstanceForm.swift */; }; 37484C2926FC83FF00287258 /* AccountForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2826FC83FF00287258 /* AccountForm.swift */; }; 37484C2A26FC83FF00287258 /* AccountForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2826FC83FF00287258 /* AccountForm.swift */; }; 37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2826FC83FF00287258 /* AccountForm.swift */; }; - 37484C2D26FC844700287258 /* AccountsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* AccountsSettings.swift */; }; - 37484C2F26FC844700287258 /* AccountsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* AccountsSettings.swift */; }; + 37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* InstanceSettings.swift */; }; + 37484C2F26FC844700287258 /* InstanceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* InstanceSettings.swift */; }; 37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; }; 37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; }; 37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; }; @@ -164,9 +166,6 @@ 3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; }; 3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; }; 3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; }; - 3761AC0F26F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */; }; - 3761AC1026F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */; }; - 3761AC1126F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */; }; 3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; }; 3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; }; 376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; }; @@ -269,12 +268,29 @@ 377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; }; 377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7EC267A0A0800A6BBAF /* SwiftyJSON */; }; 377FC7F3267A0A0800A6BBAF /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 377FC7F2267A0A0800A6BBAF /* Logging */; }; + 3782B94F27553A6700990149 /* SearchSuggestions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3782B94E27553A6700990149 /* SearchSuggestions.swift */; }; + 3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3782B94E27553A6700990149 /* SearchSuggestions.swift */; }; + 3782B9522755667600990149 /* String+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3782B9512755667600990149 /* String+Format.swift */; }; + 3782B9532755667600990149 /* String+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3782B9512755667600990149 /* String+Format.swift */; }; + 3782B9542755667600990149 /* String+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3782B9512755667600990149 /* String+Format.swift */; }; + 3782B95627557E4E00990149 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF27F26737550007FC770 /* SearchView.swift */; }; + 3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3782B94E27553A6700990149 /* SearchSuggestions.swift */; }; + 3782B95E2755858100990149 /* NSTextField+FocusRingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3782B95C2755858100990149 /* NSTextField+FocusRingType.swift */; }; 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 /* 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 */; }; + 378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */; }; + 378AE93C274EDFB2006A4EE1 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBD274DA401005EA4D6 /* Backport.swift */; }; + 378AE93D274EDFB3006A4EE1 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBD274DA401005EA4D6 /* Backport.swift */; }; + 378AE93E274EDFB4006A4EE1 /* Tint+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBF274DAEB8005EA4D6 /* Tint+Backport.swift */; }; + 378AE93F274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBF274DAEB8005EA4D6 /* Tint+Backport.swift */; }; + 378AE940274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBF274DAEB8005EA4D6 /* Tint+Backport.swift */; }; + 378AE943274EF00A006A4EE1 /* Color+Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378AE942274EF00A006A4EE1 /* Color+Background.swift */; }; + 378AE944274EF00A006A4EE1 /* Color+Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378AE942274EF00A006A4EE1 /* Color+Background.swift */; }; + 378AE945274EF00A006A4EE1 /* Color+Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378AE942274EF00A006A4EE1 /* Color+Background.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 */; }; @@ -294,8 +310,6 @@ 37A3B15F27255E7F000FB5EE /* images in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B15E27255E7F000FB5EE /* images */; }; 37A3B16127255E7F000FB5EE /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16027255E7F000FB5EE /* manifest.json */; }; 37A3B16527255E7F000FB5EE /* content.js in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16427255E7F000FB5EE /* content.js */; }; - 37A3B17027255E7F000FB5EE /* Open in Yattee (macOS).appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 37A3B15727255E7F000FB5EE /* Open in Yattee (macOS).appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 37A3B18F2725735F000FB5EE /* Open in Yattee (iOS).appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 37A3B1792725735F000FB5EE /* Open in Yattee (iOS).appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 37A3B194272574FB000FB5EE /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A3B15927255E7F000FB5EE /* SafariWebExtensionHandler.swift */; }; 37A3B19627257503000FB5EE /* content.js in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16427255E7F000FB5EE /* content.js */; }; 37A3B1982725750B000FB5EE /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 37A3B16027255E7F000FB5EE /* manifest.json */; }; @@ -307,7 +321,6 @@ 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 */; }; 37AAF29126740715007FC770 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF28F26740715007FC770 /* Channel.swift */; }; 37AAF29226740715007FC770 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF28F26740715007FC770 /* Channel.swift */; }; @@ -452,6 +465,8 @@ 37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; }; 37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; }; 37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; }; + 37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; }; + 37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; }; 37E2EEAB270656EC00170416 /* PlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E2EEAA270656EC00170416 /* PlayerControlsView.swift */; }; 37E2EEAC270656EC00170416 /* PlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E2EEAA270656EC00170416 /* PlayerControlsView.swift */; }; 37E2EEAD270656EC00170416 /* PlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E2EEAA270656EC00170416 /* PlayerControlsView.swift */; }; @@ -510,20 +525,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 37A3B16E27255E7F000FB5EE /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 37D4B0BD2671614700C925CA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 37A3B15627255E7F000FB5EE; - remoteInfo = "Open in Yattee"; - }; - 37A3B18D2725735F000FB5EE /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 37D4B0BD2671614700C925CA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 37A3B1782725735F000FB5EE; - remoteInfo = "Open in Yattee"; - }; 37D4B0D52671614900C925CA /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 37D4B0BD2671614700C925CA /* Project object */; @@ -547,31 +548,6 @@ }; /* End PBXContainerItemProxy section */ -/* Begin PBXCopyFilesBuildPhase section */ - 37A3B17127255E7F000FB5EE /* Embed App Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 13; - files = ( - 37A3B17027255E7F000FB5EE /* Open in Yattee (macOS).appex in Embed App Extensions */, - ); - name = "Embed App Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; - 37A3B1932725735F000FB5EE /* Embed App Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 13; - files = ( - 37A3B18F2725735F000FB5EE /* Open in Yattee (iOS).appex in Embed App Extensions */, - ); - name = "Embed App Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ 3700155A271B0D4D0049C794 /* PipedAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PipedAPI.swift; sourceTree = ""; }; 3700155E271B12DD0049C794 /* SiestaConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiestaConfiguration.swift; sourceTree = ""; }; @@ -585,6 +561,9 @@ 37169AA12729D98A0011DE61 /* InstancesBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesBridge.swift; sourceTree = ""; }; 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsBridge.swift; sourceTree = ""; }; 371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = ""; }; + 3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Badge+Backport.swift"; sourceTree = ""; }; + 3722AEBD274DA401005EA4D6 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; + 3722AEBF274DAEB8005EA4D6 /* Tint+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tint+Backport.swift"; sourceTree = ""; }; 3729037D2739E47400EA99F6 /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = ""; }; 372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = ""; }; @@ -598,14 +577,14 @@ 3743B86727216D3600261544 /* ChannelCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelCell.swift; sourceTree = ""; }; 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueRow.swift; sourceTree = ""; }; 3743CA51270F284F00E4D32B /* View+Borders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Borders.swift"; sourceTree = ""; }; + 374710042755291C00CE0F87 /* SearchField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchField.swift; sourceTree = ""; }; 3748186526A7627F0084E870 /* Video+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Video+Fixtures.swift"; sourceTree = ""; }; 3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Thumbnail+Fixtures.swift"; sourceTree = ""; }; 3748186D26A769D60084E870 /* DetailBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailBadge.swift; sourceTree = ""; }; 37484C1826FC837400287258 /* PlaybackSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSettings.swift; sourceTree = ""; }; - 37484C1C26FC83A400287258 /* InstancesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesSettings.swift; sourceTree = ""; }; 37484C2426FC83E000287258 /* InstanceForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceForm.swift; sourceTree = ""; }; 37484C2826FC83FF00287258 /* AccountForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountForm.swift; sourceTree = ""; }; - 37484C2C26FC844700287258 /* AccountsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSettings.swift; sourceTree = ""; }; + 37484C2C26FC844700287258 /* InstanceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSettings.swift; sourceTree = ""; }; 37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = ""; }; 374C053427242D9F009BDDBE /* ServicesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesSettings.swift; sourceTree = ""; }; 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTVMenu.swift; sourceTree = ""; }; @@ -618,7 +597,6 @@ 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 = ""; }; 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecents.swift; sourceTree = ""; }; 376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = ""; }; 376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = ""; }; @@ -634,9 +612,13 @@ 37732FF32703D32400F04329 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = ""; }; 3774122927387B6C00423605 /* InstancesModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModelTests.swift; sourceTree = ""; }; 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = ""; }; + 3782B94E27553A6700990149 /* SearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSuggestions.swift; sourceTree = ""; }; + 3782B9512755667600990149 /* String+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Format.swift"; sourceTree = ""; }; + 3782B95C2755858100990149 /* NSTextField+FocusRingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextField+FocusRingType.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 /* FavoriteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItemView.swift; sourceTree = ""; }; + 378AE942274EF00A006A4EE1 /* Color+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Background.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 = ""; }; @@ -710,7 +692,7 @@ 37D4B0D82671614900C925CA /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = ""; }; 37D4B0DE2671614900C925CA /* Tests (macOS).xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests (macOS).xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 37D4B0E22671614900C925CA /* Tests_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOS.swift; sourceTree = ""; }; - 37D4B158267164AE00C925CA /* Yattee (tvOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Yattee (tvOS).app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 37D4B158267164AE00C925CA /* Yattee.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Yattee.app; sourceTree = BUILT_PRODUCTS_DIR; }; 37D4B15E267164AF00C925CA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37D4B171267164B000C925CA /* Tests (tvOS).xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests (tvOS).xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 37D4B175267164B000C925CA /* YatteeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YatteeUITests.swift; sourceTree = ""; }; @@ -721,6 +703,7 @@ 37D526E22720B4BE00ED2F5E /* View+SwipeGesture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+SwipeGesture.swift"; sourceTree = ""; }; 37D9169A27388A81002B1BAA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = ""; }; + 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.swift; sourceTree = ""; }; 37E2EEAA270656EC00170416 /* PlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = ""; }; 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsModel.swift; sourceTree = ""; }; 37E70922271CD43000D34DDE /* WelcomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreen.swift; sourceTree = ""; }; @@ -909,7 +892,6 @@ 37E2EEAA270656EC00170416 /* PlayerControlsView.swift */, 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */, 37AAF27D26737323007FC770 /* PopularView.swift */, - 37AAF27F26737550007FC770 /* SearchView.swift */, 3784B23C2728B85300B09468 /* ShareButton.swift */, 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */, 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */, @@ -919,6 +901,16 @@ path = Views; sourceTree = ""; }; + 3722AEBA274DA312005EA4D6 /* Backports */ = { + isa = PBXGroup; + children = ( + 3722AEBD274DA401005EA4D6 /* Backport.swift */, + 3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */, + 3722AEBF274DAEB8005EA4D6 /* Tint+Backport.swift */, + ); + path = Backports; + sourceTree = ""; + }; 3743B864272169E200261544 /* Applications */ = { isa = PBXGroup; children = ( @@ -976,11 +968,11 @@ isa = PBXGroup; children = ( 37484C2826FC83FF00287258 /* AccountForm.swift */, - 37484C2C26FC844700287258 /* AccountsSettings.swift */, + 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */, 37732FEF2703A26300F04329 /* AccountValidationStatus.swift */, 376BE50A27349108009AD608 /* BrowsingSettings.swift */, 37484C2426FC83E000287258 /* InstanceForm.swift */, - 37484C1C26FC83A400287258 /* InstancesSettings.swift */, + 37484C2C26FC844700287258 /* InstanceSettings.swift */, 37484C1826FC837400287258 /* PlaybackSettings.swift */, 374C053427242D9F009BDDBE /* ServicesSettings.swift */, 376BE50627347B57009AD608 /* SettingsHeader.swift */, @@ -1002,7 +994,6 @@ isa = PBXGroup; children = ( 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */, - 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */, ); path = Modifiers; sourceTree = ""; @@ -1014,6 +1005,16 @@ name = Frameworks; sourceTree = ""; }; + 3782B95527557A2400990149 /* Search */ = { + isa = PBXGroup; + children = ( + 374710042755291C00CE0F87 /* SearchField.swift */, + 3782B94E27553A6700990149 /* SearchSuggestions.swift */, + 37AAF27F26737550007FC770 /* SearchView.swift */, + ); + path = Search; + sourceTree = ""; + }; 3788AC2126F683AB00F6BAA9 /* Favorites */ = { isa = PBXGroup; children = ( @@ -1067,8 +1068,8 @@ 37BE0BD826A214500092E2DB /* macOS */ = { isa = PBXGroup; children = ( - 37FD43E1270472060073EE42 /* Settings */, 374C0542272496E4009BDDBE /* AppDelegate.swift */, + 37FD43DB270470B70073EE42 /* InstancesSettings.swift */, 374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */, 37BE0BDB26A2367F0092E2DB /* Player.swift */, 37BE0BD926A214630092E2DB /* PlayerViewController.swift */, @@ -1083,8 +1084,11 @@ 379775922689365600DD52A8 /* Array+Next.swift */, 376578842685429C00D4EA09 /* CaseIterable+Next.swift */, 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */, + 378AE942274EF00A006A4EE1 /* Color+Background.swift */, 37C3A240272359900087A57A /* Double+Format.swift */, 37BA794E26DC3E0E002A0235 /* Int+Format.swift */, + 3782B95C2755858100990149 /* NSTextField+FocusRingType.swift */, + 3782B9512755667600990149 /* String+Format.swift */, 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */, 3743CA51270F284F00E4D32B /* View+Borders.swift */, ); @@ -1098,6 +1102,7 @@ 37BE0BD826A214500092E2DB /* macOS */, 37D4B159267164AE00C925CA /* tvOS */, 37D4B0C12671614700C925CA /* Shared */, + 3722AEBA274DA312005EA4D6 /* Backports */, 37D4B1B72672CFE300C925CA /* Model */, 37C7A9022679058300E721B4 /* Extensions */, 3748186426A762300084E870 /* Fixtures */, @@ -1120,6 +1125,7 @@ 371AAE2326CEB9E800901972 /* Navigation */, 371AAE2426CEBA4100901972 /* Player */, 371AAE2626CEBF1600901972 /* Playlists */, + 3782B95527557A2400990149 /* Search */, 37484C1726FC836500287258 /* Settings */, 371AAE2526CEBF0B00901972 /* Trending */, 371AAE2726CEBF4700901972 /* Videos */, @@ -1145,7 +1151,7 @@ 37D4B0CF2671614900C925CA /* Yattee.app */, 37D4B0D42671614900C925CA /* Tests (iOS).xctest */, 37D4B0DE2671614900C925CA /* Tests (macOS).xctest */, - 37D4B158267164AE00C925CA /* Yattee (tvOS).app */, + 37D4B158267164AE00C925CA /* Yattee.app */, 37D4B171267164B000C925CA /* Tests (tvOS).xctest */, 37A3B15727255E7F000FB5EE /* Open in Yattee (macOS).appex */, 37A3B1792725735F000FB5EE /* Open in Yattee (iOS).appex */, @@ -1242,14 +1248,6 @@ path = Search; sourceTree = ""; }; - 37FD43E1270472060073EE42 /* Settings */ = { - isa = PBXGroup; - children = ( - 37FD43DB270470B70073EE42 /* InstancesSettings.swift */, - ); - path = Settings; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1295,12 +1293,10 @@ 37D4B0C52671614900C925CA /* Sources */, 37D4B0C62671614900C925CA /* Frameworks */, 37D4B0C72671614900C925CA /* Resources */, - 37A3B1932725735F000FB5EE /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( - 37A3B18E2725735F000FB5EE /* PBXTargetDependency */, ); name = "Yattee (iOS)"; packageProductDependencies = ( @@ -1327,12 +1323,10 @@ 37D4B0CB2671614900C925CA /* Sources */, 37D4B0CC2671614900C925CA /* Frameworks */, 37D4B0CD2671614900C925CA /* Resources */, - 37A3B17127255E7F000FB5EE /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( - 37A3B16F27255E7F000FB5EE /* PBXTargetDependency */, ); name = "Yattee (macOS)"; packageProductDependencies = ( @@ -1419,7 +1413,7 @@ 3765917D27237D2A009F956E /* PINCache */, ); productName = Yattee; - productReference = 37D4B158267164AE00C925CA /* Yattee (tvOS).app */; + productReference = 37D4B158267164AE00C925CA /* Yattee.app */; productType = "com.apple.product-type.application"; }; 37D4B170267164B000C925CA /* Tests (tvOS) */ = { @@ -1723,6 +1717,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 374710052755291C00CE0F87 /* SearchField.swift in Sources */, 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */, 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, @@ -1737,6 +1732,7 @@ 37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */, 37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */, 3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, + 3782B94F27553A6700990149 /* SearchSuggestions.swift in Sources */, 378E50FF26FE8EEE00F49626 /* AccountsMenuView.swift in Sources */, 37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */, 37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */, @@ -1756,8 +1752,10 @@ 37BE0BD326A1D4780092E2DB /* Player.swift in Sources */, 37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */, 37CEE4C12677B697005A1EFE /* Stream.swift in Sources */, + 378AE943274EF00A006A4EE1 /* Color+Background.swift in Sources */, 37F4AE7226828F0900BD60EA /* VerticalCells.swift in Sources */, 376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, + 3722AEBC274DA396005EA4D6 /* Badge+Backport.swift in Sources */, 3748186626A7627F0084E870 /* Video+Fixtures.swift in Sources */, 37599F34272B44000087F250 /* FavoritesModel.swift in Sources */, 37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */, @@ -1765,7 +1763,9 @@ 37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */, 37FFC440272734C3009FFD26 /* Throttle.swift in Sources */, 3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */, + 378AE940274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */, 376BE50927347B5F009AD608 /* SettingsHeader.swift in Sources */, + 3722AEBE274DA401005EA4D6 /* Backport.swift in Sources */, 3700155F271B12DD0049C794 /* SiestaConfiguration.swift in Sources */, 37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */, 375168D62700FAFF008F96A6 /* Debounce.swift in Sources */, @@ -1773,6 +1773,7 @@ 376578892685471400D4EA09 /* Playlist.swift in Sources */, 373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */, 3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */, + 3782B9522755667600990149 /* String+Format.swift in Sources */, 373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 37BF661F27308884008CCFB0 /* DropFavoriteOutside.swift in Sources */, 3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */, @@ -1786,7 +1787,6 @@ 37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */, 37AAF29026740715007FC770 /* Channel.swift in Sources */, 3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */, - 3761AC0F26F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */, 37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */, 377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */, 376578912685490700D4EA09 /* PlaylistsView.swift in Sources */, @@ -1795,7 +1795,7 @@ 374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, 37FB28412721B22200A57617 /* ContentItem.swift in Sources */, 37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, - 37484C2D26FC844700287258 /* AccountsSettings.swift in Sources */, + 37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */, 377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */, 37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */, 37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */, @@ -1830,6 +1830,7 @@ 373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, 37B81B0226D2CAE700675966 /* PlaybackBar.swift in Sources */, 372915E62687E3B900F5A35B /* Defaults.swift in Sources */, + 37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */, 37D526E32720B4BE00ED2F5E /* View+SwipeGesture.swift in Sources */, 37732FF42703D32400F04329 /* Sidebar.swift in Sources */, 37D4B19726717E1500C925CA /* Video.swift in Sources */, @@ -1842,7 +1843,6 @@ 37CC3F50270D010D00608308 /* VideoBanner.swift in Sources */, 378E50FB26FE8B9F00F49626 /* Instance.swift in Sources */, 37E70923271CD43000D34DDE /* WelcomeScreen.swift in Sources */, - 37484C1D26FC83A400287258 /* InstancesSettings.swift in Sources */, 37BD07BB2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */, 37C0697A2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */, 37D4B0E42671614900C925CA /* YatteeApp.swift in Sources */, @@ -1857,6 +1857,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 374710062755291C00CE0F87 /* SearchField.swift in Sources */, + 378AE93F274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */, 37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */, 37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */, 3743CA53270F284F00E4D32B /* View+Borders.swift in Sources */, @@ -1870,6 +1872,7 @@ 37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 37C3A24E272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37CEE4C22677B697005A1EFE /* Stream.swift in Sources */, + 3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */, 371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */, 37001564271B1F250049C794 /* AccountsModel.swift in Sources */, 3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, @@ -1889,9 +1892,11 @@ 37484C1A26FC837400287258 /* PlaybackSettings.swift in Sources */, 37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */, 37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */, + 378AE944274EF00A006A4EE1 /* Color+Background.swift in Sources */, 37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 37EAD870267B9ED100D9E01B /* Segment.swift in Sources */, 3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, + 378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */, 37599F35272B44000087F250 /* FavoritesModel.swift in Sources */, 37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 37FFC441272734C3009FFD26 /* Throttle.swift in Sources */, @@ -1900,6 +1905,7 @@ 378E510026FE8EEE00F49626 /* AccountsMenuView.swift in Sources */, 37141670267A8ACC006CA35D /* TrendingView.swift in Sources */, 376BE50727347B57009AD608 /* SettingsHeader.swift in Sources */, + 378AE93C274EDFB2006A4EE1 /* Backport.swift in Sources */, 37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */, 377FC7E2267A084A00A6BBAF /* VideoCell.swift in Sources */, 37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */, @@ -1907,6 +1913,7 @@ 37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */, 3765788A2685471400D4EA09 /* Playlist.swift in Sources */, + 3782B9532755667600990149 /* String+Format.swift in Sources */, 37E2EEAC270656EC00170416 /* PlayerControlsView.swift in Sources */, 37BF662027308884008CCFB0 /* DropFavoriteOutside.swift in Sources */, 37E70924271CD43000D34DDE /* WelcomeScreen.swift in Sources */, @@ -1935,7 +1942,6 @@ 37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */, 37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, 37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, - 3761AC1026F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */, 37732FF52703D32400F04329 /* Sidebar.swift in Sources */, 379775942689365600DD52A8 /* Array+Next.swift in Sources */, 3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */, @@ -1976,6 +1982,7 @@ 37B17DA1268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */, 3743B86927216D3600261544 /* ChannelCell.swift in Sources */, 3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */, + 3782B95E2755858100990149 /* NSTextField+FocusRingType.swift in Sources */, 37C3A252272366440087A57A /* ChannelPlaylistView.swift in Sources */, 373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */, 37C0697B2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */, @@ -2055,7 +2062,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 37AAF28026737550007FC770 /* SearchView.swift in Sources */, 37EAD871267B9ED100D9E01B /* Segment.swift in Sources */, 37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */, 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, @@ -2071,6 +2077,7 @@ 37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */, 3748187026A769D60084E870 /* DetailBadge.swift in Sources */, 37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, + 3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */, 37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */, 376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */, 37141671267A8ACC006CA35D /* TrendingView.swift in Sources */, @@ -2080,10 +2087,12 @@ 37E70925271CD43000D34DDE /* WelcomeScreen.swift in Sources */, 376BE50D27349108009AD608 /* BrowsingSettings.swift in Sources */, 37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, + 378AE93E274EDFB4006A4EE1 /* Tint+Backport.swift in Sources */, 37FFC442272734C3009FFD26 /* Throttle.swift in Sources */, 375168D82700FDB9008F96A6 /* Debounce.swift in Sources */, 37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 37C0697C2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */, + 378AE93D274EDFB3006A4EE1 /* Backport.swift in Sources */, 37C3A243272359900087A57A /* Double+Format.swift in Sources */, 37AAF29226740715007FC770 /* Channel.swift in Sources */, 37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, @@ -2098,7 +2107,6 @@ 3743B86A27216D3600261544 /* ChannelCell.swift in Sources */, 37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */, 373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, - 3761AC1126F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */, 3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */, 37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */, @@ -2112,7 +2120,6 @@ 37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */, 376A33E62720CB35000C1D6B /* Account.swift in Sources */, 37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */, - 37484C1F26FC83A400287258 /* InstancesSettings.swift in Sources */, 37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */, 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */, 376578932685490700D4EA09 /* PlaylistsView.swift in Sources */, @@ -2123,6 +2130,7 @@ 377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */, 3748186826A7627F0084E870 /* Video+Fixtures.swift in Sources */, 37C3A253272366440087A57A /* ChannelPlaylistView.swift in Sources */, + 378AE945274EF00A006A4EE1 /* Color+Background.swift in Sources */, 3743CA54270F284F00E4D32B /* View+Borders.swift in Sources */, 371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */, 37E2EEAD270656EC00170416 /* PlayerControlsView.swift in Sources */, @@ -2148,6 +2156,7 @@ 37FAE000272ED58000330459 /* EditFavorites.swift in Sources */, 37599F32272B42810087F250 /* FavoriteItem.swift in Sources */, 37141675267A8E10006CA35D /* Country.swift in Sources */, + 3782B9542755667600990149 /* String+Format.swift in Sources */, 37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */, 37484C2726FC83E000287258 /* InstanceForm.swift in Sources */, 37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */, @@ -2158,6 +2167,8 @@ 37D526E02720AC4400ED2F5E /* VideosAPI.swift in Sources */, 37599F36272B44000087F250 /* FavoritesModel.swift in Sources */, 3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */, + 37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */, + 3782B95627557E4E00990149 /* SearchView.swift in Sources */, 3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, 37C3A24F272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37FB28432721B22200A57617 /* ContentItem.swift in Sources */, @@ -2166,7 +2177,7 @@ 372915E82687E3B900F5A35B /* Defaults.swift in Sources */, 37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */, 3797758D2689345500DD52A8 /* Store.swift in Sources */, - 37484C2F26FC844700287258 /* AccountsSettings.swift in Sources */, + 37484C2F26FC844700287258 /* InstanceSettings.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2181,16 +2192,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 37A3B16F27255E7F000FB5EE /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 37A3B15627255E7F000FB5EE /* Open in Yattee (macOS) */; - targetProxy = 37A3B16E27255E7F000FB5EE /* PBXContainerItemProxy */; - }; - 37A3B18E2725735F000FB5EE /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 37A3B1782725735F000FB5EE /* Open in Yattee (iOS) */; - targetProxy = 37A3B18D2725735F000FB5EE /* PBXContainerItemProxy */; - }; 37D4B0D62671614900C925CA /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 37D4B0C82671614900C925CA /* Yattee (iOS) */; @@ -2216,7 +2217,7 @@ CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -2229,7 +2230,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.2; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -2250,7 +2251,7 @@ CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -2263,7 +2264,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.2; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -2282,7 +2283,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Open in Yattee/Info.plist"; @@ -2294,7 +2295,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.2; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -2314,7 +2315,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Open in Yattee/Info.plist"; @@ -2326,7 +2327,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.2; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -2474,11 +2475,10 @@ 37D4B0ED2671614900C925CA /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -2488,12 +2488,12 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_NAME = Yattee; SDKROOT = iphoneos; @@ -2506,11 +2506,10 @@ 37D4B0EE2671614900C925CA /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -2520,12 +2519,12 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_NAME = Yattee; SDKROOT = iphoneos; @@ -2539,14 +2538,13 @@ 37D4B0F02671614900C925CA /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Shared/Yattee.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -2556,13 +2554,12 @@ INFOPLIST_FILE = macOS/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - INFOPLIST_KEY_NSMainStoryboardFile = Main; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.1; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_NAME = Yattee; SDKROOT = macosx; @@ -2574,14 +2571,13 @@ 37D4B0F12671614900C925CA /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Shared/Yattee.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -2591,13 +2587,12 @@ INFOPLIST_FILE = macOS/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - INFOPLIST_KEY_NSMainStoryboardFile = Main; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.1; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_NAME = Yattee; SDKROOT = macosx; @@ -2713,14 +2708,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = tvOS/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Yattee; - INFOPLIST_KEY_CFBundleExecutable = "Yattee (Apple TV)"; INFOPLIST_KEY_CFBundleName = "Yattee (Apple TV)"; INFOPLIST_KEY_CFBundleVersion = 1; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -2729,9 +2723,9 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = Yattee; SDKROOT = appletvos; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -2746,14 +2740,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = tvOS/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Yattee; - INFOPLIST_KEY_CFBundleExecutable = "Yattee (Apple TV)"; INFOPLIST_KEY_CFBundleName = "Yattee (Apple TV)"; INFOPLIST_KEY_CFBundleVersion = 1; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -2762,9 +2755,9 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = Yattee; SDKROOT = appletvos; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -2980,7 +2973,7 @@ repositoryURL = "https://github.com/sindresorhus/Defaults"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 5.0.0; + minimumVersion = 6.0.0; }; }; 3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */ = { diff --git a/Yattee.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Yattee.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3f098788..6d2dbedf 100644 --- a/Yattee.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Yattee.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/sindresorhus/Defaults", "state": { "branch": null, - "revision": "63d93f97ad545c8bceb125a8a36175ea705f7cf5", - "version": "5.0.0" + "revision": "55f3302c3ab30a8760f10042d0ebc0a6907f865a", + "version": "6.1.0" } }, { @@ -33,7 +33,7 @@ "repositoryURL": "https://github.com/pinterest/PINCache", "state": { "branch": "master", - "revision": "a16dae6cec4897a7a9710239e0cb50b6de935e7b", + "revision": "046f67609085a7d73d27105d2be91729d139208f", "version": null } }, diff --git a/Yattee.xcodeproj/xcshareddata/xcschemes/Yattee (tvOS).xcscheme b/Yattee.xcodeproj/xcshareddata/xcschemes/Yattee (tvOS).xcscheme index 178b43d0..a6d0ba20 100644 --- a/Yattee.xcodeproj/xcshareddata/xcschemes/Yattee (tvOS).xcscheme +++ b/Yattee.xcodeproj/xcshareddata/xcschemes/Yattee (tvOS).xcscheme @@ -15,7 +15,7 @@ @@ -65,7 +65,7 @@ @@ -82,7 +82,7 @@ diff --git a/macOS/Settings/InstancesSettings.swift b/macOS/InstancesSettings.swift similarity index 67% rename from macOS/Settings/InstancesSettings.swift rename to macOS/InstancesSettings.swift index c5cb3bb3..81aaee6e 100644 --- a/macOS/Settings/InstancesSettings.swift +++ b/macOS/InstancesSettings.swift @@ -40,7 +40,7 @@ struct InstancesSettings: View { if !selectedInstance.isNil, selectedInstance.app.supportsAccounts { SettingsHeader(text: "Accounts") - List(selection: $selectedAccount) { + let list = List(selection: $selectedAccount) { if selectedInstanceAccounts.isEmpty { Text("You have no accounts for this instance") .foregroundColor(.secondary) @@ -51,7 +51,7 @@ struct InstancesSettings: View { Spacer() - Button("Remove", role: .destructive) { + Button("Remove") { presentingAccountRemovalConfirmation = true } .foregroundColor(.red) @@ -60,30 +60,40 @@ struct InstancesSettings: View { .tag(account) } } - .confirmationDialog( - "Are you sure you want to remove \(selectedAccount?.description ?? "") account?", - isPresented: $presentingAccountRemovalConfirmation - ) { - Button("Remove", role: .destructive) { - AccountsModel.remove(selectedAccount!) - } + .alert(isPresented: $presentingAccountRemovalConfirmation) { + Alert( + title: Text( + "Are you sure you want to remove \(selectedAccount?.description ?? "") account?" + ), + message: Text("This cannot be undone"), + primaryButton: .destructive(Text("Delete")) { + AccountsModel.remove(selectedAccount!) + }, + secondaryButton: .cancel() + ) + } + + if #available(macOS 12.0, *) { + list + .listStyle(.inset(alternatesRowBackgrounds: true)) + } else { + list } - .listStyle(.inset(alternatesRowBackgrounds: true)) } if selectedInstance != nil, selectedInstance.app.hasFrontendURL { SettingsHeader(text: "Frontend URL") - TextField("Frontend URL", text: $frontendURL, prompt: Text("Frontend URL")) - .onAppear { - frontendURL = selectedInstance.frontendURL ?? "" + TextField("Frontend URL", text: $frontendURL) + .onChange(of: selectedInstance) { _ in + frontendURL = selectedInstanceFrontendURL } .onChange(of: frontendURL) { newValue in InstancesModel.setFrontendURL(selectedInstance, newValue) } .labelsHidden() - Text("If provided, you can copy links from videos, channels and playlist") + Text("Used to create links from videos, channels and playlist") .font(.caption) .foregroundColor(.secondary) } @@ -105,23 +115,26 @@ struct InstancesSettings: View { Spacer() - Button("Remove Instance", role: .destructive) { + Button("Remove Instance") { presentingInstanceRemovalConfirmation = true } - .confirmationDialog( - "Are you sure you want to remove \(selectedInstance!.longDescription) instance?", - isPresented: $presentingInstanceRemovalConfirmation - ) { - Button("Remove Instance", role: .destructive) { - if accounts.current?.instance == selectedInstance { - accounts.setCurrent(nil) - } + .alert(isPresented: $presentingInstanceRemovalConfirmation) { + Alert( + title: Text( + "Are you sure you want to remove \(selectedInstance!.longDescription) instance?" + ), + message: Text("This cannot be undone"), + primaryButton: .destructive(Text("Remove")) { + if accounts.current?.instance == selectedInstance { + accounts.setCurrent(nil) + } - InstancesModel.remove(selectedInstance!) - selectedInstanceID = instances.last?.id - } + InstancesModel.remove(selectedInstance!) + selectedInstanceID = instances.last?.id + }, + secondaryButton: .cancel() + ) } - .foregroundColor(.red) } } @@ -134,6 +147,7 @@ struct InstancesSettings: View { .onAppear { selectedInstanceID = instances.first?.id + frontendURL = selectedInstanceFrontendURL } .sheet(isPresented: $presentingAccountForm) { AccountForm(instance: selectedInstance, selectedAccount: $selectedAccount) @@ -154,6 +168,10 @@ struct InstancesSettings: View { InstancesModel.find(selectedInstanceID) } + var selectedInstanceFrontendURL: String { + selectedInstance?.frontendURL ?? "" + } + private var selectedInstanceAccounts: [Account] { guard selectedInstance != nil else { return [] diff --git a/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json index b65f0cdd..cb047adc 100644 --- a/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json +++ b/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -1,10 +1,12 @@ { "images" : [ { + "filename" : "TopShelf-Wide.png", "idiom" : "tv", "scale" : "1x" }, { + "filename" : "TopShelf-Wide@2x.png", "idiom" : "tv", "scale" : "2x" }, diff --git a/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/TopShelf-Wide.png b/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/TopShelf-Wide.png new file mode 100644 index 0000000000000000000000000000000000000000..bb6d72c8a5b7032bf85a231303742067da030d38 GIT binary patch literal 25177 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 literal 0 HcmV?d00001 diff --git a/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/TopShelf-Wide@2x.png b/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/TopShelf-Wide@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..35e893af8d7d74a5dd9d61c459909b1d99a2a3a5 GIT binary patch literal 62288 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 Lf6CUj|LuPORnEiZ literal 0 HcmV?d00001