Compare commits

..

81 Commits

Author SHA1 Message Date
Arkadiusz Fal
d344ab15dc Bump build number 2022-05-29 22:52:29 +02:00
Arkadiusz Fal
888813f095 Minor fixes 2022-05-29 22:51:52 +02:00
Arkadiusz Fal
a0a6020ee6 Add close video button to browser controls 2022-05-29 22:20:38 +02:00
Arkadiusz Fal
e3e68cd158 Minor improvements 2022-05-29 22:20:38 +02:00
Arkadiusz Fal
42e56d6e4b Navigation improvements 2022-05-29 22:20:38 +02:00
Arkadiusz Fal
f979af7f01 New playlist navigation 2022-05-29 22:20:38 +02:00
Arkadiusz Fal
124a48812a New channel navigation 2022-05-29 22:20:38 +02:00
Arkadiusz Fal
44e6c28fd4 Add buttons to next video and restart video (fix #106)
Previous video requires rebuilding queue a little, maybe in the future
2022-05-29 22:20:38 +02:00
Arkadiusz Fal
4f6e0e2a3d Don't push MPV to play HLS on changing backends
It takes longer to load than WEBM
2022-05-29 22:20:38 +02:00
Arkadiusz Fal
ccc1cc89ad Add Open in PiP option (fix #137) 2022-05-29 22:20:38 +02:00
Arkadiusz Fal
5d0eb2478c Fix playing video from start when history disabled 2022-05-29 21:21:29 +02:00
Arkadiusz Fal
c28429ec7f Minor improvements 2022-05-29 21:21:29 +02:00
Arkadiusz Fal
6c3a7882e1 Player animation improvements 2022-05-29 21:21:29 +02:00
Arkadiusz Fal
76286e8a45 Fix orientation (#121) 2022-05-29 21:21:29 +02:00
Arkadiusz Fal
c5128f6227 Minor improvements 2022-05-29 21:21:29 +02:00
Arkadiusz Fal
4bf171637b Player overlaying other views and swipe gesture (fix #44, #130) 2022-05-29 21:21:29 +02:00
Arkadiusz Fal
249adab427 Remove some default Favorites 2022-05-29 21:21:29 +02:00
Arkadiusz Fal
a27ebcce27 More controls improvements 2022-05-29 21:21:29 +02:00
Arkadiusz Fal
b5f3a1bd09 Minor player controls improvements (fix #94) 2022-05-29 21:21:17 +02:00
Arkadiusz Fal
b306819af9 Update build and version for TestFlight 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
aa29688b4c Bump build and version number 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
371e6a275b Fix browser controls play button disabled state 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
0b7e9f8c47 PiP improvements 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
680fe915e1 Don't skip segments that start before 4 seconds
To minimize buffering
2022-05-27 23:05:09 +02:00
Arkadiusz Fal
1e7ebe4e68 Fix #126 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
1a230d36f7 Improve stream control on macOS 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
60e1cef84f Prefer VP9/WEBM over H.264/MP4 (fix #128) 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
1150a01496 Add PiP for iOS 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
e5c9e11b17 Minor improvements to controls 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
fb3d64bcc7 Fix rate button 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
dec670b7b7 Lint 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
ea4dc2358b Add resolution 8K 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
fca7b7a1a7 Run play action async 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
ea0db9533a Fix using Watch history in player queue 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
db85be76f7 Throttle SponsorBlock seek 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
5cb2e1b8f0 Add rate change selector 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
387f29e395 Restore last played item into queue only if it's not in there yet 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
9ae8c85f4e Improve subscriptions count
Piped API now includes it in the streams response, no need for separate
query
2022-05-27 23:05:09 +02:00
Arkadiusz Fal
c94a35d2c6 Add resolutions for 50fps and 48fps (fix #120) 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
aacbd7889c Add hd2160p60fps resolution (fix #118) 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
d8020d06dd Fix player size handling 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
c556f0e21d Minor fix 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
ff1f62b6ad Bump build and version number 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
7cb4e0dccf Fix #86 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
3229e3151f Improve EOF handling 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
53103c9ecf Try to patch #78
Issue appears when app switches layout from tab to sidebar navigation
2022-05-27 23:05:09 +02:00
Arkadiusz Fal
ac53deb39b Limit formats available to AVPlayer 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
cab6f486ba Fullscreen handling changes 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
40e16fcc7e Remove redunant update of player size 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
9c687f8704 Fix project settings 2022-05-27 23:05:09 +02:00
Arkadiusz Fal
340ceff131 Improve keyboard shortcuts 2022-05-27 23:05:08 +02:00
Arkadiusz Fal
76a116659e Minor fixes 2022-05-27 23:05:08 +02:00
Arkadiusz Fal
7c009080a5 Fix optional 2022-05-27 23:05:08 +02:00
Arkadiusz Fal
61d7c53a58 Bump build and version number 2022-05-27 23:05:08 +02:00
Arkadiusz Fal
08e65aff0f Controls fixes 2022-05-27 23:05:08 +02:00
Arkadiusz Fal
722616e3d0 tvOS fixes 2022-05-27 23:05:08 +02:00
Arkadiusz Fal
0826f01150 Close fullscreen and restore portrait on closing player 2022-05-27 23:05:08 +02:00
Arkadiusz Fal
fcb9ec1f6a Improve streams quality settings 2022-05-27 23:05:08 +02:00
Arkadiusz Fal
5bb1589159 Add tvOS mpv libraries 2022-05-27 23:05:08 +02:00
Arkadiusz Fal
60e5cc5d75 Fix player window on Mac 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
bc410ad6ba Minor improvements 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
35c358de6b Bump version number 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
23955699dc Add toggle for dislikes 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
742103e4c2 Bump version number 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
6173f4610b Minor fixes 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
1217acf264 Add ReturnYoutubeDislike API 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
0b8d887cef Fixes for MPV in macOS 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
b760f172d0 Fix EOF handler 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
bb8fc78760 Minor improvements 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
d2317f725f Add hide player button cancel action 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
693c3364b0 Prevent multiple seeks 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
18bfc1abc9 Add Now Playing info center updates 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
717830ee99 Hello, mpv! 🎉 2022-05-27 23:05:07 +02:00
Arkadiusz Fal
2b001ec96c Reorganize toolbars placement 2022-05-27 23:04:20 +02:00
Arkadiusz Fal
ebeb5bc520 Update version and iOS Info.plist for Testflight 2022-05-27 23:04:20 +02:00
Arkadiusz Fal
7a7e265ba1 Update build and version numbers 2022-05-27 22:59:10 +02:00
Arkadiusz Fal
c086112a49 Update README 2022-05-25 23:11:38 +02:00
Arkadiusz Fal
0802fe0029 Fix #133 2022-05-25 09:23:34 +02:00
Arkadiusz Fal
40813c2859 Update build number for Testflight 2022-05-25 09:14:27 +02:00
Arkadiusz Fal
b1238869a6 Update README 2022-05-22 23:27:59 +02:00
Arkadiusz Fal
453a5fa71b Update README 2022-05-22 23:23:05 +02:00
32 changed files with 698 additions and 480 deletions

View File

@@ -78,6 +78,9 @@ final class NavigationModel: ObservableObject {
return return
} }
player.presentingPlayer = false
navigation.presentingChannel = false
let recent = RecentItem(from: channel) let recent = RecentItem(from: channel)
#if os(macOS) #if os(macOS)
Windows.main.open() Windows.main.open()
@@ -113,6 +116,8 @@ final class NavigationModel: ObservableObject {
navigationStyle: NavigationStyle, navigationStyle: NavigationStyle,
delay: Bool = false delay: Bool = false
) { ) {
navigation.presentingPlaylist = false
let recent = RecentItem(from: playlist) let recent = RecentItem(from: playlist)
#if os(macOS) #if os(macOS)
Windows.main.open() Windows.main.open()

View File

@@ -169,8 +169,7 @@ final class AVPlayerBackend: PlayerBackend {
} }
#else #else
func closePiP(wasPlaying: Bool) { func closePiP(wasPlaying: Bool) {
controller?.playerView.player = nil model.pipController?.stopPictureInPicture()
controller?.playerView.player = avPlayer
guard wasPlaying else { guard wasPlaying else {
return return

View File

@@ -1,5 +1,6 @@
import AVFAudio import AVFAudio
import CoreMedia import CoreMedia
import Defaults
import Foundation import Foundation
import Logging import Logging
import SwiftUI import SwiftUI
@@ -241,9 +242,21 @@ final class MPVBackend: PlayerBackend {
client?.setDoubleAsync("speed", Double(rate)) client?.setDoubleAsync("speed", Double(rate))
} }
func closeItem() {} func closeItem() {
handleEOF = false
client?.pause()
client?.stop()
}
func enterFullScreen() {} func enterFullScreen() {
model.toggleFullscreen(controls?.playingFullscreen ?? false)
#if os(iOS)
if Defaults[.lockOrientationInFullScreen] {
Orientation.lockOrientation(.landscape, andRotateTo: UIDevice.current.orientation.isLandscape ? nil : .landscapeRight)
}
#endif
}
func exitFullScreen() {} func exitFullScreen() {}

View File

@@ -134,11 +134,11 @@ final class MPVClient: ObservableObject {
} }
var currentTime: CMTime { var currentTime: CMTime {
CMTime.secondsInDefaultTimescale(getDouble("time-pos")) CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("time-pos"))
} }
var duration: CMTime { var duration: CMTime {
CMTime.secondsInDefaultTimescale(getDouble("duration")) CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("duration"))
} }
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) { func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {

View File

@@ -19,16 +19,22 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
} }
func pictureInPictureControllerDidStopPictureInPicture(_: AVPictureInPictureController) { func pictureInPictureControllerDidStopPictureInPicture(_: AVPictureInPictureController) {
if player?.avPlayerBackend.switchToMPVOnPipClose ?? false { guard let player = player else {
DispatchQueue.main.async { [weak player] in return
player?.avPlayerBackend.switchToMPVOnPipClose = false }
player?.saveTime { [weak player] in
player?.changeActiveBackend(from: .appleAVPlayer, to: .mpv) if player.avPlayerBackend.switchToMPVOnPipClose,
!player.currentItem.isNil
{
DispatchQueue.main.async {
player.avPlayerBackend.switchToMPVOnPipClose = false
player.saveTime {
player.changeActiveBackend(from: .appleAVPlayer, to: .mpv)
} }
} }
} }
player?.playingInPictureInPicture = false player.playingInPictureInPicture = false
} }
func pictureInPictureControllerWillStopPictureInPicture(_: AVPictureInPictureController) {} func pictureInPictureControllerWillStopPictureInPicture(_: AVPictureInPictureController) {}
@@ -37,6 +43,12 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
_: AVPictureInPictureController, _: AVPictureInPictureController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) { ) {
completionHandler(true) if !player.currentItem.isNil {
player?.show()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
completionHandler(true)
}
} }
} }

View File

@@ -43,7 +43,7 @@ final class PlayerControlsModel: ObservableObject {
func handlePresentationChange() { func handlePresentationChange() {
if presentingControls { if presentingControls {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.player.backend.startControlsUpdates() self?.player?.backend.startControlsUpdates()
self?.resetTimer() self?.resetTimer()
} }
} else { } else {
@@ -94,9 +94,11 @@ final class PlayerControlsModel: ObservableObject {
} }
func resetTimer() { func resetTimer() {
if !presentingControls { #if os(tvOS)
show() if !presentingControls {
} show()
}
#endif
removeTimer() removeTimer()
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in
@@ -107,6 +109,29 @@ final class PlayerControlsModel: ObservableObject {
} }
} }
func startPiP(startImmediately: Bool = true) {
if player.activeBackend == .mpv {
player.avPlayerBackend.switchToMPVOnPipClose = true
}
#if !os(macOS)
player.exitFullScreen()
#endif
if player.activeBackend != PlayerBackendType.appleAVPlayer {
player.saveTime { [weak player] in
player?.changeActiveBackend(from: .mpv, to: .appleAVPlayer)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak player] in
player?.avPlayerBackend.startPictureInPictureOnPlay = true
if startImmediately {
player?.pipController?.startPictureInPicture()
}
}
}
func removeTimer() { func removeTimer() {
timer?.invalidate() timer?.invalidate()
timer = nil timer = nil

View File

@@ -57,8 +57,6 @@ final class PlayerModel: ObservableObject {
@Published var preservedTime: CMTime? @Published var preservedTime: CMTime?
@Published var playerNavigationLinkActive = false { didSet { handleNavigationViewPlayerPresentationChange() } }
@Published var sponsorBlock = SponsorBlockAPI() @Published var sponsorBlock = SponsorBlockAPI()
@Published var segmentRestorationTime: CMTime? @Published var segmentRestorationTime: CMTime?
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } } @Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
@@ -120,23 +118,24 @@ final class PlayerModel: ObservableObject {
} }
func show() { func show() {
guard !presentingPlayer else { #if os(macOS)
#if os(macOS) if presentingPlayer {
Windows.player.focus() Windows.player.focus()
#endif return
return }
} #endif
presentingPlayer = true
#if os(macOS) #if os(macOS)
Windows.player.open() Windows.player.open()
Windows.player.focus() Windows.player.focus()
#endif #endif
presentingPlayer = true
} }
func hide() { func hide() {
controls.playingFullscreen = false controls.playingFullscreen = false
presentingPlayer = false presentingPlayer = false
playerNavigationLinkActive = false
#if os(iOS) #if os(iOS)
if Defaults[.lockPortraitWhenBrowsing] { if Defaults[.lockPortraitWhenBrowsing] {
@@ -206,16 +205,25 @@ final class PlayerModel: ObservableObject {
backend.pause() backend.pause()
} }
func play(_ video: Video, at time: CMTime? = nil, inNavigationView: Bool = false) { func play(_ video: Video, at time: CMTime? = nil, showingPlayer: Bool = true) {
playNow(video, at: time) var delay = 0.0
#if !os(macOS)
delay = 0.3
#endif
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
guard let self = self else {
return
}
self.playNow(video, at: time)
}
guard !playingInPictureInPicture else { guard !playingInPictureInPicture else {
return return
} }
if inNavigationView { if showingPlayer {
playerNavigationLinkActive = true
} else {
show() show()
} }
} }
@@ -262,6 +270,7 @@ final class PlayerModel: ObservableObject {
func saveTime(completionHandler: @escaping () -> Void = {}) { func saveTime(completionHandler: @escaping () -> Void = {}) {
guard let currentTime = backend.currentTime, currentTime.seconds > 0 else { guard let currentTime = backend.currentTime, currentTime.seconds > 0 else {
completionHandler()
return return
} }
@@ -272,8 +281,12 @@ final class PlayerModel: ObservableObject {
} }
func upgradeToStream(_ stream: Stream, force: Bool = false) { func upgradeToStream(_ stream: Stream, force: Bool = false) {
guard let video = currentVideo else {
return
}
if !self.stream.isNil, force || self.stream != stream { if !self.stream.isNil, force || self.stream != stream {
playStream(stream, of: currentVideo!, preservingTime: true, upgrading: true) playStream(stream, of: video, preservingTime: true, upgrading: true)
} }
} }
@@ -297,7 +310,18 @@ final class PlayerModel: ObservableObject {
} }
private func handlePresentationChange() { private func handlePresentationChange() {
backend.setNeedsDrawing(presentingPlayer) var delay = 0.0
#if os(iOS)
if presentingPlayer {
delay = 0.2
}
#endif
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
self?.backend.setNeedsDrawing(self?.presentingPlayer ?? false)
}
controls.hide() controls.hide()
#if !os(macOS) #if !os(macOS)
@@ -323,18 +347,11 @@ final class PlayerModel: ObservableObject {
} }
} }
private func handleNavigationViewPlayerPresentationChange() {
backend.setNeedsDrawing(playerNavigationLinkActive)
controls.hide()
if pauseOnHidingPlayer, !playingInPictureInPicture, !playerNavigationLinkActive {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.pause()
}
}
}
func changeActiveBackend(from: PlayerBackendType, to: PlayerBackendType) { func changeActiveBackend(from: PlayerBackendType, to: PlayerBackendType) {
guard activeBackend != to else {
return
}
Defaults[.activeBackend] = to Defaults[.activeBackend] = to
self.activeBackend = to self.activeBackend = to
@@ -361,7 +378,7 @@ final class PlayerModel: ObservableObject {
return return
} }
if !backend.canPlay(stream) { if !backend.canPlay(stream) || (to == .mpv && !stream.hlsURL.isNil) {
guard let preferredStream = preferredStream(availableStreams) else { guard let preferredStream = preferredStream(availableStreams) else {
return return
} }
@@ -371,7 +388,11 @@ final class PlayerModel: ObservableObject {
} }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.upgradeToStream(stream, force: true) guard let self = self else {
return
}
self.upgradeToStream(stream, force: true)
self.setNeedsDrawing(self.presentingPlayer)
} }
} }
@@ -448,6 +469,10 @@ final class PlayerModel: ObservableObject {
logger.info("exiting fullscreen") logger.info("exiting fullscreen")
if controls.playingFullscreen {
toggleFullscreen(true)
}
backend.exitFullScreen() backend.exitFullScreen()
} }
#endif #endif
@@ -524,4 +549,8 @@ final class PlayerModel: ObservableObject {
} }
#endif #endif
} }
func setNeedsDrawing(_ needsDrawing: Bool) {
backends.forEach { $0.setNeedsDrawing(needsDrawing) }
}
} }

