From e436dec4bad1f5dcbbb2c6130bdc6732ff1ac2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Tue, 21 Nov 2023 15:25:22 +0100 Subject: [PATCH 01/19] make chapters collapsible Chapters are now collapsible by default only the first two chapters are shown. The second will be shown opaque to indicate more chapters. --- .../Player/Video Details/ChaptersView.swift | 16 ++++++++--- .../Player/Video Details/VideoDetails.swift | 27 +++++++++++-------- Shared/Settings/PlayerSettings.swift | 2 +- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/Shared/Player/Video Details/ChaptersView.swift b/Shared/Player/Video Details/ChaptersView.swift index 1bf266a9..cae9d50d 100644 --- a/Shared/Player/Video Details/ChaptersView.swift +++ b/Shared/Player/Video Details/ChaptersView.swift @@ -4,6 +4,7 @@ import SwiftUI struct ChaptersView: View { @ObservedObject private var player = PlayerModel.shared + @Binding var expand: Bool var chapters: [Chapter] { player.videoForDisplay?.chapters ?? [] @@ -14,7 +15,7 @@ struct ChaptersView: View { } var body: some View { - if !chapters.isEmpty { + if expand && !chapters.isEmpty { #if os(tvOS) List { Section { @@ -45,15 +46,22 @@ struct ChaptersView: View { .padding(.horizontal) } #endif - } else { - NoCommentsView(text: "No chapters information available".localized(), systemImage: "xmark.circle.fill") + } else if !chapters.isEmpty { + Section { + ChapterView(chapter: chapters[0]) + if chapters.count > 1 { + ChapterView(chapter: chapters[1]) + .opacity(0.3) + } + } + .padding(.horizontal) } } } struct ChaptersView_Previews: PreviewProvider { static var previews: some View { - ChaptersView() + ChaptersView(expand: .constant(false)) .injectFixtureEnvironmentObjects() } } diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift index 428580c6..3c10ba58 100644 --- a/Shared/Player/Video Details/VideoDetails.swift +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -169,6 +169,7 @@ struct VideoDetails: View { @State private var subscriptionToggleButtonDisabled = false @State private var page = DetailsPage.info @State private var descriptionExpanded = false + @State private var chaptersExpanded = false @Environment(\.navigationStyle) private var navigationStyle #if os(iOS) @@ -317,10 +318,9 @@ struct VideoDetails: View { if player.videoBeingOpened.isNil { if showChapters, !video.isLocal, - !video.chapters.isEmpty - { + !video.chapters.isEmpty { Section(header: chaptersHeader) { - ChaptersView() + ChaptersView(expand: $chaptersExpanded) } } @@ -331,8 +331,7 @@ struct VideoDetails: View { if showRelated, !sidebarQueue, - !(player.videoForDisplay?.related.isEmpty ?? true) - { + !(player.videoForDisplay?.related.isEmpty ?? true) { RelatedView() .padding(.horizontal) .padding(.top, 20) @@ -390,8 +389,7 @@ struct VideoDetails: View { if showScrollToTopInComments, page == .comments, comments.loaded, - comments.all.count > 3 - { + comments.all.count > 3 { Button { withAnimation { proxy.scrollTo(Self.pageMenuID) @@ -441,10 +439,17 @@ struct VideoDetails: View { } var chaptersHeader: some View { - Text("Chapters".localized()) - .padding(.horizontal) - .font(.caption) - .foregroundColor(.secondary) + HStack { + Text("Chapters".localized()) + Spacer() + Button(action: { chaptersExpanded.toggle() }) { + Image(systemName: chaptersExpanded ? "chevron.up" : "chevron.down") + .imageScale(.small) + } + } + .padding(.horizontal) + .font(.caption) + .foregroundColor(.secondary) } } diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index 37c3d701..a2090359 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -282,7 +282,7 @@ struct PlayerSettings: View { } private var showChaptersToggle: some View { - Toggle("Chapters", isOn: $showChapters) + Toggle("Chapters (if available)", isOn: $showChapters) } private var showRelatedToggle: some View { From a33a1d76583ba4558d43f0d76092f2c731fd8a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Wed, 22 Nov 2023 10:23:42 +0100 Subject: [PATCH 02/19] formatting --- Shared/Player/Video Details/VideoDetails.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift index 3c10ba58..7bd9885c 100644 --- a/Shared/Player/Video Details/VideoDetails.swift +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -318,7 +318,8 @@ struct VideoDetails: View { if player.videoBeingOpened.isNil { if showChapters, !video.isLocal, - !video.chapters.isEmpty { + !video.chapters.isEmpty + { Section(header: chaptersHeader) { ChaptersView(expand: $chaptersExpanded) } @@ -331,7 +332,8 @@ struct VideoDetails: View { if showRelated, !sidebarQueue, - !(player.videoForDisplay?.related.isEmpty ?? true) { + !(player.videoForDisplay?.related.isEmpty ?? true) + { RelatedView() .padding(.horizontal) .padding(.top, 20) @@ -389,7 +391,8 @@ struct VideoDetails: View { if showScrollToTopInComments, page == .comments, comments.loaded, - comments.all.count > 3 { + comments.all.count > 3 + { Button { withAnimation { proxy.scrollTo(Self.pageMenuID) From b0d81cdefdbe34b1d8a24bf1bca201e8f8448470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Sat, 25 Nov 2023 22:02:28 +0100 Subject: [PATCH 03/19] iOS - chapters header is a button now --- .../Player/Video Details/VideoDetails.swift | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift index 7bd9885c..9cb125e7 100644 --- a/Shared/Player/Video Details/VideoDetails.swift +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -442,17 +442,33 @@ struct VideoDetails: View { } var chaptersHeader: some View { - HStack { - Text("Chapters".localized()) - Spacer() - Button(action: { chaptersExpanded.toggle() }) { - Image(systemName: chaptersExpanded ? "chevron.up" : "chevron.down") - .imageScale(.small) + #if canImport(UIKit) + Button(action: { + chaptersExpanded.toggle() + }) { + HStack { + Text("Chapters".localized()) + Spacer() + Image(systemName: chaptersExpanded ? "chevron.up" : "chevron.down") + .imageScale(.small) + } + .padding(.horizontal) + .font(.caption) + .foregroundColor(.secondary) } - } - .padding(.horizontal) - .font(.caption) - .foregroundColor(.secondary) + #elseif canImport(AppKit) + HStack { + Text("Chapters".localized()) + Spacer() + Button(action: { chaptersExpanded.toggle() }) { + Image(systemName: chaptersExpanded ? "chevron.up" : "chevron.down") + .imageScale(.small) + } + } + .padding(.horizontal) + .font(.caption) + .foregroundColor(.secondary) + #endif } } From a7baaeb4852a29b882aacd45ea475930d02be2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Sun, 26 Nov 2023 15:15:30 +0100 Subject: [PATCH 04/19] iOS click on collapsed chapters expands the view --- .../Player/Video Details/ChaptersView.swift | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Shared/Player/Video Details/ChaptersView.swift b/Shared/Player/Video Details/ChaptersView.swift index cae9d50d..d0c29b7f 100644 --- a/Shared/Player/Video Details/ChaptersView.swift +++ b/Shared/Player/Video Details/ChaptersView.swift @@ -47,16 +47,28 @@ struct ChaptersView: View { } #endif } else if !chapters.isEmpty { - Section { - ChapterView(chapter: chapters[0]) - if chapters.count > 1 { - ChapterView(chapter: chapters[1]) - .opacity(0.3) + #if os(iOS) + Button(action: { + self.expand.toggle() // Use your expanding logic here + }) { + contents } - } - .padding(.horizontal) + #else + contents + #endif } } + + var contents: some View { + Section { + ForEach(chapters.prefix(3).indices, id: \.self) { index in + ChapterView(chapter: chapters[index]) + .allowsHitTesting(expand) + .opacity(index == 0 ? 1.0 : 0.3) + } + } + .padding(.horizontal) + } } struct ChaptersView_Previews: PreviewProvider { From 8f08674527ad6c88dbc833f1a4a8176c38bdb68e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 27 Nov 2023 12:13:51 +0100 Subject: [PATCH 05/19] vertical chapters with images are always collapsed --- .../Player/Video Details/ChaptersView.swift | 63 +++++++++---------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/Shared/Player/Video Details/ChaptersView.swift b/Shared/Player/Video Details/ChaptersView.swift index d0c29b7f..8aa2251f 100644 --- a/Shared/Player/Video Details/ChaptersView.swift +++ b/Shared/Player/Video Details/ChaptersView.swift @@ -15,47 +15,44 @@ struct ChaptersView: View { } var body: some View { - if expand && !chapters.isEmpty { - #if os(tvOS) - List { - Section { - ForEach(chapters) { chapter in - ChapterView(chapter: chapter) - } - } - .listRowBackground(Color.clear) - } - .listStyle(.plain) - #else - if chaptersHaveImages { - ScrollView(.horizontal) { - LazyHStack(spacing: 20) { + if !chapters.isEmpty { + if expand || chaptersHaveImages { + #if os(tvOS) + List { + Section { ForEach(chapters) { chapter in ChapterView(chapter: chapter) } } - .padding(.horizontal, 15) + .listRowBackground(Color.clear) } - .frame(minHeight: ChapterView.thumbnailHeight + 100) - } else { - Section { - ForEach(chapters) { chapter in - ChapterView(chapter: chapter) + .listStyle(.plain) + #else + if chaptersHaveImages { + ScrollView(.horizontal) { + LazyHStack(spacing: 20) { + ForEach(chapters) { chapter in + ChapterView(chapter: chapter) + } + } + .padding(.horizontal, 15) } + .frame(minHeight: ChapterView.thumbnailHeight + 100) + } else { + contents } - .padding(.horizontal) - } - #endif - } else if !chapters.isEmpty { - #if os(iOS) - Button(action: { - self.expand.toggle() // Use your expanding logic here - }) { + #endif + } else { + #if os(iOS) + Button(action: { + self.expand.toggle() + }) { + contents + } + #else contents - } - #else - contents - #endif + #endif + } } } From 3feafc153c3d318bd1b70db79c7d5cb1c6a42572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 27 Nov 2023 13:34:18 +0100 Subject: [PATCH 06/19] remove toggle for vertical chapters --- .../Player/Video Details/ChaptersView.swift | 1 - .../Player/Video Details/VideoDetails.swift | 64 +++++++++++-------- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/Shared/Player/Video Details/ChaptersView.swift b/Shared/Player/Video Details/ChaptersView.swift index 8aa2251f..cabaabe0 100644 --- a/Shared/Player/Video Details/ChaptersView.swift +++ b/Shared/Player/Video Details/ChaptersView.swift @@ -37,7 +37,6 @@ struct ChaptersView: View { } .padding(.horizontal, 15) } - .frame(minHeight: ChapterView.thumbnailHeight + 100) } else { contents } diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift index 9cb125e7..43a23600 100644 --- a/Shared/Player/Video Details/VideoDetails.swift +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -441,34 +441,48 @@ struct VideoDetails: View { #endif } + var chaptersHaveImages: Bool { + player.videoForDisplay?.chapters.allSatisfy { $0.image != nil } ?? false + } + var chaptersHeader: some View { - #if canImport(UIKit) - Button(action: { - chaptersExpanded.toggle() - }) { - HStack { - Text("Chapters".localized()) - Spacer() - Image(systemName: chaptersExpanded ? "chevron.up" : "chevron.down") - .imageScale(.small) - } - .padding(.horizontal) - .font(.caption) - .foregroundColor(.secondary) - } - #elseif canImport(AppKit) - HStack { + Group { + if !chaptersHaveImages { + #if canImport(UIKit) + Button(action: { + chaptersExpanded.toggle() + }) { + HStack { + Text("Chapters".localized()) + Spacer() + Image(systemName: chaptersExpanded ? "chevron.up" : "chevron.down") + .imageScale(.small) + } + .padding(.horizontal) + .font(.caption) + .foregroundColor(.secondary) + } + #elseif canImport(AppKit) + HStack { + Text("Chapters".localized()) + Spacer() + Button(action: { chaptersExpanded.toggle() }) { + Image(systemName: chaptersExpanded ? "chevron.up" : "chevron.down") + .imageScale(.small) + } + } + .padding(.horizontal) + .font(.caption) + .foregroundColor(.secondary) + #endif + } else { + // No button, just the title when there are images Text("Chapters".localized()) - Spacer() - Button(action: { chaptersExpanded.toggle() }) { - Image(systemName: chaptersExpanded ? "chevron.up" : "chevron.down") - .imageScale(.small) - } + .font(.caption) + .foregroundColor(.secondary) + .padding(.horizontal) } - .padding(.horizontal) - .font(.caption) - .foregroundColor(.secondary) - #endif + } } } From fb5cd0f6814ed9b12b0c163ed5e91c87093f2802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 27 Nov 2023 14:18:43 +0100 Subject: [PATCH 07/19] vertical chapters are full width and max 3 lines --- Shared/Player/Video Details/ChapterView.swift | 4 ++-- .../Player/Video Details/ChaptersView.swift | 23 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Shared/Player/Video Details/ChapterView.swift b/Shared/Player/Video Details/ChapterView.swift index 9ee184e4..ab5bcbd7 100644 --- a/Shared/Player/Video Details/ChapterView.swift +++ b/Shared/Player/Video Details/ChapterView.swift @@ -49,14 +49,14 @@ struct ChapterView: View { } VStack(alignment: .leading, spacing: 4) { Text(chapter.title) - .lineLimit(2) + .lineLimit(3) .multilineTextAlignment(.leading) .font(.headline) Text(chapter.start.formattedAsPlaybackTime(allowZero: true) ?? "") .font(.system(.subheadline).monospacedDigit()) .foregroundColor(.secondary) } - .frame(maxWidth: Self.thumbnailWidth, alignment: .leading) + .frame(maxWidth: !chapter.image.isNil ? Self.thumbnailWidth : nil, alignment: .leading) } } #endif diff --git a/Shared/Player/Video Details/ChaptersView.swift b/Shared/Player/Video Details/ChaptersView.swift index cabaabe0..5f2bb17f 100644 --- a/Shared/Player/Video Details/ChaptersView.swift +++ b/Shared/Player/Video Details/ChaptersView.swift @@ -16,7 +16,7 @@ struct ChaptersView: View { var body: some View { if !chapters.isEmpty { - if expand || chaptersHaveImages { + if chaptersHaveImages { #if os(tvOS) List { Section { @@ -28,19 +28,22 @@ struct ChaptersView: View { } .listStyle(.plain) #else - if chaptersHaveImages { - ScrollView(.horizontal) { - LazyHStack(spacing: 20) { - ForEach(chapters) { chapter in - ChapterView(chapter: chapter) - } + ScrollView(.horizontal) { + LazyHStack(spacing: 20) { + ForEach(chapters) { chapter in + ChapterView(chapter: chapter) } - .padding(.horizontal, 15) } - } else { - contents + .padding(.horizontal, 15) } #endif + } else if expand { + Section { + ForEach(chapters) { chapter in + ChapterView(chapter: chapter) + } + } + .padding(.horizontal) } else { #if os(iOS) Button(action: { From 4ec6f35c5d74c50e4691ac4ef89cb2a5e57d0f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Tue, 28 Nov 2023 16:45:36 +0100 Subject: [PATCH 08/19] highlight current chapter --- Model/Player/PlayerModel.swift | 3 ++ Shared/Player/Video Details/ChapterView.swift | 29 ++++++++++++++++--- .../Player/Video Details/ChaptersView.swift | 18 ++++++++---- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index d9835211..1da1d8c8 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -131,6 +131,9 @@ final class PlayerModel: ObservableObject { @Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen #endif + @Published var playedChapters: [Int] = [] + @Published var currentChapterIndex: Int? + var accounts: AccountsModel { .shared } var comments: CommentsModel { .shared } var controls: PlayerControlsModel { .shared } diff --git a/Shared/Player/Video Details/ChapterView.swift b/Shared/Player/Video Details/ChapterView.swift index ab5bcbd7..ca10ace8 100644 --- a/Shared/Player/Video Details/ChapterView.swift +++ b/Shared/Player/Video Details/ChapterView.swift @@ -1,16 +1,27 @@ +import CoreMedia import Foundation import SDWebImageSwiftUI import SwiftUI struct ChapterView: View { var chapter: Chapter + var nextChapterStart: Double? - var player = PlayerModel.shared + var chapterIndex: Int + @ObservedObject private var player = PlayerModel.shared + + var isCurrentChapter: Bool { + player.currentChapterIndex == chapterIndex + } + + var hasBeenPlayed: Bool { + player.playedChapters.contains(chapterIndex) + } var body: some View { - Button { + Button(action: { player.backend.seek(to: chapter.start, seekType: .userInteracted) - } label: { + }) { Group { #if os(tvOS) horizontalChapter @@ -21,6 +32,15 @@ struct ChapterView: View { .contentShape(Rectangle()) } .buttonStyle(.plain) + .onReceive(PlayerTimeModel.shared.$currentTime) { cmTime in + let time = CMTimeGetSeconds(cmTime) + if time >= self.chapter.start, self.nextChapterStart == nil || time < self.nextChapterStart! { + player.currentChapterIndex = self.chapterIndex + if !player.playedChapters.contains(self.chapterIndex) { + player.playedChapters.append(self.chapterIndex) + } + } + } } #if os(tvOS) @@ -52,6 +72,7 @@ struct ChapterView: View { .lineLimit(3) .multilineTextAlignment(.leading) .font(.headline) + .foregroundColor(isCurrentChapter ? .detailBadgeOutstandingStyleBackground : .primary) Text(chapter.start.formattedAsPlaybackTime(allowZero: true) ?? "") .font(.system(.subheadline).monospacedDigit()) .foregroundColor(.secondary) @@ -87,7 +108,7 @@ struct ChapterView: View { struct ChapterView_Preview: PreviewProvider { static var previews: some View { - ChapterView(chapter: .init(title: "Chapter", start: 30)) + ChapterView(chapter: .init(title: "Chapter", start: 30), chapterIndex: 0) .injectFixtureEnvironmentObjects() } } diff --git a/Shared/Player/Video Details/ChaptersView.swift b/Shared/Player/Video Details/ChaptersView.swift index 5f2bb17f..55547886 100644 --- a/Shared/Player/Video Details/ChaptersView.swift +++ b/Shared/Player/Video Details/ChaptersView.swift @@ -30,8 +30,10 @@ struct ChaptersView: View { #else ScrollView(.horizontal) { LazyHStack(spacing: 20) { - ForEach(chapters) { chapter in - ChapterView(chapter: chapter) + ForEach(Array(chapters.indices), id: \.self) { index in + let chapter = chapters[index] + let nextChapterStart: Double? = index < chapters.count - 1 ? chapters[index + 1].start : nil + ChapterView(chapter: chapter, nextChapterStart: nextChapterStart, chapterIndex: index) } } .padding(.horizontal, 15) @@ -39,8 +41,10 @@ struct ChaptersView: View { #endif } else if expand { Section { - ForEach(chapters) { chapter in - ChapterView(chapter: chapter) + ForEach(Array(chapters.indices), id: \.self) { index in + let chapter = chapters[index] + let nextChapterStart: Double? = index < chapters.count - 1 ? chapters[index + 1].start : nil + ChapterView(chapter: chapter, nextChapterStart: nextChapterStart, chapterIndex: index) } } .padding(.horizontal) @@ -60,8 +64,10 @@ struct ChaptersView: View { var contents: some View { Section { - ForEach(chapters.prefix(3).indices, id: \.self) { index in - ChapterView(chapter: chapters[index]) + ForEach(Array(chapters.prefix(3).indices), id: \.self) { index in + let chapter = chapters[index] + let nextChapterStart: Double? = index < chapters.count - 1 ? chapters[index + 1].start : nil + ChapterView(chapter: chapter, nextChapterStart: nextChapterStart, chapterIndex: index) .allowsHitTesting(expand) .opacity(index == 0 ? 1.0 : 0.3) } From 13ef96cd02d6d8528750dd3e4f8f2579a0be3278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Tue, 28 Nov 2023 20:05:04 +0100 Subject: [PATCH 09/19] separate tvOS View --- Shared/Player/Video Details/ChapterView.swift | 177 +++++++++++------- .../Player/Video Details/ChaptersView.swift | 56 +++--- tvOS/NowPlayingView.swift | 2 +- 3 files changed, 136 insertions(+), 99 deletions(-) diff --git a/Shared/Player/Video Details/ChapterView.swift b/Shared/Player/Video Details/ChapterView.swift index ca10ace8..1e808338 100644 --- a/Shared/Player/Video Details/ChapterView.swift +++ b/Shared/Player/Video Details/ChapterView.swift @@ -3,65 +3,44 @@ import Foundation import SDWebImageSwiftUI import SwiftUI -struct ChapterView: View { - var chapter: Chapter - var nextChapterStart: Double? +#if !os(tvOS) + struct ChapterView: View { + var chapter: Chapter + var nextChapterStart: Double? - var chapterIndex: Int - @ObservedObject private var player = PlayerModel.shared + var chapterIndex: Int + @ObservedObject private var player = PlayerModel.shared - var isCurrentChapter: Bool { - player.currentChapterIndex == chapterIndex - } + var isCurrentChapter: Bool { + player.currentChapterIndex == chapterIndex + } - var hasBeenPlayed: Bool { - player.playedChapters.contains(chapterIndex) - } + var hasBeenPlayed: Bool { + player.playedChapters.contains(chapterIndex) + } - var body: some View { - Button(action: { - player.backend.seek(to: chapter.start, seekType: .userInteracted) - }) { - Group { - #if os(tvOS) - horizontalChapter - #else + var body: some View { + Button(action: { + player.backend.seek(to: chapter.start, seekType: .userInteracted) + }) { + Group { verticalChapter - #endif + } + .contentShape(Rectangle()) } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .onReceive(PlayerTimeModel.shared.$currentTime) { cmTime in - let time = CMTimeGetSeconds(cmTime) - if time >= self.chapter.start, self.nextChapterStart == nil || time < self.nextChapterStart! { - player.currentChapterIndex = self.chapterIndex - if !player.playedChapters.contains(self.chapterIndex) { - player.playedChapters.append(self.chapterIndex) + .buttonStyle(.plain) + + .onReceive(PlayerTimeModel.shared.$currentTime) { cmTime in + let time = CMTimeGetSeconds(cmTime) + if time >= self.chapter.start, self.nextChapterStart == nil || time < self.nextChapterStart! { + player.currentChapterIndex = self.chapterIndex + if !player.playedChapters.contains(self.chapterIndex) { + player.playedChapters.append(self.chapterIndex) + } } } } - } - #if os(tvOS) - - var horizontalChapter: some View { - HStack(spacing: 12) { - if !chapter.image.isNil { - smallImage(chapter) - } - - VStack(alignment: .leading, spacing: 4) { - Text(chapter.title) - .font(.headline) - Text(chapter.start.formattedAsPlaybackTime(allowZero: true) ?? "") - .font(.system(.subheadline).monospacedDigit()) - .foregroundColor(.secondary) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - } - #else var verticalChapter: some View { VStack(spacing: 12) { if !chapter.image.isNil { @@ -80,35 +59,91 @@ struct ChapterView: View { .frame(maxWidth: !chapter.image.isNil ? Self.thumbnailWidth : nil, alignment: .leading) } } - #endif - @ViewBuilder func smallImage(_ chapter: Chapter) -> some View { - WebImage(url: chapter.image, options: [.lowPriority]) - .resizable() - .placeholder { - ProgressView() + @ViewBuilder func smallImage(_ chapter: Chapter) -> some View { + WebImage(url: chapter.image, options: [.lowPriority]) + .resizable() + .placeholder { + ProgressView() + } + .indicator(.activity) + .frame(width: Self.thumbnailWidth, height: Self.thumbnailHeight) + + .mask(RoundedRectangle(cornerRadius: 6)) + } + + static var thumbnailWidth: Double { + 250 + } + + static var thumbnailHeight: Double { + thumbnailWidth / 1.7777 + } + } + +#else + struct ChapterViewTVOS: View { + var chapter: Chapter + var player = PlayerModel.shared + + var body: some View { + Button { + player.backend.seek(to: chapter.start, seekType: .userInteracted) + } label: { + Group { + horizontalChapter + } + .contentShape(Rectangle()) } - .indicator(.activity) - .frame(width: Self.thumbnailWidth, height: Self.thumbnailHeight) - #if os(tvOS) - .mask(RoundedRectangle(cornerRadius: 12)) - #else - .mask(RoundedRectangle(cornerRadius: 6)) - #endif - } + .buttonStyle(.plain) + } - static var thumbnailWidth: Double { - 250 - } + var horizontalChapter: some View { + HStack(spacing: 12) { + if !chapter.image.isNil { + smallImage(chapter) + } - static var thumbnailHeight: Double { - thumbnailWidth / 1.7777 + VStack(alignment: .leading, spacing: 4) { + Text(chapter.title) + .font(.headline) + Text(chapter.start.formattedAsPlaybackTime(allowZero: true) ?? "") + .font(.system(.subheadline).monospacedDigit()) + .foregroundColor(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + @ViewBuilder func smallImage(_ chapter: Chapter) -> some View { + WebImage(url: chapter.image, options: [.lowPriority]) + .resizable() + .placeholder { + ProgressView() + } + .indicator(.activity) + .frame(width: Self.thumbnailWidth, height: Self.thumbnailHeight) + .mask(RoundedRectangle(cornerRadius: 12)) + } + + static var thumbnailWidth: Double { + 250 + } + + static var thumbnailHeight: Double { + thumbnailWidth / 1.7777 + } } -} +#endif struct ChapterView_Preview: PreviewProvider { static var previews: some View { - ChapterView(chapter: .init(title: "Chapter", start: 30), chapterIndex: 0) - .injectFixtureEnvironmentObjects() + #if os(tvOS) + ChapterViewTVOS(chapter: .init(title: "Chapter", start: 30)) + .injectFixtureEnvironmentObjects() + #else + ChapterView(chapter: .init(title: "Chapter", start: 30), nextChapterStart: nil, chapterIndex: 0) + .injectFixtureEnvironmentObjects() + #endif } } diff --git a/Shared/Player/Video Details/ChaptersView.swift b/Shared/Player/Video Details/ChaptersView.swift index 55547886..5b02d5db 100644 --- a/Shared/Player/Video Details/ChaptersView.swift +++ b/Shared/Player/Video Details/ChaptersView.swift @@ -21,7 +21,7 @@ struct ChaptersView: View { List { Section { ForEach(chapters) { chapter in - ChapterView(chapter: chapter) + ChapterViewTVOS(chapter: chapter) } } .listRowBackground(Color.clear) @@ -29,51 +29,53 @@ struct ChaptersView: View { .listStyle(.plain) #else ScrollView(.horizontal) { - LazyHStack(spacing: 20) { - ForEach(Array(chapters.indices), id: \.self) { index in - let chapter = chapters[index] - let nextChapterStart: Double? = index < chapters.count - 1 ? chapters[index + 1].start : nil - ChapterView(chapter: chapter, nextChapterStart: nextChapterStart, chapterIndex: index) - } - } - .padding(.horizontal, 15) + LazyHStack(spacing: 20) { chapterViews(for: chapters.prefix(3), opacity: 1.0) }.padding(.horizontal, 15) } #endif } else if expand { - Section { - ForEach(Array(chapters.indices), id: \.self) { index in - let chapter = chapters[index] - let nextChapterStart: Double? = index < chapters.count - 1 ? chapters[index + 1].start : nil - ChapterView(chapter: chapter, nextChapterStart: nextChapterStart, chapterIndex: index) + #if os(tvOS) + Section { + ForEach(chapters) { chapter in + ChapterViewTVOS(chapter: chapter) + } } - } - .padding(.horizontal) + #else + Section { chapterViews(for: chapters[...], opacity: 1.0) }.padding(.horizontal) + #endif } else { #if os(iOS) Button(action: { self.expand.toggle() }) { - contents + Section { + chapterViews(for: chapters.prefix(3), opacity: 0.3) + }.padding(.horizontal) } + #elseif os(macOS) + Section { + chapterViews(for: chapters.prefix(3), opacity: 0.3) + }.padding(.horizontal) #else - contents + Section { + ForEach(chapters) { chapter in + ChapterViewTVOS(chapter: chapter) + } + } #endif } } } - var contents: some View { - Section { - ForEach(Array(chapters.prefix(3).indices), id: \.self) { index in - let chapter = chapters[index] - let nextChapterStart: Double? = index < chapters.count - 1 ? chapters[index + 1].start : nil + #if !os(tvOS) + private func chapterViews(for chaptersToShow: ArraySlice, opacity: Double = 1.0) -> some View { + ForEach(Array(chaptersToShow.indices), id: \.self) { index in + let chapter = chaptersToShow[index] + let nextChapterStart: Double? = index < chaptersToShow.count - 1 ? chaptersToShow[index + 1].start : nil ChapterView(chapter: chapter, nextChapterStart: nextChapterStart, chapterIndex: index) - .allowsHitTesting(expand) - .opacity(index == 0 ? 1.0 : 0.3) + .opacity(index == 0 ? 1.0 : opacity) } } - .padding(.horizontal) - } + #endif } struct ChaptersView_Previews: PreviewProvider { diff --git a/tvOS/NowPlayingView.swift b/tvOS/NowPlayingView.swift index 1502790f..14f3976a 100644 --- a/tvOS/NowPlayingView.swift +++ b/tvOS/NowPlayingView.swift @@ -130,7 +130,7 @@ struct NowPlayingView: View { } else { Section(header: Text("Chapters")) { ForEach(video.chapters) { chapter in - ChapterView(chapter: chapter) + ChapterViewTVOS(chapter: chapter) .padding(.horizontal, 40) } } From 7de702ad23efe4125e63c9d8b441df306d52ad1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Tue, 28 Nov 2023 23:06:37 +0100 Subject: [PATCH 10/19] change color to appRed for chapter --- Shared/Player/Video Details/ChapterView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Shared/Player/Video Details/ChapterView.swift b/Shared/Player/Video Details/ChapterView.swift index 1e808338..8de2320e 100644 --- a/Shared/Player/Video Details/ChapterView.swift +++ b/Shared/Player/Video Details/ChapterView.swift @@ -29,7 +29,6 @@ import SwiftUI .contentShape(Rectangle()) } .buttonStyle(.plain) - .onReceive(PlayerTimeModel.shared.$currentTime) { cmTime in let time = CMTimeGetSeconds(cmTime) if time >= self.chapter.start, self.nextChapterStart == nil || time < self.nextChapterStart! { @@ -51,7 +50,7 @@ import SwiftUI .lineLimit(3) .multilineTextAlignment(.leading) .font(.headline) - .foregroundColor(isCurrentChapter ? .detailBadgeOutstandingStyleBackground : .primary) + .foregroundColor(isCurrentChapter ? .appRed : .primary) Text(chapter.start.formattedAsPlaybackTime(allowZero: true) ?? "") .font(.system(.subheadline).monospacedDigit()) .foregroundColor(.secondary) From 982dca18463b0ba1707b7ddfce8c59f01cff5ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Wed, 29 Nov 2023 00:31:53 +0100 Subject: [PATCH 11/19] highlight current chapter when clicked on it --- Shared/Player/Video Details/ChapterView.swift | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Shared/Player/Video Details/ChapterView.swift b/Shared/Player/Video Details/ChapterView.swift index 8de2320e..97f85eb3 100644 --- a/Shared/Player/Video Details/ChapterView.swift +++ b/Shared/Player/Video Details/ChapterView.swift @@ -22,6 +22,10 @@ import SwiftUI var body: some View { Button(action: { player.backend.seek(to: chapter.start, seekType: .userInteracted) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Introducing a delay to give the player a chance to skip to the chapter + PlayerTimeModel.shared.currentTime = CMTime(seconds: chapter.start, preferredTimescale: 1) + handleTimeUpdate(PlayerTimeModel.shared.currentTime) + } }) { Group { verticalChapter @@ -30,13 +34,7 @@ import SwiftUI } .buttonStyle(.plain) .onReceive(PlayerTimeModel.shared.$currentTime) { cmTime in - let time = CMTimeGetSeconds(cmTime) - if time >= self.chapter.start, self.nextChapterStart == nil || time < self.nextChapterStart! { - player.currentChapterIndex = self.chapterIndex - if !player.playedChapters.contains(self.chapterIndex) { - player.playedChapters.append(self.chapterIndex) - } - } + self.handleTimeUpdate(cmTime) } } @@ -50,7 +48,7 @@ import SwiftUI .lineLimit(3) .multilineTextAlignment(.leading) .font(.headline) - .foregroundColor(isCurrentChapter ? .appRed : .primary) + .foregroundColor(isCurrentChapter ? Color("AppRedColor") : .primary) Text(chapter.start.formattedAsPlaybackTime(allowZero: true) ?? "") .font(.system(.subheadline).monospacedDigit()) .foregroundColor(.secondary) @@ -78,6 +76,16 @@ import SwiftUI static var thumbnailHeight: Double { thumbnailWidth / 1.7777 } + + private func handleTimeUpdate(_ cmTime: CMTime) { + let time = CMTimeGetSeconds(cmTime) + if time >= chapter.start, nextChapterStart == nil || time < nextChapterStart! { + player.currentChapterIndex = chapterIndex + if !player.playedChapters.contains(chapterIndex) { + player.playedChapters.append(chapterIndex) + } + } + } } #else From aa5d6733b2baa0b181dcd7fe17fb8f75267ba665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Sun, 3 Dec 2023 20:04:57 +0100 Subject: [PATCH 12/19] remove superfluous vars and chaptermodel --- Model/Player/PlayerModel.swift | 3 +-- Shared/Player/Video Details/ChapterView.swift | 12 +++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 1da1d8c8..e6c33702 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -131,8 +131,7 @@ final class PlayerModel: ObservableObject { @Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen #endif - @Published var playedChapters: [Int] = [] - @Published var currentChapterIndex: Int? + @Published var currentChapter: Int? var accounts: AccountsModel { .shared } var comments: CommentsModel { .shared } diff --git a/Shared/Player/Video Details/ChapterView.swift b/Shared/Player/Video Details/ChapterView.swift index 97f85eb3..52e2dc35 100644 --- a/Shared/Player/Video Details/ChapterView.swift +++ b/Shared/Player/Video Details/ChapterView.swift @@ -12,11 +12,7 @@ import SwiftUI @ObservedObject private var player = PlayerModel.shared var isCurrentChapter: Bool { - player.currentChapterIndex == chapterIndex - } - - var hasBeenPlayed: Bool { - player.playedChapters.contains(chapterIndex) + player.currentChapter == chapterIndex } var body: some View { @@ -35,6 +31,7 @@ import SwiftUI .buttonStyle(.plain) .onReceive(PlayerTimeModel.shared.$currentTime) { cmTime in self.handleTimeUpdate(cmTime) + print("currentChapterIndex:", player.currentChapter ?? 0) } } @@ -80,10 +77,7 @@ import SwiftUI private func handleTimeUpdate(_ cmTime: CMTime) { let time = CMTimeGetSeconds(cmTime) if time >= chapter.start, nextChapterStart == nil || time < nextChapterStart! { - player.currentChapterIndex = chapterIndex - if !player.playedChapters.contains(chapterIndex) { - player.playedChapters.append(chapterIndex) - } + player.currentChapter = chapterIndex } } } From 586cea7d441d1e86731adc29f18803c51a932aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 4 Dec 2023 00:07:39 +0100 Subject: [PATCH 13/19] subscribe chapters to currentTime notification --- Model/Player/Backends/MPVBackend.swift | 10 ++++++++++ Shared/Player/Video Details/ChapterView.swift | 15 ++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 773251d8..09a5d05b 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -191,6 +191,10 @@ final class MPVBackend: PlayerBackend { } } + deinit { + NotificationCenter.default.removeObserver(self, name: .getTimeUpdatesNotification, object: self.currentTime) + } + typealias AreInIncreasingOrder = (Stream, Stream) -> Bool func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? { @@ -432,6 +436,8 @@ final class MPVBackend: PlayerBackend { timeObserverThrottle.execute { self.model.updateWatch(time: self.currentTime) } + + NotificationCenter.default.post(name: .getTimeUpdatesNotification, object: self.currentTime) } private func stopClientUpdates() { @@ -618,3 +624,7 @@ final class MPVBackend: PlayerBackend { } } } + +extension Notification.Name { + static let getTimeUpdatesNotification = Notification.Name("getTimeUpdatesNotification") +} diff --git a/Shared/Player/Video Details/ChapterView.swift b/Shared/Player/Video Details/ChapterView.swift index 52e2dc35..216cd5e2 100644 --- a/Shared/Player/Video Details/ChapterView.swift +++ b/Shared/Player/Video Details/ChapterView.swift @@ -18,10 +18,6 @@ import SwiftUI var body: some View { Button(action: { player.backend.seek(to: chapter.start, seekType: .userInteracted) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Introducing a delay to give the player a chance to skip to the chapter - PlayerTimeModel.shared.currentTime = CMTime(seconds: chapter.start, preferredTimescale: 1) - handleTimeUpdate(PlayerTimeModel.shared.currentTime) - } }) { Group { verticalChapter @@ -29,9 +25,14 @@ import SwiftUI .contentShape(Rectangle()) } .buttonStyle(.plain) - .onReceive(PlayerTimeModel.shared.$currentTime) { cmTime in - self.handleTimeUpdate(cmTime) - print("currentChapterIndex:", player.currentChapter ?? 0) + .onReceive( + NotificationCenter.default + .publisher(for: .getTimeUpdatesNotification) + .receive(on: DispatchQueue.main) + ) { notification in + if let cmTime = notification.object as? CMTime { + self.handleTimeUpdate(cmTime) + } } } From d65224320e58e8c70dc1e3cb1b4f3d059456a998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 4 Dec 2023 00:39:29 +0100 Subject: [PATCH 14/19] chapters can only be clicked if expanded or horizontal --- Shared/Player/Video Details/ChaptersView.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Shared/Player/Video Details/ChaptersView.swift b/Shared/Player/Video Details/ChaptersView.swift index 5b02d5db..36f369ea 100644 --- a/Shared/Player/Video Details/ChaptersView.swift +++ b/Shared/Player/Video Details/ChaptersView.swift @@ -29,7 +29,7 @@ struct ChaptersView: View { .listStyle(.plain) #else ScrollView(.horizontal) { - LazyHStack(spacing: 20) { chapterViews(for: chapters.prefix(3), opacity: 1.0) }.padding(.horizontal, 15) + LazyHStack(spacing: 20) { chapterViews(for: chapters[...]) }.padding(.horizontal, 15) } #endif } else if expand { @@ -40,7 +40,7 @@ struct ChaptersView: View { } } #else - Section { chapterViews(for: chapters[...], opacity: 1.0) }.padding(.horizontal) + Section { chapterViews(for: chapters[...]) }.padding(.horizontal) #endif } else { #if os(iOS) @@ -48,12 +48,12 @@ struct ChaptersView: View { self.expand.toggle() }) { Section { - chapterViews(for: chapters.prefix(3), opacity: 0.3) + chapterViews(for: chapters.prefix(3), opacity: 0.3, clickable: false) }.padding(.horizontal) } #elseif os(macOS) Section { - chapterViews(for: chapters.prefix(3), opacity: 0.3) + chapterViews(for: chapters.prefix(3), opacity: 0.3, clickable: false) }.padding(.horizontal) #else Section { @@ -67,12 +67,13 @@ struct ChaptersView: View { } #if !os(tvOS) - private func chapterViews(for chaptersToShow: ArraySlice, opacity: Double = 1.0) -> some View { + private func chapterViews(for chaptersToShow: ArraySlice, opacity: Double = 1.0, clickable: Bool = true) -> some View { ForEach(Array(chaptersToShow.indices), id: \.self) { index in let chapter = chaptersToShow[index] let nextChapterStart: Double? = index < chaptersToShow.count - 1 ? chaptersToShow[index + 1].start : nil ChapterView(chapter: chapter, nextChapterStart: nextChapterStart, chapterIndex: index) .opacity(index == 0 ? 1.0 : opacity) + .allowsHitTesting(clickable) } } #endif From 600b8d198b8ec03ea7ce83ef60bb0737b0317ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 4 Dec 2023 01:04:39 +0100 Subject: [PATCH 15/19] add option to expand vertical chapters by default --- Shared/Defaults.swift | 1 + Shared/Player/Video Details/VideoDetails.swift | 2 ++ Shared/Settings/PlayerSettings.swift | 8 ++++++++ 3 files changed, 11 insertions(+) diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index f1ed24b6..f6e86d08 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -265,6 +265,7 @@ extension Defaults.Keys { static let hideWatched = Key("hideWatched", default: false) static let showInspector = Key("showInspector", default: .onlyLocal) static let showChapters = Key("showChapters", default: true) + static let expandChapters = Key("expandChapters", default: true) static let showRelated = Key("showRelated", default: true) static let widgetsSettings = Key<[WidgetSettings]>("widgetsSettings", default: []) } diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift index 43a23600..1da9b6d8 100644 --- a/Shared/Player/Video Details/VideoDetails.swift +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -191,6 +191,7 @@ struct VideoDetails: View { @Default(.showScrollToTopInComments) private var showScrollToTopInComments #endif @Default(.expandVideoDescription) private var expandVideoDescription + @Default(.expandChapters) private var expandChapters var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -246,6 +247,7 @@ struct VideoDetails: View { .background(colorScheme == .dark ? Color.black : .white) .onAppear { descriptionExpanded = expandVideoDescription + chaptersExpanded = expandChapters } } diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index a2090359..0ae8defd 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -32,6 +32,7 @@ struct PlayerSettings: View { @Default(.showInspector) private var showInspector @Default(.showChapters) private var showChapters + @Default(.expandChapters) private var expandChapters @Default(.showRelated) private var showRelated @ObservedObject private var accounts = AccountsModel.shared @@ -80,6 +81,7 @@ struct PlayerSettings: View { expandVideoDescriptionToggle collapsedLineDescriptionStepper showChaptersToggle + expandChaptersToggle showRelatedToggle #if os(macOS) HStack { @@ -285,6 +287,12 @@ struct PlayerSettings: View { Toggle("Chapters (if available)", isOn: $showChapters) } + private var expandChaptersToggle: some View { + Toggle("Open vertical chapters expanded", isOn: $expandChapters) + .disabled(!showChapters) + .foregroundColor(showChapters ? .primary : .secondary) + } + private var showRelatedToggle: some View { Toggle("Related", isOn: $showRelated) } From 0d9c27319d140de6c95b132307b1db9b7294b14a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 4 Dec 2023 14:47:26 +0100 Subject: [PATCH 16/19] make time updates work for .mpv and .avp - time update notifications work for both backends - only init mpv timers when mpv is the active backend - move notification extension to playerbackend --- Model/Player/Backends/AVPlayerBackend.swift | 6 ++++++ Model/Player/Backends/MPVBackend.swift | 14 ++++++++------ Model/Player/Backends/PlayerBackend.swift | 4 ++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index bba20062..3ffbbb6f 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -116,6 +116,10 @@ final class AVPlayerBackend: PlayerBackend { #endif } + deinit { + NotificationCenter.default.removeObserver(self, name: .getTimeUpdatesNotification, object: self.currentTime) + } + func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? { let sortedByResolution = streams .filter { ($0.kind == .adaptive || $0.kind == .stream) && $0.resolution <= maxResolution.value } @@ -596,6 +600,8 @@ final class AVPlayerBackend: PlayerBackend { if self.controlsUpdates { self.updateControls() } + + NotificationCenter.default.post(name: .getTimeUpdatesNotification, object: self.currentTime) } } diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 09a5d05b..5de59b13 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -183,11 +183,17 @@ final class MPVBackend: PlayerBackend { init() { clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in - self?.getTimeUpdates() + guard let self = self, self.model.activeBackend == .mpv else { + return + } + self.getTimeUpdates() } networkStateTimer = .init(interval: .seconds(Self.networkStateUpdateInterval), mode: .infinite) { [weak self] _ in - self?.updateNetworkState() + guard let self = self, self.model.activeBackend == .mpv else { + return + } + self.updateNetworkState() } } @@ -624,7 +630,3 @@ final class MPVBackend: PlayerBackend { } } } - -extension Notification.Name { - static let getTimeUpdatesNotification = Notification.Name("getTimeUpdatesNotification") -} diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index 3b09e957..45348a0b 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -154,3 +154,7 @@ extension PlayerBackend { } } } + +extension Notification.Name { + static let getTimeUpdatesNotification = Notification.Name("getTimeUpdatesNotification") +} From d361ef01d42c4f3c11e400548442d2b9da644e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 4 Dec 2023 21:58:49 +0100 Subject: [PATCH 17/19] move updating the time to PlayerModel this makes the chapter view much much smoother --- Model/Player/PlayerModel.swift | 34 ++++++++++++++++++- Shared/Player/Video Details/ChapterView.swift | 14 ++------ .../Player/Video Details/ChaptersView.swift | 3 +- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index e6c33702..d91ae3eb 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -131,7 +131,7 @@ final class PlayerModel: ObservableObject { @Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen #endif - @Published var currentChapter: Int? + @Published var currentChapterIndex: Int? var accounts: AccountsModel { .shared } var comments: CommentsModel { .shared } @@ -1114,4 +1114,36 @@ final class PlayerModel: ObservableObject { onPlayStream.forEach { $0(stream) } onPlayStream.removeAll() } + + func updateTime(_ cmTime: CMTime) { + let time = CMTimeGetSeconds(cmTime) + let newChapterIndex = chapterForTime(time) + if currentChapterIndex != newChapterIndex { + DispatchQueue.main.async { + self.currentChapterIndex = newChapterIndex + } + } + } + + private func chapterForTime(_ time: Double) -> Int? { + guard let chapters = self.videoForDisplay?.chapters else { + return nil + } + + for (index, chapter) in chapters.enumerated() { + let nextChapterStartTime = index < (chapters.count - 1) ? chapters[index + 1].start : nil + + if let nextChapterStart = nextChapterStartTime { + if time >= chapter.start, time < nextChapterStart { + return index + } + } else { + if time >= chapter.start { + return index + } + } + } + + return nil + } } diff --git a/Shared/Player/Video Details/ChapterView.swift b/Shared/Player/Video Details/ChapterView.swift index 216cd5e2..f2646a64 100644 --- a/Shared/Player/Video Details/ChapterView.swift +++ b/Shared/Player/Video Details/ChapterView.swift @@ -6,13 +6,12 @@ import SwiftUI #if !os(tvOS) struct ChapterView: View { var chapter: Chapter - var nextChapterStart: Double? var chapterIndex: Int @ObservedObject private var player = PlayerModel.shared var isCurrentChapter: Bool { - player.currentChapter == chapterIndex + player.currentChapterIndex == chapterIndex } var body: some View { @@ -31,7 +30,7 @@ import SwiftUI .receive(on: DispatchQueue.main) ) { notification in if let cmTime = notification.object as? CMTime { - self.handleTimeUpdate(cmTime) + player.updateTime(cmTime) } } } @@ -74,13 +73,6 @@ import SwiftUI static var thumbnailHeight: Double { thumbnailWidth / 1.7777 } - - private func handleTimeUpdate(_ cmTime: CMTime) { - let time = CMTimeGetSeconds(cmTime) - if time >= chapter.start, nextChapterStart == nil || time < nextChapterStart! { - player.currentChapter = chapterIndex - } - } } #else @@ -144,7 +136,7 @@ struct ChapterView_Preview: PreviewProvider { ChapterViewTVOS(chapter: .init(title: "Chapter", start: 30)) .injectFixtureEnvironmentObjects() #else - ChapterView(chapter: .init(title: "Chapter", start: 30), nextChapterStart: nil, chapterIndex: 0) + ChapterView(chapter: .init(title: "Chapter", start: 30), chapterIndex: 0) .injectFixtureEnvironmentObjects() #endif } diff --git a/Shared/Player/Video Details/ChaptersView.swift b/Shared/Player/Video Details/ChaptersView.swift index 36f369ea..65baa874 100644 --- a/Shared/Player/Video Details/ChaptersView.swift +++ b/Shared/Player/Video Details/ChaptersView.swift @@ -70,8 +70,7 @@ struct ChaptersView: View { private func chapterViews(for chaptersToShow: ArraySlice, opacity: Double = 1.0, clickable: Bool = true) -> some View { ForEach(Array(chaptersToShow.indices), id: \.self) { index in let chapter = chaptersToShow[index] - let nextChapterStart: Double? = index < chaptersToShow.count - 1 ? chaptersToShow[index + 1].start : nil - ChapterView(chapter: chapter, nextChapterStart: nextChapterStart, chapterIndex: index) + ChapterView(chapter: chapter, chapterIndex: index) .opacity(index == 0 ? 1.0 : opacity) .allowsHitTesting(clickable) } From 4ca8adc4dd167303e93b2d456fc30ba9dc8a1f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Mon, 4 Dec 2023 23:00:20 +0100 Subject: [PATCH 18/19] move onReceive to the chapter header in VideoDetails --- Shared/Player/Video Details/ChapterView.swift | 10 ---------- Shared/Player/Video Details/VideoDetails.swift | 10 ++++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Shared/Player/Video Details/ChapterView.swift b/Shared/Player/Video Details/ChapterView.swift index f2646a64..8a5fc0d2 100644 --- a/Shared/Player/Video Details/ChapterView.swift +++ b/Shared/Player/Video Details/ChapterView.swift @@ -1,4 +1,3 @@ -import CoreMedia import Foundation import SDWebImageSwiftUI import SwiftUI @@ -24,15 +23,6 @@ import SwiftUI .contentShape(Rectangle()) } .buttonStyle(.plain) - .onReceive( - NotificationCenter.default - .publisher(for: .getTimeUpdatesNotification) - .receive(on: DispatchQueue.main) - ) { notification in - if let cmTime = notification.object as? CMTime { - player.updateTime(cmTime) - } - } } var verticalChapter: some View { diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift index 1da9b6d8..7432dc40 100644 --- a/Shared/Player/Video Details/VideoDetails.swift +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -1,3 +1,4 @@ +import CoreMedia import Defaults import Foundation import SDWebImageSwiftUI @@ -485,6 +486,15 @@ struct VideoDetails: View { .padding(.horizontal) } } + .onReceive( + NotificationCenter.default + .publisher(for: .getTimeUpdatesNotification) + .receive(on: DispatchQueue.main) + ) { notification in + if let cmTime = notification.object as? CMTime { + player.updateTime(cmTime) + } + } } } From 721a97dc41aace5ac00c32351a14bb3a0b9ffe45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20F=C3=B6rster?= Date: Tue, 5 Dec 2023 00:07:36 +0100 Subject: [PATCH 19/19] no need for NotificationCenter --- Model/Player/Backends/AVPlayerBackend.swift | 6 +----- Model/Player/Backends/MPVBackend.swift | 8 +++----- Model/Player/Backends/PlayerBackend.swift | 4 ---- Shared/Player/Video Details/VideoDetails.swift | 10 ---------- 4 files changed, 4 insertions(+), 24 deletions(-) diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index 3ffbbb6f..7e71208f 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -116,10 +116,6 @@ final class AVPlayerBackend: PlayerBackend { #endif } - deinit { - NotificationCenter.default.removeObserver(self, name: .getTimeUpdatesNotification, object: self.currentTime) - } - func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? { let sortedByResolution = streams .filter { ($0.kind == .adaptive || $0.kind == .stream) && $0.resolution <= maxResolution.value } @@ -601,7 +597,7 @@ final class AVPlayerBackend: PlayerBackend { self.updateControls() } - NotificationCenter.default.post(name: .getTimeUpdatesNotification, object: self.currentTime) + self.model.updateTime(self.currentTime!) } } diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 5de59b13..c8457946 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -182,6 +182,7 @@ final class MPVBackend: PlayerBackend { } init() { + // swiftlint:disable shorthand_optional_binding clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in guard let self = self, self.model.activeBackend == .mpv else { return @@ -195,10 +196,7 @@ final class MPVBackend: PlayerBackend { } self.updateNetworkState() } - } - - deinit { - NotificationCenter.default.removeObserver(self, name: .getTimeUpdatesNotification, object: self.currentTime) + // swiftlint:enable shorthand_optional_binding } typealias AreInIncreasingOrder = (Stream, Stream) -> Bool @@ -443,7 +441,7 @@ final class MPVBackend: PlayerBackend { self.model.updateWatch(time: self.currentTime) } - NotificationCenter.default.post(name: .getTimeUpdatesNotification, object: self.currentTime) + self.model.updateTime(self.currentTime!) } private func stopClientUpdates() { diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index 45348a0b..3b09e957 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -154,7 +154,3 @@ extension PlayerBackend { } } } - -extension Notification.Name { - static let getTimeUpdatesNotification = Notification.Name("getTimeUpdatesNotification") -} diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift index 7432dc40..1da9b6d8 100644 --- a/Shared/Player/Video Details/VideoDetails.swift +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -1,4 +1,3 @@ -import CoreMedia import Defaults import Foundation import SDWebImageSwiftUI @@ -486,15 +485,6 @@ struct VideoDetails: View { .padding(.horizontal) } } - .onReceive( - NotificationCenter.default - .publisher(for: .getTimeUpdatesNotification) - .receive(on: DispatchQueue.main) - ) { notification in - if let cmTime = notification.object as? CMTime { - player.updateTime(cmTime) - } - } } }