mirror of
https://github.com/yattee/yattee.git
synced 2025-12-14 12:08:15 +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
|
||||
}
|
||||
|
||||
player.presentingPlayer = false
|
||||
navigation.presentingChannel = false
|
||||
|
||||
let recent = RecentItem(from: channel)
|
||||
#if os(macOS)
|
||||
Windows.main.open()
|
||||
@@ -113,6 +116,8 @@ final class NavigationModel: ObservableObject {
|
||||
navigationStyle: NavigationStyle,
|
||||
delay: Bool = false
|
||||
) {
|
||||
navigation.presentingPlaylist = false
|
||||
|
||||
let recent = RecentItem(from: playlist)
|
||||
#if os(macOS)
|
||||
Windows.main.open()
|
||||
|
||||
@@ -169,8 +169,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
#else
|
||||
func closePiP(wasPlaying: Bool) {
|
||||
controller?.playerView.player = nil
|
||||
controller?.playerView.player = avPlayer
|
||||
model.pipController?.stopPictureInPicture()
|
||||
|
||||
guard wasPlaying else {
|
||||
return
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AVFAudio
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import Foundation
|
||||
import Logging
|
||||
import SwiftUI
|
||||
@@ -241,9 +242,21 @@ final class MPVBackend: PlayerBackend {
|
||||
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() {}
|
||||
|
||||
|
||||
@@ -134,11 +134,11 @@ final class MPVClient: ObservableObject {
|
||||
}
|
||||
|
||||
var currentTime: CMTime {
|
||||
CMTime.secondsInDefaultTimescale(getDouble("time-pos"))
|
||||
CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("time-pos"))
|
||||
}
|
||||
|
||||
var duration: CMTime {
|
||||
CMTime.secondsInDefaultTimescale(getDouble("duration"))
|
||||
CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("duration"))
|
||||
}
|
||||
|
||||
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
|
||||
@@ -19,16 +19,22 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStopPictureInPicture(_: AVPictureInPictureController) {
|
||||
if player?.avPlayerBackend.switchToMPVOnPipClose ?? false {
|
||||
DispatchQueue.main.async { [weak player] in
|
||||
player?.avPlayerBackend.switchToMPVOnPipClose = false
|
||||
player?.saveTime { [weak player] in
|
||||
player?.changeActiveBackend(from: .appleAVPlayer, to: .mpv)
|
||||
guard let player = player else {
|
||||
return
|
||||
}
|
||||
|
||||
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) {}
|
||||
@@ -37,6 +43,12 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
|
||||
_: AVPictureInPictureController,
|
||||
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() {
|
||||
if presentingControls {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.player.backend.startControlsUpdates()
|
||||
self?.player?.backend.startControlsUpdates()
|
||||
self?.resetTimer()
|
||||
}
|
||||
} else {
|
||||
@@ -94,9 +94,11 @@ final class PlayerControlsModel: ObservableObject {
|
||||
}
|
||||
|
||||
func resetTimer() {
|
||||
if !presentingControls {
|
||||
show()
|
||||
}
|
||||
#if os(tvOS)
|
||||
if !presentingControls {
|
||||
show()
|
||||
}
|
||||
#endif
|
||||
|
||||
removeTimer()
|
||||
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() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
|
||||
@@ -57,8 +57,6 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
@Published var preservedTime: CMTime?
|
||||
|
||||
@Published var playerNavigationLinkActive = false { didSet { handleNavigationViewPlayerPresentationChange() } }
|
||||
|
||||
@Published var sponsorBlock = SponsorBlockAPI()
|
||||
@Published var segmentRestorationTime: CMTime?
|
||||
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
|
||||
@@ -120,23 +118,24 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
func show() {
|
||||
guard !presentingPlayer else {
|
||||
#if os(macOS)
|
||||
#if os(macOS)
|
||||
if presentingPlayer {
|
||||
Windows.player.focus()
|
||||
#endif
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
#endif
|
||||
|
||||
presentingPlayer = true
|
||||
|
||||
#if os(macOS)
|
||||
Windows.player.open()
|
||||
Windows.player.focus()
|
||||
#endif
|
||||
presentingPlayer = true
|
||||
}
|
||||
|
||||
func hide() {
|
||||
controls.playingFullscreen = false
|
||||
presentingPlayer = false
|
||||
playerNavigationLinkActive = false
|
||||
|
||||
#if os(iOS)
|
||||
if Defaults[.lockPortraitWhenBrowsing] {
|
||||
@@ -206,16 +205,25 @@ final class PlayerModel: ObservableObject {
|
||||
backend.pause()
|
||||
}
|
||||
|
||||
func play(_ video: Video, at time: CMTime? = nil, inNavigationView: Bool = false) {
|
||||
playNow(video, at: time)
|
||||
func play(_ video: Video, at time: CMTime? = nil, showingPlayer: Bool = true) {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
if inNavigationView {
|
||||
playerNavigationLinkActive = true
|
||||
} else {
|
||||
if showingPlayer {
|
||||
show()
|
||||
}
|
||||
}
|
||||
@@ -262,6 +270,7 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
func saveTime(completionHandler: @escaping () -> Void = {}) {
|
||||
guard let currentTime = backend.currentTime, currentTime.seconds > 0 else {
|
||||
completionHandler()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -272,8 +281,12 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
func upgradeToStream(_ stream: Stream, force: Bool = false) {
|
||||
guard let video = currentVideo else {
|
||||
return
|
||||
}
|
||||
|
||||
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() {
|
||||
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()
|
||||
|
||||
#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) {
|
||||
guard activeBackend != to else {
|
||||
return
|
||||
}
|
||||
|
||||
Defaults[.activeBackend] = to
|
||||
self.activeBackend = to
|
||||
|
||||
@@ -361,7 +378,7 @@ final class PlayerModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
if !backend.canPlay(stream) {
|
||||
if !backend.canPlay(stream) || (to == .mpv && !stream.hlsURL.isNil) {
|
||||
guard let preferredStream = preferredStream(availableStreams) else {
|
||||
return
|
||||
}
|
||||
@@ -371,7 +388,11 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
if controls.playingFullscreen {
|
||||
toggleFullscreen(true)
|
||||
}
|
||||
|
||||
backend.exitFullScreen()
|
||||
}
|
||||
#endif
|
||||
@@ -524,4 +549,8 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func setNeedsDrawing(_ needsDrawing: Bool) {
|
||||
backends.forEach { $0.setNeedsDrawing(needsDrawing) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ extension PlayerModel {
|
||||
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
|
||||
|
||||
guard let first = videosToPlay.first else {
|
||||
@@ -27,11 +27,7 @@ extension PlayerModel {
|
||||
}
|
||||
}
|
||||
|
||||
if inNavigationView {
|
||||
playerNavigationLinkActive = true
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
show()
|
||||
}
|
||||
|
||||
func playNext(_ video: Video) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import Foundation
|
||||
final class RecentsModel: ObservableObject {
|
||||
@Default(.recentlyOpened) var items
|
||||
@Default(.saveRecents) var saveRecents
|
||||
|
||||
func clear() {
|
||||
items = []
|
||||
}
|
||||
|
||||
@@ -22,4 +22,8 @@ final class Store<Data>: ResourceObserver, ObservableObject {
|
||||
func replace(_ items: Data) {
|
||||
all = items
|
||||
}
|
||||
|
||||
func clear() {
|
||||
all = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import CoreData
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import Foundation
|
||||
|
||||
@objc(Watch)
|
||||
final class Watch: NSManagedObject, Identifiable {
|
||||
@Default(.watchedThreshold) private var watchedThreshold
|
||||
@Default(.saveHistory) private var saveHistory
|
||||
}
|
||||
|
||||
extension Watch {
|
||||
@@ -45,4 +47,15 @@ extension Watch {
|
||||
formatter.unitsStyle = .full
|
||||
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">
|
||||
<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">
|
||||
<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>
|
||||
@@ -19,17 +22,16 @@
|
||||
* Fullscreen playback, Picture in Picture and AirPlay support
|
||||
* Stream quality selection
|
||||
|
||||
### Features in alpha testing
|
||||
### Features in development
|
||||
* 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
|
||||
|| Invidious | Piped |
|
||||
| - | - | - |
|
||||
| User Accounts | ✅ | ✅ |
|
||||
| Subscriptions | ✅ | ✅ |
|
||||
| Popular | ✅ | 🔴 |
|
||||
| User Playlists | ✅ | ✅ |
|
||||
| Trending | ✅ | ✅ |
|
||||
| Channels | ✅ | ✅ |
|
||||
@@ -37,13 +39,15 @@ You can leave your feedback in [discussion on v1.4 release](https://github.com/y
|
||||
| Search | ✅ | ✅ |
|
||||
| Search Suggestions | ✅ | ✅ |
|
||||
| Search Filters | ✅ | 🔴 |
|
||||
| Popular | ✅ | 🔴 |
|
||||
| Subtitles | 🔴 | ✅ |
|
||||
| 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.
|
||||
|
||||
## 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)
|
||||
* [Screenshots Gallery](https://github.com/yattee/yattee/wiki/Screenshots-Gallery)
|
||||
* [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
|
||||
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.
|
||||
## License and Liability
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -26,12 +26,6 @@ extension Defaults.Keys {
|
||||
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
|
||||
|
||||
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"))
|
||||
])
|
||||
|
||||
@@ -89,8 +83,7 @@ extension Defaults.Keys {
|
||||
#if os(iOS)
|
||||
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
|
||||
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let lockLandscapeOnRotation = Key<Bool>("lockLandscapeOnRotation", default: false)
|
||||
static let lockLandscapeWhenEnteringFullscreen = Key<Bool>("lockLandscapeWhenEnteringFullscreen", default: false)
|
||||
static let lockOrientationInFullScreen = Key<Bool>("lockOrientationInFullScreen", default: false)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
private struct InNavigationViewKey: EnvironmentKey {
|
||||
static let defaultValue = false
|
||||
}
|
||||
|
||||
private struct InChannelViewKey: EnvironmentKey {
|
||||
static let defaultValue = false
|
||||
}
|
||||
@@ -40,11 +36,6 @@ private struct ScrollViewBottomPaddingKey: EnvironmentKey {
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var inNavigationView: Bool {
|
||||
get { self[InNavigationViewKey.self] }
|
||||
set { self[InNavigationViewKey.self] = newValue }
|
||||
}
|
||||
|
||||
var inChannelView: Bool {
|
||||
get { self[InChannelViewKey.self] }
|
||||
set { self[InChannelViewKey.self] = newValue }
|
||||
|
||||
@@ -63,24 +63,6 @@ struct AppSidebarNavigation: View {
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
|
||||
@@ -43,51 +43,9 @@ struct AppTabNavigation: View {
|
||||
searchNavigationView
|
||||
}
|
||||
.id(accounts.current?.id ?? "")
|
||||
.overlay(playlistView)
|
||||
.overlay(channelView)
|
||||
.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 {
|
||||
@@ -172,15 +130,6 @@ struct AppTabNavigation: View {
|
||||
.tag(TabSelection.search)
|
||||
}
|
||||
|
||||
private var playerNavigationLink: some View {
|
||||
NavigationLink(isActive: $player.playerNavigationLinkActive, destination: {
|
||||
videoPlayer
|
||||
.environment(\.inNavigationView, true)
|
||||
}) {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
private var videoPlayer: some View {
|
||||
VideoPlayerView()
|
||||
.environmentObject(accounts)
|
||||
@@ -210,4 +159,26 @@ struct AppTabNavigation: View {
|
||||
}
|
||||
#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
|
||||
#endif
|
||||
|
||||
let persistenceController = PersistenceController.shared
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
@@ -57,50 +59,54 @@ struct ContentView: View {
|
||||
.environmentObject(subscriptions)
|
||||
.environmentObject(thumbnailsModel)
|
||||
|
||||
// iOS 14 has problem with multiple sheets in one view
|
||||
// but it's ok when it's in background
|
||||
.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)
|
||||
}
|
||||
)
|
||||
#if os(iOS)
|
||||
.overlay(videoPlayer)
|
||||
#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()
|
||||
|
||||
// iOS 14 has problem with multiple sheets in one view
|
||||
// but it's ok when it's in background
|
||||
.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
|
||||
.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() {
|
||||
@@ -222,6 +228,21 @@ struct ContentView: View {
|
||||
|
||||
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 {
|
||||
|
||||
@@ -141,7 +141,7 @@ extension AppleAVPlayerViewController: AVPlayerViewControllerDelegate {
|
||||
willBeginFullScreenPresentationWithAnimationCoordinator context: UIViewControllerTransitionCoordinator
|
||||
) {
|
||||
#if os(iOS)
|
||||
if !context.isCancelled, Defaults[.lockLandscapeWhenEnteringFullscreen] {
|
||||
if !context.isCancelled, Defaults[.lockOrientationInFullScreen] {
|
||||
Orientation.lockOrientation(.landscape, andRotateTo: UIDevice.current.orientation.isLandscape ? nil : .landscapeRight)
|
||||
}
|
||||
#endif
|
||||
@@ -178,11 +178,8 @@ extension AppleAVPlayerViewController: AVPlayerViewControllerDelegate {
|
||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
|
||||
) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
if self.navigationModel.presentingChannel {
|
||||
self.playerModel.playerNavigationLinkActive = true
|
||||
} else {
|
||||
self.playerModel.show()
|
||||
}
|
||||
self.playerModel.show()
|
||||
self.playerModel.setNeedsDrawing(true)
|
||||
|
||||
#if os(tvOS)
|
||||
if self.playerModel.playingInPictureInPicture {
|
||||
@@ -198,7 +195,6 @@ extension AppleAVPlayerViewController: AVPlayerViewControllerDelegate {
|
||||
|
||||
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {
|
||||
playerModel.playingInPictureInPicture = true
|
||||
playerModel.playerNavigationLinkActive = false
|
||||
}
|
||||
|
||||
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {
|
||||
|
||||
@@ -53,18 +53,24 @@ struct PlayerControls: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
timeline
|
||||
.offset(y: 10)
|
||||
.zIndex(1)
|
||||
Group {
|
||||
timeline
|
||||
.offset(y: 10)
|
||||
.zIndex(1)
|
||||
|
||||
bottomBar
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
#if os(macOS)
|
||||
.background(VisualEffectBlur(material: .hudWindow))
|
||||
#elseif os(iOS)
|
||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
||||
#endif
|
||||
.mask(RoundedRectangle(cornerRadius: 3))
|
||||
bottomBar
|
||||
#if os(macOS)
|
||||
.background(VisualEffectBlur(material: .hudWindow))
|
||||
#elseif os(iOS)
|
||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial))
|
||||
#endif
|
||||
.mask(RoundedRectangle(cornerRadius: 3))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
.opacity(model.presentingControls ? 1 : 0)
|
||||
@@ -104,9 +110,6 @@ struct PlayerControls: View {
|
||||
|
||||
var statusBar: some View {
|
||||
HStack(spacing: 4) {
|
||||
#if os(iOS)
|
||||
hidePlayerButton
|
||||
#endif
|
||||
Text(playbackStatus)
|
||||
|
||||
Text("•")
|
||||
@@ -129,11 +132,10 @@ struct PlayerControls: View {
|
||||
}
|
||||
|
||||
private var hidePlayerButton: some View {
|
||||
Button {
|
||||
button("Hide", systemImage: "chevron.down") {
|
||||
player.hide()
|
||||
} label: {
|
||||
Image(systemName: "chevron.down.circle.fill")
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
@@ -170,14 +172,20 @@ struct PlayerControls: View {
|
||||
}
|
||||
|
||||
var buttonsBar: some View {
|
||||
HStack {
|
||||
HStack(spacing: 20) {
|
||||
#if !os(tvOS)
|
||||
#if os(iOS)
|
||||
hidePlayerButton
|
||||
#endif
|
||||
|
||||
fullscreenButton
|
||||
#if os(iOS)
|
||||
pipButton
|
||||
#endif
|
||||
rateButton
|
||||
|
||||
closeVideoButton
|
||||
|
||||
Spacer()
|
||||
#endif
|
||||
// button("Music Mode", systemImage: "music.note")
|
||||
@@ -225,6 +233,23 @@ struct PlayerControls: View {
|
||||
#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 {
|
||||
Picker("Rate", selection: rateBinding) {
|
||||
ForEach(PlayerModel.availableRates, id: \.self) { rate in
|
||||
@@ -240,28 +265,17 @@ struct PlayerControls: View {
|
||||
|
||||
private var pipButton: some View {
|
||||
button("PiP", systemImage: "pip") {
|
||||
if player.activeBackend == .mpv {
|
||||
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()
|
||||
}
|
||||
model.startPiP()
|
||||
}
|
||||
}
|
||||
|
||||
var mediumButtonsBar: some View {
|
||||
HStack {
|
||||
#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))
|
||||
}
|
||||
|
||||
@@ -279,8 +293,7 @@ struct PlayerControls: View {
|
||||
button(
|
||||
model.isPlaying ? "Pause" : "Play",
|
||||
systemImage: model.isPlaying ? "pause.fill" : "play.fill",
|
||||
size: 50,
|
||||
cornerRadius: 10
|
||||
size: 30, cornerRadius: 5
|
||||
) {
|
||||
player.backend.togglePlay()
|
||||
}
|
||||
@@ -295,7 +308,7 @@ struct PlayerControls: View {
|
||||
Spacer()
|
||||
|
||||
#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))
|
||||
}
|
||||
#if os(tvOS)
|
||||
@@ -304,16 +317,30 @@ struct PlayerControls: View {
|
||||
.keyboardShortcut("l", modifiers: [])
|
||||
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
|
||||
#endif
|
||||
|
||||
advanceToNextItemButton
|
||||
.padding(.leading, 15)
|
||||
#endif
|
||||
}
|
||||
.font(.system(size: 30))
|
||||
.font(.system(size: 20))
|
||||
.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 {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Text(model.playbackTime)
|
||||
}
|
||||
.font(.system(size: 15))
|
||||
@@ -361,6 +388,25 @@ struct PlayerControls: View {
|
||||
|
||||
struct PlayerControls_Previews: PreviewProvider {
|
||||
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
|
||||
|
||||
private var start: Double = 0.0
|
||||
private var height = 10.0
|
||||
private var height = 8.0
|
||||
|
||||
var cornerRadius: Double
|
||||
var thumbTooltipWidth: Double = 100
|
||||
@@ -26,26 +26,25 @@ struct TimelineView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.foregroundColor(.blue)
|
||||
.frame(maxHeight: height)
|
||||
Group {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.foregroundColor(.blue)
|
||||
.frame(maxHeight: height)
|
||||
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(
|
||||
Color.green
|
||||
)
|
||||
.frame(maxHeight: height)
|
||||
.frame(width: current * oneUnitWidth)
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(Color.green)
|
||||
.frame(maxHeight: height)
|
||||
.frame(width: current * oneUnitWidth)
|
||||
|
||||
segmentsLayers
|
||||
segmentsLayers
|
||||
}
|
||||
|
||||
Circle()
|
||||
.strokeBorder(.gray, lineWidth: 1)
|
||||
.background(Circle().fill(dragging ? .gray : .white))
|
||||
.offset(x: thumbOffset)
|
||||
.foregroundColor(.red.opacity(0.6))
|
||||
|
||||
.frame(maxHeight: height * 2)
|
||||
.frame(maxHeight: height * 4)
|
||||
|
||||
#if !os(tvOS)
|
||||
.gesture(
|
||||
@@ -114,7 +113,7 @@ struct TimelineView: View {
|
||||
var projectedValue: Double {
|
||||
let change = (dragOffset / size.width) * units
|
||||
let projected = draggedFrom + change
|
||||
return projected.isFinite ? projected : start
|
||||
return projected.isFinite ? (duration - projected < (0.01 * duration) ? duration : projected) : start
|
||||
}
|
||||
|
||||
var thumbOffset: Double {
|
||||
@@ -192,6 +191,7 @@ struct TimelineView_Previews: PreviewProvider {
|
||||
TimelineView(duration: .constant(100), current: .constant(90))
|
||||
TimelineView(duration: .constant(100), current: .constant(100))
|
||||
}
|
||||
.environmentObject(PlayerModel())
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ struct VideoDetails: View {
|
||||
@State private var currentPage = Page.info
|
||||
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
@Environment(\.navigationStyle) private var navigationStyle
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@@ -112,7 +111,6 @@ struct VideoDetails: View {
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
}
|
||||
}
|
||||
.padding(.top, inNavigationView && fullScreen ? 10 : 0)
|
||||
.onAppear {
|
||||
if video.isNil && !sidebarQueue {
|
||||
currentPage = .queue
|
||||
@@ -428,7 +426,7 @@ struct VideoDetails: View {
|
||||
|
||||
var detailsPage: some View {
|
||||
Group {
|
||||
Group {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let video = player.currentVideo {
|
||||
VStack(spacing: 6) {
|
||||
HStack {
|
||||
@@ -442,6 +440,7 @@ struct VideoDetails: View {
|
||||
|
||||
Divider()
|
||||
}
|
||||
.padding(.bottom, 6)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if let description = video.description {
|
||||
|
||||
@@ -7,6 +7,10 @@ import Siesta
|
||||
import SwiftUI
|
||||
|
||||
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 var defaultMinimumHeightLeft: Double {
|
||||
#if os(macOS)
|
||||
@@ -28,7 +32,7 @@ struct VideoPlayerView: View {
|
||||
|
||||
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
|
||||
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
|
||||
@Default(.lockLandscapeOnRotation) private var lockLandscapeOnRotation
|
||||
@Default(.lockOrientationInFullScreen) private var lockOrientationInFullScreen
|
||||
|
||||
@State private var motionManager: CMMotionManager!
|
||||
@State private var orientation = UIInterfaceOrientation.portrait
|
||||
@@ -37,6 +41,10 @@ struct VideoPlayerView: View {
|
||||
var mouseLocation: CGPoint { NSEvent.mouseLocation }
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
@State private var viewVerticalOffset = Self.hiddenOffset
|
||||
#endif
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@@ -54,10 +62,6 @@ struct VideoPlayerView: View {
|
||||
content
|
||||
.onAppear {
|
||||
playerSize = geometry.size
|
||||
|
||||
#if os(iOS)
|
||||
configureOrientationUpdatesBasedOnAccelerometer()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.onChange(of: geometry.size) { size in
|
||||
@@ -70,22 +74,28 @@ struct VideoPlayerView: View {
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
|
||||
handleOrientationDidChangeNotification()
|
||||
}
|
||||
.onDisappear {
|
||||
guard !playerControls.playingFullscreen else {
|
||||
return // swiftlint:disable:this implicit_return
|
||||
}
|
||||
|
||||
if Defaults[.lockPortraitWhenBrowsing] {
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
.onChange(of: player.presentingPlayer) { newValue in
|
||||
if newValue {
|
||||
viewVerticalOffset = 0
|
||||
configureOrientationUpdatesBasedOnAccelerometer()
|
||||
} else {
|
||||
Orientation.lockOrientation(.allButUpsideDown)
|
||||
}
|
||||
if Defaults[.lockPortraitWhenBrowsing] {
|
||||
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||
} else {
|
||||
Orientation.lockOrientation(.allButUpsideDown)
|
||||
}
|
||||
|
||||
motionManager?.stopAccelerometerUpdates()
|
||||
motionManager = nil
|
||||
motionManager?.stopAccelerometerUpdates()
|
||||
motionManager = nil
|
||||
viewVerticalOffset = Self.hiddenOffset
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(iOS)
|
||||
.offset(y: viewVerticalOffset)
|
||||
.animation(.easeIn(duration: 0.2), value: viewVerticalOffset)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -138,29 +148,49 @@ struct VideoPlayerView: View {
|
||||
hoveringPlayer = hovering
|
||||
hovering ? playerControls.show() : playerControls.hide()
|
||||
}
|
||||
#if os(iOS)
|
||||
.onSwipeGesture(
|
||||
up: {
|
||||
withAnimation {
|
||||
fullScreenDetails = true
|
||||
#if !os(macOS)
|
||||
.gesture(
|
||||
DragGesture(coordinateSpace: .global)
|
||||
.onChanged { value in
|
||||
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)
|
||||
.onAppear(perform: {
|
||||
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
|
||||
if hoveringPlayer {
|
||||
playerControls.resetTimer()
|
||||
}
|
||||
|
||||
return $0
|
||||
}
|
||||
})
|
||||
return $0
|
||||
}
|
||||
})
|
||||
#endif
|
||||
|
||||
.background(Color.black)
|
||||
.background(Color.black)
|
||||
|
||||
#if !os(tvOS)
|
||||
if !playerControls.playingFullscreen {
|
||||
@@ -269,20 +299,34 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
|
||||
func playerPlaceholder(geometry: GeometryProxy) -> some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
ZStack(alignment: .topLeading) {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack(spacing: 10) {
|
||||
#if !os(tvOS)
|
||||
Image(systemName: "ticket")
|
||||
.font(.system(size: 120))
|
||||
#endif
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack(spacing: 10) {
|
||||
#if !os(tvOS)
|
||||
Image(systemName: "ticket")
|
||||
.font(.system(size: 120))
|
||||
#endif
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
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())
|
||||
.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)
|
||||
|
||||
guard lockLandscapeOnRotation else {
|
||||
guard lockOrientationInFullScreen else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -402,7 +446,8 @@ struct VideoPlayerView: View {
|
||||
} else {
|
||||
guard abs(acceleration.z) <= 0.74,
|
||||
player.lockedOrientation.isNil,
|
||||
enterFullscreenInLandscape
|
||||
enterFullscreenInLandscape,
|
||||
!lockOrientationInFullScreen
|
||||
else {
|
||||
return
|
||||
}
|
||||
@@ -417,10 +462,11 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
|
||||
private func handleOrientationDidChangeNotification() {
|
||||
viewVerticalOffset = viewVerticalOffset == 0 ? 0 : Self.hiddenOffset
|
||||
let newOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
|
||||
if newOrientation?.isLandscape ?? false,
|
||||
player.presentingPlayer,
|
||||
lockLandscapeOnRotation,
|
||||
lockOrientationInFullScreen,
|
||||
!player.lockedOrientation.isNil
|
||||
{
|
||||
Orientation.lockOrientation(.landscape, andRotateTo: newOrientation)
|
||||
|
||||
@@ -204,7 +204,7 @@ struct SearchView: View {
|
||||
visibleSections.append(.subscriptions)
|
||||
}
|
||||
|
||||
if accounts.app.supportsUserPlaylists && preferred.contains(.playlists) {
|
||||
if accounts.app.supportsUserPlaylists && accounts.signedIn && preferred.contains(.playlists) {
|
||||
visibleSections.append(.playlists)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,8 @@ struct PlayerSettings: View {
|
||||
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
||||
#if os(iOS)
|
||||
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
|
||||
@Default(.lockLandscapeOnRotation) private var lockLandscapeOnRotation
|
||||
@Default(.lockOrientationInFullScreen) private var lockOrientationInFullScreen
|
||||
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
|
||||
@Default(.lockLandscapeWhenEnteringFullscreen) private var lockLandscapeWhenEnteringFullscreen
|
||||
#endif
|
||||
@Default(.closePiPOnNavigation) private var closePiPOnNavigation
|
||||
@Default(.closePiPOnOpeningPlayer) private var closePiPOnOpeningPlayer
|
||||
@@ -96,13 +95,12 @@ struct PlayerSettings: View {
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
Section(header: SettingsHeader(text: "Orientation"), footer: orientationFooter) {
|
||||
Section(header: SettingsHeader(text: "Orientation")) {
|
||||
if idiom == .pad {
|
||||
enterFullscreenInLandscapeToggle
|
||||
}
|
||||
honorSystemOrientationLockToggle
|
||||
lockLandscapeOnRotationToggle
|
||||
lockLandscapeWhenEnteringFullscreenToggle
|
||||
lockOrientationInFullScreenToggle
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -215,18 +213,10 @@ struct PlayerSettings: View {
|
||||
Toggle("Enter fullscreen in landscape", isOn: $enterFullscreenInLandscape)
|
||||
}
|
||||
|
||||
private var lockLandscapeOnRotationToggle: some View {
|
||||
Toggle("Lock landscape on rotation", isOn: $lockLandscapeOnRotation)
|
||||
private var lockOrientationInFullScreenToggle: some View {
|
||||
Toggle("Lock orientation in fullscreen", isOn: $lockOrientationInFullScreen)
|
||||
.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
|
||||
|
||||
private var closePiPOnNavigationToggle: some View {
|
||||
|
||||
@@ -6,8 +6,8 @@ import SwiftUI
|
||||
struct VideoCell: View {
|
||||
private var video: Video
|
||||
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
@Environment(\.navigationStyle) private var navigationStyle
|
||||
@Environment(\.inChannelView) private var inChannelView
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
@@ -46,11 +46,8 @@ struct VideoCell: View {
|
||||
.buttonStyle(.plain)
|
||||
.contentShape(RoundedRectangle(cornerRadius: thumbnailRoundingCornerRadius))
|
||||
.contextMenu {
|
||||
VideoContextMenuView(
|
||||
video: video,
|
||||
playerNavigationLinkActive: $player.playerNavigationLinkActive
|
||||
)
|
||||
.environmentObject(accounts)
|
||||
VideoContextMenuView(video: video)
|
||||
.environmentObject(accounts)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +81,8 @@ struct VideoCell: View {
|
||||
|
||||
var playAt: CMTime?
|
||||
|
||||
if playNowContinues,
|
||||
if saveHistory,
|
||||
playNowContinues,
|
||||
!watch.isNil,
|
||||
!watch!.finished
|
||||
{
|
||||
@@ -93,7 +91,7 @@ struct VideoCell: View {
|
||||
|
||||
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 {
|
||||
Button {
|
||||
guard !inChannelView else {
|
||||
return
|
||||
}
|
||||
|
||||
NavigationModel.openChannel(
|
||||
video.channel,
|
||||
player: player,
|
||||
|
||||
@@ -65,15 +65,6 @@ struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
Button {
|
||||
model.closeCurrentItem()
|
||||
} label: {
|
||||
Label("Close Video", systemImage: "xmark.circle")
|
||||
.labelStyle(.automatic)
|
||||
}
|
||||
.disabled(model.currentItem.isNil)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
@@ -82,43 +73,41 @@ struct BrowserPlayerControls<Content: View, Toolbar: View>: View {
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
|
||||
ZStack(alignment: .bottom) {
|
||||
HStack {
|
||||
Group {
|
||||
if playerControls.isPlaying {
|
||||
Button(action: {
|
||||
model.pause()
|
||||
}) {
|
||||
Label("Pause", systemImage: "pause.fill")
|
||||
}
|
||||
} else {
|
||||
Button(action: {
|
||||
model.play()
|
||||
}) {
|
||||
Label("Play", systemImage: "play.fill")
|
||||
}
|
||||
HStack {
|
||||
Group {
|
||||
if !model.currentItem.isNil {
|
||||
Button {
|
||||
model.closeCurrentItem()
|
||||
model.closePiP()
|
||||
} label: {
|
||||
Label("Close Video", systemImage: "xmark")
|
||||
}
|
||||
}
|
||||
.disabled(playerControls.isLoadingVideo || model.currentItem.isNil)
|
||||
.font(.system(size: 30))
|
||||
.frame(minWidth: 30)
|
||||
|
||||
Button(action: { model.advanceToNextItem() }) {
|
||||
Label("Next", systemImage: "forward.fill")
|
||||
.padding(.vertical)
|
||||
.contentShape(Rectangle())
|
||||
if playerControls.isPlaying {
|
||||
Button(action: {
|
||||
model.pause()
|
||||
}) {
|
||||
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)
|
||||
.progressViewStyle(.linear)
|
||||
#if os(iOS)
|
||||
.frame(maxWidth: 60)
|
||||
#else
|
||||
.offset(y: 6)
|
||||
.frame(maxWidth: 70)
|
||||
#endif
|
||||
Button(action: { model.advanceToNextItem() }) {
|
||||
Label("Next", systemImage: "forward.fill")
|
||||
.padding(.vertical)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.disabled(model.queue.isEmpty)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
@@ -2,55 +2,89 @@ import Siesta
|
||||
import SwiftUI
|
||||
|
||||
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 shareURL: URL?
|
||||
|
||||
#if os(iOS)
|
||||
@State private var viewVerticalOffset = Self.hiddenOffset
|
||||
#endif
|
||||
|
||||
@StateObject private var store = Store<ChannelPlaylist>()
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
@Environment(\.navigationStyle) private var navigationStyle
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
|
||||
var items: [ContentItem] {
|
||||
private var items: [ContentItem] {
|
||||
ContentItem.array(of: store.item?.videos ?? [])
|
||||
}
|
||||
|
||||
var resource: Resource? {
|
||||
accounts.api.channelPlaylist(playlist.id)
|
||||
private var presentedPlaylist: ChannelPlaylist? {
|
||||
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 {
|
||||
#if os(iOS)
|
||||
if inNavigationView {
|
||||
content
|
||||
} else {
|
||||
if navigationStyle == .tab {
|
||||
NavigationView {
|
||||
BrowserPlayerControls {
|
||||
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 {
|
||||
content
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
var content: some View {
|
||||
VStack(alignment: .leading) {
|
||||
#if os(tvOS)
|
||||
HStack {
|
||||
Text(playlist.title)
|
||||
.font(.title2)
|
||||
.frame(alignment: .leading)
|
||||
if let playlist = presentedPlaylist {
|
||||
Text(playlist.title)
|
||||
.font(.title2)
|
||||
.frame(alignment: .leading)
|
||||
|
||||
Spacer()
|
||||
Spacer()
|
||||
|
||||
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
|
||||
.labelStyle(.iconOnly)
|
||||
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
|
||||
.labelStyle(.iconOnly)
|
||||
}
|
||||
|
||||
playButton
|
||||
.labelStyle(.iconOnly)
|
||||
@@ -69,7 +103,6 @@ struct ChannelPlaylistView: View {
|
||||
}
|
||||
#endif
|
||||
.onAppear {
|
||||
resource?.addObserver(store)
|
||||
resource?.loadIfNeeded()
|
||||
}
|
||||
#if os(tvOS)
|
||||
@@ -77,26 +110,31 @@ struct ChannelPlaylistView: View {
|
||||
#else
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigation) {
|
||||
ShareButton(
|
||||
contentItem: contentItem,
|
||||
presentingShareSheet: $presentingShareSheet,
|
||||
shareURL: $shareURL
|
||||
)
|
||||
if navigationStyle == .tab {
|
||||
Button("Done") {
|
||||
navigation.presentingPlaylist = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: playlistButtonsPlacement) {
|
||||
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
|
||||
shuffleButton
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(playlist.title)
|
||||
#if os(iOS)
|
||||
.navigationBarHidden(player.playerNavigationLinkActive)
|
||||
#endif
|
||||
.navigationTitle(presentedPlaylist?.title ?? "")
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -110,7 +148,7 @@ struct ChannelPlaylistView: View {
|
||||
|
||||
private var playButton: some View {
|
||||
Button {
|
||||
player.play(videos, inNavigationView: inNavigationView)
|
||||
player.play(videos)
|
||||
} label: {
|
||||
Label("Play All", systemImage: "play")
|
||||
}
|
||||
@@ -118,7 +156,7 @@ struct ChannelPlaylistView: View {
|
||||
|
||||
private var shuffleButton: some View {
|
||||
Button {
|
||||
player.play(videos, shuffling: true, inNavigationView: inNavigationView)
|
||||
player.play(videos, shuffling: true)
|
||||
} label: {
|
||||
Label("Shuffle", systemImage: "shuffle")
|
||||
}
|
||||
|
||||
@@ -2,46 +2,69 @@ import Siesta
|
||||
import SwiftUI
|
||||
|
||||
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 shareURL: URL?
|
||||
@State private var subscriptionToggleButtonDisabled = false
|
||||
|
||||
#if os(iOS)
|
||||
@State private var viewVerticalOffset = Self.hiddenOffset
|
||||
#endif
|
||||
|
||||
@StateObject private var store = Store<Channel>()
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.navigationStyle) private var navigationStyle
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
#endif
|
||||
|
||||
@EnvironmentObject<AccountsModel> private var accounts
|
||||
@EnvironmentObject<NavigationModel> private var navigation
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||
|
||||
@Namespace private var focusNamespace
|
||||
|
||||
var presentedChannel: Channel? {
|
||||
channel ?? recents.presentedChannel
|
||||
}
|
||||
|
||||
var videos: [ContentItem] {
|
||||
ContentItem.array(of: store.item?.videos ?? [])
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
#if os(iOS)
|
||||
if inNavigationView {
|
||||
content
|
||||
} else {
|
||||
if navigationStyle == .tab {
|
||||
NavigationView {
|
||||
BrowserPlayerControls {
|
||||
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 {
|
||||
content
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
var content: some View {
|
||||
@@ -54,8 +77,10 @@ struct ChannelVideosView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name)))
|
||||
.labelStyle(.iconOnly)
|
||||
if let channel = presentedChannel {
|
||||
FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name)))
|
||||
.labelStyle(.iconOnly)
|
||||
}
|
||||
|
||||
if let subscribers = store.item?.subscriptionsString {
|
||||
Text("**\(subscribers)** subscribers")
|
||||
@@ -77,27 +102,35 @@ struct ChannelVideosView: View {
|
||||
#if !os(tvOS)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigation) {
|
||||
ShareButton(
|
||||
contentItem: contentItem,
|
||||
presentingShareSheet: $presentingShareSheet,
|
||||
shareURL: $shareURL
|
||||
)
|
||||
if navigationStyle == .tab {
|
||||
Button("Done") {
|
||||
navigation.presentingChannel = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem {
|
||||
HStack {
|
||||
HStack(spacing: 3) {
|
||||
Text("\(store.item?.subscriptionsString ?? "loading")")
|
||||
Text("\(store.item?.subscriptionsString ?? "")")
|
||||
.fontWeight(.bold)
|
||||
Text(" subscribers")
|
||||
.allowsTightening(true)
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(store.item?.subscriptionsString != nil ? 1 : 0)
|
||||
}
|
||||
.allowsTightening(true)
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(store.item?.subscriptionsString != nil ? 1 : 0)
|
||||
|
||||
ShareButton(
|
||||
contentItem: contentItem,
|
||||
presentingShareSheet: $presentingShareSheet,
|
||||
shareURL: $shareURL
|
||||
)
|
||||
|
||||
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
|
||||
.onAppear {
|
||||
if store.item.isNil {
|
||||
resource.addObserver(store)
|
||||
resource.load()
|
||||
}
|
||||
resource?.loadIfNeeded()
|
||||
}
|
||||
#if os(iOS)
|
||||
.navigationBarHidden(player.playerNavigationLinkActive)
|
||||
#endif
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(navigationTitle)
|
||||
#endif
|
||||
|
||||
return Group {
|
||||
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)
|
||||
resource.addObserver(store)
|
||||
|
||||
return resource
|
||||
}
|
||||
|
||||
private var subscriptionToggleButton: some View {
|
||||
Group {
|
||||
if accounts.app.supportsSubscriptions && accounts.signedIn {
|
||||
if subscriptions.isSubscribing(channel.id) {
|
||||
Button("Unsubscribe") {
|
||||
subscriptionToggleButtonDisabled = true
|
||||
@ViewBuilder private var subscriptionToggleButton: some View {
|
||||
if let channel = presentedChannel {
|
||||
Group {
|
||||
if accounts.app.supportsSubscriptions && accounts.signedIn {
|
||||
if subscriptions.isSubscribing(channel.id) {
|
||||
Button("Unsubscribe") {
|
||||
subscriptionToggleButtonDisabled = true
|
||||
|
||||
subscriptions.unsubscribe(channel.id) {
|
||||
subscriptionToggleButtonDisabled = false
|
||||
subscriptions.unsubscribe(channel.id) {
|
||||
subscriptionToggleButtonDisabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button("Subscribe") {
|
||||
subscriptionToggleButtonDisabled = true
|
||||
} else {
|
||||
Button("Subscribe") {
|
||||
subscriptionToggleButtonDisabled = true
|
||||
|
||||
subscriptions.subscribe(channel.id) {
|
||||
subscriptionToggleButtonDisabled = false
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
subscriptions.subscribe(channel.id) {
|
||||
subscriptionToggleButtonDisabled = false
|
||||
navigation.sidebarSectionChanged.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(subscriptionToggleButtonDisabled)
|
||||
}
|
||||
.disabled(subscriptionToggleButtonDisabled)
|
||||
}
|
||||
|
||||
private var contentItem: ContentItem {
|
||||
ContentItem(channel: channel)
|
||||
ContentItem(channel: presentedChannel)
|
||||
}
|
||||
|
||||
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 {
|
||||
let playlist: Playlist
|
||||
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
@EnvironmentObject<PlaylistsModel> private var model
|
||||
|
||||
@@ -66,13 +65,13 @@ struct PlaylistVideosView: View {
|
||||
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
|
||||
|
||||
Button {
|
||||
player.play(videos, inNavigationView: inNavigationView)
|
||||
player.play(videos)
|
||||
} label: {
|
||||
Label("Play All", systemImage: "play")
|
||||
}
|
||||
|
||||
Button {
|
||||
player.play(videos, shuffling: true, inNavigationView: inNavigationView)
|
||||
player.play(videos, shuffling: true)
|
||||
} label: {
|
||||
Label("Shuffle", systemImage: "shuffle")
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import CoreData
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct VideoContextMenuView: View {
|
||||
let video: Video
|
||||
|
||||
@Binding var playerNavigationLinkActive: Bool
|
||||
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
@Environment(\.inChannelView) private var inChannelView
|
||||
@Environment(\.inChannelPlaylistView) private var inChannelPlaylistView
|
||||
@Environment(\.navigationStyle) private var navigationStyle
|
||||
@@ -26,9 +24,8 @@ struct VideoContextMenuView: View {
|
||||
|
||||
private var viewContext: NSManagedObjectContext = PersistenceController.shared.container.viewContext
|
||||
|
||||
init(video: Video, playerNavigationLinkActive: Binding<Bool>) {
|
||||
init(video: Video) {
|
||||
self.video = video
|
||||
_playerNavigationLinkActive = playerNavigationLinkActive
|
||||
_watchRequest = video.watchFetchRequest
|
||||
}
|
||||
|
||||
@@ -57,6 +54,9 @@ struct VideoContextMenuView: View {
|
||||
|
||||
Section {
|
||||
playNowButton
|
||||
#if os(iOS)
|
||||
playNowInPictureInPictureButton
|
||||
#endif
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -111,7 +111,7 @@ struct VideoContextMenuView: View {
|
||||
|
||||
private var continueButton: some View {
|
||||
Button {
|
||||
player.play(video, at: .secondsInDefaultTimescale(watch!.stoppedAt), inNavigationView: inNavigationView)
|
||||
player.play(video, at: .secondsInDefaultTimescale(watch!.stoppedAt))
|
||||
} label: {
|
||||
Label("Continue from \(watch!.stoppedAt.formattedAsPlaybackTime() ?? "where I left off")", systemImage: "playpause")
|
||||
}
|
||||
@@ -131,12 +131,24 @@ struct VideoContextMenuView: View {
|
||||
|
||||
private var playNowButton: some View {
|
||||
Button {
|
||||
player.play(video, inNavigationView: inNavigationView)
|
||||
player.play(video)
|
||||
} label: {
|
||||
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 {
|
||||
Button {
|
||||
player.playNext(video)
|
||||
|
||||
@@ -1407,6 +1407,7 @@
|
||||
37152EE926EFEB95004FB96D /* LazyView.swift */,
|
||||
37030FF627B0347C00ECDDAA /* MPVPlayerView.swift */,
|
||||
37E70926271CDDAE00D34DDE /* OpenSettingsButton.swift */,
|
||||
37FEF11227EFD8580033912F /* PlaceholderCell.swift */,
|
||||
3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */,
|
||||
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */,
|
||||
37AAF27D26737323007FC770 /* PopularView.swift */,
|
||||
@@ -1415,7 +1416,6 @@
|
||||
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */,
|
||||
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */,
|
||||
37E70922271CD43000D34DDE /* WelcomeScreen.swift */,
|
||||
37FEF11227EFD8580033912F /* PlaceholderCell.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -3103,7 +3103,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -3116,7 +3116,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -3137,7 +3137,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -3150,7 +3150,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -3169,7 +3169,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -3181,7 +3181,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -3201,7 +3201,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -3213,7 +3213,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -3365,7 +3365,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++14";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
@@ -3389,9 +3389,9 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Vendor/mpv/iOS/lib",
|
||||
);
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4;
|
||||
OTHER_LDFLAGS = "-lstdc++";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -3407,7 +3407,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||
@@ -3427,9 +3427,9 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Vendor/mpv/iOS/lib",
|
||||
);
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4;
|
||||
OTHER_LDFLAGS = "-lstdc++";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -3449,7 +3449,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -3468,7 +3468,7 @@
|
||||
"$(PROJECT_DIR)/Vendor/mpv/macOS/lib",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = macosx;
|
||||
@@ -3487,7 +3487,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -3506,7 +3506,7 @@
|
||||
"$(PROJECT_DIR)/Vendor/mpv/macOS/lib",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
MARKETING_VERSION = 1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = macosx;
|
||||
@@ -3530,7 +3530,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -3555,7 +3555,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -3582,7 +3582,7 @@
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-macOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
@@ -3607,7 +3607,7 @@
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "stream.yattee.Tests-macOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
@@ -3623,7 +3623,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -3643,8 +3643,8 @@
|
||||
"$(PROJECT_DIR)/Vendor/mpv",
|
||||
"$(PROJECT_DIR)/Vendor/mpv/tvOS",
|
||||
);
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha;
|
||||
MARKETING_VERSION = 1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = appletvos;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -3661,7 +3661,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 33;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -3681,8 +3681,8 @@
|
||||
"$(PROJECT_DIR)/Vendor/mpv",
|
||||
"$(PROJECT_DIR)/Vendor/mpv/tvOS",
|
||||
);
|
||||
MARKETING_VERSION = "1.4-alpha.4";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app.alpha;
|
||||
MARKETING_VERSION = 1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.app;
|
||||
PRODUCT_NAME = Yattee;
|
||||
SDKROOT = appletvos;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -3707,7 +3707,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.YatteeUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = appletvos;
|
||||
@@ -3732,7 +3732,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = stream.yattee.YatteeUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = appletvos;
|
||||
|
||||
@@ -21,5 +21,7 @@
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Need camera access to take pictures</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Reference in New Issue
Block a user