ready
This commit is contained in:
328
src/app.rs
328
src/app.rs
@@ -48,6 +48,10 @@ pub enum WorkerMessage {
|
||||
progress: f32,
|
||||
detail: Option<String>,
|
||||
},
|
||||
LogMessage {
|
||||
job_id: usize,
|
||||
message: String,
|
||||
},
|
||||
JobComplete {
|
||||
job_id: usize,
|
||||
output_path: PathBuf,
|
||||
@@ -58,6 +62,13 @@ pub enum WorkerMessage {
|
||||
},
|
||||
}
|
||||
|
||||
/// A log entry with timestamp
|
||||
#[derive(Clone)]
|
||||
pub struct LogEntry {
|
||||
pub timestamp: std::time::Instant,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Application settings for AVIF encoding
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EncodingSettings {
|
||||
@@ -101,16 +112,36 @@ pub struct AvifMakerApp {
|
||||
drop_zone: DropZone,
|
||||
settings_ui: Settings,
|
||||
queue_ui: Queue,
|
||||
/// Console log entries
|
||||
pub console_log: Vec<LogEntry>,
|
||||
/// Whether console is visible
|
||||
pub show_console: bool,
|
||||
/// Start time for relative timestamps
|
||||
start_time: std::time::Instant,
|
||||
}
|
||||
|
||||
impl AvifMakerApp {
|
||||
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
// Configure larger fonts for accessibility
|
||||
let mut style = (*cc.egui_ctx.style()).clone();
|
||||
|
||||
// Increase all font sizes
|
||||
for (_text_style, font_id) in style.text_styles.iter_mut() {
|
||||
font_id.size *= 1.3; // 30% larger
|
||||
}
|
||||
|
||||
// Increase spacing for touch/visibility
|
||||
style.spacing.item_spacing = egui::vec2(10.0, 8.0);
|
||||
style.spacing.button_padding = egui::vec2(8.0, 4.0);
|
||||
|
||||
cc.egui_ctx.set_style(style);
|
||||
|
||||
let (worker_tx, worker_rx) = mpsc::channel();
|
||||
|
||||
// Try to load saved settings
|
||||
let settings = Self::load_settings().unwrap_or_default();
|
||||
|
||||
Self {
|
||||
let mut app = Self {
|
||||
settings,
|
||||
jobs: Vec::new(),
|
||||
next_job_id: 0,
|
||||
@@ -120,9 +151,41 @@ impl AvifMakerApp {
|
||||
drop_zone: DropZone::new(),
|
||||
settings_ui: Settings::new(),
|
||||
queue_ui: Queue::new(),
|
||||
console_log: Vec::new(),
|
||||
show_console: false,
|
||||
start_time: std::time::Instant::now(),
|
||||
};
|
||||
|
||||
app.log("AVIF Maker started");
|
||||
app
|
||||
}
|
||||
|
||||
/// Add a message to the console log
|
||||
pub fn log(&mut self, message: impl Into<String>) {
|
||||
self.console_log.push(LogEntry {
|
||||
timestamp: std::time::Instant::now(),
|
||||
message: message.into(),
|
||||
});
|
||||
// Keep log reasonable size
|
||||
if self.console_log.len() > 1000 {
|
||||
self.console_log.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the console log as plain text for copy/save
|
||||
fn format_console_log(&self) -> String {
|
||||
let mut output = String::new();
|
||||
output.push_str("AVIF Maker Console Log\n");
|
||||
output.push_str("======================\n\n");
|
||||
|
||||
for entry in &self.console_log {
|
||||
let elapsed = entry.timestamp.duration_since(self.start_time);
|
||||
output.push_str(&format!("[{:8.2}s] {}\n", elapsed.as_secs_f64(), entry.message));
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn load_settings() -> Option<EncodingSettings> {
|
||||
let dirs = directories::ProjectDirs::from("", "", "avif-maker")?;
|
||||
let config_path = dirs.config_dir().join("settings.json");
|
||||
@@ -231,14 +294,41 @@ impl AvifMakerApp {
|
||||
settings: EncodingSettings,
|
||||
tx: Sender<WorkerMessage>,
|
||||
) {
|
||||
let filename = input_path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
// Log start
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!("Starting conversion: {}", filename),
|
||||
});
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!(" Input: {}", input_path.display()),
|
||||
});
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!(" Output: {}", output_path.display()),
|
||||
});
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!(" Settings: quality={}, alpha={}, speed={}",
|
||||
settings.quality, settings.alpha_quality, settings.speed),
|
||||
});
|
||||
|
||||
// Create decode progress callback
|
||||
let tx_decode = tx.clone();
|
||||
let tx_decode_log = tx.clone();
|
||||
let last_logged_frame = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let last_logged_clone = last_logged_frame.clone();
|
||||
|
||||
let decode_progress: ProgressCallback = Arc::new(move |current, total| {
|
||||
let (progress, detail) = if let Some(total) = total {
|
||||
let pct = current as f32 / total as f32;
|
||||
(pct * 0.5, format!("frame {}/{}", current, total)) // Decoding is 0-50%
|
||||
(pct * 0.5, format!("frame {}/{}", current, total))
|
||||
} else {
|
||||
// Unknown total - just show frame count
|
||||
(0.25, format!("frame {}", current))
|
||||
};
|
||||
let _ = tx_decode.send(WorkerMessage::ProgressUpdate {
|
||||
@@ -247,6 +337,18 @@ impl AvifMakerApp {
|
||||
progress,
|
||||
detail: Some(detail),
|
||||
});
|
||||
|
||||
// Log every 10 frames or when total is known and we hit milestones
|
||||
let last = last_logged_clone.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if current >= last + 10 || current == 1 {
|
||||
last_logged_clone.store(current, std::sync::atomic::Ordering::Relaxed);
|
||||
let msg = if let Some(t) = total {
|
||||
format!(" Decoded frame {}/{}", current, t)
|
||||
} else {
|
||||
format!(" Decoded frame {}", current)
|
||||
};
|
||||
let _ = tx_decode_log.send(WorkerMessage::LogMessage { job_id, message: msg });
|
||||
}
|
||||
});
|
||||
|
||||
// Update status: decoding started
|
||||
@@ -256,12 +358,21 @@ impl AvifMakerApp {
|
||||
progress: 0.0,
|
||||
detail: Some("starting...".to_string()),
|
||||
});
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: "Decoding started...".to_string(),
|
||||
});
|
||||
|
||||
// Decode input with progress
|
||||
let decode_start = std::time::Instant::now();
|
||||
let decoder = Decoder::for_path(&input_path);
|
||||
let frames = match decoder.decode_with_progress(&input_path, Some(decode_progress)) {
|
||||
Ok(frames) => frames,
|
||||
Err(e) => {
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!("ERROR: Decode failed: {}", e),
|
||||
});
|
||||
let _ = tx.send(WorkerMessage::JobFailed {
|
||||
job_id,
|
||||
error: format!("Decode error: {}", e),
|
||||
@@ -269,13 +380,30 @@ impl AvifMakerApp {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let decode_time = decode_start.elapsed();
|
||||
|
||||
let total_frames = frames.frames.len();
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!("Decode complete: {} frames in {:.2}s", total_frames, decode_time.as_secs_f64()),
|
||||
});
|
||||
|
||||
if frames.has_alpha {
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: " Source has alpha channel".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Create encode progress callback
|
||||
let tx_encode = tx.clone();
|
||||
let tx_encode_log = tx.clone();
|
||||
let last_logged_encode = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let last_logged_encode_clone = last_logged_encode.clone();
|
||||
let total_for_log = total_frames;
|
||||
|
||||
let encode_progress: EncodeProgressCallback = Arc::new(move |current, total| {
|
||||
let pct = 0.5 + (current as f32 / total as f32) * 0.5; // Encoding is 50-100%
|
||||
let pct = 0.5 + (current as f32 / total as f32) * 0.5;
|
||||
let detail = format!("frame {}/{}", current, total);
|
||||
let _ = tx_encode.send(WorkerMessage::ProgressUpdate {
|
||||
job_id,
|
||||
@@ -283,6 +411,16 @@ impl AvifMakerApp {
|
||||
progress: pct,
|
||||
detail: Some(detail),
|
||||
});
|
||||
|
||||
// Log every 10 frames
|
||||
let last = last_logged_encode_clone.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if current >= last + 10 || current == 1 || current == total_for_log {
|
||||
last_logged_encode_clone.store(current, std::sync::atomic::Ordering::Relaxed);
|
||||
let _ = tx_encode_log.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!(" Encoded frame {}/{}", current, total),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update status: encoding started
|
||||
@@ -292,17 +430,55 @@ impl AvifMakerApp {
|
||||
progress: 0.5,
|
||||
detail: Some(format!("0/{} frames", total_frames)),
|
||||
});
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!("Encoding started ({} frames)...", total_frames),
|
||||
});
|
||||
|
||||
// Encode to AVIF with progress
|
||||
let encode_start = std::time::Instant::now();
|
||||
let encoder = AvifEncoder::new(&settings);
|
||||
match encoder.encode_with_progress(&frames, &output_path, Some(encode_progress)) {
|
||||
Ok(()) => {
|
||||
let encode_time = encode_start.elapsed();
|
||||
let total_time = decode_time + encode_time;
|
||||
|
||||
// Get output file size
|
||||
let file_size = std::fs::metadata(&output_path)
|
||||
.map(|m| m.len())
|
||||
.unwrap_or(0);
|
||||
let size_str = if file_size > 1024 * 1024 {
|
||||
format!("{:.2} MB", file_size as f64 / (1024.0 * 1024.0))
|
||||
} else {
|
||||
format!("{:.2} KB", file_size as f64 / 1024.0)
|
||||
};
|
||||
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!("Encode complete in {:.2}s", encode_time.as_secs_f64()),
|
||||
});
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!("Output size: {}", size_str),
|
||||
});
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!("Total time: {:.2}s", total_time.as_secs_f64()),
|
||||
});
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!("SUCCESS: {}", filename),
|
||||
});
|
||||
let _ = tx.send(WorkerMessage::JobComplete {
|
||||
job_id,
|
||||
output_path,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx.send(WorkerMessage::LogMessage {
|
||||
job_id,
|
||||
message: format!("ERROR: Encode failed: {}", e),
|
||||
});
|
||||
let _ = tx.send(WorkerMessage::JobFailed {
|
||||
job_id,
|
||||
error: format!("Encode error: {}", e),
|
||||
@@ -326,6 +502,9 @@ impl AvifMakerApp {
|
||||
job.progress_detail = detail;
|
||||
}
|
||||
}
|
||||
WorkerMessage::LogMessage { job_id: _, message } => {
|
||||
self.log(message);
|
||||
}
|
||||
WorkerMessage::JobComplete { job_id, output_path } => {
|
||||
if let Some(job) = self.jobs.iter_mut().find(|j| j.id == job_id) {
|
||||
job.status = JobStatus::Complete;
|
||||
@@ -340,7 +519,7 @@ impl AvifMakerApp {
|
||||
WorkerMessage::JobFailed { job_id, error } => {
|
||||
if let Some(job) = self.jobs.iter_mut().find(|j| j.id == job_id) {
|
||||
job.status = JobStatus::Failed;
|
||||
job.error = Some(error);
|
||||
job.error = Some(error.clone());
|
||||
job.progress_detail = None;
|
||||
}
|
||||
self.is_converting = false;
|
||||
@@ -376,26 +555,108 @@ impl eframe::App for AvifMakerApp {
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("AVIF Maker");
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
// Console toggle button
|
||||
let console_label = if self.show_console { "Hide Console" } else { "Show Console" };
|
||||
if ui.button(console_label).clicked() {
|
||||
self.show_console = !self.show_console;
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(format!("{} files in queue", self.jobs.len()));
|
||||
});
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
});
|
||||
|
||||
// Left panel with settings
|
||||
// Left panel with settings (rendered before bottom panel so it claims full height)
|
||||
egui::SidePanel::left("settings_panel")
|
||||
.resizable(true)
|
||||
.default_width(280.0)
|
||||
.show(ctx, |ui| {
|
||||
self.settings_ui.show(ui, &mut self.settings);
|
||||
ui.add_space(16.0);
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
self.settings_ui.show(ui, &mut self.settings);
|
||||
ui.add_space(16.0);
|
||||
|
||||
// Save settings when changed
|
||||
if ui.button("Save Settings").clicked() {
|
||||
self.save_settings();
|
||||
}
|
||||
// Save settings when changed
|
||||
if ui.button("Save Settings").clicked() {
|
||||
self.save_settings();
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
});
|
||||
});
|
||||
|
||||
// Console panel (bottom) - rendered after left panel so it doesn't overlap
|
||||
if self.show_console {
|
||||
egui::TopBottomPanel::bottom("console_panel")
|
||||
.resizable(true)
|
||||
.default_height(200.0)
|
||||
.min_height(100.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("Console");
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.button("Clear").clicked() {
|
||||
self.console_log.clear();
|
||||
}
|
||||
|
||||
if ui.button("Save...").clicked() {
|
||||
if let Some(path) = rfd::FileDialog::new()
|
||||
.set_file_name("avif-maker-log.txt")
|
||||
.add_filter("Text files", &["txt", "log"])
|
||||
.save_file()
|
||||
{
|
||||
let log_text = self.format_console_log();
|
||||
if let Err(e) = std::fs::write(&path, &log_text) {
|
||||
self.log(format!("ERROR: Failed to save log: {}", e));
|
||||
} else {
|
||||
self.log(format!("Log saved to: {}", path.display()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("Copy").clicked() {
|
||||
let log_text = self.format_console_log();
|
||||
ui.ctx().copy_text(log_text);
|
||||
self.log("Console log copied to clipboard");
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.label(format!("{} entries", self.console_log.len()));
|
||||
});
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.stick_to_bottom(true)
|
||||
.show(ui, |ui| {
|
||||
ui.style_mut().override_font_id = Some(egui::FontId::monospace(14.0));
|
||||
|
||||
for entry in &self.console_log {
|
||||
let elapsed = entry.timestamp.duration_since(self.start_time);
|
||||
let timestamp = format!("[{:6.2}s]", elapsed.as_secs_f64());
|
||||
|
||||
// Color based on message content
|
||||
let color = if entry.message.contains("ERROR") {
|
||||
egui::Color32::from_rgb(255, 100, 100)
|
||||
} else if entry.message.contains("SUCCESS") {
|
||||
egui::Color32::from_rgb(100, 255, 100)
|
||||
} else if entry.message.starts_with(" ") {
|
||||
egui::Color32::from_gray(160)
|
||||
} else {
|
||||
egui::Color32::from_gray(220)
|
||||
};
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(×tamp).color(egui::Color32::from_gray(120)));
|
||||
ui.label(egui::RichText::new(&entry.message).color(color));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Collect files from drop zone (to avoid borrow issues)
|
||||
let mut dropped_paths: Vec<PathBuf> = Vec::new();
|
||||
|
||||
@@ -413,27 +674,40 @@ impl eframe::App for AvifMakerApp {
|
||||
// Action buttons
|
||||
ui.horizontal(|ui| {
|
||||
let has_queued = self.jobs.iter().any(|j| j.status == JobStatus::Queued);
|
||||
let has_completed = self.jobs.iter().any(|j|
|
||||
j.status == JobStatus::Complete || j.status == JobStatus::Failed
|
||||
);
|
||||
|
||||
// Clear Completed on the left
|
||||
if ui
|
||||
.add_enabled(
|
||||
has_queued && !self.is_converting,
|
||||
egui::Button::new("Convert All"),
|
||||
)
|
||||
.add_enabled(has_completed, egui::Button::new("Clear Completed"))
|
||||
.clicked()
|
||||
{
|
||||
self.start_conversion();
|
||||
}
|
||||
|
||||
if ui
|
||||
.add_enabled(self.is_converting, egui::Button::new("Cancel"))
|
||||
.clicked()
|
||||
{
|
||||
// Cancel is a no-op for now - could implement with cancellation tokens
|
||||
self.clear_completed();
|
||||
}
|
||||
|
||||
// Convert All and Cancel on the right
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.button("Clear Completed").clicked() {
|
||||
self.clear_completed();
|
||||
if ui
|
||||
.add_enabled(self.is_converting, egui::Button::new("Cancel"))
|
||||
.clicked()
|
||||
{
|
||||
// Cancel is a no-op for now - could implement with cancellation tokens
|
||||
}
|
||||
|
||||
// Make Convert All prominent (green) when there are files to process
|
||||
let convert_button = if has_queued && !self.is_converting {
|
||||
egui::Button::new(egui::RichText::new("Convert All").strong())
|
||||
.fill(egui::Color32::from_rgb(34, 139, 34)) // Forest green
|
||||
} else {
|
||||
egui::Button::new("Convert All")
|
||||
};
|
||||
|
||||
if ui
|
||||
.add_enabled(has_queued && !self.is_converting, convert_button)
|
||||
.clicked()
|
||||
{
|
||||
self.start_conversion();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ fn main() -> eframe::Result<()> {
|
||||
|
||||
let native_options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([900.0, 700.0])
|
||||
.with_min_inner_size([600.0, 400.0])
|
||||
.with_inner_size([1000.0, 900.0])
|
||||
.with_min_inner_size([700.0, 600.0])
|
||||
.with_drag_and_drop(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ impl DropZone {
|
||||
F: FnMut(Vec<PathBuf>),
|
||||
{
|
||||
let available_width = ui.available_width();
|
||||
let height = 120.0;
|
||||
let height = 140.0;
|
||||
|
||||
// Check for drag-and-drop hover state
|
||||
let is_hovering = ui.ctx().input(|i| !i.raw.hovered_files.is_empty());
|
||||
@@ -82,35 +82,57 @@ impl DropZone {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw text
|
||||
let text = if self.is_hovering {
|
||||
"Drop files here"
|
||||
} else {
|
||||
"Drag and drop files here\nor click to browse"
|
||||
};
|
||||
|
||||
// Text colors
|
||||
let text_color = if self.is_hovering {
|
||||
Color32::WHITE
|
||||
} else {
|
||||
Color32::from_gray(180)
|
||||
};
|
||||
|
||||
ui.painter().text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
text,
|
||||
egui::FontId::proportional(16.0),
|
||||
text_color,
|
||||
);
|
||||
let font = egui::FontId::proportional(20.0);
|
||||
let hint_font = egui::FontId::proportional(15.0);
|
||||
|
||||
// Supported formats hint
|
||||
ui.painter().text(
|
||||
egui::pos2(rect.center().x, rect.max.y - 20.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
"GIF, PNG, JPEG, WebP, MP4, MOV, WebM",
|
||||
egui::FontId::proportional(11.0),
|
||||
Color32::from_gray(120),
|
||||
);
|
||||
// Calculate vertical positions for all text elements to be visually centered together
|
||||
// Main text line height ~24px, hint ~18px, spacing between ~12px
|
||||
// Total height: 24 + 24 + 12 + 18 = ~78px for non-hover, center that in rect
|
||||
let total_text_height = 78.0;
|
||||
let top_of_text = rect.center().y - total_text_height / 2.0;
|
||||
|
||||
if self.is_hovering {
|
||||
// Single line when hovering - center it
|
||||
ui.painter().text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
"Drop files here",
|
||||
font,
|
||||
text_color,
|
||||
);
|
||||
} else {
|
||||
// Line 1: "Drag and drop files here"
|
||||
ui.painter().text(
|
||||
egui::pos2(rect.center().x, top_of_text + 12.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
"Drag and drop files here",
|
||||
font.clone(),
|
||||
text_color,
|
||||
);
|
||||
// Line 2: "or click to browse"
|
||||
ui.painter().text(
|
||||
egui::pos2(rect.center().x, top_of_text + 38.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
"or click to browse",
|
||||
font,
|
||||
text_color,
|
||||
);
|
||||
// Hint line at bottom of text block
|
||||
ui.painter().text(
|
||||
egui::pos2(rect.center().x, top_of_text + 66.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
"GIF, PNG, JPEG, WebP, MP4, MOV, WebM",
|
||||
hint_font,
|
||||
Color32::from_gray(140),
|
||||
);
|
||||
}
|
||||
|
||||
// Handle click to open file dialog
|
||||
if response.clicked() {
|
||||
|
||||
@@ -16,7 +16,7 @@ impl Queue {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label(
|
||||
RichText::new("No files in queue")
|
||||
.size(14.0)
|
||||
.size(18.0)
|
||||
.color(Color32::from_gray(120)),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -12,11 +12,17 @@ impl Settings {
|
||||
ui.heading("Encoding Settings");
|
||||
ui.add_space(12.0);
|
||||
|
||||
// Quality slider
|
||||
// Quality setting with editable number
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Quality:");
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.label(format!("{}", settings.quality));
|
||||
let mut quality = settings.quality as i32;
|
||||
let drag = egui::DragValue::new(&mut quality)
|
||||
.range(0..=100)
|
||||
.speed(1);
|
||||
if ui.add(drag).changed() {
|
||||
settings.quality = quality.clamp(0, 100) as u8;
|
||||
}
|
||||
});
|
||||
});
|
||||
let quality_slider = egui::Slider::new(&mut settings.quality, 0..=100)
|
||||
@@ -25,17 +31,22 @@ impl Settings {
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
egui::RichText::new("0 = smallest file, 100 = best quality")
|
||||
.small()
|
||||
.weak(),
|
||||
);
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
// Alpha quality slider
|
||||
// Alpha quality setting with editable number
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Alpha Quality:");
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.label(format!("{}", settings.alpha_quality));
|
||||
let mut alpha = settings.alpha_quality as i32;
|
||||
let drag = egui::DragValue::new(&mut alpha)
|
||||
.range(0..=100)
|
||||
.speed(1);
|
||||
if ui.add(drag).changed() {
|
||||
settings.alpha_quality = alpha.clamp(0, 100) as u8;
|
||||
}
|
||||
});
|
||||
});
|
||||
let alpha_slider = egui::Slider::new(&mut settings.alpha_quality, 0..=100)
|
||||
@@ -44,17 +55,22 @@ impl Settings {
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
egui::RichText::new("Separate quality for transparency")
|
||||
.small()
|
||||
.weak(),
|
||||
);
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
// Speed slider
|
||||
// Speed setting with editable number
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Speed:");
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.label(format!("{}", settings.speed));
|
||||
let mut speed = settings.speed as i32;
|
||||
let drag = egui::DragValue::new(&mut speed)
|
||||
.range(0..=10)
|
||||
.speed(0.1);
|
||||
if ui.add(drag).changed() {
|
||||
settings.speed = speed.clamp(0, 10) as u8;
|
||||
}
|
||||
});
|
||||
});
|
||||
let speed_slider = egui::Slider::new(&mut settings.speed, 0..=10)
|
||||
@@ -63,7 +79,6 @@ impl Settings {
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
egui::RichText::new("0 = slowest/best, 10 = fastest")
|
||||
.small()
|
||||
.weak(),
|
||||
);
|
||||
|
||||
@@ -83,7 +98,6 @@ impl Settings {
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
egui::RichText::new("Perfect quality, larger files")
|
||||
.small()
|
||||
.weak(),
|
||||
);
|
||||
|
||||
@@ -148,7 +162,7 @@ impl Settings {
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_else(|| "Not set".to_string());
|
||||
|
||||
ui.label(egui::RichText::new(&dir_text).small());
|
||||
ui.label(egui::RichText::new(&dir_text));
|
||||
});
|
||||
|
||||
if ui.button("Browse...").clicked() {
|
||||
|
||||
Reference in New Issue
Block a user