diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 9c435828..59e8c534 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -241,7 +241,11 @@ final class MPVBackend: PlayerBackend { client?.setDoubleAsync("speed", Double(rate)) } - func closeItem() {} + func closeItem() { + handleEOF = false + client?.pause() + client?.stop() + } func enterFullScreen() {} diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 1a0a1454..3a08a6d0 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -371,7 +371,11 @@ final class PlayerModel: ObservableObject { } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.upgradeToStream(stream, force: true) + guard let self = self else { + return + } + self.upgradeToStream(stream, force: true) + self.setNeedsDrawing(self.presentingPlayer) } } @@ -448,6 +452,10 @@ final class PlayerModel: ObservableObject { logger.info("exiting fullscreen") + if controls.playingFullscreen { + toggleFullscreen(true) + } + backend.exitFullScreen() } #endif diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index c3df2ab0..21ca3ad2 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -53,18 +53,21 @@ struct PlayerControls: View { Spacer() - timeline - .offset(y: 10) - .zIndex(1) + Group { + timeline + .offset(y: 10) + .zIndex(1) - bottomBar + bottomBar - #if os(macOS) - .background(VisualEffectBlur(material: .hudWindow)) - #elseif os(iOS) - .background(VisualEffectBlur(blurStyle: .systemThinMaterial)) - #endif - .mask(RoundedRectangle(cornerRadius: 3)) + #if os(macOS) + .background(VisualEffectBlur(material: .hudWindow)) + #elseif os(iOS) + .background(VisualEffectBlur(blurStyle: .systemThinMaterial)) + #endif + .mask(RoundedRectangle(cornerRadius: 3)) + } + .padding(.horizontal, 16) } } .opacity(model.presentingControls ? 1 : 0) @@ -104,9 +107,6 @@ struct PlayerControls: View { var statusBar: some View { HStack(spacing: 4) { - #if os(iOS) - hidePlayerButton - #endif Text(playbackStatus) Text("•") @@ -129,11 +129,10 @@ struct PlayerControls: View { } private var hidePlayerButton: some View { - Button { + button("Hide", systemImage: "chevron.down") { player.hide() - } label: { - Image(systemName: "chevron.down.circle.fill") } + #if !os(tvOS) .keyboardShortcut(.cancelAction) #endif @@ -170,14 +169,18 @@ struct PlayerControls: View { } var buttonsBar: some View { - HStack { + HStack(spacing: 20) { #if !os(tvOS) + hidePlayerButton + fullscreenButton #if os(iOS) pipButton #endif rateButton + closeVideoButton + Spacer() #endif // button("Music Mode", systemImage: "music.note") @@ -225,6 +228,19 @@ struct PlayerControls: View { #endif } + private var closeVideoButton: some View { + button("Close", systemImage: "xmark") { + player.pause() + + player.hide() + player.closePiP() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + player.closeCurrentItem() + } + } + } + var ratePicker: some View { Picker("Rate", selection: rateBinding) { ForEach(PlayerModel.availableRates, id: \.self) { rate in @@ -244,6 +260,8 @@ struct PlayerControls: View { player.avPlayerBackend.switchToMPVOnPipClose = true } + player.exitFullScreen() + if player.activeBackend != PlayerBackendType.appleAVPlayer { player.saveTime { player.changeActiveBackend(from: .mpv, to: .appleAVPlayer) @@ -261,7 +279,7 @@ struct PlayerControls: View { var mediumButtonsBar: some View { HStack { #if !os(tvOS) - button("Seek Backward", systemImage: "gobackward.10", size: 50, cornerRadius: 10) { + button("Seek Backward", systemImage: "gobackward.10", size: 30, cornerRadius: 5) { player.backend.seek(relative: .secondsInDefaultTimescale(-10)) } @@ -279,8 +297,7 @@ struct PlayerControls: View { button( model.isPlaying ? "Pause" : "Play", systemImage: model.isPlaying ? "pause.fill" : "play.fill", - size: 50, - cornerRadius: 10 + size: 30, cornerRadius: 5 ) { player.backend.togglePlay() } @@ -295,7 +312,7 @@ struct PlayerControls: View { Spacer() #if !os(tvOS) - button("Seek Forward", systemImage: "goforward.10", size: 50, cornerRadius: 10) { + button("Seek Forward", systemImage: "goforward.10", size: 30, cornerRadius: 5) { player.backend.seek(relative: .secondsInDefaultTimescale(10)) } #if os(tvOS) @@ -306,7 +323,7 @@ struct PlayerControls: View { #endif #endif } - .font(.system(size: 30)) + .font(.system(size: 20)) .padding(.horizontal, 4) } @@ -361,6 +378,25 @@ struct PlayerControls: View { struct PlayerControls_Previews: PreviewProvider { static var previews: some View { - PlayerControls(player: PlayerModel()) + let model = PlayerControlsModel() + model.presentingControls = true + model.currentTime = .secondsInDefaultTimescale(0) + model.duration = .secondsInDefaultTimescale(120) + + let view = ZStack { + Color.red + + PlayerControls(player: PlayerModel()) + .injectFixtureEnvironmentObjects() + .environmentObject(model) + } + + return Group { + if #available(iOS 15.0, *) { + view.previewInterfaceOrientation(.landscapeLeft) + } else { + view + } + } } } diff --git a/Shared/Player/TimelineView.swift b/Shared/Player/TimelineView.swift index 0e2b816a..61e127df 100644 --- a/Shared/Player/TimelineView.swift +++ b/Shared/Player/TimelineView.swift @@ -10,7 +10,7 @@ struct TimelineView: View { @State private var draggedFrom: Double = 0 private var start: Double = 0.0 - private var height = 10.0 + private var height = 8.0 var cornerRadius: Double var thumbTooltipWidth: Double = 100 @@ -26,26 +26,26 @@ struct TimelineView: View { var body: some View { ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: cornerRadius) - .foregroundColor(.blue) - .frame(maxHeight: height) + Group { + RoundedRectangle(cornerRadius: cornerRadius) + .foregroundColor(.blue) + .frame(maxHeight: height) - RoundedRectangle(cornerRadius: cornerRadius) - .fill( - Color.green - ) - .frame(maxHeight: height) - .frame(width: current * oneUnitWidth) + RoundedRectangle(cornerRadius: cornerRadius) + .fill(Color.green) + .frame(maxHeight: height) + .frame(width: current * oneUnitWidth) - segmentsLayers + segmentsLayers + } + .mask(RoundedRectangle(cornerRadius: 3)) Circle() .strokeBorder(.gray, lineWidth: 1) .background(Circle().fill(dragging ? .gray : .white)) .offset(x: thumbOffset) .foregroundColor(.red.opacity(0.6)) - - .frame(maxHeight: height * 2) + .frame(maxHeight: height * 4) #if !os(tvOS) .gesture( @@ -114,7 +114,7 @@ struct TimelineView: View { var projectedValue: Double { let change = (dragOffset / size.width) * units let projected = draggedFrom + change - return projected.isFinite ? projected : start + return projected.isFinite ? (duration - projected < (0.03 * duration) ? duration : projected) : start } var thumbOffset: Double { @@ -192,6 +192,7 @@ struct TimelineView_Previews: PreviewProvider { TimelineView(duration: .constant(100), current: .constant(90)) TimelineView(duration: .constant(100), current: .constant(100)) } + .environmentObject(PlayerModel()) .padding() } } diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index f521e950..cb61f0d5 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -138,17 +138,7 @@ struct VideoPlayerView: View { hoveringPlayer = hovering hovering ? playerControls.show() : playerControls.hide() } - #if os(iOS) - .onSwipeGesture( - up: { - withAnimation { - fullScreenDetails = true - } - }, - down: { player.hide() } - ) - - #elseif os(macOS) + #if os(macOS) .onAppear(perform: { NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { if hoveringPlayer { @@ -269,20 +259,31 @@ struct VideoPlayerView: View { } func playerPlaceholder(geometry: GeometryProxy) -> some View { - HStack { - Spacer() - VStack { + ZStack(alignment: .topLeading) { + HStack { Spacer() - VStack(spacing: 10) { - #if !os(tvOS) - Image(systemName: "ticket") - .font(.system(size: 120)) - #endif + VStack { + Spacer() + VStack(spacing: 10) { + #if !os(tvOS) + Image(systemName: "ticket") + .font(.system(size: 120)) + #endif + } + Spacer() } + .foregroundColor(.gray) Spacer() } + + Button { + player.hide() + } label: { + Image(systemName: "xmark") + .font(.system(size: 40)) + } + .padding(10) .foregroundColor(.gray) - Spacer() } .contentShape(Rectangle()) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio)