Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion App/ChannelNamesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import SwiftUI
/// typed name off again.
struct ChannelNamesView: View {
let model: RecordScreenModel
/// Closes it when it's a panel on the record screen; a sheet dismisses itself.
var done: (() -> Void)?
@Environment(\.dismiss) private var dismiss
@State private var drafts: [Int: String] = [:]
@State private var copyMessage: String?
Expand Down Expand Up @@ -51,7 +53,7 @@ struct ChannelNamesView: View {
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
ToolbarItem(placement: .confirmationAction) { Button("Done") { commitAll(); dismiss() } }
ToolbarItem(placement: .confirmationAction) { Button("Done") { commitAll(); if let done { done() } else { dismiss() } } }
}
}
.onAppear {
Expand Down
3 changes: 3 additions & 0 deletions App/MeterGrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ struct SourceLabel: View {
Text("\(number)")
.font(.caption2)
.monospacedDigit()
// One line always: a wrapped "10" would make that lane's meter shorter than the others.
.lineLimit(1)
.minimumScaleFactor(0.5)
.secondaryText()
if let source {
if showsName {
Expand Down
92 changes: 83 additions & 9 deletions App/RecordScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,28 @@ struct RecordScreen: View {
#if os(macOS)
@Environment(\.openSettings) private var openSettings
#endif
@State private var showingSettings = launchFlag("-showSettings")
@State private var showingSettings = false
@State private var showingMarkers = false
/// How wide the transport is, so the gap between Stop and Marker can shrink on a very narrow screen.
@State private var transportWidth: CGFloat = 402
@State private var showingNewShow = false
@State private var namingChannel: Int?
@State private var channelName = ""
@State private var showingChannelNames = launchFlag("-channelNames")
@State private var showingChannelNames = false
@State private var showingShowList = launchFlag("-showList")
@State private var settingsSection: SettingsSection?
@State private var showingTrialOffer = false
@AppStorage(TrialOffer.offeredKey) private var trialOffered = false

/// Landscape iPhone is short, not narrow: a Pro Max in landscape is wide but still short.
@Environment(\.verticalSizeClass) private var verticalSizeClass
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
private var isLandscape: Bool { verticalSizeClass == .compact }
/// The Marker list, Channel names or Settings, open as a panel on an iPhone so Stop and Marker stay in reach.
@State private var panel: LivePanel?
private var placement: LivePanelPlacement {
LivePanelPlacement(horizontalCompact: horizontalSizeClass == .compact, verticalCompact: verticalSizeClass == .compact)
}

var body: some View {
// One root for both layouts. The task, idle timer, sheet and dialog below hang on it, so rotating
Expand All @@ -43,7 +49,13 @@ struct RecordScreen: View {
}
// The loop is the model's, started once; the screen only says when it appears and goes away.
.task { model.start() }
.onAppear { model.screenAppeared() }
.onAppear {
model.screenAppeared()
// Screenshot and test flags open these the way a tap would: a panel on an iPhone.
if launchFlag("-showSettings") { showSettings(nil) }
if launchFlag("-channelNames") { open(.channelNames) }
if launchFlag("-showMarkers") { open(.markers) }
}
.onDisappear { model.screenDisappeared() }
.sheet(isPresented: $showingMarkers) {
NavigationStack {
Expand Down Expand Up @@ -149,16 +161,65 @@ struct RecordScreen: View {
openSettings()
#else
settingsSection = section
showingSettings = true
if placement == .sheet { showingSettings = true } else { panel = .settings }
#endif
}

/// Opens the Marker list or Channel names: as a panel on an iPhone, a sheet on an iPad or Mac.
private func open(_ kind: LivePanel) {
guard placement != .sheet else {
switch kind {
case .markers: showingMarkers = true
case .channelNames: showingChannelNames = true
case .settings: showSettings(nil)
}
return
}
panel = panel == kind ? nil : kind
}

/// A panel in the record screen, not a sheet over it, so Record, Stop and Marker stay where they are.
private func panelView(_ kind: LivePanel) -> some View {
Group {
switch kind {
case .markers:
NavigationStack {
MarkerListView(model: model)
.navigationTitle("Markers")
.toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { panel = nil } } }
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
case .channelNames:
ChannelNamesView(model: model, done: { panel = nil })
case .settings:
NavigationStack {
SettingsView(model: model, scrollTo: settingsSection)
.navigationTitle("Settings")
.toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { panel = nil } } }
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
}
}
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(.separator))
}

private var portrait: some View {
VStack(alignment: .leading, spacing: 12) {
header
StatusStrip(chips: model.chips, extras: model.extraChips, tap: chipTapped)
AlertSlot(queue: model.alerts, perform: model.perform)
MeterGrid(model: model.meterModel, clipped: model.recorder.clippedChannels, clearClip: { model.recorder.clearClip(channel: $0) }, sources: model.mixerLink.sources, typedNames: model.recorder.channelNames, nameChannel: nameChannel)
if let panel, placement == .overMeters {
// Takes the meters' place, never the transport's: Stop and Marker stay in reach.
panelView(panel)
.frame(maxHeight: .infinity)
} else {
MeterGrid(model: model.meterModel, clipped: model.recorder.clippedChannels, clearClip: { model.recorder.clearClip(channel: $0) }, sources: model.mixerLink.sources, typedNames: model.recorder.channelNames, nameChannel: nameChannel)
}
transport
}
.padding()
Expand All @@ -185,6 +246,11 @@ struct RecordScreen: View {
AlertSlot(queue: urgent, perform: model.perform, slotHeight: 44)
.allowsHitTesting(urgent.top != nil)
}
if let panel, placement == .besideMeters {
// Beside the meters, which get narrower but keep their height.
panelView(panel)
.frame(maxWidth: 360, maxHeight: .infinity)
}
if !model.transportLeading { transportColumn }
}
}
Expand Down Expand Up @@ -225,6 +291,8 @@ struct RecordScreen: View {
}

private func nameChannel(_ number: Int) {
// On an iPhone a modal alert with a keyboard would cover Stop and Marker: open the Channel names panel.
guard placement == .sheet else { panel = .channelNames; return }
channelName = model.recorder.channelNames[number] ?? (model.mixerLink.sources.indices.contains(number - 1) && model.mixerLink.sources[number - 1].hasMixerName ? model.mixerLink.sources[number - 1].name : "")
namingChannel = number
}
Expand All @@ -248,7 +316,7 @@ struct RecordScreen: View {
HStack(spacing: 16) {
Button("Shows") { showingShowList = true }
.disabled(model.recorder.isRecording)
Button("Channel names") { showingChannelNames = true }
Button("Channel names") { open(.channelNames) }
}
.font(.caption)
// Top-aligned: Marker is taller (its count sits below), so centring would drop Record.
Expand Down Expand Up @@ -296,7 +364,7 @@ struct RecordScreen: View {
HStack(spacing: 10) {
Button("Shows") { showingShowList = true }
.disabled(model.recorder.isRecording)
Button("Names") { showingChannelNames = true }
Button("Names") { open(.channelNames) }
}
.font(.caption2)
Text(model.takeTitle)
Expand Down Expand Up @@ -349,9 +417,10 @@ struct RecordScreen: View {
model.alerts.ordered.first { $0.tone == .ok || $0.tone == .info }
}

/// Opens the Marker list. A sheet, so it never takes the record screen (and a running Take) away.
/// Opens the Marker list: a panel on an iPhone, a sheet on an iPad or Mac. Neither takes the record
/// screen (and a running Take) away.
private var markersButton: some View {
Button { showingMarkers = true } label: {
Button { open(.markers) } label: {
Image(systemName: "list.bullet")
.font(.title3)
.frame(width: 44, height: 44)
Expand Down Expand Up @@ -1315,3 +1384,8 @@ struct RecordButtonLabel: View {
private extension Duration {
var inSeconds: Double { Double(components.seconds) + Double(components.attoseconds) / 1e18 }
}

/// What can open as a panel on the record screen (#250).
enum LivePanel: Equatable {
case markers, channelNames, settings
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// Where the Marker list, Channel names and Settings open on the record screen. Stop and Marker must stay in
/// reach while one is open during a Take, and the meters never get shorter for it (#250).
public enum LivePanelPlacement: Equatable, Sendable {
/// iPhone portrait: the panel takes the meters' place; the transport below stays visible.
case overMeters
/// iPhone landscape: the panel sits beside the meters, which get narrower, not shorter.
case besideMeters
/// iPad and Mac: a sheet, until they get docked panels (#255).
case sheet

/// From the screen's size classes: compact height is a landscape iPhone, compact width a portrait one.
public init(horizontalCompact: Bool, verticalCompact: Bool) {
if verticalCompact { self = .besideMeters }
else if horizontalCompact { self = .overMeters }
else { self = .sheet }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@testable import Recording
import Testing

/// Where the Marker list, Channel names and Settings open, so Stop and Marker stay in reach during a Take.
@Suite("Live panel placement")
struct LivePanelPlacementTests {
@Test("iPhone portrait opens the panel over the meters, so the transport below stays in reach")
func portraitPhone() {
#expect(LivePanelPlacement(horizontalCompact: true, verticalCompact: false) == .overMeters)
}

@Test("iPhone landscape, small or Max, opens the panel beside the meters")
func landscapePhone() {
#expect(LivePanelPlacement(horizontalCompact: true, verticalCompact: true) == .besideMeters)
#expect(LivePanelPlacement(horizontalCompact: false, verticalCompact: true) == .besideMeters)
}

@Test("iPad and Mac keep the sheet until their docked panels (#255)")
func regular() {
#expect(LivePanelPlacement(horizontalCompact: false, verticalCompact: false) == .sheet)
}
}
Loading