Commit 607fadf4 authored by Kourser's avatar Kourser
Browse files

Full-screen player with draggable seek slider



Tapping the mini player bar (artwork/title area) opens a full-screen PlayerView:
large artwork, title, transport (skip ±, play/pause), speed menu, and a position
Slider you can drag to scrub. Scrubbing shows the dragged time and commits the
seek on release (decoupled from live time updates while dragging).

Co-Authored-By: default avatarClaude <claude@anthropic.com>
parent b3bd80d7
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -247,6 +247,7 @@
				ENABLE_PREVIEWS = YES;
				GENERATE_INFOPLIST_FILE = YES;
				INFOPLIST_FILE = Info.plist;
				INFOPLIST_KEY_CFBundleDisplayName = Skingomz;
				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -257,7 +258,6 @@
				"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
				MACOSX_DEPLOYMENT_TARGET = 15.0;
				MARKETING_VERSION = 1.0;
				INFOPLIST_KEY_CFBundleDisplayName = Skingomz;
				PRODUCT_BUNDLE_IDENTIFIER = eu.cythin.skingomz;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SDKROOT = auto;
@@ -280,6 +280,7 @@
				ENABLE_PREVIEWS = YES;
				GENERATE_INFOPLIST_FILE = YES;
				INFOPLIST_FILE = Info.plist;
				INFOPLIST_KEY_CFBundleDisplayName = Skingomz;
				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -290,7 +291,6 @@
				"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
				MACOSX_DEPLOYMENT_TARGET = 15.0;
				MARKETING_VERSION = 1.0;
				INFOPLIST_KEY_CFBundleDisplayName = Skingomz;
				PRODUCT_BUNDLE_IDENTIFIER = eu.cythin.skingomz;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SDKROOT = auto;
+20 −11
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ import PlaybackKit
/// Compact transport bar shown at the bottom while something is loaded.
struct NowPlayingBar: View {
    @Environment(PlaybackController.self) private var playback
    @State private var showingPlayer = false

    private static let rates: [Float] = [0.8, 1.0, 1.25, 1.5, 2.0]

@@ -13,6 +14,7 @@ struct NowPlayingBar: View {
                ProgressView(value: playback.progress)
                    .progressViewStyle(.linear)

                HStack(spacing: 14) {
                    HStack(spacing: 14) {
                        ArtworkView(url: episode.imageURL, size: 44)

@@ -27,6 +29,9 @@ struct NowPlayingBar: View {
                        }

                        Spacer(minLength: 4)
                    }
                    .contentShape(Rectangle())
                    .onTapGesture { showingPlayer = true }

                    Button { playback.skipBackward() } label: {
                        Image(systemName: "gobackward.15")
@@ -62,6 +67,10 @@ struct NowPlayingBar: View {
                .padding(.vertical, 8)
            }
            .background(.regularMaterial)
            .sheet(isPresented: $showingPlayer) {
                PlayerView()
                    .environment(playback)
            }
        }
    }

+130 −0
Original line number Diff line number Diff line
import SwiftUI
import PlaybackKit

/// Full-screen player presented when tapping the mini bar. Provides a draggable
/// position slider plus the usual transport controls.
struct PlayerView: View {
    @Environment(PlaybackController.self) private var playback
    @Environment(\.dismiss) private var dismiss

    @State private var isScrubbing = false
    @State private var scrubTime: Double = 0

    private static let rates: [Float] = [0.8, 1.0, 1.25, 1.5, 2.0]

    var body: some View {
        VStack(spacing: 28) {
            header
            Spacer(minLength: 0)

            ArtworkView(url: playback.currentEpisode?.imageURL, size: 260)
                .shadow(radius: 14, y: 8)

            Text(playback.currentEpisode?.title ?? "")
                .font(.title3.weight(.semibold))
                .multilineTextAlignment(.center)
                .lineLimit(3)

            scrubber
            transport
            speedMenu

            Spacer(minLength: 0)
        }
        .padding(28)
        .frame(maxWidth: 600)
    }

    private var header: some View {
        HStack {
            Button { dismiss() } label: {
                Image(systemName: "chevron.down")
                    .font(.title3.weight(.semibold))
            }
            .buttonStyle(.borderless)
            Spacer()
        }
    }

    // MARK: Scrubber

    private var maxValue: Double { max(playback.duration, 1) }
    private var displayTime: Double { isScrubbing ? scrubTime : playback.currentTime }

    private var scrubber: some View {
        VStack(spacing: 4) {
            Slider(
                value: Binding(
                    get: { min(displayTime, maxValue) },
                    set: { scrubTime = $0 }
                ),
                in: 0...maxValue,
                onEditingChanged: { editing in
                    if editing {
                        scrubTime = playback.currentTime
                        isScrubbing = true
                    } else {
                        playback.seek(to: scrubTime)
                        isScrubbing = false
                    }
                }
            )
            HStack {
                Text(Self.format(displayTime))
                Spacer()
                Text("-" + Self.format(max(0, playback.duration - displayTime)))
            }
            .font(.caption)
            .monospacedDigit()
            .foregroundStyle(.secondary)
        }
    }

    private var transport: some View {
        HStack(spacing: 44) {
            Button { playback.skipBackward() } label: {
                Image(systemName: "gobackward.15").font(.title)
            }
            Button { playback.togglePlayPause() } label: {
                Image(systemName: playback.isPlaying ? "pause.circle.fill" : "play.circle.fill")
                    .font(.system(size: 64))
            }
            Button { playback.skipForward() } label: {
                Image(systemName: "goforward.30").font(.title)
            }
        }
        .buttonStyle(.plain)
    }

    private var speedMenu: some View {
        Menu {
            ForEach(Self.rates, id: \.self) { rate in
                Button {
                    playback.setRate(rate)
                } label: {
                    if rate == playback.playbackRate {
                        Label(Self.rateLabel(rate), systemImage: "checkmark")
                    } else {
                        Text(Self.rateLabel(rate))
                    }
                }
            }
        } label: {
            Text("Vitesse · \(Self.rateLabel(playback.playbackRate))")
                .font(.subheadline.weight(.medium))
        }
    }

    private static func rateLabel(_ rate: Float) -> String {
        rate == rate.rounded() ? "\(Int(rate))×" : "\(rate)×"
    }

    private static func format(_ seconds: TimeInterval) -> String {
        guard seconds.isFinite, seconds > 0 else { return "0:00" }
        let total = Int(seconds)
        let h = total / 3600, m = (total % 3600) / 60, s = total % 60
        return h > 0
            ? String(format: "%d:%02d:%02d", h, m, s)
            : String(format: "%d:%02d", m, s)
    }
}