Save todos
async in a JSON file and load on boot
This commit is contained in:
parent
02c20e6202
commit
b2392d28bd
@ -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"
|
||||||
|
@ -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,52 +41,106 @@ 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 self {
|
||||||
|
Todos::Loading => {
|
||||||
match message {
|
match message {
|
||||||
Message::InputChanged(value) => {
|
Message::Loaded(Ok(state)) => {
|
||||||
self.input_value = value;
|
*self = Todos::Loaded(State {
|
||||||
|
input_value: state.input_value,
|
||||||
|
filter: state.filter,
|
||||||
|
tasks: state.tasks,
|
||||||
|
..State::default()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Message::CreateTask => {
|
Message::Loaded(Err(_)) => {
|
||||||
if !self.input_value.is_empty() {
|
*self = Todos::Loaded(State::default());
|
||||||
self.tasks.push(Task::new(self.input_value.clone()));
|
|
||||||
self.input_value.clear();
|
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
Message::FilterChanged(filter) => {
|
|
||||||
self.filter = filter;
|
|
||||||
}
|
|
||||||
Message::TaskMessage(i, TaskMessage::Delete) => {
|
|
||||||
self.tasks.remove(i);
|
|
||||||
}
|
|
||||||
Message::TaskMessage(i, task_message) => {
|
|
||||||
if let Some(task) = self.tasks.get_mut(i) {
|
|
||||||
task.update(task_message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dbg!(self);
|
|
||||||
|
|
||||||
Command::none()
|
Command::none()
|
||||||
}
|
}
|
||||||
|
Todos::Loaded(state) => {
|
||||||
|
let mut saved = false;
|
||||||
|
|
||||||
|
match message {
|
||||||
|
Message::InputChanged(value) => {
|
||||||
|
state.input_value = value;
|
||||||
|
}
|
||||||
|
Message::CreateTask => {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn view(&mut self) -> Element<Message> {
|
fn view(&mut self) -> Element<Message> {
|
||||||
let Todos {
|
match self {
|
||||||
|
Todos::Loading => loading_message(),
|
||||||
|
Todos::Loaded(State {
|
||||||
scroll,
|
scroll,
|
||||||
input,
|
input,
|
||||||
input_value,
|
input_value,
|
||||||
filter,
|
filter,
|
||||||
tasks,
|
tasks,
|
||||||
controls,
|
controls,
|
||||||
} = self;
|
..
|
||||||
|
}) => {
|
||||||
let title = Text::new("todos")
|
let title = Text::new("todos")
|
||||||
.size(100)
|
.size(100)
|
||||||
.color([0.5, 0.5, 0.5])
|
.color([0.5, 0.5, 0.5])
|
||||||
@ -92,10 +157,10 @@ impl Application for Todos {
|
|||||||
.on_submit(Message::CreateTask);
|
.on_submit(Message::CreateTask);
|
||||||
|
|
||||||
let controls = controls.view(&tasks, *filter);
|
let controls = controls.view(&tasks, *filter);
|
||||||
let filtered_tasks = tasks.iter().filter(|task| filter.matches(task));
|
let filtered_tasks =
|
||||||
|
tasks.iter().filter(|task| filter.matches(task));
|
||||||
|
|
||||||
let tasks: Element<_> =
|
let tasks: Element<_> = if filtered_tasks.count() > 0 {
|
||||||
if filtered_tasks.count() > 0 {
|
|
||||||
tasks
|
tasks
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
@ -110,7 +175,9 @@ impl Application for Todos {
|
|||||||
empty_message(match filter {
|
empty_message(match filter {
|
||||||
Filter::All => "You have not created a task yet...",
|
Filter::All => "You have not created a task yet...",
|
||||||
Filter::Active => "All your tasks are done! :D",
|
Filter::Active => "All your tasks are done! :D",
|
||||||
Filter::Completed => "You have not completed a task yet...",
|
Filter::Completed => {
|
||||||
|
"You have not completed a task yet..."
|
||||||
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -124,19 +191,25 @@ impl Application for Todos {
|
|||||||
|
|
||||||
Scrollable::new(scroll)
|
Scrollable::new(scroll)
|
||||||
.padding(40)
|
.padding(40)
|
||||||
.push(Container::new(content).width(Length::Fill).center_x())
|
.push(
|
||||||
|
Container::new(content).width(Length::Fill).center_x(),
|
||||||
|
)
|
||||||
.into()
|
.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(())
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user