Implement Welcome screen + App skeleton with state management, file opening, and settings persistence

This commit is contained in:
Developer
2026-05-13 23:44:36 +08:00
parent 17fbe7efbb
commit 51356f7258

View File

@@ -1,11 +1,148 @@
pub struct App;
use crate::book::Book;
use crate::persistence;
use crate::theme::{self, Settings};
use eframe::egui;
use std::path::PathBuf;
pub struct App {
pub state: AppState,
settings: Settings,
settings_dir: std::path::PathBuf,
}
pub struct AppState {
pub book: Option<Book>,
pub current_section: usize,
pub current_page: usize,
pub sidebar_open: bool,
pub file_path: Option<PathBuf>,
pub error_message: Option<String>,
}
impl App {
pub fn new(_cc: &eframe::CreationContext) -> Self {
Self
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
let settings_dir = persistence::settings_dir();
let settings = persistence::load_settings(&settings_dir).unwrap_or_default();
cc.egui_ctx.set_style(theme::create_style(&settings.theme));
Self {
state: AppState {
book: None,
current_section: 0,
current_page: 0,
sidebar_open: false,
file_path: None,
error_message: None,
},
settings,
settings_dir,
}
}
pub fn open_file(&mut self, path: PathBuf) {
match crate::book::load_epub(&path) {
Ok(book) => {
let path_str = path.to_string_lossy().to_string();
let pos = self.settings.reading_positions.get(&path_str).copied();
let mut recent = Vec::new();
recent.push(path_str.clone());
for f in &self.settings.recent_files {
if *f != path_str {
recent.push(f.clone());
if recent.len() >= 10 {
break;
}
}
}
self.settings.recent_files = recent;
self.state.book = Some(book);
self.state.current_section = pos.map(|p| p.section).unwrap_or(0);
self.state.current_page = pos.map(|p| p.page).unwrap_or(0);
self.state.sidebar_open = false;
self.state.file_path = Some(path);
self.state.error_message = None;
self.save_settings();
}
Err(e) => {
self.state.error_message = Some(e);
}
}
}
fn save_settings(&self) {
let _ = persistence::save_settings(&self.settings_dir, &self.settings);
}
fn save_reading_position(&mut self) {
if let Some(ref path) = self.state.file_path {
let path_str = path.to_string_lossy().to_string();
self.settings.reading_positions.insert(
path_str,
theme::ReadingPosition {
section: self.state.current_section,
page: self.state.current_page,
},
);
}
}
fn welcome_view(&mut self, ctx: &egui::Context) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.vertical_centered(|ui| {
ui.add_space(150.0);
ui.heading("ePub Reader");
ui.add_space(20.0);
if ui
.add(egui::Button::new("📂 打开 ePub 文件").min_size(egui::vec2(200.0, 40.0)))
.clicked()
{
if let Some(path) = rfd::FileDialog::new()
.add_filter("ePub", &["epub"])
.pick_file()
{
self.open_file(path);
}
}
ui.add_space(30.0);
if !self.settings.recent_files.is_empty() {
ui.label("最近阅读:");
ui.separator();
let mut to_open: Option<PathBuf> = None;
let mut to_remove: Option<usize> = None;
for (i, path) in self.settings.recent_files.iter().enumerate() {
let name = std::path::Path::new(path)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| path.clone());
if ui.button(&name).clicked() {
let p = std::path::PathBuf::from(path);
if p.exists() {
to_open = Some(p);
} else {
to_remove = Some(i);
}
}
}
if let Some(path) = to_open {
self.open_file(path);
}
if let Some(i) = to_remove {
self.settings.recent_files.remove(i);
self.save_settings();
}
}
if let Some(ref msg) = self.state.error_message {
ui.add_space(20.0);
ui.colored_label(egui::Color32::RED, msg);
}
});
});
}
}
impl eframe::App for App {
fn update(&mut self, _ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {}
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if self.state.book.is_none() {
self.welcome_view(ctx);
}
}
}