diff --git a/.release_state b/.release_state index 2f1a6ad..9d600a5 100644 --- a/.release_state +++ b/.release_state @@ -1 +1 @@ -2a7b09b293a17369def2bd4d479dadf1fc6bd955 +f87d0125f247d6a82487bb7fbce6825a39dad51a diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..eb13c74 --- /dev/null +++ b/.swift-format @@ -0,0 +1,10 @@ +{ + "indentation" : { + "spaces" : 2 + }, + "lineLength" : 120, + "maximumBlankLines" : 1, + "respectsExistingLineBreaks" : true, + "tabWidth" : 2, + "version" : 1 +} diff --git a/ABPlayer/Resources/Assets.xcassets/ClosedCaption.imageset/Contents.json b/ABPlayer/Resources/Assets.xcassets/ClosedCaption.imageset/Contents.json new file mode 100644 index 0000000..e628b7b --- /dev/null +++ b/ABPlayer/Resources/Assets.xcassets/ClosedCaption.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "closed_caption.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ABPlayer/Resources/Assets.xcassets/ClosedCaption.imageset/closed_caption.png b/ABPlayer/Resources/Assets.xcassets/ClosedCaption.imageset/closed_caption.png new file mode 100644 index 0000000..9112a6c Binary files /dev/null and b/ABPlayer/Resources/Assets.xcassets/ClosedCaption.imageset/closed_caption.png differ diff --git a/ABPlayer/Sources/ViewModels/VideoPlayerViewModel.swift b/ABPlayer/Sources/ViewModels/VideoPlayerViewModel.swift index 69d00f5..24cc42d 100644 --- a/ABPlayer/Sources/ViewModels/VideoPlayerViewModel.swift +++ b/ABPlayer/Sources/ViewModels/VideoPlayerViewModel.swift @@ -13,6 +13,16 @@ final class VideoPlayerViewModel { var seekValue: Double = 0 var wasPlayingBeforeSeek: Bool = false + // MARK: - HUD + var hudMessage: String? + var hideHudTask: Task? + var showHudMessage: Bool { + guard let hudMessage else { + return false + } + return !hudMessage.isEmpty + } + // MARK: - Layout State var draggingWidth: Double? @@ -113,12 +123,14 @@ final class VideoPlayerViewModel { guard let manager = playerManager else { return } let targetTime = manager.currentTime - 5 manager.seek(to: targetTime) + showHUDMessage("- 5") } func seekForward() { guard let manager = playerManager else { return } let targetTime = manager.currentTime + 10 manager.seek(to: targetTime) + showHUDMessage("+ 10s") } func timeString(from value: Double) -> String { @@ -138,4 +150,26 @@ final class VideoPlayerViewModel { return String(format: "%d:%02d", minutes, seconds) } + + func showHUDMessage(_ message: String) { + hideHudTask?.cancel() + hudMessage = nil + + Task { @MainActor in + try? await Task.sleep(nanoseconds: 10_000_000) + withAnimation(.bouncy) { + hudMessage = message + } + + hideHudTask = Task { + try? await Task.sleep(nanoseconds: 2_000_000_000) + if Task.isCancelled { + return + } + withAnimation { + hudMessage = nil + } + } + } + } } diff --git a/ABPlayer/Sources/Views/Components/SegmentsSection.swift b/ABPlayer/Sources/Views/Components/SegmentsSection.swift index d112a0f..4f0c12c 100644 --- a/ABPlayer/Sources/Views/Components/SegmentsSection.swift +++ b/ABPlayer/Sources/Views/Components/SegmentsSection.swift @@ -13,7 +13,6 @@ struct SegmentsSection: View { var body: some View { VStack(alignment: .leading, spacing: 8) { - HStack { // Loop Controls Group HStack { diff --git a/ABPlayer/Sources/Views/Components/VideoControlsView.swift b/ABPlayer/Sources/Views/Components/VideoControlsView.swift index 8075c95..7b61d9a 100644 --- a/ABPlayer/Sources/Views/Components/VideoControlsView.swift +++ b/ABPlayer/Sources/Views/Components/VideoControlsView.swift @@ -4,19 +4,28 @@ import Observation struct VideoControlsView: View { @Bindable var viewModel: VideoPlayerViewModel @Environment(AudioPlayerManager.self) private var playerManager + @State private var showSubtitle = false var body: some View { ZStack(alignment: .center) { HStack { - // Loop Mode & Volume - HStack(spacing: 12) { + VideoTimeDisplay(isSeeking: viewModel.isSeeking, seekValue: viewModel.seekValue) + + Spacer() + + HStack { loopModeMenu + + Button { + showSubtitle.toggle() + } label: { + Image(.closedCaption).renderingMode(.template).resizable().aspectRatio(contentMode: .fit).frame(width: 24) + } + .buttonStyle(.plain) + .foregroundStyle(showSubtitle ? Color.accentColor : .primary) + VolumeControl(playerVolume: $viewModel.playerVolume) } - - Spacer() - - VideoTimeDisplay(isSeeking: viewModel.isSeeking, seekValue: viewModel.seekValue) } // Playback Controls @@ -39,11 +48,11 @@ struct VideoControlsView: View { } label: { Image( systemName: playerManager.loopMode != .none - ? "\(playerManager.loopMode.iconName).circle.fill" - : "repeat.circle" + ? "\(playerManager.loopMode.iconName)" + : "repeat" ) - .font(.title) - .foregroundStyle(playerManager.loopMode != .none ? .blue : .primary) + .font(.title2) + .foregroundStyle(playerManager.loopMode != .none ? Color.accentColor : .primary) } .buttonStyle(.plain) .help("Loop mode: \(playerManager.loopMode.displayName)") @@ -51,6 +60,15 @@ struct VideoControlsView: View { private var playbackControls: some View { HStack(spacing: 16) { + Button { + // switch to previous item + } label: { + Image(systemName: "backward.end") + .font(.title) + } + .buttonStyle(.plain) + .keyboardShortcut("f", modifiers: []) + Button { viewModel.seekBack() } label: { @@ -77,6 +95,15 @@ struct VideoControlsView: View { } .buttonStyle(.plain) .keyboardShortcut("g", modifiers: []) + + Button { + // switch to next item + } label: { + Image(systemName: "forward.end") + .font(.title) + } + .buttonStyle(.plain) + .keyboardShortcut("f", modifiers: []) } } } diff --git a/ABPlayer/Sources/Views/VideoPlayerView.swift b/ABPlayer/Sources/Views/VideoPlayerView.swift index 6f3bb22..462f9ef 100644 --- a/ABPlayer/Sources/Views/VideoPlayerView.swift +++ b/ABPlayer/Sources/Views/VideoPlayerView.swift @@ -126,20 +126,33 @@ struct VideoPlayerView: View { private var videoSection: some View { VStack(alignment: .leading, spacing: 0) { // 1. Video Player Area - Group { - if let player = playerManager.player { - NativeVideoPlayer(player: player) - } else { - ZStack { - Color.black - ProgressView() - .scaleEffect(1.5) - .tint(.white) + ZStack { + Group { + if let player = playerManager.player { + NativeVideoPlayer(player: player) + } else { + ZStack { + Color.black + ProgressView() + .scaleEffect(1.5) + .tint(.white) + } } } + .aspectRatio(16 / 9, contentMode: .fit) + .layoutPriority(1) + + if viewModel.showHudMessage { + Text(viewModel.hudMessage ?? "") + .font(.title) + .padding(.all, 16) + .background(.black.opacity(0.6)) + .cornerRadius(8) + .id(viewModel.hudMessage) + .transition(.scale.combined(with: .opacity)) + .animation(.bouncy, value: viewModel.hudMessage) + } } - .aspectRatio(16 / 9, contentMode: .fit) - .layoutPriority(1) // 2. Controls Area (Fixed height) VStack(spacing: 12) { @@ -150,8 +163,10 @@ struct VideoPlayerView: View { ) VideoControlsView(viewModel: viewModel) - + .padding(.horizontal) + Divider() + ContentPanelView(audioFile: audioFile) } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 76ef8e6..424e659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [0.2.9.58] - 2026-01-19 + +### Features +- add HUD feedback, subtitle toggle, and track navigation controls + +### Chores +- refine VideoPlayerView layout and SegmentsSection spacing + + ## [0.2.9.57] - 2026-01-19 ### Bug Fixes diff --git a/Project.swift b/Project.swift index ebd545e..b8770fa 100644 --- a/Project.swift +++ b/Project.swift @@ -1,6 +1,6 @@ import ProjectDescription -let buildVersionString = "57" +let buildVersionString = "58" let shortVersionString = "0.2.9" let project = Project( name: "ABPlayer",