iOS 14/macOS Big Sur Support

This commit is contained in:
Arkadiusz Fal 2021-11-28 15:37:55 +01:00
parent 696751e07c
commit 5ef89ac9f4
57 changed files with 1147 additions and 813 deletions

13
Backports/Backport.swift Normal file
View File

@ -0,0 +1,13 @@
import SwiftUI
public struct Backport<Content> {
public let content: Content
public init(_ content: Content) {
self.content = content
}
}
extension View {
var backport: Backport<Self> { Backport(self) }
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -0,0 +1,8 @@
import AppKit
extension NSTextField {
override open var focusRingType: NSFocusRingType {
get { .none }
set {} // swiftlint:disable:this unused_setter_value
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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"]

View File

@ -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,36 +227,37 @@ 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)
) {
loadedCompositionAssets = []
loadCompositionAsset(stream.audioAsset, stream: stream, type: .audio, of: video, preservingTime: preservingTime)
loadCompositionAsset(stream.videoAsset, stream: stream, type: .video, of: video, preservingTime: preservingTime)
}
guard streamSelection == stream else {
logger.critical("IGNORING LOADED")
func loadCompositionAsset(
_ asset: AVURLAsset,
stream: Stream,
type: AVMediaType,
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")
insertPlayerItem(stream, for: video, preservingTime: preservingTime)
}
let assetTracks = asset.tracks(withMediaType: type)
private func loadCompositionAsset(
_ asset: AVURLAsset,
type: AVMediaType,
of video: Video
) async {
async let assetTracks = asset.loadTracks(withMediaType: type)
logger.info("loading \(type.rawValue) track")
guard let compositionTrack = composition.addMutableTrack(
guard let compositionTrack = self.composition.addMutableTrack(
withMediaType: type,
preferredTrackID: kCMPersistentTrackID_Invalid
) else {
logger.critical("composition \(type.rawValue) addMutableTrack FAILED")
self.logger.critical("composition \(type.rawValue) addMutableTrack FAILED")
return
}
guard let assetTrack = try? await assetTracks.first else {
logger.critical("asset \(type.rawValue) track FAILED")
guard let assetTrack = assetTracks.first else {
self.logger.critical("asset \(type.rawValue) track FAILED")
return
}
@ -267,7 +267,19 @@ final class PlayerModel: ObservableObject {
at: .zero
)
logger.critical("\(type.rawValue) LOADED")
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)
}
}
}
private func playerItem(_ stream: Stream) -> AVPlayerItem? {

View File

@ -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

View File

@ -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.

View File

@ -51,7 +51,7 @@ struct FavoritesView: View {
.navigationTitle("Favorites")
#endif
#if os(macOS)
.background()
.background(Color.tertiaryBackground)
.frame(minWidth: 360)
#endif
}

View File

@ -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") {

View File

@ -1,26 +0,0 @@
import Foundation
import SwiftUI
struct UnsubscribeAlertModifier: ViewModifier {
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<SubscriptionsModel> 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"
}
}

View File

@ -15,13 +15,25 @@ struct AccountsMenuView: View {
}
}
} label: {
Label(model.current?.description ?? "Select Account", systemImage: "person.crop.circle")
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)
}

View File

@ -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)

View File

@ -18,7 +18,6 @@ struct AppSidebarSubscriptions: View {
navigation.presentUnsubscribeAlert(channel)
}
}
.modifier(UnsubscribeAlertModifier())
.id("channel\(channel.id)")
}
}

View File

@ -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)

View File

@ -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"))
}

View File

@ -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<PlayerModel> 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 {

View File

@ -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,15 +65,19 @@ 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) {
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,27 +106,43 @@ 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")
}
}
}
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")
}
}
}
private func removeAllButtonAction(history: Bool) {
_ = history ? player.removeHistoryItems() : player.removeQueueItems()
}
}

View File

