state_query_sqlite/
main.rs

1#![cfg_attr(
2    all(not(debug_assertions), target_os = "windows"),
3    windows_subsystem = "windows"
4)]
5
6use std::sync::{
7    Arc,
8    Mutex,
9};
10
11use freya::{
12    prelude::*,
13    query::*,
14};
15use rusqlite::Connection;
16
17#[derive(Clone, Debug)]
18struct Todo {
19    id: i64,
20    title: String,
21    completed: bool,
22}
23
24type Db = Captured<Arc<Mutex<Connection>>>;
25type Error = Box<dyn std::error::Error + Send + Sync>;
26
27fn init_db() -> Arc<Mutex<Connection>> {
28    let conn =
29        Connection::open("./state_query_sqlite_data").expect("Failed to create in-memory database");
30    conn.execute(
31        "CREATE TABLE IF NOT EXISTS todos (
32            id INTEGER PRIMARY KEY AUTOINCREMENT,
33            title TEXT NOT NULL,
34            completed BOOLEAN NOT NULL DEFAULT 0
35        )",
36        [],
37    )
38    .expect("Failed to create table");
39    Arc::new(Mutex::new(conn))
40}
41
42#[derive(Clone, PartialEq, Hash, Eq)]
43struct GetTodos;
44
45impl QueryCapability for GetTodos {
46    type Ok = Vec<Todo>;
47    type Err = Error;
48    type Keys = ();
49
50    async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
51        let db = consume_context::<Db>();
52        blocking::unblock(move || {
53            let conn = db.lock().map_err(|e| -> Error { e.to_string().into() })?;
54            let mut stmt = conn.prepare("SELECT id, title, completed FROM todos")?;
55            let todos = stmt
56                .query_map([], |row| {
57                    Ok(Todo {
58                        id: row.get(0)?,
59                        title: row.get(1)?,
60                        completed: row.get(2)?,
61                    })
62                })?
63                .collect::<Result<Vec<_>, _>>()?;
64            Ok(todos)
65        })
66        .await
67    }
68}
69
70#[derive(Clone, PartialEq, Hash, Eq)]
71struct AddTodo;
72
73impl MutationCapability for AddTodo {
74    type Ok = ();
75    type Err = Error;
76    type Keys = String;
77
78    async fn run(&self, title: &Self::Keys) -> Result<Self::Ok, Self::Err> {
79        let db = consume_context::<Db>();
80        let title = title.clone();
81        blocking::unblock(move || {
82            let conn = db.lock().map_err(|e| -> Error { e.to_string().into() })?;
83            conn.execute("INSERT INTO todos (title) VALUES (?)", [&title])?;
84            Ok(())
85        })
86        .await
87    }
88
89    async fn on_settled(&self, _keys: &Self::Keys, _result: &Result<Self::Ok, Self::Err>) {
90        QueriesStorage::<GetTodos>::invalidate_matching(()).await;
91    }
92}
93
94#[derive(Clone, PartialEq, Hash, Eq)]
95struct ToggleTodo(i64);
96
97impl MutationCapability for ToggleTodo {
98    type Ok = ();
99    type Err = Error;
100    type Keys = i64;
101
102    async fn run(&self, _: &Self::Keys) -> Result<Self::Ok, Self::Err> {
103        let db = consume_context::<Db>();
104        let id = self.0;
105        blocking::unblock(move || {
106            let conn = db.lock().map_err(|e| -> Error { e.to_string().into() })?;
107            conn.execute(
108                "UPDATE todos SET completed = NOT completed WHERE id = ?",
109                [id],
110            )?;
111            Ok(())
112        })
113        .await
114    }
115
116    async fn on_settled(&self, _keys: &Self::Keys, _result: &Result<Self::Ok, Self::Err>) {
117        QueriesStorage::<GetTodos>::invalidate_matching(()).await;
118    }
119}
120
121#[derive(Clone, PartialEq, Hash, Eq)]
122struct DeleteTodo(i64);
123
124impl MutationCapability for DeleteTodo {
125    type Ok = ();
126    type Err = Error;
127    type Keys = i64;
128
129    async fn run(&self, _: &Self::Keys) -> Result<Self::Ok, Self::Err> {
130        let db = consume_context::<Db>();
131        let id = self.0;
132        blocking::unblock(move || {
133            let conn = db.lock().map_err(|e| -> Error { e.to_string().into() })?;
134            conn.execute("DELETE FROM todos WHERE id = ?", [id])?;
135            Ok(())
136        })
137        .await
138    }
139
140    async fn on_settled(&self, _keys: &Self::Keys, _result: &Result<Self::Ok, Self::Err>) {
141        QueriesStorage::<GetTodos>::invalidate_matching(()).await;
142    }
143}
144
145#[derive(PartialEq)]
146struct TodoRow {
147    id: i64,
148    title: String,
149    completed: bool,
150}
151
152impl TodoRow {
153    fn new(todo: &Todo) -> Self {
154        Self {
155            id: todo.id,
156            title: todo.title.clone(),
157            completed: todo.completed,
158        }
159    }
160}
161
162impl Component for TodoRow {
163    fn render(&self) -> impl IntoElement {
164        let id = self.id;
165        let toggle_mutation = use_mutation(Mutation::new(ToggleTodo(id)));
166        let delete_mutation = use_mutation(Mutation::new(DeleteTodo(id)));
167
168        TableRow::new()
169            .child(
170                TableCell::new().child(
171                    Button::new()
172                        .on_press(move |_| toggle_mutation.mutate(id))
173                        .child(if self.completed { "✓" } else { "○" }),
174                ),
175            )
176            .child(TableCell::new().child(self.title.clone()))
177            .child(
178                TableCell::new().child(
179                    Button::new()
180                        .on_press(move |_| delete_mutation.mutate(id))
181                        .child("Delete"),
182                ),
183            )
184    }
185
186    fn render_key(&self) -> DiffKey {
187        DiffKey::from(&self.id)
188    }
189}
190
191fn app() -> impl IntoElement {
192    use_provide_context(|| Captured(init_db()));
193    let todos_query = use_query(Query::new((), GetTodos));
194    let add_mutation = use_mutation(Mutation::new(AddTodo));
195    let mut input_text = use_state(String::new);
196
197    let on_add = move |_| {
198        let text = input_text.read().clone();
199        if !text.is_empty() {
200            add_mutation.mutate(text);
201            input_text.set(String::new());
202        }
203    };
204
205    rect()
206        .expanded()
207        .padding(16.)
208        .spacing(12.)
209        .child("SQLite Todo List")
210        .child(
211            rect()
212                .horizontal()
213                .spacing(8.)
214                .cross_align(Alignment::Center)
215                .child(Input::new(input_text).placeholder("Add a new todo..."))
216                .child(Button::new().on_press(on_add).child("Add")),
217        )
218        .child(match &*todos_query.read().state() {
219            QueryStateData::Loading { res: None } | QueryStateData::Pending => {
220                "Loading...".into_element()
221            }
222            QueryStateData::Loading {
223                res: Some(Ok(todos)),
224            }
225            | QueryStateData::Settled { res: Ok(todos), .. } => {
226                if todos.is_empty() {
227                    "No todos yet. Add one above!".into_element()
228                } else {
229                    Table::new()
230                        .column_widths([Size::px(60.), Size::flex(1.), Size::px(130.)])
231                        .child(
232                            TableHead::new().child(
233                                TableRow::new()
234                                    .child(TableCell::new().child("Status"))
235                                    .child(TableCell::new().child("Title"))
236                                    .child(TableCell::new().child("Actions")),
237                            ),
238                        )
239                        .child(
240                            TableBody::new().child(
241                                ScrollView::new()
242                                    .children(todos.iter().map(|todo| TodoRow::new(todo).into())),
243                            ),
244                        )
245                        .into_element()
246                }
247            }
248            QueryStateData::Loading {
249                res: Some(Err(e)), ..
250            }
251            | QueryStateData::Settled { res: Err(e), .. } => format!("Error: {}", e).into_element(),
252        })
253}
254
255fn main() {
256    launch(
257        LaunchConfig::new().with_window(
258            WindowConfig::new(app)
259                .with_size(700., 500.)
260                .with_title("SQLite Todo List"),
261        ),
262    );
263}