Save todos async in a JSON file and load on boot

This commit is contained in:
Héctor Ramón Jiménez 2019-11-17 07:38:01 +01:00
parent 02c20e6202
commit b2392d28bd
2 changed files with 254 additions and 83 deletions

View File

@ -36,6 +36,9 @@ iced_web = { version = "0.1.0-alpha", path = "web" }
[dev-dependencies] [dev-dependencies]
env_logger = "0.7" env_logger = "0.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
directories = "2.0"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies] [target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen = "0.2.51" wasm-bindgen = "0.2.51"

View File

@ -3,23 +3,34 @@ use iced::{
Application, Background, Button, Checkbox, Color, Column, Command, Application, Background, Button, Checkbox, Color, Column, Command,
Container, Element, Font, Length, Row, Scrollable, Text, TextInput, Container, Element, Font, Length, Row, Scrollable, Text, TextInput,
}; };
use serde::{Deserialize, Serialize};
pub fn main() { pub fn main() {
Todos::run() Todos::run()
} }
#[derive(Debug)]
enum Todos {
Loading,
Loaded(State),
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Todos { struct State {
scroll: scrollable::State, scroll: scrollable::State,
input: text_input::State, input: text_input::State,
input_value: String, input_value: String,
filter: Filter, filter: Filter,
tasks: Vec<Task>, tasks: Vec<Task>,
controls: Controls, controls: Controls,
dirty: bool,
saving: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Message { enum Message {
Loaded(Result<SavedState, LoadError>),
Saved(Result<(), SaveError>),
InputChanged(String), InputChanged(String),
CreateTask, CreateTask,
FilterChanged(Filter), FilterChanged(Filter),
@ -30,113 +41,175 @@ impl Application for Todos {
type Message = Message; type Message = Message;
fn new() -> (Todos, Command<Message>) { fn new() -> (Todos, Command<Message>) {
(Todos::default(), Command::none()) (Todos::Loading, Command::attempt(load(), Message::Loaded))
} }
fn title(&self) -> String { fn title(&self) -> String {
String::from("Todos - Iced") let dirty = match self {
Todos::Loading => false,
Todos::Loaded(state) => state.dirty,
};
format!("Todos{} - Iced", if dirty { "*" } else { "" })
} }
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
match message { match self {
Message::InputChanged(value) => { Todos::Loading => {
self.input_value = value; match message {
} Message::Loaded(Ok(state)) => {
Message::CreateTask => { *self = Todos::Loaded(State {
if !self.input_value.is_empty() { input_value: state.input_value,
self.tasks.push(Task::new(self.input_value.clone())); filter: state.filter,
self.input_value.clear(); tasks: state.tasks,
..State::default()
});
}
Message::Loaded(Err(_)) => {
*self = Todos::Loaded(State::default());
}
_ => {}
} }
Command::none()
} }
Message::FilterChanged(filter) => { Todos::Loaded(state) => {
self.filter = filter; let mut saved = false;
}
Message::TaskMessage(i, TaskMessage::Delete) => { match message {
self.tasks.remove(i); Message::InputChanged(value) => {
} state.input_value = value;
Message::TaskMessage(i, task_message) => { }
if let Some(task) = self.tasks.get_mut(i) { Message::CreateTask => {
task.update(task_message); if !state.input_value.is_empty() {
state
.tasks
.push(Task::new(state.input_value.clone()));
state.input_value.clear();
}
}
Message::FilterChanged(filter) => {
state.filter = filter;
}
Message::TaskMessage(i, TaskMessage::Delete) => {
state.tasks.remove(i);
}
Message::TaskMessage(i, task_message) => {
if let Some(task) = state.tasks.get_mut(i) {
task.update(task_message);
}
}
Message::Saved(_) => {
state.saving = false;
saved = true;
}
_ => {}
}
if !saved {
state.dirty = true;
}
if state.dirty && !state.saving {
state.dirty = false;
state.saving = true;
Command::attempt(
save(SavedState {
input_value: state.input_value.clone(),
filter: state.filter,
tasks: state.tasks.clone(),
}),
Message::Saved,
)
} else {
Command::none()
} }
} }
} }
dbg!(self);
Command::none()
} }
fn view(&mut self) -> Element<Message> { fn view(&mut self) -> Element<Message> {
let Todos { match self {
scroll, Todos::Loading => loading_message(),
input, Todos::Loaded(State {
input_value, scroll,
filter, input,
tasks, input_value,
controls, filter,
} = self; tasks,
controls,
..
}) => {
let title = Text::new("todos")
.size(100)
.color([0.5, 0.5, 0.5])
.horizontal_alignment(HorizontalAlignment::Center);
let title = Text::new("todos") let input = TextInput::new(
.size(100) input,
.color([0.5, 0.5, 0.5]) "What needs to be done?",
.horizontal_alignment(HorizontalAlignment::Center); input_value,
Message::InputChanged,
)
.padding(15)
.size(30)
.on_submit(Message::CreateTask);
let input = TextInput::new( let controls = controls.view(&tasks, *filter);
input, let filtered_tasks =
"What needs to be done?", tasks.iter().filter(|task| filter.matches(task));
input_value,
Message::InputChanged,
)
.padding(15)
.size(30)
.on_submit(Message::CreateTask);
let controls = controls.view(&tasks, *filter); let tasks: Element<_> = if filtered_tasks.count() > 0 {
let filtered_tasks = tasks.iter().filter(|task| filter.matches(task)); tasks
.iter_mut()
let tasks: Element<_> = .enumerate()
if filtered_tasks.count() > 0 { .filter(|(_, task)| filter.matches(task))
tasks .fold(Column::new().spacing(20), |column, (i, task)| {
.iter_mut() column.push(task.view().map(move |message| {
.enumerate() Message::TaskMessage(i, message)
.filter(|(_, task)| filter.matches(task)) }))
.fold(Column::new().spacing(20), |column, (i, task)| { })
column.push(task.view().map(move |message| { .into()
Message::TaskMessage(i, message) } else {
})) empty_message(match filter {
Filter::All => "You have not created a task yet...",
Filter::Active => "All your tasks are done! :D",
Filter::Completed => {
"You have not completed a task yet..."
}
}) })
};
let content = Column::new()
.max_width(800)
.spacing(20)
.push(title)
.push(input)
.push(controls)
.push(tasks);
Scrollable::new(scroll)
.padding(40)
.push(
Container::new(content).width(Length::Fill).center_x(),
)
.into() .into()
} else { }
empty_message(match filter { }
Filter::All => "You have not created a task yet...",
Filter::Active => "All your tasks are done! :D",
Filter::Completed => "You have not completed a task yet...",
})
};
let content = Column::new()
.max_width(800)
.spacing(20)
.push(title)
.push(input)
.push(controls)
.push(tasks);
Scrollable::new(scroll)
.padding(40)
.push(Container::new(content).width(Length::Fill).center_x())
.into()
} }
} }
#[derive(Debug)] #[derive(Debug, Clone, Serialize, Deserialize)]
struct Task { struct Task {
description: String, description: String,
completed: bool, completed: bool,
#[serde(skip)]
state: TaskState, state: TaskState,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub enum TaskState { pub enum TaskState {
Idle { Idle {
edit_button: button::State, edit_button: button::State,
@ -147,6 +220,14 @@ pub enum TaskState {
}, },
} }
impl Default for TaskState {
fn default() -> Self {
TaskState::Idle {
edit_button: button::State::new(),
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum TaskMessage { pub enum TaskMessage {
Completed(bool), Completed(bool),
@ -255,7 +336,7 @@ impl Task {
} }
} }
#[derive(Debug, Default)] #[derive(Debug, Default, Clone)]
pub struct Controls { pub struct Controls {
all_button: button::State, all_button: button::State,
active_button: button::State, active_button: button::State,
@ -324,7 +405,7 @@ impl Controls {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Filter { pub enum Filter {
All, All,
Active, Active,
@ -347,6 +428,18 @@ impl Filter {
} }
} }
fn loading_message() -> Element<'static, Message> {
Container::new(
Text::new("Loading...")
.horizontal_alignment(HorizontalAlignment::Center)
.size(50),
)
.width(Length::Fill)
.height(Length::Fill)
.center_y()
.into()
}
fn empty_message(message: &str) -> Element<'static, Message> { fn empty_message(message: &str) -> Element<'static, Message> {
Container::new( Container::new(
Text::new(message) Text::new(message)
@ -381,3 +474,78 @@ fn edit_icon() -> Text {
fn delete_icon() -> Text { fn delete_icon() -> Text {
icon('\u{F1F8}') icon('\u{F1F8}')
} }
// Persistence
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SavedState {
input_value: String,
filter: Filter,
tasks: Vec<Task>,
}
fn save_path() -> std::path::PathBuf {
let mut path = if let Some(project_dirs) =
directories::ProjectDirs::from("rs", "Iced", "Todos")
{
project_dirs.data_dir().into()
} else {
std::env::current_dir()
.expect("The current directory is not accessible")
};
path.push("todos.json");
path
}
#[derive(Debug, Clone)]
enum LoadError {
FileError,
FormatError,
}
async fn load() -> Result<SavedState, LoadError> {
use std::io::Read;
let mut contents = String::new();
let mut file =
std::fs::File::open(save_path()).map_err(|_| LoadError::FileError)?;
file.read_to_string(&mut contents)
.map_err(|_| LoadError::FileError)?;
serde_json::from_str(&contents).map_err(|_| LoadError::FormatError)
}
#[derive(Debug, Clone)]
enum SaveError {
DirectoryError,
FileError,
WriteError,
FormatError,
}
async fn save(state: SavedState) -> Result<(), SaveError> {
use std::io::Write;
let json = serde_json::to_string_pretty(&state)
.map_err(|_| SaveError::FormatError)?;
let save_path = save_path();
let save_dir = save_path.parent().ok_or(SaveError::DirectoryError)?;
std::fs::create_dir_all(save_dir).map_err(|_| SaveError::DirectoryError)?;
let mut file =
std::fs::File::create(save_path).map_err(|_| SaveError::FileError)?;
file.write_all(json.as_bytes())
.map_err(|_| SaveError::WriteError)?;
// This is a simple way to save at most once every couple seconds
// We will be able to get rid of it once we implement event subscriptions
std::thread::sleep(std::time::Duration::from_secs(2));
Ok(())
}