yes
This commit is contained in:
935
Cargo.lock
generated
935
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -11,3 +11,6 @@ robot36-encoder = { version = "0.2", features = ["image"] }
|
||||
image = "0.24"
|
||||
hound = "3.5"
|
||||
env_logger = "0.10"
|
||||
rodio = "0.21.1"
|
||||
imageproc = "0.25.0"
|
||||
rusttype = "0.9.3"
|
||||
|
||||
16
font8x8_basic.bin
Normal file
16
font8x8_basic.bin
Normal file
@@ -0,0 +1,16 @@
|
||||
00000000 00 00 00 00 00 00 00 00 18 18 18 18 18 00 18 00
|
||||
00000010 6c 6c 6c 00 00 00 00 00 6c 6c fe 6c fe 6c 6c 00
|
||||
00000020 18 3e 60 3c 06 7c 18 00 00 c6 cc 18 30 66 c6 00
|
||||
00000030 38 6c 38 76 dc cc 76 00 18 18 30 00 00 00 00 00
|
||||
00000040 0c 18 30 30 30 18 0c 00 30 18 0c 0c 0c 18 30 00
|
||||
00000050 00 66 3c ff 3c 66 00 00 00 18 18 7e 18 18 00 00
|
||||
00000060 00 00 00 00 18 18 30 00 00 00 00 7e 00 00 00 00
|
||||
00000070 00 00 00 00 00 18 18 00 06 0c 18 30 60 c0 80 00
|
||||
00000080 38 6c c6 c6 c6 6c 38 00 18 38 18 18 18 18 7e 00
|
||||
00000090 7c c6 06 1c 30 66 fe 00 7c c6 06 3c 06 c6 7c 00
|
||||
000000a0 1c 3c 6c cc fe 0c 1e 00 fe c0 fc 06 06 c6 7c 00
|
||||
000000b0 38 60 c0 fc c6 c6 7c 00 fe c6 0c 18 30 30 30 00
|
||||
000000c0 7c c6 c6 7c c6 c6 7c 00 7c c6 c6 7e 06 0c 78 00
|
||||
000000d0 00 18 18 00 00 18 18 00 00 18 18 00 00 18 18 30
|
||||
000000e0 0c 18 30 60 30 18 0c 00 00 00 7e 00 7e 00 00 00
|
||||
000000f0 30 18 0c 06 0c 18 30 00 7c c6 0c 18 18 00 18 00
|
||||
612
src/main.rs
612
src/main.rs
@@ -1,10 +1,49 @@
|
||||
use eframe::egui;
|
||||
use robot36_encoder::{Encoder, Robot36Image};
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Instant;
|
||||
use std::thread;
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
use hound::{WavSpec, WavWriter, SampleFormat};
|
||||
use image::{Rgba, RgbaImage};
|
||||
|
||||
// Simple 8x8 bitmap font implementation
|
||||
mod builtin_font {
|
||||
// Each character is 8 bytes (8x8 pixels)
|
||||
const FONT_DATA: &[u8] = include_bytes!("../font8x8_basic.bin");
|
||||
|
||||
pub fn draw_text(
|
||||
image: &mut RgbaImage,
|
||||
x: u32,
|
||||
y: u32,
|
||||
text: &str,
|
||||
color: Rgba<u8>,
|
||||
) {
|
||||
let char_width = 8;
|
||||
let char_height = 8;
|
||||
|
||||
for (i, c) in text.chars().enumerate() {
|
||||
let char_index = c as usize;
|
||||
if char_index >= 128 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let font_data = &FONT_DATA[char_index * char_width..(char_index + 1) * char_width];
|
||||
|
||||
for row in 0..char_height {
|
||||
for col in 0..char_width {
|
||||
if font_data[row as usize] & (1 << (7 - col)) != 0 {
|
||||
let px = x + (i as u32 * char_width) + col;
|
||||
let py = y + row;
|
||||
if px < image.width() && py < image.height() {
|
||||
image.put_pixel(px, py, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum EncodingState {
|
||||
@@ -23,24 +62,17 @@ enum ProgressMessage {
|
||||
}
|
||||
|
||||
struct Robot36EncoderApp {
|
||||
// File paths
|
||||
input_file: Option<PathBuf>,
|
||||
output_file: Option<PathBuf>,
|
||||
|
||||
// Encoding state
|
||||
encoding_state: EncodingState,
|
||||
progress: f32,
|
||||
|
||||
// Audio settings
|
||||
sample_rate: u32,
|
||||
|
||||
// Threading
|
||||
callsign: String,
|
||||
encoding_thread: Option<thread::JoinHandle<()>>,
|
||||
progress_receiver: Option<Receiver<ProgressMessage>>,
|
||||
|
||||
// UI state
|
||||
show_success_dialog: bool,
|
||||
error_message: Option<String>,
|
||||
settings_open: bool,
|
||||
}
|
||||
|
||||
impl Default for Robot36EncoderApp {
|
||||
@@ -51,10 +83,12 @@ impl Default for Robot36EncoderApp {
|
||||
encoding_state: EncodingState::Idle,
|
||||
progress: 0.0,
|
||||
sample_rate: 48000,
|
||||
callsign: String::new(),
|
||||
encoding_thread: None,
|
||||
progress_receiver: None,
|
||||
show_success_dialog: false,
|
||||
error_message: None,
|
||||
settings_open: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,13 +105,9 @@ impl Robot36EncoderApp {
|
||||
.pick_file()
|
||||
{
|
||||
self.input_file = Some(path.clone());
|
||||
|
||||
// Auto-generate output filename
|
||||
let mut output_path = path.clone();
|
||||
output_path.set_extension("wav");
|
||||
self.output_file = Some(output_path);
|
||||
|
||||
// Reset state
|
||||
self.encoding_state = EncodingState::Idle;
|
||||
self.progress = 0.0;
|
||||
self.error_message = None;
|
||||
@@ -88,7 +118,12 @@ impl Robot36EncoderApp {
|
||||
if let Some(path) = rfd::FileDialog::new()
|
||||
.add_filter("Audio Files", &["wav"])
|
||||
.set_title("Save Audio Output")
|
||||
.set_file_name("output.wav")
|
||||
.set_file_name(
|
||||
self.input_file.as_ref()
|
||||
.and_then(|p| p.file_stem())
|
||||
.map(|s| format!("{}.wav", s.to_string_lossy()))
|
||||
.unwrap_or_else(|| "output.wav".to_string())
|
||||
)
|
||||
.save_file()
|
||||
{
|
||||
self.output_file = Some(path);
|
||||
@@ -107,9 +142,10 @@ impl Robot36EncoderApp {
|
||||
let input_path = input_path.clone();
|
||||
let output_path = output_path.clone();
|
||||
let sample_rate = self.sample_rate;
|
||||
let callsign = self.callsign.clone();
|
||||
|
||||
self.encoding_thread = Some(thread::spawn(move || {
|
||||
Self::encode_image_thread(input_path, output_path, sample_rate, tx);
|
||||
Self::encode_image_thread(input_path, output_path, sample_rate, callsign, tx);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -118,9 +154,10 @@ impl Robot36EncoderApp {
|
||||
input_path: PathBuf,
|
||||
output_path: PathBuf,
|
||||
sample_rate: u32,
|
||||
callsign: String,
|
||||
tx: Sender<ProgressMessage>,
|
||||
) {
|
||||
let result = Self::encode_image_impl(input_path, output_path.clone(), sample_rate, &tx);
|
||||
let result = Self::encode_image_impl(input_path, output_path.clone(), sample_rate, callsign, &tx);
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
@@ -136,31 +173,70 @@ impl Robot36EncoderApp {
|
||||
input_path: PathBuf,
|
||||
output_path: PathBuf,
|
||||
sample_rate: u32,
|
||||
callsign: String,
|
||||
tx: &Sender<ProgressMessage>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Load and process image
|
||||
let _ = tx.send(ProgressMessage::Progress(0.1));
|
||||
|
||||
let image = image::open(&input_path)?;
|
||||
// Load image and convert to RGBA
|
||||
let mut image = image::open(&input_path)?.to_rgba8();
|
||||
let _ = tx.send(ProgressMessage::Progress(0.2));
|
||||
|
||||
// Resize image to Robot36 specifications (320x240)
|
||||
let resized_image = image.resize(320, 240, image::imageops::FilterType::Lanczos3);
|
||||
// Add callsign overlay if provided
|
||||
if !callsign.is_empty() {
|
||||
// Draw black background for text
|
||||
for y in 0..16 {
|
||||
for x in 0..(callsign.len() as u32 * 8).min(image.width()) {
|
||||
if x < image.width() && y < image.height() {
|
||||
image.put_pixel(x, y, Rgba([0u8, 0u8, 0u8, 200u8]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw text
|
||||
builtin_font::draw_text(
|
||||
&mut image,
|
||||
5, 5, // x, y position
|
||||
&callsign,
|
||||
Rgba([255u8, 255u8, 255u8, 255u8]), // white text
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate target size while maintaining aspect ratio
|
||||
let target_width = 320;
|
||||
let target_height = 240;
|
||||
|
||||
let (width, height) = image.dimensions();
|
||||
let ratio = width as f32 / height as f32;
|
||||
let (new_width, new_height) = if ratio > (target_width as f32 / target_height as f32) {
|
||||
(target_width, (target_width as f32 / ratio) as u32)
|
||||
} else {
|
||||
((target_height as f32 * ratio) as u32, target_height)
|
||||
};
|
||||
|
||||
// Resize with padding to maintain 320x240
|
||||
let mut resized_image = image::DynamicImage::new_rgb8(target_width, target_height);
|
||||
let temp_image = image::DynamicImage::ImageRgba8(image)
|
||||
.resize_exact(new_width, new_height, image::imageops::FilterType::Lanczos3);
|
||||
|
||||
// Center the image
|
||||
let x_offset = (target_width - new_width) / 2;
|
||||
let y_offset = (target_height - new_height) / 2;
|
||||
|
||||
image::imageops::overlay(&mut resized_image, &temp_image, x_offset as i64, y_offset as i64);
|
||||
|
||||
let _ = tx.send(ProgressMessage::Progress(0.3));
|
||||
|
||||
// Convert to Robot36Image
|
||||
// Convert to Robot36Image (original encoding logic untouched)
|
||||
let robot36_image = Robot36Image::from_image(resized_image)?;
|
||||
let _ = tx.send(ProgressMessage::Progress(0.4));
|
||||
|
||||
// Create encoder
|
||||
let encoder = Encoder::new(robot36_image, sample_rate as u64);
|
||||
let _ = tx.send(ProgressMessage::Progress(0.5));
|
||||
|
||||
// Encode to audio samples
|
||||
let samples: Vec<i16> = encoder.encode().collect();
|
||||
let _ = tx.send(ProgressMessage::Progress(0.8));
|
||||
|
||||
// Write to WAV file
|
||||
let spec = WavSpec {
|
||||
channels: 1,
|
||||
sample_rate,
|
||||
@@ -174,7 +250,6 @@ impl Robot36EncoderApp {
|
||||
for (i, sample) in samples.into_iter().enumerate() {
|
||||
writer.write_sample(sample)?;
|
||||
|
||||
// Update progress periodically
|
||||
if i % 10000 == 0 {
|
||||
let progress = 0.8 + (i as f32 / total_samples as f32) * 0.2;
|
||||
let _ = tx.send(ProgressMessage::Progress(progress));
|
||||
@@ -192,8 +267,6 @@ impl Robot36EncoderApp {
|
||||
self.progress = 0.0;
|
||||
|
||||
if let Some(handle) = self.encoding_thread.take() {
|
||||
// Note: In a production app, you'd want to implement proper cancellation
|
||||
// For now, we'll just detach the thread
|
||||
handle.join().ok();
|
||||
}
|
||||
|
||||
@@ -202,6 +275,7 @@ impl Robot36EncoderApp {
|
||||
|
||||
fn update_progress(&mut self) {
|
||||
let mut should_clear_receiver = false;
|
||||
let mut completed_path = None;
|
||||
|
||||
if let Some(receiver) = &self.progress_receiver {
|
||||
while let Ok(message) = receiver.try_recv() {
|
||||
@@ -215,22 +289,17 @@ impl Robot36EncoderApp {
|
||||
}
|
||||
}
|
||||
ProgressMessage::Completed(output_path) => {
|
||||
self.encoding_state = EncodingState::Completed { output_path };
|
||||
completed_path = Some(output_path);
|
||||
self.encoding_state = EncodingState::Completed { output_path: completed_path.clone().unwrap() };
|
||||
self.progress = 1.0;
|
||||
self.show_success_dialog = true;
|
||||
should_clear_receiver = true;
|
||||
if let Some(handle) = self.encoding_thread.take() {
|
||||
handle.join().ok();
|
||||
}
|
||||
}
|
||||
ProgressMessage::Error(error) => {
|
||||
self.encoding_state = EncodingState::Error(error.clone());
|
||||
self.error_message = Some(error);
|
||||
self.progress = 0.0;
|
||||
should_clear_receiver = true;
|
||||
if let Some(handle) = self.encoding_thread.take() {
|
||||
handle.join().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,13 +307,15 @@ impl Robot36EncoderApp {
|
||||
|
||||
if should_clear_receiver {
|
||||
self.progress_receiver = None;
|
||||
if let Some(handle) = self.encoding_thread.take() {
|
||||
handle.join().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_remaining_time(&self) -> f32 {
|
||||
match &self.encoding_state {
|
||||
EncodingState::Encoding { start_time } => {
|
||||
// Robot36 encoding typically takes about 45 seconds
|
||||
let _elapsed = start_time.elapsed().as_secs_f32();
|
||||
let estimated_total = 45.0;
|
||||
let progress_based_remaining = (1.0 - self.progress) * estimated_total;
|
||||
@@ -263,231 +334,311 @@ impl Robot36EncoderApp {
|
||||
self.output_file.is_some() &&
|
||||
!self.is_encoding()
|
||||
}
|
||||
|
||||
fn open_with_vlc(&self, path: &PathBuf) {
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let _ = std::process::Command::new("cmd")
|
||||
.args(["/C", "start", "vlc", &path_str])
|
||||
.spawn();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = std::process::Command::new("open")
|
||||
.args(["-a", "VLC", &path_str])
|
||||
.spawn();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = std::process::Command::new("vlc")
|
||||
.arg(&path_str)
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Robot36EncoderApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.update_progress();
|
||||
|
||||
// Success dialog
|
||||
ctx.set_visuals(egui::Visuals {
|
||||
dark_mode: true,
|
||||
override_text_color: Some(egui::Color32::WHITE),
|
||||
window_fill: egui::Color32::from_rgb(32, 32, 32),
|
||||
panel_fill: egui::Color32::from_rgb(40, 40, 40),
|
||||
..egui::Visuals::dark()
|
||||
});
|
||||
|
||||
if self.show_success_dialog {
|
||||
egui::Window::new("Encoding Complete")
|
||||
let output_path = if let EncodingState::Completed { output_path } = &self.encoding_state {
|
||||
output_path.clone()
|
||||
} else {
|
||||
PathBuf::new()
|
||||
};
|
||||
|
||||
egui::Window::new("🎉 Encoding Complete!")
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.label("Image successfully encoded to Robot36 audio!");
|
||||
ui.add_space(10.0);
|
||||
|
||||
if let EncodingState::Completed { output_path } = &self.encoding_state {
|
||||
ui.label(format!("Output saved to: {}", output_path.display()));
|
||||
}
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("✅ Success!");
|
||||
ui.add_space(5.0);
|
||||
ui.label("Your image has been encoded to Robot36 audio format");
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.add_space(10.0);
|
||||
|
||||
ui.label(format!("📁 Saved to: {}", output_path.display()));
|
||||
|
||||
ui.add_space(15.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("OK").clicked() {
|
||||
self.show_success_dialog = false;
|
||||
}
|
||||
|
||||
if let EncodingState::Completed { output_path } = &self.encoding_state {
|
||||
if ui.button("Open Folder").clicked() {
|
||||
if let Some(parent) = output_path.parent() {
|
||||
let _ = std::process::Command::new("xdg-open")
|
||||
.arg(parent)
|
||||
.spawn();
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.button("Close").clicked() {
|
||||
self.show_success_dialog = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("🎵 Open with VLC").clicked() {
|
||||
self.open_with_vlc(&output_path);
|
||||
self.show_success_dialog = false;
|
||||
}
|
||||
|
||||
if ui.button("📂 Open Folder").clicked() {
|
||||
if let Some(parent) = output_path.parent() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let _ = std::process::Command::new("explorer")
|
||||
.arg(parent)
|
||||
.spawn();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = std::process::Command::new("open")
|
||||
.arg(parent)
|
||||
.spawn();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = std::process::Command::new("xdg-open")
|
||||
.arg(parent)
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
self.show_success_dialog = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("Robot36 Image Encoder");
|
||||
ui.label("Convert images to SSTV Robot36 audio format");
|
||||
ui.separator();
|
||||
|
||||
// File selection section
|
||||
ui.group(|ui| {
|
||||
ui.label("File Selection");
|
||||
ui.separator();
|
||||
|
||||
// Input file
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Select Input Image").clicked() {
|
||||
self.select_input_file();
|
||||
}
|
||||
|
||||
if let Some(file_path) = &self.input_file {
|
||||
ui.label(format!("Input: {}",
|
||||
file_path.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()));
|
||||
} else {
|
||||
ui.colored_label(egui::Color32::GRAY, "No input file selected");
|
||||
}
|
||||
});
|
||||
|
||||
// Output file
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Select Output Audio").clicked() {
|
||||
self.select_output_file();
|
||||
}
|
||||
|
||||
if let Some(file_path) = &self.output_file {
|
||||
ui.label(format!("Output: {}",
|
||||
file_path.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()));
|
||||
} else {
|
||||
ui.colored_label(egui::Color32::GRAY, "No output file selected");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Audio settings
|
||||
ui.group(|ui| {
|
||||
ui.label("Audio Settings");
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Sample Rate:");
|
||||
ui.add(egui::Slider::new(&mut self.sample_rate, 8000..=96000)
|
||||
.suffix(" Hz")
|
||||
.logarithmic(false));
|
||||
});
|
||||
|
||||
ui.label("Robot36 will encode to 320x240 resolution");
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Progress section
|
||||
ui.group(|ui| {
|
||||
ui.label("Encoding Progress");
|
||||
ui.separator();
|
||||
|
||||
// Progress bar
|
||||
let progress_text = match &self.encoding_state {
|
||||
EncodingState::Idle => "Ready".to_string(),
|
||||
EncodingState::Loading => "Loading image...".to_string(),
|
||||
EncodingState::Encoding { .. } => format!("Encoding... {:.1}%", self.progress * 100.0),
|
||||
EncodingState::Completed { .. } => "Completed".to_string(),
|
||||
EncodingState::Error(_) => "Error".to_string(),
|
||||
};
|
||||
|
||||
let progress_bar = egui::ProgressBar::new(self.progress)
|
||||
.text(progress_text)
|
||||
.show_percentage();
|
||||
ui.add(progress_bar);
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Time remaining display
|
||||
if matches!(self.encoding_state, EncodingState::Encoding { .. }) {
|
||||
let remaining_time = self.get_remaining_time();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Estimated time remaining:");
|
||||
ui.monospace(format!("{:.2} seconds", remaining_time));
|
||||
});
|
||||
}
|
||||
|
||||
// Large LCD display
|
||||
ui.separator();
|
||||
ui.vertical_centered(|ui| {
|
||||
let remaining_time = self.get_remaining_time();
|
||||
let lcd_text = if self.is_encoding() {
|
||||
format!("{:05.2}", remaining_time)
|
||||
} else {
|
||||
"45.00".to_string()
|
||||
};
|
||||
|
||||
let lcd_display = egui::Label::new(
|
||||
egui::RichText::new(lcd_text)
|
||||
.size(48.0)
|
||||
.monospace()
|
||||
.color(if self.is_encoding() {
|
||||
egui::Color32::from_rgb(0, 255, 0)
|
||||
} else {
|
||||
egui::Color32::from_rgb(100, 100, 100)
|
||||
})
|
||||
);
|
||||
|
||||
// Add background
|
||||
let response = ui.add(lcd_display);
|
||||
let rect = response.rect;
|
||||
ui.painter().rect_filled(
|
||||
rect.expand(10.0),
|
||||
egui::Rounding::same(5.0),
|
||||
egui::Color32::from_rgb(20, 20, 20),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Control buttons
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("📡 Callsign:");
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut self.callsign)
|
||||
.hint_text("Enter your callsign (optional)")
|
||||
.desired_width(150.0)
|
||||
);
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let stop_button = ui.add_enabled(
|
||||
self.is_encoding(),
|
||||
egui::Button::new("Stop")
|
||||
);
|
||||
|
||||
if stop_button.clicked() {
|
||||
self.stop_encoding();
|
||||
}
|
||||
|
||||
let start_button = ui.add_enabled(
|
||||
self.can_start_encoding(),
|
||||
egui::Button::new("Start Encoding")
|
||||
);
|
||||
|
||||
if start_button.clicked() {
|
||||
self.start_encoding();
|
||||
if ui.button("⚙️ Settings").clicked() {
|
||||
self.settings_open = !self.settings_open;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.add_space(20.0);
|
||||
|
||||
// Status messages
|
||||
match &self.encoding_state {
|
||||
EncodingState::Idle => {
|
||||
if self.can_start_encoding() {
|
||||
ui.colored_label(egui::Color32::BLUE, "Ready to encode");
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("🤖 Robot36 SSTV Encoder");
|
||||
ui.label("Convert images to SSTV audio format");
|
||||
});
|
||||
|
||||
ui.add_space(30.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::Frame::none()
|
||||
.fill(egui::Color32::from_rgb(48, 48, 48))
|
||||
.rounding(egui::Rounding::same(12.0))
|
||||
.inner_margin(egui::Margin::same(20.0))
|
||||
.show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("📁 Select Files");
|
||||
ui.add_space(10.0);
|
||||
|
||||
if ui.add_sized([300.0, 40.0], egui::Button::new("📷 Choose Image")).clicked() {
|
||||
self.select_input_file();
|
||||
}
|
||||
|
||||
if let Some(file_path) = &self.input_file {
|
||||
ui.add_space(5.0);
|
||||
ui.label(format!("📄 {}", file_path.file_name().unwrap_or_default().to_string_lossy()));
|
||||
}
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
if ui.add_sized([300.0, 40.0], egui::Button::new("💾 Save Audio As")).clicked() {
|
||||
self.select_output_file();
|
||||
}
|
||||
|
||||
if let Some(file_path) = &self.output_file {
|
||||
ui.add_space(5.0);
|
||||
ui.label(format!("🔊 {}", file_path.file_name().unwrap_or_default().to_string_lossy()));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(30.0);
|
||||
|
||||
if self.is_encoding() || matches!(self.encoding_state, EncodingState::Completed { .. }) {
|
||||
egui::Frame::none()
|
||||
.fill(egui::Color32::from_rgb(48, 48, 48))
|
||||
.rounding(egui::Rounding::same(12.0))
|
||||
.inner_margin(egui::Margin::same(20.0))
|
||||
.show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("⚡ Encoding Progress");
|
||||
ui.add_space(15.0);
|
||||
|
||||
let progress_bar = egui::ProgressBar::new(self.progress)
|
||||
.text(format!("{:.1}%", self.progress * 100.0))
|
||||
.desired_width(300.0)
|
||||
.desired_height(20.0);
|
||||
ui.add(progress_bar);
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
let status_text = match &self.encoding_state {
|
||||
EncodingState::Loading => "🔄 Loading image...",
|
||||
EncodingState::Encoding { .. } => "🎵 Encoding to audio...",
|
||||
EncodingState::Completed { .. } => "✅ Encoding complete!",
|
||||
_ => "",
|
||||
};
|
||||
ui.label(status_text);
|
||||
|
||||
if self.is_encoding() {
|
||||
let remaining = self.get_remaining_time();
|
||||
ui.label(format!("⏱️ {:.1}s remaining", remaining));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(20.0);
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if self.is_encoding() {
|
||||
if ui.add_sized([150.0, 50.0], egui::Button::new("⏹️ Stop")).clicked() {
|
||||
self.stop_encoding();
|
||||
}
|
||||
} else {
|
||||
ui.colored_label(egui::Color32::GRAY, "Select input and output files to begin");
|
||||
let start_button = egui::Button::new("🚀 Start Encoding")
|
||||
.fill(egui::Color32::from_rgb(0, 120, 215));
|
||||
|
||||
if ui.add_sized([200.0, 50.0], start_button).clicked() && self.can_start_encoding() {
|
||||
self.start_encoding();
|
||||
}
|
||||
}
|
||||
}
|
||||
EncodingState::Loading => {
|
||||
ui.colored_label(egui::Color32::YELLOW, "Loading and processing image...");
|
||||
}
|
||||
EncodingState::Encoding { .. } => {
|
||||
ui.colored_label(egui::Color32::YELLOW, "Encoding to Robot36 audio format...");
|
||||
}
|
||||
EncodingState::Completed { output_path } => {
|
||||
ui.colored_label(egui::Color32::GREEN,
|
||||
format!("Successfully encoded to {}", output_path.display()));
|
||||
}
|
||||
EncodingState::Error(msg) => {
|
||||
ui.colored_label(egui::Color32::RED, format!("Error: {}", msg));
|
||||
}
|
||||
}
|
||||
|
||||
// Error details
|
||||
if let Some(error) = &self.error_message {
|
||||
ui.add_space(5.0);
|
||||
ui.group(|ui| {
|
||||
ui.colored_label(egui::Color32::RED, "Error Details:");
|
||||
ui.monospace(error);
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(20.0);
|
||||
|
||||
match &self.encoding_state {
|
||||
EncodingState::Idle => {
|
||||
if !self.can_start_encoding() {
|
||||
ui.colored_label(egui::Color32::GRAY, "ℹ️ Please select input and output files");
|
||||
}
|
||||
}
|
||||
EncodingState::Completed { output_path } => {
|
||||
ui.colored_label(egui::Color32::GREEN,
|
||||
format!("✅ Audio saved to: {}", output_path.file_name().unwrap_or_default().to_string_lossy()));
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("🎵 Open with VLC").clicked() {
|
||||
self.open_with_vlc(output_path);
|
||||
}
|
||||
|
||||
if ui.button("📂 Open Folder").clicked() {
|
||||
if let Some(parent) = output_path.parent() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let _ = std::process::Command::new("explorer")
|
||||
.arg(parent)
|
||||
.spawn();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = std::process::Command::new("open")
|
||||
.arg(parent)
|
||||
.spawn();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = std::process::Command::new("xdg-open")
|
||||
.arg(parent)
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
EncodingState::Error(msg) => {
|
||||
ui.colored_label(egui::Color32::RED, format!("❌ Error: {}", msg));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Request repaint for smooth updates
|
||||
if self.settings_open {
|
||||
egui::Window::new("⚙️ Settings")
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.anchor(egui::Align2::RIGHT_TOP, [-10.0, 60.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.label("🎵 Audio Settings");
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Sample Rate:");
|
||||
ui.add(
|
||||
egui::Slider::new(&mut self.sample_rate, 8000..=96000)
|
||||
.suffix(" Hz")
|
||||
.logarithmic(true)
|
||||
.clamp_to_range(true)
|
||||
);
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
ui.label("ℹ️ Higher sample rates produce better quality audio but larger files.");
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
if ui.button("Close").clicked() {
|
||||
self.settings_open = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if self.is_encoding() {
|
||||
ctx.request_repaint_after(Duration::from_millis(50));
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -497,13 +648,14 @@ fn main() -> eframe::Result<()> {
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([600.0, 500.0])
|
||||
.with_min_inner_size([500.0, 400.0]),
|
||||
.with_inner_size([700.0, 600.0])
|
||||
.with_min_inner_size([600.0, 500.0])
|
||||
.with_title("Robot36 SSTV Encoder"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"Robot36 Image Encoder",
|
||||
"Robot36 SSTV Encoder",
|
||||
options,
|
||||
Box::new(|cc| Ok(Box::new(Robot36EncoderApp::new(cc)))),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user