Compare commits

...

20 Commits

Author SHA1 Message Date
Arkadiusz Fal
fe33cc5e3a Bump build number 2021-12-08 00:29:00 +01:00
Arkadiusz Fal
7e7b4e89b5 Add Sparkle update framework for macOS 2021-12-08 00:27:38 +01:00
Arkadiusz Fal
d88292662f Minor README updates 2021-12-08 00:08:13 +01:00
Arkadiusz Fal
21b04e21c4 Remove unused file 2021-12-08 00:07:23 +01:00
Arkadiusz Fal
a44a61b017 Remove redundant query for replies when collapsed and expanded 2021-12-08 00:06:59 +01:00
Arkadiusz Fal
1b090fcd51 Bump build number 2021-12-06 19:15:05 +01:00
Arkadiusz Fal
12eb4401b5 Update README 2021-12-06 19:13:57 +01:00
Arkadiusz Fal
170f2ee94e Fix reloading favorites view 2021-12-06 19:13:49 +01:00
Arkadiusz Fal
fe56739211 Fix crash on dismissing channel playlist on iOS 2021-12-06 19:13:37 +01:00
Arkadiusz Fal
759a942426 Fix search field on macOS 2021-12-06 19:13:20 +01:00
Arkadiusz Fal
8d9bbf647a Fix disabling comments on tvOS 2021-12-06 19:12:59 +01:00
Arkadiusz Fal
eeb7b1f151 Improve search suggestions 2021-12-06 19:12:33 +01:00
Arkadiusz Fal
62bff9283c Faster replacing player item 2021-12-06 19:12:02 +01:00
Arkadiusz Fal
3624c9619a Add setting for displaying comments in separate tab or below description 2021-12-06 19:11:19 +01:00
Arkadiusz Fal
f7fc2369e3 Bump build number 2021-12-05 18:31:35 +01:00
Arkadiusz Fal
82ea8733ec Fix crash when video thumbnail cannot be loaded (fixes #28) 2021-12-05 18:31:35 +01:00
Arkadiusz Fal
1f495562fc Comments improvements
* Show text when there is no comments or comments are disabled
* Show progress indicator for loading comments/replies
* Improve layout of icons and text spacing
2021-12-05 18:31:33 +01:00
Arkadiusz Fal
37b99c59e1 Fix disabling comments 2021-12-05 18:12:13 +01:00
Arkadiusz Fal
7f9b53bd1f Fix login with Invidious accounts 2021-12-05 18:10:10 +01:00
Arkadiusz Fal
941e6a909d Set full screen views background color based on color scheme on tvOS (fixes #30) 2021-12-05 18:09:25 +01:00
37 changed files with 519 additions and 278 deletions

View File

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

View File

@@ -27,7 +27,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
self.account = account
validInstance = false
signedIn = false
signedIn = !account.anonymous
configure()
}

View File

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

View File

@@ -4,20 +4,38 @@ 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 repliesPageID: String?
@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 +45,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
@@ -60,20 +78,33 @@ final class CommentsModel: ObservableObject {
return
}
replies = []
accounts.api.comments(player.currentVideo!.videoID, page: page)?.load().onSuccess { response in
if let page: CommentsPage = response.typedContent() {
self.replies = page.comments
}
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 clear() {
func reset() {
all = []
replies = []
disabled = false
firstPage = true
nextPage = nil
loaded = false
replies = []
repliesLoaded = false
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -1,14 +1,12 @@
![Yattee Banner](https://r.yattee.stream/icons/yattee-banner.png)
Video player for [Invidious](https://github.com/iv-org/invidious) and [Piped](https://github.com/TeamPiped/Piped) instances built for iOS, tvOS and macOS.
Video player for [Invidious](https://github.com/iv-org/invidious) and [Piped](https://github.com/TeamPiped/Piped) built for iOS, tvOS and macOS.
[![AGPL v3](https://shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0.en.html)
[![GitHub issues](https://img.shields.io/github/issues/yattee/yattee)](https://github.com/yattee/yattee/issues)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/yattee/yattee)](https://github.com/yattee/yattee/pulls)
[![Matrix](https://img.shields.io/matrix/yattee:matrix.org)](https://matrix.to/#/#yattee:matrix.org)
![Screenshot](https://r.yattee.stream/screenshots/all-platforms.png)
## Features
@@ -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 |
| - | - | - |
| [![Yattee Player iOS](https://r.yattee.stream/screenshots/iOS/player-thumb.png)](https://r.yattee.stream/screenshots/iOS/player.png) | [![Yattee Search iOS](https://r.yattee.stream/screenshots/iOS/search-suggestions-thumb.png)](https://r.yattee.stream/screenshots/iOS/search-suggestions.png) | [![Yattee Subscriptions iOS](https://r.yattee.stream/screenshots/iOS/playlists-thumb.png)](https://r.yattee.stream/screenshots/iOS/playlists.png) |
### iPadOS
| Settings | Player | Subscriptions |
| - | - | - |
| [![Yattee Player iPadOS](https://r.yattee.stream/screenshots/iPadOS/settings-thumb.png)](https://r.yattee.stream/screenshots/iPadOS/settings.png) | [![Yattee Player iPadOS](https://r.yattee.stream/screenshots/iPadOS/player-thumb.png)](https://r.yattee.stream/screenshots/iPadOS/player.png) | [![Yattee Subscriptions iPad S](https://r.yattee.stream/screenshots/iPadOS/subscriptions-thumb.png)](https://r.yattee.stream/screenshots/iPadOS/subscriptions.png) |
### tvOS
| Player | Popular | Search | Now Playing | Settings |
| - | - | - | - | - |
| [![Yattee Player tvOS](https://r.yattee.stream/screenshots/tvOS/player-thumb.png)](https://r.yattee.stream/screenshots/tvOS/player.png) | [![Yattee Popular tvOS](https://r.yattee.stream/screenshots/tvOS/popular-thumb.png)](https://r.yattee.stream/screenshots/tvOS/popular.png) | [![Yattee Search tvOS](https://r.yattee.stream/screenshots/tvOS/search-thumb.png)](https://r.yattee.stream/screenshots/tvOS/search.png) | [![Yattee Now Playing tvOS](https://r.yattee.stream/screenshots/tvOS/now-playing-thumb.png)](https://r.yattee.stream/screenshots/tvOS/now-playing.png) | [![Yattee Settings tvOS](https://r.yattee.stream/screenshots/tvOS/settings-thumb.png)](https://r.yattee.stream/screenshots/tvOS/settings.png) |
### macOS
| Player | Channel | Search | Settings |
| - | - | - | - |
| [![Yattee Player macOS](https://r.yattee.stream/screenshots/macOS/player-thumb.png)](https://r.yattee.stream/screenshots/macOS/player.png) | [![Yattee Channel macOS](https://r.yattee.stream/screenshots/macOS/channel-thumb.png)](https://r.yattee.stream/screenshots/macOS/channel.png) | [![Yattee Search macOS](https://r.yattee.stream/screenshots/macOS/search-thumb.png)](https://r.yattee.stream/screenshots/macOS/search.png) | [![Yattee Settings macOS](https://r.yattee.stream/screenshots/macOS/settings-thumb.png)](https://r.yattee.stream/screenshots/macOS/settings.png) |
## Tips
### Settings
* [tvOS] To open settings, press Play/Pause button while hovering over navigation menu or video
### Navigation
* Use videos context menus to add to queue, open or subscribe channel and add to playlist
* [tvOS] Pressing buttons in the app trigger switch to next available option (for example: next account in Settings). If you want to access list of all options, press and hold to open the context menu.
* [iOS] Swipe the player/title bar: up to open fullscreen details view, bottom to close fullscreen details or hide player
### Favorites
* Add more sections using ❤️ button in views channels, playlists, searches, subscriptions and popular
* [iOS/macOS] Reorganize with dragging and dropping
* [iOS/macOS] Remove section with right click/press and hold on section name
* [tvOS] Reorganize and remove from `Settings > Edit Favorites...`
### Keyboard shortcuts
* `Command+1` - Favorites
* `Command+2` - Subscriptions
* `Command+3` - Popular
* `Command+4` - Trending
* `Command+F` - Search
* `Command+P` - Play/Pause
* `Command+S` - Play Next
* `Command+O` - Toggle Player
## Donations
You can support development of this app with
[Patreon](https://www.patreon.com/arekf) or cryptocurrencies:
**Monero (XMR)**
```
48zfKjLmnXs21PinU2ucMiUPwhiKt5d7WJKiy3ACVS28BKqSn52c1TX8L337oESHJ5TZCyGkozjfWZG11h6C46mN9n4NPrD
```
**Bitcoin (BTC)**
```
bc1qe24zz5a5hm0trc7glwckz93py274eycxzju3mv
```
**Ethereum (ETH)**
```
0xa2f81A58Ec5E550132F03615c8d91954A4E37423
```
Donations will be used to cover development program access and domain renewal costs.
## 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.

View File

@@ -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: [])
@@ -48,6 +51,10 @@ extension Defaults.Keys {
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.favorites, .subscriptions, .trending, .playlists])
#if os(macOS)
static let enableBetaChannel = Key<Bool>("enableBetaChannel", default: false)
#endif
}
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
@@ -129,3 +136,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

View File

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

View File

@@ -67,6 +67,7 @@ struct AppTabNavigation: View {
NavigationView {
ChannelPlaylistView(playlist: playlist)
.environment(\.inNavigationView, true)
.environmentObject(subscriptions)
.background(playerNavigationLink)
}
}

View File

@@ -40,7 +40,7 @@ struct CommentView: View {
Spacer()
VStack(spacing: 5) {
VStack(alignment: .trailing, spacing: 8) {
likes
statusIcons
}
@@ -65,7 +65,14 @@ struct CommentView: View {
commentText
if comment.hasReplies {
repliesButton
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
@@ -137,10 +144,6 @@ struct CommentView: View {
Button {
repliesID = repliesID == comment.id ? nil : comment.id
if repliesID.isNil {
comments.replies = []
}
guard !repliesID.isNil, !comment.repliesPage.isNil else {
return
}
@@ -148,16 +151,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 +167,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 +199,8 @@ struct CommentView: View {
.padding(.vertical, 5)
}
}
.padding(.leading, 22)
}
.padding(.leading, 22)
}
private var commentText: some View {
@@ -219,3 +239,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))
}
}

View File

@@ -7,41 +7,73 @@ struct CommentsView: View {
@EnvironmentObject<PlayerModel> private var player
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
let last = comments.all.last
ForEach(comments.all) { comment in
CommentView(comment: comment, repliesID: $repliesID)
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)
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)
}
}
HStack {
if comments.nextPageAvailable {
Button {
comments.loadNextPage()
} label: {
Label("Show more", systemImage: "arrow.turn.down.right")
}
}
if !comments.firstPage {
Button {
comments.load(page: nil)
} label: {
Label("Show first", systemImage: "arrow.turn.down.left")
}
}
}
.buttonStyle(.plain)
.padding(.vertical, 5)
.foregroundColor(.secondary)
}
}
.padding(.horizontal)
.onAppear {
if !comments.loaded {
comments.load()
}
}
}
private var progressView: some View {
VStack {
Spacer()
HStack {
Spacer()
ProgressView()
Spacer()
}
Spacer()
}
}
}

View File

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

View File

@@ -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)
@@ -364,65 +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(.system(size: 14))
.lineSpacing(3)
.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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
@@ -55,6 +57,14 @@ struct SettingsView: View {
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
}

View File

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

View File

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

View File

@@ -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.opacity(0.95))
#if os(tvOS)
.background(Color.background(scheme: colorScheme))
#else
.background(Color.background.opacity(0.95))
#endif
}
}
}

View File

@@ -106,7 +106,9 @@ struct PlayerControlsView<Content: View>: View {
.background(Material.ultraThinMaterial)
} else {
controls
.background(Color.tertiaryBackground)
#if !os(tvOS)
.background(Color.tertiaryBackground)
#endif
}
}
}

View File

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

View File

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

View File

@@ -407,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 */; };
@@ -442,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 */; };
@@ -691,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>"; };
@@ -704,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>"; };
@@ -793,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 */,
@@ -1101,6 +1109,7 @@
37BE0BD826A214500092E2DB /* macOS */ = {
isa = PBXGroup;
children = (
37BE7AF227601DBF00DBECED /* Updates */,
374C0542272496E4009BDDBE /* AppDelegate.swift */,
37FD43DB270470B70073EE42 /* InstancesSettings.swift */,
374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */,
@@ -1112,6 +1121,16 @@
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 = (
@@ -1376,6 +1395,7 @@
37FB2850272209AB00A57617 /* SDWebImageWebPCoder */,
37FB285727220D9600A57617 /* SDWebImagePINPlugin */,
3765917927237D07009F956E /* PINCache */,
37C90AEE275F9CC00015EAF7 /* Sparkle */,
);
productName = "Yattee (macOS)";
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
@@ -1539,6 +1559,7 @@
37FB2847272207F000A57617 /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */,
37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */,
3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */,
37C90AED275F9CC00015EAF7 /* XCRemoteSwiftPackageReference "Sparkle" */,
);
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
projectDirPath = "";
@@ -1942,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 */,
@@ -1954,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 */,
@@ -1971,6 +1994,7 @@
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 */,
@@ -2277,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 = 6;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -2311,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 = 6;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -2343,7 +2367,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -2375,7 +2399,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -2538,7 +2562,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -2569,7 +2593,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -2604,7 +2628,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@@ -2637,7 +2661,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@@ -2768,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 = 6;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
@@ -2800,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 = 6;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
@@ -3076,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";
@@ -3226,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" */;

View File

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

View File

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

View File

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

View File

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

View File

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