diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index 392492ec..a794cc91 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -354,6 +354,8 @@ internal enum L10n { internal static var settings: String { return L10n.tr("Localizable", "settings") } /// Show Cast & Crew internal static var showCastAndCrew: String { return L10n.tr("Localizable", "showCastAndCrew") } + /// Show Chapters Info In Bottom Overlay + internal static var showChaptersInfoInBottomOverlay: String { return L10n.tr("Localizable", "showChaptersInfoInBottomOverlay") } /// Flatten Library Items internal static var showFlattenView: String { return L10n.tr("Localizable", "showFlattenView") } /// Show Missing Episodes diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index 20630909..c25b0153 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -73,6 +73,10 @@ extension Defaults.Keys { default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let shouldShowChaptersInfoInBottomOverlay = Key("shouldShowChaptersInfoInBottomOverlay", + default: true, + suite: SwiftfinStore.Defaults.generalSuite) + // Experimental settings enum Experimental { static let syncSubtitleStateWithAdjacent = Key("experimental.syncSubtitleState", diff --git a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift index 5d6a4372..69763874 100644 --- a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift @@ -131,6 +131,7 @@ final class VideoPlayerViewModel: ViewModel { let systemControlGesturesEnabled: Bool let seekSlideGestureEnabled: Bool let playerGesturesLockGestureEnabled: Bool + let shouldShowChaptersInfoInBottomOverlay: Bool let resumeOffset: Bool let streamType: ServerStreamType let container: String @@ -263,6 +264,7 @@ final class VideoPlayerViewModel: ViewModel { self.playerGesturesLockGestureEnabled = Defaults[.playerGesturesLockGestureEnabled] self.seekSlideGestureEnabled = Defaults[.seekSlideGestureEnabled] self.shouldShowJumpButtonsInOverlayMenu = Defaults[.shouldShowJumpButtonsInOverlayMenu] + self.shouldShowChaptersInfoInBottomOverlay = Defaults[.shouldShowChaptersInfoInBottomOverlay] self.resumeOffset = Defaults[.resumeOffset] @@ -334,6 +336,7 @@ extension VideoPlayerViewModel { TvShowsAPI.getEpisodes(seriesId: seriesID, userId: SessionManager.main.currentLogin.user.id, + fields: [.chapters], adjacentTo: item.id, limit: 3) .sink(receiveCompletion: { completion in diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index e52d0c28..e8fc468f 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -152,6 +152,8 @@ 6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */ = {isa = PBXBuildFile; productRef = 6220D0C826D63F3700B8E046 /* Stinsen */; }; 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */; }; 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; + 62400C4B287ED19600F6AD3D /* UDPBroadcast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; }; + 62400C4C287ED19600F6AD3D /* UDPBroadcast.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; 62553429282190A00087FE20 /* PanDirectionGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62553428282190A00087FE20 /* PanDirectionGestureRecognizer.swift */; }; 625CB56F2678C23300530A6E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56E2678C23300530A6E /* HomeView.swift */; }; @@ -250,7 +252,6 @@ 631759CF2879DB6A00A621AD /* PublicUserSignInCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631759CE2879DB6A00A621AD /* PublicUserSignInCellView.swift */; }; 6334175B287DDFB9000603CE /* QuickConnectSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334175A287DDFB9000603CE /* QuickConnectSettingsView.swift */; }; 6334175D287DE0D0000603CE /* QuickConnectSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */; }; - 637FCAF4287B5B2600C0A353 /* UDPBroadcast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; }; 637FCAF5287B5B2600C0A353 /* UDPBroadcast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; C400DB6A27FE894F007B65FE /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400DB6927FE894F007B65FE /* LiveTVChannelsView.swift */; }; @@ -551,6 +552,7 @@ dstSubfolderSpec = 10; files = ( 62666E3D27E503F200EC0ECD /* GoogleCastSDK.xcframework in Embed Frameworks */, + 62400C4C287ED19600F6AD3D /* UDPBroadcast.xcframework in Embed Frameworks */, 62666DF827E5012C00EC0ECD /* MobileVLCKit.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -974,7 +976,7 @@ 62666E0C27E501A500EC0ECD /* OpenGLES.framework in Frameworks */, C409CE9E285044C800CABC12 /* SwiftUICollection in Frameworks */, 62666E0127E5016900EC0ECD /* CoreFoundation.framework in Frameworks */, - 637FCAF4287B5B2600C0A353 /* UDPBroadcast.xcframework in Frameworks */, + 62400C4B287ED19600F6AD3D /* UDPBroadcast.xcframework in Frameworks */, E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */, 62666E2427E501F300EC0ECD /* Foundation.framework in Frameworks */, 53352571265EA0A0006CCA86 /* Introspect in Frameworks */, diff --git a/Swiftfin/Views/SettingsView/OverlaySettingsView.swift b/Swiftfin/Views/SettingsView/OverlaySettingsView.swift index 1fa04dd1..537af6a6 100644 --- a/Swiftfin/Views/SettingsView/OverlaySettingsView.swift +++ b/Swiftfin/Views/SettingsView/OverlaySettingsView.swift @@ -21,6 +21,8 @@ struct OverlaySettingsView: View { var shouldShowAutoPlay @Default(.shouldShowJumpButtonsInOverlayMenu) var shouldShowJumpButtonsInOverlayMenu + @Default(.shouldShowChaptersInfoInBottomOverlay) + var shouldShowChaptersInfoInBottomOverlay var body: some View { Form { @@ -52,6 +54,13 @@ struct OverlaySettingsView: View { } } + Toggle(isOn: $shouldShowChaptersInfoInBottomOverlay) { + HStack { + Image(systemName: "photo.on.rectangle") + L10n.showChaptersInfoInBottomOverlay.text + } + } + Toggle(L10n.editJumpLengths, isOn: $shouldShowJumpButtonsInOverlayMenu) } } diff --git a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift index 2cb6ba19..79e21cf5 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift @@ -339,59 +339,95 @@ struct VLCPlayerOverlayView: View { .frame(height: 70) } - HStack { - if viewModel.overlayType == .compact { - HStack { - Button { - viewModel.playerOverlayDelegate?.didSelectBackward() - } label: { - Image(systemName: viewModel.jumpBackwardLength.backwardImageLabel) - .padding(.horizontal, 5) - } - - Button { - viewModel.playerOverlayDelegate?.didSelectMain() - } label: { - mainButtonView - .frame(minWidth: 30, maxWidth: 30) - .padding(.horizontal, 10) - } - - Button { - viewModel.playerOverlayDelegate?.didSelectForward() - } label: { - Image(systemName: viewModel.jumpForwardLength.forwardImageLabel) - .padding(.horizontal, 5) + VStack(alignment: .leading, spacing: 0) { + if viewModel.shouldShowChaptersInfoInBottomOverlay, + let currentChapter = viewModel.currentChapter + { + Button { + viewModel.playerOverlayDelegate?.didSelectChapters() + } label: { + HStack { + Text(currentChapter.name ?? "--") + Image(systemName: "chevron.right") } + .font(.system(size: 16, weight: .semibold, design: .default)) } - .font(.system(size: 24, weight: .semibold, design: .default)) } - Text(viewModel.leftLabelText) - .font(.system(size: 18, weight: .semibold, design: .default)) - .frame(minWidth: 70, maxWidth: 70) - .accessibilityLabel(L10n.currentPosition) - .accessibilityValue(viewModel.leftLabelText) + HStack { + if viewModel.overlayType == .compact { + HStack { + Button { + viewModel.playerOverlayDelegate?.didSelectBackward() + } label: { + Image(systemName: viewModel.jumpBackwardLength.backwardImageLabel) + .padding(.horizontal, 5) + } - ValueSlider(value: $viewModel.sliderPercentage, onEditingChanged: { editing in - viewModel.sliderIsScrubbing = editing - }) - .valueSliderStyle(HorizontalValueSliderStyle(track: - HorizontalValueTrack(view: - Capsule().foregroundColor(.purple)) - .background(Capsule().foregroundColor(Color.gray.opacity(0.25))) + Button { + viewModel.playerOverlayDelegate?.didSelectMain() + } label: { + mainButtonView + .frame(minWidth: 30, maxWidth: 30) + .padding(.horizontal, 10) + } + + Button { + viewModel.playerOverlayDelegate?.didSelectForward() + } label: { + Image(systemName: viewModel.jumpForwardLength.forwardImageLabel) + .padding(.horizontal, 5) + } + } + .font(.system(size: 24, weight: .semibold, design: .default)) + } + + Text(viewModel.leftLabelText) + .font(.system(size: 18, weight: .semibold, design: .default)) + .frame(minWidth: 70, maxWidth: 70) + .accessibilityLabel(L10n.currentPosition) + .accessibilityValue(viewModel.leftLabelText) + + ValueSlider(value: $viewModel.sliderPercentage, onEditingChanged: { editing in + viewModel.sliderIsScrubbing = editing + }) + .valueSliderStyle(HorizontalValueSliderStyle(track: + GeometryReader { proxy in + ZStack(alignment: .leading) { + HorizontalValueTrack(view: + Capsule().foregroundColor(.purple)) + .background(Capsule().foregroundColor(Color.gray.opacity(0.75))) + + if viewModel.shouldShowChaptersInfoInBottomOverlay { + // Chapters seek masks + ForEach(viewModel.chapters, id: \.startPositionTicks) { chapter in + let ticksRatio = CGFloat(chapter.startPositionTicks ?? 0) / + CGFloat(viewModel.item.runTimeTicks ?? 0) + let x = proxy.size.width * ticksRatio + if x != 0 { + Rectangle() + .blendMode(.destinationOut) + .offset(x: x - 1.5) + .frame(width: 3) + } + } + } + } + .compositingGroup() + } .frame(height: 4), - thumb: Circle().foregroundColor(.purple), - thumbSize: CGSize.Circle(radius: viewModel.sliderIsScrubbing ? 20 : 15), - thumbInteractiveSize: CGSize.Circle(radius: 40), - options: .defaultOptions)) - .frame(maxHeight: 50) + thumb: Circle().foregroundColor(.purple), + thumbSize: CGSize.Circle(radius: viewModel.sliderIsScrubbing ? 20 : 15), + thumbInteractiveSize: CGSize.Circle(radius: 40), + options: .defaultOptions)) + .frame(maxHeight: 50) - Text(viewModel.rightLabelText) - .font(.system(size: 18, weight: .semibold, design: .default)) - .frame(minWidth: 70, maxWidth: 70) - .accessibilityLabel(L10n.remainingTime) - .accessibilityValue(viewModel.rightLabelText) + Text(viewModel.rightLabelText) + .font(.system(size: 18, weight: .semibold, design: .default)) + .frame(minWidth: 70, maxWidth: 70) + .accessibilityLabel(L10n.remainingTime) + .accessibilityValue(viewModel.rightLabelText) + } } .padding(.horizontal, UIDevice.current.userInterfaceIdiom == .pad ? 30 : 0) .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 10 : 0) diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 39a9f2cb..e6c374d0 100644 Binary files a/Translations/en.lproj/Localizable.strings and b/Translations/en.lproj/Localizable.strings differ