1use ratatui::style::Color;
2use ratatui::text::{Line, Span};
3
4use crate::file::McrawFileInfo;
5
6pub fn format_metadata_for_display(info: &McrawFileInfo) -> Vec<Line<'static>> {
7 let mut lines = Vec::new();
8 lines.extend(format_general_section(info));
9 lines.extend(format_video_section(info));
10 lines.extend(format_camera_section(info));
11 lines.extend(format_audio_section(info));
12 lines
13}
14
15pub fn format_general_section(info: &McrawFileInfo) -> Vec<Line<'static>> {
16 let mut lines = Vec::new();
17 lines.push(Line::from(Span::styled(
18 "General",
19 Color::Yellow,
20 )));
21
22 let filename = info
23 .path
24 .split('/')
25 .last()
26 .unwrap_or(&info.path);
27 lines.push(Line::from(format!(
28 " Filename: {}",
29 filename
30 )));
31 lines.push(Line::from(format!(" Path: {}", info.path)));
32 lines.push(Line::from(format!(" Size: {}", format_size(info.size))));
33 lines.push(Line::from(format!(
34 " Format: {}",
35 info.format_name()
36 )));
37
38 if let Some(ref date) = info.camera_metadata.capture_date {
39 lines.push(Line::from(format!(
40 " Capture Date: {}",
41 format_capture_date(date)
42 )));
43 }
44
45 lines.push(Line::from(""));
46 lines
47}
48
49pub fn format_camera_section(info: &McrawFileInfo) -> Vec<Line<'static>> {
50 let mut lines = Vec::new();
51 lines.push(Line::from(Span::styled(
52 "Camera",
53 Color::Yellow,
54 )));
55
56 if let Some(ref model) = info.camera_metadata.camera_model {
57 if !model.is_empty() {
58 lines.push(Line::from(format!(" Camera: {}", model)));
59 }
60 }
61 if let Some(ref lens) = info.camera_metadata.lens_model {
62 lines.push(Line::from(format!(" Lens: {}", lens)));
63 }
64 if let Some(fl) = info.camera_metadata.focal_length {
65 lines.push(Line::from(format!(" Focal Length: {:.1}mm", fl)));
66 }
67 if let Some(ap) = info.camera_metadata.aperture {
68 lines.push(Line::from(format!(" Aperture: f/{:.1}", ap)));
69 }
70 if let Some(iso) = info.camera_metadata.iso {
71 lines.push(Line::from(format!(" ISO: {}", iso)));
72 }
73 if let Some(et) = info.camera_metadata.exposure_time {
74 lines.push(Line::from(format!(
75 " Exposure: {}",
76 format_exposure_time(et)
77 )));
78 }
79 if let Some(wb) = info.camera_metadata.white_balance {
80 lines.push(Line::from(format!(" White Balance:{:.0}K", wb)));
81 }
82 if let Some(ref cm) = info.camera_metadata.color_matrix {
83 let vals: Vec<String> = cm.iter().map(|v| format!("{:.2}", v)).collect();
84 lines.push(Line::from(format!(" Color Matrix1: [{}]", vals.join(", "))));
85 }
86 if let Some(ref cm) = info.camera_metadata.color_matrix2 {
87 let vals: Vec<String> = cm.iter().map(|v| format!("{:.2}", v)).collect();
88 lines.push(Line::from(format!(" Color Matrix2: [{}]", vals.join(", "))));
89 }
90 if let Some(i1) = info.camera_metadata.calibration_illuminant1 {
91 if let Some(i2) = info.camera_metadata.calibration_illuminant2 {
92 lines.push(Line::from(format!(" Cal Illuminants: {} / {}", i1, i2)));
93 }
94 }
95
96 lines.push(Line::from(""));
97 lines
98}
99
100pub fn format_video_section(info: &McrawFileInfo) -> Vec<Line<'static>> {
101 let mut lines = Vec::new();
102 lines.push(Line::from(Span::styled(
103 "Video",
104 Color::Yellow,
105 )));
106
107 lines.push(Line::from(format!(
108 " Resolution: {}x{} ({})",
109 info.width, info.height, info.resolution_label()
110 )));
111 lines.push(Line::from(format!(" FPS: {:.2}", info.fps)));
112 lines.push(Line::from(format!(
113 " Duration: {}",
114 format_duration(info.duration_seconds())
115 )));
116 lines.push(Line::from(format!(" Frames: {}", info.frame_count)));
117 lines.push(Line::from(format!(
118 " Bit Depth: {}-bit",
119 info.bit_depth
120 )));
121 lines.push(Line::from(format!(
122 " Bayer: {}",
123 info.bayer_pattern.name()
124 )));
125
126 if info.active_width > 0 && info.active_height > 0 {
127 lines.push(Line::from(format!(
128 " Active Area: {}x{} @({},{})",
129 info.active_width, info.active_height, info.active_offset_x, info.active_offset_y
130 )));
131 }
132
133 lines.push(Line::from(""));
134 lines
135}
136
137pub fn format_audio_section(info: &McrawFileInfo) -> Vec<Line<'static>> {
138 let mut lines = Vec::new();
139 lines.push(Line::from(Span::styled(
140 "Audio",
141 Color::Yellow,
142 )));
143
144 if info.has_audio {
145 lines.push(Line::from(" Has Audio: Yes".to_string()));
146 if info.audio_sample_rate > 0 {
147 lines.push(Line::from(format!(
148 " Sample Rate: {} Hz",
149 info.audio_sample_rate
150 )));
151 }
152 if info.audio_channels > 0 {
153 let ch_name = if info.audio_channels == 1 {
154 "mono"
155 } else if info.audio_channels == 2 {
156 "stereo"
157 } else {
158 "multi"
159 };
160 lines.push(Line::from(format!(
161 " Channels: {} ({})",
162 info.audio_channels, ch_name
163 )));
164 }
165 if let Some(length) = info.audio_length {
166 lines.push(Line::from(format!(" Audio Length: {} bytes", length)));
167 }
168 if let Some(offset) = info.audio_offset {
169 lines.push(Line::from(format!(" Audio Offset: {} bytes", offset)));
170 }
171 } else {
172 lines.push(Line::from(" Has Audio: No".to_string()));
173 }
174
175 lines.push(Line::from(""));
176 lines
177}
178
179pub fn format_duration(seconds: f64) -> String {
180 if seconds <= 0.0 {
181 return "0:00".to_string();
182 }
183
184 let total_secs = seconds as u64;
185 let hours = total_secs / 3600;
186 let minutes = (total_secs % 3600) / 60;
187 let secs = total_secs % 60;
188
189 if hours > 0 {
190 format!("{}:{:02}:{:02}", hours, minutes, secs)
191 } else {
192 format!("{}:{:02}", minutes, secs)
193 }
194}
195
196pub fn format_size(bytes: u64) -> String {
197 const KB: u64 = 1024;
198 const MB: u64 = 1024 * 1024;
199 const GB: u64 = 1024 * 1024 * 1024;
200
201 if bytes >= GB {
202 format!("{:.2} GB", bytes as f64 / GB as f64)
203 } else if bytes >= MB {
204 format!("{:.2} MB", bytes as f64 / MB as f64)
205 } else if bytes >= KB {
206 format!("{:.2} KB", bytes as f64 / KB as f64)
207 } else {
208 format!("{} B", bytes)
209 }
210}
211
212pub fn format_exposure_time(value: f64) -> String {
213 if value <= 0.0 {
214 return "Unknown".to_string();
215 }
216
217 let denominator = (1.0 / value).round() as u64;
218 if denominator > 0 && denominator <= 10000 {
219 format!("1/{}s", denominator)
220 } else {
221 format!("{:.2}s", value)
222 }
223}
224
225pub fn format_capture_date(raw: &str) -> String {
226 let raw = raw.trim();
227
228 if raw.len() >= 19 {
229 let date_part = &raw[..10];
230 let time_part = &raw[11..19];
231 let tz_part = raw[19..].trim();
232
233 let mut result = format!("{} {}", date_part, time_part);
234 if !tz_part.is_empty() {
235 result.push_str(tz_part);
236 }
237 return result;
238 }
239
240 raw.to_string()
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn test_format_duration_minutes() {
249 assert_eq!(format_duration(0.0), "0:00");
250 assert_eq!(format_duration(60.0), "1:00");
251 assert_eq!(format_duration(120.0), "2:00");
252 assert_eq!(format_duration(90.0), "1:30");
253 }
254
255 #[test]
256 fn test_format_duration_hours() {
257 assert_eq!(format_duration(3600.0), "1:00:00");
258 assert_eq!(format_duration(3725.0), "1:02:05");
259 assert_eq!(format_duration(7200.0), "2:00:00");
260 }
261
262 #[test]
263 fn test_format_size_bytes() {
264 assert_eq!(format_size(500), "500 B");
265 assert_eq!(format_size(1024), "1.00 KB");
266 assert_eq!(format_size(1024 * 512), "512.00 KB");
267 }
268
269 #[test]
270 fn test_format_size_kb() {
271 assert_eq!(format_size(1024 * 10), "10.00 KB");
272 assert_eq!(format_size(1024 * 1024 - 1), "1024.00 KB");
273 }
274
275 #[test]
276 fn test_format_size_mb() {
277 assert_eq!(format_size(1024 * 1024), "1.00 MB");
278 assert_eq!(format_size(1024 * 1024 * 10), "10.00 MB");
279 assert_eq!(format_size(1024 * 1024 * 256), "256.00 MB");
280 }
281
282 #[test]
283 fn test_format_size_gb() {
284 assert_eq!(format_size(1024 * 1024 * 1024), "1.00 GB");
285 assert_eq!(format_size(1024 * 1024 * 1024 * 2), "2.00 GB");
286 assert_eq!(format_size(1024 * 1024 * 1024 * 4), "4.00 GB");
287 }
288
289 #[test]
290 fn test_format_exposure_time() {
291 assert_eq!(format_exposure_time(0.0), "Unknown");
292 assert_eq!(format_exposure_time(1.0), "1/1s");
293 assert_eq!(format_exposure_time(0.5), "1/2s");
294 assert_eq!(format_exposure_time(1.0 / 60.0), "1/60s");
295 assert_eq!(format_exposure_time(1.0 / 120.0), "1/120s");
296 assert_eq!(format_exposure_time(1.0 / 1000.0), "1/1000s");
297 }
298
299 #[test]
300 fn test_format_capture_date() {
301 assert_eq!(
302 format_capture_date("2024-01-15T10:30:45+00:00"),
303 "2024-01-15 10:30:45+00:00"
304 );
305 assert_eq!(
306 format_capture_date("2024-06-20T14:00:00-05:00"),
307 "2024-06-20 14:00:00-05:00"
308 );
309 assert_eq!(format_capture_date("raw-date"), "raw-date");
310 }
311}