use crate::media::{
build_command, duration_progress_label, human_size, media_kind_label, path_extension_lower,
probe_duration, start_operation, suggest_output_path, supported_audio_extension,
supported_media_extension, supported_video_extension, AudioFormat, ConversionMode,
ConversionRequest, OperationEvent, QualityPreset, VideoFormat,
};
use eframe::egui::{
self, Align2, Color32, FontId, Pos2, Rect, RichText, Rounding, Sense, Stroke, Vec2,
};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::mpsc::Receiver;
use std::time::Duration;
pub struct CaeryApp {
input_path: String,
output_path: String,
last_suggested_output: String,
mode: ConversionMode,
audio_format: AudioFormat,
video_format: VideoFormat,
quality: QualityPreset,
overwrite_output: bool,
final_confirm_open: bool,
logs: Vec<String>,
operation_rx: Option<Receiver<OperationEvent>>,
operation_running: bool,
operation_status: String,
progress: Option<(f64, f64)>,
notice: Option<AppNotice>,
details_open: bool,
media_browser: MediaBrowserState,
logo_texture: Option<egui::TextureHandle>,
}
#[derive(Debug, Clone)]
struct MediaBrowserState {
open: bool,
current_dir: PathBuf,
entries: Vec<BrowserEntry>,
error: Option<String>,
}
impl MediaBrowserState {
fn new() -> Self {
Self {
open: false,
current_dir: initial_browser_directory(""),
entries: Vec::new(),
error: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct BrowserEntry {
name: String,
path: PathBuf,
kind: BrowserEntryKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum BrowserEntryKind {
Directory,
Media,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum BrowserAction {
None,
Parent,
Refresh,
OpenDirectory(PathBuf),
SelectMedia(PathBuf),
Close,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum NoticeLevel {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct AppNotice {
level: NoticeLevel,
title: String,
detail: String,
action: Option<String>,
}
impl CaeryApp {
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
cc.egui_ctx.set_visuals(knott_visuals());
let logo_texture = load_logo_texture(&cc.egui_ctx);
Self {
input_path: String::new(),
output_path: String::new(),
last_suggested_output: String::new(),
mode: ConversionMode::ExtractAudio,
audio_format: AudioFormat::Mp3,
video_format: VideoFormat::Mp4,
quality: QualityPreset::Balanced,
overwrite_output: false,
final_confirm_open: false,
logs: vec!["CAERY ONLINE // FFMPEG ROUTE IDLE".to_owned()],
operation_rx: None,
operation_running: false,
operation_status: "READY".to_owned(),
progress: None,
notice: None,
details_open: false,
media_browser: MediaBrowserState::new(),
logo_texture,
}
}
fn current_request(&self) -> ConversionRequest {
ConversionRequest {
mode: self.mode,
input_path: PathBuf::from(self.input_path.trim()),
output_path: PathBuf::from(self.output_path.trim()),
audio_format: self.audio_format,
video_format: self.video_format,
quality: self.quality,
overwrite: self.overwrite_output,
}
}
fn can_start(&self) -> bool {
!self.operation_running && build_command(&self.current_request()).is_ok()
}
fn preflight_error(&self) -> Option<String> {
build_command(&self.current_request())
.err()
.map(|error| error.to_string())
}
fn drain_operation_events(&mut self) {
let mut finished = false;
if let Some(rx) = self.operation_rx.take() {
while let Ok(event) = rx.try_recv() {
match event {
OperationEvent::Started(command) => {
self.operation_status = "ENCODING".to_owned();
self.notice = None;
self.push_log(format!("START // {command}"));
}
OperationEvent::Log(line) => {
let lower = line.to_ascii_lowercase();
if lower.contains("input #") {
self.operation_status = "READING SOURCE".to_owned();
} else if lower.contains("output #") {
self.operation_status = "WRITING OUTPUT".to_owned();
} else if lower.contains("video:") || lower.contains("audio:") {
self.operation_status = "FINALIZING".to_owned();
}
self.push_log(line.to_ascii_uppercase());
}
OperationEvent::Progress {
current_seconds,
total_seconds,
} => {
self.progress = Some((current_seconds, total_seconds));
self.operation_status =
duration_progress_label(current_seconds, total_seconds);
}
OperationEvent::Finished(result) => {
finished = true;
self.operation_running = false;
match result {
Ok(()) => {
if let Some((_, total)) = self.progress {
self.progress = Some((total, total));
}
self.operation_status = "COMPLETE".to_owned();
self.notice = Some(AppNotice {
level: NoticeLevel::Info,
title: "Conversion complete".to_owned(),
detail: format!(
"Output is ready at {}.",
self.output_path.trim()
),
action: Some(
"Review the output before removing the source file."
.to_owned(),
),
});
self.push_log("CONVERSION COMPLETE // OUTPUT READY".to_owned());
}
Err(error) => {
self.operation_status = "FAILED".to_owned();
self.push_log(format!("CONVERSION FAILED // {error}"));
self.notice = Some(classify_operation_error(&error));
self.details_open = true;
}
}
}
}
}
if !finished {
self.operation_rx = Some(rx);
}
}
}
fn start_selected_operation(&mut self) {
if self.operation_running {
self.final_confirm_open = false;
return;
}
let request = self.current_request();
match build_command(&request) {
Ok(command) => {
let expected_duration = probe_duration(&request.input_path);
self.logs.clear();
self.notice = None;
self.details_open = false;
self.progress = None;
self.final_confirm_open = false;
self.operation_running = true;
self.operation_status = if expected_duration.is_some() {
"STARTING".to_owned()
} else {
"STARTING // NO DURATION PROBE".to_owned()
};
self.operation_rx = Some(start_operation(command, expected_duration));
}
Err(error) => {
let detail = error.to_string();
self.final_confirm_open = false;
self.notice = Some(validation_notice(detail.clone()));
self.details_open = true;
self.push_log(format!("CANNOT START // {detail}"));
}
}
}
fn review_selected_operation(&mut self) {
match build_command(&self.current_request()) {
Ok(_) => {
self.notice = None;
self.final_confirm_open = true;
}
Err(error) => {
let detail = error.to_string();
self.notice = Some(validation_notice(detail.clone()));
self.details_open = true;
self.push_log(format!("PRE-FLIGHT BLOCKED // {detail}"));
}
}
}
fn push_log(&mut self, line: String) {
self.logs.push(line);
if self.logs.len() > 250 {
let overflow = self.logs.len() - 250;
self.logs.drain(0..overflow);
}
}
fn open_media_browser(&mut self) {
self.media_browser.open = true;
self.media_browser.current_dir = initial_browser_directory(&self.input_path);
self.refresh_browser_entries();
}
fn refresh_browser_entries(&mut self) {
match scan_browser_directory(&self.media_browser.current_dir) {
Ok(entries) => {
self.media_browser.entries = entries;
self.media_browser.error = None;
if self
.notice
.as_ref()
.is_some_and(|notice| notice.title == "Media Vault unavailable")
{
self.notice = None;
}
}
Err(error) => {
let detail = error.to_string();
self.media_browser.entries.clear();
self.media_browser.error = Some(detail.clone());
self.notice = Some(AppNotice {
level: NoticeLevel::Warning,
title: "Media Vault unavailable".to_owned(),
detail: detail.clone(),
action: Some(
"Choose another directory, check permissions, then refresh.".to_owned(),
),
});
self.push_log(format!("MEDIA VAULT READ FAILED // {detail}"));
}
}
}
fn set_browser_dir(&mut self, path: PathBuf) {
self.media_browser.current_dir = path;
self.refresh_browser_entries();
}
fn set_input_path(&mut self, path: PathBuf) {
self.input_path = path.display().to_string();
self.mode = infer_route_for_source(&path, self.mode);
self.refresh_suggested_output(true);
let detail = fs::metadata(&path)
.ok()
.map(|metadata| format!(" // {}", human_size(metadata.len())))
.unwrap_or_default();
self.push_log(format!("SOURCE SELECTED // {}{}", self.input_path, detail));
}
fn refresh_suggested_output(&mut self, force: bool) {
let suggestion = suggest_output_path(
Path::new(self.input_path.trim()),
self.mode,
self.audio_format,
self.video_format,
)
.map(|path| path.display().to_string())
.unwrap_or_default();
if force
|| self.output_path.trim().is_empty()
|| self.output_path == self.last_suggested_output
{
self.output_path = suggestion.clone();
}
self.last_suggested_output = suggestion;
}
fn handle_dropped_files(&mut self, ctx: &egui::Context) {
let dropped_files = ctx.input(|input| input.raw.dropped_files.clone());
for file in dropped_files {
if let Some(path) = file.path {
if supported_media_extension(&path) {
self.set_input_path(path);
break;
}
}
}
}
fn handle_browser_action(&mut self, action: BrowserAction) {
match action {
BrowserAction::None => {}
BrowserAction::Parent => {
if let Some(parent) = self.media_browser.current_dir.parent() {
self.set_browser_dir(parent.to_path_buf());
}
}
BrowserAction::Refresh => self.refresh_browser_entries(),
BrowserAction::OpenDirectory(path) => self.set_browser_dir(path),
BrowserAction::SelectMedia(path) => {
self.set_input_path(path);
self.media_browser.open = false;
}
BrowserAction::Close => self.media_browser.open = false,
}
}
}
impl eframe::App for CaeryApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.handle_dropped_files(ctx);
self.drain_operation_events();
let phase = ctx.input(|input| input.time) as f32;
ctx.request_repaint_after(Duration::from_millis(if self.operation_running {
16
} else {
33
}));
egui::CentralPanel::default()
.frame(
egui::Frame::none()
.fill(kd_black())
.inner_margin(egui::Margin::same(8.0)),
)
.show(ctx, |ui| {
let viewport_width = ui.available_width();
let viewport_height = ui.available_height();
let compact = viewport_width < 940.0 || viewport_height < 690.0;
paint_background(ui, phase);
system_status_bar(
ui,
self.operation_running,
&self.operation_status,
phase,
self.notice.as_ref().map(|notice| notice.level),
);
ui.add_space(6.0);
self.header(ui, phase, compact);
ui.add_space(7.0);
let workspace_height = ui.available_height();
let source_width = (viewport_width * 0.43).clamp(350.0, 470.0);
ui.horizontal(|ui| {
ui.allocate_ui_with_layout(
Vec2::new(source_width, workspace_height),
egui::Layout::top_down(egui::Align::Min),
|ui| self.source_panel(ui, phase, workspace_height),
);
ui.add_space(7.0);
ui.allocate_ui_with_layout(
Vec2::new(ui.available_width(), workspace_height),
egui::Layout::top_down(egui::Align::Min),
|ui| self.route_panel(ui, phase, workspace_height),
);
});
});
self.media_browser_window(ctx, phase);
self.final_confirmation_window(ctx, phase);
self.details_window(ctx);
}
}
impl CaeryApp {
fn header(&mut self, ui: &mut egui::Ui, phase: f32, compact: bool) {
let header_height = if compact { 62.0 } else { 68.0 };
let (rect, _) = ui.allocate_exact_size(
Vec2::new(ui.available_width(), header_height),
Sense::hover(),
);
let painter = ui.painter_at(rect);
painter.rect_filled(rect, Rounding::same(11.0), Color32::from_rgb(9, 20, 28));
painter.rect_stroke(
rect,
Rounding::same(11.0),
Stroke::new(1.0, Color32::from_rgb(31, 76, 88)),
);
draw_header_sweep(&painter, rect, phase);
let logo_rect = Rect::from_min_size(
rect.left_top() + egui::vec2(9.0, 8.0),
Vec2::new(72.0, rect.height() - 16.0),
);
painter.rect_filled(logo_rect, Rounding::same(8.0), Color32::from_rgb(3, 9, 14));
painter.rect_stroke(
logo_rect,
Rounding::same(8.0),
Stroke::new(1.0, Color32::from_rgb(39, 103, 116)),
);
if let Some(texture) = &self.logo_texture {
let logo_size = texture.size_vec2();
let max_size = logo_rect.size() - egui::vec2(7.0, 7.0);
let scale = (max_size.x / logo_size.x).min(max_size.y / logo_size.y);
let image_rect = Rect::from_center_size(logo_rect.center(), logo_size * scale);
painter.image(
texture.id(),
image_rect,
Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)),
Color32::WHITE,
);
}
let copy_x = logo_rect.right() + 14.0;
painter.text(
Pos2::new(copy_x, rect.top() + 9.0),
Align2::LEFT_TOP,
"CAERY / MEDIA CONSOLE",
FontId::monospace(9.0),
kd_cyan(),
);
painter.text(
Pos2::new(copy_x, rect.top() + 23.0),
Align2::LEFT_TOP,
"Convert media with a deliberate route.",
FontId::proportional(if compact { 17.0 } else { 19.0 }),
Color32::from_rgb(244, 253, 255),
);
painter.text(
Pos2::new(copy_x, rect.bottom() - 8.0),
Align2::LEFT_BOTTOM,
"SOURCE / CODEC / CONTAINER / OUTPUT",
FontId::monospace(8.5),
Color32::from_rgb(123, 157, 165),
);
let status = if self.operation_running {
self.operation_status.as_str()
} else if source_file_ready(&self.input_path, self.mode) {
"SOURCE LOCKED"
} else {
"AWAITING SOURCE"
};
if rect.width() > 700.0 {
draw_status_pill(
&painter,
rect.right_center() + egui::vec2(-206.0, -15.0),
status,
);
}
}
fn source_panel(&mut self, ui: &mut egui::Ui, phase: f32, panel_height: f32) {
let response = kd_panel(ui, |ui| {
ui.set_min_height((panel_height - 26.0).max(0.0));
panel_heading(ui, "SOURCE VAULT", "LOCAL MEDIA");
ui.add_space(6.0);
let mut browse_clicked = false;
let mut clear_clicked = false;
let body_height = ui.available_height().max(120.0);
egui::ScrollArea::vertical()
.max_height(body_height)
.auto_shrink([false, false])
.show(ui, |ui| {
ui.horizontal(|ui| {
field_label(ui, self.mode.source_label());
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui
.add_enabled(
!self.operation_running,
kd_button("SELECT MEDIA", false),
)
.clicked()
{
browse_clicked = true;
}
if ui
.add_enabled(
!self.operation_running && !self.input_path.trim().is_empty(),
kd_button("CLEAR", false),
)
.clicked()
{
clear_clicked = true;
}
});
});
let source_edit = egui::TextEdit::singleline(&mut self.input_path)
.hint_text("/home/me/Videos/source.mp4 or drop media here")
.desired_width(f32::INFINITY);
if ui
.add_enabled(!self.operation_running, source_edit)
.changed()
{
self.mode =
infer_route_for_source(Path::new(self.input_path.trim()), self.mode);
self.refresh_suggested_output(false);
}
ui.add_space(7.0);
media_preview_card(ui, self.input_path.trim(), self.mode, phase);
ui.add_space(10.0);
ui.horizontal(|ui| {
field_label(ui, "OUTPUT FILE");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui
.add_enabled(!self.operation_running, kd_button("AUTO NAME", false))
.clicked()
{
self.refresh_suggested_output(true);
}
});
});
let output_edit = egui::TextEdit::singleline(&mut self.output_path)
.hint_text("/home/me/Videos/source-caery.mp3")
.desired_width(f32::INFINITY);
ui.add_enabled(!self.operation_running, output_edit);
ui.add_space(7.0);
output_preview_card(
ui,
self.output_path.trim(),
self.mode,
self.audio_format,
self.video_format,
self.overwrite_output,
phase,
);
ui.add_space(10.0);
tech_note(
ui,
"SOURCE ROUTING",
"DROP MEDIA OR SELECT A LOCAL FILE. THE ROUTE IS INFERRED FROM ITS TYPE.",
);
});
if browse_clicked {
self.open_media_browser();
}
if clear_clicked {
self.input_path.clear();
self.refresh_suggested_output(true);
self.push_log("SOURCE CLEARED".to_owned());
}
});
draw_panel_top_edge(ui.painter(), response.rect);
}
fn route_panel(&mut self, ui: &mut egui::Ui, phase: f32, panel_height: f32) {
let preflight_error = self.preflight_error();
let can_start = self.can_start();
let response = kd_panel(ui, |ui| {
ui.set_min_height((panel_height - 26.0).max(0.0));
panel_heading(ui, "CONVERSION ROUTE", "CODEC MATRIX");
ui.add_space(6.0);
let dock_height = if preflight_error.is_some() {
72.0
} else {
56.0
};
let body_height = (ui.available_height() - dock_height - 6.0).max(100.0);
ui.allocate_ui_with_layout(
Vec2::new(ui.available_width(), body_height),
egui::Layout::top_down(egui::Align::Min),
|ui| {
egui::ScrollArea::vertical()
.max_height(body_height)
.auto_shrink([false, false])
.show(ui, |ui| {
let previous_mode = self.mode;
route_selector(ui, &mut self.mode, self.operation_running, phase);
if self.mode != previous_mode {
self.refresh_suggested_output(false);
}
ui.add_space(6.0);
tech_note(ui, self.mode.title(), self.mode.note_text());
ui.add_space(7.0);
if format_controls(
ui,
self.mode,
&mut self.audio_format,
&mut self.video_format,
&mut self.quality,
self.operation_running,
) {
self.refresh_suggested_output(false);
}
ui.add_space(8.0);
output_write_box(
ui,
self.output_path.trim(),
&mut self.overwrite_output,
self.operation_running,
);
ui.add_space(7.0);
readiness_panel(
ui,
source_file_ready(&self.input_path, self.mode),
output_path_ready(
&self.output_path,
self.mode
.output_extension(self.audio_format, self.video_format),
self.overwrite_output,
),
self.mode,
);
if let Some((current, total)) = self.progress {
ui.add_space(7.0);
animated_progress(ui, current, total, phase);
} else if self.operation_running {
ui.add_space(7.0);
indeterminate_progress(ui, phase);
}
if let Some(notice) = &self.notice {
ui.add_space(7.0);
operation_notice_card(ui, notice);
}
});
},
);
ui.add_space(6.0);
let dock_status = if self.operation_running {
self.operation_status.as_str()
} else if can_start {
"READY TO REVIEW"
} else {
"PREFLIGHT BLOCKED"
};
if operation_action_dock(
ui,
!self.operation_running,
self.operation_running,
dock_status,
preflight_error.as_deref(),
&mut self.details_open,
self.logs.len(),
) {
self.review_selected_operation();
}
});
draw_panel_top_edge(ui.painter(), response.rect);
}
fn media_browser_window(&mut self, ctx: &egui::Context, phase: f32) {
if !self.media_browser.open {
return;
}
let mut open = self.media_browser.open;
let mut action = BrowserAction::None;
egui::Window::new("MEDIA VAULT // LOCAL FILESYSTEM")
.open(&mut open)
.collapsible(false)
.resizable(true)
.default_size(Vec2::new(820.0, 560.0))
.frame(
egui::Frame::none()
.fill(Color32::from_rgba_premultiplied(7, 14, 20, 248))
.rounding(Rounding::same(10.0))
.stroke(Stroke::new(1.0, Color32::from_rgb(39, 97, 111)))
.inner_margin(egui::Margin::same(14.0)),
)
.show(ctx, |ui| {
action = media_browser_contents(ui, &self.media_browser, phase);
});
self.media_browser.open = open;
self.handle_browser_action(action);
}
fn details_window(&mut self, ctx: &egui::Context) {
if !self.details_open {
return;
}
let mut open = self.details_open;
let mut clear = false;
let preflight_error = self.preflight_error();
egui::Window::new("DETAILS // CONVERSION INSPECTOR")
.open(&mut open)
.collapsible(false)
.resizable(true)
.default_size(Vec2::new(720.0, 380.0))
.frame(
egui::Frame::none()
.fill(Color32::from_rgb(7, 14, 20))
.rounding(Rounding::same(10.0))
.stroke(Stroke::new(1.0, Color32::from_rgb(36, 91, 104)))
.inner_margin(egui::Margin::same(14.0)),
)
.show(ctx, |ui| {
panel_heading(ui, "OPERATION DETAILS", "LOG + RECOVERY CONTEXT");
ui.add_space(8.0);
if let Some(notice) = &self.notice {
operation_notice_card(ui, notice);
ui.add_space(8.0);
}
if let Some(error) = &preflight_error {
tech_note(ui, "CURRENT PREFLIGHT", error);
ui.add_space(8.0);
}
log_panel(ui, &self.logs, false);
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(
RichText::new(format!("{} EVENTS RETAINED", self.logs.len()))
.monospace()
.size(9.0)
.color(Color32::from_rgb(105, 145, 154)),
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.add(kd_button("CLEAR LOG", false)).clicked() {
clear = true;
}
});
});
});
self.details_open = open;
if clear {
self.logs.clear();
}
}
fn final_confirmation_window(&mut self, ctx: &egui::Context, phase: f32) {
if !self.final_confirm_open {
return;
}
let request = self.current_request();
let operation = self.mode.title().to_ascii_uppercase();
let source = if self.input_path.trim().is_empty() {
"NO SOURCE".to_owned()
} else {
self.input_path.clone()
};
let output = if self.output_path.trim().is_empty() {
"NO OUTPUT".to_owned()
} else {
self.output_path.clone()
};
let format = match self.mode {
ConversionMode::ExtractAudio | ConversionMode::TranscodeAudio => {
self.audio_format.label().to_owned()
}
ConversionMode::TranscodeVideo => self.video_format.label().to_owned(),
};
let overwrite = if request.output_path.exists() {
if self.overwrite_output {
"EXISTING OUTPUT WILL BE REPLACED"
} else {
"OUTPUT EXISTS // OVERWRITE NOT ENABLED"
}
} else {
"NEW OUTPUT FILE"
};
let preflight_error = self.preflight_error();
let mut open = self.final_confirm_open;
let mut submit = false;
let mut cancel = false;
egui::Window::new("FINAL ROUTE CHECK // START CONVERSION?")
.open(&mut open)
.collapsible(false)
.resizable(false)
.default_size(Vec2::new(640.0, 390.0))
.frame(
egui::Frame::none()
.fill(Color32::from_rgba_premultiplied(7, 14, 20, 250))
.rounding(Rounding::same(10.0))
.stroke(Stroke::new(1.0, Color32::from_rgb(48, 107, 120)))
.inner_margin(egui::Margin::same(16.0)),
)
.show(ctx, |ui| {
panel_heading(ui, "ROUTE SUBMIT", "VERIFY MEDIA WRITE");
ui.add_space(12.0);
final_warning_card(ui, phase);
ui.add_space(12.0);
tech_note(ui, "OPERATION", &operation);
ui.add_space(8.0);
tech_note(ui, "SOURCE", &source);
ui.add_space(8.0);
tech_note(ui, "OUTPUT", &output);
ui.add_space(8.0);
tech_note(
ui,
"FORMAT / QUALITY",
&format!("{} // {}", format, self.quality.label()),
);
ui.add_space(8.0);
tech_note(ui, "OUTPUT POLICY", overwrite);
if let Some(error) = &preflight_error {
ui.add_space(8.0);
tech_note(ui, "PREFLIGHT BLOCKED", error);
}
ui.add_space(16.0);
ui.horizontal(|ui| {
if ui.add(kd_button("CANCEL", false)).clicked() {
cancel = true;
}
if ui
.add_enabled(self.can_start(), kd_button("START CONVERSION", true))
.clicked()
{
submit = true;
}
});
});
self.final_confirm_open = open && !cancel && !submit;
if cancel {
self.push_log("FINAL ROUTE CHECK CANCELLED".to_owned());
}
if submit {
self.start_selected_operation();
}
}
}
fn load_logo_texture(ctx: &egui::Context) -> Option<egui::TextureHandle> {
let bytes = include_bytes!("../assets/logo.png");
let image = image::load_from_memory(bytes).ok()?.to_rgba8();
let size = [image.width() as usize, image.height() as usize];
let pixels = image.as_flat_samples();
let color_image = egui::ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
Some(ctx.load_texture("caery-logo", color_image, egui::TextureOptions::LINEAR))
}
fn knott_visuals() -> egui::Visuals {
let mut visuals = egui::Visuals::dark();
visuals.override_text_color = Some(Color32::from_rgb(214, 235, 240));
visuals.panel_fill = kd_black();
visuals.window_fill = kd_black();
visuals.extreme_bg_color = kd_black();
visuals.faint_bg_color = Color32::from_rgb(7, 15, 21);
visuals.widgets.noninteractive.bg_fill = Color32::from_rgb(8, 18, 25);
visuals.widgets.inactive.bg_fill = Color32::from_rgb(13, 27, 36);
visuals.widgets.hovered.bg_fill = Color32::from_rgb(20, 48, 59);
visuals.widgets.active.bg_fill = Color32::from_rgb(27, 79, 92);
visuals.widgets.noninteractive.fg_stroke = Stroke::new(1.0, Color32::from_rgb(160, 190, 197));
visuals.widgets.inactive.fg_stroke = Stroke::new(1.0, Color32::from_rgb(190, 220, 226));
visuals.widgets.hovered.fg_stroke = Stroke::new(1.0, Color32::from_rgb(235, 252, 255));
visuals.widgets.active.fg_stroke = Stroke::new(1.0, Color32::WHITE);
visuals.selection.bg_fill = kd_cyan();
visuals.selection.stroke = Stroke::new(1.0, Color32::from_rgb(3, 16, 21));
visuals
}
fn kd_black() -> Color32 {
Color32::from_rgb(5, 9, 14)
}
fn kd_surface() -> Color32 {
Color32::from_rgb(3, 10, 15)
}
fn kd_panel_fill() -> Color32 {
Color32::from_rgba_premultiplied(11, 20, 29, 244)
}
fn kd_border() -> Color32 {
Color32::from_rgb(24, 48, 58)
}
fn kd_cyan() -> Color32 {
Color32::from_rgb(76, 229, 255)
}
fn draw_header_sweep(painter: &egui::Painter, rect: Rect, phase: f32) {
let sweep = ((phase * 0.09).fract() * (rect.width() + 260.0)) - 130.0;
let x = rect.left() + sweep;
painter.line_segment(
[
Pos2::new(x, rect.top() + 8.0),
Pos2::new(x + 130.0, rect.bottom() - 8.0),
],
Stroke::new(1.0, Color32::from_rgba_premultiplied(76, 229, 255, 24)),
);
painter.line_segment(
[
Pos2::new(x - 24.0, rect.top() + 8.0),
Pos2::new(x + 106.0, rect.bottom() - 8.0),
],
Stroke::new(0.6, Color32::from_rgba_premultiplied(76, 229, 255, 12)),
);
}
fn paint_background(ui: &mut egui::Ui, phase: f32) {
let rect = ui.max_rect();
let painter = ui.painter().clone();
painter.rect_filled(rect, Rounding::ZERO, kd_black());
let spacing = 52.0;
let mut x = rect.left() - rect.left().rem_euclid(spacing);
while x <= rect.right() {
painter.line_segment(
[Pos2::new(x, rect.top()), Pos2::new(x, rect.bottom())],
Stroke::new(1.0, Color32::from_rgba_premultiplied(255, 255, 255, 8)),
);
x += spacing;
}
let mut y = rect.top() - rect.top().rem_euclid(spacing);
while y <= rect.bottom() {
painter.line_segment(
[Pos2::new(rect.left(), y), Pos2::new(rect.right(), y)],
Stroke::new(1.0, Color32::from_rgba_premultiplied(255, 255, 255, 8)),
);
y += spacing;
}
let marker_x = rect.left() + rect.width() * ((phase * 0.025).sin() * 0.5 + 0.5);
painter.line_segment(
[
Pos2::new(marker_x, rect.top()),
Pos2::new(marker_x, rect.bottom()),
],
Stroke::new(1.0, Color32::from_rgba_premultiplied(76, 229, 255, 7)),
);
}
fn system_status_bar(
ui: &mut egui::Ui,
running: bool,
status: &str,
phase: f32,
notice_level: Option<NoticeLevel>,
) {
let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 28.0), Sense::hover());
let painter = ui.painter_at(rect);
painter.rect_filled(
rect,
Rounding::ZERO,
Color32::from_rgba_premultiplied(7, 12, 18, 246),
);
painter.line_segment(
[rect.left_bottom(), rect.right_bottom()],
Stroke::new(1.0, Color32::from_rgb(18, 51, 62)),
);
let dot = if running {
((phase * 6.0).sin() * 0.5 + 0.5) + 3.0
} else {
3.5
};
let status_color = match notice_level {
Some(NoticeLevel::Error) => Color32::from_rgb(255, 117, 128),
Some(NoticeLevel::Warning) => Color32::from_rgb(239, 196, 106),
_ => kd_cyan(),
};
painter.circle_filled(
rect.left_center() + egui::vec2(14.0, 0.0),
dot,
status_color,
);
painter.text(
rect.left_center() + egui::vec2(26.0, 0.0),
Align2::LEFT_CENTER,
"CAERY / CONVERSION SYSTEM",
FontId::monospace(9.5),
Color32::from_rgb(232, 251, 255),
);
if rect.width() > 620.0 {
painter.text(
rect.right_center() + egui::vec2(-12.0, 0.0),
Align2::RIGHT_CENTER,
format!("{} // v{}", status, env!("CARGO_PKG_VERSION")),
FontId::monospace(9.0),
status_color,
);
}
}
fn kd_panel<R>(ui: &mut egui::Ui, add_contents: impl FnOnce(&mut egui::Ui) -> R) -> egui::Response {
egui::Frame::none()
.fill(kd_panel_fill())
.rounding(Rounding::same(10.0))
.stroke(Stroke::new(1.0, kd_border()))
.inner_margin(egui::Margin::same(12.0))
.show(ui, add_contents)
.response
}
fn draw_panel_top_edge(painter: &egui::Painter, rect: Rect) {
painter.line_segment(
[
rect.left_top() + egui::vec2(12.0, 1.0),
rect.right_top() + egui::vec2(-12.0, 1.0),
],
Stroke::new(1.0, Color32::from_rgba_premultiplied(190, 241, 250, 24)),
);
}
fn panel_heading(ui: &mut egui::Ui, tag: &str, title: &str) {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label(RichText::new(tag).monospace().size(9.0).color(kd_cyan()));
ui.label(
RichText::new(title)
.size(18.0)
.strong()
.color(Color32::from_rgb(244, 253, 255)),
);
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let (rect, _) = ui.allocate_exact_size(Vec2::new(66.0, 22.0), Sense::hover());
let painter = ui.painter_at(rect);
painter.rect_stroke(
rect,
Rounding::same(6.0),
Stroke::new(1.0, Color32::from_rgb(35, 83, 95)),
);
painter.text(
rect.center(),
Align2::CENTER_CENTER,
"MK.CY",
FontId::monospace(9.0),
Color32::from_rgb(131, 186, 196),
);
});
});
ui.add_space(2.0);
let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 1.0), Sense::hover());
ui.painter().line_segment(
[rect.left_center(), rect.right_center()],
Stroke::new(1.0, Color32::from_rgb(24, 55, 64)),
);
}
fn kd_button(label: &str, primary: bool) -> egui::Button<'_> {
let text = RichText::new(label)
.monospace()
.size(if primary { 11.0 } else { 9.5 })
.strong()
.color(if primary {
Color32::from_rgb(3, 16, 21)
} else {
Color32::from_rgb(205, 232, 237)
});
egui::Button::new(text)
.fill(if primary {
kd_cyan()
} else {
Color32::from_rgb(8, 20, 27)
})
.stroke(Stroke::new(
1.0,
if primary {
Color32::from_rgb(142, 243, 255)
} else {
Color32::from_rgb(33, 82, 94)
},
))
.rounding(Rounding::same(7.0))
.min_size(Vec2::new(
if primary { 142.0 } else { 84.0 },
if primary { 34.0 } else { 28.0 },
))
}
fn draw_status_pill(painter: &egui::Painter, pos: Pos2, label: &str) {
let rect = Rect::from_min_size(pos, Vec2::new(190.0, 30.0));
painter.rect_filled(rect, Rounding::same(7.0), kd_surface());
painter.rect_stroke(
rect,
Rounding::same(7.0),
Stroke::new(1.0, Color32::from_rgb(35, 88, 101)),
);
painter.circle_filled(rect.left_center() + egui::vec2(13.0, 0.0), 3.5, kd_cyan());
painter.text(
rect.left_center() + egui::vec2(25.0, 0.0),
Align2::LEFT_CENTER,
label,
FontId::monospace(9.5),
Color32::from_rgb(196, 232, 238),
);
}
fn tech_note(ui: &mut egui::Ui, tag: &str, text: &str) {
egui::Frame::none()
.fill(Color32::from_rgb(3, 11, 16))
.rounding(Rounding::same(8.0))
.stroke(Stroke::new(1.0, Color32::from_rgb(22, 58, 68)))
.inner_margin(egui::Margin::same(8.0))
.show(ui, |ui| {
ui.label(RichText::new(tag).monospace().size(9.0).color(kd_cyan()));
ui.label(
RichText::new(text)
.monospace()
.size(9.5)
.color(Color32::from_rgb(167, 199, 206)),
);
});
}
fn route_selector(ui: &mut egui::Ui, mode: &mut ConversionMode, disabled: bool, phase: f32) {
field_label(ui, "ROUTE");
egui::Frame::none()
.fill(Color32::from_rgb(3, 8, 12))
.rounding(Rounding::same(9.0))
.stroke(Stroke::new(1.0, Color32::from_rgb(22, 58, 68)))
.inner_margin(egui::Margin::same(3.0))
.show(ui, |ui| {
ui.columns(3, |columns| {
for (index, candidate) in ConversionMode::ALL.iter().copied().enumerate() {
let response = route_segment(
&mut columns[index],
candidate,
*mode == candidate,
disabled,
phase,
);
if response.clicked() && !disabled {
*mode = candidate;
}
}
});
});
}
fn route_segment(
ui: &mut egui::Ui,
mode: ConversionMode,
selected: bool,
disabled: bool,
phase: f32,
) -> egui::Response {
let (rect, response) = ui.allocate_exact_size(
Vec2::new(ui.available_width(), 38.0),
if disabled {
Sense::hover()
} else {
Sense::click()
},
);
let pulse = ((phase * 2.0).sin() * 0.5 + 0.5) * 14.0;
ui.painter().rect_filled(
rect,
Rounding::same(7.0),
if selected {
kd_cyan()
} else {
Color32::TRANSPARENT
},
);
ui.painter().rect_stroke(
rect,
Rounding::same(7.0),
Stroke::new(
1.0,
if selected {
Color32::from_rgba_premultiplied(152, 243, 255, (180.0 + pulse) as u8)
} else {
Color32::TRANSPARENT
},
),
);
ui.painter().text(
rect.center(),
Align2::CENTER_CENTER,
mode.title().to_ascii_uppercase(),
FontId::monospace(8.5),
if selected {
Color32::from_rgb(3, 16, 21)
} else {
Color32::from_rgb(190, 220, 226)
},
);
response
}
fn field_label(ui: &mut egui::Ui, label: &str) {
ui.label(
RichText::new(label)
.monospace()
.size(9.5)
.color(Color32::from_rgb(139, 184, 193)),
);
}
fn format_controls(
ui: &mut egui::Ui,
mode: ConversionMode,
audio_format: &mut AudioFormat,
video_format: &mut VideoFormat,
quality: &mut QualityPreset,
disabled: bool,
) -> bool {
let mut changed = false;
match mode {
ConversionMode::ExtractAudio | ConversionMode::TranscodeAudio => {
field_label(ui, "AUDIO OUTPUT FORMAT");
ui.horizontal_wrapped(|ui| {
for candidate in AudioFormat::ALL {
ui.add_enabled_ui(!disabled, |ui| {
if ui
.selectable_value(audio_format, candidate, candidate.label())
.changed()
{
changed = true;
}
});
}
});
}
ConversionMode::TranscodeVideo => {
field_label(ui, "VIDEO OUTPUT FORMAT");
ui.horizontal_wrapped(|ui| {
for candidate in VideoFormat::ALL {
ui.add_enabled_ui(!disabled, |ui| {
if ui
.selectable_value(video_format, candidate, candidate.label())
.changed()
{
changed = true;
}
});
}
});
}
}
ui.add_space(8.0);
field_label(ui, "QUALITY PROFILE");
ui.horizontal_wrapped(|ui| {
for candidate in QualityPreset::ALL {
ui.add_enabled_ui(!disabled, |ui| {
if ui
.selectable_value(
quality,
candidate,
format!("{} // {}", candidate.label(), candidate.description()),
)
.changed()
{
changed = true;
}
});
}
});
changed
}
fn media_preview_card(ui: &mut egui::Ui, media_path: &str, mode: ConversionMode, phase: f32) {
let pulse = ((phase * 3.4).sin() * 0.5 + 0.5) * 44.0;
let (title, detail, badge, badge_color, stroke_color, icon) =
media_preview_state(media_path, mode, pulse);
let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 78.0), Sense::hover());
ui.painter()
.rect_filled(rect, Rounding::same(8.0), kd_surface());
ui.painter()
.rect_stroke(rect, Rounding::same(8.0), Stroke::new(1.0, stroke_color));
let icon_rect = Rect::from_min_size(
rect.left_top() + egui::vec2(12.0, 17.0),
Vec2::new(44.0, 44.0),
);
ui.painter().rect_stroke(
icon_rect,
Rounding::same(6.0),
Stroke::new(1.0, Color32::from_rgb(43, 102, 115)),
);
ui.painter().line_segment(
[
icon_rect.left_top() + egui::vec2(8.0, 11.0),
icon_rect.right_top() + egui::vec2(-8.0, 11.0),
],
Stroke::new(1.0, Color32::from_rgb(23, 62, 72)),
);
ui.painter().text(
icon_rect.center(),
Align2::CENTER_CENTER,
icon,
FontId::monospace(10.0),
Color32::from_rgb(210, 239, 244),
);
ui.painter().text(
rect.left_top() + egui::vec2(68.0, 14.0),
Align2::LEFT_TOP,
title.to_ascii_uppercase(),
FontId::proportional(15.0),
Color32::from_rgb(238, 250, 252),
);
ui.painter().text(
rect.left_top() + egui::vec2(68.0, 40.0),
Align2::LEFT_TOP,
detail.to_ascii_uppercase(),
FontId::monospace(9.0),
Color32::from_rgb(128, 169, 178),
);
ui.painter().text(
rect.right_top() + egui::vec2(-12.0, 13.0),
Align2::RIGHT_TOP,
badge,
FontId::monospace(8.5),
badge_color,
);
}
fn media_preview_state(
media_path: &str,
mode: ConversionMode,
pulse: f32,
) -> (String, String, &'static str, Color32, Color32, &'static str) {
if media_path.is_empty() {
return (
"Choose a media source".to_owned(),
"Open Media Vault, paste a path, or drag an audio/video file here.".to_owned(),
"WAITING",
Color32::from_gray(140),
Color32::from_gray(42),
"---",
);
}
let path = Path::new(media_path);
if !supported_media_extension(path) {
return (
file_name_or_path(path),
"Unsupported extension for the media source list.".to_owned(),
"CHECK EXT",
Color32::from_gray(210),
Color32::from_gray(112),
"FILE",
);
}
if mode.expects_video_input() && !supported_video_extension(path) {
return (
file_name_or_path(path),
"The selected route expects a video source.".to_owned(),
"WRONG ROUTE",
Color32::from_gray(210),
Color32::from_gray(112),
media_kind_label(path),
);
}
if matches!(mode, ConversionMode::TranscodeAudio) && !supported_audio_extension(path) {
return (
file_name_or_path(path),
"The selected route expects an audio source.".to_owned(),
"WRONG ROUTE",
Color32::from_gray(210),
Color32::from_gray(112),
media_kind_label(path),
);
}
match fs::metadata(path) {
Ok(metadata) if metadata.is_file() => {
let name = file_name_or_path(path);
let folder = path
.parent()
.map(format_path_display)
.unwrap_or_else(|| "CURRENT FOLDER".to_owned());
(
name,
format!("{} // {}", human_size(metadata.len()), folder),
"SOURCE READY",
kd_cyan(),
Color32::from_rgba_premultiplied(76, 229, 255, (96.0 + pulse) as u8),
media_kind_label(path),
)
}
Ok(_) => (
file_name_or_path(path),
"That path exists, but it is not a file.".to_owned(),
"NOT FILE",
Color32::from_gray(210),
Color32::from_gray(112),
media_kind_label(path),
),
Err(_) => (
file_name_or_path(path),
"File not found yet. Pick an existing media file.".to_owned(),
"MISSING",
Color32::from_gray(185),
Color32::from_gray(82),
media_kind_label(path),
),
}
}
fn output_preview_card(
ui: &mut egui::Ui,
output_path: &str,
mode: ConversionMode,
audio_format: AudioFormat,
video_format: VideoFormat,
overwrite: bool,
phase: f32,
) {
let pulse = ((phase * 3.0).sin() * 0.5 + 0.5) * 44.0;
let expected = mode.output_extension(audio_format, video_format);
let (title, detail, badge, badge_color, stroke_color) =
output_preview_state(output_path, expected, overwrite, pulse);
let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 72.0), Sense::hover());
ui.painter()
.rect_filled(rect, Rounding::same(8.0), kd_surface());
ui.painter()
.rect_stroke(rect, Rounding::same(8.0), Stroke::new(1.0, stroke_color));
let icon_rect = Rect::from_min_size(
rect.left_top() + egui::vec2(12.0, 14.0),
Vec2::new(42.0, 42.0),
);
ui.painter().rect_stroke(
icon_rect,
Rounding::same(6.0),
Stroke::new(1.0, Color32::from_rgb(43, 102, 115)),
);
ui.painter().text(
icon_rect.center(),
Align2::CENTER_CENTER,
"OUT",
FontId::monospace(9.5),
Color32::from_rgb(210, 239, 244),
);
ui.painter().text(
rect.left_top() + egui::vec2(66.0, 12.0),
Align2::LEFT_TOP,
title.to_ascii_uppercase(),
FontId::proportional(15.0),
Color32::from_rgb(238, 250, 252),
);
ui.painter().text(
rect.left_top() + egui::vec2(66.0, 37.0),
Align2::LEFT_TOP,
detail.to_ascii_uppercase(),
FontId::monospace(9.0),
Color32::from_rgb(128, 169, 178),
);
ui.painter().text(
rect.right_top() + egui::vec2(-12.0, 12.0),
Align2::RIGHT_TOP,
badge,
FontId::monospace(8.5),
badge_color,
);
}
fn output_preview_state(
output_path: &str,
expected_extension: &str,
overwrite: bool,
pulse: f32,
) -> (String, String, &'static str, Color32, Color32) {
if output_path.is_empty() {
return (
"Choose an output path".to_owned(),
format!("Expected .{expected_extension} for the current route."),
"WAITING",
Color32::from_gray(140),
Color32::from_gray(42),
);
}
let path = Path::new(output_path);
if path_extension_lower(path).as_deref() != Some(expected_extension) {
return (
file_name_or_path(path),
format!("Output extension should be .{expected_extension}."),
"CHECK EXT",
Color32::from_gray(210),
Color32::from_gray(112),
);
}
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() && !parent.is_dir() {
return (
file_name_or_path(path),
format!("Folder does not exist: {}", format_path_display(parent)),
"BAD PATH",
Color32::from_gray(210),
Color32::from_gray(112),
);
}
}
if path.exists() {
return (
file_name_or_path(path),
"Output already exists.".to_owned(),
if overwrite { "OVERWRITE" } else { "EXISTS" },
if overwrite {
kd_cyan()
} else {
Color32::from_gray(210)
},
if overwrite {
Color32::from_rgba_premultiplied(76, 229, 255, (90.0 + pulse) as u8)
} else {
Color32::from_gray(112)
},
);
}
(
file_name_or_path(path),
path.parent()
.map(format_path_display)
.unwrap_or_else(|| "CURRENT FOLDER".to_owned()),
"READY",
kd_cyan(),
Color32::from_rgba_premultiplied(76, 229, 255, (88.0 + pulse) as u8),
)
}
fn readiness_panel(
ui: &mut egui::Ui,
source_ready: bool,
output_ready: bool,
mode: ConversionMode,
) {
field_label(ui, "PRE-FLIGHT");
egui::Frame::none()
.fill(Color32::from_rgb(3, 9, 14))
.rounding(Rounding::same(8.0))
.stroke(Stroke::new(1.0, Color32::from_rgb(22, 58, 68)))
.inner_margin(egui::Margin::same(4.0))
.show(ui, |ui| {
ui.columns(4, |columns| {
readiness_chip(&mut columns[0], source_ready, mode.source_label());
readiness_chip(&mut columns[1], output_ready, "OUTPUT READY");
readiness_chip(&mut columns[2], true, "ROUTE SET");
readiness_chip(&mut columns[3], false, "REVIEW NEXT");
});
});
}
fn readiness_chip(ui: &mut egui::Ui, ready: bool, label: &str) {
let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 30.0), Sense::hover());
ui.painter().rect_filled(
rect,
Rounding::same(6.0),
if ready {
Color32::from_rgb(8, 25, 31)
} else {
Color32::from_rgb(6, 12, 17)
},
);
ui.painter().circle_filled(
rect.left_center() + egui::vec2(8.0, 0.0),
3.0,
if ready {
kd_cyan()
} else {
Color32::from_rgb(54, 75, 81)
},
);
ui.painter().text(
rect.left_center() + egui::vec2(15.0, 0.0),
Align2::LEFT_CENTER,
label,
FontId::monospace(7.0),
if ready {
Color32::from_rgb(193, 231, 237)
} else {
Color32::from_rgb(91, 121, 128)
},
);
}
fn output_write_box(ui: &mut egui::Ui, output_path: &str, overwrite: &mut bool, disabled: bool) {
egui::Frame::none()
.fill(Color32::from_rgb(4, 11, 16))
.rounding(Rounding::same(8.0))
.stroke(Stroke::new(1.0, Color32::from_rgb(25, 60, 69)))
.inner_margin(egui::Margin::same(8.0))
.show(ui, |ui| {
field_label(ui, "OUTPUT WRITE POLICY");
let exists = Path::new(output_path).exists();
let message = if exists {
"Allow Caery to replace the existing output file."
} else {
"Output path is new. Enable only if you expect to replace existing files."
};
ui.add_enabled(
!disabled,
egui::Checkbox::new(
overwrite,
RichText::new(message.to_ascii_uppercase())
.monospace()
.size(9.0)
.color(Color32::from_rgb(200, 226, 232)),
),
);
});
}
fn validation_notice(detail: String) -> AppNotice {
AppNotice {
level: NoticeLevel::Warning,
title: "Pre-flight check incomplete".to_owned(),
detail,
action: Some(
"Correct the highlighted source, output, or route setting and review again.".to_owned(),
),
}
}
fn classify_operation_error(error: &str) -> AppNotice {
let lower = error.to_ascii_lowercase();
let (title, action) = if lower.contains("failed to start ffmpeg") {
(
"FFmpeg could not start",
"Install ffmpeg or confirm it is available on PATH, then retry.",
)
} else if lower.contains("permission denied") {
(
"Output write was denied",
"Choose a writable output folder or correct its permissions, then retry.",
)
} else if lower.contains("exited with status") {
(
"Conversion process failed",
"Inspect the technical output for codec or media details, then adjust the route.",
)
} else if lower.contains("failed to wait for ffmpeg") {
(
"Conversion process was interrupted",
"Confirm the system is healthy and retry the conversion.",
)
} else {
(
"Conversion failed",
"Inspect the technical output, correct the reported problem, then retry.",
)
};
AppNotice {
level: NoticeLevel::Error,
title: title.to_owned(),
detail: error.to_owned(),
action: Some(action.to_owned()),
}
}
fn notice_color(level: NoticeLevel) -> Color32 {
match level {
NoticeLevel::Info => kd_cyan(),
NoticeLevel::Warning => Color32::from_rgb(239, 196, 106),
NoticeLevel::Error => Color32::from_rgb(255, 117, 128),
}
}
fn operation_notice_card(ui: &mut egui::Ui, notice: &AppNotice) {
let color = notice_color(notice.level);
egui::Frame::none()
.fill(match notice.level {
NoticeLevel::Info => Color32::from_rgb(5, 18, 24),
NoticeLevel::Warning => Color32::from_rgb(24, 19, 9),
NoticeLevel::Error => Color32::from_rgb(29, 10, 14),
})
.rounding(Rounding::same(8.0))
.stroke(Stroke::new(
1.0,
Color32::from_rgba_premultiplied(color.r(), color.g(), color.b(), 92),
))
.inner_margin(egui::Margin::same(9.0))
.show(ui, |ui| {
ui.label(
RichText::new(notice.title.to_ascii_uppercase())
.monospace()
.size(10.0)
.strong()
.color(color),
);
ui.label(
RichText::new(¬ice.detail)
.size(10.0)
.color(Color32::from_rgb(207, 229, 234)),
);
if let Some(action) = ¬ice.action {
ui.label(
RichText::new(action)
.monospace()
.size(9.0)
.color(Color32::from_rgb(132, 171, 180)),
);
}
});
}
fn operation_action_dock(
ui: &mut egui::Ui,
review_enabled: bool,
running: bool,
status: &str,
preflight_error: Option<&str>,
details_open: &mut bool,
log_count: usize,
) -> bool {
let mut review_clicked = false;
egui::Frame::none()
.fill(Color32::from_rgb(9, 21, 29))
.rounding(Rounding::same(9.0))
.stroke(Stroke::new(1.0, Color32::from_rgb(30, 81, 94)))
.inner_margin(egui::Margin::same(6.0))
.show(ui, |ui| {
ui.horizontal(|ui| {
let (pill, _) = ui.allocate_exact_size(Vec2::new(124.0, 34.0), Sense::hover());
ui.painter()
.rect_filled(pill, Rounding::same(7.0), Color32::from_rgb(2, 8, 12));
ui.painter().circle_filled(
pill.left_center() + egui::vec2(11.0, 0.0),
3.5,
if preflight_error.is_some() && !running {
Color32::from_rgb(239, 196, 106)
} else {
kd_cyan()
},
);
ui.painter().text(
pill.left_center() + egui::vec2(21.0, 0.0),
Align2::LEFT_CENTER,
status,
FontId::monospace(8.0),
Color32::from_rgb(149, 224, 236),
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
review_clicked = ui
.add_enabled(
review_enabled,
kd_button(
if running {
"RUNNING"
} else {
"REVIEW + CONVERT"
},
true,
),
)
.clicked();
if ui
.add(kd_button(&format!("DETAILS {log_count}"), false))
.clicked()
{
*details_open = true;
}
});
});
if let Some(error) = preflight_error {
ui.label(
RichText::new(error)
.monospace()
.size(8.5)
.color(Color32::from_rgb(205, 169, 92)),
);
}
});
review_clicked
}
fn final_warning_card(ui: &mut egui::Ui, phase: f32) {
let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 58.0), Sense::hover());
let pulse = ((phase * 3.0).sin() * 0.5 + 0.5) * 30.0;
ui.painter()
.rect_filled(rect, Rounding::same(8.0), Color32::from_rgb(24, 19, 9));
ui.painter().rect_stroke(
rect,
Rounding::same(8.0),
Stroke::new(
1.0,
Color32::from_rgba_premultiplied(239, 196, 106, (95.0 + pulse) as u8),
),
);
ui.painter().text(
rect.center(),
Align2::CENTER_CENTER,
"LAST GATE BEFORE MEDIA FILE WRITE",
FontId::monospace(13.0),
Color32::from_rgb(245, 221, 166),
);
}
fn animated_progress(ui: &mut egui::Ui, current: f64, total: f64, phase: f32) {
let percent = if total <= 0.0 { 0.0 } else { current / total }.clamp(0.0, 1.0) as f32;
let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 30.0), Sense::hover());
draw_progress_bar(
ui.painter(),
rect,
percent,
phase,
&duration_progress_label(current, total),
);
}
fn indeterminate_progress(ui: &mut egui::Ui, phase: f32) {
let value = ((phase * 1.8).sin() * 0.5 + 0.5).clamp(0.04, 0.96);
let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 30.0), Sense::hover());
draw_progress_bar(ui.painter(), rect, value, phase, "WORKING // FFMPEG ACTIVE");
}
fn draw_progress_bar(painter: &egui::Painter, rect: Rect, value: f32, phase: f32, label: &str) {
painter.rect_filled(rect, Rounding::same(7.0), kd_surface());
painter.rect_stroke(
rect,
Rounding::same(7.0),
Stroke::new(1.0, Color32::from_rgb(28, 73, 85)),
);
let fill_rect = Rect::from_min_max(
rect.left_top(),
Pos2::new(rect.left() + rect.width() * value, rect.bottom()),
);
painter.rect_filled(fill_rect, Rounding::same(7.0), kd_cyan());
let shimmer_x = rect.left() + rect.width() * ((phase * 0.4).fract());
painter.line_segment(
[
Pos2::new(shimmer_x, rect.top()),
Pos2::new(shimmer_x + 34.0, rect.bottom()),
],
Stroke::new(1.0, Color32::from_rgba_premultiplied(235, 252, 255, 90)),
);
painter.text(
rect.center(),
Align2::CENTER_CENTER,
label.to_ascii_uppercase(),
FontId::monospace(9.0),
if value > 0.48 {
Color32::from_rgb(3, 16, 21)
} else {
Color32::from_rgb(190, 220, 226)
},
);
}
fn log_panel(ui: &mut egui::Ui, logs: &[String], compact: bool) {
field_label(ui, "LIVE TELEMETRY");
egui::Frame::none()
.fill(Color32::from_rgb(2, 8, 12))
.rounding(Rounding::same(8.0))
.stroke(Stroke::new(1.0, Color32::from_rgb(23, 57, 67)))
.inner_margin(egui::Margin::same(10.0))
.show(ui, |ui| {
egui::ScrollArea::vertical()
.max_height(if compact { 105.0 } else { 150.0 })
.stick_to_bottom(true)
.show(ui, |ui| {
if logs.is_empty() {
ui.label(
RichText::new("TELEMETRY STREAM IDLE")
.monospace()
.size(11.0)
.color(Color32::from_rgb(82, 115, 122)),
);
}
for line in logs {
ui.label(
RichText::new(format!("> {line}"))
.monospace()
.size(11.0)
.color(Color32::from_rgb(166, 203, 210)),
);
}
});
});
}
fn warning_text(ui: &mut egui::Ui, text: &str) {
ui.label(
RichText::new(text)
.monospace()
.size(11.0)
.color(Color32::from_rgb(239, 196, 106)),
);
}
fn media_browser_contents(
ui: &mut egui::Ui,
state: &MediaBrowserState,
phase: f32,
) -> BrowserAction {
let mut action = BrowserAction::None;
panel_heading(ui, "MEDIA VAULT", "SELECT SOURCE MEDIA");
ui.add_space(10.0);
let path_text = format_path_display(&state.current_dir);
tech_note(ui, "CURRENT DIRECTORY", &path_text);
ui.add_space(8.0);
ui.horizontal(|ui| {
if ui
.add_enabled(
state.current_dir.parent().is_some(),
kd_button("PARENT", false),
)
.clicked()
{
action = BrowserAction::Parent;
}
if ui.add(kd_button("REFRESH", false)).clicked() {
action = BrowserAction::Refresh;
}
if ui.add(kd_button("CLOSE", false)).clicked() {
action = BrowserAction::Close;
}
});
ui.add_space(10.0);
if let Some(error) = &state.error {
warning_text(ui, &format!("DIRECTORY READ FAILED // {error}"));
return action;
}
egui::Frame::none()
.fill(Color32::from_rgb(2, 8, 12))
.rounding(Rounding::same(8.0))
.stroke(Stroke::new(1.0, Color32::from_rgb(24, 62, 73)))
.inner_margin(egui::Margin::same(10.0))
.show(ui, |ui| {
egui::ScrollArea::vertical()
.max_height(350.0)
.auto_shrink([false, false])
.show(ui, |ui| {
if state.entries.is_empty() {
ui.label(
RichText::new("NO FILES IN DIRECTORY")
.monospace()
.size(11.0)
.color(Color32::from_gray(96)),
);
}
for entry in &state.entries {
let response = browser_entry_row(ui, entry, phase);
if response.clicked() {
match entry.kind {
BrowserEntryKind::Directory => {
action = BrowserAction::OpenDirectory(entry.path.clone());
}
BrowserEntryKind::Media => {
action = BrowserAction::SelectMedia(entry.path.clone());
}
BrowserEntryKind::Other => {}
}
}
ui.add_space(6.0);
}
});
});
action
}
fn browser_entry_row(ui: &mut egui::Ui, entry: &BrowserEntry, phase: f32) -> egui::Response {
let clickable = entry.kind != BrowserEntryKind::Other;
let (rect, response) = ui.allocate_exact_size(
Vec2::new(ui.available_width(), 42.0),
if clickable {
Sense::click()
} else {
Sense::hover()
},
);
let fill = if response.hovered() && clickable {
Color32::from_rgb(14, 34, 43)
} else {
kd_surface()
};
ui.painter().rect_filled(rect, Rounding::same(7.0), fill);
ui.painter().rect_stroke(
rect,
Rounding::same(7.0),
Stroke::new(1.0, Color32::from_rgb(22, 57, 67)),
);
let kind = match entry.kind {
BrowserEntryKind::Directory => "DIR",
BrowserEntryKind::Media => media_kind_label(&entry.path),
BrowserEntryKind::Other => "---",
};
let color = match entry.kind {
BrowserEntryKind::Directory => Color32::from_gray(215),
BrowserEntryKind::Media => kd_cyan(),
BrowserEntryKind::Other => Color32::from_gray(74),
};
let pulse = ((phase * 4.0).sin() * 0.5 + 0.5) * 35.0;
ui.painter().text(
rect.left_center() + egui::vec2(12.0, 0.0),
Align2::LEFT_CENTER,
kind,
FontId::monospace(10.0),
if entry.kind == BrowserEntryKind::Media {
Color32::from_rgba_premultiplied(76, 229, 255, (160.0 + pulse) as u8)
} else {
color
},
);
ui.painter().text(
rect.left_center() + egui::vec2(72.0, 0.0),
Align2::LEFT_CENTER,
entry.name.to_ascii_uppercase(),
FontId::monospace(11.0),
color,
);
response
}
fn infer_route_for_source(path: &Path, current: ConversionMode) -> ConversionMode {
if supported_audio_extension(path) {
ConversionMode::TranscodeAudio
} else if supported_video_extension(path) {
match current {
ConversionMode::ExtractAudio | ConversionMode::TranscodeVideo => current,
ConversionMode::TranscodeAudio => ConversionMode::ExtractAudio,
}
} else {
current
}
}
fn source_file_ready(media_path: &str, mode: ConversionMode) -> bool {
let media_path = media_path.trim();
if media_path.is_empty() {
return false;
}
let path = Path::new(media_path);
let route_matches = if mode.expects_video_input() {
supported_video_extension(path)
} else {
supported_audio_extension(path)
};
route_matches
&& fs::metadata(path)
.map(|metadata| metadata.is_file())
.unwrap_or(false)
}
fn output_path_ready(output_path: &str, expected_extension: &str, overwrite: bool) -> bool {
let output_path = output_path.trim();
if output_path.is_empty() {
return false;
}
let path = Path::new(output_path);
if path_extension_lower(path).as_deref() != Some(expected_extension) {
return false;
}
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() && !parent.is_dir() {
return false;
}
}
!path.exists() || overwrite
}
fn file_name_or_path(path: &Path) -> String {
path.file_name()
.and_then(|name| name.to_str())
.map(str::to_owned)
.unwrap_or_else(|| path.display().to_string())
}
fn format_path_display(path: &Path) -> String {
let display = path.display().to_string();
if let Some(home) = env::var_os("HOME").map(PathBuf::from) {
if let Ok(stripped) = path.strip_prefix(&home) {
if stripped.as_os_str().is_empty() {
return "~".to_owned();
}
return format!("~/{}", stripped.display());
}
}
display
}
fn initial_browser_directory(current_media_path: &str) -> PathBuf {
let current_media_path = current_media_path.trim();
if !current_media_path.is_empty() {
let path = Path::new(current_media_path);
if path.is_dir() {
return path.to_path_buf();
}
if let Some(parent) = path.parent().filter(|parent| parent.is_dir()) {
return parent.to_path_buf();
}
}
if let Some(home) = env::var_os("HOME")
.map(PathBuf::from)
.filter(|path| path.is_dir())
{
return home;
}
env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))
}
fn scan_browser_directory(path: &Path) -> std::io::Result<Vec<BrowserEntry>> {
let mut entries = Vec::new();
for entry in fs::read_dir(path)? {
let entry = match entry {
Ok(entry) => entry,
Err(_) => continue,
};
let entry_path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
let kind = match entry.file_type() {
Ok(file_type) if file_type.is_dir() => BrowserEntryKind::Directory,
Ok(file_type) if file_type.is_file() && supported_media_extension(&entry_path) => {
BrowserEntryKind::Media
}
Ok(file_type) if file_type.is_file() => BrowserEntryKind::Other,
_ => continue,
};
entries.push(BrowserEntry {
name,
path: entry_path,
kind,
});
}
entries.sort_by(|left, right| {
left.kind.cmp(&right.kind).then_with(|| {
left.name
.to_ascii_lowercase()
.cmp(&right.name.to_ascii_lowercase())
})
});
Ok(entries)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn route_inference_tracks_source_kind_and_preserves_video_mode() {
assert_eq!(
infer_route_for_source(Path::new("song.flac"), ConversionMode::ExtractAudio),
ConversionMode::TranscodeAudio
);
assert_eq!(
infer_route_for_source(Path::new("clip.mkv"), ConversionMode::TranscodeVideo),
ConversionMode::TranscodeVideo
);
assert_eq!(
infer_route_for_source(Path::new("clip.mp4"), ConversionMode::ExtractAudio),
ConversionMode::ExtractAudio
);
assert_eq!(
infer_route_for_source(Path::new("clip.mp4"), ConversionMode::TranscodeAudio),
ConversionMode::ExtractAudio
);
}
#[test]
fn operation_errors_are_classified_with_recovery_context() {
let missing_ffmpeg = classify_operation_error(
"failed to start ffmpeg: No such file or directory (os error 2)",
);
assert_eq!(missing_ffmpeg.level, NoticeLevel::Error);
assert_eq!(missing_ffmpeg.title, "FFmpeg could not start");
assert!(missing_ffmpeg
.action
.as_deref()
.is_some_and(|action| action.contains("PATH")));
let codec_failure = classify_operation_error("ffmpeg exited with status exit status: 1");
assert_eq!(codec_failure.title, "Conversion process failed");
assert_eq!(
codec_failure.detail,
"ffmpeg exited with status exit status: 1"
);
let unknown = classify_operation_error("unexpected worker channel failure");
assert_eq!(unknown.title, "Conversion failed");
}
#[test]
fn source_ready_respects_route_media_type() {
let dir = tempdir().expect("temp dir");
let video = dir.path().join("clip.mp4");
let audio = dir.path().join("song.wav");
fs::write(&video, b"fake video").expect("write video");
fs::write(&audio, b"fake audio").expect("write audio");
assert!(source_file_ready(
&video.display().to_string(),
ConversionMode::ExtractAudio
));
assert!(!source_file_ready(
&audio.display().to_string(),
ConversionMode::ExtractAudio
));
assert!(source_file_ready(
&audio.display().to_string(),
ConversionMode::TranscodeAudio
));
}
#[test]
fn output_ready_requires_matching_extension() {
let dir = tempdir().expect("temp dir");
let mp3 = dir.path().join("out.mp3");
let wav = dir.path().join("out.wav");
assert!(output_path_ready(&mp3.display().to_string(), "mp3", false));
assert!(!output_path_ready(&wav.display().to_string(), "mp3", false));
}
#[test]
fn scan_browser_directory_prioritizes_folders_then_media() {
let dir = tempdir().expect("temp dir");
fs::create_dir(dir.path().join("z_folder")).expect("create folder");
fs::write(dir.path().join("clip.mp4"), b"fake video").expect("write video");
fs::write(dir.path().join("notes.txt"), b"notes").expect("write notes");
let entries = scan_browser_directory(dir.path()).expect("scan dir");
let kinds = entries.iter().map(|entry| entry.kind).collect::<Vec<_>>();
assert_eq!(
kinds,
vec![
BrowserEntryKind::Directory,
BrowserEntryKind::Media,
BrowserEntryKind::Other
]
);
}
}