Move close video button from toolbar into now playing card in RemoteControlView

This commit is contained in:
Arkadiusz Fal
2026-02-12 08:13:17 +01:00
parent 4f954b7c9c
commit a8c59e61cd

View File

@@ -185,18 +185,6 @@ struct RemoteControlView: View {
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
deviceHeader deviceHeader
} }
ToolbarItem(placement: .primaryAction) {
Button(role: .destructive) {
Task {
await remoteControl?.closeVideo(on: device)
dismiss()
}
} label: {
Label(String(localized: "remoteControl.closeVideo"), systemImage: "xmark")
.labelStyle(.iconOnly)
}
.disabled(remoteState.videoID == nil)
}
} }
} }
@@ -358,95 +346,112 @@ struct RemoteControlView: View {
@ViewBuilder @ViewBuilder
private var nowPlayingSection: some View { private var nowPlayingSection: some View {
VStack(spacing: 12) { ZStack(alignment: .topTrailing) {
HStack { VStack(spacing: 12) {
// Thumbnail - only show if there's an active video HStack {
if remoteState.videoID != nil, // Thumbnail - only show if there's an active video
let thumbnailURL = remoteState.thumbnailURL ?? device.currentVideoThumbnailURL { if remoteState.videoID != nil,
AsyncImage(url: thumbnailURL) { phase in let thumbnailURL = remoteState.thumbnailURL ?? device.currentVideoThumbnailURL {
switch phase { AsyncImage(url: thumbnailURL) { phase in
case .success(let image): switch phase {
image case .success(let image):
.resizable() image
.aspectRatio(16/9, contentMode: .fit) .resizable()
.clipShape(.rect(cornerRadius: 8)) .aspectRatio(16/9, contentMode: .fit)
case .failure: .clipShape(.rect(cornerRadius: 8))
thumbnailPlaceholder case .failure:
case .empty: thumbnailPlaceholder
thumbnailPlaceholder case .empty:
.overlay(ProgressView()) thumbnailPlaceholder
@unknown default: .overlay(ProgressView())
thumbnailPlaceholder @unknown default:
thumbnailPlaceholder
}
} }
}
.frame(maxWidth: 120)
} else {
thumbnailPlaceholder
.frame(maxWidth: 120) .frame(maxWidth: 120)
} } else {
thumbnailPlaceholder
// Video info - only show details if there's an active video .frame(maxWidth: 120)
VStack(alignment: .leading, spacing: 4) { }
if remoteState.videoID != nil {
Text(remoteState.videoTitle ?? device.currentVideoTitle ?? String(localized: "remoteControl.noVideo")) // Video info - only show details if there's an active video
.font(.headline) VStack(alignment: .leading, spacing: 4) {
.lineLimit(2) if remoteState.videoID != nil {
Text(remoteState.videoTitle ?? device.currentVideoTitle ?? String(localized: "remoteControl.noVideo"))
if let channel = remoteState.channelName { .font(.headline)
Text(channel) .lineLimit(2)
.font(.subheadline)
if let channel = remoteState.channelName {
Text(channel)
.font(.subheadline)
.foregroundStyle(.secondary)
}
} else {
Text(String(localized: "remoteControl.noVideo"))
.font(.headline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} else {
Text(String(localized: "remoteControl.noVideo"))
.font(.headline)
.foregroundStyle(.secondary)
} }
.frame(maxWidth: .infinity, alignment: .leading)
} }
.frame(maxWidth: .infinity, alignment: .leading) // Scrubber
} VStack(spacing: 4) {
// Scrubber #if os(tvOS)
VStack(spacing: 4) { ProgressView(value: displayCurrentTime, total: max(remoteState.duration, 1))
#if os(tvOS) .tint(.accentColor)
ProgressView(value: displayCurrentTime, total: max(remoteState.duration, 1)) #else
Slider(
value: Binding(
get: { displayCurrentTime },
set: { newValue in
scrubTime = newValue
if !isScrubbing {
isScrubbing = true
}
}
),
in: 0...max(remoteState.duration, 1),
onEditingChanged: { editing in
if !editing {
// User released - send seek command
estimatedCurrentTime = scrubTime
Task {
await remoteControl?.seek(to: scrubTime, on: device)
}
isScrubbing = false
}
}
)
.tint(.accentColor) .tint(.accentColor)
#else .disabled(remoteState.duration == 0)
Slider( #endif
value: Binding(
get: { displayCurrentTime },
set: { newValue in
scrubTime = newValue
if !isScrubbing {
isScrubbing = true
}
}
),
in: 0...max(remoteState.duration, 1),
onEditingChanged: { editing in
if !editing {
// User released - send seek command
estimatedCurrentTime = scrubTime
Task {
await remoteControl?.seek(to: scrubTime, on: device)
}
isScrubbing = false
}
}
)
.tint(.accentColor)
.disabled(remoteState.duration == 0)
#endif
HStack { HStack {
Text(formatTime(displayCurrentTime)) Text(formatTime(displayCurrentTime))
Spacer() Spacer()
Text("-" + formatTime(remoteState.duration - displayCurrentTime)) Text("-" + formatTime(remoteState.duration - displayCurrentTime))
}
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
} }
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
} }
.padding()
.padding(.top, 8)
// Close video button
Button(role: .destructive) {
Task {
await remoteControl?.closeVideo(on: device)
dismiss()
}
} label: {
Label(String(localized: "remoteControl.closeVideo"), systemImage: "xmark")
.labelStyle(.iconOnly)
.font(.caption)
}
.disabled(remoteState.videoID == nil)
.padding(12)
} }
.padding()
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12)) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
} }