jellyflood/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift

154 lines
6.3 KiB
Swift

//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Defaults
import JellyfinAPI
import SwiftUI
import VLCUI
extension VideoPlayer.Overlay {
struct ChapterOverlay: View {
@Default(.accentColor)
private var accentColor
@Environment(\.currentOverlayType)
@Binding
private var currentOverlayType
@Environment(\.safeAreaInsets)
private var safeAreaInsets
@EnvironmentObject
private var currentProgressHandler: VideoPlayerManager.CurrentProgressHandler
@EnvironmentObject
private var overlayTimer: TimerProxy
@EnvironmentObject
private var videoPlayerManager: VideoPlayerManager
@EnvironmentObject
private var videoPlayerProxy: VLCVideoPlayer.Proxy
@EnvironmentObject
private var viewModel: VideoPlayerViewModel
@State
private var scrollViewProxy: ScrollViewProxy? = nil
var body: some View {
VStack {
Spacer()
.allowsHitTesting(false)
HStack {
L10n.chapters.text
.font(.title2)
.fontWeight(.semibold)
.foregroundColor(.white)
.accessibility(addTraits: [.isHeader])
Spacer()
Button {
if let currentChapter = viewModel.chapter(from: currentProgressHandler.seconds) {
withAnimation {
scrollViewProxy?.scrollTo(currentChapter.hashValue, anchor: .center)
}
}
} label: {
Text(L10n.current)
.font(.title2)
.foregroundColor(accentColor)
}
}
.padding(.leading, safeAreaInsets.leading)
.padding(.trailing, safeAreaInsets.trailing)
.if(UIDevice.isIPad) { view in
view.padding(.horizontal)
}
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 15) {
ForEach(viewModel.chapters, id: \.hashValue) { chapter in
PosterButton(
state: .item(chapter),
type: .landscape
)
.imageOverlay { type in
if case let PosterButtonType.item(info) = type,
info.secondsRange.contains(currentProgressHandler.seconds)
{
RoundedRectangle(cornerRadius: 6)
.stroke(accentColor, lineWidth: 8)
}
}
.content { type in
if case let PosterButtonType.item(info) = type {
VStack(alignment: .leading, spacing: 5) {
Text(info.chapterInfo.displayTitle)
.font(.subheadline)
.fontWeight(.semibold)
.lineLimit(1)
.foregroundColor(.white)
Text(info.chapterInfo.timestampLabel)
.font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(Color(UIColor.systemBlue))
.padding(.vertical, 2)
.padding(.horizontal, 4)
.background {
Color(UIColor.darkGray).opacity(0.2).cornerRadius(4)
}
}
}
}
.onSelect {
let seconds = chapter.chapterInfo.startTimeSeconds
videoPlayerProxy.setTime(.seconds(seconds))
if videoPlayerManager.state != .playing {
videoPlayerProxy.play()
}
}
}
}
.padding(.leading, safeAreaInsets.leading)
.padding(.trailing, safeAreaInsets.trailing)
.padding(.bottom)
.if(UIDevice.isIPad) { view in
view.padding(.horizontal)
}
}
.onChange(of: currentOverlayType) { newValue in
guard newValue == .chapters else { return }
if let currentChapter = viewModel.chapter(from: currentProgressHandler.seconds) {
scrollViewProxy?.scrollTo(currentChapter.hashValue, anchor: .center)
}
}
.onAppear {
scrollViewProxy = proxy
}
}
}
.background {
LinearGradient(
stops: [
.init(color: .clear, location: 0),
.init(color: .black.opacity(0.4), location: 0.4),
.init(color: .black.opacity(0.9), location: 1),
],
startPoint: .top,
endPoint: .bottom
)
.allowsHitTesting(false)
}
}
}
}