@ -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<AccountsModel> 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") {
.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()
}
}
},
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,8 +354,14 @@ struct VideoDetails: View {
VStack(alignment: .leading, spacing: 10) {
if let description = video.description {
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)

View File

@ -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

View File

@ -7,7 +7,7 @@ struct AddToPlaylistView: View {
@State private var selectedPlaylistID: Playlist.ID = ""
@Environment(\.dismiss) private var dismiss
@Environment(\.presentationMode) private var presentationMode
@EnvironmentObject<PlaylistsModel> 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()
}
}

View File

@ -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<AccountsModel> private var accounts
@EnvironmentObject<PlaylistsModel> private var playlists
@ -22,6 +20,7 @@ struct PlaylistFormView: View {
}
var body: some View {
Group {
#if os(macOS) || os(iOS)
VStack(alignment: .leading) {
HStack(alignment: .center) {
@ -31,7 +30,7 @@ struct PlaylistFormView: View {
Spacer()
Button("Cancel") {
dismiss()
presentationMode.wrappedValue.dismiss()
}.keyboardShortcut(.cancelAction)
}
.padding(.horizontal)
@ -40,7 +39,6 @@ struct PlaylistFormView: View {
TextField("Name", text: $name, onCommit: validate)
.frame(maxWidth: 450)
.padding(.leading, 10)
.focused($focused)
visibilityFormItem
.pickerStyle(.segmented)
@ -63,8 +61,7 @@ struct PlaylistFormView: View {
.frame(minHeight: 35)
.padding(.horizontal)
}
.onChange(of: name) { _ in validate() }
.onAppear(perform: initializeForm)
#if os(iOS)
.padding(.vertical)
#else
@ -79,21 +76,13 @@ struct PlaylistFormView: View {
}
.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)
.background(Color.tertiaryBackground)
#endif
}
.onChange(of: name) { _ in validate() }
.onAppear(perform: initializeForm)
}
#if os(tvOS)
var header: some View {
@ -152,17 +141,17 @@ struct PlaylistFormView: View {
#endif
func initializeForm() {
focused = true
guard editing else {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
name = playlist.title
visibility = playlist.visibility
validate()
}
}
func validate() {
valid = !name.isEmpty
@ -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()
}
}
}

View File

@ -71,17 +71,20 @@ struct PlaylistsView: View {
ToolbarItemGroup {
#if !os(iOS)
if !model.isEmpty {
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
}

View File

@ -0,0 +1,80 @@
import SwiftUI
struct SearchTextField: View {
@Environment(\.navigationStyle) private var navigationStyle
@EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> 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)
}
}

View File

@ -0,0 +1,77 @@
import SwiftUI
struct SearchSuggestions: View {
@EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> 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()
}
}

View File

@ -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 {
#if os(iOS)
VStack {
if showRecentQueries {
recentQueries
SearchTextField()
if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty {
SearchSuggestions()
} 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))
results
}
}
HorizontalCells(items: items)
}
.edgesIgnoringSafeArea(.horizontal)
#else
VerticalCells(items: items)
#endif
if noResults {
Text("No results")
if searchFiltersActive {
Button("Reset search filters", action: resetFilters)
}
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) {
private func deleteButton(_ item: RecentItem) -> some View {
Button {
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) {
private var deleteAllButton: some View {
Button {
recents.clearQueries()
}
recentsChanged.toggle()
} label: {
Label("Delete All", systemImage: "trash.fill")
}
}
var searchFiltersActive: Bool {
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

View File

@ -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 {

View File

@ -0,0 +1,31 @@
import SwiftUI
struct AccountsNavigationLink: View {
@EnvironmentObject<AccountsModel> 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)
}
}

View File

@ -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<AccountsModel> private var model
@EnvironmentObject<InstancesModel> 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()
}
}

View File

@ -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()
}
}

View File

@ -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<AccountsModel> private var model
@EnvironmentObject<InstancesModel> 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()
}
}

View File

@ -1,70 +0,0 @@
import Defaults
import SwiftUI
struct InstancesSettings: View {
@Default(.instances) private var instances
@EnvironmentObject<AccountsModel> 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)
}
}

