state_query_sqlite/
main.rs1#![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}