mirror of
https://github.com/yattee/yattee.git
synced 2025-12-13 19:48:14 +00:00
Compare commits
15 Commits
v1.3-beta.
...
v1.3-beta.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5498e2c4ab | ||
|
|
00778b585f | ||
|
|
d6e75295e1 | ||
|
|
aec7480353 | ||
|
|
e29982454b | ||
|
|
117057dd0e | ||
|
|
9ede4b9b1f | ||
|
|
f0d1b74e34 | ||
|
|
2a75d0a1d4 | ||
|
|
04df9551ba | ||
|
|
ba21583a95 | ||
|
|
149607efbc | ||
|
|
89957e3b56 | ||
|
|
0af2db2fd7 | ||
|
|
ab174c73fd |
@@ -1,5 +1,6 @@
|
|||||||
import CoreData
|
import CoreData
|
||||||
import CoreMedia
|
import CoreMedia
|
||||||
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension PlayerModel {
|
extension PlayerModel {
|
||||||
@@ -42,6 +43,10 @@ extension PlayerModel {
|
|||||||
watch.videoID = id
|
watch.videoID = id
|
||||||
} else {
|
} else {
|
||||||
watch = results?.first
|
watch = results?.first
|
||||||
|
|
||||||
|
if !Defaults[.resetWatchedStatusOnPlaying], watch.finished {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let seconds = playerItemDuration?.seconds {
|
if let seconds = playerItemDuration?.seconds {
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
import AVKit
|
import AVKit
|
||||||
import CoreData
|
import CoreData
|
||||||
|
#if os(iOS)
|
||||||
|
import CoreMotion
|
||||||
|
#endif
|
||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
#if !os(macOS)
|
|
||||||
import UIKit
|
|
||||||
#endif
|
|
||||||
import Siesta
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
#if !os(macOS)
|
||||||
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
final class PlayerModel: ObservableObject {
|
final class PlayerModel: ObservableObject {
|
||||||
static let availableRates: [Float] = [0.5, 0.67, 0.8, 1, 1.25, 1.5, 2]
|
static let availableRates: [Float] = [0.5, 0.67, 0.8, 1, 1.25, 1.5, 2]
|
||||||
|
static let assetKeysToLoad = ["tracks", "playable", "duration"]
|
||||||
let logger = Logger(label: "stream.yattee.app")
|
let logger = Logger(label: "stream.yattee.app")
|
||||||
|
|
||||||
private(set) var player = AVPlayer()
|
private(set) var player = AVPlayer()
|
||||||
var playerView = Player()
|
var playerView = Player()
|
||||||
var controller: PlayerViewController?
|
var controller: PlayerViewController?
|
||||||
|
var playerItem: AVPlayerItem?
|
||||||
|
|
||||||
@Published var presentingPlayer = false { didSet { handlePresentationChange() } }
|
@Published var presentingPlayer = false { didSet { handlePresentationChange() } }
|
||||||
|
|
||||||
@@ -42,9 +47,16 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
@Published var channelWithDetails: Channel?
|
@Published var channelWithDetails: Channel?
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
@Published var motionManager: CMMotionManager!
|
||||||
|
@Published var lockedOrientation: UIInterfaceOrientation?
|
||||||
|
@Published var lastOrientation: UIInterfaceOrientation?
|
||||||
|
#endif
|
||||||
|
|
||||||
var accounts: AccountsModel
|
var accounts: AccountsModel
|
||||||
var comments: CommentsModel
|
var comments: CommentsModel
|
||||||
|
|
||||||
|
var asset: AVURLAsset?
|
||||||
var composition = AVMutableComposition()
|
var composition = AVMutableComposition()
|
||||||
var loadedCompositionAssets = [AVMediaType]()
|
var loadedCompositionAssets = [AVMediaType]()
|
||||||
|
|
||||||
@@ -60,6 +72,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
private var timeObserverThrottle = Throttle(interval: 2)
|
private var timeObserverThrottle = Throttle(interval: 2)
|
||||||
|
|
||||||
var playingInPictureInPicture = false
|
var playingInPictureInPicture = false
|
||||||
|
var playingFullscreen = false
|
||||||
|
|
||||||
@Published var presentingErrorDetails = false
|
@Published var presentingErrorDetails = false
|
||||||
var playerError: Error? { didSet {
|
var playerError: Error? { didSet {
|
||||||
@@ -82,7 +95,6 @@ final class PlayerModel: ObservableObject {
|
|||||||
self.accounts = accounts ?? AccountsModel()
|
self.accounts = accounts ?? AccountsModel()
|
||||||
self.comments = comments ?? CommentsModel()
|
self.comments = comments ?? CommentsModel()
|
||||||
|
|
||||||
addItemDidPlayToEndTimeObserver()
|
|
||||||
addFrequentTimeObserver()
|
addFrequentTimeObserver()
|
||||||
addInfrequentTimeObserver()
|
addInfrequentTimeObserver()
|
||||||
addPlayerTimeControlStatusObserver()
|
addPlayerTimeControlStatusObserver()
|
||||||
@@ -103,11 +115,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func hide() {
|
func hide() {
|
||||||
guard presentingPlayer else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
presentingPlayer = false
|
presentingPlayer = false
|
||||||
|
playerNavigationLinkActive = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func togglePlayer() {
|
func togglePlayer() {
|
||||||
@@ -125,6 +134,14 @@ final class PlayerModel: ObservableObject {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isLoadingVideo: Bool {
|
||||||
|
guard !currentVideo.isNil else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return player.currentItem == nil || time == nil || !time!.isValid
|
||||||
|
}
|
||||||
|
|
||||||
var isPlaying: Bool {
|
var isPlaying: Bool {
|
||||||
player.timeControlStatus == .playing
|
player.timeControlStatus == .playing
|
||||||
}
|
}
|
||||||
@@ -189,20 +206,21 @@ final class PlayerModel: ObservableObject {
|
|||||||
if !upgrading {
|
if !upgrading {
|
||||||
resetSegments()
|
resetSegments()
|
||||||
|
|
||||||
sponsorBlock.loadSegments(
|
DispatchQueue.main.async { [weak self] in
|
||||||
videoID: video.videoID,
|
self?.sponsorBlock.loadSegments(
|
||||||
categories: Defaults[.sponsorBlockCategories]
|
videoID: video.videoID,
|
||||||
) { [weak self] in
|
categories: Defaults[.sponsorBlockCategories]
|
||||||
if Defaults[.showChannelSubscribers] {
|
) { [weak self] in
|
||||||
self?.loadCurrentItemChannelDetails()
|
if Defaults[.showChannelSubscribers] {
|
||||||
|
self?.loadCurrentItemChannelDetails()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let url = stream.singleAssetURL {
|
if let url = stream.singleAssetURL {
|
||||||
logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)")
|
logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)")
|
||||||
|
loadSingleAsset(url, stream: stream, of: video, preservingTime: preservingTime)
|
||||||
insertPlayerItem(stream, for: video, preservingTime: preservingTime)
|
|
||||||
} else {
|
} else {
|
||||||
logger.info("playing stream with many assets:")
|
logger.info("playing stream with many assets:")
|
||||||
logger.info("composition audio asset: \(stream.audioAsset.url)")
|
logger.info("composition audio asset: \(stream.audioAsset.url)")
|
||||||
@@ -274,11 +292,14 @@ final class PlayerModel: ObservableObject {
|
|||||||
for video: Video,
|
for video: Video,
|
||||||
preservingTime: Bool = false
|
preservingTime: Bool = false
|
||||||
) {
|
) {
|
||||||
let playerItem = playerItem(stream)
|
removeItemDidPlayToEndTimeObserver()
|
||||||
|
|
||||||
|
playerItem = playerItem(stream)
|
||||||
guard playerItem != nil else {
|
guard playerItem != nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addItemDidPlayToEndTimeObserver()
|
||||||
attachMetadata(to: playerItem!, video: video, for: stream)
|
attachMetadata(to: playerItem!, video: video, for: stream)
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
@@ -288,6 +309,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
self.composition = AVMutableComposition()
|
self.composition = AVMutableComposition()
|
||||||
|
self.asset = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let startPlaying = {
|
let startPlaying = {
|
||||||
@@ -295,7 +317,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
try? AVAudioSession.sharedInstance().setActive(true)
|
try? AVAudioSession.sharedInstance().setActive(true)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if self.isAutoplaying(playerItem!) {
|
if self.isAutoplaying(self.playerItem!) {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||||
guard let self = self else {
|
guard let self = self else {
|
||||||
return
|
return
|
||||||
@@ -326,7 +348,10 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let replaceItemAndSeek = {
|
let replaceItemAndSeek = {
|
||||||
self.player.replaceCurrentItem(with: playerItem)
|
guard video == self.currentVideo else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.player.replaceCurrentItem(with: self.playerItem)
|
||||||
self.seekToPreservedTime { finished in
|
self.seekToPreservedTime { finished in
|
||||||
guard finished else {
|
guard finished else {
|
||||||
return
|
return
|
||||||
@@ -353,6 +378,32 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadSingleAsset(
|
||||||
|
_ url: URL,
|
||||||
|
stream: Stream,
|
||||||
|
of video: Video,
|
||||||
|
preservingTime: Bool = false
|
||||||
|
) {
|
||||||
|
asset?.cancelLoading()
|
||||||
|
asset = AVURLAsset(url: url)
|
||||||
|
asset?.loadValuesAsynchronously(forKeys: Self.assetKeysToLoad) { [weak self] in
|
||||||
|
var error: NSError?
|
||||||
|
|
||||||
|
switch self?.asset?.statusOfValue(forKey: "duration", error: &error) {
|
||||||
|
case .loaded:
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.insertPlayerItem(stream, for: video, preservingTime: preservingTime)
|
||||||
|
}
|
||||||
|
case .failed:
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.playerError = error
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func loadComposition(
|
private func loadComposition(
|
||||||
_ stream: Stream,
|
_ stream: Stream,
|
||||||
of video: Video,
|
of video: Video,
|
||||||
@@ -370,7 +421,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
of video: Video,
|
of video: Video,
|
||||||
preservingTime: Bool = false
|
preservingTime: Bool = false
|
||||||
) {
|
) {
|
||||||
asset.loadValuesAsynchronously(forKeys: ["playable"]) { [weak self] in
|
asset.loadValuesAsynchronously(forKeys: Self.assetKeysToLoad) { [weak self] in
|
||||||
guard let self = self else {
|
guard let self = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -412,9 +463,9 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func playerItem(_ stream: Stream) -> AVPlayerItem? {
|
private func playerItem(_: Stream) -> AVPlayerItem? {
|
||||||
if let url = stream.singleAssetURL {
|
if let asset = asset {
|
||||||
return AVPlayerItem(asset: AVURLAsset(url: url))
|
return AVPlayerItem(asset: asset)
|
||||||
} else {
|
} else {
|
||||||
return AVPlayerItem(asset: composition)
|
return AVPlayerItem(asset: composition)
|
||||||
}
|
}
|
||||||
@@ -481,7 +532,15 @@ final class PlayerModel: ObservableObject {
|
|||||||
self,
|
self,
|
||||||
selector: #selector(itemDidPlayToEndTime),
|
selector: #selector(itemDidPlayToEndTime),
|
||||||
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
||||||
object: nil
|
object: playerItem
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeItemDidPlayToEndTimeObserver() {
|
||||||
|
NotificationCenter.default.removeObserver(
|
||||||
|
self,
|
||||||
|
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
|
||||||
|
object: playerItem
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,8 +679,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !currentItem.video.live {
|
if !currentItem.video.live {
|
||||||
let itemDuration = currentItem.videoDuration ?? 0
|
let itemDuration = currentItem.videoDuration ?? currentItem.duration
|
||||||
let duration = itemDuration.isFinite ? Int(itemDuration) : nil
|
let duration = itemDuration.isFinite ? Double(itemDuration) : nil
|
||||||
|
|
||||||
if !duration.isNil {
|
if !duration.isNil {
|
||||||
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration as AnyObject
|
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration as AnyObject
|
||||||
@@ -758,5 +817,27 @@ final class PlayerModel: ObservableObject {
|
|||||||
show()
|
show()
|
||||||
closePiP()
|
closePiP()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func enterFullScreen() {
|
||||||
|
guard !playingFullscreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("entering fullscreen")
|
||||||
|
|
||||||
|
controller?.playerView
|
||||||
|
.perform(NSSelectorFromString("enterFullScreenAnimated:completionHandler:"), with: false, with: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exitFullScreen() {
|
||||||
|
guard playingFullscreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("exiting fullscreen")
|
||||||
|
|
||||||
|
controller?.playerView
|
||||||
|
.perform(NSSelectorFromString("exitFullScreenAnimated:completionHandler:"), with: false, with: nil)
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,30 @@ extension PlayerModel {
|
|||||||
currentItem?.video
|
currentItem?.video
|
||||||
}
|
}
|
||||||
|
|
||||||
func playAll(_ videos: [Video]) {
|
func play(_ videos: [Video], shuffling: Bool = false, inNavigationView: Bool = false) {
|
||||||
let first = videos.first
|
let videosToPlay = shuffling ? videos.shuffled() : videos
|
||||||
|
|
||||||
videos.forEach { video in
|
guard let first = videosToPlay.first else {
|
||||||
enqueueVideo(video) { _, item in
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enqueueVideo(first, prepending: true) { _, item in
|
||||||
|
self.advanceToItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
videosToPlay.dropFirst().reversed().forEach { video in
|
||||||
|
enqueueVideo(video, prepending: true) { _, item in
|
||||||
if item.video == first {
|
if item.video == first {
|
||||||
self.advanceToItem(item)
|
self.advanceToItem(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if inNavigationView {
|
||||||
|
playerNavigationLinkActive = true
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func playNext(_ video: Video) {
|
func playNext(_ video: Video) {
|
||||||
@@ -62,7 +76,13 @@ extension PlayerModel {
|
|||||||
preservedTime = currentItem.playbackTime
|
preservedTime = currentItem.playbackTime
|
||||||
restoreLoadedChannel()
|
restoreLoadedChannel()
|
||||||
|
|
||||||
loadAvailableStreams(currentVideo!)
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let video = self?.currentVideo else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self?.loadAvailableStreams(video)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func preferredStream(_ streams: [Stream]) -> Stream? {
|
func preferredStream(_ streams: [Stream]) -> Stream? {
|
||||||
@@ -95,6 +115,9 @@ extension PlayerModel {
|
|||||||
|
|
||||||
remove(newItem)
|
remove(newItem)
|
||||||
|
|
||||||
|
currentItem = newItem
|
||||||
|
player.pause()
|
||||||
|
|
||||||
accounts.api.loadDetails(newItem) { newItem in
|
accounts.api.loadDetails(newItem) { newItem in
|
||||||
self.playItem(newItem, video: newItem.video, at: time)
|
self.playItem(newItem, video: newItem.video, at: time)
|
||||||
}
|
}
|
||||||
@@ -135,6 +158,12 @@ extension PlayerModel {
|
|||||||
) -> PlayerQueueItem? {
|
) -> PlayerQueueItem? {
|
||||||
let item = PlayerQueueItem(video, playbackTime: atTime)
|
let item = PlayerQueueItem(video, playbackTime: atTime)
|
||||||
|
|
||||||
|
if play {
|
||||||
|
currentItem = item
|
||||||
|
// pause playing current video as it's going to be replaced with next one
|
||||||
|
player.pause()
|
||||||
|
}
|
||||||
|
|
||||||
queue.insert(item, at: prepending ? 0 : queue.endIndex)
|
queue.insert(item, at: prepending ? 0 : queue.endIndex)
|
||||||
|
|
||||||
accounts.api.loadDetails(item) { newItem in
|
accounts.api.loadDetails(item) { newItem in
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ extension PlayerModel {
|
|||||||
.load()
|
.load()
|
||||||
.onSuccess { response in
|
.onSuccess { response in
|
||||||
if let video: Video = response.typedContent() {
|
if let video: Video = response.typedContent() {
|
||||||
|
guard video == self.currentVideo else {
|
||||||
|
self.logger.info("ignoring loaded streams from \(instance.description) as current video has changed")
|
||||||
|
return
|
||||||
|
}
|
||||||
self.availableStreams += self.streamsWithInstance(instance: instance, streams: video.streams)
|
self.availableStreams += self.streamsWithInstance(instance: instance, streams: video.streams)
|
||||||
} else {
|
} else {
|
||||||
self.logger.critical("no streams available from \(instance.description)")
|
self.logger.critical("no streams available from \(instance.description)")
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ final class SponsorBlockAPI: ObservableObject {
|
|||||||
|
|
||||||
self.videoID = videoID
|
self.videoID = videoID
|
||||||
|
|
||||||
requestSegments(categories: categories, completionHandler: completionHandler)
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.requestSegments(categories: categories, completionHandler: completionHandler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func requestSegments(categories: Set<String>, completionHandler: @escaping () -> Void = {}) {
|
private func requestSegments(categories: Set<String>, completionHandler: @escaping () -> Void = {}) {
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ extension Watch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let progress = (stoppedAt / videoDuration) * 100
|
let progress = (stoppedAt / videoDuration) * 100
|
||||||
|
|
||||||
|
if progress >= Double(watchedThreshold) {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
|
||||||
return min(max(progress, 0), 100)
|
return min(max(progress, 0), 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
38
Shared/Assets.xcassets/AppBlueColor.colorset/Contents.json
Normal file
38
Shared/Assets.xcassets/AppBlueColor.colorset/Contents.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "display-p3",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.361",
|
||||||
|
"green" : "0.200",
|
||||||
|
"red" : "0.129"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "display-p3",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.361",
|
||||||
|
"green" : "0.200",
|
||||||
|
"red" : "0.129"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Shared/Assets.xcassets/AppRedColor.colorset/Contents.json
Normal file
38
Shared/Assets.xcassets/AppRedColor.colorset/Contents.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "display-p3",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.263",
|
||||||
|
"green" : "0.290",
|
||||||
|
"red" : "0.859"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "display-p3",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.263",
|
||||||
|
"green" : "0.290",
|
||||||
|
"red" : "0.859"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.824",
|
||||||
|
"green" : "0.659",
|
||||||
|
"red" : "0.455"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.824",
|
||||||
|
"green" : "0.659",
|
||||||
|
"red" : "0.455"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
"color-space" : "display-p3",
|
"color-space" : "display-p3",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.263",
|
"blue" : "0.361",
|
||||||
"green" : "0.290",
|
"green" : "0.200",
|
||||||
"red" : "0.859"
|
"red" : "0.129"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
#if os(iOS)
|
||||||
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
extension Defaults.Keys {
|
extension Defaults.Keys {
|
||||||
static let kavinPipedInstanceID = "kavin-piped"
|
static let kavinPipedInstanceID = "kavin-piped"
|
||||||
@@ -31,8 +34,15 @@ extension Defaults.Keys {
|
|||||||
.init(section: .searchQuery("Apple Pie Recipes", "", "", ""))
|
.init(section: .searchQuery("Apple Pie Recipes", "", "", ""))
|
||||||
])
|
])
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: false)
|
||||||
|
#endif
|
||||||
|
#if os(iOS)
|
||||||
|
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||||
|
#endif
|
||||||
static let channelOnThumbnail = Key<Bool>("channelOnThumbnail", default: true)
|
static let channelOnThumbnail = Key<Bool>("channelOnThumbnail", default: true)
|
||||||
static let timeOnThumbnail = Key<Bool>("timeOnThumbnail", default: true)
|
static let timeOnThumbnail = Key<Bool>("timeOnThumbnail", default: true)
|
||||||
|
static let showHistoryInPlayer = Key<Bool>("showHistoryInPlayer", default: false)
|
||||||
|
|
||||||
static let quality = Key<ResolutionSetting>("quality", default: .best)
|
static let quality = Key<ResolutionSetting>("quality", default: .best)
|
||||||
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: PlayerSidebarSetting.defaultValue)
|
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: PlayerSidebarSetting.defaultValue)
|
||||||
@@ -59,7 +69,9 @@ extension Defaults.Keys {
|
|||||||
static let showWatchingProgress = Key<Bool>("showWatchingProgress", default: true)
|
static let showWatchingProgress = Key<Bool>("showWatchingProgress", default: true)
|
||||||
static let watchedThreshold = Key<Int>("watchedThreshold", default: 90)
|
static let watchedThreshold = Key<Int>("watchedThreshold", default: 90)
|
||||||
static let watchedVideoStyle = Key<WatchedVideoStyle>("watchedVideoStyle", default: .badge)
|
static let watchedVideoStyle = Key<WatchedVideoStyle>("watchedVideoStyle", default: .badge)
|
||||||
|
static let watchedVideoBadgeColor = Key<WatchedVideoBadgeColor>("WatchedVideoBadgeColor", default: .red)
|
||||||
static let watchedVideoPlayNowBehavior = Key<WatchedVideoPlayNowBehavior>("watchedVideoPlayNowBehavior", default: .continue)
|
static let watchedVideoPlayNowBehavior = Key<WatchedVideoPlayNowBehavior>("watchedVideoPlayNowBehavior", default: .continue)
|
||||||
|
static let resetWatchedStatusOnPlaying = Key<Bool>("resetWatchedStatusOnPlaying", default: false)
|
||||||
static let saveRecents = Key<Bool>("saveRecents", default: true)
|
static let saveRecents = Key<Bool>("saveRecents", default: true)
|
||||||
|
|
||||||
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
|
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
|
||||||
@@ -70,6 +82,12 @@ extension Defaults.Keys {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
static let enableBetaChannel = Key<Bool>("enableBetaChannel", default: false)
|
static let enableBetaChannel = Key<Bool>("enableBetaChannel", default: false)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
|
||||||
|
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||||
|
static let lockLandscapeWhenEnteringFullscreen = Key<Bool>("lockLandscapeWhenEnteringFullscreen", default: false)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
|
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
|
||||||
@@ -149,7 +167,11 @@ enum VisibleSection: String, CaseIterable, Comparable, Defaults.Serializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum WatchedVideoStyle: String, Defaults.Serializable {
|
enum WatchedVideoStyle: String, Defaults.Serializable {
|
||||||
case nothing, badge, decreasedOpacity
|
case nothing, badge, decreasedOpacity, both
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WatchedVideoBadgeColor: String, Defaults.Serializable {
|
||||||
|
case colorSchemeBased, red, blue
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WatchedVideoPlayNowBehavior: String, Defaults.Serializable {
|
enum WatchedVideoPlayNowBehavior: String, Defaults.Serializable {
|
||||||
|
|||||||
@@ -6,21 +6,29 @@ struct AccountsMenuView: View {
|
|||||||
|
|
||||||
@Default(.accounts) private var accounts
|
@Default(.accounts) private var accounts
|
||||||
@Default(.instances) private var instances
|
@Default(.instances) private var instances
|
||||||
|
@Default(.accountPickerDisplaysUsername) private var accountPickerDisplaysUsername
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Menu {
|
Menu {
|
||||||
ForEach(allAccounts, id: \.id) { account in
|
ForEach(allAccounts, id: \.id) { account in
|
||||||
Button(accountButtonTitle(account: account)) {
|
Button {
|
||||||
model.setCurrent(account)
|
model.setCurrent(account)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text(accountButtonTitle(account: account))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if model.current == account {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
if #available(iOS 15.0, macOS 12.0, *) {
|
HStack {
|
||||||
label
|
Image(systemName: "person.crop.circle")
|
||||||
.labelStyle(.titleAndIcon)
|
if accountPickerDisplaysUsername {
|
||||||
} else {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "person.crop.circle")
|
|
||||||
label
|
label
|
||||||
.labelStyle(.titleOnly)
|
.labelStyle(.titleOnly)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,16 @@ struct AppSidebarNavigation: View {
|
|||||||
"Current User: \(accounts.current?.description ?? "Not set")"
|
"Current User: \(accounts.current?.description ?? "Not set")"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
ToolbarItem(placement: .navigation) {
|
||||||
|
Button {
|
||||||
|
NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
|
||||||
|
} label: {
|
||||||
|
Label("Toggle Sidebar", systemImage: "sidebar.left")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ struct AppSidebarPlaylists: View {
|
|||||||
}
|
}
|
||||||
.id(playlist.id)
|
.id(playlist.id)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button("Add to queue...") {
|
Button("Play All") {
|
||||||
playlists.find(id: playlist.id)?.videos.forEach { video in
|
player.play(playlists.find(id: playlist.id)?.videos ?? [])
|
||||||
player.enqueueVideo(video)
|
}
|
||||||
}
|
Button("Shuffle All") {
|
||||||
|
player.play(playlists.find(id: playlist.id)?.videos ?? [], shuffling: true)
|
||||||
}
|
}
|
||||||
Button("Edit") {
|
Button("Edit") {
|
||||||
navigation.presentEditPlaylistForm(playlists.find(id: playlist.id))
|
navigation.presentEditPlaylistForm(playlists.find(id: playlist.id))
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ struct ContentView: View {
|
|||||||
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
if Defaults[.lockPortraitWhenBrowsing] {
|
||||||
|
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if let account = accounts.lastUsed ??
|
if let account = accounts.lastUsed ??
|
||||||
instances.lastUsed?.anonymousAccount ??
|
instances.lastUsed?.anonymousAccount ??
|
||||||
InstancesModel.all.first?.anonymousAccount
|
InstancesModel.all.first?.anonymousAccount
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ struct CommentsView: View {
|
|||||||
Text("No comments")
|
Text("No comments")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
} else if !comments.loaded {
|
} else if !comments.loaded {
|
||||||
progressView
|
PlaceholderProgressView()
|
||||||
.onAppear {
|
.onAppear {
|
||||||
comments.load()
|
comments.load()
|
||||||
}
|
}
|
||||||
@@ -60,19 +60,6 @@ struct CommentsView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var progressView: some View {
|
|
||||||
VStack {
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
ProgressView()
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CommentsView_Previews: PreviewProvider {
|
struct CommentsView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ struct PlaybackBar: View {
|
|||||||
return "LIVE"
|
return "LIVE"
|
||||||
}
|
}
|
||||||
|
|
||||||
guard player.time != nil, player.time!.isValid, !player.currentVideo.isNil else {
|
guard !player.isLoadingVideo else {
|
||||||
return "loading..."
|
return "loading..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ struct PlayerQueueView: View {
|
|||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
@Default(.saveHistory) private var saveHistory
|
@Default(.saveHistory) private var saveHistory
|
||||||
|
@Default(.showHistoryInPlayer) private var showHistoryInPlayer
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
@@ -21,7 +22,7 @@ struct PlayerQueueView: View {
|
|||||||
if sidebarQueue {
|
if sidebarQueue {
|
||||||
related
|
related
|
||||||
}
|
}
|
||||||
if saveHistory {
|
if saveHistory, showHistoryInPlayer {
|
||||||
playedPreviously
|
playedPreviously
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,9 +66,15 @@ final class PlayerViewController: UIViewController {
|
|||||||
if CommentsModel.enabled {
|
if CommentsModel.enabled {
|
||||||
infoViewControllers.append(infoViewController([.comments], title: "Comments"))
|
infoViewControllers.append(infoViewController([.comments], title: "Comments"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var queueSections = [NowPlayingView.ViewSection.playingNext]
|
||||||
|
if Defaults[.showHistoryInPlayer] {
|
||||||
|
queueSections.append(.playedPreviously)
|
||||||
|
}
|
||||||
|
|
||||||
infoViewControllers.append(contentsOf: [
|
infoViewControllers.append(contentsOf: [
|
||||||
infoViewController([.related], title: "Related"),
|
infoViewController([.related], title: "Related"),
|
||||||
infoViewController([.playingNext, .playedPreviously], title: "Playing Next")
|
infoViewController(queueSections, title: "Queue")
|
||||||
])
|
])
|
||||||
|
|
||||||
playerView.customInfoViewControllers = infoViewControllers
|
playerView.customInfoViewControllers = infoViewControllers
|
||||||
@@ -126,17 +132,32 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
|||||||
func playerViewController(
|
func playerViewController(
|
||||||
_: AVPlayerViewController,
|
_: AVPlayerViewController,
|
||||||
willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
||||||
) {}
|
) {
|
||||||
|
playerModel.playingFullscreen = true
|
||||||
|
}
|
||||||
|
|
||||||
func playerViewController(
|
func playerViewController(
|
||||||
_: AVPlayerViewController,
|
_: AVPlayerViewController,
|
||||||
willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
||||||
) {
|
) {
|
||||||
|
let wasPlaying = playerModel.isPlaying
|
||||||
coordinator.animate(alongsideTransition: nil) { context in
|
coordinator.animate(alongsideTransition: nil) { context in
|
||||||
|
#if os(iOS)
|
||||||
|
if wasPlaying {
|
||||||
|
self.playerModel.play()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if !context.isCancelled {
|
if !context.isCancelled {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if self.traitCollection.verticalSizeClass == .compact {
|
self.playerModel.lockedOrientation = nil
|
||||||
self.dismiss(animated: true)
|
if Defaults[.enterFullscreenInLandscape] {
|
||||||
|
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.playerModel.playingFullscreen = false
|
||||||
|
|
||||||
|
if wasPlaying {
|
||||||
|
self.playerModel.play()
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,8 +97,12 @@ struct VideoDetails: View {
|
|||||||
|
|
||||||
switch currentPage {
|
switch currentPage {
|
||||||
case .info:
|
case .info:
|
||||||
ScrollView(.vertical) {
|
if player.isLoadingVideo {
|
||||||
detailsPage
|
PlaceholderProgressView()
|
||||||
|
} else {
|
||||||
|
ScrollView(.vertical) {
|
||||||
|
detailsPage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .queue:
|
case .queue:
|
||||||
PlayerQueueView(sidebarQueue: $sidebarQueue, fullScreen: $fullScreen)
|
PlayerQueueView(sidebarQueue: $sidebarQueue, fullScreen: $fullScreen)
|
||||||
@@ -470,7 +474,7 @@ struct VideoDetails: View {
|
|||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.background(Color("VideoDetailLikesSymbolColor"))
|
.background(Color("KeywordBackgroundColor"))
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import AVKit
|
import AVKit
|
||||||
|
#if os(iOS)
|
||||||
|
import CoreMotion
|
||||||
|
#endif
|
||||||
import Defaults
|
import Defaults
|
||||||
import Siesta
|
import Siesta
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
@@ -22,6 +25,16 @@ struct VideoPlayerView: View {
|
|||||||
@Environment(\.presentationMode) private var presentationMode
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
|
|
||||||
|
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
|
||||||
|
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
|
||||||
|
@Default(.lockLandscapeWhenEnteringFullscreen) private var lockLandscapeWhenEnteringFullscreen
|
||||||
|
|
||||||
|
@State private var motionManager: CMMotionManager!
|
||||||
|
@State private var orientation = UIInterfaceOrientation.portrait
|
||||||
|
@State private var lastOrientation: UIInterfaceOrientation?
|
||||||
|
|
||||||
|
private var orientationThrottle = Throttle(interval: 2)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@@ -38,13 +51,36 @@ struct VideoPlayerView: View {
|
|||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
content
|
content
|
||||||
}
|
.onAppear {
|
||||||
.onAppear {
|
playerSize = geometry.size
|
||||||
self.playerSize = geometry.size
|
|
||||||
|
#if os(iOS)
|
||||||
|
configureOrientationUpdatesBasedOnAccelerometer()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: geometry.size) { size in
|
.onChange(of: geometry.size) { size in
|
||||||
self.playerSize = size
|
self.playerSize = size
|
||||||
}
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
|
||||||
|
handleOrientationDidChangeNotification()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
guard !player.playingFullscreen else {
|
||||||
|
return // swiftlint:disable:this implicit_return
|
||||||
|
}
|
||||||
|
|
||||||
|
if Defaults[.lockPortraitWhenBrowsing] {
|
||||||
|
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||||
|
} else {
|
||||||
|
Orientation.lockOrientation(.allButUpsideDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
motionManager?.stopAccelerometerUpdates()
|
||||||
|
motionManager = nil
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
#endif
|
#endif
|
||||||
@@ -192,6 +228,110 @@ struct VideoPlayerView: View {
|
|||||||
set: { _ in }
|
set: { _ in }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
private func configureOrientationUpdatesBasedOnAccelerometer() {
|
||||||
|
if UIDevice.current.orientation.isLandscape, enterFullscreenInLandscape, !player.playingFullscreen {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
player.enterFullScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !honorSystemOrientationLock, motionManager.isNil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
motionManager = CMMotionManager()
|
||||||
|
motionManager.accelerometerUpdateInterval = 0.2
|
||||||
|
motionManager.startAccelerometerUpdates(to: OperationQueue()) { data, _ in
|
||||||
|
guard player.presentingPlayer, !data.isNil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let acceleration = data?.acceleration else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var orientation = UIInterfaceOrientation.unknown
|
||||||
|
|
||||||
|
if acceleration.x >= 0.65 {
|
||||||
|
orientation = .landscapeLeft
|
||||||
|
} else if acceleration.x <= -0.65 {
|
||||||
|
orientation = .landscapeRight
|
||||||
|
} else if acceleration.y <= -0.65 {
|
||||||
|
orientation = .portrait
|
||||||
|
} else if acceleration.y >= 0.65 {
|
||||||
|
orientation = .portraitUpsideDown
|
||||||
|
}
|
||||||
|
|
||||||
|
guard lastOrientation != orientation else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastOrientation = orientation
|
||||||
|
|
||||||
|
if orientation.isLandscape {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||||
|
guard enterFullscreenInLandscape else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.enterFullScreen()
|
||||||
|
|
||||||
|
let orientationLockMask = orientation == .landscapeLeft ? UIInterfaceOrientationMask.landscapeLeft : .landscapeRight
|
||||||
|
|
||||||
|
Orientation.lockOrientation(orientationLockMask, andRotateTo: orientation)
|
||||||
|
|
||||||
|
guard lockLandscapeWhenEnteringFullscreen else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.lockedOrientation = orientation
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
guard abs(acceleration.z) <= 0.74,
|
||||||
|
player.lockedOrientation.isNil,
|
||||||
|
enterFullscreenInLandscape
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||||
|
player.exitFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
Orientation.lockOrientation(.portrait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleOrientationDidChangeNotification() {
|
||||||
|
let newOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
|
||||||
|
if newOrientation?.isLandscape ?? false, player.presentingPlayer, lockLandscapeWhenEnteringFullscreen, !player.lockedOrientation.isNil {
|
||||||
|
Orientation.lockOrientation(.landscape, andRotateTo: newOrientation)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard player.presentingPlayer, enterFullscreenInLandscape, honorSystemOrientationLock else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if UIDevice.current.orientation.isLandscape {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
player.lockedOrientation = newOrientation
|
||||||
|
player.enterFullScreen()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
player.exitFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||||
|
player.exitFullScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VideoPlayerView_Previews: PreviewProvider {
|
struct VideoPlayerView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -100,12 +100,12 @@ struct PlaylistsView: View {
|
|||||||
Text("No Playlists")
|
Text("No Playlists")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
} else {
|
} else {
|
||||||
Text("Current Playlist")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
selectPlaylistButton
|
selectPlaylistButton
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playButton
|
||||||
|
shuffleButton
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
newPlaylistButton
|
newPlaylistButton
|
||||||
@@ -142,23 +142,14 @@ struct PlaylistsView: View {
|
|||||||
selectPlaylistButton
|
selectPlaylistButton
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
|
||||||
player.playAll(items.compactMap(\.video))
|
|
||||||
player.show()
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 15) {
|
|
||||||
Image(systemName: "play.fill")
|
|
||||||
Text("Play All")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentPlaylist != nil {
|
|
||||||
editPlaylistButton
|
|
||||||
}
|
|
||||||
|
|
||||||
if let playlist = currentPlaylist {
|
if let playlist = currentPlaylist {
|
||||||
|
editPlaylistButton
|
||||||
|
|
||||||
FavoriteButton(item: FavoriteItem(section: .playlist(playlist.id)))
|
FavoriteButton(item: FavoriteItem(section: .playlist(playlist.id)))
|
||||||
.labelStyle(.iconOnly)
|
.labelStyle(.iconOnly)
|
||||||
|
|
||||||
|
playButton
|
||||||
|
shuffleButton
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -265,6 +256,22 @@ struct PlaylistsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var playButton: some View {
|
||||||
|
Button {
|
||||||
|
player.play(items.compactMap(\.video))
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "play")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var shuffleButton: some View {
|
||||||
|
Button {
|
||||||
|
player.play(items.compactMap(\.video), shuffling: true)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "shuffle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var currentPlaylist: Playlist? {
|
private var currentPlaylist: Playlist? {
|
||||||
model.find(id: selectedPlaylistID) ?? model.all.first
|
model.find(id: selectedPlaylistID) ?? model.all.first
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,7 @@ struct SearchSuggestions: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Button {
|
Button {
|
||||||
state.changeQuery { query in
|
runQueryAction()
|
||||||
query.query = state.queryText
|
|
||||||
state.fieldIsFocused = false
|
|
||||||
}
|
|
||||||
|
|
||||||
recents.addQuery(state.queryText)
|
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "magnifyingglass")
|
Image(systemName: "magnifyingglass")
|
||||||
@@ -20,28 +15,45 @@ struct SearchSuggestions: View {
|
|||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.vertical, 5)
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.onHover(perform: onHover(_:))
|
.onHover(perform: onHover(_:))
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
ForEach(visibleSuggestions, id: \.self) { suggestion in
|
ForEach(visibleSuggestions, id: \.self) { suggestion in
|
||||||
Button {
|
HStack {
|
||||||
state.queryText = suggestion
|
Button {
|
||||||
} label: {
|
state.queryText = suggestion
|
||||||
HStack {
|
runQueryAction()
|
||||||
Image(systemName: "arrow.up.left.circle")
|
} label: {
|
||||||
.foregroundColor(.secondary)
|
HStack {
|
||||||
HStack(spacing: 0) {
|
Image(systemName: "magnifyingglass")
|
||||||
Text(state.suggestionsText)
|
HStack(spacing: 0) {
|
||||||
.lineLimit(1)
|
Text(state.suggestionsText)
|
||||||
.layoutPriority(2)
|
.lineLimit(1)
|
||||||
.foregroundColor(.secondary)
|
.layoutPriority(2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Text(querySuffix(suggestion))
|
Text(querySuffix(suggestion))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.layoutPriority(1)
|
.layoutPriority(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button {
|
||||||
|
state.queryText = suggestion
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "arrow.up.left.circle")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 5)
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.onHover(perform: onHover(_:))
|
.onHover(perform: onHover(_:))
|
||||||
@@ -53,6 +65,15 @@ struct SearchSuggestions: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func runQueryAction() {
|
||||||
|
state.changeQuery { query in
|
||||||
|
query.query = state.queryText
|
||||||
|
state.fieldIsFocused = false
|
||||||
|
}
|
||||||
|
|
||||||
|
recents.addQuery(state.queryText)
|
||||||
|
}
|
||||||
|
|
||||||
private var visibleSuggestions: [String] {
|
private var visibleSuggestions: [String] {
|
||||||
state.querySuggestions.collection.filter {
|
state.querySuggestions.collection.filter {
|
||||||
$0.compare(state.queryText, options: .caseInsensitive) != .orderedSame
|
$0.compare(state.queryText, options: .caseInsensitive) != .orderedSame
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import Defaults
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct BrowsingSettings: View {
|
struct BrowsingSettings: View {
|
||||||
|
#if !os(tvOS)
|
||||||
|
@Default(.accountPickerDisplaysUsername) private var accountPickerDisplaysUsername
|
||||||
|
#endif
|
||||||
|
#if os(iOS)
|
||||||
|
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
|
||||||
|
#endif
|
||||||
@Default(.channelOnThumbnail) private var channelOnThumbnail
|
@Default(.channelOnThumbnail) private var channelOnThumbnail
|
||||||
@Default(.timeOnThumbnail) private var timeOnThumbnail
|
@Default(.timeOnThumbnail) private var timeOnThumbnail
|
||||||
@Default(.visibleSections) private var visibleSections
|
@Default(.visibleSections) private var visibleSections
|
||||||
@@ -9,6 +15,19 @@ struct BrowsingSettings: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
Section(header: SettingsHeader(text: "Browsing")) {
|
Section(header: SettingsHeader(text: "Browsing")) {
|
||||||
|
#if !os(tvOS)
|
||||||
|
Toggle("Show username in the account picker button", isOn: $accountPickerDisplaysUsername)
|
||||||
|
#endif
|
||||||
|
#if os(iOS)
|
||||||
|
Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing)
|
||||||
|
.onChange(of: lockPortraitWhenBrowsing) { lock in
|
||||||
|
if lock {
|
||||||
|
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
||||||
|
} else {
|
||||||
|
Orientation.lockOrientation(.allButUpsideDown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Toggle("Show channel name on thumbnail", isOn: $channelOnThumbnail)
|
Toggle("Show channel name on thumbnail", isOn: $channelOnThumbnail)
|
||||||
Toggle("Show video length on thumbnail", isOn: $timeOnThumbnail)
|
Toggle("Show video length on thumbnail", isOn: $timeOnThumbnail)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ struct HistorySettings: View {
|
|||||||
@Default(.showWatchingProgress) private var showWatchingProgress
|
@Default(.showWatchingProgress) private var showWatchingProgress
|
||||||
@Default(.watchedThreshold) private var watchedThreshold
|
@Default(.watchedThreshold) private var watchedThreshold
|
||||||
@Default(.watchedVideoStyle) private var watchedVideoStyle
|
@Default(.watchedVideoStyle) private var watchedVideoStyle
|
||||||
|
@Default(.watchedVideoBadgeColor) private var watchedVideoBadgeColor
|
||||||
@Default(.watchedVideoPlayNowBehavior) private var watchedVideoPlayNowBehavior
|
@Default(.watchedVideoPlayNowBehavior) private var watchedVideoPlayNowBehavior
|
||||||
|
@Default(.resetWatchedStatusOnPlaying) private var resetWatchedStatusOnPlaying
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
@@ -26,14 +28,18 @@ struct HistorySettings: View {
|
|||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
watchedThresholdPicker
|
watchedThresholdPicker
|
||||||
watchedVideoStylePicker
|
watchedVideoStylePicker
|
||||||
|
watchedVideoBadgeColorPicker
|
||||||
watchedVideoPlayNowBehaviorPicker
|
watchedVideoPlayNowBehaviorPicker
|
||||||
|
resetWatchedStatusOnPlayingToggle
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
watchedThresholdPicker
|
watchedThresholdPicker
|
||||||
watchedVideoStylePicker
|
watchedVideoStylePicker
|
||||||
|
watchedVideoBadgeColorPicker
|
||||||
watchedVideoPlayNowBehaviorPicker
|
watchedVideoPlayNowBehaviorPicker
|
||||||
|
resetWatchedStatusOnPlayingToggle
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@@ -68,6 +74,7 @@ struct HistorySettings: View {
|
|||||||
Text("Nothing").tag(WatchedVideoStyle.nothing)
|
Text("Nothing").tag(WatchedVideoStyle.nothing)
|
||||||
Text("Badge").tag(WatchedVideoStyle.badge)
|
Text("Badge").tag(WatchedVideoStyle.badge)
|
||||||
Text("Decreased opacity").tag(WatchedVideoStyle.decreasedOpacity)
|
Text("Decreased opacity").tag(WatchedVideoStyle.decreasedOpacity)
|
||||||
|
Text("Badge & Decreased opacity").tag(WatchedVideoStyle.both)
|
||||||
}
|
}
|
||||||
.disabled(!saveHistory)
|
.disabled(!saveHistory)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
@@ -80,6 +87,25 @@ struct HistorySettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var watchedVideoBadgeColorPicker: some View {
|
||||||
|
Section(header: header("Badge color")) {
|
||||||
|
Picker("Badge color", selection: $watchedVideoBadgeColor) {
|
||||||
|
Text("Based on system color scheme").tag(WatchedVideoBadgeColor.colorSchemeBased)
|
||||||
|
Text("Blue").tag(WatchedVideoBadgeColor.blue)
|
||||||
|
Text("Red").tag(WatchedVideoBadgeColor.red)
|
||||||
|
}
|
||||||
|
.disabled(!saveHistory)
|
||||||
|
.disabled(watchedVideoStyle == .decreasedOpacity)
|
||||||
|
.labelsHidden()
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
.pickerStyle(.automatic)
|
||||||
|
#elseif os(tvOS)
|
||||||
|
.pickerStyle(.inline)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var watchedVideoPlayNowBehaviorPicker: some View {
|
private var watchedVideoPlayNowBehaviorPicker: some View {
|
||||||
Section(header: header("When partially watched video is played")) {
|
Section(header: header("When partially watched video is played")) {
|
||||||
Picker("When partially watched video is played", selection: $watchedVideoPlayNowBehavior) {
|
Picker("When partially watched video is played", selection: $watchedVideoPlayNowBehavior) {
|
||||||
@@ -97,6 +123,10 @@ struct HistorySettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var resetWatchedStatusOnPlayingToggle: some View {
|
||||||
|
Toggle("Reset watched status when playing again", isOn: $resetWatchedStatusOnPlaying)
|
||||||
|
}
|
||||||
|
|
||||||
private var clearHistoryButton: some View {
|
private var clearHistoryButton: some View {
|
||||||
Button("Clear History") {
|
Button("Clear History") {
|
||||||
presentingClearHistoryConfirmation = true
|
presentingClearHistoryConfirmation = true
|
||||||
|
|||||||
@@ -6,9 +6,15 @@ struct PlaybackSettings: View {
|
|||||||
@Default(.playerInstanceID) private var playerInstanceID
|
@Default(.playerInstanceID) private var playerInstanceID
|
||||||
@Default(.quality) private var quality
|
@Default(.quality) private var quality
|
||||||
@Default(.playerSidebar) private var playerSidebar
|
@Default(.playerSidebar) private var playerSidebar
|
||||||
|
@Default(.showHistoryInPlayer) private var showHistory
|
||||||
@Default(.showKeywords) private var showKeywords
|
@Default(.showKeywords) private var showKeywords
|
||||||
@Default(.showChannelSubscribers) private var channelSubscribers
|
@Default(.showChannelSubscribers) private var channelSubscribers
|
||||||
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
||||||
|
#if os(iOS)
|
||||||
|
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
|
||||||
|
@Default(.lockLandscapeWhenEnteringFullscreen) private var lockLandscapeWhenEnteringFullscreen
|
||||||
|
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
|
||||||
|
#endif
|
||||||
@Default(.closePiPOnNavigation) private var closePiPOnNavigation
|
@Default(.closePiPOnNavigation) private var closePiPOnNavigation
|
||||||
@Default(.closePiPOnOpeningPlayer) private var closePiPOnOpeningPlayer
|
@Default(.closePiPOnOpeningPlayer) private var closePiPOnOpeningPlayer
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
@@ -33,8 +39,16 @@ struct PlaybackSettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keywordsToggle
|
keywordsToggle
|
||||||
|
showHistoryToggle
|
||||||
channelSubscribersToggle
|
channelSubscribersToggle
|
||||||
pauseOnHidingPlayerToggle
|
pauseOnHidingPlayerToggle
|
||||||
|
|
||||||
|
if idiom == .pad {
|
||||||
|
enterFullscreenInLandscapeToggle
|
||||||
|
}
|
||||||
|
|
||||||
|
honorSystemOrientationLockToggle
|
||||||
|
lockLandscapeWhenEnteringFullscreenToggle
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: SettingsHeader(text: "Picture in Picture")) {
|
Section(header: SettingsHeader(text: "Picture in Picture")) {
|
||||||
@@ -58,6 +72,7 @@ struct PlaybackSettings: View {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
keywordsToggle
|
keywordsToggle
|
||||||
|
showHistoryToggle
|
||||||
channelSubscribersToggle
|
channelSubscribersToggle
|
||||||
pauseOnHidingPlayerToggle
|
pauseOnHidingPlayerToggle
|
||||||
|
|
||||||
@@ -132,6 +147,10 @@ struct PlaybackSettings: View {
|
|||||||
Toggle("Show video keywords", isOn: $showKeywords)
|
Toggle("Show video keywords", isOn: $showKeywords)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var showHistoryToggle: some View {
|
||||||
|
Toggle("Show history of videos", isOn: $showHistory)
|
||||||
|
}
|
||||||
|
|
||||||
private var channelSubscribersToggle: some View {
|
private var channelSubscribersToggle: some View {
|
||||||
Toggle("Show channel subscribers count", isOn: $channelSubscribers)
|
Toggle("Show channel subscribers count", isOn: $channelSubscribers)
|
||||||
}
|
}
|
||||||
@@ -140,6 +159,22 @@ struct PlaybackSettings: View {
|
|||||||
Toggle("Pause when player is closed", isOn: $pauseOnHidingPlayer)
|
Toggle("Pause when player is closed", isOn: $pauseOnHidingPlayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
private var honorSystemOrientationLockToggle: some View {
|
||||||
|
Toggle("Honor system orientation lock", isOn: $honorSystemOrientationLock)
|
||||||
|
.disabled(!enterFullscreenInLandscape)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var enterFullscreenInLandscapeToggle: some View {
|
||||||
|
Toggle("Enter fullscreen in landscape", isOn: $enterFullscreenInLandscape)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lockLandscapeWhenEnteringFullscreenToggle: some View {
|
||||||
|
Toggle("Lock landscape orientation when entering fullscreen", isOn: $lockLandscapeWhenEnteringFullscreen)
|
||||||
|
.disabled(!enterFullscreenInLandscape)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
private var closePiPOnNavigationToggle: some View {
|
private var closePiPOnNavigationToggle: some View {
|
||||||
Toggle("Close PiP when starting playing other video", isOn: $closePiPOnNavigation)
|
Toggle("Close PiP when starting playing other video", isOn: $closePiPOnNavigation)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ struct SettingsView: View {
|
|||||||
.tag(Tabs.updates)
|
.tag(Tabs.updates)
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.frame(width: 400, height: 380)
|
.frame(width: 400, height: 400)
|
||||||
#else
|
#else
|
||||||
NavigationView {
|
NavigationView {
|
||||||
List {
|
List {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ struct VideoCell: View {
|
|||||||
@Default(.saveHistory) private var saveHistory
|
@Default(.saveHistory) private var saveHistory
|
||||||
@Default(.showWatchingProgress) private var showWatchingProgress
|
@Default(.showWatchingProgress) private var showWatchingProgress
|
||||||
@Default(.watchedVideoStyle) private var watchedVideoStyle
|
@Default(.watchedVideoStyle) private var watchedVideoStyle
|
||||||
|
@Default(.watchedVideoBadgeColor) private var watchedVideoBadgeColor
|
||||||
@Default(.watchedVideoPlayNowBehavior) private var watchedVideoPlayNowBehavior
|
@Default(.watchedVideoPlayNowBehavior) private var watchedVideoPlayNowBehavior
|
||||||
|
|
||||||
@FetchRequest private var watchRequest: FetchedResults<Watch>
|
@FetchRequest private var watchRequest: FetchedResults<Watch>
|
||||||
@@ -112,7 +113,7 @@ struct VideoCell: View {
|
|||||||
private var contentOpacity: Double {
|
private var contentOpacity: Double {
|
||||||
guard saveHistory,
|
guard saveHistory,
|
||||||
!watch.isNil,
|
!watch.isNil,
|
||||||
watchedVideoStyle == .decreasedOpacity
|
watchedVideoStyle == .decreasedOpacity || watchedVideoStyle == .both
|
||||||
else {
|
else {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -290,7 +291,7 @@ struct VideoCell: View {
|
|||||||
thumbnailImage
|
thumbnailImage
|
||||||
if saveHistory, showWatchingProgress, watch?.progress ?? 0 > 0 {
|
if saveHistory, showWatchingProgress, watch?.progress ?? 0 > 0 {
|
||||||
ProgressView(value: watch!.progress, total: 100)
|
ProgressView(value: watch!.progress, total: 100)
|
||||||
.progressViewStyle(LinearProgressViewStyle(tint: Color("WatchProgressBarColor")))
|
.progressViewStyle(LinearProgressViewStyle(tint: Color("AppRedColor")))
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
#else
|
#else
|
||||||
@@ -328,11 +329,14 @@ struct VideoCell: View {
|
|||||||
|
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
if saveHistory,
|
if saveHistory,
|
||||||
watchedVideoStyle == .badge,
|
watchedVideoStyle == .badge || watchedVideoStyle == .both,
|
||||||
watch?.finished ?? false
|
watch?.finished ?? false
|
||||||
{
|
{
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.foregroundColor(Color("WatchProgressBarColor"))
|
.foregroundColor(Color(
|
||||||
|
watchedVideoBadgeColor == .colorSchemeBased ? "WatchProgressBarColor" :
|
||||||
|
watchedVideoBadgeColor == .red ? "AppRedColor" : "AppBlueColor"
|
||||||
|
))
|
||||||
.background(Color.white)
|
.background(Color.white)
|
||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
|
|||||||
@@ -10,13 +10,10 @@ struct ChannelPlaylistView: View {
|
|||||||
@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
|
||||||
#if os(iOS)
|
|
||||||
@Environment(\.inNavigationView) private var inNavigationView
|
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
var items: [ContentItem] {
|
var items: [ContentItem] {
|
||||||
ContentItem.array(of: store.item?.videos ?? [])
|
ContentItem.array(of: store.item?.videos ?? [])
|
||||||
@@ -54,6 +51,11 @@ struct ChannelPlaylistView: View {
|
|||||||
|
|
||||||
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
|
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
|
||||||
.labelStyle(.iconOnly)
|
.labelStyle(.iconOnly)
|
||||||
|
|
||||||
|
playButton
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
shuffleButton
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
VerticalCells(items: items)
|
VerticalCells(items: items)
|
||||||
@@ -70,7 +72,9 @@ struct ChannelPlaylistView: View {
|
|||||||
resource?.addObserver(store)
|
resource?.addObserver(store)
|
||||||
resource?.loadIfNeeded()
|
resource?.loadIfNeeded()
|
||||||
}
|
}
|
||||||
#if !os(tvOS)
|
#if os(tvOS)
|
||||||
|
.background(Color.background(scheme: colorScheme))
|
||||||
|
#else
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigation) {
|
ToolbarItem(placement: .navigation) {
|
||||||
ShareButton(
|
ShareButton(
|
||||||
@@ -80,19 +84,50 @@ struct ChannelPlaylistView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem {
|
ToolbarItem(placement: playlistButtonsPlacement) {
|
||||||
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
|
HStack {
|
||||||
|
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
|
||||||
|
|
||||||
|
playButton
|
||||||
|
shuffleButton
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(playlist.title)
|
.navigationTitle(playlist.title)
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.navigationBarHidden(player.playerNavigationLinkActive)
|
.navigationBarHidden(player.playerNavigationLinkActive)
|
||||||
#endif
|
#endif
|
||||||
#else
|
|
||||||
.background(Color.background(scheme: colorScheme))
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var playlistButtonsPlacement: ToolbarItemPlacement {
|
||||||
|
#if os(iOS)
|
||||||
|
.navigationBarTrailing
|
||||||
|
#else
|
||||||
|
.automatic
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var playButton: some View {
|
||||||
|
Button {
|
||||||
|
player.play(videos, inNavigationView: inNavigationView)
|
||||||
|
} label: {
|
||||||
|
Label("Play All", systemImage: "play")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var shuffleButton: some View {
|
||||||
|
Button {
|
||||||
|
player.play(videos, shuffling: true, inNavigationView: inNavigationView)
|
||||||
|
} label: {
|
||||||
|
Label("Shuffle", systemImage: "shuffle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var videos: [Video] {
|
||||||
|
items.compactMap(\.video)
|
||||||
|
}
|
||||||
|
|
||||||
private var contentItem: ContentItem {
|
private var contentItem: ContentItem {
|
||||||
ContentItem(playlist: playlist)
|
ContentItem(playlist: playlist)
|
||||||
}
|
}
|
||||||
|
|||||||
21
Shared/Views/PlaceholderProgressView.swift
Normal file
21
Shared/Views/PlaceholderProgressView.swift
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PlaceholderProgressView: View {
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
ProgressView()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PlaceholderProgressView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
PlaceholderProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,25 +4,54 @@ import SwiftUI
|
|||||||
struct PlaylistVideosView: View {
|
struct PlaylistVideosView: View {
|
||||||
let playlist: Playlist
|
let playlist: Playlist
|
||||||
|
|
||||||
var videos: [ContentItem] {
|
@Environment(\.inNavigationView) private var inNavigationView
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
|
var contentItems: [ContentItem] {
|
||||||
ContentItem.array(of: playlist.videos)
|
ContentItem.array(of: playlist.videos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var videos: [Video] {
|
||||||
|
contentItems.compactMap(\.video)
|
||||||
|
}
|
||||||
|
|
||||||
init(_ playlist: Playlist) {
|
init(_ playlist: Playlist) {
|
||||||
self.playlist = playlist
|
self.playlist = playlist
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
PlayerControlsView {
|
PlayerControlsView {
|
||||||
VerticalCells(items: videos)
|
VerticalCells(items: contentItems)
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.navigationTitle("\(playlist.title) Playlist")
|
.navigationTitle("\(playlist.title) Playlist")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem {
|
ToolbarItem(placement: playlistButtonsPlacement) {
|
||||||
FavoriteButton(item: FavoriteItem(section: .playlist(playlist.id)))
|
HStack {
|
||||||
|
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
|
||||||
|
|
||||||
|
Button {
|
||||||
|
player.play(videos, inNavigationView: inNavigationView)
|
||||||
|
} label: {
|
||||||
|
Label("Play All", systemImage: "play")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
player.play(videos, shuffling: true, inNavigationView: inNavigationView)
|
||||||
|
} label: {
|
||||||
|
Label("Shuffle", systemImage: "shuffle")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var playlistButtonsPlacement: ToolbarItemPlacement {
|
||||||
|
#if os(iOS)
|
||||||
|
.navigationBarTrailing
|
||||||
|
#else
|
||||||
|
.automatic
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ struct YatteeApp: App {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
@StateObject private var updater = UpdaterModel()
|
@StateObject private var updater = UpdaterModel()
|
||||||
|
#elseif os(iOS)
|
||||||
|
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@StateObject private var accounts = AccountsModel()
|
@StateObject private var accounts = AccountsModel()
|
||||||
|
|||||||
@@ -206,6 +206,9 @@
|
|||||||
3765917E27237D2A009F956E /* PINCache in Frameworks */ = {isa = PBXBuildFile; productRef = 3765917D27237D2A009F956E /* PINCache */; };
|
3765917E27237D2A009F956E /* PINCache in Frameworks */ = {isa = PBXBuildFile; productRef = 3765917D27237D2A009F956E /* PINCache */; };
|
||||||
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37666BA927023AF000F869E5 /* AccountSelectionView.swift */; };
|
37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37666BA927023AF000F869E5 /* AccountSelectionView.swift */; };
|
||||||
3766AFD2273DA97D00686348 /* Int+FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA796D26DC412E002A0235 /* Int+FormatTests.swift */; };
|
3766AFD2273DA97D00686348 /* Int+FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA796D26DC412E002A0235 /* Int+FormatTests.swift */; };
|
||||||
|
3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */; };
|
||||||
|
3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */; };
|
||||||
|
3769C0302779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */; };
|
||||||
376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
||||||
376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
376A33E12720CAD6000C1D6B /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
||||||
376A33E22720CAD6000C1D6B /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
376A33E22720CAD6000C1D6B /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
||||||
@@ -364,6 +367,8 @@
|
|||||||
37B2631A2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B263192735EAAB00FE0D40 /* FavoriteResourceObserver.swift */; };
|
37B2631A2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B263192735EAAB00FE0D40 /* FavoriteResourceObserver.swift */; };
|
||||||
37B2631B2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B263192735EAAB00FE0D40 /* FavoriteResourceObserver.swift */; };
|
37B2631B2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B263192735EAAB00FE0D40 /* FavoriteResourceObserver.swift */; };
|
||||||
37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B263192735EAAB00FE0D40 /* FavoriteResourceObserver.swift */; };
|
37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B263192735EAAB00FE0D40 /* FavoriteResourceObserver.swift */; };
|
||||||
|
37B4E803277D0A72004BF56A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B4E802277D0A72004BF56A /* AppDelegate.swift */; };
|
||||||
|
37B4E805277D0AB4004BF56A /* Orientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B4E804277D0AB4004BF56A /* Orientation.swift */; };
|
||||||
37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
||||||
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
||||||
37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
|
||||||
@@ -653,6 +658,7 @@
|
|||||||
376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; };
|
376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; };
|
||||||
376578902685490700D4EA09 /* PlaylistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsView.swift; sourceTree = "<group>"; };
|
376578902685490700D4EA09 /* PlaylistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsView.swift; sourceTree = "<group>"; };
|
||||||
37666BA927023AF000F869E5 /* AccountSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSelectionView.swift; sourceTree = "<group>"; };
|
37666BA927023AF000F869E5 /* AccountSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSelectionView.swift; sourceTree = "<group>"; };
|
||||||
|
3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderProgressView.swift; sourceTree = "<group>"; };
|
||||||
376A33DF2720CAD6000C1D6B /* VideosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosApp.swift; sourceTree = "<group>"; };
|
376A33DF2720CAD6000C1D6B /* VideosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosApp.swift; sourceTree = "<group>"; };
|
||||||
376A33E32720CB35000C1D6B /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
|
376A33E32720CB35000C1D6B /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
|
||||||
376B2E0626F920D600B1D64D /* SignInRequiredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInRequiredView.swift; sourceTree = "<group>"; };
|
376B2E0626F920D600B1D64D /* SignInRequiredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInRequiredView.swift; sourceTree = "<group>"; };
|
||||||
@@ -695,6 +701,8 @@
|
|||||||
37B044B626F7AB9000E1419D /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
37B044B626F7AB9000E1419D /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoContextMenuView.swift; sourceTree = "<group>"; };
|
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoContextMenuView.swift; sourceTree = "<group>"; };
|
||||||
37B263192735EAAB00FE0D40 /* FavoriteResourceObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteResourceObserver.swift; sourceTree = "<group>"; };
|
37B263192735EAAB00FE0D40 /* FavoriteResourceObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteResourceObserver.swift; sourceTree = "<group>"; };
|
||||||
|
37B4E802277D0A72004BF56A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
37B4E804277D0AB4004BF56A /* Orientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Orientation.swift; sourceTree = "<group>"; };
|
||||||
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerModel.swift; sourceTree = "<group>"; };
|
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerModel.swift; sourceTree = "<group>"; };
|
||||||
37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenURLHandler.swift; sourceTree = "<group>"; };
|
37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenURLHandler.swift; sourceTree = "<group>"; };
|
||||||
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSizeModifier.swift; sourceTree = "<group>"; };
|
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSizeModifier.swift; sourceTree = "<group>"; };
|
||||||
@@ -961,6 +969,7 @@
|
|||||||
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */,
|
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */,
|
||||||
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */,
|
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */,
|
||||||
37E70922271CD43000D34DDE /* WelcomeScreen.swift */,
|
37E70922271CD43000D34DDE /* WelcomeScreen.swift */,
|
||||||
|
3769C02D2779F18600DDB3EA /* PlaceholderProgressView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1096,6 +1105,8 @@
|
|||||||
37992DC826CC50CD003D4C27 /* iOS */ = {
|
37992DC826CC50CD003D4C27 /* iOS */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
37B4E802277D0A72004BF56A /* AppDelegate.swift */,
|
||||||
|
37B4E804277D0AB4004BF56A /* Orientation.swift */,
|
||||||
3784B23A272894DA00B09468 /* ShareSheet.swift */,
|
3784B23A272894DA00B09468 /* ShareSheet.swift */,
|
||||||
37992DC726CC50BC003D4C27 /* Info.plist */,
|
37992DC726CC50BC003D4C27 /* Info.plist */,
|
||||||
);
|
);
|
||||||
@@ -1843,6 +1854,7 @@
|
|||||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||||
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||||
37BC50A82778A84700510953 /* HistorySettings.swift in Sources */,
|
37BC50A82778A84700510953 /* HistorySettings.swift in Sources */,
|
||||||
|
37B4E805277D0AB4004BF56A /* Orientation.swift in Sources */,
|
||||||
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
||||||
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||||
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
|
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
|
||||||
@@ -1867,6 +1879,7 @@
|
|||||||
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */,
|
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */,
|
||||||
37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||||
376578892685471400D4EA09 /* Playlist.swift in Sources */,
|
376578892685471400D4EA09 /* Playlist.swift in Sources */,
|
||||||
|
37B4E803277D0A72004BF56A /* AppDelegate.swift in Sources */,
|
||||||
373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||||
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||||
3782B9522755667600990149 /* String+Format.swift in Sources */,
|
3782B9522755667600990149 /* String+Format.swift in Sources */,
|
||||||
@@ -1938,6 +1951,7 @@
|
|||||||
37E70927271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
37E70927271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
||||||
371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */,
|
||||||
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
|
||||||
|
3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
||||||
3758638A2721B0A9000CB14E /* ChannelCell.swift in Sources */,
|
3758638A2721B0A9000CB14E /* ChannelCell.swift in Sources */,
|
||||||
37001563271B1F250049C794 /* AccountsModel.swift in Sources */,
|
37001563271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||||
37CC3F50270D010D00608308 /* VideoBanner.swift in Sources */,
|
37CC3F50270D010D00608308 /* VideoBanner.swift in Sources */,
|
||||||
@@ -2108,6 +2122,7 @@
|
|||||||
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||||
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||||
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||||
|
3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
||||||
37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */,
|
37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */,
|
||||||
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||||
);
|
);
|
||||||
@@ -2196,6 +2211,7 @@
|
|||||||
37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||||
3700155D271B0D4D0049C794 /* PipedAPI.swift in Sources */,
|
3700155D271B0D4D0049C794 /* PipedAPI.swift in Sources */,
|
||||||
375DFB5A26F9DA010013F468 /* InstancesModel.swift in Sources */,
|
375DFB5A26F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||||
|
3769C0302779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
||||||
37F4AE7426828F0900BD60EA /* VerticalCells.swift in Sources */,
|
37F4AE7426828F0900BD60EA /* VerticalCells.swift in Sources */,
|
||||||
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
||||||
37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */,
|
37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */,
|
||||||
@@ -2350,7 +2366,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 = 12;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -2384,7 +2400,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 = 12;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -2416,7 +2432,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 = 12;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
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";
|
||||||
@@ -2448,7 +2464,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 = 12;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
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";
|
||||||
@@ -2611,14 +2627,15 @@
|
|||||||
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 = 12;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = iOS/Info.plist;
|
INFOPLIST_FILE = iOS/Info.plist;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
@@ -2642,14 +2659,15 @@
|
|||||||
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 = 12;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = iOS/Info.plist;
|
INFOPLIST_FILE = iOS/Info.plist;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
INFOPLIST_KEY_UIRequiresFullScreen = YES;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
@@ -2677,7 +2695,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 = 12;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -2710,7 +2728,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 = 12;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -2841,7 +2859,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 = 12;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -2873,7 +2891,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 = 12;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
|||||||
17
iOS/AppDelegate.swift
Normal file
17
iOS/AppDelegate.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
var orientationLock = UIInterfaceOrientationMask.all
|
||||||
|
|
||||||
|
private(set) static var instance: AppDelegate!
|
||||||
|
|
||||||
|
func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask {
|
||||||
|
orientationLock
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection
|
||||||
|
AppDelegate.instance = self
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
31
iOS/Orientation.swift
Normal file
31
iOS/Orientation.swift
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import CoreMotion
|
||||||
|
import Defaults
|
||||||
|
import Logging
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct Orientation {
|
||||||
|
static var logger = Logger(label: "stream.yattee.orientation")
|
||||||
|
|
||||||
|
static func lockOrientation(_ orientation: UIInterfaceOrientationMask) {
|
||||||
|
if let delegate = AppDelegate.instance {
|
||||||
|
delegate.orientationLock = orientation
|
||||||
|
|
||||||
|
let orientationString = orientation == .portrait ? "portrait" : orientation == .landscapeLeft ? "landscapeLeft" :
|
||||||
|
orientation == .landscapeRight ? "landscapeRight" : orientation == .portraitUpsideDown ? "portraitUpsideDown" :
|
||||||
|
orientation == .landscape ? "landscape" : orientation == .all ? "all" : "allButUpsideDown"
|
||||||
|
|
||||||
|
logger.info("locking \(orientationString)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) {
|
||||||
|
lockOrientation(orientation)
|
||||||
|
|
||||||
|
guard !rotateOrientation.isNil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UIDevice.current.setValue(rotateOrientation!.rawValue, forKey: "orientation")
|
||||||
|
UINavigationController.attemptRotationToDeviceOrientation()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ struct UpdatesSettings: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
CheckForUpdatesView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,9 +68,13 @@ struct NowPlayingView: View {
|
|||||||
VideoBanner(video: item.video)
|
VideoBanner(video: item.video)
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button("Delete", role: .destructive) {
|
Button("Remove", role: .destructive) {
|
||||||
player.remove(item)
|
player.remove(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button("Remove All", role: .destructive) {
|
||||||
|
player.removeQueueItems()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,7 +132,7 @@ struct NowPlayingView: View {
|
|||||||
if sections.contains(.comments) {
|
if sections.contains(.comments) {
|
||||||
if !comments.loaded {
|
if !comments.loaded {
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
progressView
|
PlaceholderProgressView()
|
||||||
.onAppear {
|
.onAppear {
|
||||||
comments.load()
|
comments.load()
|
||||||
}
|
}
|
||||||
@@ -153,19 +157,6 @@ struct NowPlayingView: View {
|
|||||||
private var visibleWatches: [Watch] {
|
private var visibleWatches: [Watch] {
|
||||||
watches.filter { $0.videoID != player.currentVideo?.videoID }
|
watches.filter { $0.videoID != player.currentVideo?.videoID }
|
||||||
}
|
}
|
||||||
|
|
||||||
private var progressView: some View {
|
|
||||||
VStack {
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
ProgressView()
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NowPlayingView_Previews: PreviewProvider {
|
struct NowPlayingView_Previews: PreviewProvider {
|
||||||
|
|||||||
Reference in New Issue
Block a user