Compare commits

..

15 Commits

Author SHA1 Message Date
Arkadiusz Fal
5498e2c4ab Bump build number 2022-01-02 22:38:56 +01:00
Arkadiusz Fal
00778b585f Add iOS options for handling landscape fullscreen (fixes #38) 2022-01-02 22:38:56 +01:00
Arkadiusz Fal
d6e75295e1 Add iOS option to lock portrait mode in browsing 2022-01-02 20:50:59 +01:00
Arkadiusz Fal
aec7480353 Add Play/Shuffle All buttons to playlists context menu 2022-01-02 20:50:59 +01:00
Arkadiusz Fal
e29982454b Add options for history: badge color and reset watched status on playing 2022-01-02 20:50:59 +01:00
Arkadiusz Fal
117057dd0e Add option to show/hide history of videos in player queue view 2022-01-02 20:50:59 +01:00
Arkadiusz Fal
9ede4b9b1f Add option to show/hide username in account picker button 2022-01-02 20:50:58 +01:00
Arkadiusz Fal
f0d1b74e34 Add Toggle Sidebar button for macOS 2022-01-02 20:46:02 +01:00
Arkadiusz Fal
2a75d0a1d4 Improve search suggestions layout, add separate button for search/append 2022-01-02 20:46:02 +01:00
Arkadiusz Fal
04df9551ba Add Play/Shuffle All for playlists (fixes #39)
Add Remove All from queue button on tvOS
2022-01-02 20:46:02 +01:00
Arkadiusz Fal
ba21583a95 Add Check for Updates button in macOS settings 2022-01-02 20:46:00 +01:00
Arkadiusz Fal
149607efbc Fix reporting player item duration to Now Playing 2021-12-29 20:20:09 +01:00
Arkadiusz Fal
89957e3b56 Better UI handling for loading video details (fixes #46) 2021-12-29 19:55:41 +01:00
Arkadiusz Fal
0af2db2fd7 Fix keywords background color 2021-12-29 19:40:25 +01:00
Arkadiusz Fal
ab174c73fd Extract progress view, show video details loading 2021-12-29 19:39:38 +01:00
37 changed files with 859 additions and 157 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = {}) {

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -21,6 +21,8 @@ struct UpdatesSettings: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
Spacer() Spacer()
CheckForUpdatesView()
} }
} }

View File

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