Compare commits

...

35 Commits

Author SHA1 Message Date
Arkadiusz Fal
fe33cc5e3a Bump build number 2021-12-08 00:29:00 +01:00
Arkadiusz Fal
7e7b4e89b5 Add Sparkle update framework for macOS 2021-12-08 00:27:38 +01:00
Arkadiusz Fal
d88292662f Minor README updates 2021-12-08 00:08:13 +01:00
Arkadiusz Fal
21b04e21c4 Remove unused file 2021-12-08 00:07:23 +01:00
Arkadiusz Fal
a44a61b017 Remove redundant query for replies when collapsed and expanded 2021-12-08 00:06:59 +01:00
Arkadiusz Fal
1b090fcd51 Bump build number 2021-12-06 19:15:05 +01:00
Arkadiusz Fal
12eb4401b5 Update README 2021-12-06 19:13:57 +01:00
Arkadiusz Fal
170f2ee94e Fix reloading favorites view 2021-12-06 19:13:49 +01:00
Arkadiusz Fal
fe56739211 Fix crash on dismissing channel playlist on iOS 2021-12-06 19:13:37 +01:00
Arkadiusz Fal
759a942426 Fix search field on macOS 2021-12-06 19:13:20 +01:00
Arkadiusz Fal
8d9bbf647a Fix disabling comments on tvOS 2021-12-06 19:12:59 +01:00
Arkadiusz Fal
eeb7b1f151 Improve search suggestions 2021-12-06 19:12:33 +01:00
Arkadiusz Fal
62bff9283c Faster replacing player item 2021-12-06 19:12:02 +01:00
Arkadiusz Fal
3624c9619a Add setting for displaying comments in separate tab or below description 2021-12-06 19:11:19 +01:00
Arkadiusz Fal
f7fc2369e3 Bump build number 2021-12-05 18:31:35 +01:00
Arkadiusz Fal
82ea8733ec Fix crash when video thumbnail cannot be loaded (fixes #28) 2021-12-05 18:31:35 +01:00
Arkadiusz Fal
1f495562fc Comments improvements
* Show text when there is no comments or comments are disabled
* Show progress indicator for loading comments/replies
* Improve layout of icons and text spacing
2021-12-05 18:31:33 +01:00
Arkadiusz Fal
37b99c59e1 Fix disabling comments 2021-12-05 18:12:13 +01:00
Arkadiusz Fal
7f9b53bd1f Fix login with Invidious accounts 2021-12-05 18:10:10 +01:00
Arkadiusz Fal
941e6a909d Set full screen views background color based on color scheme on tvOS (fixes #30) 2021-12-05 18:09:25 +01:00
Arkadiusz Fal
5143c4f8ce Bump build number 2021-12-04 20:57:11 +01:00
Arkadiusz Fal
19a3f08336 Comments (fixes #4) 2021-12-04 20:57:09 +01:00
Arkadiusz Fal
eb537676e6 Update README 2021-12-02 21:35:55 +01:00
Arkadiusz Fal
e97daa1944 Minor UI fixes 2021-12-02 21:35:42 +01:00
Arkadiusz Fal
bd59b8e2c3 Improve favorite button 2021-12-02 21:35:25 +01:00
Arkadiusz Fal
19b146c6ad Close current video (fixes #15) 2021-12-02 21:19:10 +01:00
Arkadiusz Fal
dd995105b5 Minor UI fixes for macOS Big Sur 2021-12-02 20:33:32 +01:00
Arkadiusz Fal
c4b5c7ce41 Fix scrolling of favorites on macOS Big Sur 2021-12-02 20:33:32 +01:00
Arkadiusz Fal
cc2bf90218 Bump build number 2021-12-02 00:18:51 +01:00
Arkadiusz Fal
45c917160e Display build number next to version 2021-12-02 00:17:19 +01:00
Arkadiusz Fal
9f5e9ea237 Add context menu to related items for queuing 2021-12-02 00:15:36 +01:00
Arkadiusz Fal
1c61ad37a9 Fix search on tvOS 2021-12-02 00:13:41 +01:00
Arkadiusz Fal
06f7391ad9 Add setting for saving recents (fixes #14) 2021-12-02 00:12:15 +01:00
Arkadiusz Fal
e61d1dfe2e Add settings for selecting visible sections (fixes #16) 2021-12-02 00:10:21 +01:00
Arkadiusz Fal
ff83abd103 Fix crash on opening player in iOS 14 (fixes #20) 2021-12-02 00:08:48 +01:00
63 changed files with 1576 additions and 478 deletions

View File

@@ -5,7 +5,12 @@ extension Backport where Content: View {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
content.badge(count) content.badge(count)
} else { } else {
content HStack {
content
Spacer()
Text("\(count)")
.foregroundColor(.secondary)
}
} }
} }
} }

View File

@@ -10,8 +10,8 @@ extension Color {
static let secondaryBackground = Color(UIColor.secondarySystemBackground) static let secondaryBackground = Color(UIColor.secondarySystemBackground)
static let tertiaryBackground = Color(UIColor.tertiarySystemBackground) static let tertiaryBackground = Color(UIColor.tertiarySystemBackground)
#else #else
static let background = Color.black static func background(scheme: ColorScheme) -> Color {
static let secondaryBackground = Color.black scheme == .dark ? .black : .init(white: 0.8)
static let tertiaryBackground = Color.black }
#endif #endif
} }

View File

@@ -0,0 +1,18 @@
import Foundation
extension Comment {
static var fixture: Comment {
Comment(
id: UUID().uuidString,
author: "The Author",
authorAvatarURL: "https://pipedproxy-ams-2.kavin.rocks/Si7ZhtmpX84wj6MoJYLs8kwALw2Hm53wzbrPamoU-z3qvCKs2X3zPNYKMSJEvPDLUHzbvTfLcg=s176-c-k-c0x00ffffff-no-rw?host=yt3.ggpht.com",
time: "2 months ago",
pinned: true,
hearted: true,
likeCount: 30032,
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus feugiat mi, suscipit pharetra lectus dapibus vel. Vivamus orci erat, sagittis sit amet dui vel, feugiat cursus ante. Pellentesque eget orci tortor. Suspendisse pulvinar orci tortor, eu scelerisque neque consequat nec. Aliquam sit amet turpis et nunc placerat finibus eget sit amet justo. Nullam tincidunt ornare neque. Donec ornare, arcu at elementum pulvinar, urna elit pharetra diam, vel ultrices lacus diam at lorem. Sed vel maximus dolor. Morbi massa est, interdum quis justo sit amet, dapibus bibendum tellus. Integer at purus nec neque tincidunt convallis sit amet eu odio. Duis et ante vitae sem tincidunt facilisis sit amet ac mauris. Quisque varius non nisi vel placerat. Nulla orci metus, imperdiet ac accumsan sed, pellentesque eget nisl. Praesent a suscipit lacus, ut finibus orci. Nulla ut eros commodo, fermentum purus at, porta leo. In finibus luctus nulla, eget posuere eros mollis vel. ",
repliesPage: "some url",
channel: .init(id: "", name: "")
)
}
}

View File

@@ -5,6 +5,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.environmentObject(AccountsModel()) .environmentObject(AccountsModel())
.environmentObject(CommentsModel())
.environmentObject(InstancesModel()) .environmentObject(InstancesModel())
.environmentObject(invidious) .environmentObject(invidious)
.environmentObject(NavigationModel()) .environmentObject(NavigationModel())

View File

@@ -27,10 +27,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
self.account = account self.account = account
validInstance = false validInstance = false
signedIn = false signedIn = !account.anonymous
configure() configure()
validate()
} }
func validate() { func validate() {
@@ -257,6 +256,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
.withParam("q", query.lowercased()) .withParam("q", query.lowercased())
} }
func comments(_: Video.ID, page _: String?) -> Resource? { nil }
private func searchQuery(_ query: String) -> String { private func searchQuery(_ query: String) -> String {
var searchQuery = query var searchQuery = query

View File

@@ -71,6 +71,15 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
content.json.arrayValue.map { PipedAPI.extractVideo(from: $0)! } content.json.arrayValue.map { PipedAPI.extractVideo(from: $0)! }
} }
configureTransformer(pathPattern("comments/*")) { (content: Entity<JSON>) -> CommentsPage in
let details = content.json.dictionaryValue
let comments = details["comments"]?.arrayValue.map { PipedAPI.extractComment(from: $0)! } ?? []
let nextPage = details["nextpage"]?.stringValue
let disabled = details["disabled"]?.boolValue ?? false
return CommentsPage(comments: comments, nextPage: nextPage, disabled: disabled)
}
if account.token.isNil { if account.token.isNil {
updateToken() updateToken()
} }
@@ -80,9 +89,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
PipedAPI.authorizedEndpoints.contains { url.absoluteString.contains($0) } PipedAPI.authorizedEndpoints.contains { url.absoluteString.contains($0) }
} }
@discardableResult func updateToken() -> Request { func updateToken() {
guard !account.anonymous else {
return
}
account.token = nil account.token = nil
return login.request(
login.request(
.post, .post,
json: ["username": account.username, "password": account.password] json: ["username": account.username, "password": account.password]
) )
@@ -161,6 +175,17 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
func playlistVideo(_: String, _: String) -> Resource? { nil } func playlistVideo(_: String, _: String) -> Resource? { nil }
func playlistVideos(_: String) -> Resource? { nil } func playlistVideos(_: String) -> Resource? { nil }
func comments(_ id: Video.ID, page: String?) -> Resource? {
let path = page.isNil ? "comments/\(id)" : "nextpage/comments/\(id)"
let resource = resource(baseURL: account.url, path: path)
if page.isNil {
return resource
}
return resource.withParam("nextpage", page)
}
private func pathPattern(_ path: String) -> String { private func pathPattern(_ path: String) -> String {
"**\(path)" "**\(path)"
} }
@@ -395,4 +420,23 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
.arrayValue .arrayValue
.filter { $0.dictionaryValue["format"] == "MPEG_4" } ?? [] .filter { $0.dictionaryValue["format"] == "MPEG_4" } ?? []
} }
private static func extractComment(from content: JSON) -> Comment? {
let details = content.dictionaryValue
let author = details["author"]?.stringValue ?? ""
let commentorUrl = details["commentorUrl"]?.stringValue
let channelId = commentorUrl?.components(separatedBy: "/")[2] ?? ""
return Comment(
id: details["commentId"]?.stringValue ?? UUID().uuidString,
author: author,
authorAvatarURL: details["thumbnail"]?.stringValue ?? "",
time: details["commentedTime"]?.stringValue ?? "",
pinned: details["pinned"]?.boolValue ?? false,
hearted: details["hearted"]?.boolValue ?? false,
likeCount: details["likeCount"]?.intValue ?? 0,
text: details["commentText"]?.stringValue ?? "",
repliesPage: details["repliesPage"]?.stringValue,
channel: Channel(id: channelId, name: author)
)
}
} }

View File