View File

@ -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)
}
}
Group {
if #available(macOS 12.0, *) {
list
.listStyle(.inset(alternatesRowBackgrounds: true))
} else {
list
.listStyle(.inset)
}
}
Spacer()
#else
ForEach(SponsorBlockAPI.categories, id: \.self) { category in

View File

@ -10,11 +10,16 @@ struct SettingsView: View {
#endif
#if os(iOS)
@Environment(\.dismiss) private var dismiss
@Environment(\.presentationMode) private var presentationMode
#endif
@EnvironmentObject<AccountsModel> 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 {

View File

@ -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 {
if #available(iOS 15.0, macOS 12.0, *) {
TextField("Country", text: $query, prompt: Text(TrendingCountry.prompt))
.focused($countryIsFocused)
} else {
TextField(TrendingCountry.prompt, text: $query)
}
Button("Done") { selectCountryAndDismiss() }
.keyboardShortcut(.defaultAction)
.keyboardShortcut(.cancelAction)
}
.padding([.horizontal, .top])
#endif
countriesList
#else
NavigationView {
countriesList
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) {
Button("Done") { selectCountryAndDismiss() }
}
}
#if os(iOS)
.navigationBarTitle("Trending Country", displayMode: .automatic)
#endif
}
#endif
}
.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 {
}
}
return Group {
#if os(macOS)
if #available(macOS 12.0, *) {
list
.listStyle(.inset(alternatesRowBackgrounds: true))
.padding(.bottom, 5)
#endif
} else {
list
}
#if !os(macOS)
var searchPlacement: SearchFieldPlacement {
#if os(iOS)
.navigationBarDrawer(displayMode: .always)
#else
.automatic
list
#endif
}
#if os(macOS)
.padding(.bottom, 5)
#endif
}
func selectCountryAndDismiss(_ country: Country? = nil) {
if let selected = country ?? selection {
selectedCountry = selected
}
dismiss()
presentationMode.wrappedValue.dismiss()
}
}

View File

@ -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
}

View File

@ -19,7 +19,7 @@ struct VerticalCells: View {
}
.edgesIgnoringSafeArea(.horizontal)
#if os(macOS)
.background()
.background(Color.tertiaryBackground)
.frame(minWidth: 360)
#endif
}

View File

@ -12,6 +12,7 @@ struct VideoCell: View {
@Environment(\.horizontalCells) private var horizontalCells
#endif
@EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<ThumbnailsModel> 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
}

View File

@ -83,7 +83,7 @@ struct ChannelPlaylistView: View {
.navigationTitle(playlist.title)
#else
.background(.thickMaterial)
.background(Color.tertiaryBackground)
#endif
}

View File

@ -9,7 +9,7 @@ struct ChannelVideosView: View {
@StateObject private var store = Store<Channel>()
@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,16 +65,19 @@ struct ChannelVideosView: View {
.frame(maxWidth: .infinity)
#endif
#if os(iOS)
VerticalCells(items: videos)
#else
if #available(macOS 12.0, *) {
VerticalCells(items: videos)
#if !os(iOS)
.prefersDefaultFocus(in: focusNamespace)
} else {
VerticalCells(items: videos)
}
#endif
}
.environment(\.inChannelView, true)
#if !os(iOS)
.focusScope(focusNamespace)
#endif
#if !os(tvOS)
.toolbar {
ToolbarItem(placement: .navigation) {
@ -98,7 +101,7 @@ struct ChannelVideosView: View {
}
}
#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 {

View File

@ -26,8 +26,13 @@ struct DetailBadge: View {
struct DefaultStyleModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
content
.background(.thinMaterial)
} else {
content
.background(Color.background)
}
}
}

View File

@ -1,15 +1,15 @@
import SwiftUI
struct OpenSettingsButton: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.presentationMode) private var presentationMode
#if !os(macOS)
@EnvironmentObject<NavigationModel> 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")
}
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
button
.buttonStyle(.borderedProminent)
} else {
button
}
}
}

