Implement Welcome screen + App skeleton with state management, file opening, and settings persistence
This commit is contained in:
145
src/app.rs
145
src/app.rs
@@ -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 {
|
impl App {
|
||||||
pub fn new(_cc: &eframe::CreationContext) -> Self {
|
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
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 {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user