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 if info.black_level_count > 0 {
134 lines.push(Line::from(format!(
135 " Black Level: {}",
136 info.black_level_per_channel[..info.black_level_count.min(4) as usize]
137 .iter()
138 .map(|v| format!("{}", v))
139 .collect::<Vec<_>>()
140 .join(", ")
141 )));
142 }
143 lines.push(Line::from(format!(" White Level: {}", info.white_level)));
144
145 if let Some(ref lsm) = info.lens_shading_map {
146 lines.push(Line::from(format!(
147 " Lens Shading: {}x{} grid, 4 ch",
148 lsm.width, lsm.height
149 )));
150 } else {
151 lines.push(Line::from(" Lens Shading: none".to_string()));
152 }
153
154 lines.push(Line::from(""));
155 lines
156}
157
158pub fn format_audio_section(info: &McrawFileInfo) -> Vec<Line<'static>> {
159 let mut lines = Vec::new();
160 lines.push(Line::from(Span::styled(
161 "Audio",
162 Color::Yellow,
163 )));
164
165 if info.has_audio {
166 lines.push(Line::from(" Has Audio: Yes".to_string()));
167 if info.audio_sample_rate > 0 {
168 lines.push(Line::from(format!(
169 " Sample Rate: {} Hz",
170 info.audio_sample_rate
171 )));
172 }
173 if info.audio_channels > 0 {
174 let ch_name = if info.audio_channels == 1 {
175 "mono"
176 } else if info.audio_channels == 2 {
177 "stereo"
178 } else {
179 "multi"
180 };
181 lines.push(Line::from(format!(
182 " Channels: {} ({})",
183 info.audio_channels, ch_name
184 )));
185 }
186 if let Some(length) = info.audio_length {
187 lines.push(Line::from(format!(" Audio Length: {} bytes", length)));
188 }
189 if let Some(offset) = info.audio_offset {
190 lines.push(Line::from(format!(" Audio Offset: {} bytes", offset)));
191 }
192 } else {
193 lines.push(Line::from(" Has Audio: No".to_string()));
194 }
195
196 lines.push(Line::from(""));
197 lines
198}
199
200pub fn format_duration(seconds: f64) -> String {
201 if seconds <= 0.0 {
202 return "0:00".to_string();
203 }
204
205 let total_secs = seconds as u64;
206 let hours = total_secs / 3600;
207 let minutes = (total_secs % 3600) / 60;
208 let secs = total_secs % 60;
209
210 if hours > 0 {
211 format!("{}:{:02}:{:02}", hours, minutes, secs)
212 } else {
213 format!("{}:{:02}", minutes, secs)
214 }
215}
216
217pub fn format_size(bytes: u64) -> String {
218 const KB: u64 = 1024;
219 const MB: u64 = 1024 * 1024;
220 const GB: u64 = 1024 * 1024 * 1024;
221
222 if bytes >= GB {
223 format!("{:.2} GB", bytes as f64 / GB as f64)
224 } else if bytes >= MB {
225 format!("{:.2} MB", bytes as f64 / MB as f64)
226 } else if bytes >= KB {
227 format!("{:.2} KB", bytes as f64 / KB as f64)
228 } else {
229 format!("{} B", bytes)
230 }
231}
232
233pub fn format_exposure_time(value: f64) -> String {
234 if value <= 0.0 {
235 return "Unknown".to_string();
236 }
237
238 let denominator = (1.0 / value).round() as u64;
239 if denominator > 0 && denominator <= 10000 {
240 format!("1/{}s", denominator)
241 } else {
242 format!("{:.2}s", value)
243 }
244}
245
246pub fn format_capture_date(raw: &str) -> String {
247 let raw = raw.trim();
248
249 if raw.len() >= 19 {
250 let date_part = &raw[..10];
251 let time_part = &raw[11..19];
252 let tz_part = raw[19..].trim();
253
254 let mut result = format!("{} {}", date_part, time_part);
255 if !tz_part.is_empty() {
256 result.push_str(tz_part);
257 }
258 return result;
259 }
260
261 raw.to_string()
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_format_duration_minutes() {
270 assert_eq!(format_duration(0.0), "0:00");
271 assert_eq!(format_duration(60.0), "1:00");
272 assert_eq!(format_duration(120.0), "2:00");
273 assert_eq!(format_duration(90.0), "1:30");
274 }
275
276 #[test]
277 fn test_format_duration_hours() {
278 assert_eq!(format_duration(3600.0), "1:00:00");
279 assert_eq!(format_duration(3725.0), "1:02:05");
280 assert_eq!(format_duration(7200.0), "2:00:00");
281 }
282
283 #[test]
284 fn test_format_size_bytes() {
285 assert_eq!(format_size(500), "500 B");
286 assert_eq!(format_size(1024), "1.00 KB");
287 assert_eq!(format_size(1024 * 512), "512.00 KB");
288 }
289
290 #[test]
291 fn test_format_size_kb() {
292 assert_eq!(format_size(1024 * 10), "10.00 KB");
293 assert_eq!(format_size(1024 * 1024 - 1), "1024.00 KB");
294 }
295
296 #[test]
297 fn test_format_size_mb() {
298 assert_eq!(format_size(1024 * 1024), "1.00 MB");
299 assert_eq!(format_size(1024 * 1024 * 10), "10.00 MB");
300 assert_eq!(format_size(1024 * 1024 * 256), "256.00 MB");
301 }
302
303 #[test]
304 fn test_format_size_gb() {
305 assert_eq!(format_size(1024 * 1024 * 1024), "1.00 GB");
306 assert_eq!(format_size(1024 * 1024 * 1024 * 2), "2.00 GB");
307 assert_eq!(format_size(1024 * 1024 * 1024 * 4), "4.00 GB");
308 }
309
310 #[test]
311 fn test_format_exposure_time() {
312 assert_eq!(format_exposure_time(0.0), "Unknown");
313 assert_eq!(format_exposure_time(1.0), "1/1s");
314 assert_eq!(format_exposure_time(0.5), "1/2s");
315 assert_eq!(format_exposure_time(1.0 / 60.0), "1/60s");
316 assert_eq!(format_exposure_time(1.0 / 120.0), "1/120s");
317 assert_eq!(format_exposure_time(1.0 / 1000.0), "1/1000s");
318 }
319
320 #[test]
321 fn test_format_capture_date() {
322 assert_eq!(
323 format_capture_date("2024-01-15T10:30:45+00:00"),
324 "2024-01-15 10:30:45+00:00"
325 );
326 assert_eq!(
327 format_capture_date("2024-06-20T14:00:00-05:00"),
328 "2024-06-20 14:00:00-05:00"
329 );
330 assert_eq!(format_capture_date("raw-date"), "raw-date");
331 }
332}