mirror of
https://github.com/yattee/yattee.git
synced 2025-12-12 19:18:16 +00:00
Compare commits
35 Commits
v1.2-beta.
...
v1.2-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe33cc5e3a | ||
|
|
7e7b4e89b5 | ||
|
|
d88292662f | ||
|
|
21b04e21c4 | ||
|
|
a44a61b017 | ||
|
|
1b090fcd51 | ||
|
|
12eb4401b5 | ||
|
|
170f2ee94e | ||
|
|
fe56739211 | ||
|
|
759a942426 | ||
|
|
8d9bbf647a | ||
|
|
eeb7b1f151 | ||
|
|
62bff9283c | ||
|
|
3624c9619a | ||
|
|
f7fc2369e3 | ||
|
|
82ea8733ec | ||
|
|
1f495562fc | ||
|
|
37b99c59e1 | ||
|
|
7f9b53bd1f | ||
|
|
941e6a909d | ||
|
|
5143c4f8ce | ||
|
|
19a3f08336 | ||
|
|
eb537676e6 | ||
|
|
e97daa1944 | ||
|
|
bd59b8e2c3 | ||
|
|
19b146c6ad | ||
|
|
dd995105b5 | ||
|
|
c4b5c7ce41 | ||
|
|
cc2bf90218 | ||
|
|
45c917160e | ||
|
|
9f5e9ea237 | ||
|
|
1c61ad37a9 | ||
|
|
06f7391ad9 | ||
|
|
e61d1dfe2e | ||
|
|
ff83abd103 |
@@ -5,7 +5,12 @@ extension Backport where Content: View {
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
content.badge(count)
|
||||
} else {
|
||||
content
|
||||
HStack {
|
||||
content
|
||||
Spacer()
|
||||
Text("\(count)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ extension Color {
|
||||
static let secondaryBackground = Color(UIColor.secondarySystemBackground)
|
||||
static let tertiaryBackground = Color(UIColor.tertiarySystemBackground)
|
||||
#else
|
||||
static let background = Color.black
|
||||
static let secondaryBackground = Color.black
|
||||
static let tertiaryBackground = Color.black
|
||||
static func background(scheme: ColorScheme) -> Color {
|
||||
scheme == .dark ? .black : .init(white: 0.8)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
18
Fixtures/Comment+Fixtures.swift
Normal file
18
Fixtures/Comment+Fixtures.swift
Normal 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: "")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.environmentObject(AccountsModel())
|
||||
.environmentObject(CommentsModel())
|
||||
.environmentObject(InstancesModel())
|
||||
.environmentObject(invidious)
|
||||
.environmentObject(NavigationModel())
|
||||
|
||||
@@ -27,10 +27,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
self.account = account
|
||||
|
||||
validInstance = false
|
||||
signedIn = false
|
||||
signedIn = !account.anonymous
|
||||
|
||||
configure()
|
||||
validate()
|
||||
}
|
||||
|
||||
func validate() {
|
||||
@@ -257,6 +256,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
.withParam("q", query.lowercased())
|
||||
}
|
||||
|
||||
func comments(_: Video.ID, page _: String?) -> Resource? { nil }
|
||||
|
||||
private func searchQuery(_ query: String) -> String {
|
||||
var searchQuery = query
|
||||
|
||||
|
||||
@@ -71,6 +71,15 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
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 {
|
||||
updateToken()
|
||||
}
|
||||
@@ -80,9 +89,14 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
PipedAPI.authorizedEndpoints.contains { url.absoluteString.contains($0) }
|
||||
}
|
||||
|
||||
@discardableResult func updateToken() -> Request {
|
||||
func updateToken() {
|
||||
guard !account.anonymous else {
|
||||
return
|
||||
}
|
||||
|
||||
account.token = nil
|
||||
return login.request(
|
||||
|
||||
login.request(
|
||||
.post,
|
||||
json: ["username": account.username, "password": account.password]
|
||||
)
|
||||
@@ -161,6 +175,17 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
func playlistVideo(_: String, _: 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 {
|
||||
"**\(path)"
|
||||
}
|
||||
@@ -395,4 +420,23 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
.arrayValue
|
||||
.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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ protocol VideosAPI {
|
||||
|
||||
func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void)
|
||||
func shareURL(_ item: ContentItem, frontendHost: String?, time: CMTime?) -> URL?
|
||||
|
||||
func comments(_ id: Video.ID, page: String?) -> Resource?
|
||||
}
|
||||
|
||||
extension VideosAPI {
|
||||
|
||||
@@ -38,4 +38,8 @@ enum VideosApp: String, CaseIterable {
|
||||
var hasFrontendURL: Bool {
|
||||
self == .piped
|
||||
}
|
||||
|
||||
var supportsComments: Bool {
|
||||
self == .piped
|
||||
}
|
||||
}
|
||||
|
||||
16
Model/Comment.swift
Normal file
16
Model/Comment.swift
Normal 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
110
Model/CommentsModel.swift
Normal 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
7
Model/CommentsPage.swift
Normal file
@@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
struct CommentsPage {
|
||||
var comments = [Comment]()
|
||||
var nextPage: String?
|
||||
var disabled = false
|
||||
}
|
||||
@@ -5,6 +5,11 @@ struct FavoritesModel {
|
||||
static let shared = FavoritesModel()
|
||||
|
||||
@Default(.favorites) var all
|
||||
@Default(.visibleSections) var visibleSections
|
||||
|
||||
var isEnabled: Bool {
|
||||
visibleSections.contains(.favorites)
|
||||
}
|
||||
|
||||
func contains(_ item: FavoriteItem) -> Bool {
|
||||
all.contains { $0 == item }
|
||||
|
||||
@@ -23,7 +23,7 @@ final class NavigationModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@Published var tabSelection: TabSelection! = .favorites
|
||||
@Published var tabSelection: TabSelection!
|
||||
|
||||
@Published var presentingAddToPlaylist = false
|
||||
@Published var videoToAddToPlaylist: Video!
|
||||
|
||||
@@ -43,6 +43,7 @@ final class PlayerModel: ObservableObject {
|
||||
@Published var restoredSegments = [Segment]()
|
||||
|
||||
var accounts: AccountsModel
|
||||
var comments: CommentsModel
|
||||
|
||||
var composition = AVMutableComposition()
|
||||
var loadedCompositionAssets = [AVMediaType]()
|
||||
@@ -67,8 +68,9 @@ final class PlayerModel: ObservableObject {
|
||||
#endif
|
||||
}}
|
||||
|
||||
init(accounts: AccountsModel? = nil, instances _: InstancesModel? = nil) {
|
||||
init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil) {
|
||||
self.accounts = accounts ?? AccountsModel()
|
||||
self.comments = comments ?? CommentsModel()
|
||||
|
||||
addItemDidPlayToEndTimeObserver()
|
||||
addFrequentTimeObserver()
|
||||
@@ -138,6 +140,7 @@ final class PlayerModel: ObservableObject {
|
||||
playerError = nil
|
||||
resetSegments()
|
||||
sponsorBlock.loadSegments(videoID: video.videoID, categories: Defaults[.sponsorBlockCategories])
|
||||
comments.load()
|
||||
|
||||
if let url = stream.singleAssetURL {
|
||||
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)
|
||||
}
|
||||
|
||||
func loadCompositionAsset(
|
||||
private func loadCompositionAsset(
|
||||
_ asset: AVURLAsset,
|
||||
stream: Stream,
|
||||
type: AVMediaType,
|
||||
@@ -478,10 +481,9 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
fileprivate func updateNowPlayingInfo() {
|
||||
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,
|
||||
MPMediaItemPropertyArtist: currentItem.video.author as AnyObject,
|
||||
MPMediaItemPropertyArtwork: currentArtwork as AnyObject,
|
||||
MPMediaItemPropertyPlaybackDuration: duration as AnyObject,
|
||||
MPNowPlayingInfoPropertyIsLiveStream: currentItem.video.live as AnyObject,
|
||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime().seconds as AnyObject,
|
||||
@@ -489,6 +491,10 @@ final class PlayerModel: ObservableObject {
|
||||
MPMediaItemPropertyMediaType: MPMediaType.anyVideo.rawValue as AnyObject
|
||||
]
|
||||
|
||||
if !currentArtwork.isNil {
|
||||
nowPlayingInfo[MPMediaItemPropertyArtwork] = currentArtwork as AnyObject
|
||||
}
|
||||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||
}
|
||||
|
||||
@@ -517,4 +523,10 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
return "\(formatter.string(from: NSNumber(value: rate))!)×"
|
||||
}
|
||||
|
||||
func closeCurrentItem() {
|
||||
addCurrentItemToHistory()
|
||||
currentItem = nil
|
||||
player.replaceCurrentItem(with: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
func playNow(_ video: Video, at time: TimeInterval? = nil) {
|
||||
player.replaceCurrentItem(with: nil)
|
||||
addCurrentItemToHistory()
|
||||
|
||||
enqueueVideo(video, prepending: true) { _, item in
|
||||
@@ -37,6 +38,7 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) {
|
||||
comments.reset()
|
||||
currentItem = item
|
||||
|
||||
if !time.isNil {
|
||||
@@ -91,6 +93,7 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) {
|
||||
player.replaceCurrentItem(with: nil)
|
||||
addCurrentItemToHistory()
|
||||
|
||||
remove(newItem)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import Foundation
|
||||
|
||||
final class RecentsModel: ObservableObject {
|
||||
@Default(.recentlyOpened) var items
|
||||
|
||||
@Default(.saveRecents) var saveRecents
|
||||
func clear() {
|
||||
items = []
|
||||
}
|
||||
@@ -13,6 +13,14 @@ final class RecentsModel: ObservableObject {
|
||||
}
|
||||
|
||||
func add(_ item: RecentItem) {
|
||||
if !saveRecents {
|
||||
clear()
|
||||
|
||||
if item.type != .channel {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let index = items.firstIndex(where: { $0.id == item.id }) {
|
||||
items.remove(at: index)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ final class SearchModel: ObservableObject {
|
||||
@Published var query = SearchQuery()
|
||||
@Published var queryText = ""
|
||||
@Published var querySuggestions = Store<[String]>()
|
||||
@Published var suggestionsText = ""
|
||||
|
||||
@Published var fieldIsFocused = false
|
||||
|
||||
@@ -88,7 +89,7 @@ final class SearchModel: ObservableObject {
|
||||
|
||||
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)
|
||||
|
||||
resource.addObserver(self.querySuggestions)
|
||||
@@ -99,9 +100,11 @@ final class SearchModel: ObservableObject {
|
||||
if let suggestions: [String] = response.typedContent() {
|
||||
self.querySuggestions = Store<[String]>(suggestions)
|
||||
}
|
||||
self.suggestionsText = query
|
||||
}
|
||||
} else {
|
||||
self.querySuggestions = Store<[String]>(self.querySuggestions.collection)
|
||||
self.suggestionsText = query
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
106
README.md
106
README.md
@@ -1,14 +1,12 @@
|
||||

|
||||
|
||||
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.
|
||||
|
||||
[](https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
[](https://github.com/yattee/yattee/issues)
|
||||
[](https://github.com/yattee/yattee/pulls)
|
||||
[](https://matrix.to/#/#yattee:matrix.org)
|
||||
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
@@ -35,101 +33,17 @@ Video player for [Invidious](https://github.com/iv-org/invidious) and [Piped](ht
|
||||
| Search Suggestions | ✅ | ✅ |
|
||||
| Search Filters | ✅ | 🔴 |
|
||||
| Subtitles | 🔴 | ✅ |
|
||||
| Comments | 🔴 | ✅ |
|
||||
|
||||
## Installation
|
||||
### Requirements
|
||||
System requirements:
|
||||
* iOS 14 (or newer)
|
||||
* tvOS 15 (or newer)
|
||||
* macOS Big Sur (or newer)
|
||||
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.
|
||||
|
||||
### How to install?
|
||||
#### [AltStore](https://altstore.io/) (free)
|
||||
You can sideload IPA files downloaded from the [Releases](https://github.com/yattee/yattee/releases) page to your iOS or tvOS device - check [AltStore FAQ](https://altstore.io/faq/) for more information.
|
||||
|
||||
If you have to access to the beta AltStore version (v1.5, for Patreons only), you can add the following repository in `Browse > Sources` screen:
|
||||
|
||||
`https://alt.yattee.stream`
|
||||
|
||||
#### Signing IPA files online (paid)
|
||||
[UDID Registrations](https://www.udidregistrations.com/) provides services to sign IPA files for your devices. Refer to: ***Break free from the App Store*** section of the website for more information.
|
||||
|
||||
#### Manual installation
|
||||
Download sources and compile them on a Mac using Xcode, install to your devices. Please note that if you are not registered in Apple Developer Program 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 |
|
||||
| - | - | - |
|
||||
| [](https://r.yattee.stream/screenshots/iOS/player.png) | [](https://r.yattee.stream/screenshots/iOS/search-suggestions.png) | [](https://r.yattee.stream/screenshots/iOS/playlists.png) |
|
||||
### iPadOS
|
||||
| Settings | Player | Subscriptions |
|
||||
| - | - | - |
|
||||
| [](https://r.yattee.stream/screenshots/iPadOS/settings.png) | [](https://r.yattee.stream/screenshots/iPadOS/player.png) | [](https://r.yattee.stream/screenshots/iPadOS/subscriptions.png) |
|
||||
### tvOS
|
||||
| Player | Popular | Search | Now Playing | Settings |
|
||||
| - | - | - | - | - |
|
||||
| [](https://r.yattee.stream/screenshots/tvOS/player.png) | [](https://r.yattee.stream/screenshots/tvOS/popular.png) | [](https://r.yattee.stream/screenshots/tvOS/search.png) | [](https://r.yattee.stream/screenshots/tvOS/now-playing.png) | [](https://r.yattee.stream/screenshots/tvOS/settings.png) |
|
||||
### macOS
|
||||
| Player | Channel | Search | Settings |
|
||||
| - | - | - | - |
|
||||
| [](https://r.yattee.stream/screenshots/macOS/player.png) | [](https://r.yattee.stream/screenshots/macOS/channel.png) | [](https://r.yattee.stream/screenshots/macOS/search.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.
|
||||
## Documentation
|
||||
* [Installation Instructions](https://github.com/yattee/yattee/wiki/Installation-instructions)
|
||||
* [Integrations](https://github.com/yattee/yattee/wiki/Integrations)
|
||||
* [Screenshots Gallery](https://github.com/yattee/yattee/wiki/Screenshots-Gallery)
|
||||
* [Tips](https://github.com/yattee/yattee/wiki/Tips)
|
||||
* [FAQ](https://github.com/yattee/yattee/wiki)
|
||||
* [Donations](https://github.com/yattee/yattee/wiki/Donations)
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -2,10 +2,11 @@ import Defaults
|
||||
import Foundation
|
||||
|
||||
extension Defaults.Keys {
|
||||
static let kavinPipedInstanceID = "kavin-piped"
|
||||
static let instances = Key<[Instance]>("instances", default: [
|
||||
.init(
|
||||
app: .piped,
|
||||
id: "default-piped-instance",
|
||||
id: kavinPipedInstanceID,
|
||||
name: "Kavin",
|
||||
apiURL: "https://pipedapi.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 playerInstanceID = Key<Instance.ID?>("playerInstance")
|
||||
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: [])
|
||||
|
||||
@@ -40,12 +45,15 @@ extension Defaults.Keys {
|
||||
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
|
||||
|
||||
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 trendingCountry = Key<Country>("trendingCountry", default: .us)
|
||||
|
||||
#if os(iOS)
|
||||
static let tabNavigationSection = Key<TabNavigationSectionSetting>("tabNavigationSection", default: .trending)
|
||||
static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.favorites, .subscriptions, .trending, .playlists])
|
||||
|
||||
#if os(macOS)
|
||||
static let enableBetaChannel = Key<Bool>("enableBetaChannel", default: false)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -83,8 +91,54 @@ enum PlayerSidebarSetting: String, CaseIterable, Defaults.Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
enum TabNavigationSectionSetting: String, Defaults.Serializable {
|
||||
case trending, popular
|
||||
enum VisibleSection: String, CaseIterable, Comparable, Defaults.Serializable {
|
||||
case favorites, subscriptions, popular, trending, playlists
|
||||
|
||||
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
|
||||
|
||||
@@ -66,6 +66,10 @@ struct FavoriteItemView: View {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.onChange(of: accounts.current) { _ in
|
||||
resource?.addObserver(store)
|
||||
resource?.load()
|
||||
}
|
||||
}
|
||||
|
||||
private var isVisible: Bool {
|
||||
|
||||
@@ -29,6 +29,9 @@ struct FavoritesView: View {
|
||||
#else
|
||||
ForEach(favorites) { item in
|
||||
FavoriteItemView(item: item, dragging: $dragging)
|
||||
#if os(macOS)
|
||||
.workaroundForVerticalScrollingBug()
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
#if os(iOS)
|
||||
import Introspect
|
||||
@@ -5,8 +6,19 @@ import SwiftUI
|
||||
|
||||
struct AppSidebarNavigation: View {
|
||||
@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)
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@State private var didApplyPrimaryViewWorkAround = false
|
||||
#endif
|
||||
|
||||
@@ -38,15 +50,50 @@ struct AppSidebarNavigation: View {
|
||||
.frame(minWidth: sidebarMinWidth)
|
||||
|
||||
VStack {
|
||||
Image(systemName: "play.tv")
|
||||
.renderingMode(.original)
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(.accentColor)
|
||||
PlayerControlsView {
|
||||
HStack(alignment: .center) {
|
||||
Spacer()
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
|
||||
@@ -3,74 +3,41 @@ import SwiftUI
|
||||
|
||||
struct AppTabNavigation: View {
|
||||
@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(.tabNavigationSection) private var tabNavigationSection
|
||||
@Default(.visibleSections) private var visibleSections
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: navigation.tabSelectionBinding) {
|
||||
NavigationView {
|
||||
LazyView(FavoritesView())
|
||||
.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 visibleSections.contains(.favorites) {
|
||||
favoritesNavigationView
|
||||
}
|
||||
|
||||
if subscriptionsVisible {
|
||||
if accounts.app.supportsPopular {
|
||||
if tabNavigationSection == .popular {
|
||||
popularNavigationView
|
||||
} else {
|
||||
trendingNavigationView
|
||||
}
|
||||
} else {
|
||||
trendingNavigationView
|
||||
}
|
||||
} else {
|
||||
if accounts.app.supportsPopular {
|
||||
popularNavigationView
|
||||
}
|
||||
subscriptionsNavigationView
|
||||
}
|
||||
|
||||
if visibleSections.contains(.popular), accounts.app.supportsPopular, visibleSections.count < 5 {
|
||||
popularNavigationView
|
||||
}
|
||||
|
||||
if visibleSections.contains(.trending) {
|
||||
trendingNavigationView
|
||||
}
|
||||
|
||||
if accounts.app.supportsUserPlaylists {
|
||||
NavigationView {
|
||||
LazyView(PlaylistsView())
|
||||
.toolbar { toolbarContent }
|
||||
}
|
||||
.tabItem {
|
||||
Label("Playlists", systemImage: "list.and.film")
|
||||
.accessibility(label: Text("Playlists"))
|
||||
}
|
||||
.tag(TabSelection.playlists)
|
||||
if visibleSections.contains(.playlists), accounts.app.supportsUserPlaylists {
|
||||
playlistsNavigationView
|
||||
}
|
||||
|
||||
NavigationView {
|
||||
LazyView(SearchView())
|
||||
}
|
||||
.tabItem {
|
||||
Label("Search", systemImage: "magnifyingglass")
|
||||
.accessibility(label: Text("Search"))
|
||||
}
|
||||
.tag(TabSelection.search)
|
||||
searchNavigationView
|
||||
}
|
||||
.id(accounts.current?.id ?? "")
|
||||
.environment(\.navigationStyle, .tab)
|
||||
@@ -100,15 +67,47 @@ struct AppTabNavigation: View {
|
||||
NavigationView {
|
||||
ChannelPlaylistView(playlist: playlist)
|
||||
.environment(\.inNavigationView, true)
|
||||
.environmentObject(subscriptions)
|
||||
.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 {
|
||||
accounts.app.supportsSubscriptions && !(accounts.current?.anonymous ?? true)
|
||||
visibleSections.contains(.subscriptions) &&
|
||||
accounts.app.supportsSubscriptions && !(accounts.current?.anonymous ?? true)
|
||||
}
|
||||
|
||||
private var popularNavigationView: some View {
|
||||
@@ -135,6 +134,30 @@ struct AppTabNavigation: View {
|
||||
.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 {
|
||||
NavigationLink(isActive: $player.playerNavigationLinkActive, destination: {
|
||||
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 {
|
||||
#if os(iOS)
|
||||
Group {
|
||||
|
||||
@@ -8,6 +8,7 @@ import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject private var accounts = AccountsModel()
|
||||
@StateObject private var comments = CommentsModel()
|
||||
@StateObject private var instances = InstancesModel()
|
||||
@StateObject private var navigation = NavigationModel()
|
||||
@StateObject private var player = PlayerModel()
|
||||
@@ -40,6 +41,7 @@ struct ContentView: View {
|
||||
.onAppear(perform: configure)
|
||||
|
||||
.environmentObject(accounts)
|
||||
.environmentObject(comments)
|
||||
.environmentObject(instances)
|
||||
.environmentObject(navigation)
|
||||
.environmentObject(player)
|
||||
@@ -58,20 +60,6 @@ struct ContentView: View {
|
||||
.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)
|
||||
.handlesExternalEvents(preferring: Set(["*"]), allowing: Set(["*"]))
|
||||
.onOpenURL(perform: handleOpenedURL)
|
||||
@@ -98,21 +86,10 @@ struct ContentView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
private var videoPlayer: some View {
|
||||
VideoPlayerView()
|
||||
.environmentObject(accounts)
|
||||
.environmentObject(instances)
|
||||
.environmentObject(navigation)
|
||||
.environmentObject(player)
|
||||
.environmentObject(playlists)
|
||||
.environmentObject(subscriptions)
|
||||
.environmentObject(thumbnailsModel)
|
||||
}
|
||||
|
||||
func configure() {
|
||||
SiestaLog.Category.enabled = .common
|
||||
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
|
||||
SDWebImageManager.defaultImageCache = PINCache(name: "net.yattee.app")
|
||||
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
|
||||
#if !os(macOS)
|
||||
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
||||
#endif
|
||||
@@ -128,18 +105,37 @@ struct ContentView: View {
|
||||
navigation.presentingWelcomeScreen = true
|
||||
}
|
||||
|
||||
player.accounts = accounts
|
||||
playlists.accounts = accounts
|
||||
search.accounts = accounts
|
||||
subscriptions.accounts = accounts
|
||||
|
||||
comments.accounts = accounts
|
||||
comments.player = player
|
||||
|
||||
menu.accounts = accounts
|
||||
menu.navigation = navigation
|
||||
menu.player = player
|
||||
|
||||
player.accounts = accounts
|
||||
player.comments = comments
|
||||
|
||||
if !accounts.current.isNil {
|
||||
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() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct Sidebar: View {
|
||||
@@ -6,6 +7,8 @@ struct Sidebar: View {
|
||||
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
@Default(.visibleSections) private var visibleSections
|
||||
|
||||
var body: some View {
|
||||
ScrollViewReader { scrollView in
|
||||
List {
|
||||
@@ -16,11 +19,11 @@ struct Sidebar: View {
|
||||
.id("recentlyOpened")
|
||||
|
||||
if accounts.api.signedIn {
|
||||
if accounts.app.supportsSubscriptions {
|
||||
if visibleSections.contains(.subscriptions), accounts.app.supportsSubscriptions {
|
||||
AppSidebarSubscriptions()
|
||||
}
|
||||
|
||||
if accounts.app.supportsUserPlaylists {
|
||||
if visibleSections.contains(.playlists), accounts.app.supportsUserPlaylists {
|
||||
AppSidebarPlaylists()
|
||||
}
|
||||
}
|
||||
@@ -47,27 +50,33 @@ struct Sidebar: View {
|
||||
|
||||
var mainNavigationLinks: some View {
|
||||
Section(header: Text("Videos")) {
|
||||
NavigationLink(destination: LazyView(FavoritesView()), tag: TabSelection.favorites, selection: $navigation.tabSelection) {
|
||||
Label("Favorites", systemImage: "heart")
|
||||
.accessibility(label: Text("Favorites"))
|
||||
if visibleSections.contains(.favorites) {
|
||||
NavigationLink(destination: LazyView(FavoritesView()), tag: TabSelection.favorites, selection: $navigation.tabSelection) {
|
||||
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) {
|
||||
Label("Subscriptions", systemImage: "star.circle")
|
||||
.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) {
|
||||
Label("Popular", systemImage: "arrow.up.right.circle")
|
||||
.accessibility(label: Text("Popular"))
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(destination: LazyView(TrendingView()), tag: TabSelection.trending, selection: $navigation.tabSelection) {
|
||||
Label("Trending", systemImage: "chart.bar")
|
||||
.accessibility(label: Text("Trending"))
|
||||
if visibleSections.contains(.trending) {
|
||||
NavigationLink(destination: LazyView(TrendingView()), tag: TabSelection.trending, selection: $navigation.tabSelection) {
|
||||
Label("Trending", systemImage: "chart.bar")
|
||||
.accessibility(label: Text("Trending"))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
scrollView.scrollTo("recentlyOpened")
|
||||
} else if case let .playlist(id) = selection {
|
||||
|
||||
251
Shared/Player/CommentView.swift
Normal file
251
Shared/Player/CommentView.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
91
Shared/Player/CommentsView.swift
Normal file
91
Shared/Player/CommentsView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct Player: UIViewControllerRepresentable {
|
||||
@EnvironmentObject<CommentsModel> private var comments
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
|
||||
@@ -18,6 +19,7 @@ struct Player: UIViewControllerRepresentable {
|
||||
|
||||
let controller = PlayerViewController()
|
||||
|
||||
controller.commentsModel = comments
|
||||
controller.navigationModel = navigation
|
||||
controller.playerModel = player
|
||||
player.controller = controller
|
||||
|
||||
@@ -44,18 +44,11 @@ struct PlayerQueueView: View {
|
||||
}
|
||||
|
||||
ForEach(player.queue) { item in
|
||||
let row = PlayerQueueRow(item: item, fullScreen: $fullScreen)
|
||||
PlayerQueueRow(item: item, fullScreen: $fullScreen)
|
||||
.contextMenu {
|
||||
removeButton(item, 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 {
|
||||
Section(header: Text("Played Previously")) {
|
||||
ForEach(player.history) { item in
|
||||
let row = PlayerQueueRow(item: item, history: true, fullScreen: $fullScreen)
|
||||
PlayerQueueRow(item: item, history: true, fullScreen: $fullScreen)
|
||||
.contextMenu {
|
||||
removeButton(item, 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 {
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
return Button(role: .destructive) {
|
||||
removeButtonAction(item, history: history)
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}
|
||||
} else {
|
||||
return Button {
|
||||
removeButtonAction(item, history: history)
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}
|
||||
Button {
|
||||
removeButtonAction(item, history: history)
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,18 +102,10 @@ struct PlayerQueueView: View {
|
||||
}
|
||||
|
||||
private func removeAllButton(history: Bool) -> some View {
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
return Button(role: .destructive) {
|
||||
removeAllButtonAction(history: history)
|
||||
} label: {
|
||||
Label("Remove All", systemImage: "trash.fill")
|
||||
}
|
||||
} else {
|
||||
return Button {
|
||||
removeAllButtonAction(history: history)
|
||||
} label: {
|
||||
Label("Remove All", systemImage: "trash.fill")
|
||||
}
|
||||
Button {
|
||||
removeAllButtonAction(history: history)
|
||||
} label: {
|
||||
Label("Remove All", systemImage: "trash.fill")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import SwiftUI
|
||||
|
||||
final class PlayerViewController: UIViewController {
|
||||
var playerLoaded = false
|
||||
var commentsModel: CommentsModel!
|
||||
var navigationModel: NavigationModel!
|
||||
var playerModel: PlayerModel!
|
||||
var playerViewController = AVPlayerViewController()
|
||||
@@ -44,10 +45,16 @@ final class PlayerViewController: UIViewController {
|
||||
|
||||
#if os(tvOS)
|
||||
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([.playingNext, .playedPreviously], title: "Playing Next")
|
||||
]
|
||||
])
|
||||
|
||||
playerViewController.customInfoViewControllers = infoViewControllers
|
||||
#else
|
||||
embedViewController()
|
||||
#endif
|
||||
@@ -62,6 +69,7 @@ final class PlayerViewController: UIViewController {
|
||||
AnyView(
|
||||
NowPlayingView(sections: sections, inInfoViewController: true)
|
||||
.frame(maxHeight: 600)
|
||||
.environmentObject(commentsModel)
|
||||
.environmentObject(playerModel)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -9,6 +9,18 @@ struct RelatedView: View {
|
||||
Section(header: Text("Related")) {
|
||||
ForEach(player.currentVideo!.related) { video in
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import SwiftUI
|
||||
|
||||
struct VideoDetails: View {
|
||||
enum Page {
|
||||
case details, queue, related
|
||||
case info, queue, related, comments
|
||||
}
|
||||
|
||||
@Binding var sidebarQueue: Bool
|
||||
@@ -16,7 +16,7 @@ struct VideoDetails: View {
|
||||
@State private var presentingShareSheet = false
|
||||
@State private var shareURL: URL?
|
||||
|
||||
@State private var currentPage = Page.details
|
||||
@State private var currentPage = Page.info
|
||||
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
@@ -65,7 +65,7 @@ struct VideoDetails: View {
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
if !sidebarQueue {
|
||||
if CommentsModel.enabled, CommentsModel.placement == .separate {
|
||||
pagePicker
|
||||
.padding(.horizontal)
|
||||
}
|
||||
@@ -89,7 +89,7 @@ struct VideoDetails: View {
|
||||
)
|
||||
|
||||
switch currentPage {
|
||||
case .details:
|
||||
case .info:
|
||||
ScrollView(.vertical) {
|
||||
detailsPage
|
||||
}
|
||||
@@ -100,6 +100,9 @@ struct VideoDetails: View {
|
||||
case .related:
|
||||
RelatedView()
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
case .comments:
|
||||
CommentsView()
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
}
|
||||
}
|
||||
.padding(.top, inNavigationView && fullScreen ? 10 : 0)
|
||||
@@ -116,7 +119,7 @@ struct VideoDetails: View {
|
||||
.onChange(of: sidebarQueue) { queue in
|
||||
if queue {
|
||||
if currentPage == .queue {
|
||||
currentPage = .details
|
||||
currentPage = .info
|
||||
}
|
||||
} else if video.isNil {
|
||||
currentPage = .queue
|
||||
@@ -131,7 +134,20 @@ struct VideoDetails: View {
|
||||
if video != nil {
|
||||
Text(video!.title)
|
||||
.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())
|
||||
@@ -228,15 +244,23 @@ struct VideoDetails: View {
|
||||
var pagePicker: some View {
|
||||
Picker("Page", selection: $currentPage) {
|
||||
if !video.isNil {
|
||||
Text("Details").tag(Page.details)
|
||||
Text("Related").tag(Page.related)
|
||||
Text("Info").tag(Page.info)
|
||||
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()
|
||||
.pickerStyle(.segmented)
|
||||
.onDisappear {
|
||||
currentPage = .details
|
||||
currentPage = .info
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,19 +310,19 @@ struct VideoDetails: View {
|
||||
Spacer()
|
||||
|
||||
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 {
|
||||
Divider()
|
||||
|
||||
videoDetail(label: "Likes", value: likes, symbol: "hand.thumbsup.circle.fill")
|
||||
videoDetail(label: "Likes", value: likes, symbol: "hand.thumbsup")
|
||||
}
|
||||
|
||||
if let dislikes = video.dislikesCount {
|
||||
Divider()
|
||||
|
||||
videoDetail(label: "Dislikes", value: dislikes, symbol: "hand.thumbsdown.circle.fill")
|
||||
videoDetail(label: "Dislikes", value: dislikes, symbol: "hand.thumbsdown")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -342,64 +366,81 @@ struct VideoDetails: View {
|
||||
|
||||
var detailsPage: some View {
|
||||
Group {
|
||||
if let video = player.currentItem?.video {
|
||||
Group {
|
||||
HStack {
|
||||
publishedDateSection
|
||||
Spacer()
|
||||
Group {
|
||||
if let video = player.currentItem?.video {
|
||||
Group {
|
||||
HStack {
|
||||
publishedDateSection
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
countsSection
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
countsSection
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if let description = video.description {
|
||||
Group {
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
Text(description)
|
||||
.textSelection(.enabled)
|
||||
} else {
|
||||
Text(description)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.font(.caption)
|
||||
.padding(.bottom, 4)
|
||||
} 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))
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if let description = video.description {
|
||||
Group {
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
Text(description)
|
||||
.textSelection(.enabled)
|
||||
} else {
|
||||
Text(description)
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
|
||||
@@ -105,8 +105,9 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 650)
|
||||
.frame(minWidth: 650)
|
||||
#endif
|
||||
#if os(iOS)
|
||||
if sidebarQueue {
|
||||
@@ -116,7 +117,7 @@ struct VideoPlayerView: View {
|
||||
#elseif os(macOS)
|
||||
if Defaults[.playerSidebar] != .never {
|
||||
PlayerQueueView(sidebarQueue: sidebarQueueBinding, fullScreen: $fullScreen)
|
||||
.frame(minWidth: 250)
|
||||
.frame(minWidth: 300)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ struct AddToPlaylistView: View {
|
||||
|
||||
@State private var selectedPlaylistID: Playlist.ID = ""
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@EnvironmentObject<PlaylistsModel> private var model
|
||||
|
||||
@@ -37,7 +38,7 @@ struct AddToPlaylistView: View {
|
||||
.padding(.vertical)
|
||||
#elseif os(tvOS)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.background(Color.tertiaryBackground)
|
||||
.background(Color.background(scheme: colorScheme))
|
||||
#else
|
||||
.padding(.vertical)
|
||||
#endif
|
||||
|
||||
@@ -10,6 +10,7 @@ struct PlaylistFormView: View {
|
||||
@State private var valid = false
|
||||
@State private var showingDeleteConfirmation = false
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@@ -77,7 +78,7 @@ struct PlaylistFormView: View {
|
||||
.frame(maxWidth: 1000)
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.background(Color.tertiaryBackground)
|
||||
.background(Color.background(scheme: colorScheme))
|
||||
#endif
|
||||
}
|
||||
.onChange(of: name) { _ in validate() }
|
||||
|
||||
@@ -6,6 +6,12 @@ struct SearchTextField: View {
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SearchModel> private var state
|
||||
|
||||
@Binding var favoriteItem: FavoriteItem?
|
||||
|
||||
init(favoriteItem: Binding<FavoriteItem?>? = nil) {
|
||||
_favoriteItem = favoriteItem ?? .constant(nil)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
#if os(macOS)
|
||||
@@ -31,14 +37,27 @@ struct SearchTextField: View {
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.frame(maxWidth: 190)
|
||||
.textFieldStyle(.plain)
|
||||
#else
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.leading)
|
||||
.padding(.trailing, 15)
|
||||
#endif
|
||||
|
||||
if !self.state.queryText.isEmpty {
|
||||
#if os(iOS)
|
||||
FavoriteButton(item: favoriteItem)
|
||||
.id(favoriteItem?.id)
|
||||
.labelStyle(.iconOnly)
|
||||
.padding(.trailing)
|
||||
#endif
|
||||
clearButton
|
||||
} else {
|
||||
#if os(macOS)
|
||||
clearButton
|
||||
.opacity(0)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@ struct SearchSuggestions: View {
|
||||
|
||||
recents.addQuery(state.queryText)
|
||||
} label: {
|
||||
HStack(spacing: 5) {
|
||||
Label(state.queryText, systemImage: "magnifyingglass")
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
Text(state.queryText)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
@@ -27,15 +28,19 @@ struct SearchSuggestions: View {
|
||||
Button {
|
||||
state.queryText = suggestion
|
||||
} label: {
|
||||
HStack(spacing: 0) {
|
||||
Label(state.queryText, systemImage: "arrow.up.left.circle")
|
||||
.lineLimit(1)
|
||||
.layoutPriority(2)
|
||||
HStack {
|
||||
Image(systemName: "arrow.up.left.circle")
|
||||
.foregroundColor(.secondary)
|
||||
HStack(spacing: 0) {
|
||||
Text(state.suggestionsText)
|
||||
.lineLimit(1)
|
||||
.layoutPriority(2)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(querySuffix(suggestion))
|
||||
.lineLimit(1)
|
||||
.layoutPriority(1)
|
||||
Text(querySuffix(suggestion))
|
||||
.lineLimit(1)
|
||||
.layoutPriority(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
@@ -55,7 +60,7 @@ struct SearchSuggestions: View {
|
||||
}
|
||||
|
||||
private func querySuffix(_ suggestion: String) -> String {
|
||||
suggestion.replacingFirstOccurrence(of: state.queryText.lowercased(), with: "")
|
||||
suggestion.replacingFirstOccurrence(of: state.suggestionsText.lowercased(), with: "")
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
@@ -23,6 +23,9 @@ struct SearchView: View {
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SearchModel> private var state
|
||||
private var favorites = FavoritesModel.shared
|
||||
|
||||
@Default(.saveRecents) private var saveRecents
|
||||
|
||||
private var videos = [Video]()
|
||||
|
||||
@@ -39,7 +42,7 @@ struct SearchView: View {
|
||||
PlayerControlsView {
|
||||
#if os(iOS)
|
||||
VStack {
|
||||
SearchTextField()
|
||||
SearchTextField(favoriteItem: $favoriteItem)
|
||||
|
||||
if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty {
|
||||
SearchSuggestions()
|
||||
@@ -51,14 +54,16 @@ struct SearchView: View {
|
||||
ZStack {
|
||||
results
|
||||
|
||||
if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty {
|
||||
HStack {
|
||||
Spacer()
|
||||
SearchSuggestions()
|
||||
.borderLeading(width: 1, color: Color("ControlsBorderColor"))
|
||||
.frame(maxWidth: 280)
|
||||
#if !os(tvOS)
|
||||
if state.query.query != state.queryText, !state.queryText.isEmpty, !state.querySuggestions.collection.isEmpty {
|
||||
HStack {
|
||||
Spacer()
|
||||
SearchSuggestions()
|
||||
.borderLeading(width: 1, color: Color("ControlsBorderColor"))
|
||||
.frame(maxWidth: 280)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -66,10 +71,8 @@ struct SearchView: View {
|
||||
#if !os(tvOS)
|
||||
ToolbarItemGroup(placement: toolbarPlacement) {
|
||||
#if os(macOS)
|
||||
if let favoriteItem = favoriteItem {
|
||||
FavoriteButton(item: favoriteItem)
|
||||
.id(favoriteItem.id)
|
||||
}
|
||||
FavoriteButton(item: favoriteItem)
|
||||
.id(favoriteItem?.id)
|
||||
#endif
|
||||
|
||||
if accounts.app.supportsSearchFilters {
|
||||
@@ -90,17 +93,6 @@ struct SearchView: View {
|
||||
.transaction { t in t.animation = .none }
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
Spacer()
|
||||
|
||||
if let favoriteItem = favoriteItem {
|
||||
FavoriteButton(item: favoriteItem)
|
||||
.id(favoriteItem.id)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
#endif
|
||||
|
||||
if accounts.app.supportsSearchFilters {
|
||||
filtersMenu
|
||||
}
|
||||
@@ -171,12 +163,20 @@ struct SearchView: View {
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
.navigationTitle("Search")
|
||||
#if os(tvOS)
|
||||
.searchable(text: $state.queryText) {
|
||||
ForEach(state.querySuggestions.collection, id: \.self) { suggestion in
|
||||
Text(suggestion)
|
||||
.searchCompletion(suggestion)
|
||||
}
|
||||
}
|
||||
#else
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
.navigationTitle("Search")
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarHidden(!Defaults[.visibleSections].isEmpty || navigationStyle == .sidebar)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -192,12 +192,10 @@ struct SearchView: View {
|
||||
filtersHorizontalStack
|
||||
}
|
||||
|
||||
if let favoriteItem = favoriteItem {
|
||||
FavoriteButton(item: favoriteItem)
|
||||
.id(favoriteItem.id)
|
||||
.labelStyle(.iconOnly)
|
||||
.font(.system(size: 25))
|
||||
}
|
||||
FavoriteButton(item: favoriteItem)
|
||||
.id(favoriteItem?.id)
|
||||
.labelStyle(.iconOnly)
|
||||
.font(.system(size: 25))
|
||||
}
|
||||
|
||||
HorizontalCells(items: items)
|
||||
@@ -222,14 +220,14 @@ struct SearchView: View {
|
||||
|
||||
private var toolbarPlacement: ToolbarItemPlacement {
|
||||
#if os(iOS)
|
||||
.bottomBar
|
||||
accounts.app.supportsSearchFilters || favorites.isEnabled ? .bottomBar : .automatic
|
||||
#else
|
||||
.automatic
|
||||
.automatic
|
||||
#endif
|
||||
}
|
||||
|
||||
private var showRecentQueries: Bool {
|
||||
navigationStyle == .tab && state.queryText.isEmpty
|
||||
navigationStyle == .tab && saveRecents && state.queryText.isEmpty
|
||||
}
|
||||
|
||||
private var filtersActive: Bool {
|
||||
|
||||
@@ -15,6 +15,7 @@ struct AccountForm: View {
|
||||
@State private var validationError: String?
|
||||
@State private var validationDebounce = Debounce()
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
var body: some View {
|
||||
@@ -30,7 +31,7 @@ struct AccountForm: View {
|
||||
.padding(.vertical)
|
||||
#elseif os(tvOS)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.background(Color.tertiaryBackground)
|
||||
.background(Color.background(scheme: colorScheme))
|
||||
#else
|
||||
.frame(width: 400, height: 145)
|
||||
#endif
|
||||
|
||||
@@ -4,42 +4,102 @@ import SwiftUI
|
||||
struct BrowsingSettings: View {
|
||||
@Default(.channelOnThumbnail) private var channelOnThumbnail
|
||||
@Default(.timeOnThumbnail) private var timeOnThumbnail
|
||||
#if os(iOS)
|
||||
@Default(.tabNavigationSection) private var tabNavigationSection
|
||||
#endif
|
||||
@Default(.saveRecents) private var saveRecents
|
||||
@Default(.saveHistory) private var saveHistory
|
||||
@Default(.visibleSections) private var visibleSections
|
||||
|
||||
var body: some View {
|
||||
Section(header: SettingsHeader(text: "Browsing"), footer: footer) {
|
||||
Toggle("Display channel names on thumbnails", isOn: $channelOnThumbnail)
|
||||
Toggle("Display video length on thumbnails", isOn: $timeOnThumbnail)
|
||||
Group {
|
||||
Section(header: SettingsHeader(text: "Browsing")) {
|
||||
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)
|
||||
preferredTabPicker
|
||||
#endif
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
Group {
|
||||
if #available(macOS 12.0, *) {
|
||||
list
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
} else {
|
||||
list
|
||||
.listStyle(.inset)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
Spacer()
|
||||
#endif
|
||||
}
|
||||
|
||||
var footer: some View {
|
||||
#if os(iOS)
|
||||
Text("This tab will be displayed when there is no space to display all tabs")
|
||||
#else
|
||||
EmptyView()
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
var preferredTabPicker: some View {
|
||||
Picker("Preferred tab", selection: $tabNavigationSection) {
|
||||
Text("Trending").tag(TabNavigationSectionSetting.trending)
|
||||
Text("Popular").tag(TabNavigationSectionSetting.popular)
|
||||
Spacer()
|
||||
}
|
||||
#else
|
||||
ForEach(VisibleSection.allCases, id: \.self) { section in
|
||||
VisibleSectionSelectionRow(
|
||||
title: section.title,
|
||||
selected: visibleSections.contains(section)
|
||||
) { value in
|
||||
toggleSection(section, value: value)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#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 {
|
||||
|
||||
@@ -13,6 +13,7 @@ struct InstanceForm: View {
|
||||
@State private var validationError: String?
|
||||
@State private var validationDebounce = Debounce()
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
var body: some View {
|
||||
@@ -32,7 +33,7 @@ struct InstanceForm: View {
|
||||
.padding(.vertical)
|
||||
#elseif os(tvOS)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.background(Color.tertiaryBackground)
|
||||
.background(Color.background(scheme: colorScheme))
|
||||
#else
|
||||
.frame(width: 400, height: 190)
|
||||
#endif
|
||||
@@ -76,6 +77,7 @@ struct InstanceForm: View {
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.labelsHidden()
|
||||
|
||||
TextField("Name", text: $name)
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ struct PlaybackSettings: View {
|
||||
}
|
||||
|
||||
keywordsToggle
|
||||
saveHistoryToggle
|
||||
}
|
||||
#else
|
||||
Section(header: SettingsHeader(text: "Source")) {
|
||||
@@ -45,7 +44,6 @@ struct PlaybackSettings: View {
|
||||
#endif
|
||||
|
||||
keywordsToggle
|
||||
saveHistoryToggle
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -59,7 +57,7 @@ struct PlaybackSettings: View {
|
||||
Text("Best available stream").tag(String?.none)
|
||||
|
||||
ForEach(instances) { instance in
|
||||
Text(instance.longDescription).tag(Optional(instance.id))
|
||||
Text(instance.description).tag(Optional(instance.id))
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
@@ -109,10 +107,6 @@ struct PlaybackSettings: View {
|
||||
private var keywordsToggle: some View {
|
||||
Toggle("Show video keywords", isOn: $showKeywords)
|
||||
}
|
||||
|
||||
private var saveHistoryToggle: some View {
|
||||
Toggle("Save history of played videos", isOn: $saveHistory)
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaybackSettings_Previews: PreviewProvider {
|
||||
|
||||
@@ -4,8 +4,21 @@ import SwiftUI
|
||||
struct ServicesSettings: View {
|
||||
@Default(.sponsorBlockInstance) private var sponsorBlockInstance
|
||||
@Default(.sponsorBlockCategories) private var sponsorBlockCategories
|
||||
@Default(.commentsInstanceID) private var commentsInstanceID
|
||||
|
||||
#if !os(tvOS)
|
||||
@Default(.commentsPlacement) private var commentsPlacement
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
Section(header: SettingsHeader(text: "Comments")) {
|
||||
commentsInstancePicker
|
||||
#if !os(tvOS)
|
||||
commentsPlacementPicker
|
||||
.disabled(!CommentsModel.enabled)
|
||||
#endif
|
||||
}
|
||||
|
||||
Section(header: SettingsHeader(text: "SponsorBlock API")) {
|
||||
TextField(
|
||||
"SponsorBlock API Instance",
|
||||
@@ -20,7 +33,7 @@ struct ServicesSettings: View {
|
||||
|
||||
Section(header: SettingsHeader(text: "Categories to Skip")) {
|
||||
#if os(macOS)
|
||||
let list = List(SponsorBlockAPI.categories, id: \.self) { category in
|
||||
let list = ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
||||
SponsorBlockCategorySelectionRow(
|
||||
title: SponsorBlockAPI.categoryDescription(category) ?? "Unknown",
|
||||
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) {
|
||||
if let index = sponsorBlockCategories.firstIndex(where: { $0 == category }), !value {
|
||||
sponsorBlockCategories.remove(at: index)
|
||||
|
||||
@@ -5,10 +5,12 @@ import SwiftUI
|
||||
struct SettingsView: View {
|
||||
#if os(macOS)
|
||||
private enum Tabs: Hashable {
|
||||
case instances, browsing, playback, services
|
||||
case instances, browsing, playback, services, updates
|
||||
}
|
||||
#endif
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
#endif
|
||||
@@ -44,7 +46,7 @@ struct SettingsView: View {
|
||||
PlaybackSettings()
|
||||
}
|
||||
.tabItem {
|
||||
Label("Playback", systemImage: "play.rectangle.on.rectangle.fill")
|
||||
Label("Playback", systemImage: "play.rectangle")
|
||||
}
|
||||
.tag(Tabs.playback)
|
||||
|
||||
@@ -52,9 +54,17 @@ struct SettingsView: View {
|
||||
ServicesSettings()
|
||||
}
|
||||
.tabItem {
|
||||
Label("Services", systemImage: "puzzlepiece.extension")
|
||||
Label("Services", systemImage: "puzzlepiece")
|
||||
}
|
||||
.tag(Tabs.services)
|
||||
|
||||
Form {
|
||||
UpdatesSettings()
|
||||
}
|
||||
.tabItem {
|
||||
Label("Updates", systemImage: "gearshape.2")
|
||||
}
|
||||
.tag(Tabs.updates)
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 400, height: 380)
|
||||
@@ -102,7 +112,7 @@ struct SettingsView: View {
|
||||
InstanceForm(savedInstanceID: $savedFormInstanceID)
|
||||
}
|
||||
#if os(tvOS)
|
||||
.background(Color.black)
|
||||
.background(Color.background(scheme: colorScheme))
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ struct ChannelPlaylistView: View {
|
||||
|
||||
@StateObject private var store = Store<ChannelPlaylist>()
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
#endif
|
||||
@@ -83,7 +85,7 @@ struct ChannelPlaylistView: View {
|
||||
.navigationTitle(playlist.title)
|
||||
|
||||
#else
|
||||
.background(Color.tertiaryBackground)
|
||||
.background(Color.background(scheme: colorScheme))
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ struct ChannelVideosView: View {
|
||||
|
||||
@StateObject private var store = Store<Channel>()
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
|
||||
@@ -90,9 +91,14 @@ struct ChannelVideosView: View {
|
||||
|
||||
ToolbarItem {
|
||||
HStack {
|
||||
Text("**\(store.item?.subscriptionsString ?? "loading")** subscribers")
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(store.item?.subscriptionsString != nil ? 1 : 0)
|
||||
HStack(spacing: 3) {
|
||||
Text("\(store.item?.subscriptionsString ?? "loading")")
|
||||
.fontWeight(.bold)
|
||||
Text(" subscribers")
|
||||
}
|
||||
.allowsTightening(true)
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(store.item?.subscriptionsString != nil ? 1 : 0)
|
||||
|
||||
subscriptionToggleButton
|
||||
|
||||
@@ -100,8 +106,6 @@ struct ChannelVideosView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
.background(Color.tertiaryBackground)
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.sheet(isPresented: $presentingShareSheet) {
|
||||
@@ -121,6 +125,9 @@ struct ChannelVideosView: View {
|
||||
return Group {
|
||||
if #available(macOS 12.0, *) {
|
||||
content
|
||||
#if os(tvOS)
|
||||
.background(Color.background(scheme: colorScheme))
|
||||
#endif
|
||||
#if !os(iOS)
|
||||
.focusScope(focusNamespace)
|
||||
#endif
|
||||
|
||||
@@ -25,13 +25,19 @@ struct DetailBadge: View {
|
||||
}
|
||||
|
||||
struct DefaultStyleModifier: ViewModifier {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
content
|
||||
.background(.thinMaterial)
|
||||
} else {
|
||||
content
|
||||
.background(Color.background)
|
||||
#if os(tvOS)
|
||||
.background(Color.background(scheme: colorScheme))
|
||||
#else
|
||||
.background(Color.background.opacity(0.95))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
import Defaults
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct FavoriteButton: View {
|
||||
let item: FavoriteItem
|
||||
let item: FavoriteItem!
|
||||
let favorites = FavoritesModel.shared
|
||||
|
||||
@State private var isFavorite = false
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
favorites.toggle(item)
|
||||
isFavorite.toggle()
|
||||
} label: {
|
||||
if isFavorite {
|
||||
Label("Remove from Favorites", systemImage: "heart.fill")
|
||||
Group {
|
||||
if favorites.isEnabled {
|
||||
Button {
|
||||
guard !item.isNil else {
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
Label("Add to Favorites", systemImage: "heart")
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
isFavorite = favorites.contains(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ struct PlayerControlsView<Content: View>: View {
|
||||
.foregroundColor(model.currentItem.isNil ? .secondary : .accentColor)
|
||||
.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)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.secondary)
|
||||
@@ -106,7 +106,9 @@ struct PlayerControlsView<Content: View>: View {
|
||||
.background(Material.ultraThinMaterial)
|
||||
} else {
|
||||
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"
|
||||
}
|
||||
|
||||
private var appBuild: String {
|
||||
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
|
||||
}
|
||||
|
||||
private var progressViewValue: Double {
|
||||
[model.time?.seconds, model.videoDuration].compactMap { $0 }.min() ?? 0
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ struct SignInRequiredView<Content: View>: View {
|
||||
|
||||
Group {
|
||||
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 {
|
||||
Text("You need to select an account\nto access **\(title)** section")
|
||||
Text("You need to select an account\nto access \(title) section")
|
||||
}
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<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>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -5,6 +5,7 @@ import SwiftUI
|
||||
struct YatteeApp: App {
|
||||
#if os(macOS)
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
@StateObject private var updater = UpdaterModel()
|
||||
#endif
|
||||
|
||||
@StateObject private var menu = MenuModel()
|
||||
@@ -18,7 +19,16 @@ struct YatteeApp: App {
|
||||
.handlesExternalEvents(matching: Set(["*"]))
|
||||
.commands {
|
||||
SidebarCommands()
|
||||
|
||||
CommandGroup(replacing: .newItem, addition: {})
|
||||
|
||||
#if os(macOS)
|
||||
CommandGroup(after: .appInfo) {
|
||||
CheckForUpdatesView()
|
||||
.environmentObject(updater)
|
||||
}
|
||||
#endif
|
||||
|
||||
MenuCommands(model: Binding<MenuModel>(get: { menu }, set: { _ in }))
|
||||
}
|
||||
#endif
|
||||
@@ -28,6 +38,7 @@ struct YatteeApp: App {
|
||||
SettingsView()
|
||||
.environmentObject(AccountsModel())
|
||||
.environmentObject(InstancesModel())
|
||||
.environmentObject(updater)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -74,6 +74,20 @@
|
||||
37169AA62729E2CC0011DE61 /* 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 */; };
|
||||
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 */; };
|
||||
371F2F1B269B43D300E4A7AB /* 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 */; };
|
||||
37319F0627103F94004ECCD0 /* 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 */; };
|
||||
373CFACC26966264003CB2C6 /* 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 */; };
|
||||
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD926A214630092E2DB /* PlayerViewController.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 */; };
|
||||
37BF661D27308859008CCFB0 /* DropFavorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661B27308859008CCFB0 /* DropFavorite.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 */; };
|
||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.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 */; };
|
||||
37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.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 */; };
|
||||
37DD87C8271C9CFE0027CBF9 /* 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 */; };
|
||||
37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.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 */; };
|
||||
37EF5C232739D37B00B03725 /* 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 */; };
|
||||
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 */; };
|
||||
@@ -560,6 +587,10 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -569,6 +600,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -663,6 +695,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -676,6 +709,8 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -703,6 +738,7 @@
|
||||
37D526E22720B4BE00ED2F5E /* View+SwipeGesture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+SwipeGesture.swift"; sourceTree = "<group>"; };
|
||||
37D9169A27388A81002B1BAA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -711,6 +747,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -763,6 +800,7 @@
|
||||
37FB284F272209AB00A57617 /* SDWebImageSwiftUI in Frameworks */,
|
||||
3765917A27237D07009F956E /* PINCache in Frameworks */,
|
||||
37BD07BE2698AC96003EBB87 /* Defaults in Frameworks */,
|
||||
37C90AEF275F9CC00015EAF7 /* Sparkle in Frameworks */,
|
||||
37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */,
|
||||
377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */,
|
||||
37BD07C02698AC97003EBB87 /* Siesta in Frameworks */,
|
||||
@@ -833,6 +871,8 @@
|
||||
371AAE2426CEBA4100901972 /* Player */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
371B7E602759706A00D21217 /* CommentsView.swift */,
|
||||
37EF9A75275BEB8E0043B585 /* CommentView.swift */,
|
||||
37B81B0126D2CAE700675966 /* PlaybackBar.swift */,
|
||||
37BE0BD226A1D4780092E2DB /* Player.swift */,
|
||||
3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */,
|
||||
@@ -955,6 +995,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */,
|
||||
371B7E652759786B00D21217 /* Comment+Fixtures.swift */,
|
||||
376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */,
|
||||
37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */,
|
||||
3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */,
|
||||
@@ -1068,16 +1109,28 @@
|
||||
37BE0BD826A214500092E2DB /* macOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37BE7AF227601DBF00DBECED /* Updates */,
|
||||
374C0542272496E4009BDDBE /* AppDelegate.swift */,
|
||||
37FD43DB270470B70073EE42 /* InstancesSettings.swift */,
|
||||
374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */,
|
||||
37BE0BDB26A2367F0092E2DB /* Player.swift */,
|
||||
37BE0BD926A214630092E2DB /* PlayerViewController.swift */,
|
||||
37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */,
|
||||
374C0544272496FD009BDDBE /* Info.plist */,
|
||||
);
|
||||
path = macOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
37BE7AF227601DBF00DBECED /* Updates */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
37C90AF2275F9D5D0015EAF7 /* CheckForUpdatesView.swift */,
|
||||
37C90AF0275F9D450015EAF7 /* UpdaterModel.swift */,
|
||||
37BE7AF02760013C00DBECED /* UpdatesSettings.swift */,
|
||||
);
|
||||
path = Updates;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
37C7A9022679058300E721B4 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1209,6 +1262,9 @@
|
||||
374C0539272436DA009BDDBE /* SponsorBlock */,
|
||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
|
||||
371B7E5B27596B8400D21217 /* Comment.swift */,
|
||||
371B7E692759791900D21217 /* CommentsModel.swift */,
|
||||
373C8FE3275B955100CB5936 /* CommentsPage.swift */,
|
||||
37FB28402721B22200A57617 /* ContentItem.swift */,
|
||||
37141672267A8E10006CA35D /* Country.swift */,
|
||||
37599F2F272B42810087F250 /* FavoriteItem.swift */,
|
||||
@@ -1339,6 +1395,7 @@
|
||||
37FB2850272209AB00A57617 /* SDWebImageWebPCoder */,
|
||||
37FB285727220D9600A57617 /* SDWebImagePINPlugin */,
|
||||
3765917927237D07009F956E /* PINCache */,
|
||||
37C90AEE275F9CC00015EAF7 /* Sparkle */,
|
||||
);
|
||||
productName = "Yattee (macOS)";
|
||||
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
|
||||
@@ -1502,6 +1559,7 @@
|
||||
37FB2847272207F000A57617 /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */,
|
||||
37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */,
|
||||
3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */,
|
||||
37C90AED275F9CC00015EAF7 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||
);
|
||||
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -1719,6 +1777,8 @@
|
||||
files = (
|
||||
374710052755291C00CE0F87 /* SearchField.swift in Sources */,
|
||||
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||
371B7E612759706A00D21217 /* CommentsView.swift in Sources */,
|
||||
371B7E6A2759791900D21217 /* CommentsModel.swift in Sources */,
|
||||
37CC3F45270CE30600608308 /* PlayerQueueItem.swift in Sources */,
|
||||
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
|
||||
37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
||||
@@ -1749,6 +1809,7 @@
|
||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
||||
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
|
||||
37A9965E26D6F9B9006E3224 /* FavoritesView.swift in Sources */,
|
||||
37CEE4C12677B697005A1EFE /* Stream.swift in Sources */,
|
||||
@@ -1776,6 +1837,8 @@
|
||||
3782B9522755667600990149 /* String+Format.swift in Sources */,
|
||||
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||
37BF661F27308884008CCFB0 /* DropFavoriteOutside.swift in Sources */,
|
||||
37EF9A76275BEB8E0043B585 /* CommentView.swift in Sources */,
|
||||
373C8FE4275B955100CB5936 /* CommentsPage.swift in Sources */,
|
||||
3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */,
|
||||
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||
377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */,
|
||||
@@ -1802,6 +1865,7 @@
|
||||
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||
37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
||||
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||
371B7E5C27596B8400D21217 /* Comment.swift in Sources */,
|
||||
37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */,
|
||||
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||
3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */,
|
||||
@@ -1873,6 +1937,7 @@
|
||||
37C3A24E272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
|
||||
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
|
||||
3782B95027553A6700990149 /* SearchSuggestions.swift in Sources */,
|
||||
371B7E6B2759791900D21217 /* CommentsModel.swift in Sources */,
|
||||
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||
37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||
@@ -1898,6 +1963,7 @@
|
||||
3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
||||
378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */,
|
||||
37599F35272B44000087F250 /* FavoritesModel.swift in Sources */,
|
||||
37C90AF3275F9D5D0015EAF7 /* CheckForUpdatesView.swift in Sources */,
|
||||
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
||||
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||
37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
||||
@@ -1910,6 +1976,7 @@
|
||||
377FC7E2267A084A00A6BBAF /* VideoCell.swift in Sources */,
|
||||
37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */,
|
||||
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||
37BE7AF12760013C00DBECED /* UpdatesSettings.swift in Sources */,
|
||||
37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
||||
37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */,
|
||||
3765788A2685471400D4EA09 /* Playlist.swift in Sources */,
|
||||
@@ -1922,10 +1989,12 @@
|
||||
37AAF29126740715007FC770 /* Channel.swift in Sources */,
|
||||
376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
||||
37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
||||
37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */,
|
||||
37BF661D27308859008CCFB0 /* DropFavorite.swift in Sources */,
|
||||
3748186F26A769D60084E870 /* DetailBadge.swift in Sources */,
|
||||
372915E72687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||
37C3A242272359900087A57A /* Double+Format.swift in Sources */,
|
||||
37C90AF1275F9D450015EAF7 /* UpdaterModel.swift in Sources */,
|
||||
37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
||||
376578922685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||
37484C2626FC83E000287258 /* InstanceForm.swift in Sources */,
|
||||
@@ -1953,6 +2022,7 @@
|
||||
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
||||
371B7E622759706A00D21217 /* CommentsView.swift in Sources */,
|
||||
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
||||
37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
||||
37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */,
|
||||
@@ -1964,11 +2034,14 @@
|
||||
3700155C271B0D4D0049C794 /* PipedAPI.swift in Sources */,
|
||||
376BE50C27349108009AD608 /* BrowsingSettings.swift in Sources */,
|
||||
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
||||
371B7E5D27596B8400D21217 /* Comment.swift in Sources */,
|
||||
37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */,
|
||||
37599F31272B42810087F250 /* FavoriteItem.swift in Sources */,
|
||||
3730F75A2733481E00F385FC /* RelatedView.swift in Sources */,
|
||||
37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */,
|
||||
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */,
|
||||
37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
||||
373C8FE5275B955100CB5936 /* CommentsPage.swift in Sources */,
|
||||
37D4B0E52671614900C925CA /* YatteeApp.swift in Sources */,
|
||||
37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */,
|
||||
37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||
@@ -1989,6 +2062,7 @@
|
||||
37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
||||
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||
37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */,
|
||||
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||
);
|
||||
@@ -2008,6 +2082,7 @@
|
||||
files = (
|
||||
3774124C27387D2300423605 /* RecentsModel.swift in Sources */,
|
||||
3774122A27387B6C00423605 /* InstancesModelTests.swift in Sources */,
|
||||
371B7E642759706A00D21217 /* CommentsView.swift in Sources */,
|
||||
3774124927387D2300423605 /* Channel.swift in Sources */,
|
||||
3774125727387D2300423605 /* FavoriteItem.swift in Sources */,
|
||||
3774126B27387D6D00423605 /* CMTime+DefaultTimescale.swift in Sources */,
|
||||
@@ -2017,6 +2092,7 @@
|
||||
3774126827387D6D00423605 /* Double+Format.swift in Sources */,
|
||||
3774126E27387D8800423605 /* PlayerQueueItem.swift in Sources */,
|
||||
3774125627387D2300423605 /* Segment.swift in Sources */,
|
||||
373C8FE7275B955100CB5936 /* CommentsPage.swift in Sources */,
|
||||
3774126427387D4A00423605 /* VideosAPI.swift in Sources */,
|
||||
3774124D27387D2300423605 /* PlaylistsModel.swift in Sources */,
|
||||
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */,
|
||||
@@ -2024,6 +2100,7 @@
|
||||
37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */,
|
||||
3774125427387D2300423605 /* Store.swift in Sources */,
|
||||
3774125027387D2300423605 /* Video.swift in Sources */,
|
||||
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */,
|
||||
3774125327387D2300423605 /* Country.swift in Sources */,
|
||||
3774125E27387D2D00423605 /* InstancesModel.swift in Sources */,
|
||||
37CB128C2724CC8400213B45 /* VideoURLParser.swift in Sources */,
|
||||
@@ -2041,6 +2118,7 @@
|
||||
3774125D27387D2D00423605 /* Instance.swift in Sources */,
|
||||
3774125927387D2300423605 /* ChannelPlaylist.swift in Sources */,
|
||||
3774125527387D2300423605 /* Stream.swift in Sources */,
|
||||
371B7E5F27596B8400D21217 /* Comment.swift in Sources */,
|
||||
3774126F27387D8D00423605 /* SearchQuery.swift in Sources */,
|
||||
3774127127387D9E00423605 /* PlayerQueueItemBridge.swift in Sources */,
|
||||
3774125227387D2300423605 /* Thumbnail.swift in Sources */,
|
||||
@@ -2063,6 +2141,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||
373C8FE6275B955100CB5936 /* CommentsPage.swift in Sources */,
|
||||
37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */,
|
||||
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||
@@ -2076,6 +2155,7 @@
|
||||
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
||||
37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */,
|
||||
3748187026A769D60084E870 /* DetailBadge.swift in Sources */,
|
||||
371B7E632759706A00D21217 /* CommentsView.swift in Sources */,
|
||||
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
|
||||
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
||||
@@ -2096,6 +2176,7 @@
|
||||
37C3A243272359900087A57A /* Double+Format.swift in Sources */,
|
||||
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||
371B7E5E27596B8400D21217 /* Comment.swift in Sources */,
|
||||
37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */,
|
||||
37C0698427260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */,
|
||||
@@ -2110,6 +2191,7 @@
|
||||
3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */,
|
||||
37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
||||
37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */,
|
||||
371B7E682759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||
37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||
37AAF27E26737323007FC770 /* PopularView.swift in Sources */,
|
||||
3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||
@@ -2158,6 +2240,7 @@
|
||||
37141675267A8E10006CA35D /* Country.swift in Sources */,
|
||||
3782B9542755667600990149 /* String+Format.swift in Sources */,
|
||||
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */,
|
||||
37484C2726FC83E000287258 /* InstanceForm.swift in Sources */,
|
||||
37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */,
|
||||
373197DA2732060100EF734F /* RelatedView.swift in Sources */,
|
||||
@@ -2168,6 +2251,7 @@
|
||||
37599F36272B44000087F250 /* FavoritesModel.swift in Sources */,
|
||||
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||
37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */,
|
||||
371B7E6C2759791900D21217 /* CommentsModel.swift in Sources */,
|
||||
3782B95627557E4E00990149 /* SearchView.swift in Sources */,
|
||||
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.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_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -2251,7 +2335,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -2283,7 +2367,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -2315,7 +2399,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -2478,7 +2562,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -2509,7 +2593,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -2544,7 +2628,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -2577,7 +2661,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -2708,7 +2792,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2740,7 +2824,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -3016,6 +3100,14 @@
|
||||
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" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON.git";
|
||||
@@ -3166,6 +3258,11 @@
|
||||
package = 37BD07C52698B27B003EBB87 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
|
||||
productName = Introspect;
|
||||
};
|
||||
37C90AEE275F9CC00015EAF7 /* Sparkle */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 37C90AED275F9CC00015EAF7 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||
productName = Sparkle;
|
||||
};
|
||||
37D4B19C2671817900C925CA /* SwiftyJSON */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */;
|
||||
|
||||
@@ -91,6 +91,15 @@
|
||||
"version": "1.5.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Sparkle",
|
||||
"repositoryURL": "https://github.com/sparkle-project/Sparkle",
|
||||
"state": {
|
||||
"branch": "2.x",
|
||||
"revision": "831e9b4eb7e871a9c072469fb14049614fc92810",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-log",
|
||||
"repositoryURL": "https://github.com/apple/swift-log.git",
|
||||
|
||||
@@ -15,5 +15,11 @@
|
||||
</array>
|
||||
</dict>
|
||||
</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>
|
||||
</plist>
|
||||
|
||||
@@ -14,6 +14,7 @@ struct InstancesSettings: View {
|
||||
|
||||
@State private var frontendURL = ""
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
|
||||
@Default(.instances) private var instances
|
||||
@@ -54,7 +55,7 @@ struct InstancesSettings: View {
|
||||
Button("Remove") {
|
||||
presentingAccountRemovalConfirmation = true
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
.foregroundColor(colorScheme == .dark ? .white : .red)
|
||||
.opacity(account == selectedAccount ? 1 : 0)
|
||||
}
|
||||
.tag(account)
|
||||
|
||||
10
macOS/Updates/CheckForUpdatesView.swift
Normal file
10
macOS/Updates/CheckForUpdatesView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
41
macOS/Updates/UpdaterModel.swift
Normal file
41
macOS/Updates/UpdaterModel.swift
Normal 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"] : [])
|
||||
}
|
||||
}
|
||||
32
macOS/Updates/UpdatesSettings.swift
Normal file
32
macOS/Updates/UpdatesSettings.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
42
macOS/VerticalScrollingFix.swift
Normal file
42
macOS/VerticalScrollingFix.swift
Normal 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 }
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,17 @@ import SwiftUI
|
||||
|
||||
struct NowPlayingView: View {
|
||||
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
|
||||
|
||||
@State private var repliesID: Comment.ID?
|
||||
|
||||
@EnvironmentObject<CommentsModel> private var comments
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
|
||||
@Default(.saveHistory) private var saveHistory
|
||||
|
||||
@@ -33,6 +37,13 @@ struct NowPlayingView: View {
|
||||
} label: {
|
||||
VideoBanner(video: item.video)
|
||||
}
|
||||
.contextMenu {
|
||||
Button("Close Video") {
|
||||
player.closeCurrentItem()
|
||||
}
|
||||
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
}
|
||||
.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))
|
||||
.padding(.vertical, 20)
|
||||
|
||||
@@ -3,34 +3,39 @@ import SwiftUI
|
||||
|
||||
struct TVNavigationView: View {
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SearchModel> private var search
|
||||
|
||||
@Default(.visibleSections) private var visibleSections
|
||||
var body: some View {
|
||||
TabView(selection: navigation.tabSelectionBinding) {
|
||||
FavoritesView()
|
||||
.tabItem { Text("Favorites") }
|
||||
.tag(TabSelection.favorites)
|
||||
if visibleSections.contains(.favorites) {
|
||||
FavoritesView()
|
||||
.tabItem { Text("Favorites") }
|
||||
.tag(TabSelection.favorites)
|
||||
}
|
||||
|
||||
if accounts.app.supportsSubscriptions {
|
||||
if visibleSections.contains(.subscriptions), accounts.app.supportsSubscriptions {
|
||||
SubscriptionsView()
|
||||
.tabItem { Text("Subscriptions") }
|
||||
.tag(TabSelection.subscriptions)
|
||||
}
|
||||
|
||||
if accounts.app.supportsPopular {
|
||||
if visibleSections.contains(.popular), accounts.app.supportsPopular {
|
||||
PopularView()
|
||||
.tabItem { Text("Popular") }
|
||||
.tag(TabSelection.popular)
|
||||
}
|
||||
|
||||
TrendingView()
|
||||
.tabItem { Text("Trending") }
|
||||
.tag(TabSelection.trending)
|
||||
if visibleSections.contains(.trending) {
|
||||
TrendingView()
|
||||
.tabItem { Text("Trending") }
|
||||
.tag(TabSelection.trending)
|
||||
}
|
||||
|
||||
if accounts.app.supportsUserPlaylists {
|
||||
if visibleSections.contains(.playlists), accounts.app.supportsUserPlaylists {
|
||||
PlaylistsView()
|
||||
.tabItem { Text("Playlists") }
|
||||
.tag(TabSelection.playlists)
|
||||
|
||||
Reference in New Issue
Block a user