mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
Improve history, resume videos, mark watched videos (fixes #42)
This commit is contained in:
parent
adcebb77a5
commit
ac1c6685a1
@ -32,7 +32,6 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
|
|||||||
|
|
||||||
player.currentItem = PlayerQueueItem(Video.fixture)
|
player.currentItem = PlayerQueueItem(Video.fixture)
|
||||||
player.queue = Video.allFixtures.map { PlayerQueueItem($0) }
|
player.queue = Video.allFixtures.map { PlayerQueueItem($0) }
|
||||||
player.history = player.queue
|
|
||||||
|
|
||||||
return player
|
return player
|
||||||
}
|
}
|
||||||
|
73
Model/HistoryModel.swift
Normal file
73
Model/HistoryModel.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import CoreData
|
||||||
|
import CoreMedia
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension PlayerModel {
|
||||||
|
func historyVideo(_ id: String) -> Video? {
|
||||||
|
historyVideos.first { $0.videoID == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadHistoryVideoDetails(_ id: Video.ID) {
|
||||||
|
guard historyVideo(id).isNil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts.api.video(id).load().onSuccess { [weak self] response in
|
||||||
|
guard let video: Video = response.typedContent() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self?.historyVideos.append(video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateWatch(finished: Bool = false) {
|
||||||
|
guard let id = currentVideo?.videoID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let time = player.currentTime().seconds
|
||||||
|
|
||||||
|
let watch: Watch!
|
||||||
|
let watchFetchRequest = Watch.fetchRequest()
|
||||||
|
watchFetchRequest.predicate = NSPredicate(format: "videoID = %@", id as String)
|
||||||
|
|
||||||
|
let results = try? context.fetch(watchFetchRequest)
|
||||||
|
|
||||||
|
if results?.isEmpty ?? true {
|
||||||
|
if time < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
watch = Watch(context: context)
|
||||||
|
watch.videoID = id
|
||||||
|
} else {
|
||||||
|
watch = results?.first
|
||||||
|
}
|
||||||
|
|
||||||
|
if let seconds = playerItemDuration?.seconds {
|
||||||
|
watch.videoDuration = seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
if finished {
|
||||||
|
watch.stoppedAt = watch.videoDuration
|
||||||
|
} else if time.isFinite, time > 0 {
|
||||||
|
watch.stoppedAt = time
|
||||||
|
}
|
||||||
|
|
||||||
|
watch.watchedAt = Date()
|
||||||
|
|
||||||
|
try? context.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeWatch(_ watch: Watch) {
|
||||||
|
context.delete(watch)
|
||||||
|
try? context.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeAllWatches() {
|
||||||
|
let watchesFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Watch")
|
||||||
|
let deleteRequest = NSBatchDeleteRequest(fetchRequest: watchesFetchRequest)
|
||||||
|
_ = try? context.execute(deleteRequest)
|
||||||
|
_ = try? context.save()
|
||||||
|
}
|
||||||
|
}
|
47
Model/PersistenceController.swift
Normal file
47
Model/PersistenceController.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import CoreData
|
||||||
|
|
||||||
|
struct PersistenceController {
|
||||||
|
static let shared = PersistenceController()
|
||||||
|
|
||||||
|
static var preview: PersistenceController = {
|
||||||
|
let result = PersistenceController(inMemory: true)
|
||||||
|
let viewContext = result.container.viewContext
|
||||||
|
|
||||||
|
do {
|
||||||
|
try viewContext.save()
|
||||||
|
} catch {
|
||||||
|
// Replace this implementation with code to handle the error appropriately.
|
||||||
|
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||||
|
let nsError = error as NSError
|
||||||
|
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}()
|
||||||
|
|
||||||
|
let container: NSPersistentContainer
|
||||||
|
|
||||||
|
init(inMemory: Bool = false) {
|
||||||
|
container = NSPersistentContainer(name: "Yattee")
|
||||||
|
if inMemory {
|
||||||
|
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
||||||
|
}
|
||||||
|
container.viewContext.automaticallyMergesChangesFromParent = true
|
||||||
|
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||||
|
container.loadPersistentStores { _, error in
|
||||||
|
if let error = error as NSError? {
|
||||||
|
// Replace this implementation with code to handle the error appropriately.
|
||||||
|
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Typical reasons for an error here include:
|
||||||
|
* The parent directory does not exist, cannot be created, or disallows writing.
|
||||||
|
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
|
||||||
|
* The device is out of space.
|
||||||
|
* The store could not be migrated to the current model version.
|
||||||
|
Check the error message to determine what the actual problem was.
|
||||||
|
*/
|
||||||
|
fatalError("Unresolved error \(error), \(error.userInfo)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import AVKit
|
import AVKit
|
||||||
|
import CoreData
|
||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
@ -27,8 +28,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
||||||
|
|
||||||
@Published var queue = [PlayerQueueItem]() { didSet { Defaults[.queue] = queue } }
|
@Published var queue = [PlayerQueueItem]() { didSet { Defaults[.queue] = queue } }
|
||||||
@Published var currentItem: PlayerQueueItem! { didSet { Defaults[.lastPlayed] = currentItem } }
|
@Published var currentItem: PlayerQueueItem!
|
||||||
@Published var history = [PlayerQueueItem]() { didSet { Defaults[.history] = history } }
|
@Published var historyVideos = [Video]()
|
||||||
|
|
||||||
@Published var preservedTime: CMTime?
|
@Published var preservedTime: CMTime?
|
||||||
|
|
||||||
@ -47,6 +48,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
var composition = AVMutableComposition()
|
var composition = AVMutableComposition()
|
||||||
var loadedCompositionAssets = [AVMediaType]()
|
var loadedCompositionAssets = [AVMediaType]()
|
||||||
|
|
||||||
|
var context: NSManagedObjectContext = PersistenceController.shared.container.viewContext
|
||||||
|
|
||||||
private var currentArtwork: MPMediaItemArtwork?
|
private var currentArtwork: MPMediaItemArtwork?
|
||||||
private var frequentTimeObserver: Any?
|
private var frequentTimeObserver: Any?
|
||||||
private var infrequentTimeObserver: Any?
|
private var infrequentTimeObserver: Any?
|
||||||
@ -131,7 +134,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var live: Bool {
|
var live: Bool {
|
||||||
currentItem?.video?.live ?? false
|
currentVideo?.live ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerItemDuration: CMTime? {
|
var playerItemDuration: CMTime? {
|
||||||
@ -162,9 +165,17 @@ final class PlayerModel: ObservableObject {
|
|||||||
player.pause()
|
player.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
func upgradeToStream(_ stream: Stream) {
|
func play(_ video: Video, at time: TimeInterval? = nil, inNavigationView: Bool = false) {
|
||||||
if !self.stream.isNil, self.stream != stream {
|
playNow(video, at: time)
|
||||||
playStream(stream, of: currentVideo!, preservingTime: true, upgrading: true)
|
|
||||||
|
guard !playingInPictureInPicture else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if inNavigationView {
|
||||||
|
playerNavigationLinkActive = true
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,6 +216,12 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func upgradeToStream(_ stream: Stream) {
|
||||||
|
if !self.stream.isNil, self.stream != stream {
|
||||||
|
playStream(stream, of: currentVideo!, preservingTime: true, upgrading: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func handleAvailableStreamsChange() {
|
private func handleAvailableStreamsChange() {
|
||||||
rebuildTVMenu()
|
rebuildTVMenu()
|
||||||
|
|
||||||
@ -469,18 +486,20 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func itemDidPlayToEndTime() {
|
@objc func itemDidPlayToEndTime() {
|
||||||
currentItem.playbackTime = playerItemDuration
|
prepareCurrentItemForHistory(finished: true)
|
||||||
|
|
||||||
if queue.isEmpty {
|
if queue.isEmpty {
|
||||||
#if !os(macOS)
|
#if !os(macOS)
|
||||||
try? AVAudioSession.sharedInstance().setActive(false)
|
try? AVAudioSession.sharedInstance().setActive(false)
|
||||||
#endif
|
#endif
|
||||||
addCurrentItemToHistory()
|
|
||||||
resetQueue()
|
resetQueue()
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
controller?.dismiss(animated: true)
|
controller?.playerView.dismiss(animated: false) { [weak self] in
|
||||||
#endif
|
self?.controller?.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
#else
|
||||||
hide()
|
hide()
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
advanceToNextItem()
|
advanceToNextItem()
|
||||||
}
|
}
|
||||||
@ -551,7 +570,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.timeObserverThrottle.execute {
|
self.timeObserverThrottle.execute {
|
||||||
self.updateCurrentItemIntervals()
|
self.updateWatch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -581,26 +600,15 @@ final class PlayerModel: ObservableObject {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
self.timeObserverThrottle.execute {
|
self.timeObserverThrottle.execute {
|
||||||
self.updateCurrentItemIntervals()
|
self.updateWatch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateCurrentItemIntervals() {
|
|
||||||
currentItem?.playbackTime = player.currentTime()
|
|
||||||
currentItem?.videoDuration = player.currentItem?.asset.duration.seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func updateNowPlayingInfo() {
|
fileprivate func updateNowPlayingInfo() {
|
||||||
var duration: Int?
|
|
||||||
if !currentItem.video.live {
|
|
||||||
let itemDuration = currentItem.videoDuration ?? 0
|
|
||||||
duration = itemDuration.isFinite ? Int(itemDuration) : nil
|
|
||||||
}
|
|
||||||
var nowPlayingInfo: [String: AnyObject] = [
|
var nowPlayingInfo: [String: AnyObject] = [
|
||||||
MPMediaItemPropertyTitle: currentItem.video.title as AnyObject,
|
MPMediaItemPropertyTitle: currentItem.video.title as AnyObject,
|
||||||
MPMediaItemPropertyArtist: currentItem.video.author as AnyObject,
|
MPMediaItemPropertyArtist: currentItem.video.author as AnyObject,
|
||||||
MPMediaItemPropertyPlaybackDuration: duration as AnyObject,
|
|
||||||
MPNowPlayingInfoPropertyIsLiveStream: currentItem.video.live as AnyObject,
|
MPNowPlayingInfoPropertyIsLiveStream: currentItem.video.live as AnyObject,
|
||||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime().seconds as AnyObject,
|
MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime().seconds as AnyObject,
|
||||||
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
||||||
@ -611,6 +619,15 @@ final class PlayerModel: ObservableObject {
|
|||||||
nowPlayingInfo[MPMediaItemPropertyArtwork] = currentArtwork as AnyObject
|
nowPlayingInfo[MPMediaItemPropertyArtwork] = currentArtwork as AnyObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !currentItem.video.live {
|
||||||
|
let itemDuration = currentItem.videoDuration ?? 0
|
||||||
|
let duration = itemDuration.isFinite ? Int(itemDuration) : nil
|
||||||
|
|
||||||
|
if !duration.isNil {
|
||||||
|
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration as AnyObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -647,7 +664,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
if let channel: Channel = response.typedContent() {
|
if let channel: Channel = response.typedContent() {
|
||||||
self?.channelWithDetails = channel
|
self?.channelWithDetails = channel
|
||||||
withAnimation {
|
withAnimation {
|
||||||
self?.currentItem.video.channel = channel
|
self?.currentItem?.video.channel = channel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -671,7 +688,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func closeCurrentItem() {
|
func closeCurrentItem() {
|
||||||
addCurrentItemToHistory()
|
prepareCurrentItemForHistory()
|
||||||
currentItem = nil
|
currentItem = nil
|
||||||
player.replaceCurrentItem(with: nil)
|
player.replaceCurrentItem(with: nil)
|
||||||
}
|
}
|
||||||
|
@ -29,11 +29,11 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func playNow(_ video: Video, at time: TimeInterval? = nil) {
|
func playNow(_ video: Video, at time: TimeInterval? = nil) {
|
||||||
if !playingInPictureInPicture || closePiPOnNavigation {
|
if playingInPictureInPicture, closePiPOnNavigation {
|
||||||
closePiP()
|
closePiP()
|
||||||
}
|
}
|
||||||
|
|
||||||
addCurrentItemToHistory()
|
prepareCurrentItemForHistory()
|
||||||
|
|
||||||
enqueueVideo(video, prepending: true) { _, item in
|
enqueueVideo(video, prepending: true) { _, item in
|
||||||
self.advanceToItem(item, at: time)
|
self.advanceToItem(item, at: time)
|
||||||
@ -83,7 +83,7 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func advanceToNextItem() {
|
func advanceToNextItem() {
|
||||||
addCurrentItemToHistory()
|
prepareCurrentItemForHistory()
|
||||||
|
|
||||||
if let nextItem = queue.first {
|
if let nextItem = queue.first {
|
||||||
advanceToItem(nextItem)
|
advanceToItem(nextItem)
|
||||||
@ -91,7 +91,7 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) {
|
func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) {
|
||||||
addCurrentItemToHistory()
|
prepareCurrentItemForHistory()
|
||||||
|
|
||||||
remove(newItem)
|
remove(newItem)
|
||||||
|
|
||||||
@ -148,18 +148,13 @@ extension PlayerModel {
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
func addCurrentItemToHistory() {
|
func prepareCurrentItemForHistory(finished: Bool = false) {
|
||||||
if let item = currentItem, Defaults[.saveHistory] {
|
if !currentItem.isNil, Defaults[.saveHistory] {
|
||||||
addItemToHistory(item)
|
if let video = currentVideo, !historyVideos.contains(where: { $0 == video }) {
|
||||||
|
historyVideos.append(video)
|
||||||
}
|
}
|
||||||
|
updateWatch(finished: finished)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addItemToHistory(_ item: PlayerQueueItem) {
|
|
||||||
if let index = history.firstIndex(where: { $0.video?.videoID == item.video?.videoID }) {
|
|
||||||
history.remove(at: index)
|
|
||||||
}
|
|
||||||
|
|
||||||
history.insert(currentItem, at: 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func playHistory(_ item: PlayerQueueItem) {
|
func playHistory(_ item: PlayerQueueItem) {
|
||||||
@ -172,67 +167,9 @@ extension PlayerModel {
|
|||||||
let newItem = enqueueVideo(item.video, atTime: time, prepending: true)
|
let newItem = enqueueVideo(item.video, atTime: time, prepending: true)
|
||||||
|
|
||||||
advanceToItem(newItem!)
|
advanceToItem(newItem!)
|
||||||
|
|
||||||
if let historyItemIndex = history.firstIndex(of: item) {
|
|
||||||
history.remove(at: historyItemIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult func removeHistory(_ item: PlayerQueueItem) -> PlayerQueueItem? {
|
|
||||||
if let index = history.firstIndex(where: { $0 == item }) {
|
|
||||||
return history.remove(at: index)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeQueueItems() {
|
func removeQueueItems() {
|
||||||
queue.removeAll()
|
queue.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeHistoryItems() {
|
|
||||||
history.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadHistoryDetails() {
|
|
||||||
guard !accounts.current.isNil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
queue = Defaults[.queue]
|
|
||||||
queue.forEach { item in
|
|
||||||
accounts.api.loadDetails(item) { newItem in
|
|
||||||
if let index = self.queue.firstIndex(where: { $0.id == item.id }) {
|
|
||||||
self.queue[index] = newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var savedHistory = Defaults[.history]
|
|
||||||
|
|
||||||
if let lastPlayed = Defaults[.lastPlayed] {
|
|
||||||
if let index = savedHistory.firstIndex(where: { $0.videoID == lastPlayed.videoID }) {
|
|
||||||
var updatedLastPlayed = savedHistory[index]
|
|
||||||
|
|
||||||
updatedLastPlayed.playbackTime = lastPlayed.playbackTime
|
|
||||||
updatedLastPlayed.videoDuration = lastPlayed.videoDuration
|
|
||||||
|
|
||||||
savedHistory.remove(at: index)
|
|
||||||
savedHistory.insert(updatedLastPlayed, at: 0)
|
|
||||||
} else {
|
|
||||||
savedHistory.insert(lastPlayed, at: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
Defaults[.lastPlayed] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
history = savedHistory
|
|
||||||
history.forEach { item in
|
|
||||||
accounts.api.loadDetails(item) { newItem in
|
|
||||||
if let index = self.history.firstIndex(where: { $0.id == item.id }) {
|
|
||||||
self.history[index] = newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,15 @@ struct PlayerQueueItem: Hashable, Identifiable, Defaults.Serializable {
|
|||||||
var playbackTime: CMTime?
|
var playbackTime: CMTime?
|
||||||
var videoDuration: TimeInterval?
|
var videoDuration: TimeInterval?
|
||||||
|
|
||||||
|
static func from(_ watch: Watch, video: Video? = nil) -> Self {
|
||||||
|
.init(
|
||||||
|
video,
|
||||||
|
videoID: watch.videoID,
|
||||||
|
playbackTime: CMTime.secondsInDefaultTimescale(watch.stoppedAt),
|
||||||
|
videoDuration: watch.videoDuration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
init(_ video: Video? = nil, videoID: Video.ID? = nil, playbackTime: CMTime? = nil, videoDuration: TimeInterval? = nil) {
|
init(_ video: Video? = nil, videoID: Video.ID? = nil, playbackTime: CMTime? = nil, videoDuration: TimeInterval? = nil) {
|
||||||
self.video = video
|
self.video = video
|
||||||
self.videoID = videoID ?? video!.videoID
|
self.videoID = videoID ?? video!.videoID
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Alamofire
|
import Alamofire
|
||||||
import AVKit
|
import AVKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
struct Video: Identifiable, Equatable, Hashable {
|
struct Video: Identifiable, Equatable, Hashable {
|
||||||
@ -105,10 +106,24 @@ struct Video: Identifiable, Equatable, Hashable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: Video, rhs: Video) -> Bool {
|
static func == (lhs: Video, rhs: Video) -> Bool {
|
||||||
lhs.id == rhs.id
|
let videoIDIsEqual = lhs.videoID == rhs.videoID
|
||||||
|
|
||||||
|
if !lhs.indexID.isNil, !rhs.indexID.isNil {
|
||||||
|
return videoIDIsEqual && lhs.indexID == rhs.indexID
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoIDIsEqual
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var watchFetchRequest: FetchRequest<Watch> {
|
||||||
|
FetchRequest<Watch>(
|
||||||
|
entity: Watch.entity(),
|
||||||
|
sortDescriptors: [],
|
||||||
|
predicate: NSPredicate(format: "videoID = %@", videoID)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
47
Model/Watch.swift
Normal file
47
Model/Watch.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import CoreData
|
||||||
|
import Defaults
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc(Watch)
|
||||||
|
final class Watch: NSManagedObject, Identifiable {
|
||||||
|
@Default(.watchedThreshold) private var watchedThreshold
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Watch {
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<Watch> {
|
||||||
|
NSFetchRequest<Watch>(entityName: "Watch")
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged var videoID: String
|
||||||
|
@NSManaged var videoDuration: Double
|
||||||
|
|
||||||
|
@NSManaged var watchedAt: Date?
|
||||||
|
@NSManaged var stoppedAt: Double
|
||||||
|
|
||||||
|
var progress: Double {
|
||||||
|
guard videoDuration.isFinite, !videoDuration.isZero else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress = (stoppedAt / videoDuration) * 100
|
||||||
|
return min(max(progress, 0), 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
var finished: Bool {
|
||||||
|
progress >= Double(watchedThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
var watchedAtString: String? {
|
||||||
|
guard let watchedAt = watchedAt else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if watchedAt.timeIntervalSinceNow < 5 {
|
||||||
|
return "just now"
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatter = RelativeDateTimeFormatter()
|
||||||
|
formatter.unitsStyle = .full
|
||||||
|
return formatter.localizedString(for: watchedAt, relativeTo: Date())
|
||||||
|
}
|
||||||
|
}
|
17
Model/Yattee.xcdatamodeld/Yattee.xcdatamodel/contents
Normal file
17
Model/Yattee.xcdatamodeld/Yattee.xcdatamodel/contents
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21D5025f" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="1">
|
||||||
|
<entity name="Watch" representedClassName="Watch" syncable="YES">
|
||||||
|
<attribute name="stoppedAt" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="videoDuration" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="videoID" attributeType="String"/>
|
||||||
|
<attribute name="watchedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="videoID"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
|
<elements>
|
||||||
|
<element name="Watch" positionX="-63" positionY="-18" width="128" height="89"/>
|
||||||
|
</elements>
|
||||||
|
</model>
|
@ -5,9 +5,9 @@
|
|||||||
"color-space" : "display-p3",
|
"color-space" : "display-p3",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.781",
|
"blue" : "0.757",
|
||||||
"green" : "0.781",
|
"green" : "0.761",
|
||||||
"red" : "0.781"
|
"red" : "0.757"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
@ -23,9 +23,9 @@
|
|||||||
"color-space" : "display-p3",
|
"color-space" : "display-p3",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.311",
|
"blue" : "0.259",
|
||||||
"green" : "0.311",
|
"green" : "0.259",
|
||||||
"red" : "0.311"
|
"red" : "0.259"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "extended-gray",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"white" : "0.724"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "display-p3",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.328",
|
|
||||||
"green" : "0.328",
|
|
||||||
"red" : "0.325"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.537",
|
|
||||||
"green" : "0.522",
|
|
||||||
"red" : "1.000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.537",
|
|
||||||
"green" : "0.522",
|
|
||||||
"red" : "1.000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.329",
|
|
||||||
"green" : "0.224",
|
|
||||||
"red" : "0.043"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0.329",
|
|
||||||
"green" : "0.224",
|
|
||||||
"red" : "0.043"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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.638",
|
"blue" : "0.263",
|
||||||
"green" : "0.638",
|
"green" : "0.290",
|
||||||
"red" : "0.638"
|
"red" : "0.859"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
@ -23,9 +23,9 @@
|
|||||||
"color-space" : "display-p3",
|
"color-space" : "display-p3",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0.256",
|
"blue" : "0.263",
|
||||||
"green" : "0.256",
|
"green" : "0.290",
|
||||||
"red" : "0.253"
|
"red" : "0.859"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
@ -54,10 +54,12 @@ extension Defaults.Keys {
|
|||||||
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
|
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
|
||||||
|
|
||||||
static let queue = Key<[PlayerQueueItem]>("queue", default: [])
|
static let queue = Key<[PlayerQueueItem]>("queue", default: [])
|
||||||
static let history = Key<[PlayerQueueItem]>("history", default: [])
|
|
||||||
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
|
|
||||||
|
|
||||||
static let saveHistory = Key<Bool>("saveHistory", default: true)
|
static let saveHistory = Key<Bool>("saveHistory", default: true)
|
||||||
|
static let showWatchingProgress = Key<Bool>("showWatchingProgress", default: true)
|
||||||
|
static let watchedThreshold = Key<Int>("watchedThreshold", default: 90)
|
||||||
|
static let watchedVideoStyle = Key<WatchedVideoStyle>("watchedVideoStyle", default: .badge)
|
||||||
|
static let watchedVideoPlayNowBehavior = Key<WatchedVideoPlayNowBehavior>("watchedVideoPlayNowBehavior", default: .continue)
|
||||||
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)
|
||||||
@ -146,6 +148,14 @@ enum VisibleSection: String, CaseIterable, Comparable, Defaults.Serializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum WatchedVideoStyle: String, Defaults.Serializable {
|
||||||
|
case nothing, badge, decreasedOpacity
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WatchedVideoPlayNowBehavior: String, Defaults.Serializable {
|
||||||
|
case `continue`, restart
|
||||||
|
}
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
enum CommentsPlacement: String, CaseIterable, Defaults.Serializable {
|
enum CommentsPlacement: String, CaseIterable, Defaults.Serializable {
|
||||||
case info, separate
|
case info, separate
|
||||||
|
@ -84,6 +84,7 @@ struct ContentView: View {
|
|||||||
SettingsView()
|
SettingsView()
|
||||||
.environmentObject(accounts)
|
.environmentObject(accounts)
|
||||||
.environmentObject(instances)
|
.environmentObject(instances)
|
||||||
|
.environmentObject(player)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
#endif
|
#endif
|
||||||
@ -132,10 +133,6 @@ struct ContentView: View {
|
|||||||
player.accounts = accounts
|
player.accounts = accounts
|
||||||
player.comments = comments
|
player.comments = comments
|
||||||
|
|
||||||
if !accounts.current.isNil {
|
|
||||||
player.loadHistoryDetails()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !Defaults[.saveRecents] {
|
if !Defaults[.saveRecents] {
|
||||||
recents.clear()
|
recents.clear()
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ struct PlaybackBar: View {
|
|||||||
|
|
||||||
private var closeButton: some View {
|
private var closeButton: some View {
|
||||||
Button {
|
Button {
|
||||||
presentationMode.wrappedValue.dismiss()
|
player.hide()
|
||||||
} label: {
|
} label: {
|
||||||
Label(
|
Label(
|
||||||
"Close",
|
"Close",
|
||||||
|
@ -14,7 +14,7 @@ struct PlayerQueueRow: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
Button {
|
Button {
|
||||||
player.addCurrentItemToHistory()
|
player.prepareCurrentItemForHistory()
|
||||||
|
|
||||||
if history {
|
if history {
|
||||||
player.playHistory(item)
|
player.playHistory(item)
|
||||||
|
@ -6,6 +6,10 @@ struct PlayerQueueView: View {
|
|||||||
@Binding var sidebarQueue: Bool
|
@Binding var sidebarQueue: Bool
|
||||||
@Binding var fullScreen: Bool
|
@Binding var fullScreen: Bool
|
||||||
|
|
||||||
|
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
|
||||||
|
var watches: FetchedResults<Watch>
|
||||||
|
|
||||||
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
@Default(.saveHistory) private var saveHistory
|
@Default(.saveHistory) private var saveHistory
|
||||||
@ -46,22 +50,32 @@ struct PlayerQueueView: View {
|
|||||||
ForEach(player.queue) { item in
|
ForEach(player.queue) { item in
|
||||||
PlayerQueueRow(item: item, fullScreen: $fullScreen)
|
PlayerQueueRow(item: item, fullScreen: $fullScreen)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
removeButton(item, history: false)
|
removeButton(item)
|
||||||
removeAllButton(history: false)
|
removeAllButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var visibleWatches: [Watch] {
|
||||||
|
watches.filter { $0.videoID != player.currentVideo?.videoID }
|
||||||
|
}
|
||||||
|
|
||||||
var playedPreviously: some View {
|
var playedPreviously: some View {
|
||||||
Group {
|
Group {
|
||||||
if !player.history.isEmpty {
|
if !visibleWatches.isEmpty {
|
||||||
Section(header: Text("Played Previously")) {
|
Section(header: Text("Played Previously")) {
|
||||||
ForEach(player.history) { item in
|
ForEach(visibleWatches, id: \.videoID) { watch in
|
||||||
PlayerQueueRow(item: item, history: true, fullScreen: $fullScreen)
|
PlayerQueueRow(
|
||||||
|
item: PlayerQueueItem.from(watch, video: player.historyVideo(watch.videoID)),
|
||||||
|
history: true,
|
||||||
|
fullScreen: $fullScreen
|
||||||
|
)
|
||||||
|
.onAppear {
|
||||||
|
player.loadHistoryVideoDetails(watch.videoID)
|
||||||
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
removeButton(item, history: true)
|
removeHistoryButton(watch)
|
||||||
removeAllButton(history: true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,28 +108,28 @@ struct PlayerQueueView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeButton(_ item: PlayerQueueItem, history: Bool) -> some View {
|
private func removeButton(_ item: PlayerQueueItem) -> some View {
|
||||||
Button {
|
Button {
|
||||||
removeButtonAction(item, history: history)
|
player.remove(item)
|
||||||
} label: {
|
} label: {
|
||||||
Label("Remove", systemImage: "trash")
|
Label("Remove", systemImage: "trash")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeButtonAction(_ item: PlayerQueueItem, history: Bool) {
|
private func removeAllButton() -> some View {
|
||||||
_ = history ? player.removeHistory(item) : player.remove(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func removeAllButton(history: Bool) -> some View {
|
|
||||||
Button {
|
Button {
|
||||||
removeAllButtonAction(history: history)
|
player.removeQueueItems()
|
||||||
} label: {
|
} label: {
|
||||||
Label("Remove All", systemImage: "trash.fill")
|
Label("Remove All", systemImage: "trash.fill")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeAllButtonAction(history: Bool) {
|
private func removeHistoryButton(_ watch: Watch) -> some View {
|
||||||
_ = history ? player.removeHistoryItems() : player.removeQueueItems()
|
Button {
|
||||||
|
player.removeWatch(watch)
|
||||||
|
} label: {
|
||||||
|
Label("Remove", systemImage: "trash")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ struct VideoDetails: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var video: Video? {
|
var video: Video? {
|
||||||
player.currentItem?.video
|
player.currentVideo
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -423,7 +423,7 @@ struct VideoDetails: View {
|
|||||||
var detailsPage: some View {
|
var detailsPage: some View {
|
||||||
Group {
|
Group {
|
||||||
Group {
|
Group {
|
||||||
if let video = player.currentItem?.video {
|
if let video = player.currentVideo {
|
||||||
Group {
|
Group {
|
||||||
HStack {
|
HStack {
|
||||||
publishedDateSection
|
publishedDateSection
|
||||||
|
@ -4,8 +4,6 @@ import SwiftUI
|
|||||||
struct BrowsingSettings: View {
|
struct BrowsingSettings: View {
|
||||||
@Default(.channelOnThumbnail) private var channelOnThumbnail
|
@Default(.channelOnThumbnail) private var channelOnThumbnail
|
||||||
@Default(.timeOnThumbnail) private var timeOnThumbnail
|
@Default(.timeOnThumbnail) private var timeOnThumbnail
|
||||||
@Default(.saveRecents) private var saveRecents
|
|
||||||
@Default(.saveHistory) private var saveHistory
|
|
||||||
@Default(.visibleSections) private var visibleSections
|
@Default(.visibleSections) private var visibleSections
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -13,8 +11,6 @@ struct BrowsingSettings: View {
|
|||||||
Section(header: SettingsHeader(text: "Browsing")) {
|
Section(header: SettingsHeader(text: "Browsing")) {
|
||||||
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)
|
||||||
Toggle("Save recent queries and channels", isOn: $saveRecents)
|
|
||||||
Toggle("Save history of played videos", isOn: $saveHistory)
|
|
||||||
}
|
}
|
||||||
Section(header: SettingsHeader(text: "Sections")) {
|
Section(header: SettingsHeader(text: "Sections")) {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
142
Shared/Settings/HistorySettings.swift
Normal file
142
Shared/Settings/HistorySettings.swift
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct HistorySettings: View {
|
||||||
|
static let watchedThresholds = [50, 60, 70, 80, 90, 95, 100]
|
||||||
|
|
||||||
|
@State private var presentingClearHistoryConfirmation = false
|
||||||
|
|
||||||
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
|
@Default(.saveRecents) private var saveRecents
|
||||||
|
@Default(.saveHistory) private var saveHistory
|
||||||
|
@Default(.showWatchingProgress) private var showWatchingProgress
|
||||||
|
@Default(.watchedThreshold) private var watchedThreshold
|
||||||
|
@Default(.watchedVideoStyle) private var watchedVideoStyle
|
||||||
|
@Default(.watchedVideoPlayNowBehavior) private var watchedVideoPlayNowBehavior
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
Section(header: SettingsHeader(text: "History")) {
|
||||||
|
Toggle("Save recent queries and channels", isOn: $saveRecents)
|
||||||
|
Toggle("Save history of played videos", isOn: $saveHistory)
|
||||||
|
Toggle("Show progress of watching on thumbnails", isOn: $showWatchingProgress)
|
||||||
|
.disabled(!saveHistory)
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
watchedThresholdPicker
|
||||||
|
watchedVideoStylePicker
|
||||||
|
watchedVideoPlayNowBehaviorPicker
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
watchedThresholdPicker
|
||||||
|
watchedVideoStylePicker
|
||||||
|
watchedVideoPlayNowBehaviorPicker
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
Spacer()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
clearHistoryButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var watchedThresholdPicker: some View {
|
||||||
|
Section(header: header("Mark video as watched after playing")) {
|
||||||
|
Picker("Mark video as watched after playing", selection: $watchedThreshold) {
|
||||||
|
ForEach(Self.watchedThresholds, id: \.self) { threshold in
|
||||||
|
Text("\(threshold)%").tag(threshold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(!saveHistory)
|
||||||
|
.labelsHidden()
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
.pickerStyle(.automatic)
|
||||||
|
#elseif os(tvOS)
|
||||||
|
.pickerStyle(.inline)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var watchedVideoStylePicker: some View {
|
||||||
|
Section(header: header("Mark watched videos with")) {
|
||||||
|
Picker("Mark watched videos with", selection: $watchedVideoStyle) {
|
||||||
|
Text("Nothing").tag(WatchedVideoStyle.nothing)
|
||||||
|
Text("Badge").tag(WatchedVideoStyle.badge)
|
||||||
|
Text("Decreased opacity").tag(WatchedVideoStyle.decreasedOpacity)
|
||||||
|
}
|
||||||
|
.disabled(!saveHistory)
|
||||||
|
.labelsHidden()
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
.pickerStyle(.automatic)
|
||||||
|
#elseif os(tvOS)
|
||||||
|
.pickerStyle(.inline)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var watchedVideoPlayNowBehaviorPicker: some View {
|
||||||
|
Section(header: header("When partially watched video is played")) {
|
||||||
|
Picker("When partially watched video is played", selection: $watchedVideoPlayNowBehavior) {
|
||||||
|
Text("Continue").tag(WatchedVideoPlayNowBehavior.continue)
|
||||||
|
Text("Restart").tag(WatchedVideoPlayNowBehavior.restart)
|
||||||
|
}
|
||||||
|
.disabled(!saveHistory)
|
||||||
|
.labelsHidden()
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
.pickerStyle(.automatic)
|
||||||
|
#elseif os(tvOS)
|
||||||
|
.pickerStyle(.inline)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var clearHistoryButton: some View {
|
||||||
|
Button("Clear History") {
|
||||||
|
presentingClearHistoryConfirmation = true
|
||||||
|
}
|
||||||
|
.alert(isPresented: $presentingClearHistoryConfirmation) {
|
||||||
|
Alert(
|
||||||
|
title: Text(
|
||||||
|
"Are you sure you want to clear history of watched videos?"
|
||||||
|
),
|
||||||
|
message: Text(
|
||||||
|
"This cannot be undone. You might need to switch between views or restart the app to see changes."
|
||||||
|
),
|
||||||
|
primaryButton: .destructive(Text("Clear All")) {
|
||||||
|
player.removeAllWatches()
|
||||||
|
presentingClearHistoryConfirmation = false
|
||||||
|
},
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.disabled(!saveHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func header(_ text: String) -> some View {
|
||||||
|
#if os(iOS)
|
||||||
|
return EmptyView()
|
||||||
|
#elseif os(macOS)
|
||||||
|
return Text(text)
|
||||||
|
.opacity(saveHistory ? 1 : 0.3)
|
||||||
|
#else
|
||||||
|
return Text(text)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.opacity(saveHistory ? 1 : 0.2)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HistorySettings_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
HistorySettings()
|
||||||
|
.injectFixtureEnvironmentObjects()
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@ import SwiftUI
|
|||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
private enum Tabs: Hashable {
|
private enum Tabs: Hashable {
|
||||||
case instances, browsing, playback, services, updates
|
case instances, browsing, history, playback, services, updates
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -42,6 +42,14 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
.tag(Tabs.browsing)
|
.tag(Tabs.browsing)
|
||||||
|
|
||||||
|
Form {
|
||||||
|
HistorySettings()
|
||||||
|
}
|
||||||
|
.tabItem {
|
||||||
|
Label("History", systemImage: "clock.arrow.circlepath")
|
||||||
|
}
|
||||||
|
.tag(Tabs.history)
|
||||||
|
|
||||||
Form {
|
Form {
|
||||||
PlaybackSettings()
|
PlaybackSettings()
|
||||||
}
|
}
|
||||||
@ -89,6 +97,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BrowsingSettings()
|
BrowsingSettings()
|
||||||
|
HistorySettings()
|
||||||
PlaybackSettings()
|
PlaybackSettings()
|
||||||
ServicesSettings()
|
ServicesSettings()
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ struct VideoBanner: View {
|
|||||||
|
|
||||||
private var progressView: some View {
|
private var progressView: some View {
|
||||||
Group {
|
Group {
|
||||||
if !playbackTime.isNil {
|
if !playbackTime.isNil, !(video?.live ?? false) {
|
||||||
ProgressView(value: progressViewValue, total: progressViewTotal)
|
ProgressView(value: progressViewValue, total: progressViewTotal)
|
||||||
.progressViewStyle(.linear)
|
.progressViewStyle(.linear)
|
||||||
.frame(maxWidth: thumbnailWidth)
|
.frame(maxWidth: thumbnailWidth)
|
||||||
|
@ -3,7 +3,7 @@ import SDWebImageSwiftUI
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideoCell: View {
|
struct VideoCell: View {
|
||||||
var video: Video
|
private var video: Video
|
||||||
|
|
||||||
@Environment(\.inNavigationView) private var inNavigationView
|
@Environment(\.inNavigationView) private var inNavigationView
|
||||||
|
|
||||||
@ -18,25 +18,25 @@ struct VideoCell: View {
|
|||||||
|
|
||||||
@Default(.channelOnThumbnail) private var channelOnThumbnail
|
@Default(.channelOnThumbnail) private var channelOnThumbnail
|
||||||
@Default(.timeOnThumbnail) private var timeOnThumbnail
|
@Default(.timeOnThumbnail) private var timeOnThumbnail
|
||||||
|
@Default(.saveHistory) private var saveHistory
|
||||||
|
@Default(.showWatchingProgress) private var showWatchingProgress
|
||||||
|
@Default(.watchedVideoStyle) private var watchedVideoStyle
|
||||||
|
@Default(.watchedVideoPlayNowBehavior) private var watchedVideoPlayNowBehavior
|
||||||
|
|
||||||
|
@FetchRequest private var watchRequest: FetchedResults<Watch>
|
||||||
|
|
||||||
|
init(video: Video) {
|
||||||
|
self.video = video
|
||||||
|
_watchRequest = video.watchFetchRequest
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
Button(action: {
|
Button(action: playAction) {
|
||||||
player.playNow(video)
|
|
||||||
|
|
||||||
guard !player.playingInPictureInPicture else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if inNavigationView {
|
|
||||||
player.playerNavigationLinkActive = true
|
|
||||||
} else {
|
|
||||||
player.show()
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.opacity(contentOpacity)
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.contentShape(RoundedRectangle(cornerRadius: 12))
|
.contentShape(RoundedRectangle(cornerRadius: 12))
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
@ -48,7 +48,50 @@ struct VideoCell: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var content: some View {
|
private func playAction() {
|
||||||
|
if watchingNow {
|
||||||
|
if !player.playingInPictureInPicture {
|
||||||
|
player.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !playNowContinues {
|
||||||
|
player.player.seek(to: .zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
player.play()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var playAt: TimeInterval?
|
||||||
|
|
||||||
|
if playNowContinues,
|
||||||
|
!watch.isNil,
|
||||||
|
!watch!.finished
|
||||||
|
{
|
||||||
|
playAt = watch!.stoppedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
player.play(video, at: playAt, inNavigationView: inNavigationView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var playNowContinues: Bool {
|
||||||
|
watchedVideoPlayNowBehavior == .continue
|
||||||
|
}
|
||||||
|
|
||||||
|
private var watch: Watch? {
|
||||||
|
watchRequest.first
|
||||||
|
}
|
||||||
|
|
||||||
|
private var finished: Bool {
|
||||||
|
watch?.finished ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
private var watchingNow: Bool {
|
||||||
|
player.currentVideo == video
|
||||||
|
}
|
||||||
|
|
||||||
|
private var content: some View {
|
||||||
VStack {
|
VStack {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if verticalSizeClass == .compact, !horizontalCells {
|
if verticalSizeClass == .compact, !horizontalCells {
|
||||||
@ -66,8 +109,19 @@ struct VideoCell: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var contentOpacity: Double {
|
||||||
|
guard saveHistory,
|
||||||
|
!watch.isNil,
|
||||||
|
watchedVideoStyle == .decreasedOpacity
|
||||||
|
else {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return watch!.finished ? 0.5 : 1
|
||||||
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
var horizontalRow: some View {
|
private var horizontalRow: some View {
|
||||||
HStack(alignment: .top, spacing: 2) {
|
HStack(alignment: .top, spacing: 2) {
|
||||||
Section {
|
Section {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@ -151,7 +205,7 @@ struct VideoCell: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var verticalRow: some View {
|
private var verticalRow: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
thumbnail
|
thumbnail
|
||||||
|
|
||||||
@ -201,7 +255,7 @@ struct VideoCell: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let time = video.length.formattedAsPlaybackTime(), !timeOnThumbnail {
|
if let time = time, !timeOnThumbnail {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
@ -225,13 +279,30 @@ struct VideoCell: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var additionalDetailsAvailable: Bool {
|
private var additionalDetailsAvailable: Bool {
|
||||||
video.publishedDate != nil || video.views != 0 || (!timeOnThumbnail && !video.length.formattedAsPlaybackTime().isNil)
|
video.publishedDate != nil || video.views != 0 ||
|
||||||
|
(!timeOnThumbnail && !video.length.formattedAsPlaybackTime().isNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var thumbnail: some View {
|
private var thumbnail: some View {
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
|
ZStack(alignment: .bottomLeading) {
|
||||||
thumbnailImage
|
thumbnailImage
|
||||||
|
if saveHistory, showWatchingProgress, watch?.progress ?? 0 > 0 {
|
||||||
|
ProgressView(value: watch!.progress, total: 100)
|
||||||
|
.progressViewStyle(LinearProgressViewStyle(tint: Color("WatchProgressBarColor")))
|
||||||
|
#if os(tvOS)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
#else
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
#endif
|
||||||
|
#if os(macOS)
|
||||||
|
.offset(x: 0, y: 4)
|
||||||
|
#else
|
||||||
|
.offset(x: 0, y: -3)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
@ -247,24 +318,49 @@ struct VideoCell: View {
|
|||||||
DetailBadge(text: video.author, style: .prominent)
|
DetailBadge(text: video.author, style: .prominent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.padding(16)
|
||||||
|
#else
|
||||||
.padding(10)
|
.padding(10)
|
||||||
|
#endif
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .center) {
|
||||||
|
if saveHistory,
|
||||||
|
watchedVideoStyle == .badge,
|
||||||
|
watch?.finished ?? false
|
||||||
|
{
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(Color("WatchProgressBarColor"))
|
||||||
|
.background(Color.white)
|
||||||
|
.clipShape(Circle())
|
||||||
|
#if os(tvOS)
|
||||||
|
.font(.system(size: 40))
|
||||||
|
#else
|
||||||
|
.font(.system(size: 30))
|
||||||
|
#endif
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if timeOnThumbnail, let time = video.length.formattedAsPlaybackTime() {
|
if timeOnThumbnail,
|
||||||
|
!video.live,
|
||||||
|
let time = time
|
||||||
|
{
|
||||||
DetailBadge(text: time, style: .prominent)
|
DetailBadge(text: time, style: .prominent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.padding(16)
|
||||||
|
#else
|
||||||
.padding(10)
|
.padding(10)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var thumbnailImage: some View {
|
private var thumbnailImage: some View {
|
||||||
Group {
|
Group {
|
||||||
if let url = thumbnails.best(video) {
|
if let url = thumbnails.best(video) {
|
||||||
WebImage(url: url)
|
WebImage(url: url)
|
||||||
@ -293,7 +389,29 @@ struct VideoCell: View {
|
|||||||
.modifier(AspectRatioModifier())
|
.modifier(AspectRatioModifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoDetail(_ text: String, lineLimit: Int = 1) -> some View {
|
private var time: String? {
|
||||||
|
guard var videoTime = video.length.formattedAsPlaybackTime() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !saveHistory || !showWatchingProgress || watch?.finished ?? false {
|
||||||
|
return videoTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if let stoppedAt = watch?.stoppedAt,
|
||||||
|
stoppedAt.isFinite,
|
||||||
|
let stoppedAtFormatted = stoppedAt.formattedAsPlaybackTime()
|
||||||
|
{
|
||||||
|
if watch?.videoDuration ?? 0 > 0 {
|
||||||
|
videoTime = watch!.videoDuration.formattedAsPlaybackTime() ?? "?"
|
||||||
|
}
|
||||||
|
return "\(stoppedAtFormatted) / \(videoTime)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoTime
|
||||||
|
}
|
||||||
|
|
||||||
|
private func videoDetail(_ text: String, lineLimit: Int = 1) -> some View {
|
||||||
Text(text)
|
Text(text)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
.lineLimit(lineLimit)
|
.lineLimit(lineLimit)
|
||||||
@ -309,7 +427,10 @@ struct VideoCell: View {
|
|||||||
content
|
content
|
||||||
} else {
|
} else {
|
||||||
content
|
content
|
||||||
.aspectRatio(1.777, contentMode: .fill)
|
.aspectRatio(
|
||||||
|
VideoPlayerView.defaultAspectRatio,
|
||||||
|
contentMode: .fill
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,12 +31,12 @@ struct PlayerControlsView<Content: View>: View {
|
|||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
Text(model.currentItem?.video?.title ?? "Not playing")
|
Text(model.currentVideo?.title ?? "Not playing")
|
||||||
.font(.system(size: 14).bold())
|
.font(.system(size: 14).bold())
|
||||||
.foregroundColor(model.currentItem.isNil ? .secondary : .accentColor)
|
.foregroundColor(model.currentItem.isNil ? .secondary : .accentColor)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
Text(model.currentItem?.video?.author ?? "Yattee v\(appVersion) (build \(appBuild))")
|
Text(model.currentVideo?.author ?? "Yattee v\(appVersion) (build \(appBuild))")
|
||||||
.fontWeight(model.currentItem.isNil ? .light : .bold)
|
.fontWeight(model.currentItem.isNil ? .light : .bold)
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 10))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import CoreData
|
||||||
import Defaults
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@ -19,7 +20,35 @@ struct VideoContextMenuView: View {
|
|||||||
@EnvironmentObject<RecentsModel> private var recents
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
|
|
||||||
|
@FetchRequest private var watchRequest: FetchedResults<Watch>
|
||||||
|
|
||||||
|
@Default(.saveHistory) private var saveHistory
|
||||||
|
|
||||||
|
private var viewContext: NSManagedObjectContext = PersistenceController.shared.container.viewContext
|
||||||
|
|
||||||
|
init(video: Video, playerNavigationLinkActive: Binding<Bool>) {
|
||||||
|
self.video = video
|
||||||
|
_playerNavigationLinkActive = playerNavigationLinkActive
|
||||||
|
_watchRequest = video.watchFetchRequest
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
if saveHistory {
|
||||||
|
Section {
|
||||||
|
if let watchedAtString = watchedAtString {
|
||||||
|
Text(watchedAtString)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !watch.isNil, !watch!.finished, !watchingNow {
|
||||||
|
continueButton
|
||||||
|
}
|
||||||
|
|
||||||
|
if !watch.isNil, !watchingNow {
|
||||||
|
removeFromHistoryButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
playNowButton
|
playNowButton
|
||||||
}
|
}
|
||||||
@ -54,19 +83,49 @@ struct VideoContextMenuView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var playNowButton: some View {
|
private var watch: Watch? {
|
||||||
Button {
|
watchRequest.first
|
||||||
player.playNow(video)
|
}
|
||||||
|
|
||||||
guard !player.playingInPictureInPicture else {
|
private var watchingNow: Bool {
|
||||||
|
player.currentVideo == video
|
||||||
|
}
|
||||||
|
|
||||||
|
private var watchedAtString: String? {
|
||||||
|
if watchingNow {
|
||||||
|
return "Watching now"
|
||||||
|
}
|
||||||
|
|
||||||
|
if let watch = watch, let watchedAtString = watch.watchedAtString {
|
||||||
|
return "Watched \(watchedAtString)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private var continueButton: some View {
|
||||||
|
Button {
|
||||||
|
player.play(video, at: watch!.stoppedAt, inNavigationView: inNavigationView)
|
||||||
|
} label: {
|
||||||
|
Label("Continue from \(watch!.stoppedAt.formattedAsPlaybackTime() ?? "where I left off")", systemImage: "playpause")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeFromHistoryButton: some View {
|
||||||
|
Button {
|
||||||
|
guard let watch = watch else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if inNavigationView {
|
player.removeWatch(watch)
|
||||||
playerNavigationLinkActive = true
|
} label: {
|
||||||
} else {
|
Label("Remove from history", systemImage: "delete.left.fill")
|
||||||
player.show()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var playNowButton: some View {
|
||||||
|
Button {
|
||||||
|
player.play(video, inNavigationView: inNavigationView)
|
||||||
} label: {
|
} label: {
|
||||||
Label("Play Now", systemImage: "play")
|
Label("Play Now", systemImage: "play")
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,12 @@ struct YatteeApp: App {
|
|||||||
@StateObject private var subscriptions = SubscriptionsModel()
|
@StateObject private var subscriptions = SubscriptionsModel()
|
||||||
@StateObject private var thumbnails = ThumbnailsModel()
|
@StateObject private var thumbnails = ThumbnailsModel()
|
||||||
|
|
||||||
|
let persistenceController = PersistenceController.shared
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
|
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||||
.environmentObject(accounts)
|
.environmentObject(accounts)
|
||||||
.environmentObject(comments)
|
.environmentObject(comments)
|
||||||
.environmentObject(instances)
|
.environmentObject(instances)
|
||||||
@ -70,6 +73,7 @@ struct YatteeApp: App {
|
|||||||
VideoPlayerView()
|
VideoPlayerView()
|
||||||
.onAppear { player.presentingPlayer = true }
|
.onAppear { player.presentingPlayer = true }
|
||||||
.onDisappear { player.presentingPlayer = false }
|
.onDisappear { player.presentingPlayer = false }
|
||||||
|
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||||
.environment(\.navigationStyle, .sidebar)
|
.environment(\.navigationStyle, .sidebar)
|
||||||
.environmentObject(accounts)
|
.environmentObject(accounts)
|
||||||
.environmentObject(comments)
|
.environmentObject(comments)
|
||||||
@ -86,8 +90,10 @@ struct YatteeApp: App {
|
|||||||
|
|
||||||
Settings {
|
Settings {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
|
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||||
.environmentObject(accounts)
|
.environmentObject(accounts)
|
||||||
.environmentObject(instances)
|
.environmentObject(instances)
|
||||||
|
.environmentObject(player)
|
||||||
.environmentObject(updater)
|
.environmentObject(updater)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@ -59,6 +59,12 @@
|
|||||||
3711403F26B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
3711403F26B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
||||||
3711404026B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
3711404026B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
||||||
3711404126B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
3711404126B206A6005B3555 /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3711403E26B206A6005B3555 /* SearchModel.swift */; };
|
||||||
|
37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 37130A59277657090033018A /* Yattee.xcdatamodeld */; };
|
||||||
|
37130A5C277657090033018A /* Yattee.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 37130A59277657090033018A /* Yattee.xcdatamodeld */; };
|
||||||
|
37130A5D277657090033018A /* Yattee.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 37130A59277657090033018A /* Yattee.xcdatamodeld */; };
|
||||||
|
37130A5F277657300033018A /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37130A5E277657300033018A /* PersistenceController.swift */; };
|
||||||
|
37130A60277657300033018A /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37130A5E277657300033018A /* PersistenceController.swift */; };
|
||||||
|
37130A61277657300033018A /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37130A5E277657300033018A /* PersistenceController.swift */; };
|
||||||
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714166E267A8ACC006CA35D /* TrendingView.swift */; };
|
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714166E267A8ACC006CA35D /* TrendingView.swift */; };
|
||||||
37141670267A8ACC006CA35D /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714166E267A8ACC006CA35D /* TrendingView.swift */; };
|
37141670267A8ACC006CA35D /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714166E267A8ACC006CA35D /* TrendingView.swift */; };
|
||||||
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714166E267A8ACC006CA35D /* TrendingView.swift */; };
|
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3714166E267A8ACC006CA35D /* TrendingView.swift */; };
|
||||||
@ -298,6 +304,9 @@
|
|||||||
3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784B23A272894DA00B09468 /* ShareSheet.swift */; };
|
3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784B23A272894DA00B09468 /* ShareSheet.swift */; };
|
||||||
3784B23D2728B85300B09468 /* ShareButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784B23C2728B85300B09468 /* ShareButton.swift */; };
|
3784B23D2728B85300B09468 /* ShareButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784B23C2728B85300B09468 /* ShareButton.swift */; };
|
||||||
3784B23E2728B85300B09468 /* ShareButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784B23C2728B85300B09468 /* ShareButton.swift */; };
|
3784B23E2728B85300B09468 /* ShareButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784B23C2728B85300B09468 /* ShareButton.swift */; };
|
||||||
|
3784CDE227772EE40055BBF2 /* Watch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784CDDE27772EE40055BBF2 /* Watch.swift */; };
|
||||||
|
3784CDE327772EE40055BBF2 /* Watch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784CDDE27772EE40055BBF2 /* Watch.swift */; };
|
||||||
|
3784CDE427772EE40055BBF2 /* Watch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3784CDDE27772EE40055BBF2 /* Watch.swift */; };
|
||||||
3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */; };
|
3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */; };
|
||||||
3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */; };
|
3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */; };
|
||||||
3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */; };
|
3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */; };
|
||||||
@ -387,6 +396,12 @@
|
|||||||
37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA42699FB72009BE4FB /* Alamofire */; };
|
37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA42699FB72009BE4FB /* Alamofire */; };
|
||||||
37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA6269A552E009BE4FB /* Alamofire */; };
|
37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA6269A552E009BE4FB /* Alamofire */; };
|
||||||
37BADCA9269A570B009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA8269A570B009BE4FB /* Alamofire */; };
|
37BADCA9269A570B009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA8269A570B009BE4FB /* Alamofire */; };
|
||||||
|
37BC50A82778A84700510953 /* HistorySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BC50A72778A84700510953 /* HistorySettings.swift */; };
|
||||||
|
37BC50A92778A84700510953 /* HistorySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BC50A72778A84700510953 /* HistorySettings.swift */; };
|
||||||
|
37BC50AA2778A84700510953 /* HistorySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BC50A72778A84700510953 /* HistorySettings.swift */; };
|
||||||
|
37BC50AC2778BCBA00510953 /* HistoryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BC50AB2778BCBA00510953 /* HistoryModel.swift */; };
|
||||||
|
37BC50AD2778BCBA00510953 /* HistoryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BC50AB2778BCBA00510953 /* HistoryModel.swift */; };
|
||||||
|
37BC50AE2778BCBA00510953 /* HistoryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BC50AB2778BCBA00510953 /* HistoryModel.swift */; };
|
||||||
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BD07B42698AA4D003EBB87 /* ContentView.swift */; };
|
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BD07B42698AA4D003EBB87 /* ContentView.swift */; };
|
||||||
37BD07B72698AB2E003EBB87 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 37BD07B62698AB2E003EBB87 /* Defaults */; };
|
37BD07B72698AB2E003EBB87 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 37BD07B62698AB2E003EBB87 /* Defaults */; };
|
||||||
37BD07B92698AB2E003EBB87 /* Siesta in Frameworks */ = {isa = PBXBuildFile; productRef = 37BD07B82698AB2E003EBB87 /* Siesta */; };
|
37BD07B92698AB2E003EBB87 /* Siesta in Frameworks */ = {isa = PBXBuildFile; productRef = 37BD07B82698AB2E003EBB87 /* Siesta */; };
|
||||||
@ -584,6 +599,8 @@
|
|||||||
3705B17F267B4DFB00704544 /* TrendingCountry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCountry.swift; sourceTree = "<group>"; };
|
3705B17F267B4DFB00704544 /* TrendingCountry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCountry.swift; sourceTree = "<group>"; };
|
||||||
3705B181267B4E4900704544 /* TrendingCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCategory.swift; sourceTree = "<group>"; };
|
3705B181267B4E4900704544 /* TrendingCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCategory.swift; sourceTree = "<group>"; };
|
||||||
3711403E26B206A6005B3555 /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = "<group>"; };
|
3711403E26B206A6005B3555 /* SearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = "<group>"; };
|
||||||
|
37130A5A277657090033018A /* Yattee.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Yattee.xcdatamodel; sourceTree = "<group>"; };
|
||||||
|
37130A5E277657300033018A /* PersistenceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||||
3714166E267A8ACC006CA35D /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = "<group>"; };
|
3714166E267A8ACC006CA35D /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = "<group>"; };
|
||||||
37141672267A8E10006CA35D /* Country.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = "<group>"; };
|
37141672267A8E10006CA35D /* Country.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = "<group>"; };
|
||||||
37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
|
||||||
@ -652,6 +669,7 @@
|
|||||||
3782B95C2755858100990149 /* NSTextField+FocusRingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextField+FocusRingType.swift"; sourceTree = "<group>"; };
|
3782B95C2755858100990149 /* NSTextField+FocusRingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextField+FocusRingType.swift"; sourceTree = "<group>"; };
|
||||||
3784B23A272894DA00B09468 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = "<group>"; };
|
3784B23A272894DA00B09468 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = "<group>"; };
|
||||||
3784B23C2728B85300B09468 /* ShareButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareButton.swift; sourceTree = "<group>"; };
|
3784B23C2728B85300B09468 /* ShareButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareButton.swift; sourceTree = "<group>"; };
|
||||||
|
3784CDDE27772EE40055BBF2 /* Watch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Watch.swift; sourceTree = "<group>"; };
|
||||||
3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItemView.swift; sourceTree = "<group>"; };
|
3788AC2626F6840700F6BAA9 /* FavoriteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteItemView.swift; sourceTree = "<group>"; };
|
||||||
378AE942274EF00A006A4EE1 /* Color+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Background.swift"; sourceTree = "<group>"; };
|
378AE942274EF00A006A4EE1 /* Color+Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Background.swift"; sourceTree = "<group>"; };
|
||||||
378E50FA26FE8B9F00F49626 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = "<group>"; };
|
378E50FA26FE8B9F00F49626 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = "<group>"; };
|
||||||
@ -691,6 +709,8 @@
|
|||||||
37BA794E26DC3E0E002A0235 /* Int+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Format.swift"; sourceTree = "<group>"; };
|
37BA794E26DC3E0E002A0235 /* Int+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Format.swift"; sourceTree = "<group>"; };
|
||||||
37BA796D26DC412E002A0235 /* Int+FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+FormatTests.swift"; sourceTree = "<group>"; };
|
37BA796D26DC412E002A0235 /* Int+FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+FormatTests.swift"; sourceTree = "<group>"; };
|
||||||
37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVNavigationView.swift; sourceTree = "<group>"; };
|
37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVNavigationView.swift; sourceTree = "<group>"; };
|
||||||
|
37BC50A72778A84700510953 /* HistorySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySettings.swift; sourceTree = "<group>"; };
|
||||||
|
37BC50AB2778BCBA00510953 /* HistoryModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HistoryModel.swift; path = Model/HistoryModel.swift; sourceTree = SOURCE_ROOT; };
|
||||||
37BD07B42698AA4D003EBB87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
37BD07B42698AA4D003EBB87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarNavigation.swift; sourceTree = "<group>"; };
|
37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarNavigation.swift; sourceTree = "<group>"; };
|
||||||
37BD07C42698ADEE003EBB87 /* Yattee.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Yattee.entitlements; sourceTree = "<group>"; };
|
37BD07C42698ADEE003EBB87 /* Yattee.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Yattee.entitlements; sourceTree = "<group>"; };
|
||||||
@ -1016,6 +1036,7 @@
|
|||||||
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */,
|
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */,
|
||||||
37732FEF2703A26300F04329 /* AccountValidationStatus.swift */,
|
37732FEF2703A26300F04329 /* AccountValidationStatus.swift */,
|
||||||
376BE50A27349108009AD608 /* BrowsingSettings.swift */,
|
376BE50A27349108009AD608 /* BrowsingSettings.swift */,
|
||||||
|
37BC50A72778A84700510953 /* HistorySettings.swift */,
|
||||||
37484C2426FC83E000287258 /* InstanceForm.swift */,
|
37484C2426FC83E000287258 /* InstanceForm.swift */,
|
||||||
37484C2C26FC844700287258 /* InstanceSettings.swift */,
|
37484C2C26FC844700287258 /* InstanceSettings.swift */,
|
||||||
37484C1826FC837400287258 /* PlaybackSettings.swift */,
|
37484C1826FC837400287258 /* PlaybackSettings.swift */,
|
||||||
@ -1275,8 +1296,10 @@
|
|||||||
37141672267A8E10006CA35D /* Country.swift */,
|
37141672267A8E10006CA35D /* Country.swift */,
|
||||||
37599F2F272B42810087F250 /* FavoriteItem.swift */,
|
37599F2F272B42810087F250 /* FavoriteItem.swift */,
|
||||||
37599F33272B44000087F250 /* FavoritesModel.swift */,
|
37599F33272B44000087F250 /* FavoritesModel.swift */,
|
||||||
|
37BC50AB2778BCBA00510953 /* HistoryModel.swift */,
|
||||||
37EF5C212739D37B00B03725 /* MenuModel.swift */,
|
37EF5C212739D37B00B03725 /* MenuModel.swift */,
|
||||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
||||||
|
37130A5E277657300033018A /* PersistenceController.swift */,
|
||||||
376578882685471400D4EA09 /* Playlist.swift */,
|
376578882685471400D4EA09 /* Playlist.swift */,
|
||||||
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
37BA794226DBA973002A0235 /* PlaylistsModel.swift */,
|
||||||
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */,
|
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */,
|
||||||
@ -1289,6 +1312,8 @@
|
|||||||
37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */,
|
37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */,
|
||||||
3705B181267B4E4900704544 /* TrendingCategory.swift */,
|
3705B181267B4E4900704544 /* TrendingCategory.swift */,
|
||||||
37D4B19626717E1500C925CA /* Video.swift */,
|
37D4B19626717E1500C925CA /* Video.swift */,
|
||||||
|
3784CDDE27772EE40055BBF2 /* Watch.swift */,
|
||||||
|
37130A59277657090033018A /* Yattee.xcdatamodeld */,
|
||||||
);
|
);
|
||||||
path = Model;
|
path = Model;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1797,6 +1822,7 @@
|
|||||||
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||||
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||||
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
||||||
|
37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */,
|
||||||
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||||
3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||||
3782B94F27553A6700990149 /* SearchSuggestions.swift in Sources */,
|
3782B94F27553A6700990149 /* SearchSuggestions.swift in Sources */,
|
||||||
@ -1804,6 +1830,7 @@
|
|||||||
37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
||||||
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */,
|
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */,
|
||||||
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||||
|
37130A5F277657300033018A /* PersistenceController.swift in Sources */,
|
||||||
37FD43E32704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
37FD43E32704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
||||||
37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
||||||
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||||
@ -1815,6 +1842,7 @@
|
|||||||
376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
||||||
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 */,
|
||||||
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 */,
|
||||||
@ -1855,6 +1883,7 @@
|
|||||||
37BF661C27308859008CCFB0 /* DropFavorite.swift in Sources */,
|
37BF661C27308859008CCFB0 /* DropFavorite.swift in Sources */,
|
||||||
376A33E42720CB35000C1D6B /* Account.swift in Sources */,
|
376A33E42720CB35000C1D6B /* Account.swift in Sources */,
|
||||||
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||||
|
37BC50AC2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
||||||
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
||||||
3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||||
37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */,
|
37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||||
@ -1918,6 +1947,7 @@
|
|||||||
37C0697A2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */,
|
37C0697A2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */,
|
||||||
37D4B0E42671614900C925CA /* YatteeApp.swift in Sources */,
|
37D4B0E42671614900C925CA /* YatteeApp.swift in Sources */,
|
||||||
37C3A241272359900087A57A /* Double+Format.swift in Sources */,
|
37C3A241272359900087A57A /* Double+Format.swift in Sources */,
|
||||||
|
3784CDE227772EE40055BBF2 /* Watch.swift in Sources */,
|
||||||
37FB285E272225E800A57617 /* ContentItemView.swift in Sources */,
|
37FB285E272225E800A57617 /* ContentItemView.swift in Sources */,
|
||||||
3797758B2689345500DD52A8 /* Store.swift in Sources */,
|
3797758B2689345500DD52A8 /* Store.swift in Sources */,
|
||||||
37B795902771DAE0001CF27B /* OpenURLHandler.swift in Sources */,
|
37B795902771DAE0001CF27B /* OpenURLHandler.swift in Sources */,
|
||||||
@ -1954,6 +1984,7 @@
|
|||||||
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||||
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
|
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
|
||||||
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
||||||
|
3784CDE327772EE40055BBF2 /* Watch.swift in Sources */,
|
||||||
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */,
|
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||||
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
37FB285F272225E800A57617 /* ContentItemView.swift in Sources */,
|
37FB285F272225E800A57617 /* ContentItemView.swift in Sources */,
|
||||||
@ -1984,6 +2015,7 @@
|
|||||||
37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */,
|
37152EEB26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||||
377FC7E2267A084A00A6BBAF /* VideoCell.swift in Sources */,
|
377FC7E2267A084A00A6BBAF /* VideoCell.swift in Sources */,
|
||||||
37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */,
|
37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */,
|
||||||
|
37BC50AD2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
||||||
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
|
378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||||
37BE7AF12760013C00DBECED /* UpdatesSettings.swift in Sources */,
|
37BE7AF12760013C00DBECED /* UpdatesSettings.swift in Sources */,
|
||||||
37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
||||||
@ -2034,6 +2066,7 @@
|
|||||||
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
||||||
371B7E622759706A00D21217 /* CommentsView.swift in Sources */,
|
371B7E622759706A00D21217 /* CommentsView.swift in Sources */,
|
||||||
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
||||||
|
37130A5C277657090033018A /* Yattee.xcdatamodeld in Sources */,
|
||||||
37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
||||||
37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */,
|
37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */,
|
||||||
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||||
@ -2046,6 +2079,7 @@
|
|||||||
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
||||||
371B7E5D27596B8400D21217 /* Comment.swift in Sources */,
|
371B7E5D27596B8400D21217 /* Comment.swift in Sources */,
|
||||||
37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */,
|
37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */,
|
||||||
|
37BC50A92778A84700510953 /* HistorySettings.swift in Sources */,
|
||||||
37599F31272B42810087F250 /* FavoriteItem.swift in Sources */,
|
37599F31272B42810087F250 /* FavoriteItem.swift in Sources */,
|
||||||
3730F75A2733481E00F385FC /* RelatedView.swift in Sources */,
|
3730F75A2733481E00F385FC /* RelatedView.swift in Sources */,
|
||||||
37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */,
|
37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */,
|
||||||
@ -2053,6 +2087,7 @@
|
|||||||
37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
37D526DF2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
||||||
373C8FE5275B955100CB5936 /* CommentsPage.swift in Sources */,
|
373C8FE5275B955100CB5936 /* CommentsPage.swift in Sources */,
|
||||||
37D4B0E52671614900C925CA /* YatteeApp.swift in Sources */,
|
37D4B0E52671614900C925CA /* YatteeApp.swift in Sources */,
|
||||||
|
37130A60277657300033018A /* PersistenceController.swift in Sources */,
|
||||||
37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */,
|
37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */,
|
||||||
37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||||
3711404026B206A6005B3555 /* SearchModel.swift in Sources */,
|
3711404026B206A6005B3555 /* SearchModel.swift in Sources */,
|
||||||
@ -2169,6 +2204,7 @@
|
|||||||
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||||
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
|
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
|
||||||
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
||||||
|
37BC50AA2778A84700510953 /* HistorySettings.swift in Sources */,
|
||||||
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||||
37C3A24727235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
37C3A24727235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
||||||
@ -2183,6 +2219,7 @@
|
|||||||
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||||
37C0697C2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */,
|
37C0697C2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */,
|
||||||
378AE93D274EDFB3006A4EE1 /* Backport.swift in Sources */,
|
378AE93D274EDFB3006A4EE1 /* Backport.swift in Sources */,
|
||||||
|
37130A5D277657090033018A /* Yattee.xcdatamodeld in Sources */,
|
||||||
37C3A243272359900087A57A /* Double+Format.swift in Sources */,
|
37C3A243272359900087A57A /* Double+Format.swift in Sources */,
|
||||||
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
||||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||||
@ -2198,6 +2235,7 @@
|
|||||||
3743B86A27216D3600261544 /* ChannelCell.swift in Sources */,
|
3743B86A27216D3600261544 /* ChannelCell.swift in Sources */,
|
||||||
37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||||
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||||
|
3784CDE427772EE40055BBF2 /* Watch.swift in Sources */,
|
||||||
3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */,
|
3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */,
|
||||||
37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
37169AA42729D98A0011DE61 /* InstancesBridge.swift in Sources */,
|
||||||
37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */,
|
37D4B18E26717B3800C925CA /* VideoCell.swift in Sources */,
|
||||||
@ -2209,6 +2247,7 @@
|
|||||||
37A9966026D6F9B9006E3224 /* FavoritesView.swift in Sources */,
|
37A9966026D6F9B9006E3224 /* FavoritesView.swift in Sources */,
|
||||||
37001565271B1F250049C794 /* AccountsModel.swift in Sources */,
|
37001565271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||||
374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
||||||
|
37130A61277657300033018A /* PersistenceController.swift in Sources */,
|
||||||
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
||||||
376A33E62720CB35000C1D6B /* Account.swift in Sources */,
|
376A33E62720CB35000C1D6B /* Account.swift in Sources */,
|
||||||
37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
||||||
@ -2256,6 +2295,7 @@
|
|||||||
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
37D4B19926717E1500C925CA /* Video.swift in Sources */,
|
||||||
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
|
378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */,
|
||||||
37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
||||||
|
37BC50AE2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
||||||
37D526E02720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
37D526E02720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
||||||
37599F36272B44000087F250 /* FavoritesModel.swift in Sources */,
|
37599F36272B44000087F250 /* FavoritesModel.swift in Sources */,
|
||||||
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
@ -3323,6 +3363,19 @@
|
|||||||
productName = SDWebImagePINPlugin;
|
productName = SDWebImagePINPlugin;
|
||||||
};
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
|
/* Begin XCVersionGroup section */
|
||||||
|
37130A59277657090033018A /* Yattee.xcdatamodeld */ = {
|
||||||
|
isa = XCVersionGroup;
|
||||||
|
children = (
|
||||||
|
37130A5A277657090033018A /* Yattee.xcdatamodel */,
|
||||||
|
);
|
||||||
|
currentVersion = 37130A5A277657090033018A /* Yattee.xcdatamodel */;
|
||||||
|
path = Yattee.xcdatamodeld;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
versionGroupType = wrapper.xcdatamodel;
|
||||||
|
};
|
||||||
|
/* End XCVersionGroup section */
|
||||||
};
|
};
|
||||||
rootObject = 37D4B0BD2671614700C925CA /* Project object */;
|
rootObject = 37D4B0BD2671614700C925CA /* Project object */;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import CoreMedia
|
||||||
import Defaults
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@ -11,6 +12,9 @@ struct NowPlayingView: View {
|
|||||||
|
|
||||||
@State private var repliesID: Comment.ID?
|
@State private var repliesID: Comment.ID?
|
||||||
|
|
||||||
|
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
|
||||||
|
var watches: FetchedResults<Watch>
|
||||||
|
|
||||||
@EnvironmentObject<CommentsModel> private var comments
|
@EnvironmentObject<CommentsModel> private var comments
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@EnvironmentObject<RecentsModel> private var recents
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@ -94,22 +98,27 @@ struct NowPlayingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sections.contains(.playedPreviously), saveHistory, !player.history.isEmpty {
|
if sections.contains(.playedPreviously), saveHistory, !visibleWatches.isEmpty {
|
||||||
Section(header: Text("Played Previously")) {
|
Section(header: Text("Played Previously")) {
|
||||||
ForEach(player.history) { item in
|
ForEach(visibleWatches, id: \.videoID) { watch in
|
||||||
Button {
|
Button {
|
||||||
player.playHistory(item)
|
player.playHistory(
|
||||||
|
PlayerQueueItem.from(watch, video: player.historyVideo(watch.videoID))
|
||||||
|
)
|
||||||
player.show()
|
player.show()
|
||||||
} label: {
|
} label: {
|
||||||
VideoBanner(video: item.video, playbackTime: item.playbackTime, videoDuration: item.videoDuration)
|
VideoBanner(
|
||||||
|
video: player.historyVideo(watch.videoID),
|
||||||
|
playbackTime: CMTime.secondsInDefaultTimescale(watch.stoppedAt),
|
||||||
|
videoDuration: watch.videoDuration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
player.loadHistoryVideoDetails(watch.videoID)
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button("Remove", role: .destructive) {
|
Button("Remove", role: .destructive) {
|
||||||
player.removeHistory(item)
|
player.removeWatch(watch)
|
||||||
}
|
|
||||||
|
|
||||||
Button("Remove All", role: .destructive) {
|
|
||||||
player.removeHistoryItems()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,6 +150,10 @@ struct NowPlayingView: View {
|
|||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 560, maxHeight: .infinity, alignment: .leading)
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 560, maxHeight: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var visibleWatches: [Watch] {
|
||||||
|
watches.filter { $0.videoID != player.currentVideo?.videoID }
|
||||||
|
}
|
||||||
|
|
||||||
private var progressView: some View {
|
private var progressView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
Loading…
Reference in New Issue
Block a user