@@ -31,6 +31,8 @@ protocol VideosAPI {
func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void) func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void)
func shareURL(_ item: ContentItem, frontendHost: String?, time: CMTime?) -> URL? func shareURL(_ item: ContentItem, frontendHost: String?, time: CMTime?) -> URL?
func comments(_ id: Video.ID, page: String?) -> Resource?
} }
extension VideosAPI { extension VideosAPI {

View File

@@ -38,4 +38,8 @@ enum VideosApp: String, CaseIterable {
var hasFrontendURL: Bool { var hasFrontendURL: Bool {
self == .piped self == .piped
} }
var supportsComments: Bool {
self == .piped
}
} }

16
Model/Comment.swift Normal file
View File

@@ -0,0 +1,16 @@
struct Comment: Identifiable, Equatable {
let id: String
let author: String
let authorAvatarURL: String
let time: String
let pinned: Bool
let hearted: Bool
var likeCount: Int
let text: String
let repliesPage: String?
let channel: Channel
var hasReplies: Bool {
!(repliesPage?.isEmpty ?? true)
}
}

110
Model/CommentsModel.swift Normal file
View File

@@ -0,0 +1,110 @@
import Defaults
import Foundation
import SwiftyJSON
final class CommentsModel: ObservableObject {
@Published var all = [Comment]()
@Published var nextPage: String?
@Published var firstPage = true
@Published var loaded = true
@Published var disabled = false
@Published var replies = [Comment]()
@Published var repliesPageID: String?
@Published var repliesLoaded = false
var accounts: AccountsModel!
var player: PlayerModel!
var instance: Instance? {
InstancesModel.find(Defaults[.commentsInstanceID])
}
var api: VideosAPI? {
instance.isNil ? nil : PipedAPI(account: instance!.anonymousAccount)
}
static var enabled: Bool {
!Defaults[.commentsInstanceID].isNil && !Defaults[.commentsInstanceID]!.isEmpty
}
#if !os(tvOS)
static var placement: CommentsPlacement {
Defaults[.commentsPlacement]
}
#endif
var nextPageAvailable: Bool {
!(nextPage?.isEmpty ?? true)
}
func load(page: String? = nil) {
guard Self.enabled else {
return
}
reset()
guard !instance.isNil,
!(player?.currentVideo.isNil ?? true)
else {
return
}
firstPage = page.isNil || page!.isEmpty
api?.comments(player.currentVideo!.videoID, page: page)?
.load()
.onSuccess { [weak self] response in
if let page: CommentsPage = response.typedContent() {
self?.all = page.comments
self?.nextPage = page.nextPage
self?.disabled = page.disabled
}
}
.onCompletion { [weak self] _ in
self?.loaded = true
}
}
func loadNextPage() {
load(page: nextPage)
}
func loadReplies(page: String) {
guard !player.currentVideo.isNil else {
return
}
if page == repliesPageID {
return
}
replies = []
repliesPageID = page
repliesLoaded = false
api?.comments(player.currentVideo!.videoID, page: page)?
.load()
.onSuccess { [weak self] response in
if let page: CommentsPage = response.typedContent() {
self?.replies = page.comments
}
}
.onCompletion { [weak self] _ in
self?.repliesLoaded = true
}
}
func reset() {
all = []
disabled = false
firstPage = true
nextPage = nil
loaded = false
replies = []
repliesLoaded = false
}
}

7
Model/CommentsPage.swift Normal file
View File

@@ -0,0 +1,7 @@
import Foundation
struct CommentsPage {
var comments = [Comment]()
var nextPage: String?
var disabled = false
}

View File

@@ -5,6 +5,11 @@ struct FavoritesModel {
static let shared = FavoritesModel() static let shared = FavoritesModel()
@Default(.favorites) var all @Default(.favorites) var all
@Default(.visibleSections) var visibleSections
var isEnabled: Bool {
visibleSections.contains(.favorites)
}
func contains(_ item: FavoriteItem) -> Bool { func contains(_ item: FavoriteItem) -> Bool {
all.contains { $0 == item } all.contains { $0 == item }

View File

@@ -23,7 +23,7 @@ final class NavigationModel: ObservableObject {
} }
} }
@Published var tabSelection: TabSelection! = .favorites @Published var tabSelection: TabSelection!
@Published var presentingAddToPlaylist = false @Published var presentingAddToPlaylist = false
@Published var videoToAddToPlaylist: Video! @Published var videoToAddToPlaylist: Video!

View File

@@ -43,6 +43,7 @@ final class PlayerModel: ObservableObject {
@Published var restoredSegments = [Segment]() @Published var restoredSegments = [Segment]()
var accounts: AccountsModel var accounts: AccountsModel
var comments: CommentsModel
var composition = AVMutableComposition() var composition = AVMutableComposition()
var loadedCompositionAssets = [AVMediaType]() var loadedCompositionAssets = [AVMediaType]()
@@ -67,8 +68,9 @@ final class PlayerModel: ObservableObject {
#endif #endif
}} }}
init(accounts: AccountsModel? = nil, instances _: InstancesModel? = nil) { init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil) {
self.accounts = accounts ?? AccountsModel() self.accounts = accounts ?? AccountsModel()
self.comments = comments ?? CommentsModel()
addItemDidPlayToEndTimeObserver() addItemDidPlayToEndTimeObserver()
addFrequentTimeObserver() addFrequentTimeObserver()
@@ -138,6 +140,7 @@ final class PlayerModel: ObservableObject {
playerError = nil playerError = nil
resetSegments() resetSegments()
sponsorBlock.loadSegments(videoID: video.videoID, categories: Defaults[.sponsorBlockCategories]) sponsorBlock.loadSegments(videoID: video.videoID, categories: Defaults[.sponsorBlockCategories])
comments.load()
if let url = stream.singleAssetURL { if let url = stream.singleAssetURL {
logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)") logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)")
@@ -233,7 +236,7 @@ final class PlayerModel: ObservableObject {
loadCompositionAsset(stream.videoAsset, stream: stream, type: .video, of: video, preservingTime: preservingTime) loadCompositionAsset(stream.videoAsset, stream: stream, type: .video, of: video, preservingTime: preservingTime)
} }
func loadCompositionAsset( private func loadCompositionAsset(
_ asset: AVURLAsset, _ asset: AVURLAsset,
stream: Stream, stream: Stream,
type: AVMediaType, type: AVMediaType,
@@ -478,10 +481,9 @@ final class PlayerModel: ObservableObject {
fileprivate func updateNowPlayingInfo() { fileprivate func updateNowPlayingInfo() {
let duration: Int? = currentItem.video.live ? nil : Int(currentItem.videoDuration ?? 0) let duration: Int? = currentItem.video.live ? nil : Int(currentItem.videoDuration ?? 0)
let nowPlayingInfo: [String: AnyObject] = [ var nowPlayingInfo: [String: AnyObject] = [
MPMediaItemPropertyTitle: currentItem.video.title as AnyObject, MPMediaItemPropertyTitle: currentItem.video.title as AnyObject,
MPMediaItemPropertyArtist: currentItem.video.author as AnyObject, MPMediaItemPropertyArtist: currentItem.video.author as AnyObject,
MPMediaItemPropertyArtwork: currentArtwork as AnyObject,
MPMediaItemPropertyPlaybackDuration: duration as AnyObject, MPMediaItemPropertyPlaybackDuration: duration as AnyObject,
MPNowPlayingInfoPropertyIsLiveStream: currentItem.video.live as AnyObject, MPNowPlayingInfoPropertyIsLiveStream: currentItem.video.live as AnyObject,
MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime().seconds as AnyObject, MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime().seconds as AnyObject,
@@ -489,6 +491,10 @@ final class PlayerModel: ObservableObject {
MPMediaItemPropertyMediaType: MPMediaType.anyVideo.rawValue as AnyObject MPMediaItemPropertyMediaType: MPMediaType.anyVideo.rawValue as AnyObject
] ]
if !currentArtwork.isNil {
nowPlayingInfo[MPMediaItemPropertyArtwork] = currentArtwork as AnyObject
}
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
} }
@@ -517,4 +523,10 @@ final class PlayerModel: ObservableObject {
return "\(formatter.string(from: NSNumber(value: rate))!)×" return "\(formatter.string(from: NSNumber(value: rate))!)×"
} }
func closeCurrentItem() {
addCurrentItemToHistory()
currentItem = nil
player.replaceCurrentItem(with: nil)
}
} }

View File

@@ -29,6 +29,7 @@ extension PlayerModel {
} }
func playNow(_ video: Video, at time: TimeInterval? = nil) { func playNow(_ video: Video, at time: TimeInterval? = nil) {
player.replaceCurrentItem(with: nil)
addCurrentItemToHistory() addCurrentItemToHistory()
enqueueVideo(video, prepending: true) { _, item in enqueueVideo(video, prepending: true) { _, item in
@@ -37,6 +38,7 @@ extension PlayerModel {
} }
func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) { func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) {
comments.reset()
currentItem = item currentItem = item
if !time.isNil { if !time.isNil {
@@ -91,6 +93,7 @@ extension PlayerModel {
} }
func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) { func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) {
player.replaceCurrentItem(with: nil)
addCurrentItemToHistory() addCurrentItemToHistory()
remove(newItem) remove(newItem)

View File

@@ -1,22 +0,0 @@
import Alamofire
import Foundation
import SwiftyJSON
final class PlaylistsProvider: DataProvider {
@Published var playlists = [Playlist]()
let profile = Profile()
func load(successHandler: @escaping ([Playlist]) -> Void = { _ in }) {
let headers = HTTPHeaders([HTTPHeader(name: "Cookie", value: "SID=\(profile.sid)")])
DataProvider.request("auth/playlists", headers: headers).responseJSON { response in
switch response.result {
case let .success(value):
self.playlists = JSON(value).arrayValue.map { Playlist($0) }
successHandler(self.playlists)
case let .failure(error):
print(error)
}
}
}
}

View File

@@ -3,7 +3,7 @@ import Foundation
final class RecentsModel: ObservableObject { final class RecentsModel: ObservableObject {
@Default(.recentlyOpened) var items @Default(.recentlyOpened) var items
@Default(.saveRecents) var saveRecents
func clear() { func clear() {
items = [] items = []
} }
@@ -13,6 +13,14 @@ final class RecentsModel: ObservableObject {
} }
func add(_ item: RecentItem) { func add(_ item: RecentItem) {
if !saveRecents {
clear()
if item.type != .channel {
return
}
}
if let index = items.firstIndex(where: { $0.id == item.id }) { if let index = items.firstIndex(where: { $0.id == item.id }) {
items.remove(at: index) items.remove(at: index)
} }

View File

@@ -9,6 +9,7 @@ final class SearchModel: ObservableObject {
@Published var query = SearchQuery() @Published var query = SearchQuery()
@Published var queryText = "" @Published var queryText = ""
@Published var querySuggestions = Store<[String]>() @Published var querySuggestions = Store<[String]>()
@Published var suggestionsText = ""
@Published var fieldIsFocused = false @Published var fieldIsFocused = false
@@ -88,7 +89,7 @@ final class SearchModel: ObservableObject {
suggestionsDebounceTimer?.invalidate() suggestionsDebounceTimer?.invalidate()
suggestionsDebounceTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in suggestionsDebounceTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in
let resource = self.accounts.api.searchSuggestions(query: query) let resource = self.accounts.api.searchSuggestions(query: query)
resource.addObserver(self.querySuggestions) resource.addObserver(self.querySuggestions)
@@ -99,9 +100,11 @@ final class SearchModel: ObservableObject {
if let suggestions: [String] = response.typedContent() { if let suggestions: [String] = response.typedContent() {
self.querySuggestions = Store<[String]>(suggestions) self.querySuggestions = Store<[String]>(suggestions)
} }
self.suggestionsText = query
} }
} else { } else {
self.querySuggestions = Store<[String]>(self.querySuggestions.collection) self.querySuggestions = Store<[String]>(self.querySuggestions.collection)
self.suggestionsText = query
} }
} }
} }

106
README.md
View File

@@ -1,14 +1,12 @@
![Yattee Banner](https://r.yattee.stream/icons/yattee-banner.png) ![Yattee Banner](https://r.yattee.stream/icons/yattee-banner.png)
Video player for [Invidious](https://github.com/iv-org/invidious) and [Piped](https://github.com/TeamPiped/Piped) instances built for iOS, tvOS and macOS. Video player for [Invidious](https://github.com/iv-org/invidious) and [Piped](https://github.com/TeamPiped/Piped) 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) [![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/yattee)](https://github.com/yattee/yattee/issues) [![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) [![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) [![Matrix](https://img.shields.io/matrix/yattee:matrix.org)](https://matrix.to/#/#yattee:matrix.org)
![Screenshot](https://r.yattee.stream/screenshots/all-platforms.png) ![Screenshot](https://r.yattee.stream/screenshots/all-platforms.png)
## Features ## Features
@@ -35,101 +33,17 @@ Video player for [Invidious](https://github.com/iv-org/invidious) and [Piped](ht
| Search Suggestions | ✅ | ✅ | | Search Suggestions | ✅ | ✅ |
| Search Filters | ✅ | 🔴 | | Search Filters | ✅ | 🔴 |
| Subtitles | 🔴 | ✅ | | Subtitles | 🔴 | ✅ |
| Comments | 🔴 | ✅ |
## Installation You can browse and use accounts from one app and play videos with another (for example: use Invidious account for subscriptions and use Piped as playback source). Comments can be displayed from Piped even when Invidious is used for browsing/playing.
### Requirements
System requirements:
* iOS 14 (or newer)
* tvOS 15 (or newer)
* macOS Big Sur (or newer)
### How to install? ## Documentation
#### [AltStore](https://altstore.io/) (free) * [Installation Instructions](https://github.com/yattee/yattee/wiki/Installation-instructions)
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. * [Integrations](https://github.com/yattee/yattee/wiki/Integrations)
* [Screenshots Gallery](https://github.com/yattee/yattee/wiki/Screenshots-Gallery)
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: * [Tips](https://github.com/yattee/yattee/wiki/Tips)
* [FAQ](https://github.com/yattee/yattee/wiki)
`https://alt.yattee.stream` * [Donations](https://github.com/yattee/yattee/wiki/Donations)
#### 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 you will need to reinstall every 7 days.
## Integrations
### macOS
With [Finicky](https://github.com/johnste/finicky) you can configure your system to open all the video links in the app. Example configuration:
```js
{
match: [
finicky.matchDomains(/(.*\.)?youtube.com/),
finicky.matchDomains(/(.*\.)?youtu.be/)
],
browser: "/Applications/Yattee.app"
}
```
## Screenshots
### iOS
| Player | Search | Playlists |
| - | - | - |
| [![Yattee Player iOS](https://r.yattee.stream/screenshots/iOS/player-thumb.png)](https://r.yattee.stream/screenshots/iOS/player.png) | [![Yattee Search iOS](https://r.yattee.stream/screenshots/iOS/search-suggestions-thumb.png)](https://r.yattee.stream/screenshots/iOS/search-suggestions.png) | [![Yattee Subscriptions iOS](https://r.yattee.stream/screenshots/iOS/playlists-thumb.png)](https://r.yattee.stream/screenshots/iOS/playlists.png) |
### iPadOS
| Settings | Player | Subscriptions |
| - | - | - |
| [![Yattee Player iPadOS](https://r.yattee.stream/screenshots/iPadOS/settings-thumb.png)](https://r.yattee.stream/screenshots/iPadOS/settings.png) | [![Yattee Player iPadOS](https://r.yattee.stream/screenshots/iPadOS/player-thumb.png)](https://r.yattee.stream/screenshots/iPadOS/player.png) | [![Yattee Subscriptions iPad S](https://r.yattee.stream/screenshots/iPadOS/subscriptions-thumb.png)](https://r.yattee.stream/screenshots/iPadOS/subscriptions.png) |
### tvOS
| Player | Popular | Search | Now Playing | Settings |
| - | - | - | - | - |
| [![Yattee Player tvOS](https://r.yattee.stream/screenshots/tvOS/player-thumb.png)](https://r.yattee.stream/screenshots/tvOS/player.png) | [![Yattee Popular tvOS](https://r.yattee.stream/screenshots/tvOS/popular-thumb.png)](https://r.yattee.stream/screenshots/tvOS/popular.png) | [![Yattee Search tvOS](https://r.yattee.stream/screenshots/tvOS/search-thumb.png)](https://r.yattee.stream/screenshots/tvOS/search.png) | [![Yattee Now Playing tvOS](https://r.yattee.stream/screenshots/tvOS/now-playing-thumb.png)](https://r.yattee.stream/screenshots/tvOS/now-playing.png) | [![Yattee Settings tvOS](https://r.yattee.stream/screenshots/tvOS/settings-thumb.png)](https://r.yattee.stream/screenshots/tvOS/settings.png) |
### macOS
| Player | Channel | Search | Settings |
| - | - | - | - |
| [![Yattee Player macOS](https://r.yattee.stream/screenshots/macOS/player-thumb.png)](https://r.yattee.stream/screenshots/macOS/player.png) | [![Yattee Channel macOS](https://r.yattee.stream/screenshots/macOS/channel-thumb.png)](https://r.yattee.stream/screenshots/macOS/channel.png) | [![Yattee Search macOS](https://r.yattee.stream/screenshots/macOS/search-thumb.png)](https://r.yattee.stream/screenshots/macOS/search.png) | [![Yattee Settings macOS](https://r.yattee.stream/screenshots/macOS/settings-thumb.png)](https://r.yattee.stream/screenshots/macOS/settings.png) |
## Tips
### Settings
* [tvOS] To open settings, press Play/Pause button while hovering over navigation menu or video
### Navigation
* Use videos context menus to add to queue, open or subscribe channel and add to playlist
* [tvOS] Pressing buttons in the app trigger switch to next available option (for example: next account in Settings). If you want to access list of all options, press and hold to open the context menu.
* [iOS] Swipe the player/title bar: up to open fullscreen details view, bottom to close fullscreen details or hide player
### Favorites
* Add more sections using ❤️ button in views channels, playlists, searches, subscriptions and popular
* [iOS/macOS] Reorganize with dragging and dropping
* [iOS/macOS] Remove section with right click/press and hold on section name
* [tvOS] Reorganize and remove from `Settings > Edit Favorites...`
### Keyboard shortcuts
* `Command+1` - Favorites
* `Command+2` - Subscriptions
* `Command+3` - Popular
* `Command+4` - Trending
* `Command+F` - Search
* `Command+P` - Play/Pause
* `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 ## 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. 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.

View File

@@ -2,10 +2,11 @@ import Defaults
import Foundation import Foundation
extension Defaults.Keys { extension Defaults.Keys {
static let kavinPipedInstanceID = "kavin-piped"
static let instances = Key<[Instance]>("instances", default: [ static let instances = Key<[Instance]>("instances", default: [
.init( .init(
app: .piped, app: .piped,
id: "default-piped-instance", id: kavinPipedInstanceID,
name: "Kavin", name: "Kavin",
apiURL: "https://pipedapi.kavin.rocks", apiURL: "https://pipedapi.kavin.rocks",
frontendURL: "https://piped.kavin.rocks" frontendURL: "https://piped.kavin.rocks"
@@ -32,6 +33,10 @@ extension Defaults.Keys {
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: PlayerSidebarSetting.defaultValue) static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: PlayerSidebarSetting.defaultValue)
static let playerInstanceID = Key<Instance.ID?>("playerInstance") static let playerInstanceID = Key<Instance.ID?>("playerInstance")
static let showKeywords = Key<Bool>("showKeywords", default: false) static let showKeywords = Key<Bool>("showKeywords", default: false)
static let commentsInstanceID = Key<Instance.ID?>("commentsInstance", default: kavinPipedInstanceID)
#if !os(tvOS)
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)
#endif
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: []) static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
@@ -40,12 +45,15 @@ extension Defaults.Keys {
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed") static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
static let saveHistory = Key<Bool>("saveHistory", default: true) static let saveHistory = Key<Bool>("saveHistory", default: true)
static let saveRecents = Key<Bool>("saveRecents", default: true)
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default) static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
static let trendingCountry = Key<Country>("trendingCountry", default: .us) static let trendingCountry = Key<Country>("trendingCountry", default: .us)
#if os(iOS) static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.favorites, .subscriptions, .trending, .playlists])
static let tabNavigationSection = Key<TabNavigationSectionSetting>("tabNavigationSection", default: .trending)
#if os(macOS)
static let enableBetaChannel = Key<Bool>("enableBetaChannel", default: false)
#endif #endif
} }
@@ -83,8 +91,54 @@ enum PlayerSidebarSetting: String, CaseIterable, Defaults.Serializable {
} }
} }
#if os(iOS) enum VisibleSection: String, CaseIterable, Comparable, Defaults.Serializable {
enum TabNavigationSectionSetting: String, Defaults.Serializable { case favorites, subscriptions, popular, trending, playlists
case trending, popular
static func from(_ string: String) -> VisibleSection {
allCases.first { $0.rawValue == string }!
}
var title: String {
rawValue.localizedCapitalized
}
var tabSelection: TabSelection {
switch self {
case .favorites:
return TabSelection.favorites
case .subscriptions:
return TabSelection.subscriptions
case .popular:
return TabSelection.popular
case .trending:
return TabSelection.trending
case .playlists:
return TabSelection.playlists
}
}
private var sortOrder: Int {
switch self {
case .favorites:
return 0
case .subscriptions:
return 1
case .popular:
return 2
case .trending:
return 3
case .playlists:
return 4
}
}
static func < (lhs: Self, rhs: Self) -> Bool {
lhs.sortOrder < rhs.sortOrder
}
}
#if !os(tvOS)
enum CommentsPlacement: String, CaseIterable, Defaults.Serializable {
case info, separate
} }
#endif #endif

View File

@@ -66,6 +66,10 @@ struct FavoriteItemView: View {
#endif #endif
} }
} }
.onChange(of: accounts.current) { _ in
resource?.addObserver(store)
resource?.load()
}
} }
private var isVisible: Bool { private var isVisible: Bool {

View File

@@ -29,6 +29,9 @@ struct FavoritesView: View {
#else #else
ForEach(favorites) { item in ForEach(favorites) { item in
FavoriteItemView(item: item, dragging: $dragging) FavoriteItemView(item: item, dragging: $dragging)
#if os(macOS)
.workaroundForVerticalScrollingBug()
#endif
} }
#endif #endif
} }

View File

@@ -1,3 +1,4 @@
import Defaults
import SwiftUI import SwiftUI
#if os(iOS) #if os(iOS)
import Introspect import Introspect
@@ -5,8 +6,19 @@ import SwiftUI
struct AppSidebarNavigation: View { struct AppSidebarNavigation: View {
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<InstancesModel> private var instances
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlaylistsModel> private var playlists
@EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> private var search
@EnvironmentObject<SubscriptionsModel> private var subscriptions
@EnvironmentObject<ThumbnailsModel> private var thumbnailsModel
@Default(.visibleSections) private var visibleSections
#if os(iOS) #if os(iOS)
@EnvironmentObject<NavigationModel> private var navigation
@State private var didApplyPrimaryViewWorkAround = false @State private var didApplyPrimaryViewWorkAround = false
#endif #endif
@@ -38,15 +50,50 @@ struct AppSidebarNavigation: View {
.frame(minWidth: sidebarMinWidth) .frame(minWidth: sidebarMinWidth)
VStack { VStack {
Image(systemName: "play.tv") PlayerControlsView {
.renderingMode(.original) HStack(alignment: .center) {
.font(.system(size: 60)) Spacer()
.foregroundColor(.accentColor) Image(systemName: "play.tv")
.renderingMode(.original)
.font(.system(size: 60))
.foregroundColor(.accentColor)
Spacer()
}
}
} }
} }
#if os(iOS)
.background(
EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) {
videoPlayer
.environment(\.navigationStyle, .sidebar)
}
)
#elseif os(macOS)
.background(
EmptyView().sheet(isPresented: $player.presentingPlayer) {
videoPlayer
.frame(minWidth: 1000, minHeight: 750)
.environment(\.navigationStyle, .sidebar)
}
)
#endif
.environment(\.navigationStyle, .sidebar) .environment(\.navigationStyle, .sidebar)
} }
private var videoPlayer: some View {
VideoPlayerView()
.environmentObject(accounts)
.environmentObject(comments)
.environmentObject(instances)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(playlists)
.environmentObject(recents)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
}
var toolbarContent: some ToolbarContent { var toolbarContent: some ToolbarContent {
Group { Group {
#if os(iOS) #if os(iOS)

View File

@@ -3,74 +3,41 @@ import SwiftUI
struct AppTabNavigation: View { struct AppTabNavigation: View {
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<InstancesModel> private var instances
@EnvironmentObject<NavigationModel> private var navigation @EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlaylistsModel> private var playlists
@EnvironmentObject<RecentsModel> private var recents @EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> private var search @EnvironmentObject<SearchModel> private var search
@EnvironmentObject<SubscriptionsModel> private var subscriptions
@EnvironmentObject<ThumbnailsModel> private var thumbnailsModel
@Default(.tabNavigationSection) private var tabNavigationSection @Default(.visibleSections) private var visibleSections
var body: some View { var body: some View {
TabView(selection: navigation.tabSelectionBinding) { TabView(selection: navigation.tabSelectionBinding) {
NavigationView { if visibleSections.contains(.favorites) {
LazyView(FavoritesView()) favoritesNavigationView
.toolbar { toolbarContent }
}
.tabItem {
Label("Favorites", systemImage: "heart")
.accessibility(label: Text("Favorites"))
}
.tag(TabSelection.favorites)
if subscriptionsVisible {
NavigationView {
LazyView(SubscriptionsView())
.toolbar { toolbarContent }
}
.tabItem {
Label("Subscriptions", systemImage: "star.circle.fill")
.accessibility(label: Text("Subscriptions"))
}
.tag(TabSelection.subscriptions)
} }
if subscriptionsVisible { if subscriptionsVisible {
if accounts.app.supportsPopular { subscriptionsNavigationView
if tabNavigationSection == .popular { }
popularNavigationView
} else { if visibleSections.contains(.popular), accounts.app.supportsPopular, visibleSections.count < 5 {
trendingNavigationView popularNavigationView
} }
} else {
trendingNavigationView if visibleSections.contains(.trending) {
}
} else {
if accounts.app.supportsPopular {
popularNavigationView
}
trendingNavigationView trendingNavigationView
} }
if accounts.app.supportsUserPlaylists { if visibleSections.contains(.playlists), accounts.app.supportsUserPlaylists {
NavigationView { playlistsNavigationView
LazyView(PlaylistsView())
.toolbar { toolbarContent }
}
.tabItem {
Label("Playlists", systemImage: "list.and.film")
.accessibility(label: Text("Playlists"))
}
.tag(TabSelection.playlists)
} }
NavigationView { searchNavigationView
LazyView(SearchView())
}
.tabItem {
Label("Search", systemImage: "magnifyingglass")
.accessibility(label: Text("Search"))
}
.tag(TabSelection.search)
} }
.id(accounts.current?.id ?? "") .id(accounts.current?.id ?? "")
.environment(\.navigationStyle, .tab) .environment(\.navigationStyle, .tab)
@@ -100,15 +67,47 @@ struct AppTabNavigation: View {
NavigationView { NavigationView {
ChannelPlaylistView(playlist: playlist) ChannelPlaylistView(playlist: playlist)
.environment(\.inNavigationView, true) .environment(\.inNavigationView, true)
.environmentObject(subscriptions)
.background(playerNavigationLink) .background(playerNavigationLink)
} }
} }
} }
) )
.background(
EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) {
videoPlayer
.environment(\.navigationStyle, .sidebar)
}
)
}
private var favoritesNavigationView: some View {
NavigationView {
LazyView(FavoritesView())
.toolbar { toolbarContent }
}
.tabItem {
Label("Favorites", systemImage: "heart")
.accessibility(label: Text("Favorites"))
}
.tag(TabSelection.favorites)
}
private var subscriptionsNavigationView: some View {
NavigationView {
LazyView(SubscriptionsView())
.toolbar { toolbarContent }
}
.tabItem {
Label("Subscriptions", systemImage: "star.circle.fill")
.accessibility(label: Text("Subscriptions"))
}
.tag(TabSelection.subscriptions)
} }
private var subscriptionsVisible: Bool { private var subscriptionsVisible: Bool {
accounts.app.supportsSubscriptions && !(accounts.current?.anonymous ?? true) visibleSections.contains(.subscriptions) &&
accounts.app.supportsSubscriptions && !(accounts.current?.anonymous ?? true)
} }
private var popularNavigationView: some View { private var popularNavigationView: some View {
@@ -135,6 +134,30 @@ struct AppTabNavigation: View {
.tag(TabSelection.trending) .tag(TabSelection.trending)
} }
private var playlistsNavigationView: some View {
NavigationView {
LazyView(PlaylistsView())
.toolbar { toolbarContent }
}
.tabItem {
Label("Playlists", systemImage: "list.and.film")
.accessibility(label: Text("Playlists"))
}
.tag(TabSelection.playlists)
}
private var searchNavigationView: some View {
NavigationView {
LazyView(SearchView())
.toolbar { toolbarContent }
}
.tabItem {
Label("Search", systemImage: "magnifyingglass")
.accessibility(label: Text("Search"))
}
.tag(TabSelection.search)
}
private var playerNavigationLink: some View { private var playerNavigationLink: some View {
NavigationLink(isActive: $player.playerNavigationLinkActive, destination: { NavigationLink(isActive: $player.playerNavigationLinkActive, destination: {
VideoPlayerView() VideoPlayerView()
@@ -144,6 +167,19 @@ struct AppTabNavigation: View {
} }
} }
private var videoPlayer: some View {
VideoPlayerView()
.environmentObject(accounts)
.environmentObject(comments)
.environmentObject(instances)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(playlists)
.environmentObject(recents)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
}
var toolbarContent: some ToolbarContent { var toolbarContent: some ToolbarContent {
#if os(iOS) #if os(iOS)
Group { Group {

View File

@@ -8,6 +8,7 @@ import SwiftUI
struct ContentView: View { struct ContentView: View {
@StateObject private var accounts = AccountsModel() @StateObject private var accounts = AccountsModel()
@StateObject private var comments = CommentsModel()
@StateObject private var instances = InstancesModel() @StateObject private var instances = InstancesModel()
@StateObject private var navigation = NavigationModel() @StateObject private var navigation = NavigationModel()
@StateObject private var player = PlayerModel() @StateObject private var player = PlayerModel()
@@ -40,6 +41,7 @@ struct ContentView: View {
.onAppear(perform: configure) .onAppear(perform: configure)
.environmentObject(accounts) .environmentObject(accounts)
.environmentObject(comments)
.environmentObject(instances) .environmentObject(instances)
.environmentObject(navigation) .environmentObject(navigation)
.environmentObject(player) .environmentObject(player)
@@ -58,20 +60,6 @@ struct ContentView: View {
.environmentObject(navigation) .environmentObject(navigation)
} }
) )
#if os(iOS)
.background(
EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) {
videoPlayer
}
)
#elseif os(macOS)
.background(
EmptyView().sheet(isPresented: $player.presentingPlayer) {
videoPlayer
.frame(minWidth: 900, minHeight: 800)
}
)
#endif
#if !os(tvOS) #if !os(tvOS)
.handlesExternalEvents(preferring: Set(["*"]), allowing: Set(["*"])) .handlesExternalEvents(preferring: Set(["*"]), allowing: Set(["*"]))
.onOpenURL(perform: handleOpenedURL) .onOpenURL(perform: handleOpenedURL)
@@ -98,21 +86,10 @@ struct ContentView: View {
#endif #endif
} }
private var videoPlayer: some View {
VideoPlayerView()
.environmentObject(accounts)
.environmentObject(instances)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(playlists)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
}
func configure() { func configure() {
SiestaLog.Category.enabled = .common SiestaLog.Category.enabled = .common
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared) SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
SDWebImageManager.defaultImageCache = PINCache(name: "net.yattee.app") SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
#if !os(macOS) #if !os(macOS)
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback) try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
#endif #endif
@@ -128,18 +105,37 @@ struct ContentView: View {
navigation.presentingWelcomeScreen = true navigation.presentingWelcomeScreen = true
} }
player.accounts = accounts
playlists.accounts = accounts playlists.accounts = accounts
search.accounts = accounts search.accounts = accounts
subscriptions.accounts = accounts subscriptions.accounts = accounts
comments.accounts = accounts
comments.player = player
menu.accounts = accounts menu.accounts = accounts
menu.navigation = navigation menu.navigation = navigation
menu.player = player menu.player = player
player.accounts = accounts
player.comments = comments
if !accounts.current.isNil { if !accounts.current.isNil {
player.loadHistoryDetails() player.loadHistoryDetails()
} }
if !Defaults[.saveRecents] {
recents.clear()
}
var section = Defaults[.visibleSections].min()?.tabSelection
#if os(macOS)
if section == .playlists {
section = .search
}
#endif
navigation.tabSelection = section ?? .search
} }
func openWelcomeScreenIfAccountEmpty() { func openWelcomeScreenIfAccountEmpty() {

View File

@@ -1,3 +1,4 @@
import Defaults
import SwiftUI import SwiftUI
struct Sidebar: View { struct Sidebar: View {
@@ -6,6 +7,8 @@ struct Sidebar: View {
@EnvironmentObject<PlaylistsModel> private var playlists @EnvironmentObject<PlaylistsModel> private var playlists
@EnvironmentObject<SubscriptionsModel> private var subscriptions @EnvironmentObject<SubscriptionsModel> private var subscriptions
@Default(.visibleSections) private var visibleSections
var body: some View { var body: some View {
ScrollViewReader { scrollView in ScrollViewReader { scrollView in
List { List {
@@ -16,11 +19,11 @@ struct Sidebar: View {
.id("recentlyOpened") .id("recentlyOpened")
if accounts.api.signedIn { if accounts.api.signedIn {
if accounts.app.supportsSubscriptions { if visibleSections.contains(.subscriptions), accounts.app.supportsSubscriptions {
AppSidebarSubscriptions() AppSidebarSubscriptions()
} }
if accounts.app.supportsUserPlaylists { if visibleSections.contains(.playlists), accounts.app.supportsUserPlaylists {
AppSidebarPlaylists() AppSidebarPlaylists()
} }
} }
@@ -47,27 +50,33 @@ struct Sidebar: View {
var mainNavigationLinks: some View { var mainNavigationLinks: some View {
Section(header: Text("Videos")) { Section(header: Text("Videos")) {
NavigationLink(destination: LazyView(FavoritesView()), tag: TabSelection.favorites, selection: $navigation.tabSelection) { if visibleSections.contains(.favorites) {
Label("Favorites", systemImage: "heart") NavigationLink(destination: LazyView(FavoritesView()), tag: TabSelection.favorites, selection: $navigation.tabSelection) {
.accessibility(label: Text("Favorites")) Label("Favorites", systemImage: "heart")
.accessibility(label: Text("Favorites"))
}
} }
if accounts.app.supportsSubscriptions && accounts.signedIn { if visibleSections.contains(.subscriptions),
accounts.app.supportsSubscriptions && accounts.signedIn
{
NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: $navigation.tabSelection) { NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: $navigation.tabSelection) {
Label("Subscriptions", systemImage: "star.circle") Label("Subscriptions", systemImage: "star.circle")
.accessibility(label: Text("Subscriptions")) .accessibility(label: Text("Subscriptions"))
} }
} }
if accounts.app.supportsPopular { if visibleSections.contains(.popular), accounts.app.supportsPopular {
NavigationLink(destination: LazyView(PopularView()), tag: TabSelection.popular, selection: $navigation.tabSelection) { NavigationLink(destination: LazyView(PopularView()), tag: TabSelection.popular, selection: $navigation.tabSelection) {
Label("Popular", systemImage: "arrow.up.right.circle") Label("Popular", systemImage: "arrow.up.right.circle")
.accessibility(label: Text("Popular")) .accessibility(label: Text("Popular"))
} }
} }
NavigationLink(destination: LazyView(TrendingView()), tag: TabSelection.trending, selection: $navigation.tabSelection) { if visibleSections.contains(.trending) {
Label("Trending", systemImage: "chart.bar") NavigationLink(destination: LazyView(TrendingView()), tag: TabSelection.trending, selection: $navigation.tabSelection) {
.accessibility(label: Text("Trending")) Label("Trending", systemImage: "chart.bar")
.accessibility(label: Text("Trending"))
}
} }
NavigationLink(destination: LazyView(SearchView()), tag: TabSelection.search, selection: $navigation.tabSelection) { NavigationLink(destination: LazyView(SearchView()), tag: TabSelection.search, selection: $navigation.tabSelection) {
@@ -78,7 +87,7 @@ struct Sidebar: View {
} }
} }
func scrollScrollViewToItem(scrollView: ScrollViewProxy, for selection: TabSelection) { private func scrollScrollViewToItem(scrollView: ScrollViewProxy, for selection: TabSelection) {
if case .recentlyOpened = selection { if case .recentlyOpened = selection {
scrollView.scrollTo("recentlyOpened") scrollView.scrollTo("recentlyOpened")
} else if case let .playlist(id) = selection { } else if case let .playlist(id) = selection {

View File

@@ -0,0 +1,251 @@
import SDWebImageSwiftUI
import SwiftUI
struct CommentView: View {
let comment: Comment
@Binding var repliesID: Comment.ID?
#if os(iOS)
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
#endif
@Environment(\.navigationStyle) private var navigationStyle
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<RecentsModel> private var recents
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .center, spacing: 10) {
authorAvatar
#if os(iOS)
Group {
if horizontalSizeClass == .regular {
HStack(spacing: 20) {
authorAndTime
Spacer()
Group {
statusIcons
likes
}
}
} else {
HStack(alignment: .center, spacing: 20) {
authorAndTime
Spacer()
VStack(alignment: .trailing, spacing: 8) {
likes
statusIcons
}
}
}
}
.font(.system(size: 15))
#else
HStack(spacing: 20) {
authorAndTime
Spacer()
statusIcons
likes
}
#endif
}
Group {
commentText
if comment.hasReplies {
HStack(spacing: repliesButtonStackSpacing) {
repliesButton
ProgressView()
.scaleEffect(progressViewScale, anchor: .center)
.opacity(repliesID == comment.id && !comments.repliesLoaded ? 1 : 0)
.frame(maxHeight: 0)
}
if comment.id == repliesID {
repliesList
}
}
}
}
#if os(tvOS)
.padding(.horizontal, 20)
#endif
}
private var authorAvatar: some View {
WebImage(url: URL(string: comment.authorAvatarURL)!)
.resizable()
.placeholder {
Rectangle().fill(Color("PlaceholderColor"))
}
.retryOnAppear(false)
.indicator(.activity)
.mask(RoundedRectangle(cornerRadius: 60))
.frame(width: 45, height: 45, alignment: .leading)
.contextMenu {
Button(action: openChannelAction) {
Label("\(comment.channel.name) Channel", systemImage: "rectangle.stack.fill.badge.person.crop")
}
}
#if os(tvOS)
.focusable()
#endif
}
private var authorAndTime: some View {
VStack(alignment: .leading) {
Text(comment.author)
.fontWeight(.bold)
Text(comment.time)
.foregroundColor(.secondary)
}
.lineLimit(1)
}
private var statusIcons: some View {
HStack(spacing: 15) {
if comment.pinned {
Image(systemName: "pin.fill")
}
if comment.hearted {
Image(systemName: "heart.fill")
}
}
.foregroundColor(.secondary)
}
private var likes: some View {
Group {
if comment.likeCount > 0 {
HStack(spacing: 5) {
Image(systemName: "hand.thumbsup")
Text("\(comment.likeCount.formattedAsAbbreviation())")
}
}
}
.foregroundColor(.secondary)
}
private var repliesButton: some View {
Button {
repliesID = repliesID == comment.id ? nil : comment.id
guard !repliesID.isNil, !comment.repliesPage.isNil else {
return
}
comments.loadReplies(page: comment.repliesPage!)
} label: {
HStack(spacing: 5) {
Image(systemName: repliesID == comment.id ? "arrow.turn.left.up" : "arrow.turn.right.down")
Text("Replies")
}
#if os(tvOS)
.padding(10)
#endif
}
.buttonStyle(.plain)
.padding(.vertical, 2)
#if os(tvOS)
.padding(.leading, 5)
#else
.foregroundColor(.secondary)
#endif
}
private var repliesButtonStackSpacing: Double {
#if os(tvOS)
24
#elseif os(iOS)
4
#else
2
#endif
}
private var progressViewScale: Double {
#if os(macOS)
0.4
#else
0.8
#endif
}
private var repliesList: some View {
Group {
let last = comments.replies.last
ForEach(comments.replies) { comment in
CommentView(comment: comment, repliesID: $repliesID)
#if os(tvOS)
.focusable()
#endif
if comment != last {
Divider()
.padding(.vertical, 5)
}
}
}
.padding(.leading, 22)
}
private var commentText: some View {
Group {
let text = Text(comment.text)
#if os(macOS)
.font(.system(size: 14))
#elseif os(iOS)
.font(.system(size: 15))
#endif
.lineSpacing(3)
.fixedSize(horizontal: false, vertical: true)
if #available(iOS 15.0, macOS 12.0, *) {
text
#if !os(tvOS)
.textSelection(.enabled)
#endif
} else {
text
}
}
}
private func openChannelAction() {
player.presentingPlayer = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let recent = RecentItem(from: comment.channel)
recents.add(recent)
navigation.presentingChannel = true
if navigationStyle == .sidebar {
navigation.sidebarSectionChanged.toggle()
navigation.tabSelection = .recentlyOpened(recent.tag)
}
}
}
}
struct CommentView_Previews: PreviewProvider {
static var fixture: Comment {
Comment.fixture
}
static var previews: some View {
CommentView(comment: fixture, repliesID: .constant(fixture.id))
}
}

View File

@@ -0,0 +1,91 @@
import SwiftUI
struct CommentsView: View {
@State private var repliesID: Comment.ID?
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<PlayerModel> private var player
var body: some View {
Group {
if comments.disabled {
Text("Comments are disabled for this video")
.foregroundColor(.secondary)
} else if comments.loaded && comments.all.isEmpty {
Text("No comments")
.foregroundColor(.secondary)
} else if !comments.loaded {
progressView
} else {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
let last = comments.all.last
ForEach(comments.all) { comment in
CommentView(comment: comment, repliesID: $repliesID)
if comment != last {
Divider()
.padding(.vertical, 5)
}
}
HStack {
if comments.nextPageAvailable {
Button {
repliesID = nil
comments.loadNextPage()
} label: {
Label("Show more", systemImage: "arrow.turn.down.right")
}
}
if !comments.firstPage {
Button {
repliesID = nil
comments.load(page: nil)
} label: {
Label("Show first", systemImage: "arrow.turn.down.left")
}
}
}
.buttonStyle(.plain)
.padding(.vertical, 8)
.foregroundColor(.secondary)
}
}
}
}
.padding(.horizontal)
.onAppear {
if !comments.loaded {
comments.load()
}
}
}
private var progressView: some View {
VStack {
Spacer()
HStack {
Spacer()
ProgressView()
Spacer()
}
Spacer()
}
}
}
struct CommentsView_Previews: PreviewProvider {
static var previews: some View {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
CommentsView()
.previewInterfaceOrientation(.landscapeRight)
.injectFixtureEnvironmentObjects()
}
CommentsView()
.injectFixtureEnvironmentObjects()
}
}

View File

@@ -2,6 +2,7 @@ import Defaults
import SwiftUI import SwiftUI
struct Player: UIViewControllerRepresentable { struct Player: UIViewControllerRepresentable {
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<NavigationModel> private var navigation @EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@@ -18,6 +19,7 @@ struct Player: UIViewControllerRepresentable {
let controller = PlayerViewController() let controller = PlayerViewController()
controller.commentsModel = comments
controller.navigationModel = navigation controller.navigationModel = navigation
controller.playerModel = player controller.playerModel = player
player.controller = controller player.controller = controller

View File

@@ -44,18 +44,11 @@ struct PlayerQueueView: View {
} }
ForEach(player.queue) { item in ForEach(player.queue) { item in
let row = PlayerQueueRow(item: item, fullScreen: $fullScreen) PlayerQueueRow(item: item, fullScreen: $fullScreen)
.contextMenu { .contextMenu {
removeButton(item, history: false) removeButton(item, history: false)
removeAllButton(history: false) removeAllButton(history: false)
} }
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
row.swipeActions(edge: .trailing, allowsFullSwipe: true) {
removeButton(item, history: false)
}
} else {
row
}
} }
} }
} }
@@ -65,20 +58,11 @@ struct PlayerQueueView: View {
if !player.history.isEmpty { if !player.history.isEmpty {
Section(header: Text("Played Previously")) { Section(header: Text("Played Previously")) {
ForEach(player.history) { item in ForEach(player.history) { item in
let row = PlayerQueueRow(item: item, history: true, fullScreen: $fullScreen) PlayerQueueRow(item: item, history: true, fullScreen: $fullScreen)
.contextMenu { .contextMenu {
removeButton(item, history: true) removeButton(item, history: true)
removeAllButton(history: true) removeAllButton(history: true)
} }
#if os(iOS)
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
row.swipeActions(edge: .trailing, allowsFullSwipe: true) {
removeButton(item, history: true)
}
} else {
row
}
#endif
} }
} }
} }
@@ -106,18 +90,10 @@ struct PlayerQueueView: View {
} }
private func removeButton(_ item: PlayerQueueItem, history: Bool) -> some View { private func removeButton(_ item: PlayerQueueItem, history: Bool) -> some View {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { Button {
return Button(role: .destructive) { removeButtonAction(item, history: history)
removeButtonAction(item, history: history) } label: {
} label: { Label("Remove", systemImage: "trash")
Label("Remove", systemImage: "trash")
}
} else {
return Button {
removeButtonAction(item, history: history)
} label: {
Label("Remove", systemImage: "trash")
}
} }
} }
@@ -126,18 +102,10 @@ struct PlayerQueueView: View {
} }
private func removeAllButton(history: Bool) -> some View { private func removeAllButton(history: Bool) -> some View {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { Button {
return Button(role: .destructive) { removeAllButtonAction(history: history)
removeAllButtonAction(history: history) } label: {
} label: { Label("Remove All", systemImage: "trash.fill")
Label("Remove All", systemImage: "trash.fill")
}
} else {
return Button {
removeAllButtonAction(history: history)
} label: {
Label("Remove All", systemImage: "trash.fill")
}
} }
} }

View File

@@ -4,6 +4,7 @@ import SwiftUI
final class PlayerViewController: UIViewController { final class PlayerViewController: UIViewController {
var playerLoaded = false var playerLoaded = false
var commentsModel: CommentsModel!
var navigationModel: NavigationModel! var navigationModel: NavigationModel!
var playerModel: PlayerModel! var playerModel: PlayerModel!
var playerViewController = AVPlayerViewController() var playerViewController = AVPlayerViewController()
@@ -44,10 +45,16 @@ final class PlayerViewController: UIViewController {
#if os(tvOS) #if os(tvOS)
playerModel.avPlayerViewController = playerViewController playerModel.avPlayerViewController = playerViewController
playerViewController.customInfoViewControllers = [ var infoViewControllers = [UIHostingController<AnyView>]()
if CommentsModel.enabled {
infoViewControllers.append(infoViewController([.comments], title: "Comments"))
}
infoViewControllers.append(contentsOf: [
infoViewController([.related], title: "Related"), infoViewController([.related], title: "Related"),
infoViewController([.playingNext, .playedPreviously], title: "Playing Next") infoViewController([.playingNext, .playedPreviously], title: "Playing Next")
] ])
playerViewController.customInfoViewControllers = infoViewControllers
#else #else
embedViewController() embedViewController()
#endif #endif
@@ -62,6 +69,7 @@ final class PlayerViewController: UIViewController {
AnyView( AnyView(
NowPlayingView(sections: sections, inInfoViewController: true) NowPlayingView(sections: sections, inInfoViewController: true)
.frame(maxHeight: 600) .frame(maxHeight: 600)
.environmentObject(commentsModel)
.environmentObject(playerModel) .environmentObject(playerModel)
) )
) )

View File

@@ -9,6 +9,18 @@ struct RelatedView: View {
Section(header: Text("Related")) { Section(header: Text("Related")) {
ForEach(player.currentVideo!.related) { video in ForEach(player.currentVideo!.related) { video in
PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: .constant(false)) PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: .constant(false))
.contextMenu {
Button {
player.playNext(video)
} label: {
Label("Play Next", systemImage: "text.insert")
}
Button {
player.enqueueVideo(video)
} label: {
Label("Play Last", systemImage: "text.append")
}
}
} }
} }
} }

View File

@@ -4,7 +4,7 @@ import SwiftUI
struct VideoDetails: View { struct VideoDetails: View {
enum Page { enum Page {
case details, queue, related case info, queue, related, comments
} }
@Binding var sidebarQueue: Bool @Binding var sidebarQueue: Bool
@@ -16,7 +16,7 @@ struct VideoDetails: View {
@State private var presentingShareSheet = false @State private var presentingShareSheet = false
@State private var shareURL: URL? @State private var shareURL: URL?
@State private var currentPage = Page.details @State private var currentPage = Page.info
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
@Environment(\.inNavigationView) private var inNavigationView @Environment(\.inNavigationView) private var inNavigationView
@@ -65,7 +65,7 @@ struct VideoDetails: View {
} }
.padding(.horizontal) .padding(.horizontal)
if !sidebarQueue { if CommentsModel.enabled, CommentsModel.placement == .separate {
pagePicker pagePicker
.padding(.horizontal) .padding(.horizontal)
} }
@@ -89,7 +89,7 @@ struct VideoDetails: View {
) )
switch currentPage { switch currentPage {
case .details: case .info:
ScrollView(.vertical) { ScrollView(.vertical) {
detailsPage detailsPage
} }
@@ -100,6 +100,9 @@ struct VideoDetails: View {
case .related: case .related:
RelatedView() RelatedView()
.edgesIgnoringSafeArea(.horizontal) .edgesIgnoringSafeArea(.horizontal)
case .comments:
CommentsView()
.edgesIgnoringSafeArea(.horizontal)
} }
} }
.padding(.top, inNavigationView && fullScreen ? 10 : 0) .padding(.top, inNavigationView && fullScreen ? 10 : 0)
@@ -116,7 +119,7 @@ struct VideoDetails: View {
.onChange(of: sidebarQueue) { queue in .onChange(of: sidebarQueue) { queue in
if queue { if queue {
if currentPage == .queue { if currentPage == .queue {
currentPage = .details currentPage = .info
} }
} else if video.isNil { } else if video.isNil {
currentPage = .queue currentPage = .queue
@@ -131,7 +134,20 @@ struct VideoDetails: View {
if video != nil { if video != nil {
Text(video!.title) Text(video!.title)
.onAppear { .onAppear {
currentPage = .details currentPage = .info
}
.contextMenu {
Button {
player.closeCurrentItem()
if !sidebarQueue {
currentPage = .queue
} else {
currentPage = .info
}
} label: {
Label("Close Video", systemImage: "xmark.circle")
}
.disabled(player.currentItem.isNil)
} }
.font(.title2.bold()) .font(.title2.bold())
@@ -228,15 +244,23 @@ struct VideoDetails: View {
var pagePicker: some View { var pagePicker: some View {
Picker("Page", selection: $currentPage) { Picker("Page", selection: $currentPage) {
if !video.isNil { if !video.isNil {
Text("Details").tag(Page.details) Text("Info").tag(Page.info)
Text("Related").tag(Page.related) if CommentsModel.enabled, CommentsModel.placement == .separate {
Text("Comments")
.tag(Page.comments)
}
if !sidebarQueue {
Text("Related").tag(Page.related)
}
}
if !sidebarQueue {
Text("Queue").tag(Page.queue)
} }
Text("Queue").tag(Page.queue)
} }
.labelsHidden() .labelsHidden()
.pickerStyle(.segmented) .pickerStyle(.segmented)
.onDisappear { .onDisappear {
currentPage = .details currentPage = .info
} }
} }
@@ -286,19 +310,19 @@ struct VideoDetails: View {
Spacer() Spacer()
if let views = video.viewsCount { if let views = video.viewsCount {
videoDetail(label: "Views", value: views, symbol: "eye.fill") videoDetail(label: "Views", value: views, symbol: "eye")
} }
if let likes = video.likesCount { if let likes = video.likesCount {
Divider() Divider()
videoDetail(label: "Likes", value: likes, symbol: "hand.thumbsup.circle.fill") videoDetail(label: "Likes", value: likes, symbol: "hand.thumbsup")
} }
if let dislikes = video.dislikesCount { if let dislikes = video.dislikesCount {
Divider() Divider()
videoDetail(label: "Dislikes", value: dislikes, symbol: "hand.thumbsdown.circle.fill") videoDetail(label: "Dislikes", value: dislikes, symbol: "hand.thumbsdown")
} }
Spacer() Spacer()
@@ -342,64 +366,81 @@ struct VideoDetails: View {
var detailsPage: some View { var detailsPage: some View {
Group { Group {
if let video = player.currentItem?.video { Group {
Group { if let video = player.currentItem?.video {
HStack { Group {
publishedDateSection HStack {
Spacer() publishedDateSection
Spacer()
}
Divider()
countsSection
} }
Divider() Divider()
countsSection VStack(alignment: .leading, spacing: 10) {
} if let description = video.description {
Group {
Divider() if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
Text(description)
VStack(alignment: .leading, spacing: 10) { .textSelection(.enabled)
if let description = video.description { } else {
Group { Text(description)
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
Text(description)
.textSelection(.enabled)
} else {
Text(description)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.font(.caption)
.padding(.bottom, 4)
} else {
Text("No description")
.foregroundColor(.secondary)
}
if showKeywords {
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
HStack {
ForEach(video.keywords, id: \.self) { keyword in
HStack(alignment: .center, spacing: 0) {
Text("#")
.font(.system(size: 11).bold())
Text(keyword)
.frame(maxWidth: 500)
}
.font(.caption)
.foregroundColor(.white)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(Color("VideoDetailLikesSymbolColor"))
.mask(RoundedRectangle(cornerRadius: 3))
} }
} }
.padding(.bottom, 10) .frame(maxWidth: .infinity, alignment: .leading)
.font(.system(size: 14))
.lineSpacing(3)
} else {
Text("No description")
.foregroundColor(.secondary)
}
if showKeywords {
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
HStack {
ForEach(video.keywords, id: \.self) { keyword in
HStack(alignment: .center, spacing: 0) {
Text("#")
.font(.system(size: 11).bold())
Text(keyword)
.frame(maxWidth: 500)
}
.font(.caption)
.foregroundColor(.white)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(Color("VideoDetailLikesSymbolColor"))
.mask(RoundedRectangle(cornerRadius: 3))
}
}
.padding(.bottom, 10)
}
} }
} }
} }
if !video.isNil, CommentsModel.placement == .info {
Divider()
#if os(macOS)
.padding(.bottom, 20)
#else
.padding(.vertical, 10)
#endif
}
}
.padding(.horizontal)
Group {
if !video.isNil, CommentsModel.placement == .info {
CommentsView()
}
} }
} }
.padding(.horizontal)
} }
func videoDetail(label: String, value: String, symbol: String) -> some View { func videoDetail(label: String, value: String, symbol: String) -> some View {

View File

@@ -105,8 +105,9 @@ struct VideoPlayerView: View {
} }
#endif #endif
} }
.background(colorScheme == .dark ? Color.black : Color.white)
#if os(macOS) #if os(macOS)
.frame(minWidth: 650) .frame(minWidth: 650)
#endif #endif
#if os(iOS) #if os(iOS)
if sidebarQueue { if sidebarQueue {
@@ -116,7 +117,7 @@ struct VideoPlayerView: View {
#elseif os(macOS) #elseif os(macOS)
if Defaults[.playerSidebar] != .never { if Defaults[.playerSidebar] != .never {
PlayerQueueView(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreen) PlayerQueueView(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreen)
.frame(minWidth: 250) .frame(minWidth: 300)
} }
#endif #endif
} }

View File

@@ -7,6 +7,7 @@ struct AddToPlaylistView: View {
@State private var selectedPlaylistID: Playlist.ID = "" @State private var selectedPlaylistID: Playlist.ID = ""
@Environment(\.colorScheme) private var colorScheme
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
@EnvironmentObject<PlaylistsModel> private var model @EnvironmentObject<PlaylistsModel> private var model
@@ -37,7 +38,7 @@ struct AddToPlaylistView: View {
.padding(.vertical) .padding(.vertical)
#elseif os(tvOS) #elseif os(tvOS)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.tertiaryBackground) .background(Color.background(scheme: colorScheme))
#else #else
.padding(.vertical) .padding(.vertical)
#endif #endif

View File

@@ -10,6 +10,7 @@ struct PlaylistFormView: View {
@State private var valid = false @State private var valid = false
@State private var showingDeleteConfirmation = false @State private var showingDeleteConfirmation = false
@Environment(\.colorScheme) private var colorScheme
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@@ -77,7 +78,7 @@ struct PlaylistFormView: View {
.frame(maxWidth: 1000) .frame(maxWidth: 1000)
} }
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.tertiaryBackground) .background(Color.background(scheme: colorScheme))
#endif #endif
} }
.onChange(of: name) { _ in validate() } .onChange(of: name) { _ in validate() }

View File

@@ -6,6 +6,12 @@ struct SearchTextField: View {
@EnvironmentObject<RecentsModel> private var recents @EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> private var state @EnvironmentObject<SearchModel> private var state
@Binding var favoriteItem: FavoriteItem?
init(favoriteItem: Binding<FavoriteItem?>? = nil) {
_favoriteItem = favoriteItem ?? .constant(nil)
}
var body: some View { var body: some View {
ZStack { ZStack {
#if os(macOS) #if os(macOS)
@@ -31,14 +37,27 @@ struct SearchTextField: View {
} }
} }
#if os(macOS) #if os(macOS)
.frame(maxWidth: 190)
.textFieldStyle(.plain) .textFieldStyle(.plain)
#else #else
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.padding(.leading) .padding(.leading)
.padding(.trailing, 15) .padding(.trailing, 15)
#endif #endif
if !self.state.queryText.isEmpty { if !self.state.queryText.isEmpty {
#if os(iOS)
FavoriteButton(item: favoriteItem)
.id(favoriteItem?.id)
.labelStyle(.iconOnly)
.padding(.trailing)
#endif
clearButton clearButton
} else {
#if os(macOS)
clearButton
.opacity(0)
#endif
} }
} }
} }

View File

@@ -14,8 +14,9 @@ struct SearchSuggestions: View {
recents.addQuery(state.queryText) recents.addQuery(state.queryText)
} label: { } label: {
HStack(spacing: 5) { HStack {
Label(state.queryText, systemImage: "magnifyingglass") Image(systemName: "magnifyingglass")
Text(state.queryText)
.lineLimit(1) .lineLimit(1)
} }
} }
@@ -27,15 +28,19 @@ struct SearchSuggestions: View {
Button { Button {
state.queryText = suggestion state.queryText = suggestion
} label: { } label: {
HStack(spacing: 0) { HStack {
Label(state.queryText, systemImage: "arrow.up.left.circle") Image(systemName: "arrow.up.left.circle")
.lineLimit(1)
.layoutPriority(2)
.foregroundColor(.secondary) .foregroundColor(.secondary)
HStack(spacing: 0) {
Text(state.suggestionsText)
.lineLimit(1)
.layoutPriority(2)
.foregroundColor(.secondary)
Text(querySuffix(suggestion)) Text(querySuffix(suggestion))
.lineLimit(1) .lineLimit(1)
.layoutPriority(1) .layoutPriority(1)
}
} }
} }
#if os(macOS) #if os(macOS)
@@ -55,7 +60,7 @@ struct SearchSuggestions: View {
} }
private func querySuffix(_ suggestion: String) -> String { private func querySuffix(_ suggestion: String) -> String {
suggestion.replacingFirstOccurrence(of: state.queryText.lowercased(), with: "") suggestion.replacingFirstOccurrence(of: state.suggestionsText.lowercased(), with: "")
} }
#if os(macOS) #if os(macOS)

View File

@@ -23,6 +23,9 @@ struct SearchView: View {
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<RecentsModel> private var recents @EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> private var state @EnvironmentObject<SearchModel> private var state
private var favorites = FavoritesModel.shared
@Default(.saveRecents) private var saveRecents
private var videos = [Video]() private var videos = [Video]()
@@ -39,7 +42,7 @@ struct SearchView: View {
PlayerControlsView { PlayerControlsView {
#if os(iOS) #if os(iOS)
VStack { VStack {
SearchTextField() SearchTextField(favoriteItem: $favoriteItem)
if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty { if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty {
SearchSuggestions() SearchSuggestions()
@@ -51,14 +54,16 @@ struct SearchView: View {
ZStack { ZStack {
results results
if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty { #if !os(tvOS)
HStack { if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty {
Spacer() HStack {
SearchSuggestions() Spacer()
.borderLeading(width: 1, color: Color("ControlsBorderColor")) SearchSuggestions()
.frame(maxWidth: 280) .borderLeading(width: 1, color: Color("ControlsBorderColor"))
.frame(maxWidth: 280)
}
} }
} #endif
} }
#endif #endif
} }
@@ -66,10 +71,8 @@ struct SearchView: View {
#if !os(tvOS) #if !os(tvOS)
ToolbarItemGroup(placement: toolbarPlacement) { ToolbarItemGroup(placement: toolbarPlacement) {
#if os(macOS) #if os(macOS)
if let favoriteItem = favoriteItem { FavoriteButton(item: favoriteItem)
FavoriteButton(item: favoriteItem) .id(favoriteItem?.id)
.id(favoriteItem.id)
}
#endif #endif
if accounts.app.supportsSearchFilters { if accounts.app.supportsSearchFilters {
@@ -90,17 +93,6 @@ struct SearchView: View {
.transaction { t in t.animation = .none } .transaction { t in t.animation = .none }
} }
#if os(iOS)
Spacer()
if let favoriteItem = favoriteItem {
FavoriteButton(item: favoriteItem)
.id(favoriteItem.id)
}
Spacer()
#endif
if accounts.app.supportsSearchFilters { if accounts.app.supportsSearchFilters {
filtersMenu filtersMenu
} }
@@ -171,12 +163,20 @@ struct SearchView: View {
updateFavoriteItem() updateFavoriteItem()
} }
} }
#if !os(tvOS) #if os(tvOS)
.ignoresSafeArea(.keyboard, edges: .bottom) .searchable(text: $state.queryText) {
.navigationTitle("Search") ForEach(state.querySuggestions.collection, id: \.self) { suggestion in
Text(suggestion)
.searchCompletion(suggestion)
}
}
#else
.ignoresSafeArea(.keyboard, edges: .bottom)
.navigationTitle("Search")
#endif #endif
#if os(iOS) #if os(iOS)
.navigationBarHidden(true) .navigationBarHidden(!Defaults[.visibleSections].isEmpty || navigationStyle == .sidebar)
.navigationBarTitleDisplayMode(.inline)
#endif #endif
} }
@@ -192,12 +192,10 @@ struct SearchView: View {
filtersHorizontalStack filtersHorizontalStack
} }
if let favoriteItem = favoriteItem { FavoriteButton(item: favoriteItem)
FavoriteButton(item: favoriteItem) .id(favoriteItem?.id)
.id(favoriteItem.id) .labelStyle(.iconOnly)
.labelStyle(.iconOnly) .font(.system(size: 25))
.font(.system(size: 25))
}
} }
HorizontalCells(items: items) HorizontalCells(items: items)
@@ -222,14 +220,14 @@ struct SearchView: View {
private var toolbarPlacement: ToolbarItemPlacement { private var toolbarPlacement: ToolbarItemPlacement {
#if os(iOS) #if os(iOS)
.bottomBar accounts.app.supportsSearchFilters || favorites.isEnabled ? .bottomBar : .automatic
#else #else
.automatic .automatic
#endif #endif
} }
private var showRecentQueries: Bool { private var showRecentQueries: Bool {
navigationStyle == .tab && state.queryText.isEmpty navigationStyle == .tab && saveRecents && state.queryText.isEmpty
} }
private var filtersActive: Bool { private var filtersActive: Bool {

View File

@@ -15,6 +15,7 @@ struct AccountForm: View {
@State private var validationError: String? @State private var validationError: String?
@State private var validationDebounce = Debounce() @State private var validationDebounce = Debounce()
@Environment(\.colorScheme) private var colorScheme
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
var body: some View { var body: some View {
@@ -30,7 +31,7 @@ struct AccountForm: View {
.padding(.vertical) .padding(.vertical)
#elseif os(tvOS) #elseif os(tvOS)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.tertiaryBackground) .background(Color.background(scheme: colorScheme))
#else #else
.frame(width: 400, height: 145) .frame(width: 400, height: 145)
#endif #endif

View File

@@ -4,42 +4,102 @@ import SwiftUI
struct BrowsingSettings: View { struct BrowsingSettings: View {
@Default(.channelOnThumbnail) private var channelOnThumbnail @Default(.channelOnThumbnail) private var channelOnThumbnail
@Default(.timeOnThumbnail) private var timeOnThumbnail @Default(.timeOnThumbnail) private var timeOnThumbnail
#if os(iOS) @Default(.saveRecents) private var saveRecents
@Default(.tabNavigationSection) private var tabNavigationSection @Default(.saveHistory) private var saveHistory
#endif @Default(.visibleSections) private var visibleSections
var body: some View { var body: some View {
Section(header: SettingsHeader(text: "Browsing"), footer: footer) { Group {
Toggle("Display channel names on thumbnails", isOn: $channelOnThumbnail) Section(header: SettingsHeader(text: "Browsing")) {
Toggle("Display video length on thumbnails", isOn: $timeOnThumbnail) Toggle("Show channel name on thumbnail", isOn: $channelOnThumbnail)
Toggle("Show video length on thumbnail", isOn: $timeOnThumbnail)
Toggle("Save recent queries and channels", isOn: $saveRecents)
Toggle("Save history of played videos", isOn: $saveHistory)
}
Section(header: SettingsHeader(text: "Sections")) {
#if os(macOS)
let list = ForEach(VisibleSection.allCases, id: \.self) { section in
VisibleSectionSelectionRow(
title: section.title,
selected: visibleSections.contains(section)
) { value in
toggleSection(section, value: value)
}
}
#if os(iOS) Group {
preferredTabPicker if #available(macOS 12.0, *) {
#endif list
} .listStyle(.inset(alternatesRowBackgrounds: true))
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) } else {
list
.listStyle(.inset)
}
#if os(macOS) Spacer()
Spacer() }
#endif #else
} ForEach(VisibleSection.allCases, id: \.self) { section in
VisibleSectionSelectionRow(
var footer: some View { title: section.title,
#if os(iOS) selected: visibleSections.contains(section)
Text("This tab will be displayed when there is no space to display all tabs") ) { value in
#else toggleSection(section, value: value)
EmptyView() }
#endif }
} #endif
#if os(iOS)
var preferredTabPicker: some View {
Picker("Preferred tab", selection: $tabNavigationSection) {
Text("Trending").tag(TabNavigationSectionSetting.trending)
Text("Popular").tag(TabNavigationSectionSetting.popular)
} }
} }
#endif .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
func toggleSection(_ section: VisibleSection, value: Bool) {
if value {
visibleSections.insert(section)
} else {
visibleSections.remove(section)
}
}
struct VisibleSectionSelectionRow: View {
let title: String
let selected: Bool
var action: (Bool) -> Void
@State private var toggleChecked = false
var body: some View {
Button(action: { action(!selected) }) {
HStack {
#if os(macOS)
Toggle(isOn: $toggleChecked) {
Text(self.title)
Spacer()
}
.onAppear {
toggleChecked = selected
}
.onChange(of: toggleChecked) { new in
action(new)
}
#else
Text(self.title)
Spacer()
if selected {
Image(systemName: "checkmark")
#if os(iOS)
.foregroundColor(.accentColor)
#endif
}
#endif
}
.contentShape(Rectangle())
}
#if !os(tvOS)
.buttonStyle(.plain)
#endif
}
}
} }
struct BrowsingSettings_Previews: PreviewProvider { struct BrowsingSettings_Previews: PreviewProvider {

View File

@@ -13,6 +13,7 @@ struct InstanceForm: View {
@State private var validationError: String? @State private var validationError: String?
@State private var validationDebounce = Debounce() @State private var validationDebounce = Debounce()
@Environment(\.colorScheme) private var colorScheme
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
var body: some View { var body: some View {
@@ -32,7 +33,7 @@ struct InstanceForm: View {
.padding(.vertical) .padding(.vertical)
#elseif os(tvOS) #elseif os(tvOS)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.tertiaryBackground) .background(Color.background(scheme: colorScheme))
#else #else
.frame(width: 400, height: 190) .frame(width: 400, height: 190)
#endif #endif
@@ -76,6 +77,7 @@ struct InstanceForm: View {
} }
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
.labelsHidden()
TextField("Name", text: $name) TextField("Name", text: $name)

View File

@@ -27,7 +27,6 @@ struct PlaybackSettings: View {
} }
keywordsToggle keywordsToggle
saveHistoryToggle
} }
#else #else
Section(header: SettingsHeader(text: "Source")) { Section(header: SettingsHeader(text: "Source")) {
@@ -45,7 +44,6 @@ struct PlaybackSettings: View {
#endif #endif
keywordsToggle keywordsToggle
saveHistoryToggle
#endif #endif
} }
@@ -59,7 +57,7 @@ struct PlaybackSettings: View {
Text("Best available stream").tag(String?.none) Text("Best available stream").tag(String?.none)
ForEach(instances) { instance in ForEach(instances) { instance in
Text(instance.longDescription).tag(Optional(instance.id)) Text(instance.description).tag(Optional(instance.id))
} }
} }
.labelsHidden() .labelsHidden()
@@ -109,10 +107,6 @@ struct PlaybackSettings: View {
private var keywordsToggle: some View { private var keywordsToggle: some View {
Toggle("Show video keywords", isOn: $showKeywords) Toggle("Show video keywords", isOn: $showKeywords)
} }
private var saveHistoryToggle: some View {
Toggle("Save history of played videos", isOn: $saveHistory)
}
} }
struct PlaybackSettings_Previews: PreviewProvider { struct PlaybackSettings_Previews: PreviewProvider {

View File

@@ -4,8 +4,21 @@ import SwiftUI
struct ServicesSettings: View { struct ServicesSettings: View {
@Default(.sponsorBlockInstance) private var sponsorBlockInstance @Default(.sponsorBlockInstance) private var sponsorBlockInstance
@Default(.sponsorBlockCategories) private var sponsorBlockCategories @Default(.sponsorBlockCategories) private var sponsorBlockCategories
@Default(.commentsInstanceID) private var commentsInstanceID
#if !os(tvOS)
@Default(.commentsPlacement) private var commentsPlacement
#endif
var body: some View { var body: some View {
Section(header: SettingsHeader(text: "Comments")) {
commentsInstancePicker
#if !os(tvOS)
commentsPlacementPicker
.disabled(!CommentsModel.enabled)
#endif
}
Section(header: SettingsHeader(text: "SponsorBlock API")) { Section(header: SettingsHeader(text: "SponsorBlock API")) {
TextField( TextField(
"SponsorBlock API Instance", "SponsorBlock API Instance",
@@ -20,7 +33,7 @@ struct ServicesSettings: View {
Section(header: SettingsHeader(text: "Categories to Skip")) { Section(header: SettingsHeader(text: "Categories to Skip")) {
#if os(macOS) #if os(macOS)
let list = List(SponsorBlockAPI.categories, id: \.self) { category in let list = ForEach(SponsorBlockAPI.categories, id: \.self) { category in
SponsorBlockCategorySelectionRow( SponsorBlockCategorySelectionRow(
title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown", title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown",
selected: sponsorBlockCategories.contains(category) selected: sponsorBlockCategories.contains(category)
@@ -52,6 +65,35 @@ struct ServicesSettings: View {
} }
} }
private var commentsInstancePicker: some View {
Picker("Source", selection: $commentsInstanceID) {
Text("Disabled").tag(Optional(""))
ForEach(InstancesModel.all.filter { $0.app.supportsComments }) { instance in
Text(instance.description).tag(Optional(instance.id))
}
}
.labelsHidden()
#if os(iOS)
.pickerStyle(.automatic)
#elseif os(tvOS)
.pickerStyle(.inline)
#endif
}
#if !os(tvOS)
private var commentsPlacementPicker: some View {
Picker("Placement", selection: $commentsPlacement) {
Text("Below video description").tag(CommentsPlacement.info)
Text("Separate tab").tag(CommentsPlacement.separate)
}
.labelsHidden()
#if os(iOS)
.pickerStyle(.automatic)
#endif
}
#endif
func toggleCategory(_ category: String, value: Bool) { func toggleCategory(_ category: String, value: Bool) {
if let index = sponsorBlockCategories.firstIndex(where: { $0 == category }), !value { if let index = sponsorBlockCategories.firstIndex(where: { $0 == category }), !value {
sponsorBlockCategories.remove(at: index) sponsorBlockCategories.remove(at: index)

View File

@@ -5,10 +5,12 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
#if os(macOS) #if os(macOS)
private enum Tabs: Hashable { private enum Tabs: Hashable {
case instances, browsing, playback, services case instances, browsing, playback, services, updates
} }
#endif #endif
@Environment(\.colorScheme) private var colorScheme
#if os(iOS) #if os(iOS)
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
#endif #endif
@@ -44,7 +46,7 @@ struct SettingsView: View {
PlaybackSettings() PlaybackSettings()
} }
.tabItem { .tabItem {
Label("Playback", systemImage: "play.rectangle.on.rectangle.fill") Label("Playback", systemImage: "play.rectangle")
} }
.tag(Tabs.playback) .tag(Tabs.playback)
@@ -52,9 +54,17 @@ struct SettingsView: View {
ServicesSettings() ServicesSettings()
} }
.tabItem { .tabItem {
Label("Services", systemImage: "puzzlepiece.extension") Label("Services", systemImage: "puzzlepiece")
} }
.tag(Tabs.services) .tag(Tabs.services)
Form {
UpdatesSettings()
}
.tabItem {
Label("Updates", systemImage: "gearshape.2")
}
.tag(Tabs.updates)
} }
.padding(20) .padding(20)
.frame(width: 400, height: 380) .frame(width: 400, height: 380)
@@ -102,7 +112,7 @@ struct SettingsView: View {
InstanceForm(savedInstanceID: $savedFormInstanceID) InstanceForm(savedInstanceID: $savedFormInstanceID)
} }
#if os(tvOS) #if os(tvOS)
.background(Color.black) .background(Color.background(scheme: colorScheme))
#endif #endif
#endif #endif
} }

View File

@@ -9,6 +9,8 @@ struct ChannelPlaylistView: View {
@StateObject private var store = Store<ChannelPlaylist>() @StateObject private var store = Store<ChannelPlaylist>()
@Environment(\.colorScheme) private var colorScheme
#if os(iOS) #if os(iOS)
@Environment(\.inNavigationView) private var inNavigationView @Environment(\.inNavigationView) private var inNavigationView
#endif #endif
@@ -83,7 +85,7 @@ struct ChannelPlaylistView: View {
.navigationTitle(playlist.title) .navigationTitle(playlist.title)
#else #else
.background(Color.tertiaryBackground) .background(Color.background(scheme: colorScheme))
#endif #endif
} }

View File

@@ -9,6 +9,7 @@ struct ChannelVideosView: View {
@StateObject private var store = Store<Channel>() @StateObject private var store = Store<Channel>()
@Environment(\.colorScheme) private var colorScheme
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
@Environment(\.inNavigationView) private var inNavigationView @Environment(\.inNavigationView) private var inNavigationView
@@ -90,9 +91,14 @@ struct ChannelVideosView: View {
ToolbarItem { ToolbarItem {
HStack { HStack {
Text("**\(store.item?.subscriptionsString ?? "loading")** subscribers") HStack(spacing: 3) {
.foregroundColor(.secondary) Text("\(store.item?.subscriptionsString ?? "loading")")
.opacity(store.item?.subscriptionsString != nil ? 1 : 0) .fontWeight(.bold)
Text(" subscribers")
}
.allowsTightening(true)
.foregroundColor(.secondary)
.opacity(store.item?.subscriptionsString != nil ? 1 : 0)
subscriptionToggleButton subscriptionToggleButton
@@ -100,8 +106,6 @@ struct ChannelVideosView: View {
} }
} }
} }
#else
.background(Color.tertiaryBackground)
#endif #endif
#if os(iOS) #if os(iOS)
.sheet(isPresented: $presentingShareSheet) { .sheet(isPresented: $presentingShareSheet) {
@@ -121,6 +125,9 @@ struct ChannelVideosView: View {
return Group { return Group {
if #available(macOS 12.0, *) { if #available(macOS 12.0, *) {
content content
#if os(tvOS)
.background(Color.background(scheme: colorScheme))
#endif
#if !os(iOS) #if !os(iOS)
.focusScope(focusNamespace) .focusScope(focusNamespace)
#endif #endif

View File

@@ -25,13 +25,19 @@ struct DetailBadge: View {
} }
struct DefaultStyleModifier: ViewModifier { struct DefaultStyleModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
func body(content: Content) -> some View { func body(content: Content) -> some View {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
content content
.background(.thinMaterial) .background(.thinMaterial)
} else { } else {
content content
.background(Color.background) #if os(tvOS)
.background(Color.background(scheme: colorScheme))
#else
.background(Color.background.opacity(0.95))
#endif
} }
} }
} }

View File

@@ -1,25 +1,37 @@
import Defaults
import Foundation import Foundation
import SwiftUI import SwiftUI
struct FavoriteButton: View { struct FavoriteButton: View {
let item: FavoriteItem let item: FavoriteItem!
let favorites = FavoritesModel.shared let favorites = FavoritesModel.shared
@State private var isFavorite = false @State private var isFavorite = false
var body: some View { var body: some View {
Button { Group {
favorites.toggle(item) if favorites.isEnabled {
isFavorite.toggle() Button {
} label: { guard !item.isNil else {
if isFavorite { return
Label("Remove from Favorites", systemImage: "heart.fill") }
favorites.toggle(item)
isFavorite.toggle()
} label: {
if isFavorite {
Label("Remove from Favorites", systemImage: "heart.fill")
} else {
Label("Add to Favorites", systemImage: "heart")
}
}
.disabled(item.isNil)
.onAppear {
isFavorite = item.isNil ? false : favorites.contains(item)
}
} else { } else {
Label("Add to Favorites", systemImage: "heart") EmptyView()
} }
} }
.onAppear {
isFavorite = favorites.contains(item)
}
} }
} }

View File

@@ -36,7 +36,7 @@ struct PlayerControlsView<Content: View>: View {
.foregroundColor(model.currentItem.isNil ? .secondary : .accentColor) .foregroundColor(model.currentItem.isNil ? .secondary : .accentColor)
.lineLimit(1) .lineLimit(1)
Text(model.currentItem?.video?.author ?? "Yattee v\(appVersion)") Text(model.currentItem?.video?.author ?? "Yattee v\(appVersion) (build \(appBuild))")
.fontWeight(model.currentItem.isNil ? .light : .bold) .fontWeight(model.currentItem.isNil ? .light : .bold)
.font(.system(size: 10)) .font(.system(size: 10))
.foregroundColor(.secondary) .foregroundColor(.secondary)
@@ -106,7 +106,9 @@ struct PlayerControlsView<Content: View>: View {
.background(Material.ultraThinMaterial) .background(Material.ultraThinMaterial)
} else { } else {
controls controls
.background(Color.tertiaryBackground) #if !os(tvOS)
.background(Color.tertiaryBackground)
#endif
} }
} }
} }
@@ -115,6 +117,10 @@ struct PlayerControlsView<Content: View>: View {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
} }
private var appBuild: String {
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
}
private var progressViewValue: Double { private var progressViewValue: Double {
[model.time?.seconds, model.videoDuration].compactMap { $0 }.min() ?? 0 [model.time?.seconds, model.videoDuration].compactMap { $0 }.min() ?? 0
} }

View File

@@ -34,9 +34,9 @@ struct SignInRequiredView<Content: View>: View {
Group { Group {
if instances.isEmpty { if instances.isEmpty {
Text("You need to create an instance and accounts\nto access **\(title)** section") Text("You need to create an instance and accounts\nto access \(title) section")
} else { } else {
Text("You need to select an account\nto access **\(title)** section") Text("You need to select an account\nto access \(title) section")
} }
} }
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@@ -4,6 +4,11 @@
<dict> <dict>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
</array>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>
<true/> <true/>
</dict> </dict>

View File

@@ -5,6 +5,7 @@ import SwiftUI
struct YatteeApp: App { struct YatteeApp: App {
#if os(macOS) #if os(macOS)
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var updater = UpdaterModel()
#endif #endif
@StateObject private var menu = MenuModel() @StateObject private var menu = MenuModel()
@@ -18,7 +19,16 @@ struct YatteeApp: App {
.handlesExternalEvents(matching: Set(["*"])) .handlesExternalEvents(matching: Set(["*"]))
.commands { .commands {
SidebarCommands() SidebarCommands()
CommandGroup(replacing: .newItem, addition: {}) CommandGroup(replacing: .newItem, addition: {})
#if os(macOS)
CommandGroup(after: .appInfo) {
CheckForUpdatesView()
.environmentObject(updater)
}
#endif
MenuCommands(model: Binding<MenuModel>(get: { menu }, set: { _ in })) MenuCommands(model: Binding<MenuModel>(get: { menu }, set: { _ in }))
} }
#endif #endif
@@ -28,6 +38,7 @@ struct YatteeApp: App {
SettingsView() SettingsView()
.environmentObject(AccountsModel()) .environmentObject(AccountsModel())
.environmentObject(InstancesModel()) .environmentObject(InstancesModel())
.environmentObject(updater)
} }
#endif #endif
} }

View File

@@ -74,6 +74,20 @@
37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; 37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; };
37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; 37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; };
37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; 37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; };
371B7E5C27596B8400D21217 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E5B27596B8400D21217 /* Comment.swift */; };
371B7E5D27596B8400D21217 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E5B27596B8400D21217 /* Comment.swift */; };
371B7E5E27596B8400D21217 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E5B27596B8400D21217 /* Comment.swift */; };
371B7E5F27596B8400D21217 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E5B27596B8400D21217 /* Comment.swift */; };
371B7E612759706A00D21217 /* CommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E602759706A00D21217 /* CommentsView.swift */; };
371B7E622759706A00D21217 /* CommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E602759706A00D21217 /* CommentsView.swift */; };
371B7E632759706A00D21217 /* CommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E602759706A00D21217 /* CommentsView.swift */; };
371B7E642759706A00D21217 /* CommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E602759706A00D21217 /* CommentsView.swift */; };
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E652759786B00D21217 /* Comment+Fixtures.swift */; };
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E652759786B00D21217 /* Comment+Fixtures.swift */; };
371B7E682759786B00D21217 /* Comment+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E652759786B00D21217 /* Comment+Fixtures.swift */; };
371B7E6A2759791900D21217 /* CommentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E692759791900D21217 /* CommentsModel.swift */; };
371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E692759791900D21217 /* CommentsModel.swift */; };
371B7E6C2759791900D21217 /* CommentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E692759791900D21217 /* CommentsModel.swift */; };
371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; 371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
371F2F1B269B43D300E4A7AB /* 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 */; }; 371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
@@ -92,6 +106,10 @@
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; }; 37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; }; 37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; }; 37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37319F0427103F94004ECCD0 /* PlayerQueue.swift */; };
373C8FE4275B955100CB5936 /* CommentsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373C8FE3275B955100CB5936 /* CommentsPage.swift */; };
373C8FE5275B955100CB5936 /* CommentsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373C8FE3275B955100CB5936 /* CommentsPage.swift */; };
373C8FE6275B955100CB5936 /* CommentsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373C8FE3275B955100CB5936 /* CommentsPage.swift */; };
373C8FE7275B955100CB5936 /* CommentsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373C8FE3275B955100CB5936 /* CommentsPage.swift */; };
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; }; 373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; };
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; }; 373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; };
373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; }; 373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; };
@@ -389,6 +407,7 @@
37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */; }; 37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */; };
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD926A214630092E2DB /* PlayerViewController.swift */; }; 37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD926A214630092E2DB /* PlayerViewController.swift */; };
37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* Player.swift */; }; 37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* Player.swift */; };
37BE7AF12760013C00DBECED /* UpdatesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE7AF02760013C00DBECED /* UpdatesSettings.swift */; };
37BF661C27308859008CCFB0 /* DropFavorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661B27308859008CCFB0 /* DropFavorite.swift */; }; 37BF661C27308859008CCFB0 /* DropFavorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661B27308859008CCFB0 /* DropFavorite.swift */; };
37BF661D27308859008CCFB0 /* DropFavorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661B27308859008CCFB0 /* DropFavorite.swift */; }; 37BF661D27308859008CCFB0 /* DropFavorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661B27308859008CCFB0 /* DropFavorite.swift */; };
37BF661F27308884008CCFB0 /* DropFavoriteOutside.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661E27308884008CCFB0 /* DropFavoriteOutside.swift */; }; 37BF661F27308884008CCFB0 /* DropFavoriteOutside.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661E27308884008CCFB0 /* DropFavoriteOutside.swift */; };
@@ -424,6 +443,9 @@
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; }; 37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; };
37C90AEF275F9CC00015EAF7 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 37C90AEE275F9CC00015EAF7 /* Sparkle */; };
37C90AF1275F9D450015EAF7 /* UpdaterModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C90AF0275F9D450015EAF7 /* UpdaterModel.swift */; };
37C90AF3275F9D5D0015EAF7 /* CheckForUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C90AF2275F9D5D0015EAF7 /* CheckForUpdatesView.swift */; };
37CB12792724C76D00213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.swift */; }; 37CB12792724C76D00213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.swift */; };
37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.swift */; }; 37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.swift */; };
37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */; }; 37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */; };
@@ -465,6 +487,7 @@
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; }; 37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
37DD87C8271C9CFE0027CBF9 /* 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 */; }; 37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */; };
37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; }; 37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; };
37E084AD2753D95F00039B7D /* 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 */; }; 37E2EEAB270656EC00170416 /* PlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E2EEAA270656EC00170416 /* PlayerControlsView.swift */; };
@@ -488,6 +511,10 @@
37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; }; 37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; };
37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; }; 37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; };
37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; }; 37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; };
37EF9A76275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; };
37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; }; 37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; };
37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; }; 37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; };
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; }; 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; };
@@ -560,6 +587,10 @@
37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; }; 37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
37169AA12729D98A0011DE61 /* InstancesBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesBridge.swift; sourceTree = "<group>"; }; 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>"; }; 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsBridge.swift; sourceTree = "<group>"; };
371B7E5B27596B8400D21217 /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = "<group>"; };
371B7E602759706A00D21217 /* CommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsView.swift; sourceTree = "<group>"; };
371B7E652759786B00D21217 /* Comment+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comment+Fixtures.swift"; sourceTree = "<group>"; };
371B7E692759791900D21217 /* CommentsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsModel.swift; sourceTree = "<group>"; };
371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.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>"; }; 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>"; }; 3722AEBD274DA401005EA4D6 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
@@ -569,6 +600,7 @@
3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = "<group>"; }; 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = "<group>"; };
373197D82732015300EF734F /* RelatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedView.swift; sourceTree = "<group>"; }; 373197D82732015300EF734F /* RelatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedView.swift; sourceTree = "<group>"; };
37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = "<group>"; }; 37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = "<group>"; };
373C8FE3275B955100CB5936 /* CommentsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsPage.swift; sourceTree = "<group>"; };
373CFACA26966264003CB2C6 /* SearchQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchQuery.swift; sourceTree = "<group>"; }; 373CFACA26966264003CB2C6 /* SearchQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchQuery.swift; sourceTree = "<group>"; };
373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = "<group>"; }; 373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = "<group>"; };
373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistFormView.swift; sourceTree = "<group>"; }; 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistFormView.swift; sourceTree = "<group>"; };
@@ -663,6 +695,7 @@
37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; }; 37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
37BE0BD926A214630092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; }; 37BE0BD926A214630092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; }; 37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
37BE7AF02760013C00DBECED /* UpdatesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatesSettings.swift; sourceTree = "<group>"; };
37BF661B27308859008CCFB0 /* DropFavorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropFavorite.swift; sourceTree = "<group>"; }; 37BF661B27308859008CCFB0 /* DropFavorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropFavorite.swift; sourceTree = "<group>"; };
37BF661E27308884008CCFB0 /* DropFavoriteOutside.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropFavoriteOutside.swift; sourceTree = "<group>"; }; 37BF661E27308884008CCFB0 /* DropFavoriteOutside.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropFavoriteOutside.swift; sourceTree = "<group>"; };
37C069772725962F00F7F6CB /* ScreenSaverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverManager.swift; sourceTree = "<group>"; }; 37C069772725962F00F7F6CB /* ScreenSaverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverManager.swift; sourceTree = "<group>"; };
@@ -676,6 +709,8 @@
37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChannelPlaylist+Fixtures.swift"; sourceTree = "<group>"; }; 37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChannelPlaylist+Fixtures.swift"; sourceTree = "<group>"; };
37C3A250272366440087A57A /* ChannelPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylistView.swift; sourceTree = "<group>"; }; 37C3A250272366440087A57A /* ChannelPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylistView.swift; sourceTree = "<group>"; };
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; }; 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
37C90AF0275F9D450015EAF7 /* UpdaterModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdaterModel.swift; sourceTree = "<group>"; };
37C90AF2275F9D5D0015EAF7 /* CheckForUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForUpdatesView.swift; sourceTree = "<group>"; };
37CB12782724C76D00213B45 /* VideoURLParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoURLParser.swift; sourceTree = "<group>"; }; 37CB12782724C76D00213B45 /* VideoURLParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoURLParser.swift; sourceTree = "<group>"; };
37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoURLParserTests.swift; sourceTree = "<group>"; }; 37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoURLParserTests.swift; sourceTree = "<group>"; };
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItem.swift; sourceTree = "<group>"; }; 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItem.swift; sourceTree = "<group>"; };
@@ -703,6 +738,7 @@
37D526E22720B4BE00ED2F5E /* View+SwipeGesture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+SwipeGesture.swift"; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalScrollingFix.swift; sourceTree = "<group>"; };
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.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>"; }; 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>"; }; 37E64DD026D597EB00C71877 /* SubscriptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsModel.swift; sourceTree = "<group>"; };
@@ -711,6 +747,7 @@
37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = "<group>"; }; 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = "<group>"; };
37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = "<group>"; }; 37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = "<group>"; };
37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = "<group>"; }; 37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = "<group>"; };
37EF9A75275BEB8E0043B585 /* CommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentView.swift; sourceTree = "<group>"; };
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = "<group>"; }; 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = "<group>"; };
37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = "<group>"; }; 37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = "<group>"; };
37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = "<group>"; }; 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = "<group>"; };
@@ -763,6 +800,7 @@
37FB284F272209AB00A57617 /* SDWebImageSwiftUI in Frameworks */, 37FB284F272209AB00A57617 /* SDWebImageSwiftUI in Frameworks */,
3765917A27237D07009F956E /* PINCache in Frameworks */, 3765917A27237D07009F956E /* PINCache in Frameworks */,
37BD07BE2698AC96003EBB87 /* Defaults in Frameworks */, 37BD07BE2698AC96003EBB87 /* Defaults in Frameworks */,
37C90AEF275F9CC00015EAF7 /* Sparkle in Frameworks */,
37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */, 37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */,
377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */, 377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */,
37BD07C02698AC97003EBB87 /* Siesta in Frameworks */, 37BD07C02698AC97003EBB87 /* Siesta in Frameworks */,
@@ -833,6 +871,8 @@
371AAE2426CEBA4100901972 /* Player */ = { 371AAE2426CEBA4100901972 /* Player */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
371B7E602759706A00D21217 /* CommentsView.swift */,
37EF9A75275BEB8E0043B585 /* CommentView.swift */,
37B81B0126D2CAE700675966 /* PlaybackBar.swift */, 37B81B0126D2CAE700675966 /* PlaybackBar.swift */,
37BE0BD226A1D4780092E2DB /* Player.swift */, 37BE0BD226A1D4780092E2DB /* Player.swift */,
3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */, 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */,
@@ -955,6 +995,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */, 37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */,
371B7E652759786B00D21217 /* Comment+Fixtures.swift */,
376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */, 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */,
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */, 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */,
3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */, 3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */,
@@ -1068,16 +1109,28 @@
37BE0BD826A214500092E2DB /* macOS */ = { 37BE0BD826A214500092E2DB /* macOS */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
37BE7AF227601DBF00DBECED /* Updates */,
374C0542272496E4009BDDBE /* AppDelegate.swift */, 374C0542272496E4009BDDBE /* AppDelegate.swift */,
37FD43DB270470B70073EE42 /* InstancesSettings.swift */, 37FD43DB270470B70073EE42 /* InstancesSettings.swift */,
374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */, 374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */,
37BE0BDB26A2367F0092E2DB /* Player.swift */, 37BE0BDB26A2367F0092E2DB /* Player.swift */,
37BE0BD926A214630092E2DB /* PlayerViewController.swift */, 37BE0BD926A214630092E2DB /* PlayerViewController.swift */,
37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */,
374C0544272496FD009BDDBE /* Info.plist */, 374C0544272496FD009BDDBE /* Info.plist */,
); );
path = macOS; path = macOS;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
37BE7AF227601DBF00DBECED /* Updates */ = {
isa = PBXGroup;
children = (
37C90AF2275F9D5D0015EAF7 /* CheckForUpdatesView.swift */,
37C90AF0275F9D450015EAF7 /* UpdaterModel.swift */,
37BE7AF02760013C00DBECED /* UpdatesSettings.swift */,
);
path = Updates;
sourceTree = "<group>";
};
37C7A9022679058300E721B4 /* Extensions */ = { 37C7A9022679058300E721B4 /* Extensions */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1209,6 +1262,9 @@
374C0539272436DA009BDDBE /* SponsorBlock */, 374C0539272436DA009BDDBE /* SponsorBlock */,
37AAF28F26740715007FC770 /* Channel.swift */, 37AAF28F26740715007FC770 /* Channel.swift */,
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */, 37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
371B7E5B27596B8400D21217 /* Comment.swift */,
371B7E692759791900D21217 /* CommentsModel.swift */,
373C8FE3275B955100CB5936 /* CommentsPage.swift */,
37FB28402721B22200A57617 /* ContentItem.swift */, 37FB28402721B22200A57617 /* ContentItem.swift */,
37141672267A8E10006CA35D /* Country.swift */, 37141672267A8E10006CA35D /* Country.swift */,
37599F2F272B42810087F250 /* FavoriteItem.swift */, 37599F2F272B42810087F250 /* FavoriteItem.swift */,
@@ -1339,6 +1395,7 @@
37FB2850272209AB00A57617 /* SDWebImageWebPCoder */, 37FB2850272209AB00A57617 /* SDWebImageWebPCoder */,
37FB285727220D9600A57617 /* SDWebImagePINPlugin */, 37FB285727220D9600A57617 /* SDWebImagePINPlugin */,
3765917927237D07009F956E /* PINCache */, 3765917927237D07009F956E /* PINCache */,
37C90AEE275F9CC00015EAF7 /* Sparkle */,
); );
productName = "Yattee (macOS)"; productName = "Yattee (macOS)";
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */; productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
@@ -1502,6 +1559,7 @@
37FB2847272207F000A57617 /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */, 37FB2847272207F000A57617 /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */,
37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */, 37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */,
3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */, 3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */,
37C90AED275F9CC00015EAF7 /* XCRemoteSwiftPackageReference "Sparkle" */,
); );
productRefGroup = 37D4B0CA2671614900C925CA /* Products */; productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -1719,6 +1777,8 @@
files = ( files = (
374710052755291C00CE0F87 /* SearchField.swift in Sources */, 374710052755291C00CE0F87 /* SearchField.swift in Sources */,
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
371B7E612759706A00D21217 /* CommentsView.swift in Sources */,
371B7E6A2759791900D21217 /* CommentsModel.swift in Sources */,
37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */, 37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */,
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */, 37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */,
@@ -1749,6 +1809,7 @@
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */, 37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, 37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */, 37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */, 37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */,
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */, 37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
@@ -1776,6 +1837,8 @@
3782B9522755667600990149 /* String+Format.swift in Sources */, 3782B9522755667600990149 /* String+Format.swift in Sources */,
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
37BF661F27308884008CCFB0 /* DropFavoriteOutside.swift in Sources */, 37BF661F27308884008CCFB0 /* DropFavoriteOutside.swift in Sources */,
37EF9A76275BEB8E0043B585 /* CommentView.swift in Sources */,
373C8FE4275B955100CB5936 /* CommentsPage.swift in Sources */,
3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */, 3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */,
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */, 37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */,
377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */, 377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */,
@@ -1802,6 +1865,7 @@
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */, 376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, 37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, 3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
371B7E5C27596B8400D21217 /* Comment.swift in Sources */,
37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */, 37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */,
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */, 37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */, 3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */,
@@ -1873,6 +1937,7 @@
37C3A24E272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37C3A24E272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */, 37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */, 3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */,
371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */,
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */, 371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
37001564271B1F250049C794 /* AccountsModel.swift in Sources */, 37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, 3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
@@ -1898,6 +1963,7 @@
3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, 3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */, 378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */,
37599F35272B44000087F250 /* FavoritesModel.swift in Sources */, 37599F35272B44000087F250 /* FavoritesModel.swift in Sources */,
37C90AF3275F9D5D0015EAF7 /* CheckForUpdatesView.swift in Sources */,
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */, 37FFC441272734C3009FFD26 /* Throttle.swift in Sources */,
37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */, 37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
@@ -1910,6 +1976,7 @@
377FC7E2267A084A00A6BBAF /* VideoCell.swift in Sources */, 377FC7E2267A084A00A6BBAF /* VideoCell.swift in Sources */,
37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */, 37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */,
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */, 378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
37BE7AF12760013C00DBECED /* UpdatesSettings.swift in Sources */,
37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */,
37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */, 37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */,
3765788A2685471400D4EA09 /* Playlist.swift in Sources */, 3765788A2685471400D4EA09 /* Playlist.swift in Sources */,
@@ -1922,10 +1989,12 @@
37AAF29126740715007FC770 /* Channel.swift in Sources */, 37AAF29126740715007FC770 /* Channel.swift in Sources */,
376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */, 376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */,
37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */, 37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */,
37BF661D27308859008CCFB0 /* DropFavorite.swift in Sources */, 37BF661D27308859008CCFB0 /* DropFavorite.swift in Sources */,
3748186F26A769D60084E870 /* DetailBadge.swift in Sources */, 3748186F26A769D60084E870 /* DetailBadge.swift in Sources */,
372915E72687E3B900F5A35B /* Defaults.swift in Sources */, 372915E72687E3B900F5A35B /* Defaults.swift in Sources */,
37C3A242272359900087A57A /* Double+Format.swift in Sources */, 37C3A242272359900087A57A /* Double+Format.swift in Sources */,
37C90AF1275F9D450015EAF7 /* UpdaterModel.swift in Sources */,
37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, 37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
376578922685490700D4EA09 /* PlaylistsView.swift in Sources */, 376578922685490700D4EA09 /* PlaylistsView.swift in Sources */,
37484C2626FC83E000287258 /* InstanceForm.swift in Sources */, 37484C2626FC83E000287258 /* InstanceForm.swift in Sources */,
@@ -1953,6 +2022,7 @@
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */, 37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */,
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */, 37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
3797758C2689345500DD52A8 /* Store.swift in Sources */, 3797758C2689345500DD52A8 /* Store.swift in Sources */,
371B7E622759706A00D21217 /* CommentsView.swift in Sources */,
37141674267A8E10006CA35D /* Country.swift in Sources */, 37141674267A8E10006CA35D /* Country.swift in Sources */,
37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */, 37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */,
37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */, 37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */,
@@ -1964,11 +2034,14 @@
3700155C271B0D4D0049C794 /* PipedAPI.swift in Sources */, 3700155C271B0D4D0049C794 /* PipedAPI.swift in Sources */,
376BE50C27349108009AD608 /* BrowsingSettings.swift in Sources */, 376BE50C27349108009AD608 /* BrowsingSettings.swift in Sources */,
37D4B19826717E1500C925CA /* Video.swift in Sources */, 37D4B19826717E1500C925CA /* Video.swift in Sources */,
371B7E5D27596B8400D21217 /* Comment.swift in Sources */,
37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */, 37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */,
37599F31272B42810087F250 /* FavoriteItem.swift in Sources */, 37599F31272B42810087F250 /* FavoriteItem.swift in Sources */,
3730F75A2733481E00F385FC /* RelatedView.swift in Sources */, 3730F75A2733481E00F385FC /* RelatedView.swift in Sources */,
37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */,
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */, 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */,
37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */, 37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
373C8FE5275B955100CB5936 /* CommentsPage.swift in Sources */,
37D4B0E52671614900C925CA /* YatteeApp.swift in Sources */, 37D4B0E52671614900C925CA /* YatteeApp.swift in Sources */,
37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */, 37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */,
37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
@@ -1989,6 +2062,7 @@
37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, 3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */,
37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */, 37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */,
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */, 37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */,
); );
@@ -2008,6 +2082,7 @@
files = ( files = (
3774124C27387D2300423605 /* RecentsModel.swift in Sources */, 3774124C27387D2300423605 /* RecentsModel.swift in Sources */,
3774122A27387B6C00423605 /* InstancesModelTests.swift in Sources */, 3774122A27387B6C00423605 /* InstancesModelTests.swift in Sources */,
371B7E642759706A00D21217 /* CommentsView.swift in Sources */,
3774124927387D2300423605 /* Channel.swift in Sources */, 3774124927387D2300423605 /* Channel.swift in Sources */,
3774125727387D2300423605 /* FavoriteItem.swift in Sources */, 3774125727387D2300423605 /* FavoriteItem.swift in Sources */,
3774126B27387D6D00423605 /* CMTime+DefaultTimescale.swift in Sources */, 3774126B27387D6D00423605 /* CMTime+DefaultTimescale.swift in Sources */,
@@ -2017,6 +2092,7 @@
3774126827387D6D00423605 /* Double+Format.swift in Sources */, 3774126827387D6D00423605 /* Double+Format.swift in Sources */,
3774126E27387D8800423605 /* PlayerQueueItem.swift in Sources */, 3774126E27387D8800423605 /* PlayerQueueItem.swift in Sources */,
3774125627387D2300423605 /* Segment.swift in Sources */, 3774125627387D2300423605 /* Segment.swift in Sources */,
373C8FE7275B955100CB5936 /* CommentsPage.swift in Sources */,
3774126427387D4A00423605 /* VideosAPI.swift in Sources */, 3774126427387D4A00423605 /* VideosAPI.swift in Sources */,
3774124D27387D2300423605 /* PlaylistsModel.swift in Sources */, 3774124D27387D2300423605 /* PlaylistsModel.swift in Sources */,
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */, 3774123427387CC100423605 /* InvidiousAPI.swift in Sources */,
@@ -2024,6 +2100,7 @@
37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */, 37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */,
3774125427387D2300423605 /* Store.swift in Sources */, 3774125427387D2300423605 /* Store.swift in Sources */,
3774125027387D2300423605 /* Video.swift in Sources */, 3774125027387D2300423605 /* Video.swift in Sources */,
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */,
3774125327387D2300423605 /* Country.swift in Sources */, 3774125327387D2300423605 /* Country.swift in Sources */,
3774125E27387D2D00423605 /* InstancesModel.swift in Sources */, 3774125E27387D2D00423605 /* InstancesModel.swift in Sources */,
37CB128C2724CC8400213B45 /* VideoURLParser.swift in Sources */, 37CB128C2724CC8400213B45 /* VideoURLParser.swift in Sources */,
@@ -2041,6 +2118,7 @@
3774125D27387D2D00423605 /* Instance.swift in Sources */, 3774125D27387D2D00423605 /* Instance.swift in Sources */,
3774125927387D2300423605 /* ChannelPlaylist.swift in Sources */, 3774125927387D2300423605 /* ChannelPlaylist.swift in Sources */,
3774125527387D2300423605 /* Stream.swift in Sources */, 3774125527387D2300423605 /* Stream.swift in Sources */,
371B7E5F27596B8400D21217 /* Comment.swift in Sources */,
3774126F27387D8D00423605 /* SearchQuery.swift in Sources */, 3774126F27387D8D00423605 /* SearchQuery.swift in Sources */,
3774127127387D9E00423605 /* PlayerQueueItemBridge.swift in Sources */, 3774127127387D9E00423605 /* PlayerQueueItemBridge.swift in Sources */,
3774125227387D2300423605 /* Thumbnail.swift in Sources */, 3774125227387D2300423605 /* Thumbnail.swift in Sources */,
@@ -2063,6 +2141,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */, 37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
373C8FE6275B955100CB5936 /* CommentsPage.swift in Sources */,
37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */, 37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */,
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */, 376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
@@ -2076,6 +2155,7 @@
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */, 37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */,
3748187026A769D60084E870 /* DetailBadge.swift in Sources */, 3748187026A769D60084E870 /* DetailBadge.swift in Sources */,
371B7E632759706A00D21217 /* CommentsView.swift in Sources */,
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, 37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */, 3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */, 37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
@@ -2096,6 +2176,7 @@
37C3A243272359900087A57A /* Double+Format.swift in Sources */, 37C3A243272359900087A57A /* Double+Format.swift in Sources */,
37AAF29226740715007FC770 /* Channel.swift in Sources */, 37AAF29226740715007FC770 /* Channel.swift in Sources */,
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
371B7E5E27596B8400D21217 /* Comment.swift in Sources */,
37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */, 37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */,
37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */, 37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */,
@@ -2110,6 +2191,7 @@
3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */, 3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */,
37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */,
37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */, 37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */,
371B7E682759786B00D21217 /* Comment+Fixtures.swift in Sources */,
37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, 37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
37AAF27E26737323007FC770 /* PopularView.swift in Sources */, 37AAF27E26737323007FC770 /* PopularView.swift in Sources */,
3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, 3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
@@ -2158,6 +2240,7 @@
37141675267A8E10006CA35D /* Country.swift in Sources */, 37141675267A8E10006CA35D /* Country.swift in Sources */,
3782B9542755667600990149 /* String+Format.swift in Sources */, 3782B9542755667600990149 /* String+Format.swift in Sources */,
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */, 37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */,
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */,
37484C2726FC83E000287258 /* InstanceForm.swift in Sources */, 37484C2726FC83E000287258 /* InstanceForm.swift in Sources */,
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */, 37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
373197DA2732060100EF734F /* RelatedView.swift in Sources */, 373197DA2732060100EF734F /* RelatedView.swift in Sources */,
@@ -2168,6 +2251,7 @@
37599F36272B44000087F250 /* FavoritesModel.swift in Sources */, 37599F36272B44000087F250 /* FavoritesModel.swift in Sources */,
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */, 3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */,
37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */, 37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */,
371B7E6C2759791900D21217 /* CommentsModel.swift in Sources */,
3782B95627557E4E00990149 /* SearchView.swift in Sources */, 3782B95627557E4E00990149 /* SearchView.swift in Sources */,
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, 3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
37C3A24F272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37C3A24F272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
@@ -2217,7 +2301,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements"; CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -2251,7 +2335,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements"; CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -2283,7 +2367,7 @@
buildSettings = { buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist"; INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -2315,7 +2399,7 @@
buildSettings = { buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist"; INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -2478,7 +2562,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -2509,7 +2593,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -2544,7 +2628,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -2577,7 +2661,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -2708,7 +2792,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -2740,7 +2824,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -3016,6 +3100,14 @@
minimumVersion = 0.1.3; minimumVersion = 0.1.3;
}; };
}; };
37C90AED275F9CC00015EAF7 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sparkle-project/Sparkle";
requirement = {
branch = 2.x;
kind = branch;
};
};
37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON.git"; repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON.git";
@@ -3166,6 +3258,11 @@
package = 37BD07C52698B27B003EBB87 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; package = 37BD07C52698B27B003EBB87 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
productName = Introspect; productName = Introspect;
}; };
37C90AEE275F9CC00015EAF7 /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
package = 37C90AED275F9CC00015EAF7 /* XCRemoteSwiftPackageReference "Sparkle" */;
productName = Sparkle;
};
37D4B19C2671817900C925CA /* SwiftyJSON */ = { 37D4B19C2671817900C925CA /* SwiftyJSON */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */; package = 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */;

View File

@@ -91,6 +91,15 @@
"version": "1.5.2" "version": "1.5.2"
} }
}, },
{
"package": "Sparkle",
"repositoryURL": "https://github.com/sparkle-project/Sparkle",
"state": {
"branch": "2.x",
"revision": "831e9b4eb7e871a9c072469fb14049614fc92810",
"version": null
}
},
{ {
"package": "swift-log", "package": "swift-log",
"repositoryURL": "https://github.com/apple/swift-log.git", "repositoryURL": "https://github.com/apple/swift-log.git",

View File

@@ -15,5 +15,11 @@
</array> </array>
</dict> </dict>
</array> </array>
<key>SUEnableInstallerLauncherService</key>
<true/>
<key>SUFeedURL</key>
<string>https://repos.yattee.stream/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>73U5at3utQRS7F/z/7nztpjp3l1gw1Ih+ztOelRLSx4=</string>
</dict> </dict>
</plist> </plist>

View File

@@ -14,6 +14,7 @@ struct InstancesSettings: View {
@State private var frontendURL = "" @State private var frontendURL = ""
@Environment(\.colorScheme) private var colorScheme
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@Default(.instances) private var instances @Default(.instances) private var instances
@@ -54,7 +55,7 @@ struct InstancesSettings: View {
Button("Remove") { Button("Remove") {
presentingAccountRemovalConfirmation = true presentingAccountRemovalConfirmation = true
} }
.foregroundColor(.red) .foregroundColor(colorScheme == .dark ? .white : .red)
.opacity(account == selectedAccount ? 1 : 0) .opacity(account == selectedAccount ? 1 : 0)
} }
.tag(account) .tag(account)

View File

@@ -0,0 +1,10 @@
import SwiftUI
struct CheckForUpdatesView: View {
@EnvironmentObject<UpdaterModel> private var updater
var body: some View {
Button("Check For Updates…", action: updater.checkForUpdates)
.disabled(!updater.canCheckForUpdates)
}
}

View File

@@ -0,0 +1,41 @@
import Defaults
import Sparkle
import SwiftUI
final class UpdaterModel: ObservableObject {
@Published var canCheckForUpdates = false
private let updaterController: SPUStandardUpdaterController
private let updaterDelegate = UpdaterDelegate()
init() {
updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: updaterDelegate,
userDriverDelegate: nil
)
updaterController.updater.publisher(for: \.canCheckForUpdates)
.assign(to: &$canCheckForUpdates)
}
func checkForUpdates() {
updaterController.checkForUpdates(nil)
}
var automaticallyChecksForUpdates: Bool {
updaterController.updater.automaticallyChecksForUpdates
}
func setAutomaticallyChecksForUpdates(_ value: Bool) {
updaterController.updater.automaticallyChecksForUpdates = value
}
}
final class UpdaterDelegate: NSObject, SPUUpdaterDelegate {
@Default(.enableBetaChannel) private var enableBetaChannel
func allowedChannels(for _: SPUUpdater) -> Set<String> {
Set(enableBetaChannel ? ["beta"] : [])
}
}

View File

@@ -0,0 +1,32 @@
import Defaults
import SwiftUI
struct UpdatesSettings: View {
@EnvironmentObject<UpdaterModel> private var updater
@State private var automaticallyChecksForUpdates = false
@Default(.enableBetaChannel) private var enableBetaChannel
var body: some View {
Section(header: SettingsHeader(text: "Updates")) {
Toggle("Check automatically", isOn: $automaticallyChecksForUpdates)
Toggle("Enable beta channel", isOn: $enableBetaChannel)
}
.onAppear {
automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates
}
.onChange(of: automaticallyChecksForUpdates) { _ in
updater.setAutomaticallyChecksForUpdates(automaticallyChecksForUpdates)
}
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
}
}
struct UpdatesSettings_Previews: PreviewProvider {
static var previews: some View {
UpdatesSettings()
.injectFixtureEnvironmentObjects()
}
}

View File

@@ -0,0 +1,42 @@
// source: https://stackoverflow.com/a/65002837
import SwiftUI
// we need this workaround only for macOS
// this is the NSView that implements proper `wantsForwardedScrollEvents` method
final class VerticalScrollingFixHostingView<Content>: NSHostingView<Content> where Content: View {
override func wantsForwardedScrollEvents(for axis: NSEvent.GestureAxis) -> Bool {
axis == .vertical
}
}
// this is the SwiftUI wrapper for our NSView
struct VerticalScrollingFixViewRepresentable<Content>: NSViewRepresentable where Content: View {
let content: Content
func makeNSView(context _: Context) -> NSHostingView<Content> {
VerticalScrollingFixHostingView<Content>(rootView: content)
}
func updateNSView(_: NSHostingView<Content>, context _: Context) {}
}
// this is the SwiftUI wrapper that makes it easy to insert the view
// into the existing SwiftUI view builders structure
struct VerticalScrollingFixWrapper<Content>: View where Content: View {
let content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
VerticalScrollingFixViewRepresentable(content: self.content())
}
}
extension View {
@ViewBuilder func workaroundForVerticalScrollingBug() -> some View {
VerticalScrollingFixWrapper { self }
}
}

View File

@@ -3,13 +3,17 @@ import SwiftUI
struct NowPlayingView: View { struct NowPlayingView: View {
enum ViewSection: CaseIterable { enum ViewSection: CaseIterable {
case nowPlaying, playingNext, playedPreviously, related case nowPlaying, playingNext, playedPreviously, related, comments
} }
var sections = ViewSection.allCases var sections = [ViewSection.nowPlaying, .playingNext, .playedPreviously, .related]
var inInfoViewController = false var inInfoViewController = false
@State private var repliesID: Comment.ID?
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<RecentsModel> private var recents
@Default(.saveHistory) private var saveHistory @Default(.saveHistory) private var saveHistory
@@ -33,6 +37,13 @@ struct NowPlayingView: View {
} label: { } label: {
VideoBanner(video: item.video) VideoBanner(video: item.video)
} }
.contextMenu {
Button("Close Video") {
player.closeCurrentItem()
}
Button("Cancel", role: .cancel) {}
}
} }
.onPlayPauseCommand(perform: player.togglePlay) .onPlayPauseCommand(perform: player.togglePlay)
} }
@@ -104,6 +115,14 @@ struct NowPlayingView: View {
} }
} }
} }
if sections.contains(.comments) {
Section {
ForEach(comments.all) { comment in
CommentView(comment: comment, repliesID: $repliesID)
}
}
}
} }
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20)) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20))
.padding(.vertical, 20) .padding(.vertical, 20)

View File

@@ -3,34 +3,39 @@ import SwiftUI
struct TVNavigationView: View { struct TVNavigationView: View {
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<NavigationModel> private var navigation @EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<RecentsModel> private var recents @EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> private var search @EnvironmentObject<SearchModel> private var search
@Default(.visibleSections) private var visibleSections
var body: some View { var body: some View {
TabView(selection: navigation.tabSelectionBinding) { TabView(selection: navigation.tabSelectionBinding) {
FavoritesView() if visibleSections.contains(.favorites) {
.tabItem { Text("Favorites") } FavoritesView()
.tag(TabSelection.favorites) .tabItem { Text("Favorites") }
.tag(TabSelection.favorites)
}
if accounts.app.supportsSubscriptions { if visibleSections.contains(.subscriptions), accounts.app.supportsSubscriptions {
SubscriptionsView() SubscriptionsView()
.tabItem { Text("Subscriptions") } .tabItem { Text("Subscriptions") }
.tag(TabSelection.subscriptions) .tag(TabSelection.subscriptions)
} }
if accounts.app.supportsPopular { if visibleSections.contains(.popular), accounts.app.supportsPopular {
PopularView() PopularView()
.tabItem { Text("Popular") } .tabItem { Text("Popular") }
.tag(TabSelection.popular) .tag(TabSelection.popular)
} }
TrendingView() if visibleSections.contains(.trending) {
.tabItem { Text("Trending") } TrendingView()
.tag(TabSelection.trending) .tabItem { Text("Trending") }
.tag(TabSelection.trending)
}
if accounts.app.supportsUserPlaylists { if visibleSections.contains(.playlists), accounts.app.supportsUserPlaylists {
PlaylistsView() PlaylistsView()
.tabItem { Text("Playlists") } .tabItem { Text("Playlists") }
.tag(TabSelection.playlists) .tag(TabSelection.playlists)