View File

@ -25,7 +25,7 @@ struct PlayerControlsView<Content: View>: View {
}
private var controls: some View {
HStack {
let controls = HStack {
Button(action: {
model.presentingPlayer.toggle()
}) {
@ -92,14 +92,23 @@ struct PlayerControlsView<Content: View>: 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 {

View File

@ -31,9 +31,6 @@ struct SubscriptionsView: View {
FavoriteButton(item: FavoriteItem(section: .subscriptions))
}
}
.refreshable {
loadResources(force: true)
}
}
fileprivate func loadResources(force: Bool = false) {

View File

@ -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")

View File

@ -2,14 +2,14 @@ import Defaults
import SwiftUI
struct WelcomeScreen: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.presentationMode) private var presentationMode
@EnvironmentObject<AccountsModel> 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)
#endif
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
welcomeScreen
.interactiveDismissDisabled()
} else {
welcomeScreen
}
}
}

View File

@ -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 = "<group>"; };
3700155E271B12DD0049C794 /* SiestaConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiestaConfiguration.swift; sourceTree = "<group>"; };
@ -585,6 +561,9 @@
37169AA12729D98A0011DE61 /* InstancesBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesBridge.swift; sourceTree = "<group>"; };
37169AA52729E2CC0011DE61 /* AccountsBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsBridge.swift; sourceTree = "<group>"; };
371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = "<group>"; };
3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Badge+Backport.swift"; sourceTree = "<group>"; };
3722AEBD274DA401005EA4D6 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
3722AEBF274DAEB8005EA4D6 /* Tint+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tint+Backport.swift"; sourceTree = "<group>"; };
3729037D2739E47400EA99F6 /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = "<group>"; };
372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = "<group>"; };
@ -598,14 +577,14 @@
3743B86727216D3600261544 /* ChannelCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelCell.swift; sourceTree = "<group>"; };
3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueRow.swift; sourceTree = "<group>"; };
3743CA51270F284F00E4D32B /* View+Borders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Borders.swift"; sourceTree = "<group>"; };
374710042755291C00CE0F87 /* SearchField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchField.swift; sourceTree = "<group>"; };
3748186526A7627F0084E870 /* Video+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Video+Fixtures.swift"; sourceTree = "<group>"; };
3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Thumbnail+Fixtures.swift"; sourceTree = "<group>"; };
3748186D26A769D60084E870 /* DetailBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailBadge.swift; sourceTree = "<group>"; };
37484C1826FC837400287258 /* PlaybackSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSettings.swift; sourceTree = "<group>"; };
37484C1C26FC83A400287258 /* InstancesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesSettings.swift; sourceTree = "<group>"; };
37484C2426FC83E000287258 /* InstanceForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceForm.swift; sourceTree = "<group>"; };
37484C2826FC83FF00287258 /* AccountForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountForm.swift; sourceTree = "<group>"; };
37484C2C26FC844700287258 /* AccountsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsSettings.swift; sourceTree = "<group>"; };
37484C2C26FC844700287258 /* InstanceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSettings.swift; sourceTree = "<group>"; };
37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = "<group>"; };
374C053427242D9F009BDDBE /* ServicesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesSettings.swift; sourceTree = "<group>"; };
374C053A2724614F009BDDBE /* PlayerTVMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTVMenu.swift; sourceTree = "<group>"; };
@ -618,7 +597,6 @@
37599F37272B4D740087F250 /* FavoriteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteButton.swift; sourceTree = "<group>"; };
375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = "<group>"; };
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsubscribeAlertModifier.swift; sourceTree = "<group>"; };
3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecents.swift; sourceTree = "<group>"; };
376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = "<group>"; };
376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; };
@ -634,9 +612,13 @@
37732FF32703D32400F04329 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
3774122927387B6C00423605 /* InstancesModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModelTests.swift; sourceTree = "<group>"; };
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; };
3782B94E27553A6700990149 /* SearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSuggestions.swift; sourceTree = "<group>"; };
3782B9512755667600990149 /* String+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Format.swift"; sourceTree = "<group>"; };
3782B95C2755858100990149 /* NSTextField+FocusRingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextField+FocusRingType.swift"; sourceTree = "<group>"; };
3784B23A272894DA00B09468 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = "<group>"; };
3784B23C2728B85300B09468 /* ShareButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareButton.swift; sourceTree = "<group>"; };
3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItemView.swift; sourceTree = "<group>"; };
378AE942274EF00A006A4EE1 /* Color+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Background.swift"; sourceTree = "<group>"; };
378E50FA26FE8B9F00F49626 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = "<group>"; };
378E50FE26FE8EEE00F49626 /* AccountsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsMenuView.swift; sourceTree = "<group>"; };
37977582268922F600DD52A8 /* InvidiousAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvidiousAPI.swift; sourceTree = "<group>"; };
@ -710,7 +692,7 @@
37D4B0D82671614900C925CA /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
@ -721,6 +703,7 @@
37D526E22720B4BE00ED2F5E /* View+SwipeGesture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+SwipeGesture.swift"; sourceTree = "<group>"; };
37D9169A27388A81002B1BAA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.swift; sourceTree = "<group>"; };
37E2EEAA270656EC00170416 /* PlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; };
37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsModel.swift; sourceTree = "<group>"; };
37E70922271CD43000D34DDE /* WelcomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreen.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
3722AEBA274DA312005EA4D6 /* Backports */ = {
isa = PBXGroup;
children = (
3722AEBD274DA401005EA4D6 /* Backport.swift */,
3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */,
3722AEBF274DAEB8005EA4D6 /* Tint+Backport.swift */,
);
path = Backports;
sourceTree = "<group>";
};
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 = "<group>";
@ -1014,6 +1005,16 @@
name = Frameworks;
sourceTree = "<group>";
};
3782B95527557A2400990149 /* Search */ = {
isa = PBXGroup;
children = (
374710042755291C00CE0F87 /* SearchField.swift */,
3782B94E27553A6700990149 /* SearchSuggestions.swift */,
37AAF27F26737550007FC770 /* SearchView.swift */,
);
path = Search;
sourceTree = "<group>";
};
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 = "<group>";
};
37FD43E1270472060073EE42 /* Settings */ = {
isa = PBXGroup;
children = (
37FD43DB270470B70073EE42 /* InstancesSettings.swift */,
);
path = Settings;
sourceTree = "<group>";
};
/* 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" */ = {

View File

@ -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
}
},

View File

@ -15,7 +15,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "37D4B157267164AE00C925CA"
BuildableName = "Yattee (tvOS).app"
BuildableName = "Yattee.app"
BlueprintName = "Yattee (tvOS)"
ReferencedContainer = "container:Yattee.xcodeproj">
</BuildableReference>
@ -65,7 +65,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "37D4B157267164AE00C925CA"
BuildableName = "Yattee (tvOS).app"
BuildableName = "Yattee.app"
BlueprintName = "Yattee (tvOS)"
ReferencedContainer = "container:Yattee.xcodeproj">
</BuildableReference>
@ -82,7 +82,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "37D4B157267164AE00C925CA"
BuildableName = "Yattee (tvOS).app"
BuildableName = "Yattee.app"
BlueprintName = "Yattee (tvOS)"
ReferencedContainer = "container:Yattee.xcodeproj">
</BuildableReference>

View File

@ -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) {
.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
}
}
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) {
.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
},
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 []

View File

@ -1,10 +1,12 @@
{
"images" : [
{
"filename" : "TopShelf-Wide.png",
"idiom" : "tv",
"scale" : "1x"
},
{
"filename" : "TopShelf-Wide@2x.png",
"idiom" : "tv",
"scale" : "2x"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB