diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 4e8499e8..114504ff 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -77,6 +77,8 @@ extension Defaults.Keys { static let collapsedLinesDescription = Key("collapsedLinesDescription", default: 5) static let showChapters = Key("showChapters", default: true) + static let showChapterThumbnails = Key("showChapterThumbnails", default: true) + static let showChapterThumbnailsOnlyWhenDifferent = Key("showChapterThumbnailsOnlyWhenDifferent", default: true) static let expandChapters = Key("expandChapters", default: true) static let showRelated = Key("showRelated", default: true) static let showInspector = Key("showInspector", default: .onlyLocal) diff --git a/Shared/Player/Video Details/ChapterView.swift b/Shared/Player/Video Details/ChapterView.swift index aaad5b87..54362637 100644 --- a/Shared/Player/Video Details/ChapterView.swift +++ b/Shared/Player/Video Details/ChapterView.swift @@ -9,8 +9,13 @@ import SwiftUI var chapterIndex: Int @ObservedObject private var player = PlayerModel.shared + var showThumbnail: Bool + var isCurrentChapter: Bool { - player.currentChapterIndex == chapterIndex + if let currentChapterIndex = player.currentChapterIndex { + return currentChapterIndex == chapterIndex + } + return false } var body: some View { @@ -27,7 +32,7 @@ import SwiftUI var verticalChapter: some View { VStack(spacing: 12) { - if !chapter.image.isNil { + if !chapter.image.isNil, showThumbnail { smallImage(chapter) } VStack(alignment: .leading, spacing: 4) { @@ -40,7 +45,7 @@ import SwiftUI .font(.system(.subheadline).monospacedDigit()) .foregroundColor(.secondary) } - .frame(maxWidth: !chapter.image.isNil ? Self.thumbnailWidth : nil, alignment: .leading) + .frame(maxWidth: !chapter.image.isNil && showThumbnail ? Self.thumbnailWidth : nil, alignment: .leading) } } @@ -126,7 +131,7 @@ struct ChapterView_Preview: PreviewProvider { ChapterViewTVOS(chapter: .init(title: "Chapter", start: 30)) .injectFixtureEnvironmentObjects() #else - ChapterView(chapter: .init(title: "Chapter", start: 30), chapterIndex: 0) + ChapterView(chapter: .init(title: "Chapter", start: 30), chapterIndex: 0, showThumbnail: true) .injectFixtureEnvironmentObjects() #endif } diff --git a/Shared/Player/Video Details/ChaptersView.swift b/Shared/Player/Video Details/ChaptersView.swift index 65baa874..f43a4503 100644 --- a/Shared/Player/Video Details/ChaptersView.swift +++ b/Shared/Player/Video Details/ChaptersView.swift @@ -5,18 +5,16 @@ import SwiftUI struct ChaptersView: View { @ObservedObject private var player = PlayerModel.shared @Binding var expand: Bool + let chaptersHaveImages: Bool + let showThumbnails: Bool var chapters: [Chapter] { player.videoForDisplay?.chapters ?? [] } - var chaptersHaveImages: Bool { - chapters.allSatisfy { $0.image != nil } - } - var body: some View { if !chapters.isEmpty { - if chaptersHaveImages { + if chaptersHaveImages, showThumbnails { #if os(tvOS) List { Section { @@ -29,7 +27,22 @@ struct ChaptersView: View { .listStyle(.plain) #else ScrollView(.horizontal) { - LazyHStack(spacing: 20) { chapterViews(for: chapters[...]) }.padding(.horizontal, 15) + ScrollViewReader { scrollViewProxy in + LazyHStack(spacing: 20) { + chapterViews(for: chapters[...], scrollViewProxy: scrollViewProxy) + } + .padding(.horizontal, 15) + .onAppear { + if let currentChapterIndex = player.currentChapterIndex { + scrollViewProxy.scrollTo(currentChapterIndex, anchor: .center) + } + } + .onChange(of: player.currentChapterIndex) { currentChapterIndex in + if let index = currentChapterIndex { + scrollViewProxy.scrollTo(index, anchor: .center) + } + } + } } #endif } else if expand { @@ -67,10 +80,11 @@ struct ChaptersView: View { } #if !os(tvOS) - private func chapterViews(for chaptersToShow: ArraySlice, opacity: Double = 1.0, clickable: Bool = true) -> some View { + private func chapterViews(for chaptersToShow: ArraySlice, opacity: Double = 1.0, clickable: Bool = true, scrollViewProxy: ScrollViewProxy? = nil) -> some View { ForEach(Array(chaptersToShow.indices), id: \.self) { index in let chapter = chaptersToShow[index] - ChapterView(chapter: chapter, chapterIndex: index) + ChapterView(chapter: chapter, chapterIndex: index, showThumbnail: showThumbnails) + .id(index) .opacity(index == 0 ? 1.0 : opacity) .allowsHitTesting(clickable) } @@ -80,7 +94,7 @@ struct ChaptersView: View { struct ChaptersView_Previews: PreviewProvider { static var previews: some View { - ChaptersView(expand: .constant(false)) + ChaptersView(expand: .constant(false), chaptersHaveImages: false, showThumbnails: true) .injectFixtureEnvironmentObjects() } } diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift index 1da9b6d8..2656ee2d 100644 --- a/Shared/Player/Video Details/VideoDetails.swift +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -186,6 +186,8 @@ struct VideoDetails: View { @Default(.playerSidebar) private var playerSidebar @Default(.showInspector) private var showInspector @Default(.showChapters) private var showChapters + @Default(.showChapterThumbnails) private var showChapterThumbnails + @Default(.showChapterThumbnailsOnlyWhenDifferent) private var showChapterThumbnailsOnlyWhenDifferent @Default(.showRelated) private var showRelated #if !os(tvOS) @Default(.showScrollToTopInComments) private var showScrollToTopInComments @@ -287,6 +289,63 @@ struct VideoDetails: View { } } + func infoView(video: Video) -> some View { + VStack(alignment: .leading, spacing: 10) { + if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) { + VStack { + ProgressView() + .progressViewStyle(.circular) + } + .frame(maxWidth: .infinity) + } else if let description = video.description, !description.isEmpty { + Section(header: descriptionHeader) { + VideoDescription(video: video, detailsSize: detailsSize, expand: $descriptionExpanded) + .padding(.horizontal) + } + } else if !video.isLocal { + Text("No description") + .font(.caption) + .foregroundColor(.secondary) + .padding(.horizontal) + } + + if player.videoBeingOpened.isNil { + if showChapters, + !video.isLocal, + !video.chapters.isEmpty + { + Section(header: chaptersHeader) { + ChaptersView(expand: $chaptersExpanded, chaptersHaveImages: chaptersHaveImages, showThumbnails: showThumbnails) + } + } + + if showInspector == .always || video.isLocal { + InspectorView(video: player.videoForDisplay) + .padding(.horizontal) + } + + if showRelated, + !sidebarQueue, + !(player.videoForDisplay?.related.isEmpty ?? true) + { + RelatedView() + .padding(.horizontal) + .padding(.top, 20) + } + } + } + .onAppear { + if !pageAvailable(page) { + page = .info + } + } + .transition(.opacity) + .animation(nil, value: player.currentItem) + #if os(iOS) + .frame(maxWidth: YatteeApp.isForPreviews ? .infinity : maxWidth) + #endif + } + var pageView: some View { ScrollView(.vertical) { LazyVStack { @@ -296,69 +355,12 @@ struct VideoDetails: View { switch page { case .info: - Group { - if let video { - VStack(alignment: .leading, spacing: 10) { - if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) { - VStack { - ProgressView() - .progressViewStyle(.circular) - } - .frame(maxWidth: .infinity) - } else if let description = video.description, !description.isEmpty { - Section(header: descriptionHeader) { - VideoDescription(video: video, detailsSize: detailsSize, expand: $descriptionExpanded) - .padding(.horizontal) - } - } else if !video.isLocal { - Text("No description") - .font(.caption) - .foregroundColor(.secondary) - .padding(.horizontal) - } - - if player.videoBeingOpened.isNil { - if showChapters, - !video.isLocal, - !video.chapters.isEmpty - { - Section(header: chaptersHeader) { - ChaptersView(expand: $chaptersExpanded) - } - } - - if showInspector == .always || video.isLocal { - InspectorView(video: player.videoForDisplay) - .padding(.horizontal) - } - - if showRelated, - !sidebarQueue, - !(player.videoForDisplay?.related.isEmpty ?? true) - { - RelatedView() - .padding(.horizontal) - .padding(.top, 20) - } - } - } - } + if let video = self.video { + infoView(video: video) } - .onAppear { - if video != nil, !pageAvailable(page) { - page = .info - } - } - .transition(.opacity) - .animation(nil, value: player.currentItem) - #if os(iOS) - .frame(maxWidth: YatteeApp.isForPreviews ? .infinity : maxWidth) - #endif - case .queue: PlayerQueueView(sidebarQueue: false) .padding(.horizontal) - case .comments: CommentsView() .onAppear { @@ -447,9 +449,27 @@ struct VideoDetails: View { player.videoForDisplay?.chapters.allSatisfy { $0.image != nil } ?? false } + var chapterImagesTheSame: Bool { + guard let firstChapterURL = player.videoForDisplay?.chapters.first?.image else { + return false + } + + return player.videoForDisplay?.chapters.allSatisfy { $0.image == firstChapterURL } ?? false + } + + var showThumbnails: Bool { + if !chaptersHaveImages || !showChapterThumbnails { + return false + } + if showChapterThumbnailsOnlyWhenDifferent { + return !chapterImagesTheSame + } + return true + } + var chaptersHeader: some View { Group { - if !chaptersHaveImages { + if !chaptersHaveImages || !showThumbnails { #if canImport(UIKit) Button(action: { chaptersExpanded.toggle() diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index 0ae8defd..eaa890f9 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -32,6 +32,8 @@ struct PlayerSettings: View { @Default(.showInspector) private var showInspector @Default(.showChapters) private var showChapters + @Default(.showChapterThumbnails) private var showThumbnails + @Default(.showChapterThumbnailsOnlyWhenDifferent) private var showThumbnailsOnlyWhenDifferent @Default(.expandChapters) private var expandChapters @Default(.showRelated) private var showRelated @@ -80,8 +82,6 @@ struct PlayerSettings: View { Section(header: SettingsHeader(text: "Info".localized())) { expandVideoDescriptionToggle collapsedLineDescriptionStepper - showChaptersToggle - expandChaptersToggle showRelatedToggle #if os(macOS) HStack { @@ -93,6 +93,13 @@ struct PlayerSettings: View { inspectorVisibilityPicker #endif } + + Section(header: SettingsHeader(text: "Chapters".localized())) { + showChaptersToggle + showThumbnailsToggle + showThumbnailsWhenDifferentToggle + expandChaptersToggle + } #endif let interface = Section(header: SettingsHeader(text: "Interface".localized())) { @@ -284,7 +291,19 @@ struct PlayerSettings: View { } private var showChaptersToggle: some View { - Toggle("Chapters (if available)", isOn: $showChapters) + Toggle("Show chapters", isOn: $showChapters) + } + + private var showThumbnailsToggle: some View { + Toggle("Show thumbnails", isOn: $showThumbnails) + .disabled(!showChapters) + .foregroundColor(showChapters ? .primary : .secondary) + } + + private var showThumbnailsWhenDifferentToggle: some View { + Toggle("Show thumbnails only when unique", isOn: $showThumbnailsOnlyWhenDifferent) + .disabled(!showChapters || !showThumbnails) + .foregroundColor(showChapters && showThumbnails ? .primary : .secondary) } private var expandChaptersToggle: some View {