@@ -781,6 +781,10 @@ private struct TimelineExportCard: View {
781781 @ObservedObject var playbackUI : PlaybackUIState
782782 let timelineMarkers : [ Date ]
783783 let isSingleDayTimeline : Bool
784+ @State private var trimStartInput = " "
785+ @State private var trimEndInput = " "
786+ @FocusState private var focusedTrimField : TrimField ?
787+ @State private var lastFocusedTrimField : TrimField ?
784788
785789 var body : some View {
786790 let minDate = state. minDate ?? Date ( )
@@ -793,6 +797,25 @@ private struct TimelineExportCard: View {
793797 }
794798 . padding ( TeslaCamTheme . Metrics. cardPaddingCompact)
795799 . teslaCamCard ( )
800+ . onAppear ( perform: syncTrimInputs)
801+ . onChange ( of: state. trimStartSeconds) {
802+ guard focusedTrimField != . start else { return }
803+ trimStartInput = formatHMS ( state. trimStartSeconds)
804+ }
805+ . onChange ( of: state. trimEndSeconds) {
806+ guard focusedTrimField != . end else { return }
807+ trimEndInput = formatHMS ( state. trimEndSeconds)
808+ }
809+ . onChange ( of: focusedTrimField) {
810+ let newValue = focusedTrimField
811+ if lastFocusedTrimField == . start, newValue != . start {
812+ commitTrimStartInput ( )
813+ }
814+ if lastFocusedTrimField == . end, newValue != . end {
815+ commitTrimEndInput ( )
816+ }
817+ lastFocusedTrimField = newValue
818+ }
796819 }
797820
798821 private var timelineDock : some View {
@@ -945,9 +968,8 @@ private struct TimelineExportCard: View {
945968 . frame ( height: TeslaCamTheme . Metrics. compactControlHeight)
946969
947970 HStack ( spacing: TeslaCamTheme . Spacing. s) {
948- // Opt-in engraving. Default off keeps the fast, lossless muxed copy;
949- // turning it on burns the telemetry HUD into the file, which forces a
950- // composite re-encode (the caption to the right reflects the switch).
971+ // Telemetry burn-in remains optional. The base export stays a regular
972+ // encoded grid unless the user explicitly asks for original tracks.
951973 Toggle ( isOn: $state. exportOverlayOptions. telemetryHUD) {
952974 Text ( " Engrave telemetry " )
953975 . font ( TeslaCamTheme . Typography. label)
@@ -957,7 +979,7 @@ private struct TimelineExportCard: View {
957979 . tint ( TeslaCamTheme . Colors. accent)
958980 . fixedSize ( )
959981 . accessibilityIdentifier ( " engrave-telemetry-toggle " )
960- . accessibilityHint ( " Burns the telemetry HUD into the exported video. Off keeps a lossless, faster muxed copy. " )
982+ . accessibilityHint ( " Burns the telemetry HUD into the exported video. " )
961983
962984 Spacer ( minLength: 0 )
963985
@@ -990,17 +1012,17 @@ private struct TimelineExportCard: View {
9901012
9911013 private func controlTopRow( timeWidth: CGFloat , exportWidth: CGFloat ) -> some View {
9921014 HStack ( spacing: TeslaCamTheme . Spacing. s) {
993- macTimeField ( formatHMS ( playbackUI . currentSeconds ) , alignment : . leading , width: timeWidth)
1015+ trimInputField ( " In " , text : $trimStartInput , field : . start , width: timeWidth)
9941016
9951017 transportButtonCluster
9961018
997- macTimeField ( formatHMS ( state . totalDuration ) , alignment : . trailing , width: timeWidth)
1019+ trimInputField ( " Out " , text : $trimEndInput , field : . end , width: timeWidth)
9981020
9991021 inOutCluster
10001022
10011023 Spacer ( minLength: 0 )
10021024
1003- codecField
1025+ codecPickerField
10041026
10051027 Button {
10061028 state. exportRange ( )
@@ -1015,35 +1037,71 @@ private struct TimelineExportCard: View {
10151037 }
10161038 }
10171039
1018- private func macTimeField ( _ value : String , alignment : Alignment , width: CGFloat = 118 ) -> some View {
1040+ private func trimInputField ( _ title : String , text : Binding < String > , field : TrimField , width: CGFloat = 118 ) -> some View {
10191041 HStack ( spacing: TeslaCamTheme . Spacing. tightGap) {
1020- Image ( systemName : " timer " )
1042+ Text ( title . uppercased ( ) )
10211043 . font ( TeslaCamTheme . Typography. label)
1022- Text ( value)
1044+ . foregroundStyle ( TeslaCamTheme . Colors. textTertiary)
1045+ TextField ( title, text: text)
1046+ . textFieldStyle ( . plain)
10231047 . font ( TeslaCamTheme . Typography. monoDetail)
1048+ . foregroundStyle ( TeslaCamTheme . Colors. textSecondary)
1049+ . multilineTextAlignment ( . leading)
1050+ . focused ( $focusedTrimField, equals: field)
1051+ . onSubmit {
1052+ switch field {
1053+ case . start:
1054+ commitTrimStartInput ( )
1055+ case . end:
1056+ commitTrimEndInput ( )
1057+ }
1058+ }
10241059 . lineLimit ( 1 )
10251060 . minimumScaleFactor ( 0.72 )
10261061 }
1027- . foregroundStyle ( TeslaCamTheme . Colors. textSecondary)
10281062 . padding ( . horizontal, TeslaCamTheme . Spacing. s)
1029- . frame ( width: width, height: TeslaCamTheme . Metrics. compactControlHeight, alignment: alignment )
1063+ . frame ( width: width, height: TeslaCamTheme . Metrics. compactControlHeight, alignment: . leading )
10301064 . teslaCamCard ( fill: TeslaCamTheme . Colors. surface, radius: TeslaCamTheme . Metrics. controlCorner)
10311065 }
10321066
1033- // Read-only codec indicator. The export codec is fully automatic — adopted
1034- // from the source footage (H.264 vs H.265) — so this is informational, not a
1035- // menu. A fixed-width segment, so it can never overflow into the timeline the
1036- // way the old PRESET dropdown did.
1037- private var codecField : some View {
1067+ private var codecPickerField : some View {
10381068 HStack ( spacing: TeslaCamTheme . Spacing. tightGap) {
10391069 Text ( " Codec " . uppercased ( ) )
10401070 . font ( TeslaCamTheme . Typography. label)
10411071 . foregroundStyle ( TeslaCamTheme . Colors. textTertiary)
10421072
1043- CodecSegmentIndicator ( activeCodec: state. dominantSourceCodec)
1073+ Picker ( " " , selection: exportPresetBinding) {
1074+ Text ( " H.265 " ) . tag ( ExportPreset . maxQualityHEVC)
1075+ Text ( " H.264 " ) . tag ( ExportPreset . maxQualityH264)
1076+ }
1077+ . labelsHidden ( )
1078+ . pickerStyle ( . segmented)
1079+ . frame ( width: 132 )
10441080 }
10451081 }
10461082
1083+ private var exportPresetBinding : Binding < ExportPreset > {
1084+ Binding (
1085+ get: { state. exportPreset == . maxQualityH264 ? . maxQualityH264 : . maxQualityHEVC } ,
1086+ set: { state. setExportPreset ( $0) }
1087+ )
1088+ }
1089+
1090+ private func syncTrimInputs( ) {
1091+ trimStartInput = formatHMS ( state. trimStartSeconds)
1092+ trimEndInput = formatHMS ( state. trimEndSeconds)
1093+ }
1094+
1095+ private func commitTrimStartInput( ) {
1096+ _ = state. applyTrimStartInput ( trimStartInput)
1097+ trimStartInput = formatHMS ( state. trimStartSeconds)
1098+ }
1099+
1100+ private func commitTrimEndInput( ) {
1101+ _ = state. applyTrimEndInput ( trimEndInput)
1102+ trimEndInput = formatHMS ( state. trimEndSeconds)
1103+ }
1104+
10471105 private var playbackSecondsBinding : Binding < Double > {
10481106 Binding (
10491107 get: { playbackUI. currentSeconds } ,
@@ -1100,6 +1158,11 @@ private struct TimelineExportCard: View {
11001158
11011159 return parts. joined ( separator: " • " )
11021160 }
1161+
1162+ private enum TrimField : Hashable {
1163+ case start
1164+ case end
1165+ }
11031166}
11041167
11051168private struct ClipInformationPanel : View {
0 commit comments