mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
Watch Next menu improvements
This commit is contained in:
parent
2ce903b6c3
commit
636e8205fe
@ -36,7 +36,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
case .queue:
|
case .queue:
|
||||||
return "Queue"
|
return "Queue"
|
||||||
case .shuffle:
|
case .shuffle:
|
||||||
return "Queue, shuffled"
|
return "Queue - shuffled"
|
||||||
case .loopOne:
|
case .loopOne:
|
||||||
return "Loop one"
|
return "Loop one"
|
||||||
case .related:
|
case .related:
|
||||||
|
@ -42,7 +42,7 @@ final class WatchNextViewModel: ObservableObject {
|
|||||||
@Published var countdown = 0.0
|
@Published var countdown = 0.0
|
||||||
var countdownTimer: Timer?
|
var countdownTimer: Timer?
|
||||||
|
|
||||||
private var player = PlayerModel.shared
|
var player = PlayerModel.shared
|
||||||
|
|
||||||
var autoplayTimer: Timer?
|
var autoplayTimer: Timer?
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ final class WatchNextViewModel: ObservableObject {
|
|||||||
|
|
||||||
private func open(reason: PresentationReason) {
|
private func open(reason: PresentationReason) {
|
||||||
self.reason = reason
|
self.reason = reason
|
||||||
page = Page.allCases.first { isAvailable($0) } ?? .history
|
setPageAfterOpening()
|
||||||
|
|
||||||
guard !isPresenting else { return }
|
guard !isPresenting else { return }
|
||||||
withAnimation(Self.animation) {
|
withAnimation(Self.animation) {
|
||||||
@ -137,6 +137,19 @@ final class WatchNextViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setPageAfterOpening() {
|
||||||
|
let firstAvailable = Page.allCases.first { isAvailable($0) } ?? .history
|
||||||
|
|
||||||
|
switch reason {
|
||||||
|
case .finishedWatching:
|
||||||
|
page = player.playbackMode == .related ? .queue : firstAvailable
|
||||||
|
case .closed:
|
||||||
|
page = player.playbackMode == .related ? .queue : firstAvailable
|
||||||
|
default:
|
||||||
|
page = firstAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func close() {
|
func close() {
|
||||||
let close = {
|
let close = {
|
||||||
self.player.closeCurrentItem()
|
self.player.closeCurrentItem()
|
||||||
|
@ -87,11 +87,9 @@ struct PlayerQueueView: View {
|
|||||||
ForEach(player.queue) { item in
|
ForEach(player.queue) { item in
|
||||||
PlayerQueueRow(item: item)
|
PlayerQueueRow(item: item)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
removeButton(item)
|
|
||||||
removeAllButton()
|
|
||||||
|
|
||||||
if let video = item.video {
|
if let video = item.video {
|
||||||
VideoContextMenuView(video: video)
|
VideoContextMenuView(video: video)
|
||||||
|
.environment(\.inQueueListing, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,22 +114,6 @@ struct PlayerQueueView: View {
|
|||||||
.transaction { t in t.disablesAnimations = true }
|
.transaction { t in t.disablesAnimations = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeButton(_ item: PlayerQueueItem) -> some View {
|
|
||||||
Button {
|
|
||||||
player.remove(item)
|
|
||||||
} label: {
|
|
||||||
Label("Remove from the queue", systemImage: "trash")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func removeAllButton() -> some View {
|
|
||||||
Button {
|
|
||||||
player.removeQueueItems()
|
|
||||||
} label: {
|
|
||||||
Label("Clear the queue", systemImage: "trash.fill")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PlayerQueueView_Previews: PreviewProvider {
|
struct PlayerQueueView_Previews: PreviewProvider {
|
||||||
|
@ -96,13 +96,13 @@ struct VideoDetails: View {
|
|||||||
ContentItem(video: player.currentVideo)
|
ContentItem(video: player.currentVideo)
|
||||||
}
|
}
|
||||||
|
|
||||||
var pageMenu: some View {
|
@ViewBuilder var pageMenu: some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
pagePicker
|
pagePicker
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
.offset(x: 15, y: 15)
|
.offset(x: 15, y: 15)
|
||||||
.frame(maxWidth: 200)
|
.frame(maxWidth: 200)
|
||||||
#else
|
#elseif os(iOS)
|
||||||
Menu {
|
Menu {
|
||||||
pagePicker
|
pagePicker
|
||||||
} label: {
|
} label: {
|
||||||
@ -224,6 +224,8 @@ struct VideoDetails: View {
|
|||||||
.secondaryBackground
|
.secondaryBackground
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
.background
|
.background
|
||||||
|
#else
|
||||||
|
.clear
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +35,15 @@ struct WatchNextView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
if model.isRestartable {
|
Text("Mode")
|
||||||
reopenButton
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
playbackModeControl
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
if model.isRestartable {
|
||||||
|
reopenButton
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
@ -83,6 +90,9 @@ struct WatchNextView: View {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
PlayerQueueRow(item: item)
|
PlayerQueueRow(item: item)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.padding(.vertical, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
moreVideos
|
moreVideos
|
||||||
@ -123,6 +133,7 @@ struct WatchNextView: View {
|
|||||||
#else
|
#else
|
||||||
Menu {
|
Menu {
|
||||||
pagePicker
|
pagePicker
|
||||||
|
playbackModePicker
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
menuLabel
|
menuLabel
|
||||||
@ -142,7 +153,7 @@ struct WatchNextView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image(systemName: model.page.systemImageName)
|
Image(systemName: model.page.systemImageName)
|
||||||
.imageScale(.small)
|
.imageScale(.small)
|
||||||
Text(model.page.title)
|
Text(model.page == .queue ? queueTitle : model.page.title)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,12 +161,19 @@ struct WatchNextView: View {
|
|||||||
var pagePicker: some View {
|
var pagePicker: some View {
|
||||||
Picker("Page", selection: $model.page) {
|
Picker("Page", selection: $model.page) {
|
||||||
ForEach(WatchNextViewModel.Page.allCases, id: \.rawValue) { page in
|
ForEach(WatchNextViewModel.Page.allCases, id: \.rawValue) { page in
|
||||||
Label(page.title, systemImage: page.systemImageName)
|
Label(
|
||||||
.tag(page)
|
page == .queue ? queueTitle : page.title,
|
||||||
|
systemImage: page.systemImageName
|
||||||
|
)
|
||||||
|
.tag(page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var queueTitle: String {
|
||||||
|
"\(WatchNextViewModel.Page.queue.title) • \(player.queue.count)"
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder var hideCloseButton: some View {
|
@ViewBuilder var hideCloseButton: some View {
|
||||||
if model.isHideable {
|
if model.isHideable {
|
||||||
hideButton
|
hideButton
|
||||||
@ -194,14 +212,30 @@ struct WatchNextView: View {
|
|||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
switch model.page {
|
switch model.page {
|
||||||
case .queue:
|
case .queue:
|
||||||
let queueForMoreVideos = player.queue.isEmpty ? [] : player.queue.suffix(from: model.isAutoplaying ? 1 : 0)
|
|
||||||
|
if player.playbackMode == .related, !(model.isAutoplaying && model.canAutoplay) {
|
||||||
|
autoplaying
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
|
||||||
|
let queueForMoreVideos = player.queue.isEmpty ? [] : player.queue.suffix(from: player.playbackMode == .queue ? 1 : 0)
|
||||||
|
|
||||||
|
if (model.isAutoplaying && model.canAutoplay && !queueForMoreVideos.isEmpty) ||
|
||||||
|
(!model.isAutoplaying && !queueForMoreVideos.isEmpty)
|
||||||
|
{
|
||||||
|
Text("Next in queue")
|
||||||
|
.font(.headline)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
if !queueForMoreVideos.isEmpty {
|
if !queueForMoreVideos.isEmpty {
|
||||||
ForEach(queueForMoreVideos) { item in
|
ForEach(queueForMoreVideos) { item in
|
||||||
ContentItemView(item: .init(video: item.video))
|
ContentItemView(item: .init(video: item.video))
|
||||||
.environment(\.inQueueListing, true)
|
.environment(\.inQueueListing, true)
|
||||||
.environment(\.listingStyle, .list)
|
.environment(\.listingStyle, .list)
|
||||||
}
|
}
|
||||||
} else if player.playbackMode != .related && player.playbackMode != .loopOne {
|
} else {
|
||||||
Label(
|
Label(
|
||||||
model.isAutoplaying ? "Nothing more in the queue" : "Queue is empty",
|
model.isAutoplaying ? "Nothing more in the queue" : "Queue is empty",
|
||||||
systemImage: WatchNextViewModel.Page.queue.systemImageName
|
systemImage: WatchNextViewModel.Page.queue.systemImageName
|
||||||
@ -226,6 +260,71 @@ struct WatchNextView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var playbackModeControl: some View {
|
||||||
|
#if os(tvOS)
|
||||||
|
Button {
|
||||||
|
player.playbackMode = player.playbackMode.next()
|
||||||
|
} label: {
|
||||||
|
Label(player.playbackMode.description, systemImage: player.playbackMode.systemImage)
|
||||||
|
}
|
||||||
|
#elseif os(macOS)
|
||||||
|
playbackModePicker
|
||||||
|
.modifier(SettingsPickerModifier())
|
||||||
|
#if os(macOS)
|
||||||
|
.frame(maxWidth: 150)
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
Menu {
|
||||||
|
playbackModePicker
|
||||||
|
} label: {
|
||||||
|
Label(player.playbackMode.description, systemImage: player.playbackMode.systemImage)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var playbackModePicker: some View {
|
||||||
|
Picker("Playback Mode", selection: $model.player.playbackMode) {
|
||||||
|
ForEach(PlayerModel.PlaybackMode.allCases, id: \.rawValue) { mode in
|
||||||
|
Label(mode.description, systemImage: mode.systemImage).tag(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var autoplaying: some View {
|
||||||
|
Section(header: autoplayingHeader) {
|
||||||
|
if let item = player.autoplayItem {
|
||||||
|
PlayerQueueRow(item: item, autoplay: true)
|
||||||
|
} else {
|
||||||
|
Group {
|
||||||
|
if player.currentItem.isNil {
|
||||||
|
Text("Not Playing")
|
||||||
|
} else {
|
||||||
|
Text("Finding something to play...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var autoplayingHeader: some View {
|
||||||
|
HStack {
|
||||||
|
Text("Autoplaying Next")
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
player.setRelatedAutoplayItem()
|
||||||
|
} label: {
|
||||||
|
Label("Find Other", systemImage: "arrow.triangle.2.circlepath.circle")
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
.disabled(player.currentItem.isNil)
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WatchNextView_Previews: PreviewProvider {
|
struct WatchNextView_Previews: PreviewProvider {
|
||||||
|
Loading…
Reference in New Issue
Block a user