mirror of
https://github.com/yattee/yattee.git
synced 2025-12-13 19:48:14 +00:00
Compare commits
15 Commits
v1.2-beta.
...
v1.2-beta.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b090fcd51 | ||
|
|
12eb4401b5 | ||
|
|
170f2ee94e | ||
|
|
fe56739211 | ||
|
|
759a942426 | ||
|
|
8d9bbf647a | ||
|
|
eeb7b1f151 | ||
|
|
62bff9283c | ||
|
|
3624c9619a | ||
|
|
f7fc2369e3 | ||
|
|
82ea8733ec | ||
|
|
1f495562fc | ||
|
|
37b99c59e1 | ||
|
|
7f9b53bd1f | ||
|
|
941e6a909d |
@@ -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
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
self.account = account
|
||||
|
||||
validInstance = false
|
||||
signedIn = false
|
||||
signedIn = !account.anonymous
|
||||
|
||||
configure()
|
||||
}
|
||||
|
||||
@@ -72,10 +72,12 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
configureTransformer(pathPattern("comments/*")) { (content: Entity<JSON>) -> CommentsPage in
|
||||
let comments = content.json.dictionaryValue["comments"]?.arrayValue.map { PipedAPI.extractComment(from: $0)! } ?? []
|
||||
let nextPage = content.json.dictionaryValue["nextpage"]?.stringValue
|
||||
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)
|
||||
return CommentsPage(comments: comments, nextPage: nextPage, disabled: disabled)
|
||||
}
|
||||
|
||||
if account.token.isNil {
|
||||
|
||||
@@ -4,20 +4,37 @@ import SwiftyJSON
|
||||
|
||||
final class CommentsModel: ObservableObject {
|
||||
@Published var all = [Comment]()
|
||||
@Published var replies = [Comment]()
|
||||
|
||||
@Published var nextPage: String?
|
||||
@Published var firstPage = true
|
||||
|
||||
@Published var loaded = false
|
||||
@Published var loaded = true
|
||||
@Published var disabled = false
|
||||
|
||||
@Published var replies = [Comment]()
|
||||
@Published var repliesLoaded = false
|
||||
|
||||
var accounts: AccountsModel!
|
||||
var player: PlayerModel!
|
||||
|
||||
static var enabled: Bool {
|
||||
!Defaults[.commentsInstanceID].isNil
|
||||
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)
|
||||
}
|
||||
@@ -27,23 +44,23 @@ final class CommentsModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
loaded = false
|
||||
clear()
|
||||
reset()
|
||||
|
||||
guard let instance = InstancesModel.find(Defaults[.commentsInstanceID]),
|
||||
!player.currentVideo.isNil
|
||||
guard !instance.isNil,
|
||||
!(player?.currentVideo.isNil ?? true)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
firstPage = page.isNil || page!.isEmpty
|
||||
|
||||
PipedAPI(account: instance.anonymousAccount).comments(player.currentVideo!.videoID, page: page)?
|
||||
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
|
||||
@@ -61,19 +78,27 @@ final class CommentsModel: ObservableObject {
|
||||
}
|
||||
|
||||
replies = []
|
||||
repliesLoaded = false
|
||||
|
||||
accounts.api.comments(player.currentVideo!.videoID, page: page)?.load().onSuccess { response in
|
||||
api?.comments(player.currentVideo!.videoID, page: page)?
|
||||
.load()
|
||||
.onSuccess { [weak self] response in
|
||||
if let page: CommentsPage = response.typedContent() {
|
||||
self.replies = page.comments
|
||||
self?.replies = page.comments
|
||||
}
|
||||
}
|
||||
.onCompletion { [weak self] _ in
|
||||
self?.repliesLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
func clear() {
|
||||
func reset() {
|
||||
all = []
|
||||
replies = []
|
||||
disabled = false
|
||||
firstPage = true
|
||||
nextPage = nil
|
||||
loaded = false
|
||||
replies = []
|
||||
repliesLoaded = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,5 @@ import Foundation
|
||||
struct CommentsPage {
|
||||
var comments = [Comment]()
|
||||
var nextPage: String?
|
||||
var disabled = false
|
||||
}
|
||||
|
||||
@@ -481,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,
|
||||
@@ -492,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,7 +38,7 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) {
|
||||
comments.clear()
|
||||
comments.reset()
|
||||
currentItem = item
|
||||
|
||||
if !time.isNil {
|
||||
@@ -92,6 +93,7 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) {
|
||||
player.replaceCurrentItem(with: nil)
|
||||
addCurrentItemToHistory()
|
||||
|
||||
remove(newItem)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
109
README.md
109
README.md
@@ -2,16 +2,14 @@
|
||||
|
||||
Video player for [Invidious](https://github.com/iv-org/invidious) and [Piped](https://github.com/TeamPiped/Piped) instances 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
|
||||
## Major Features
|
||||
* Native user interface built with [SwiftUI](https://developer.apple.com/xcode/swiftui/)
|
||||
* Multiple instances and accounts, fast switching
|
||||
* [SponsorBlock](https://sponsor.ajay.app/), configurable categories to skip
|
||||
@@ -37,104 +35,15 @@ Video player for [Invidious](https://github.com/iv-org/invidious) and [Piped](ht
|
||||
| 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?
|
||||
|
||||
#### macOS
|
||||
Download and run latest version from the [Releases](https://github.com/yattee/yattee/releases) page.
|
||||
|
||||
#### iOS/tvOS: [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`
|
||||
|
||||
#### iOS/tvOS: 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.
|
||||
|
||||
#### iOS/tvOS: 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.
|
||||
|
||||
@@ -34,6 +34,9 @@ extension Defaults.Keys {
|
||||
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: [])
|
||||
|
||||
@@ -129,3 +132,9 @@ enum VisibleSection: String, CaseIterable, Comparable, Defaults.Serializable {
|
||||
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 {
|
||||
|
||||
@@ -67,6 +67,7 @@ struct AppTabNavigation: View {
|
||||
NavigationView {
|
||||
ChannelPlaylistView(playlist: playlist)
|
||||
.environment(\.inNavigationView, true)
|
||||
.environmentObject(subscriptions)
|
||||
.background(playerNavigationLink)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ struct CommentView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 5) {
|
||||
VStack(alignment: .trailing, spacing: 8) {
|
||||
likes
|
||||
statusIcons
|
||||
}
|
||||
@@ -65,8 +65,15 @@ struct CommentView: View {
|
||||
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
|
||||
}
|
||||
@@ -148,16 +155,15 @@ struct CommentView: View {
|
||||
comments.loadReplies(page: comment.repliesPage!)
|
||||
} label: {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: self.repliesID == comment.id ? "arrow.turn.left.up" : "arrow.turn.right.down")
|
||||
Image(systemName: repliesID == comment.id ? "arrow.turn.left.up" : "arrow.turn.right.down")
|
||||
Text("Replies")
|
||||
}
|
||||
#if os(tvOS)
|
||||
.padding(10)
|
||||
#endif
|
||||
}
|
||||
|
||||
.buttonStyle(.plain)
|
||||
.padding(.top, 2)
|
||||
.padding(.vertical, 2)
|
||||
#if os(tvOS)
|
||||
.padding(.leading, 5)
|
||||
#else
|
||||
@@ -165,6 +171,24 @@ struct CommentView: View {
|
||||
#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
|
||||
@@ -179,8 +203,8 @@ struct CommentView: View {
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
}
|
||||
.padding(.leading, 22)
|
||||
}
|
||||
.padding(.leading, 22)
|
||||
}
|
||||
|
||||
private var commentText: some View {
|
||||
@@ -219,3 +243,13 @@ struct CommentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CommentView_Previews: PreviewProvider {
|
||||
static var fixture: Comment {
|
||||
Comment.fixture
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
CommentView(comment: fixture, repliesID: .constant(fixture.id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,16 @@ struct CommentsView: View {
|
||||
@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
|
||||
@@ -22,6 +32,7 @@ struct CommentsView: View {
|
||||
HStack {
|
||||
if comments.nextPageAvailable {
|
||||
Button {
|
||||
repliesID = nil
|
||||
comments.loadNextPage()
|
||||
} label: {
|
||||
Label("Show more", systemImage: "arrow.turn.down.right")
|
||||
@@ -30,6 +41,7 @@ struct CommentsView: View {
|
||||
|
||||
if !comments.firstPage {
|
||||
Button {
|
||||
repliesID = nil
|
||||
comments.load(page: nil)
|
||||
} label: {
|
||||
Label("Show first", systemImage: "arrow.turn.down.left")
|
||||
@@ -37,11 +49,31 @@ struct CommentsView: View {
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.vertical, 5)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,11 +45,16 @@ final class PlayerViewController: UIViewController {
|
||||
|
||||
#if os(tvOS)
|
||||
playerModel.avPlayerViewController = playerViewController
|
||||
playerViewController.customInfoViewControllers = [
|
||||
infoViewController([.comments], title: "Comments"),
|
||||
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
|
||||
|
||||
@@ -65,7 +65,7 @@ struct VideoDetails: View {
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
if CommentsModel.enabled {
|
||||
if CommentsModel.enabled, CommentsModel.placement == .separate {
|
||||
pagePicker
|
||||
.padding(.horizontal)
|
||||
}
|
||||
@@ -141,6 +141,8 @@ struct VideoDetails: View {
|
||||
player.closeCurrentItem()
|
||||
if !sidebarQueue {
|
||||
currentPage = .queue
|
||||
} else {
|
||||
currentPage = .info
|
||||
}
|
||||
} label: {
|
||||
Label("Close Video", systemImage: "xmark.circle")
|
||||
@@ -243,13 +245,13 @@ struct VideoDetails: View {
|
||||
Picker("Page", selection: $currentPage) {
|
||||
if !video.isNil {
|
||||
Text("Info").tag(Page.info)
|
||||
if !sidebarQueue {
|
||||
Text("Related").tag(Page.related)
|
||||
}
|
||||
if CommentsModel.enabled {
|
||||
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)
|
||||
@@ -363,6 +365,7 @@ struct VideoDetails: View {
|
||||
}
|
||||
|
||||
var detailsPage: some View {
|
||||
Group {
|
||||
Group {
|
||||
if let video = player.currentItem?.video {
|
||||
Group {
|
||||
@@ -391,7 +394,6 @@ struct VideoDetails: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.font(.system(size: 14))
|
||||
.lineSpacing(3)
|
||||
.padding(.bottom, 4)
|
||||
} else {
|
||||
Text("No description")
|
||||
.foregroundColor(.secondary)
|
||||
@@ -421,8 +423,24 @@ struct VideoDetails: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func videoDetail(label: String, value: String, symbol: String) -> some View {
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -37,6 +37,7 @@ struct SearchTextField: View {
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.frame(maxWidth: 190)
|
||||
.textFieldStyle(.plain)
|
||||
#else
|
||||
.textFieldStyle(.roundedBorder)
|
||||
@@ -52,6 +53,11 @@ struct SearchTextField: View {
|
||||
.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,8 +28,11 @@ struct SearchSuggestions: View {
|
||||
Button {
|
||||
state.queryText = suggestion
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "arrow.up.left.circle")
|
||||
.foregroundColor(.secondary)
|
||||
HStack(spacing: 0) {
|
||||
Label(state.queryText, systemImage: "arrow.up.left.circle")
|
||||
Text(state.suggestionsText)
|
||||
.lineLimit(1)
|
||||
.layoutPriority(2)
|
||||
.foregroundColor(.secondary)
|
||||
@@ -38,6 +42,7 @@ struct SearchSuggestions: View {
|
||||
.layoutPriority(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.onHover(perform: onHover(_:))
|
||||
#endif
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,9 +6,17 @@ struct ServicesSettings: View {
|
||||
@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")) {
|
||||
@@ -58,8 +66,8 @@ struct ServicesSettings: View {
|
||||
}
|
||||
|
||||
private var commentsInstancePicker: some View {
|
||||
Picker("Comments", selection: $commentsInstanceID) {
|
||||
Text("Disabled").tag(String?.none)
|
||||
Picker("Source", selection: $commentsInstanceID) {
|
||||
Text("Disabled").tag(Optional(""))
|
||||
|
||||
ForEach(InstancesModel.all.filter { $0.app.supportsComments }) { instance in
|
||||
Text(instance.description).tag(Optional(instance.id))
|
||||
@@ -73,6 +81,19 @@ struct ServicesSettings: View {
|
||||
#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)
|
||||
|
||||
@@ -9,6 +9,8 @@ struct SettingsView: View {
|
||||
}
|
||||
#endif
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
#endif
|
||||
@@ -102,7 +104,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
|
||||
|
||||
@@ -105,8 +106,6 @@ struct ChannelVideosView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
.background(Color.tertiaryBackground)
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.sheet(isPresented: $presentingShareSheet) {
|
||||
@@ -126,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
|
||||
#if os(tvOS)
|
||||
.background(Color.background(scheme: colorScheme))
|
||||
#else
|
||||
.background(Color.background.opacity(0.95))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,9 @@ struct PlayerControlsView<Content: View>: View {
|
||||
.background(Material.ultraThinMaterial)
|
||||
} else {
|
||||
controls
|
||||
#if !os(tvOS)
|
||||
.background(Color.tertiaryBackground)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2277,7 +2277,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -2311,7 +2311,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -2343,7 +2343,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -2375,7 +2375,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -2538,7 +2538,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -2569,7 +2569,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -2604,7 +2604,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -2637,7 +2637,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -2768,7 +2768,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -2800,7 +2800,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
||||
Reference in New Issue
Block a user