mirror of
https://github.com/yattee/yattee.git
synced 2025-12-16 21:18:16 +00:00
Compare commits
81 Commits
v1.4-alpha
...
v1.4-alpha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d344ab15dc | ||
|
|
888813f095 | ||
|
|
a0a6020ee6 | ||
|
|
e3e68cd158 | ||
|
|
42e56d6e4b | ||
|
|
f979af7f01 | ||
|
|
124a48812a | ||
|
|
44e6c28fd4 | ||
|
|
4f6e0e2a3d | ||
|
|
ccc1cc89ad | ||
|
|
5d0eb2478c | ||
|
|
c28429ec7f | ||
|
|
6c3a7882e1 | ||
|
|
76286e8a45 | ||
|
|
c5128f6227 | ||
|
|
4bf171637b | ||
|
|
249adab427 | ||
|
|
a27ebcce27 | ||
|
|
b5f3a1bd09 | ||
|
|
b306819af9 | ||
|
|
aa29688b4c | ||
|
|
371e6a275b | ||
|
|
0b7e9f8c47 | ||
|
|
680fe915e1 | ||
|
|
1e7ebe4e68 | ||
|
|
1a230d36f7 | ||
|
|
60e1cef84f | ||
|
|
1150a01496 | ||
|
|
e5c9e11b17 | ||
|
|
fb3d64bcc7 | ||
|
|
dec670b7b7 | ||
|
|
ea4dc2358b | ||
|
|
fca7b7a1a7 | ||
|
|
ea0db9533a | ||
|
|
db85be76f7 | ||
|
|
5cb2e1b8f0 | ||
|
|
387f29e395 | ||
|
|
9ae8c85f4e | ||
|
|
c94a35d2c6 | ||
|
|
aacbd7889c | ||
|
|
d8020d06dd | ||
|
|
c556f0e21d | ||
|
|
ff1f62b6ad | ||
|
|
7cb4e0dccf | ||
|
|
3229e3151f | ||
|
|
53103c9ecf | ||
|
|
ac53deb39b | ||
|
|
cab6f486ba | ||
|
|
40e16fcc7e | ||
|
|
9c687f8704 | ||
|
|
340ceff131 | ||
|
|
76a116659e | ||
|
|
7c009080a5 | ||
|
|
61d7c53a58 | ||
|
|
08e65aff0f | ||
|
|
722616e3d0 | ||
|
|
0826f01150 | ||
|
|
fcb9ec1f6a | ||
|
|
5bb1589159 | ||
|
|
60e5cc5d75 | ||
|
|
bc410ad6ba | ||
|
|
35c358de6b | ||
|
|
23955699dc | ||
|
|
742103e4c2 | ||
|
|
6173f4610b | ||
|
|
1217acf264 | ||
|
|
0b8d887cef | ||
|
|
b760f172d0 | ||
|
|
bb8fc78760 | ||
|
|
d2317f725f | ||
|
|
693c3364b0 | ||
|
|
18bfc1abc9 | ||
|
|
717830ee99 | ||
|
|
2b001ec96c | ||
|
|
ebeb5bc520 | ||
|
|
7a7e265ba1 | ||
|
|
c086112a49 | ||
|
|
0802fe0029 | ||
|
|
40813c2859 | ||
|
|
b1238869a6 | ||
|
|
453a5fa71b |
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 = []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: "")
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user