View File

@@ -8,7 +8,7 @@ extension PlayerModel {
currentItem?.video currentItem?.video
} }
func play(_ videos: [Video], shuffling: Bool = false, inNavigationView: Bool = false) { func play(_ videos: [Video], shuffling: Bool = false) {
let videosToPlay = shuffling ? videos.shuffled() : videos let videosToPlay = shuffling ? videos.shuffled() : videos
guard let first = videosToPlay.first else { guard let first = videosToPlay.first else {
@@ -27,11 +27,7 @@ extension PlayerModel {
} }
} }
if inNavigationView { show()
playerNavigationLinkActive = true
} else {
show()
}
} }
func playNext(_ video: Video) { func playNext(_ video: Video) {

View File

@@ -4,6 +4,7 @@ import Foundation
final class RecentsModel: ObservableObject { final class RecentsModel: ObservableObject {
@Default(.recentlyOpened) var items @Default(.recentlyOpened) var items
@Default(.saveRecents) var saveRecents @Default(.saveRecents) var saveRecents
func clear() { func clear() {
items = [] items = []
} }

View File

@@ -22,4 +22,8 @@ final class Store<Data>: ResourceObserver, ObservableObject {
func replace(_ items: Data) { func replace(_ items: Data) {
all = items all = items
} }
func clear() {
all = nil
}
} }

View File

@@ -1,10 +1,12 @@
import CoreData import CoreData
import CoreMedia
import Defaults import Defaults
import Foundation import Foundation
@objc(Watch) @objc(Watch)
final class Watch: NSManagedObject, Identifiable { final class Watch: NSManagedObject, Identifiable {
@Default(.watchedThreshold) private var watchedThreshold @Default(.watchedThreshold) private var watchedThreshold
@Default(.saveHistory) private var saveHistory
} }
extension Watch { extension Watch {
@@ -45,4 +47,15 @@ extension Watch {
formatter.unitsStyle = .full formatter.unitsStyle = .full
return formatter.localizedString(for: watchedAt, relativeTo: Date()) return formatter.localizedString(for: watchedAt, relativeTo: Date())
} }
var timeToRestart: CMTime? {
finished ? nil : saveHistory ? .secondsInDefaultTimescale(stoppedAt) : nil
}
var video: Video {
Video(
videoID: videoID, title: "", author: "",
length: 0, published: "", views: -1, channel: Channel(id: "", name: "")
)
}
} }

View File

@@ -1,4 +1,7 @@
<div align="center"> <div align="center">
<h3><strong>📣 <a href="https://yattee.stream/beta">TestFlight beta</a> now available 📣 </strong></h3>
<hr />
<img src="https://r.yattee.stream/icons/yattee-150.png" width="150" height="150" alt="Yattee logo"> <img src="https://r.yattee.stream/icons/yattee-150.png" width="150" height="150" alt="Yattee logo">
<h1>Yattee</h1> <h1>Yattee</h1>
<p>Alternative YouTube frontend for iOS, tvOS and macOS<br />built with <a href="https://github.com/iv-org/invidious">Invidious</a> and <a href="https://github.com/TeamPiped/Piped">Piped</a></p> <p>Alternative YouTube frontend for iOS, tvOS and macOS<br />built with <a href="https://github.com/iv-org/invidious">Invidious</a> and <a href="https://github.com/TeamPiped/Piped">Piped</a></p>
@@ -19,17 +22,16 @@
* Fullscreen playback, Picture in Picture and AirPlay support * Fullscreen playback, Picture in Picture and AirPlay support
* Stream quality selection * Stream quality selection
### Features in alpha testing ### Features in development
* New player component with custom controls, gestures and support for 4K playback * New player component with custom controls, gestures and support for 4K playback
You can leave your feedback in [discussion on v1.4 release](https://github.com/yattee/yattee/discussions/93) or join [Matrix Channel](https://matrix.to/#/#yattee:matrix.org) for a chat. Thanks! You can leave your feedback in [discussion on v1.4.alpha.4 release](https://github.com/yattee/yattee/discussions/132) or join [Matrix Channel](https://matrix.to/#/#yattee:matrix.org) for a chat. Thanks!
### Availability ### Availability
|| Invidious | Piped | || Invidious | Piped |
| - | - | - | | - | - | - |
| User Accounts | ✅ | ✅ | | User Accounts | ✅ | ✅ |
| Subscriptions | ✅ | ✅ | | Subscriptions | ✅ | ✅ |
| Popular | ✅ | 🔴 |
| User Playlists | ✅ | ✅ | | User Playlists | ✅ | ✅ |
| Trending | ✅ | ✅ | | Trending | ✅ | ✅ |
| Channels | ✅ | ✅ | | Channels | ✅ | ✅ |
@@ -37,13 +39,15 @@ You can leave your feedback in [discussion on v1.4 release](https://github.com/y
| Search | ✅ | ✅ | | Search | ✅ | ✅ |
| Search Suggestions | ✅ | ✅ | | Search Suggestions | ✅ | ✅ |
| Search Filters | ✅ | 🔴 | | Search Filters | ✅ | 🔴 |
| Popular | ✅ | 🔴 |
| Subtitles | 🔴 | ✅ | | Subtitles | 🔴 | ✅ |
| Comments | 🔴 | ✅ | | Comments | 🔴 | ✅ |
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. 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.
## Documentation ## Documentation
* [Installation Instructions](https://github.com/yattee/yattee/wiki/Installation-Instructions) * [Installation](https://github.com/yattee/yattee/wiki/Installation-Instructions)
* [Building](https://github.com/yattee/yattee/wiki/Building-instructions)
* [FAQ](https://github.com/yattee/yattee/wiki) * [FAQ](https://github.com/yattee/yattee/wiki)
* [Screenshots Gallery](https://github.com/yattee/yattee/wiki/Screenshots-Gallery) * [Screenshots Gallery](https://github.com/yattee/yattee/wiki/Screenshots-Gallery)
* [Tips](https://github.com/yattee/yattee/wiki/Tips) * [Tips](https://github.com/yattee/yattee/wiki/Tips)
@@ -53,11 +57,16 @@ You can browse and use accounts from one app and play videos with another (for e
## Contributing ## Contributing
If you're interestred in contributing, you can browse the [issues](https://github.com/yattee/yattee/issues) list or create a new one to discuss your feature idea. Every contribution is very welcome. If you're interestred in contributing, you can browse the [issues](https://github.com/yattee/yattee/issues) list or create a new one to discuss your feature idea. Every contribution is very welcome.
Use [building instructions](https://github.com/yattee/yattee/wiki/Building-instructions) or
Join [Matrix Channel](https://matrix.to/#/#yattee:matrix.org) for a chat if you need an advice or want to discuss the project. Join [Matrix Channel](https://matrix.to/#/#yattee:matrix.org) for a chat if you need an advice or want to discuss the project.
## License and Liability
## License
Yattee and its components is shared on [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) license. Yattee and its components is shared on [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) license.
Contributors take no responsibility for the use of the tool (Point 16. of the license). We strongly recommend you abide by the valid official regulations in your country. Furthermore, we refuse liability for any inappropriate use of the tool, such as downloading materials without proper consent.
## Disclaimer
The Yattee project and its contents are not affiliated with, funded, authorized, endorsed by, or in any way accociated with YouTube, Google LLC or any of its affiliates and subsidaries. The official YouTube website can be found at www.youtube.com.
Any trademark, service mark, trade name, or other intellectual property rights used in the Yattee project are owned by the respective owners.
This tool is an open source software built for learning and research purposes. This tool is an open source software built for learning and research purposes.

View File

@@ -26,12 +26,6 @@ extension Defaults.Keys {
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false) static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
static let favorites = Key<[FavoriteItem]>("favorites", default: [ static let favorites = Key<[FavoriteItem]>("favorites", default: [
.init(section: .trending("US", "default")),
.init(section: .trending("GB", "default")),
.init(section: .trending("ES", "default")),
.init(section: .channel("UC-lHJZR3Gqxm24_Vd_AJ5Yw", "PewDiePie")),
.init(section: .channel("UCXuqSBlHAE6Xw-yeJA0Tunw", "Linus Tech Tips")),
.init(section: .channel("UCBJycsmduvYEL83R_U4JriQ", "Marques Brownlee")),
.init(section: .channel("UCE_M8A5yxnLfW0KghEeajjw", "Apple")) .init(section: .channel("UCE_M8A5yxnLfW0KghEeajjw", "Apple"))
]) ])
@@ -89,8 +83,7 @@ extension Defaults.Keys {
#if os(iOS) #if os(iOS)
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true) static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone) static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
static let lockLandscapeOnRotation = Key<Bool>("lockLandscapeOnRotation", default: false) static let lockOrientationInFullScreen = Key<Bool>("lockOrientationInFullScreen", default: false)
static let lockLandscapeWhenEnteringFullscreen = Key<Bool>("lockLandscapeWhenEnteringFullscreen", default: false)
#endif #endif
} }

View File

@@ -1,10 +1,6 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
private struct InNavigationViewKey: EnvironmentKey {
static let defaultValue = false
}
private struct InChannelViewKey: EnvironmentKey { private struct InChannelViewKey: EnvironmentKey {
static let defaultValue = false static let defaultValue = false
} }
@@ -40,11 +36,6 @@ private struct ScrollViewBottomPaddingKey: EnvironmentKey {
} }
extension EnvironmentValues { extension EnvironmentValues {
var inNavigationView: Bool {
get { self[InNavigationViewKey.self] }
set { self[InNavigationViewKey.self] = newValue }
}
var inChannelView: Bool { var inChannelView: Bool {
get { self[InChannelViewKey.self] } get { self[InChannelViewKey.self] }
set { self[InChannelViewKey.self] = newValue } set { self[InChannelViewKey.self] = newValue }

View File

@@ -63,24 +63,6 @@ struct AppSidebarNavigation: View {
} }
} }
.environment(\.navigationStyle, .sidebar) .environment(\.navigationStyle, .sidebar)
#if os(iOS)
.background(
EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) {
VideoPlayerView()
.environmentObject(accounts)
.environmentObject(comments)
.environmentObject(instances)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(playerControls)
.environmentObject(playlists)
.environmentObject(recents)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
.environment(\.navigationStyle, .sidebar)
}
)
#endif
} }
var toolbarContent: some ToolbarContent { var toolbarContent: some ToolbarContent {

View File

@@ -43,51 +43,9 @@ struct AppTabNavigation: View {
searchNavigationView searchNavigationView
} }
.id(accounts.current?.id ?? "") .id(accounts.current?.id ?? "")
.overlay(playlistView)
.overlay(channelView)
.environment(\.navigationStyle, .tab) .environment(\.navigationStyle, .tab)
.background(
EmptyView().sheet(isPresented: $navigation.presentingChannel) {
if let channel = recents.presentedChannel {
NavigationView {
ChannelVideosView(channel: channel)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environment(\.inChannelView, true)
.environment(\.inNavigationView, true)
.environmentObject(accounts)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
.background(playerNavigationLink)
}
}
}
)
.background(
EmptyView().sheet(isPresented: $navigation.presentingPlaylist) {
if let playlist = recents.presentedPlaylist {
NavigationView {
ChannelPlaylistView(playlist: playlist)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environment(\.inNavigationView, true)
.environmentObject(accounts)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
.background(playerNavigationLink)
}
}
}
)
.background(
EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) {
videoPlayer
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environment(\.navigationStyle, .tab)
}
)
} }
private var favoritesNavigationView: some View { private var favoritesNavigationView: some View {
@@ -172,15 +130,6 @@ struct AppTabNavigation: View {
.tag(TabSelection.search) .tag(TabSelection.search)
} }
private var playerNavigationLink: some View {
NavigationLink(isActive: $player.playerNavigationLinkActive, destination: {
videoPlayer
.environment(\.inNavigationView, true)
}) {
EmptyView()
}
}
private var videoPlayer: some View { private var videoPlayer: some View {
VideoPlayerView() VideoPlayerView()
.environmentObject(accounts) .environmentObject(accounts)
@@ -210,4 +159,26 @@ struct AppTabNavigation: View {
} }
#endif #endif
} }
private var channelView: some View {
ChannelVideosView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environment(\.inChannelView, true)
.environment(\.navigationStyle, .tab)
.environmentObject(accounts)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
}
private var playlistView: some View {
ChannelPlaylistView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(accounts)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
}
} }

View File

@@ -26,6 +26,8 @@ struct ContentView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.horizontalSizeClass) private var horizontalSizeClass
#endif #endif
let persistenceController = PersistenceController.shared
var body: some View { var body: some View {
Group { Group {
#if os(iOS) #if os(iOS)
@@ -57,50 +59,54 @@ struct ContentView: View {
.environmentObject(subscriptions) .environmentObject(subscriptions)
.environmentObject(thumbnailsModel) .environmentObject(thumbnailsModel)
// iOS 14 has problem with multiple sheets in one view #if os(iOS)
// but it's ok when it's in background .overlay(videoPlayer)
.background(
EmptyView().sheet(isPresented: $navigation.presentingWelcomeScreen) {
WelcomeScreen()
.environmentObject(accounts)
.environmentObject(navigation)
}
)
#if !os(tvOS)
.onOpenURL { OpenURLHandler(accounts: accounts, player: player).handle($0) }
.background(
EmptyView().sheet(isPresented: $navigation.presentingAddToPlaylist) {
AddToPlaylistView(video: navigation.videoToAddToPlaylist)
.environmentObject(playlists)
}
)
.background(
EmptyView().sheet(isPresented: $navigation.presentingPlaylistForm) {
PlaylistFormView(playlist: $navigation.editedPlaylist)
.environmentObject(accounts)
.environmentObject(playlists)
}
)
.background(
EmptyView().sheet(isPresented: $navigation.presentingSettings, onDismiss: openWelcomeScreenIfAccountEmpty) {
SettingsView()
.environmentObject(accounts)
.environmentObject(instances)
.environmentObject(player)
}
)
#endif #endif
.alert(isPresented: $navigation.presentingUnsubscribeAlert) {
Alert( // iOS 14 has problem with multiple sheets in one view
title: Text( // but it's ok when it's in background
"Are you sure you want to unsubscribe from \(navigation.channelToUnsubscribe.name)?" .background(
), EmptyView().sheet(isPresented: $navigation.presentingWelcomeScreen) {
primaryButton: .destructive(Text("Unsubscribe")) { WelcomeScreen()
subscriptions.unsubscribe(navigation.channelToUnsubscribe.id) .environmentObject(accounts)
}, .environmentObject(navigation)
secondaryButton: .cancel() }
) )
} #if !os(tvOS)
.onOpenURL { OpenURLHandler(accounts: accounts, player: player).handle($0) }
.background(
EmptyView().sheet(isPresented: $navigation.presentingAddToPlaylist) {
AddToPlaylistView(video: navigation.videoToAddToPlaylist)
.environmentObject(playlists)
}
)
.background(
EmptyView().sheet(isPresented: $navigation.presentingPlaylistForm) {
PlaylistFormView(playlist: $navigation.editedPlaylist)
.environmentObject(accounts)
.environmentObject(playlists)
}
)
.background(
EmptyView().sheet(isPresented: $navigation.presentingSettings, onDismiss: openWelcomeScreenIfAccountEmpty) {
SettingsView()
.environmentObject(accounts)
.environmentObject(instances)
.environmentObject(player)
}
)
#endif
.alert(isPresented: $navigation.presentingUnsubscribeAlert) {
Alert(
title: Text(
"Are you sure you want to unsubscribe from \(navigation.channelToUnsubscribe.name)?"
),
primaryButton: .destructive(Text("Unsubscribe")) {
subscriptions.unsubscribe(navigation.channelToUnsubscribe.id)
},
secondaryButton: .cancel()
)
}
} }
func configure() { func configure() {
@@ -222,6 +228,21 @@ struct ContentView: View {
navigation.presentingWelcomeScreen = true navigation.presentingWelcomeScreen = true
} }
var videoPlayer: some View {
VideoPlayerView()
.environmentObject(accounts)
.environmentObject(comments)
.environmentObject(instances)
.environmentObject(navigation)
.environmentObject(player)
.environmentObject(playerControls)
.environmentObject(playlists)
.environmentObject(recents)
.environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
.environment(\.navigationStyle, .sidebar)
}
} }
struct ContentView_Previews: PreviewProvider { struct ContentView_Previews: PreviewProvider {

View File

@@ -141,7 +141,7 @@ extension AppleAVPlayerViewController: AVPlayerViewControllerDelegate {
willBeginFullScreenPresentationWithAnimationCoordinator context: UIViewControllerTransitionCoordinator willBeginFullScreenPresentationWithAnimationCoordinator context: UIViewControllerTransitionCoordinator
) { ) {
#if os(iOS) #if os(iOS)
if !context.isCancelled, Defaults[.lockLandscapeWhenEnteringFullscreen] { if !context.isCancelled, Defaults[.lockOrientationInFullScreen] {
Orientation.lockOrientation(.landscape, andRotateTo: UIDevice.current.orientation.isLandscape ? nil : .landscapeRight) Orientation.lockOrientation(.landscape, andRotateTo: UIDevice.current.orientation.isLandscape ? nil : .landscapeRight)
} }
#endif #endif
@@ -178,11 +178,8 @@ extension AppleAVPlayerViewController: AVPlayerViewControllerDelegate {
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) { ) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if self.navigationModel.presentingChannel { self.playerModel.show()
self.playerModel.playerNavigationLinkActive = true self.playerModel.setNeedsDrawing(true)
} else {
self.playerModel.show()
}
#if os(tvOS) #if os(tvOS)
if self.playerModel.playingInPictureInPicture { if self.playerModel.playingInPictureInPicture {
@@ -198,7 +195,6 @@ extension AppleAVPlayerViewController: AVPlayerViewControllerDelegate {
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) { func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {
playerModel.playingInPictureInPicture = true playerModel.playingInPictureInPicture = true
playerModel.playerNavigationLinkActive = false
} }
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) { func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {

View File

@@ -53,18 +53,24 @@ struct PlayerControls: View {
Spacer() Spacer()
timeline Group {
.offset(y: 10) timeline
.zIndex(1) .offset(y: 10)
.zIndex(1)
bottomBar HStack {
Spacer()
#if os(macOS) bottomBar
.background(VisualEffectBlur(material: .hudWindow)) #if os(macOS)
#elseif os(iOS) .background(VisualEffectBlur(material: .hudWindow))
.background(VisualEffectBlur(blurStyle: .systemThinMaterial)) #elseif os(iOS)
#endif .background(VisualEffectBlur(blurStyle: .systemThinMaterial))
.mask(RoundedRectangle(cornerRadius: 3)) #endif
.mask(RoundedRectangle(cornerRadius: 3))
}
}
.padding(.horizontal, 16)
} }
} }
.opacity(model.presentingControls ? 1 : 0) .opacity(model.presentingControls ? 1 : 0)
@@ -104,9 +110,6 @@ struct PlayerControls: View {
var statusBar: some View { var statusBar: some View {
HStack(spacing: 4) { HStack(spacing: 4) {
#if os(iOS)
hidePlayerButton
#endif
Text(playbackStatus) Text(playbackStatus)
Text("") Text("")
@@ -129,11 +132,10 @@ struct PlayerControls: View {
} }
private var hidePlayerButton: some View { private var hidePlayerButton: some View {
Button { button("Hide", systemImage: "chevron.down") {
player.hide() player.hide()
} label: {
Image(systemName: "chevron.down.circle.fill")
} }
#if !os(tvOS) #if !os(tvOS)
.keyboardShortcut(.cancelAction) .keyboardShortcut(.cancelAction)
#endif #endif
@@ -170,14 +172,20 @@ struct PlayerControls: View {
} }
var buttonsBar: some View { var buttonsBar: some View {
HStack { HStack(spacing: 20) {
#if !os(tvOS) #if !os(tvOS)
#if os(iOS)
hidePlayerButton
#endif
fullscreenButton fullscreenButton
#if os(iOS) #if os(iOS)
pipButton pipButton
#endif #endif
rateButton rateButton
closeVideoButton
Spacer() Spacer()
#endif #endif
// button("Music Mode", systemImage: "music.note") // button("Music Mode", systemImage: "music.note")
@@ -225,6 +233,23 @@ struct PlayerControls: View {
#endif #endif
} }
private var closeVideoButton: some View {
button("Close", systemImage: "xmark") {
player.pause()
player.hide()
player.closePiP()
var delay = 0.2
#if os(macOS)
delay = 0.0
#endif
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
player.closeCurrentItem()
}
}
}
var ratePicker: some View { var ratePicker: some View {
Picker("Rate", selection: rateBinding) { Picker("Rate", selection: rateBinding) {
ForEach(PlayerModel.availableRates, id: \.self) { rate in ForEach(PlayerModel.availableRates, id: \.self) { rate in
@@ -240,28 +265,17 @@ struct PlayerControls: View {
private var pipButton: some View { private var pipButton: some View {
button("PiP", systemImage: "pip") { button("PiP", systemImage: "pip") {
if player.activeBackend == .mpv { model.startPiP()
player.avPlayerBackend.switchToMPVOnPipClose = true
}
if player.activeBackend != PlayerBackendType.appleAVPlayer {
player.saveTime {
player.changeActiveBackend(from: .mpv, to: .appleAVPlayer)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
print(player.pipController?.isPictureInPicturePossible ?? false ? "possible" : "NOT possible")
player.avPlayerBackend.startPictureInPictureOnPlay = true
player.pipController?.startPictureInPicture()
}
} }
} }
var mediumButtonsBar: some View { var mediumButtonsBar: some View {
HStack { HStack {
#if !os(tvOS) #if !os(tvOS)
button("Seek Backward", systemImage: "gobackward.10", size: 50, cornerRadius: 10) { restartVideoButton
.padding(.trailing, 15)
button("Seek Backward", systemImage: "gobackward.10", size: 30, cornerRadius: 5) {
player.backend.seek(relative: .secondsInDefaultTimescale(-10)) player.backend.seek(relative: .secondsInDefaultTimescale(-10))
} }
@@ -279,8 +293,7 @@ struct PlayerControls: View {
button( button(
model.isPlaying ? "Pause" : "Play", model.isPlaying ? "Pause" : "Play",
systemImage: model.isPlaying ? "pause.fill" : "play.fill", systemImage: model.isPlaying ? "pause.fill" : "play.fill",
size: 50, size: 30, cornerRadius: 5
cornerRadius: 10
) { ) {
player.backend.togglePlay() player.backend.togglePlay()
} }
@@ -295,7 +308,7 @@ struct PlayerControls: View {
Spacer() Spacer()
#if !os(tvOS) #if !os(tvOS)
button("Seek Forward", systemImage: "goforward.10", size: 50, cornerRadius: 10) { button("Seek Forward", systemImage: "goforward.10", size: 30, cornerRadius: 5) {
player.backend.seek(relative: .secondsInDefaultTimescale(10)) player.backend.seek(relative: .secondsInDefaultTimescale(10))
} }
#if os(tvOS) #if os(tvOS)
@@ -304,16 +317,30 @@ struct PlayerControls: View {
.keyboardShortcut("l", modifiers: []) .keyboardShortcut("l", modifiers: [])
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: []) .keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
#endif #endif
advanceToNextItemButton
.padding(.leading, 15)
#endif #endif
} }
.font(.system(size: 30)) .font(.system(size: 20))
.padding(.horizontal, 4) .padding(.horizontal, 4)
} }
private var restartVideoButton: some View {
button("Restart video", systemImage: "backward.end.fill", size: 30, cornerRadius: 5) {
player.backend.seek(to: 0.0)
}
}
private var advanceToNextItemButton: some View {
button("Next", systemImage: "forward.fill", size: 30, cornerRadius: 5) {
player.advanceToNextItem()
}
.disabled(player.queue.isEmpty)
}
var bottomBar: some View { var bottomBar: some View {
HStack { HStack {
Spacer()
Text(model.playbackTime) Text(model.playbackTime)
} }
.font(.system(size: 15)) .font(.system(size: 15))
@@ -361,6 +388,25 @@ struct PlayerControls: View {
struct PlayerControls_Previews: PreviewProvider { struct PlayerControls_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
PlayerControls(player: PlayerModel()) let model = PlayerControlsModel()
model.presentingControls = true
model.currentTime = .secondsInDefaultTimescale(0)
model.duration = .secondsInDefaultTimescale(120)
let view = ZStack {
Color.gray
PlayerControls(player: PlayerModel())
.injectFixtureEnvironmentObjects()
.environmentObject(model)
}
return Group {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
view.previewInterfaceOrientation(.landscapeLeft)
} else {
view
}
}
} }
} }

View File

@@ -10,7 +10,7 @@ struct TimelineView: View {
@State private var draggedFrom: Double = 0 @State private var draggedFrom: Double = 0
private var start: Double = 0.0 private var start: Double = 0.0
private var height = 10.0 private var height = 8.0
var cornerRadius: Double var cornerRadius: Double
var thumbTooltipWidth: Double = 100 var thumbTooltipWidth: Double = 100
@@ -26,26 +26,25 @@ struct TimelineView: View {
var body: some View { var body: some View {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: cornerRadius) Group {
.foregroundColor(.blue) RoundedRectangle(cornerRadius: cornerRadius)
.frame(maxHeight: height) .foregroundColor(.blue)
.frame(maxHeight: height)
RoundedRectangle(cornerRadius: cornerRadius) RoundedRectangle(cornerRadius: cornerRadius)
.fill( .fill(Color.green)
Color.green .frame(maxHeight: height)
) .frame(width: current * oneUnitWidth)
.frame(maxHeight: height)
.frame(width: current * oneUnitWidth)
segmentsLayers segmentsLayers
}
Circle() Circle()
.strokeBorder(.gray, lineWidth: 1) .strokeBorder(.gray, lineWidth: 1)
.background(Circle().fill(dragging ? .gray : .white)) .background(Circle().fill(dragging ? .gray : .white))
.offset(x: thumbOffset) .offset(x: thumbOffset)
.foregroundColor(.red.opacity(0.6)) .foregroundColor(.red.opacity(0.6))
.frame(maxHeight: height * 4)
.frame(maxHeight: height * 2)
#if !os(tvOS) #if !os(tvOS)
.gesture( .gesture(
@@ -114,7 +113,7 @@ struct TimelineView: View {
var projectedValue: Double { var projectedValue: Double {
let change = (dragOffset / size.width) * units let change = (dragOffset / size.width) * units
let projected = draggedFrom + change let projected = draggedFrom + change
return projected.isFinite ? projected : start return projected.isFinite ? (duration - projected < (0.01 * duration) ? duration : projected) : start
} }
var thumbOffset: Double { var thumbOffset: Double {
@@ -192,6 +191,7 @@ struct TimelineView_Previews: PreviewProvider {
TimelineView(duration: .constant(100), current: .constant(90)) TimelineView(duration: .constant(100), current: .constant(90))
TimelineView(duration: .constant(100), current: .constant(100)) TimelineView(duration: .constant(100), current: .constant(100))
} }
.environmentObject(PlayerModel())
.padding() .padding()
} }
} }

View File

@@ -21,7 +21,6 @@ struct VideoDetails: View {
@State private var currentPage = Page.info @State private var currentPage = Page.info
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
@Environment(\.inNavigationView) private var inNavigationView
@Environment(\.navigationStyle) private var navigationStyle @Environment(\.navigationStyle) private var navigationStyle
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@@ -112,7 +111,6 @@ struct VideoDetails: View {
.edgesIgnoringSafeArea(.horizontal) .edgesIgnoringSafeArea(.horizontal)
} }
} }
.padding(.top, inNavigationView && fullScreen ? 10 : 0)
.onAppear { .onAppear {
if video.isNil && !sidebarQueue { if video.isNil && !sidebarQueue {
currentPage = .queue currentPage = .queue
@@ -428,7 +426,7 @@ struct VideoDetails: View {
var detailsPage: some View { var detailsPage: some View {
Group { Group {
Group { VStack(alignment: .leading, spacing: 0) {
if let video = player.currentVideo { if let video = player.currentVideo {
VStack(spacing: 6) { VStack(spacing: 6) {
HStack { HStack {
@@ -442,6 +440,7 @@ struct VideoDetails: View {
Divider() Divider()
} }
.padding(.bottom, 6)
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
if let description = video.description { if let description = video.description {

View File

@@ -7,6 +7,10 @@ import Siesta
import SwiftUI import SwiftUI
struct VideoPlayerView: View { struct VideoPlayerView: View {
#if os(iOS)
static let hiddenOffset = max(UIScreen.main.bounds.height, UIScreen.main.bounds.width) + 100
#endif
static let defaultAspectRatio = 16 / 9.0 static let defaultAspectRatio = 16 / 9.0
static var defaultMinimumHeightLeft: Double { static var defaultMinimumHeightLeft: Double {
#if os(macOS) #if os(macOS)
@@ -28,7 +32,7 @@ struct VideoPlayerView: View {
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape @Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock @Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
@Default(.lockLandscapeOnRotation) private var lockLandscapeOnRotation @Default(.lockOrientationInFullScreen) private var lockOrientationInFullScreen
@State private var motionManager: CMMotionManager! @State private var motionManager: CMMotionManager!
@State private var orientation = UIInterfaceOrientation.portrait @State private var orientation = UIInterfaceOrientation.portrait
@@ -37,6 +41,10 @@ struct VideoPlayerView: View {
var mouseLocation: CGPoint { NSEvent.mouseLocation } var mouseLocation: CGPoint { NSEvent.mouseLocation }
#endif #endif
#if os(iOS)
@State private var viewVerticalOffset = Self.hiddenOffset
#endif
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<PlayerControlsModel> private var playerControls @EnvironmentObject<PlayerControlsModel> private var playerControls
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@@ -54,10 +62,6 @@ struct VideoPlayerView: View {
content content
.onAppear { .onAppear {
playerSize = geometry.size playerSize = geometry.size
#if os(iOS)
configureOrientationUpdatesBasedOnAccelerometer()
#endif
} }
} }
.onChange(of: geometry.size) { size in .onChange(of: geometry.size) { size in
@@ -70,22 +74,28 @@ struct VideoPlayerView: View {
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
handleOrientationDidChangeNotification() handleOrientationDidChangeNotification()
} }
.onDisappear { .onChange(of: player.presentingPlayer) { newValue in
guard !playerControls.playingFullscreen else { if newValue {
return // swiftlint:disable:this implicit_return viewVerticalOffset = 0
} configureOrientationUpdatesBasedOnAccelerometer()
if Defaults[.lockPortraitWhenBrowsing] {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else { } else {
Orientation.lockOrientation(.allButUpsideDown) if Defaults[.lockPortraitWhenBrowsing] {
} Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else {
Orientation.lockOrientation(.allButUpsideDown)
}
motionManager?.stopAccelerometerUpdates() motionManager?.stopAccelerometerUpdates()
motionManager = nil motionManager = nil
viewVerticalOffset = Self.hiddenOffset
}
} }
#endif #endif
} }
#if os(iOS)
.offset(y: viewVerticalOffset)
.animation(.easeIn(duration: 0.2), value: viewVerticalOffset)
#endif
#endif #endif
} }
@@ -138,29 +148,49 @@ struct VideoPlayerView: View {
hoveringPlayer = hovering hoveringPlayer = hovering
hovering ? playerControls.show() : playerControls.hide() hovering ? playerControls.show() : playerControls.hide()
} }
#if os(iOS) #if !os(macOS)
.onSwipeGesture( .gesture(
up: { DragGesture(coordinateSpace: .global)
withAnimation { .onChanged { value in
fullScreenDetails = true guard !fullScreenLayout else {
return // swiftlint:disable:this implicit_return
}
player.backend.setNeedsDrawing(false)
let drag = value.translation.height
guard drag > 0 else {
return // swiftlint:disable:this implicit_return
}
withAnimation(.easeInOut(duration: 0.2)) {
viewVerticalOffset = drag
}
}
.onEnded { _ in
if viewVerticalOffset > 100 {
player.backend.setNeedsDrawing(false)
player.hide()
} else {
viewVerticalOffset = 0
player.backend.setNeedsDrawing(true)
player.show()
}
} }
},
down: { player.hide() }
) )
#else
.onAppear(perform: {
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
if hoveringPlayer {
playerControls.resetTimer()
}
#elseif os(macOS) return $0
.onAppear(perform: { }
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { })
if hoveringPlayer {
playerControls.resetTimer()
}
return $0
}
})
#endif #endif
.background(Color.black) .background(Color.black)
#if !os(tvOS) #if !os(tvOS)
if !playerControls.playingFullscreen { if !playerControls.playingFullscreen {
@@ -269,20 +299,34 @@ struct VideoPlayerView: View {
} }
func playerPlaceholder(geometry: GeometryProxy) -> some View { func playerPlaceholder(geometry: GeometryProxy) -> some View {
HStack { ZStack(alignment: .topLeading) {
Spacer() HStack {
VStack {
Spacer() Spacer()
VStack(spacing: 10) { VStack {
#if !os(tvOS) Spacer()
Image(systemName: "ticket") VStack(spacing: 10) {
.font(.system(size: 120)) #if !os(tvOS)
#endif Image(systemName: "ticket")
.font(.system(size: 120))
#endif
}
Spacer()
} }
.foregroundColor(.gray)
Spacer() Spacer()
} }
.foregroundColor(.gray)
Spacer() #if os(iOS)
Button {
player.hide()
} label: {
Image(systemName: "xmark")
.font(.system(size: 40))
}
.buttonStyle(.plain)
.padding(10)
.foregroundColor(.gray)
#endif
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio)
@@ -393,7 +437,7 @@ struct VideoPlayerView: View {
Orientation.lockOrientation(orientationLockMask, andRotateTo: orientation) Orientation.lockOrientation(orientationLockMask, andRotateTo: orientation)
guard lockLandscapeOnRotation else { guard lockOrientationInFullScreen else {
return return
} }
@@ -402,7 +446,8 @@ struct VideoPlayerView: View {
} else { } else {
guard abs(acceleration.z) <= 0.74, guard abs(acceleration.z) <= 0.74,
player.lockedOrientation.isNil, player.lockedOrientation.isNil,
enterFullscreenInLandscape enterFullscreenInLandscape,
!lockOrientationInFullScreen
else { else {
return return
} }
@@ -417,10 +462,11 @@ struct VideoPlayerView: View {
} }
private func handleOrientationDidChangeNotification() { private func handleOrientationDidChangeNotification() {
viewVerticalOffset = viewVerticalOffset == 0 ? 0 : Self.hiddenOffset
let newOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation let newOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
if newOrientation?.isLandscape ?? false, if newOrientation?.isLandscape ?? false,
player.presentingPlayer, player.presentingPlayer,
lockLandscapeOnRotation, lockOrientationInFullScreen,
!player.lockedOrientation.isNil !player.lockedOrientation.isNil
{ {
Orientation.lockOrientation(.landscape, andRotateTo: newOrientation) Orientation.lockOrientation(.landscape, andRotateTo: newOrientation)

View File

@@ -204,7 +204,7 @@ struct SearchView: View {
visibleSections.append(.subscriptions) visibleSections.append(.subscriptions)
} }
if accounts.app.supportsUserPlaylists && preferred.contains(.playlists) { if accounts.app.supportsUserPlaylists && accounts.signedIn && preferred.contains(.playlists) {
visibleSections.append(.playlists) visibleSections.append(.playlists)
} }

View File

@@ -17,9 +17,8 @@ struct PlayerSettings: View {
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer @Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
#if os(iOS) #if os(iOS)
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock @Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
@Default(.lockLandscapeOnRotation) private var lockLandscapeOnRotation @Default(.lockOrientationInFullScreen) private var lockOrientationInFullScreen
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape @Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
@Default(.lockLandscapeWhenEnteringFullscreen) private var lockLandscapeWhenEnteringFullscreen
#endif #endif
@Default(.closePiPOnNavigation) private var closePiPOnNavigation @Default(.closePiPOnNavigation) private var closePiPOnNavigation
@Default(.closePiPOnOpeningPlayer) private var closePiPOnOpeningPlayer @Default(.closePiPOnOpeningPlayer) private var closePiPOnOpeningPlayer
@@ -96,13 +95,12 @@ struct PlayerSettings: View {
} }
#if os(iOS) #if os(iOS)
Section(header: SettingsHeader(text: "Orientation"), footer: orientationFooter) { Section(header: SettingsHeader(text: "Orientation")) {
if idiom == .pad { if idiom == .pad {
enterFullscreenInLandscapeToggle enterFullscreenInLandscapeToggle
} }
honorSystemOrientationLockToggle honorSystemOrientationLockToggle
lockLandscapeOnRotationToggle lockOrientationInFullScreenToggle
lockLandscapeWhenEnteringFullscreenToggle
} }
#endif #endif
} }
@@ -215,18 +213,10 @@ struct PlayerSettings: View {
Toggle("Enter fullscreen in landscape", isOn: $enterFullscreenInLandscape) Toggle("Enter fullscreen in landscape", isOn: $enterFullscreenInLandscape)
} }
private var lockLandscapeOnRotationToggle: some View { private var lockOrientationInFullScreenToggle: some View {
Toggle("Lock landscape on rotation", isOn: $lockLandscapeOnRotation) Toggle("Lock orientation in fullscreen", isOn: $lockOrientationInFullScreen)
.disabled(!enterFullscreenInLandscape) .disabled(!enterFullscreenInLandscape)
} }
private var lockLandscapeWhenEnteringFullscreenToggle: some View {
Toggle("Rotate and lock landscape on entering fullscreen", isOn: $lockLandscapeWhenEnteringFullscreen)
}
private var orientationFooter: some View {
Text("Orientation settings are experimental and do not yet work properly with all devices and iOS versions")
}
#endif #endif
private var closePiPOnNavigationToggle: some View { private var closePiPOnNavigationToggle: some View {

View File

@@ -6,8 +6,8 @@ import SwiftUI
struct VideoCell: View { struct VideoCell: View {
private var video: Video private var video: Video
@Environment(\.inNavigationView) private var inNavigationView
@Environment(\.navigationStyle) private var navigationStyle @Environment(\.navigationStyle) private var navigationStyle
@Environment(\.inChannelView) private var inChannelView
#if os(iOS) #if os(iOS)
@Environment(\.verticalSizeClass) private var verticalSizeClass @Environment(\.verticalSizeClass) private var verticalSizeClass
@@ -46,11 +46,8 @@ struct VideoCell: View {
.buttonStyle(.plain) .buttonStyle(.plain)
.contentShape(RoundedRectangle(cornerRadius: thumbnailRoundingCornerRadius)) .contentShape(RoundedRectangle(cornerRadius: thumbnailRoundingCornerRadius))
.contextMenu { .contextMenu {
VideoContextMenuView( VideoContextMenuView(video: video)
video: video, .environmentObject(accounts)
playerNavigationLinkActive: $player.playerNavigationLinkActive
)
.environmentObject(accounts)
} }
} }
@@ -84,7 +81,8 @@ struct VideoCell: View {
var playAt: CMTime? var playAt: CMTime?
if playNowContinues, if saveHistory,
playNowContinues,
!watch.isNil, !watch.isNil,
!watch!.finished !watch!.finished
{ {
@@ -93,7 +91,7 @@ struct VideoCell: View {
player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture player.avPlayerBackend.startPictureInPictureOnPlay = player.playingInPictureInPicture
player.play(video, at: playAt, inNavigationView: inNavigationView) player.play(video, at: playAt)
} }
} }
@@ -297,6 +295,10 @@ struct VideoCell: View {
private func channelButton(badge: Bool = true) -> some View { private func channelButton(badge: Bool = true) -> some View {
Button { Button {
guard !inChannelView else {
return
}
NavigationModel.openChannel( NavigationModel.openChannel(
video.channel, video.channel,
player: player, player: player,

View File

@@ -65,15 +65,6 @@ struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
.lineLimit(1) .lineLimit(1)
} }
} }
.contextMenu {
Button {
model.closeCurrentItem()
} label: {
Label("Close Video", systemImage: "xmark.circle")
.labelStyle(.automatic)
}
.disabled(model.currentItem.isNil)
}
Spacer() Spacer()
} }
@@ -82,43 +73,41 @@ struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
} }
.padding(.vertical, 20) .padding(.vertical, 20)
ZStack(alignment: .bottom) { HStack {
HStack { Group {
Group { if !model.currentItem.isNil {
if playerControls.isPlaying { Button {
Button(action: { model.closeCurrentItem()
model.pause() model.closePiP()
}) { } label: {
Label("Pause", systemImage: "pause.fill") Label("Close Video", systemImage: "xmark")
}
} else {
Button(action: {
model.play()
}) {
Label("Play", systemImage: "play.fill")
}
} }
} }
.disabled(playerControls.isLoadingVideo || model.currentItem.isNil)
.font(.system(size: 30))
.frame(minWidth: 30)
Button(action: { model.advanceToNextItem() }) { if playerControls.isPlaying {
Label("Next", systemImage: "forward.fill") Button(action: {
.padding(.vertical) model.pause()
.contentShape(Rectangle()) }) {
Label("Pause", systemImage: "pause.fill")
}
} else {
Button(action: {
model.play()
}) {
Label("Play", systemImage: "play.fill")
}
} }
.disabled(model.queue.isEmpty)
} }
.disabled(playerControls.isLoadingVideo || model.currentItem.isNil)
.font(.system(size: 30))
.frame(minWidth: 30)
ProgressView(value: progressViewValue, total: progressViewTotal) Button(action: { model.advanceToNextItem() }) {
.progressViewStyle(.linear) Label("Next", systemImage: "forward.fill")
#if os(iOS) .padding(.vertical)
.frame(maxWidth: 60) .contentShape(Rectangle())
#else }
.offset(y: 6) .disabled(model.queue.isEmpty)
.frame(maxWidth: 70)
#endif
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)

View File

@@ -2,55 +2,89 @@ import Siesta
import SwiftUI import SwiftUI
struct ChannelPlaylistView: View { struct ChannelPlaylistView: View {
var playlist: ChannelPlaylist #if os(iOS)
static let hiddenOffset = max(UIScreen.main.bounds.height, UIScreen.main.bounds.width) + 100
#endif
var playlist: ChannelPlaylist?
@State private var presentingShareSheet = false @State private var presentingShareSheet = false
@State private var shareURL: URL? @State private var shareURL: URL?
#if os(iOS)
@State private var viewVerticalOffset = Self.hiddenOffset
#endif
@StateObject private var store = Store<ChannelPlaylist>() @StateObject private var store = Store<ChannelPlaylist>()
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@Environment(\.inNavigationView) private var inNavigationView @Environment(\.navigationStyle) private var navigationStyle
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<RecentsModel> private var recents
var items: [ContentItem] { private var items: [ContentItem] {
ContentItem.array(of: store.item?.videos ?? []) ContentItem.array(of: store.item?.videos ?? [])
} }
var resource: Resource? { private var presentedPlaylist: ChannelPlaylist? {
accounts.api.channelPlaylist(playlist.id) playlist ?? recents.presentedPlaylist
}
private var resource: Resource? {
guard let playlist = presentedPlaylist else {
return nil
}
let resource = accounts.api.channelPlaylist(playlist.id)
resource?.addObserver(store)
return resource
} }
var body: some View { var body: some View {
#if os(iOS) if navigationStyle == .tab {
if inNavigationView { NavigationView {
content
} else {
BrowserPlayerControls { BrowserPlayerControls {
content content
} }
} }
#else #if os(iOS)
.onChange(of: navigation.presentingPlaylist) { newValue in
if newValue {
store.clear()
viewVerticalOffset = 0
resource?.load()
} else {
viewVerticalOffset = Self.hiddenOffset
}
}
.offset(y: viewVerticalOffset)
.animation(.easeIn(duration: 0.2), value: viewVerticalOffset)
#endif
} else {
BrowserPlayerControls { BrowserPlayerControls {
content content
} }
#endif }
} }
var content: some View { var content: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
#if os(tvOS) #if os(tvOS)
HStack { HStack {
Text(playlist.title) if let playlist = presentedPlaylist {
.font(.title2) Text(playlist.title)
.frame(alignment: .leading) .font(.title2)
.frame(alignment: .leading)
Spacer() Spacer()
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title))) FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
}
playButton playButton
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
@@ -69,7 +103,6 @@ struct ChannelPlaylistView: View {
} }
#endif #endif
.onAppear { .onAppear {
resource?.addObserver(store)
resource?.loadIfNeeded() resource?.loadIfNeeded()
} }
#if os(tvOS) #if os(tvOS)
@@ -77,26 +110,31 @@ struct ChannelPlaylistView: View {
#else #else
.toolbar { .toolbar {
ToolbarItem(placement: .navigation) { ToolbarItem(placement: .navigation) {
ShareButton( if navigationStyle == .tab {
contentItem: contentItem, Button("Done") {
presentingShareSheet: $presentingShareSheet, navigation.presentingPlaylist = false
shareURL: $shareURL }
) }
} }
ToolbarItem(placement: playlistButtonsPlacement) { ToolbarItem(placement: playlistButtonsPlacement) {
HStack { HStack {
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title))) ShareButton(
contentItem: contentItem,
presentingShareSheet: $presentingShareSheet,
shareURL: $shareURL
)
if let playlist = presentedPlaylist {
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
}
playButton playButton
shuffleButton shuffleButton
} }
} }
} }
.navigationTitle(playlist.title) .navigationTitle(presentedPlaylist?.title ?? "")
#if os(iOS)
.navigationBarHidden(player.playerNavigationLinkActive)
#endif
#endif #endif
} }
@@ -110,7 +148,7 @@ struct ChannelPlaylistView: View {
private var playButton: some View { private var playButton: some View {
Button { Button {
player.play(videos, inNavigationView: inNavigationView) player.play(videos)
} label: { } label: {
Label("Play All", systemImage: "play") Label("Play All", systemImage: "play")
} }
@@ -118,7 +156,7 @@ struct ChannelPlaylistView: View {
private var shuffleButton: some View { private var shuffleButton: some View {
Button { Button {
player.play(videos, shuffling: true, inNavigationView: inNavigationView) player.play(videos, shuffling: true)
} label: { } label: {
Label("Shuffle", systemImage: "shuffle") Label("Shuffle", systemImage: "shuffle")
} }

View File

@@ -2,46 +2,69 @@ import Siesta
import SwiftUI import SwiftUI
struct ChannelVideosView: View { struct ChannelVideosView: View {
let channel: Channel #if os(iOS)
static let hiddenOffset = max(UIScreen.main.bounds.height, UIScreen.main.bounds.width) + 100
#endif
var channel: Channel?
@State private var presentingShareSheet = false @State private var presentingShareSheet = false
@State private var shareURL: URL? @State private var shareURL: URL?
@State private var subscriptionToggleButtonDisabled = false @State private var subscriptionToggleButtonDisabled = false
#if os(iOS)
@State private var viewVerticalOffset = Self.hiddenOffset
#endif
@StateObject private var store = Store<Channel>() @StateObject private var store = Store<Channel>()
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@Environment(\.navigationStyle) private var navigationStyle
#if os(iOS) #if os(iOS)
@Environment(\.inNavigationView) private var inNavigationView
@Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.horizontalSizeClass) private var horizontalSizeClass
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
#endif #endif
@EnvironmentObject<AccountsModel> private var accounts @EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<NavigationModel> private var navigation @EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SubscriptionsModel> private var subscriptions @EnvironmentObject<SubscriptionsModel> private var subscriptions
@Namespace private var focusNamespace @Namespace private var focusNamespace
var presentedChannel: Channel? {
channel ?? recents.presentedChannel
}
var videos: [ContentItem] { var videos: [ContentItem] {
ContentItem.array(of: store.item?.videos ?? []) ContentItem.array(of: store.item?.videos ?? [])
} }
var body: some View { var body: some View {
#if os(iOS) if navigationStyle == .tab {
if inNavigationView { NavigationView {
content
} else {
BrowserPlayerControls { BrowserPlayerControls {
content content
} }
} }
#else #if os(iOS)
.onChange(of: navigation.presentingChannel) { newValue in
if newValue {
store.clear()
viewVerticalOffset = 0
resource?.load()
} else {
viewVerticalOffset = Self.hiddenOffset
}
}
.offset(y: viewVerticalOffset)
.animation(.easeIn(duration: 0.2), value: viewVerticalOffset)
#endif
} else {
BrowserPlayerControls { BrowserPlayerControls {
content content
} }
#endif }
} }
var content: some View { var content: some View {
@@ -54,8 +77,10 @@ struct ChannelVideosView: View {
Spacer() Spacer()
FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name))) if let channel = presentedChannel {
.labelStyle(.iconOnly) FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name)))
.labelStyle(.iconOnly)
}
if let subscribers = store.item?.subscriptionsString { if let subscribers = store.item?.subscriptionsString {
Text("**\(subscribers)** subscribers") Text("**\(subscribers)** subscribers")
@@ -77,27 +102,35 @@ struct ChannelVideosView: View {
#if !os(tvOS) #if !os(tvOS)
.toolbar { .toolbar {
ToolbarItem(placement: .navigation) { ToolbarItem(placement: .navigation) {
ShareButton( if navigationStyle == .tab {
contentItem: contentItem, Button("Done") {
presentingShareSheet: $presentingShareSheet, navigation.presentingChannel = false
shareURL: $shareURL }
) }
} }
ToolbarItem { ToolbarItem {
HStack { HStack {
HStack(spacing: 3) { HStack(spacing: 3) {
Text("\(store.item?.subscriptionsString ?? "loading")") Text("\(store.item?.subscriptionsString ?? "")")
.fontWeight(.bold) .fontWeight(.bold)
Text(" subscribers") Text(" subscribers")
.allowsTightening(true)
.foregroundColor(.secondary)
.opacity(store.item?.subscriptionsString != nil ? 1 : 0)
} }
.allowsTightening(true)
.foregroundColor(.secondary) ShareButton(
.opacity(store.item?.subscriptionsString != nil ? 1 : 0) contentItem: contentItem,
presentingShareSheet: $presentingShareSheet,
shareURL: $shareURL
)
subscriptionToggleButton subscriptionToggleButton
FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name))) if let channel = presentedChannel {
FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name)))
}
} }
} }
} }
@@ -110,15 +143,11 @@ struct ChannelVideosView: View {
} }
#endif #endif
.onAppear { .onAppear {
if store.item.isNil { resource?.loadIfNeeded()
resource.addObserver(store)
resource.load()
}
} }
#if os(iOS) #if !os(tvOS)
.navigationBarHidden(player.playerNavigationLinkActive)
#endif
.navigationTitle(navigationTitle) .navigationTitle(navigationTitle)
#endif
return Group { return Group {
if #available(macOS 12.0, *) { if #available(macOS 12.0, *) {
@@ -135,44 +164,50 @@ struct ChannelVideosView: View {
} }
} }
private var resource: Resource { private var resource: Resource? {
guard let channel = presentedChannel else {
return nil
}
let resource = accounts.api.channel(channel.id) let resource = accounts.api.channel(channel.id)
resource.addObserver(store) resource.addObserver(store)
return resource return resource
} }
private var subscriptionToggleButton: some View { @ViewBuilder private var subscriptionToggleButton: some View {
Group { if let channel = presentedChannel {
if accounts.app.supportsSubscriptions && accounts.signedIn { Group {
if subscriptions.isSubscribing(channel.id) { if accounts.app.supportsSubscriptions && accounts.signedIn {
Button("Unsubscribe") { if subscriptions.isSubscribing(channel.id) {
subscriptionToggleButtonDisabled = true Button("Unsubscribe") {
subscriptionToggleButtonDisabled = true
subscriptions.unsubscribe(channel.id) { subscriptions.unsubscribe(channel.id) {
subscriptionToggleButtonDisabled = false subscriptionToggleButtonDisabled = false
}
} }
} } else {
} else { Button("Subscribe") {
Button("Subscribe") { subscriptionToggleButtonDisabled = true
subscriptionToggleButtonDisabled = true
subscriptions.subscribe(channel.id) { subscriptions.subscribe(channel.id) {
subscriptionToggleButtonDisabled = false subscriptionToggleButtonDisabled = false
navigation.sidebarSectionChanged.toggle() navigation.sidebarSectionChanged.toggle()
}
} }
} }
} }
} }
.disabled(subscriptionToggleButtonDisabled)
} }
.disabled(subscriptionToggleButtonDisabled)
} }
private var contentItem: ContentItem { private var contentItem: ContentItem {
ContentItem(channel: channel) ContentItem(channel: presentedChannel)
} }
private var navigationTitle: String { private var navigationTitle: String {
store.item?.name ?? channel.name presentedChannel?.name ?? store.item?.name ?? "No channel"
} }
} }

View File

@@ -4,7 +4,6 @@ import SwiftUI
struct PlaylistVideosView: View { struct PlaylistVideosView: View {
let playlist: Playlist let playlist: Playlist
@Environment(\.inNavigationView) private var inNavigationView
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<PlaylistsModel> private var model @EnvironmentObject<PlaylistsModel> private var model
@@ -66,13 +65,13 @@ struct PlaylistVideosView: View {
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title))) FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
Button { Button {
player.play(videos, inNavigationView: inNavigationView) player.play(videos)
} label: { } label: {
Label("Play All", systemImage: "play") Label("Play All", systemImage: "play")
} }
Button { Button {
player.play(videos, shuffling: true, inNavigationView: inNavigationView) player.play(videos, shuffling: true)
} label: { } label: {
Label("Shuffle", systemImage: "shuffle") Label("Shuffle", systemImage: "shuffle")
} }

View File

@@ -1,13 +1,11 @@
import CoreData import CoreData
import CoreMedia
import Defaults import Defaults
import SwiftUI import SwiftUI
struct VideoContextMenuView: View { struct VideoContextMenuView: View {
let video: Video let video: Video
@Binding var playerNavigationLinkActive: Bool
@Environment(\.inNavigationView) private var inNavigationView
@Environment(\.inChannelView) private var inChannelView @Environment(\.inChannelView) private var inChannelView
@Environment(\.inChannelPlaylistView) private var inChannelPlaylistView @Environment(\.inChannelPlaylistView) private var inChannelPlaylistView
@Environment(\.navigationStyle) private var navigationStyle @Environment(\.navigationStyle) private var navigationStyle
@@ -26,9 +24,8 @@ struct VideoContextMenuView: View {
private var viewContext: NSManagedObjectContext = PersistenceController.shared.container.viewContext private var viewContext: NSManagedObjectContext = PersistenceController.shared.container.viewContext
init(video: Video, playerNavigationLinkActive: Binding<Bool>) { init(video: Video) {
self.video = video self.video = video
_playerNavigationLinkActive = playerNavigationLinkActive
_watchRequest = video.watchFetchRequest _watchRequest = video.watchFetchRequest
} }
@@ -57,6 +54,9 @@ struct VideoContextMenuView: View {
Section { Section {
playNowButton playNowButton
#if os(iOS)
playNowInPictureInPictureButton
#endif
} }
Section { Section {
@@ -111,7 +111,7 @@ struct VideoContextMenuView: View {
private var continueButton: some View { private var continueButton: some View {
Button { Button {
player.play(video, at: .secondsInDefaultTimescale(watch!.stoppedAt), inNavigationView: inNavigationView) player.play(video, at: .secondsInDefaultTimescale(watch!.stoppedAt))
} label: { } label: {
Label("Continue from \(watch!.stoppedAt.formattedAsPlaybackTime() ?? "where I left off")", systemImage: "playpause") Label("Continue from \(watch!.stoppedAt.formattedAsPlaybackTime() ?? "where I left off")", systemImage: "playpause")
} }
@@ -131,12 +131,24 @@ struct VideoContextMenuView: View {
private var playNowButton: some View { private var playNowButton: some View {
Button { Button {
player.play(video, inNavigationView: inNavigationView) player.play(video)
} label: { } label: {
Label("Play Now", systemImage: "play") Label("Play Now", systemImage: "play")
} }
} }
private var playNowInPictureInPictureButton: some View {
Button {
player.controls.startPiP(startImmediately: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
player.play(video, at: watch?.timeToRestart, showingPlayer: false)
}
} label: {
Label("Play in PiP", systemImage: "pip")
}
}
private var playNextButton: some View { private var playNextButton: some View {
Button { Button {
player.playNext(video) player.playNext(video)

View File

@@ -1407,6 +1407,7 @@
37152EE926EFEB95004FB96D /* LazyView.swift */, 37152EE926EFEB95004FB96D /* LazyView.swift */,
37030FF627B0347C00ECDDAA /* MPVPlayerView.swift */, 37030FF627B0347C00ECDDAA /* MPVPlayerView.swift */,
37E70926271CDDAE00D34DDE /* OpenSettingsButton.swift */, 37E70926271CDDAE00D34DDE /* OpenSettingsButton.swift */,
37FEF11227EFD8580033912F /* PlaceholderCell.swift */,
3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */, 3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */,
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */, 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */,
37AAF27D26737323007FC770 /* PopularView.swift */, 37AAF27D26737323007FC770 /* PopularView.swift */,
@@ -1415,7 +1416,6 @@
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */, 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */,
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */, 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */,
37E70922271CD43000D34DDE /* WelcomeScreen.swift */, 37E70922271CD43000D34DDE /* WelcomeScreen.swift */,
37FEF11227EFD8580033912F /* PlaceholderCell.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -3103,7 +3103,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements"; CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -3116,7 +3116,7 @@
"@executable_path/../../../../Frameworks", "@executable_path/../../../../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -3137,7 +3137,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements"; CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -3150,7 +3150,7 @@
"@executable_path/../../../../Frameworks", "@executable_path/../../../../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -3169,7 +3169,7 @@
buildSettings = { buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist"; INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -3181,7 +3181,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -3201,7 +3201,7 @@
buildSettings = { buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist"; INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -3213,7 +3213,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -3365,7 +3365,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_CXX_LANGUAGE_STANDARD = "c++14"; CLANG_CXX_LANGUAGE_STANDARD = "c++14";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = ( GCC_PREPROCESSOR_DEFINITIONS = (
@@ -3389,9 +3389,9 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Vendor/mpv/iOS/lib", "$(PROJECT_DIR)/Vendor/mpv/iOS/lib",
); );
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4;
OTHER_LDFLAGS = "-lstdc++"; OTHER_LDFLAGS = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -3407,7 +3407,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1"; GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
@@ -3427,9 +3427,9 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Vendor/mpv/iOS/lib", "$(PROJECT_DIR)/Vendor/mpv/iOS/lib",
); );
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4;
OTHER_LDFLAGS = "-lstdc++"; OTHER_LDFLAGS = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -3449,7 +3449,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -3468,7 +3468,7 @@
"$(PROJECT_DIR)/Vendor/mpv/macOS/lib", "$(PROJECT_DIR)/Vendor/mpv/macOS/lib",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = macosx; SDKROOT = macosx;
@@ -3487,7 +3487,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES; ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -3506,7 +3506,7 @@
"$(PROJECT_DIR)/Vendor/mpv/macOS/lib", "$(PROJECT_DIR)/Vendor/mpv/macOS/lib",
); );
MACOSX_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 11.0;
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = macosx; SDKROOT = macosx;
@@ -3530,7 +3530,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-iOS"; PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -3555,7 +3555,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-iOS"; PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -3582,7 +3582,7 @@
"@loader_path/../Frameworks", "@loader_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0; MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-macOS"; PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-macOS";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx; SDKROOT = macosx;
@@ -3607,7 +3607,7 @@
"@loader_path/../Frameworks", "@loader_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 12.0; MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-macOS"; PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-macOS";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx; SDKROOT = macosx;
@@ -3623,7 +3623,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -3643,8 +3643,8 @@
"$(PROJECT_DIR)/Vendor/mpv", "$(PROJECT_DIR)/Vendor/mpv",
"$(PROJECT_DIR)/Vendor/mpv/tvOS", "$(PROJECT_DIR)/Vendor/mpv/tvOS",
); );
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = appletvos; SDKROOT = appletvos;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -3661,7 +3661,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -3681,8 +3681,8 @@
"$(PROJECT_DIR)/Vendor/mpv", "$(PROJECT_DIR)/Vendor/mpv",
"$(PROJECT_DIR)/Vendor/mpv/tvOS", "$(PROJECT_DIR)/Vendor/mpv/tvOS",
); );
MARKETING_VERSION = "1.4-alpha.4"; MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
PRODUCT_NAME = Yattee; PRODUCT_NAME = Yattee;
SDKROOT = appletvos; SDKROOT = appletvos;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -3707,7 +3707,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.YatteeUITests; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.YatteeUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos; SDKROOT = appletvos;
@@ -3732,7 +3732,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.4;
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.YatteeUITests; PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.YatteeUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos; SDKROOT = appletvos;

View File

@@ -21,5 +21,7 @@
<array> <array>
<string>audio</string> <string>audio</string>
</array> </array>
<key>NSCameraUsageDescription</key>
<string>Need camera access to take pictures</string>
</dict> </dict>
</plist> </plist>