diff --git a/Demo/Pillarbox-demo.xcodeproj/project.pbxproj b/Demo/Pillarbox-demo.xcodeproj/project.pbxproj index b7ce5195b..798a4bfd6 100644 --- a/Demo/Pillarbox-demo.xcodeproj/project.pbxproj +++ b/Demo/Pillarbox-demo.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 0E4128BF2AFB959B00D67759 /* PlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4128BE2AFB959B00D67759 /* PlayerViewModel.swift */; }; 0E48F3FC2B2DBAD4001982BB /* CustomNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E48F3FB2B2DBAD4001982BB /* CustomNavigationLink.swift */; }; 0E6B995C29D43E4200D0276D /* OptInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6B995B29D43E4200D0276D /* OptInView.swift */; }; + 0EB94A1F2B5AE29000FF3175 /* HighSpeedView~ios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB94A1E2B5AE29000FF3175 /* HighSpeedView~ios.swift */; }; 0ECC5AD52A517A4C0064E701 /* PlaybackSlider~ios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC5AD42A517A4C0064E701 /* PlaybackSlider~ios.swift */; }; 0EDFF0DB2B0740DA005030B4 /* SourceCodeViewable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDFF0DA2B0740DA005030B4 /* SourceCodeViewable.swift */; }; 0EE2A3AE2B29D6D000BAAD65 /* CustomList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE2A3AD2B29D6D000BAAD65 /* CustomList.swift */; }; @@ -98,6 +99,7 @@ 0E4128BE2AFB959B00D67759 /* PlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewModel.swift; sourceTree = ""; }; 0E48F3FB2B2DBAD4001982BB /* CustomNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationLink.swift; sourceTree = ""; }; 0E6B995B29D43E4200D0276D /* OptInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptInView.swift; sourceTree = ""; }; + 0EB94A1E2B5AE29000FF3175 /* HighSpeedView~ios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HighSpeedView~ios.swift"; sourceTree = ""; }; 0ECC5AD42A517A4C0064E701 /* PlaybackSlider~ios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaybackSlider~ios.swift"; sourceTree = ""; }; 0EDFF0DA2B0740DA005030B4 /* SourceCodeViewable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCodeViewable.swift; sourceTree = ""; }; 0EE2A3AD2B29D6D000BAAD65 /* CustomList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomList.swift; sourceTree = ""; }; @@ -381,6 +383,7 @@ 0EE2A3AD2B29D6D000BAAD65 /* CustomList.swift */, 0E48F3FB2B2DBAD4001982BB /* CustomNavigationLink.swift */, 0EE2A3AF2B29F82200BAAD65 /* CustomSection.swift */, + 0EB94A1E2B5AE29000FF3175 /* HighSpeedView~ios.swift */, 0E011D192B2DF9BE00DAAD3D /* MediaCardView.swift */, 6F59E86829CF31E10093E6FB /* MessageViews.swift */, 6FC5D6A12A6FA8D20012BC89 /* Modal.swift */, @@ -639,6 +642,7 @@ 6F59E88029CF31E10093E6FB /* RadioChannel.swift in Sources */, 6F0E5CD32B3394EA0031E313 /* PiPButton.swift in Sources */, 6F59E89629CF31E20093E6FB /* Cell.swift in Sources */, + 0EB94A1F2B5AE29000FF3175 /* HighSpeedView~ios.swift in Sources */, 0EF2A5452B443FEF00F01804 /* WebView~ios.swift in Sources */, 6F59E89A29CF31E20093E6FB /* SimplePlayerView.swift in Sources */, 6F59E89329CF31E20093E6FB /* LinkView.swift in Sources */, diff --git a/Demo/Resources/Localizable.xcstrings b/Demo/Resources/Localizable.xcstrings index 4d5228842..3832fbc08 100644 --- a/Demo/Resources/Localizable.xcstrings +++ b/Demo/Resources/Localizable.xcstrings @@ -29,6 +29,16 @@ } } }, + "%g× %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$g× %2$@" + } + } + } + }, "Active" : { }, diff --git a/Demo/Sources/Players/PlaybackView.swift b/Demo/Sources/Players/PlaybackView.swift index f29e291c6..f56831876 100644 --- a/Demo/Sources/Players/PlaybackView.swift +++ b/Demo/Sources/Players/PlaybackView.swift @@ -36,6 +36,7 @@ private struct MainView: View { } .statusBarHidden(isFullScreen ? isUserInterfaceHidden : false) .animation(.defaultLinear, value: isUserInterfaceHidden) + .supportsHighSpeed(for: player) .bind(visibilityTracker, to: player) ._debugBodyCounter() } diff --git a/Demo/Sources/Views/HighSpeedView~ios.swift b/Demo/Sources/Views/HighSpeedView~ios.swift new file mode 100644 index 000000000..177830824 --- /dev/null +++ b/Demo/Sources/Views/HighSpeedView~ios.swift @@ -0,0 +1,93 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import PillarboxPlayer +import SwiftUI + +private struct HighSpeedCapsule: View { + let speed: Float + + var body: some View { + Text("\(speed, specifier: "%g×") \(Image(systemName: "forward.fill"))") + .font(.footnote) + .bold() + .padding(.horizontal, 10) + .padding(.vertical, 5) + .foregroundStyle(.white) + .background(.black.opacity(0.7)) + .clipShape(Capsule()) + } +} + +private struct HighSpeedGestureView: View where Content: View { + @GestureState private var isLongPressing = false + @State private var timer: Timer? + + let content: () -> Content + let action: (_ finished: Bool) -> Void + + var body: some View { + content() + .simultaneousGesture(longPressGesture()) + .onChange(of: isLongPressing) { isPressing in + timer?.invalidate() + if !isPressing { + action(true) + } + else { + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in + action(false) + } + } + } + } + + private func longPressGesture() -> some Gesture { + LongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity) + .updating($isLongPressing) { value, state, _ in + state = value + } + } +} + +private struct HighSpeedView: View where Content: View { + private let highSpeed: Float = 2 + @State private var speed: Float? + + private var displaysCapsule: Bool { + speed != nil && speed != highSpeed && player.playbackSpeedRange.contains(highSpeed) && player.playbackState == .playing + } + + @ObservedObject var player: Player + let content: () -> Content + + var body: some View { + HighSpeedGestureView(content: content) { finished in + if !finished { + speed = player.effectivePlaybackSpeed + player.setDesiredPlaybackSpeed(highSpeed) + } + else if let speed { + player.setDesiredPlaybackSpeed(speed) + self.speed = nil + } + } + .overlay(alignment: .top) { + HighSpeedCapsule(speed: highSpeed) + .opacity(displaysCapsule ? 1 : 0) + .animation(.easeInOut(duration: 0.1), value: displaysCapsule) + .padding() + } + } +} + +extension View { + func supportsHighSpeed(for player: Player) -> some View { + HighSpeedView(player: player) { + self + } + } +}