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 00000000..bb6d72c8 Binary files /dev/null and b/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/TopShelf-Wide.png differ 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 00000000..35e893af Binary files /dev/null and b/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/TopShelf-Wide@2x.png differ