Desktop: convert Settings/Stats/Maintenance to scrollable screens
The three big dialogs were floating egui::Windows that crammed everything into a fixed-height popover and hid the library underneath. They're now full CentralPanel views with a "← Library" back button and a vertical ScrollArea, so long settings panels and big stats reports scroll cleanly. Top-bar nav uses a Screen enum instead of a trio of show_* booleans; clicking the active screen returns to Library. Floating dialogs that are genuinely modal (channel options, folder manager, move-to-folder) stay as windows since they're actions, not screens. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9e64297da6
commit
980ffbc4e7
1 changed files with 105 additions and 70 deletions
159
src/app.rs
159
src/app.rs
|
|
@ -29,6 +29,20 @@ const BROWSERS: &[(&str, &str)] = &[
|
||||||
("none", "None (no cookies)"),
|
("none", "None (no cookies)"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Top-level navigation. The desktop swaps the CentralPanel content
|
||||||
|
/// between these instead of stacking floating windows. Smaller dialogs
|
||||||
|
/// (channel options, folder manager, move-to-folder) stay as floating
|
||||||
|
/// `egui::Window` since they're modal-ish actions, not screens.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
|
||||||
|
enum Screen {
|
||||||
|
/// Channel sidebar + video grid + detail panel. The default.
|
||||||
|
#[default]
|
||||||
|
Library,
|
||||||
|
Settings,
|
||||||
|
Stats,
|
||||||
|
Maintenance,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
enum SortMode {
|
enum SortMode {
|
||||||
Title,
|
Title,
|
||||||
|
|
@ -90,7 +104,10 @@ pub struct App {
|
||||||
search: String,
|
search: String,
|
||||||
downloader: Downloader,
|
downloader: Downloader,
|
||||||
show_downloads: bool,
|
show_downloads: bool,
|
||||||
show_settings: bool,
|
/// Which top-level screen is currently rendered into CentralPanel.
|
||||||
|
/// Settings / Stats / Maintenance now take the full window (with
|
||||||
|
/// scroll) instead of floating over the library.
|
||||||
|
current_screen: Screen,
|
||||||
dl_url: String,
|
dl_url: String,
|
||||||
dl_full_scan: bool,
|
dl_full_scan: bool,
|
||||||
dl_quality: DownloadQuality,
|
dl_quality: DownloadQuality,
|
||||||
|
|
@ -154,11 +171,9 @@ pub struct App {
|
||||||
// is.
|
// is.
|
||||||
backup_save_tx: Sender<PathBuf>,
|
backup_save_tx: Sender<PathBuf>,
|
||||||
backup_save_rx: Receiver<PathBuf>,
|
backup_save_rx: Receiver<PathBuf>,
|
||||||
// Maintenance (library health) window
|
// Maintenance (library health) screen state. The flag is gone now —
|
||||||
show_maintenance: bool,
|
// Screen::Maintenance + this report's presence drive the render.
|
||||||
health_report: Option<crate::maintenance::HealthReport>,
|
health_report: Option<crate::maintenance::HealthReport>,
|
||||||
// Statistics window
|
|
||||||
show_stats: bool,
|
|
||||||
stats_report: Option<crate::stats::StatsReport>,
|
stats_report: Option<crate::stats::StatsReport>,
|
||||||
// Per-channel download-options dialog state
|
// Per-channel download-options dialog state
|
||||||
show_channel_options: bool,
|
show_channel_options: bool,
|
||||||
|
|
@ -286,7 +301,7 @@ impl App {
|
||||||
search: String::new(),
|
search: String::new(),
|
||||||
downloader: Downloader::new(channels_root, browser, max_concurrent, use_bundled_ytdlp),
|
downloader: Downloader::new(channels_root, browser, max_concurrent, use_bundled_ytdlp),
|
||||||
show_downloads: false,
|
show_downloads: false,
|
||||||
show_settings: false,
|
current_screen: Screen::Library,
|
||||||
dl_url: String::new(),
|
dl_url: String::new(),
|
||||||
dl_full_scan: true,
|
dl_full_scan: true,
|
||||||
dl_quality: DownloadQuality::Best,
|
dl_quality: DownloadQuality::Best,
|
||||||
|
|
@ -332,9 +347,7 @@ impl App {
|
||||||
cookies_pick_rx,
|
cookies_pick_rx,
|
||||||
backup_save_tx,
|
backup_save_tx,
|
||||||
backup_save_rx,
|
backup_save_rx,
|
||||||
show_maintenance: false,
|
|
||||||
health_report: None,
|
health_report: None,
|
||||||
show_stats: false,
|
|
||||||
stats_report: None,
|
stats_report: None,
|
||||||
show_channel_options: false,
|
show_channel_options: false,
|
||||||
channel_options_target: None,
|
channel_options_target: None,
|
||||||
|
|
@ -929,27 +942,37 @@ impl App {
|
||||||
if ui.selectable_label(self.show_downloads, dl_label).clicked() {
|
if ui.selectable_label(self.show_downloads, dl_label).clicked() {
|
||||||
self.show_downloads = !self.show_downloads;
|
self.show_downloads = !self.show_downloads;
|
||||||
}
|
}
|
||||||
if ui.selectable_label(self.show_stats, "📊 Stats").clicked() {
|
// Top-level screen nav. Clicking the active screen returns
|
||||||
self.show_stats = !self.show_stats;
|
// to Library (which is the canonical "home" view).
|
||||||
if self.show_stats {
|
if ui.selectable_label(self.current_screen == Screen::Library, "📚 Library").clicked() {
|
||||||
|
self.current_screen = Screen::Library;
|
||||||
|
}
|
||||||
|
if ui.selectable_label(self.current_screen == Screen::Stats, "📊 Stats").clicked() {
|
||||||
|
if self.current_screen == Screen::Stats {
|
||||||
|
self.current_screen = Screen::Library;
|
||||||
|
} else {
|
||||||
self.stats_report = Some(crate::stats::build(
|
self.stats_report = Some(crate::stats::build(
|
||||||
&self.library,
|
&self.library,
|
||||||
&self.watched,
|
&self.watched,
|
||||||
&self.resume_positions,
|
&self.resume_positions,
|
||||||
crate::stats::now_unix(),
|
crate::stats::now_unix(),
|
||||||
));
|
));
|
||||||
|
self.current_screen = Screen::Stats;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ui.selectable_label(self.show_maintenance, "🩺 Maintenance").clicked() {
|
if ui.selectable_label(self.current_screen == Screen::Maintenance, "🩺 Maintenance").clicked() {
|
||||||
self.show_maintenance = !self.show_maintenance;
|
if self.current_screen == Screen::Maintenance {
|
||||||
if self.show_maintenance {
|
self.current_screen = Screen::Library;
|
||||||
|
} else {
|
||||||
self.health_report =
|
self.health_report =
|
||||||
Some(crate::maintenance::scan(&self.library_root, &self.library));
|
Some(crate::maintenance::scan(&self.library_root, &self.library));
|
||||||
|
self.current_screen = Screen::Maintenance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ui.selectable_label(self.show_settings, "⚙ Settings").clicked() {
|
if ui.selectable_label(self.current_screen == Screen::Settings, "⚙ Settings").clicked() {
|
||||||
self.show_settings = !self.show_settings;
|
if self.current_screen == Screen::Settings {
|
||||||
if self.show_settings {
|
self.current_screen = Screen::Library;
|
||||||
|
} else {
|
||||||
self.settings_dir = self.channels_root.display().to_string();
|
self.settings_dir = self.channels_root.display().to_string();
|
||||||
self.settings_plex_path = self.config.plex.library_path
|
self.settings_plex_path = self.config.plex.library_path
|
||||||
.as_deref().map(|p| p.display().to_string()).unwrap_or_default();
|
.as_deref().map(|p| p.display().to_string()).unwrap_or_default();
|
||||||
|
|
@ -967,6 +990,7 @@ impl App {
|
||||||
} else {
|
} else {
|
||||||
"no cookies.txt".to_string()
|
"no cookies.txt".to_string()
|
||||||
};
|
};
|
||||||
|
self.current_screen = Screen::Settings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
|
|
@ -1473,11 +1497,7 @@ impl App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn maintenance_window(&mut self, ctx: &egui::Context) {
|
fn maintenance_screen(&mut self, ctx: &egui::Context) {
|
||||||
if !self.show_maintenance {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mut open = self.show_maintenance;
|
|
||||||
let report = self.health_report.clone().unwrap_or_default();
|
let report = self.health_report.clone().unwrap_or_default();
|
||||||
|
|
||||||
// Actions are collected during rendering and applied after the closure
|
// Actions are collected during rendering and applied after the closure
|
||||||
|
|
@ -1486,18 +1506,19 @@ impl App {
|
||||||
let mut to_repair: Vec<String> = Vec::new();
|
let mut to_repair: Vec<String> = Vec::new();
|
||||||
let mut rescan_health = false;
|
let mut rescan_health = false;
|
||||||
|
|
||||||
egui::Window::new("🩺 Library health")
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
.open(&mut open)
|
ui.horizontal(|ui| {
|
||||||
.collapsible(false)
|
if ui.button("← Library").clicked() {
|
||||||
.resizable(true)
|
self.current_screen = Screen::Library;
|
||||||
.default_width(620.0)
|
}
|
||||||
.default_height(500.0)
|
ui.heading("🩺 Library health");
|
||||||
.show(ctx, |ui| {
|
ui.separator();
|
||||||
if ui.button("⟳ Rescan health").clicked() {
|
if ui.button("⟳ Rescan health").clicked() {
|
||||||
rescan_health = true;
|
rescan_health = true;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
|
||||||
ui.heading(format!("Duplicates ({})", report.duplicates.len()));
|
ui.heading(format!("Duplicates ({})", report.duplicates.len()));
|
||||||
if report.duplicates.is_empty() {
|
if report.duplicates.is_empty() {
|
||||||
ui.label(egui::RichText::new("No duplicate video IDs found.").weak());
|
ui.label(egui::RichText::new("No duplicate video IDs found.").weak());
|
||||||
|
|
@ -1565,7 +1586,6 @@ impl App {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
self.show_maintenance = open;
|
|
||||||
|
|
||||||
// ── Apply collected actions ────────────────────────────────────────
|
// ── Apply collected actions ────────────────────────────────────────
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
|
|
@ -1595,24 +1615,35 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stats_window(&mut self, ctx: &egui::Context) {
|
fn stats_screen(&mut self, ctx: &egui::Context) {
|
||||||
if !self.show_stats { return; }
|
|
||||||
let mut open = self.show_stats;
|
|
||||||
let report = match &self.stats_report {
|
let report = match &self.stats_report {
|
||||||
Some(r) => r.clone(),
|
Some(r) => r.clone(),
|
||||||
None => return,
|
None => {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("← Library").clicked() {
|
||||||
|
self.current_screen = Screen::Library;
|
||||||
|
}
|
||||||
|
ui.heading("📊 Library statistics");
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
ui.label(egui::RichText::new("No stats yet — try Recompute.").weak());
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let mut rescan = false;
|
let mut rescan = false;
|
||||||
egui::Window::new("📊 Library statistics")
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
.open(&mut open)
|
ui.horizontal(|ui| {
|
||||||
.collapsible(false)
|
if ui.button("← Library").clicked() {
|
||||||
.resizable(true)
|
self.current_screen = Screen::Library;
|
||||||
.default_width(560.0)
|
}
|
||||||
.default_height(520.0)
|
ui.heading("📊 Library statistics");
|
||||||
.show(ctx, |ui| {
|
|
||||||
if ui.button("⟳ Recompute").clicked() { rescan = true; }
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
if ui.button("⟳ Recompute").clicked() { rescan = true; }
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
|
||||||
ui.heading("Totals");
|
ui.heading("Totals");
|
||||||
egui::Grid::new("stats_totals").num_columns(2).striped(true).show(ui, |ui| {
|
egui::Grid::new("stats_totals").num_columns(2).striped(true).show(ui, |ui| {
|
||||||
ui.label("Channels"); ui.label(report.total_channels.to_string()); ui.end_row();
|
ui.label("Channels"); ui.label(report.total_channels.to_string()); ui.end_row();
|
||||||
|
|
@ -1665,7 +1696,6 @@ impl App {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
self.show_stats = open;
|
|
||||||
if rescan {
|
if rescan {
|
||||||
self.stats_report = Some(crate::stats::build(
|
self.stats_report = Some(crate::stats::build(
|
||||||
&self.library, &self.watched, &self.resume_positions, crate::stats::now_unix(),
|
&self.library, &self.watched, &self.resume_positions, crate::stats::now_unix(),
|
||||||
|
|
@ -2011,17 +2041,16 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn settings_window(&mut self, ctx: &egui::Context) {
|
fn settings_screen(&mut self, ctx: &egui::Context) {
|
||||||
if !self.show_settings {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
return;
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("← Library").clicked() {
|
||||||
|
self.current_screen = Screen::Library;
|
||||||
}
|
}
|
||||||
let mut open = self.show_settings;
|
ui.heading("⚙ Settings");
|
||||||
egui::Window::new("⚙ Settings")
|
});
|
||||||
.open(&mut open)
|
ui.separator();
|
||||||
.collapsible(false)
|
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
|
||||||
.resizable(false)
|
|
||||||
.default_width(480.0)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
egui::Grid::new("settings_grid")
|
egui::Grid::new("settings_grid")
|
||||||
.num_columns(2)
|
.num_columns(2)
|
||||||
.spacing([12.0, 8.0])
|
.spacing([12.0, 8.0])
|
||||||
|
|
@ -2443,7 +2472,7 @@ impl App {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
self.show_settings = open;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detail_panel(&mut self, ctx: &egui::Context) {
|
fn detail_panel(&mut self, ctx: &egui::Context) {
|
||||||
|
|
@ -3089,21 +3118,27 @@ impl eframe::App for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.top_bar(ctx);
|
self.top_bar(ctx);
|
||||||
|
// Floating sub-dialogs that overlay any screen.
|
||||||
|
self.channel_options_window(ctx);
|
||||||
|
self.folder_manager_window(ctx);
|
||||||
|
self.move_to_folder_window(ctx);
|
||||||
|
|
||||||
|
match self.current_screen {
|
||||||
|
Screen::Library => {
|
||||||
self.channel_panel(ctx);
|
self.channel_panel(ctx);
|
||||||
if self.show_downloads {
|
if self.show_downloads {
|
||||||
self.downloads_panel(ctx);
|
self.downloads_panel(ctx);
|
||||||
}
|
}
|
||||||
self.settings_window(ctx);
|
|
||||||
self.maintenance_window(ctx);
|
|
||||||
self.stats_window(ctx);
|
|
||||||
self.channel_options_window(ctx);
|
|
||||||
self.folder_manager_window(ctx);
|
|
||||||
self.move_to_folder_window(ctx);
|
|
||||||
self.detail_panel(ctx);
|
self.detail_panel(ctx);
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
self.video_list(ctx, ui);
|
self.video_list(ctx, ui);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Screen::Settings => self.settings_screen(ctx),
|
||||||
|
Screen::Stats => self.stats_screen(ctx),
|
||||||
|
Screen::Maintenance => self.maintenance_screen(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue