From b5ddcd392e1a7bb5201fe7c785cf8577a2df1794 Mon Sep 17 00:00:00 2001 From: blueJpg <2238288979@qq.com> Date: Wed, 27 Dec 2023 15:45:29 +0800 Subject: [PATCH] [+] address book add item function --- bitbox/src/db/address_book.rs | 158 ++++++++++++++++++ bitbox/src/db/mod.rs | 2 + bitbox/src/logic/address_book.rs | 96 ++++++++++- bitbox/src/logic/message.rs | 18 +- bitbox/src/logic/setting.rs | 34 ++-- bitbox/src/util/translator.rs | 3 + bitbox/ui/appwindow.slint | 17 +- bitbox/ui/dialog/address-book-add-item.slint | 72 ++++++++ bitbox/ui/dialog/setting/ui.slint | 5 +- bitbox/ui/images/qrcode.svg | 1 + bitbox/ui/logic.slint | 4 +- .../ui/panel/bodyer/tool/address-book.slint | 13 +- bitbox/ui/store.slint | 3 + bitbox/ui/theme.slint | 2 +- bitbox/ui/translator.slint | 11 +- 15 files changed, 410 insertions(+), 29 deletions(-) create mode 100644 bitbox/src/db/address_book.rs create mode 100644 bitbox/ui/dialog/address-book-add-item.slint create mode 100644 bitbox/ui/images/qrcode.svg diff --git a/bitbox/src/db/address_book.rs b/bitbox/src/db/address_book.rs new file mode 100644 index 0000000..434056a --- /dev/null +++ b/bitbox/src/db/address_book.rs @@ -0,0 +1,158 @@ +use super::pool; +use anyhow::Result; + +#[derive(Serialize, Deserialize, Debug, Clone, sqlx::FromRow)] +pub struct AddressBook { + pub uuid: String, + pub data: String, +} + +pub async fn new() -> Result<()> { + sqlx::query( + "CREATE TABLE IF NOT EXISTS address_book ( + id INTEGER PRIMARY KEY, + uuid TEXT NOT NULL UNIQUE, + data TEXT NOT NULL)", + ) + .execute(&pool()) + .await?; + + Ok(()) +} + +pub async fn delete(uuid: &str) -> Result<()> { + sqlx::query("DELETE FROM address_book WHERE uuid=?") + .bind(uuid) + .execute(&pool()) + .await?; + Ok(()) +} + +pub async fn delete_all() -> Result<()> { + sqlx::query("DELETE FROM address_book").execute(&pool()).await?; + Ok(()) +} + +pub async fn insert(uuid: &str, data: &str) -> Result<()> { + sqlx::query("INSERT INTO address_book (uuid, data) VALUES (?, ?)") + .bind(uuid) + .bind(data) + .execute(&pool()) + .await?; + Ok(()) +} +pub async fn update(uuid: &str, data: &str) -> Result<()> { + sqlx::query("UPDATE address_book SET data=? WHERE uuid=?") + .bind(data) + .bind(uuid) + .execute(&pool()) + .await?; + + Ok(()) +} + +pub async fn select(uuid: &str) -> Result { + let pool = pool(); + let stream = sqlx::query_as::<_, AddressBook>("SELECT * FROM address_book WHERE uuid=?") + .bind(uuid) + .fetch_one(&pool); + + Ok(stream.await?) +} + +pub async fn select_all() -> Result> { + Ok(sqlx::query_as::<_, AddressBook>("SELECT * FROM address_book") + .fetch_all(&pool()) + .await?) +} + +#[allow(dead_code)] +pub async fn is_exist(uuid: &str) -> Result<()> { + select(uuid).await?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db; + + #[tokio::test] + async fn test_address_book_table_new() -> Result<()> { + db::init("/tmp/bitbox-test.db").await; + new().await + } + + #[tokio::test] + async fn test_address_book_table_delete_one() -> Result<()> { + db::init("/tmp/bitbox-test.db").await; + new().await?; + delete_all().await?; + insert("uuid-1", "data-1").await?; + delete("uuid-1").await + } + + #[tokio::test] + async fn test_address_book_table_delete_all() -> Result<()> { + db::init("/tmp/bitbox-test.db").await; + new().await?; + delete_all().await + } + + #[tokio::test] + async fn test_address_book_table_insert() -> Result<()> { + db::init("/tmp/bitbox-test.db").await; + new().await?; + delete_all().await?; + insert("uuid-1", "data-1").await?; + insert("uuid-2", "data-2").await + } + + #[tokio::test] + async fn test_address_book_table_update() -> Result<()> { + db::init("/tmp/bitbox-test.db").await; + new().await?; + delete_all().await?; + insert("uuid-1", "data-1").await?; + update("uuid-1", "data-1.1").await + } + + #[tokio::test] + async fn test_address_book_table_select_one() -> Result<()> { + db::init("/tmp/bitbox-test.db").await; + new().await?; + delete_all().await?; + assert!(select("uuid-1").await.is_err()); + + insert("uuid-1", "data-1").await?; + assert_eq!(select("uuid-1").await?.data, "data-1"); + Ok(()) + } + + #[tokio::test] + async fn test_address_book_table_select_all() -> Result<()> { + db::init("/tmp/bitbox-test.db").await; + new().await?; + delete_all().await?; + insert("uuid-1", "data-1").await?; + insert("uuid-2", "data-2").await?; + let address_books = select_all().await?; + + assert_eq!(address_books.len(), 2); + assert_eq!(address_books[0].uuid, "uuid-1"); + assert_eq!(address_books[1].uuid, "uuid-2"); + Ok(()) + } + + #[tokio::test] + async fn test_address_book_table_is_exist() -> Result<()> { + db::init("/tmp/bitbox-test.db").await; + new().await?; + delete_all().await?; + insert("uuid-1", "data-1").await?; + + assert!(is_exist("uuid-0").await.is_err()); + assert!(is_exist("uuid-1").await.is_ok()); + Ok(()) + } +} diff --git a/bitbox/src/db/mod.rs b/bitbox/src/db/mod.rs index 9757ba1..315b788 100644 --- a/bitbox/src/db/mod.rs +++ b/bitbox/src/db/mod.rs @@ -7,6 +7,7 @@ use sqlx::{ use std::sync::Mutex; pub mod account; +pub mod address_book; const MAX_CONNECTIONS: u32 = 3; @@ -34,6 +35,7 @@ async fn create_db(db_path: &str) -> Result<(), sqlx::Error> { pub async fn init(db_path: &str) { create_db(db_path).await.expect("create db"); account::new().await.expect("account new"); + address_book::new().await.expect("address_book new"); } #[allow(dead_code)] diff --git a/bitbox/src/logic/address_book.rs b/bitbox/src/logic/address_book.rs index 15dc1d4..61094d2 100644 --- a/bitbox/src/logic/address_book.rs +++ b/bitbox/src/logic/address_book.rs @@ -1,7 +1,15 @@ +use crate::db; +use crate::message::{async_message_success, async_message_warn}; use crate::slint_generatedAppWindow::{AddressBookItem, AppWindow, Logic, Store}; -use slint::{ComponentHandle, Model, VecModel}; +use crate::util::translator::tr; +use serde_json::{json, Value}; +use slint::{ComponentHandle, Model, VecModel, Weak}; +use tokio::task::spawn; +use uuid::Uuid; pub fn init(ui: &AppWindow) { + load_items(ui.as_weak()); + let ui_handle = ui.as_weak(); ui.global::() .on_address_book_delete_item(move |uuid| { @@ -21,7 +29,16 @@ pub fn init(ui: &AppWindow) { .expect("We know we set a VecModel earlier") .remove(index); - // TODO: remove data from the database + let (ui, uuid) = (ui.as_weak(), uuid.to_string()); + spawn(async move { + match db::address_book::delete(&uuid).await { + Ok(_) => async_message_success(ui.clone(), tr("删除成功")), + Err(e) => async_message_warn( + ui.clone(), + format!("{}. {}: {}", tr("删除失败"), tr("原因"), e), + ), + } + }); return; } } @@ -40,4 +57,79 @@ pub fn init(ui: &AppWindow) { slint::SharedString::default() }); + + let ui_handle = ui.as_weak(); + ui.global::() + .on_address_book_add_item(move |name, address| { + let ui = ui_handle.unwrap(); + let uuid = Uuid::new_v4().to_string(); + + let item = AddressBookItem { + uuid: uuid.clone().into(), + name: name, + address: address, + }; + + let json_item = json!({ + "uuid": uuid.clone(), + "name": item.name.to_string(), + "address": item.address.to_string() + }); + let json = serde_json::to_string(&json_item).unwrap(); + + ui.global::() + .get_address_book_datas() + .as_any() + .downcast_ref::>() + .expect("We know we set a VecModel earlier") + .push(item); + + let ui = ui.as_weak(); + spawn(async move { + match db::address_book::insert(&uuid, &json).await { + Ok(_) => async_message_success(ui.clone(), tr("添加成功")), + Err(e) => async_message_warn( + ui.clone(), + format!("{}. {}: {}", tr("添加失败"), tr("原因"), e), + ), + } + }); + }); +} + +fn load_items(ui: Weak) { + spawn(async move { + match db::address_book::select_all().await { + Ok(items) => { + let mut address_items = vec![]; + for item in items.iter() { + match serde_json::from_str::(&item.data) { + Err(e) => log::warn!("Error: {e:?}"), + Ok(value) => { + address_items.push(AddressBookItem { + uuid: value["uuid"].as_str().unwrap().into(), + name: value["name"].as_str().unwrap().into(), + address: value["address"].as_str().unwrap().into(), + }); + } + } + } + + let _ = slint::invoke_from_event_loop(move || { + ui.clone() + .unwrap() + .global::() + .get_address_book_datas() + .as_any() + .downcast_ref::>() + .expect("We know we set a VecModel earlier") + .set_vec(address_items); + }); + } + Err(e) => async_message_warn( + ui.clone(), + format!("{}. {}: {}", tr("加载失败"), tr("原因"), e), + ), + } + }); } diff --git a/bitbox/src/logic/message.rs b/bitbox/src/logic/message.rs index b4b5bf9..dea15d8 100644 --- a/bitbox/src/logic/message.rs +++ b/bitbox/src/logic/message.rs @@ -1,6 +1,6 @@ use crate::slint_generatedAppWindow::{AppWindow, Logic, MessageItem, Store}; use slint::ComponentHandle; -use slint::{Timer, TimerMode}; +use slint::{Timer, TimerMode, Weak}; #[macro_export] macro_rules! message_warn { @@ -18,6 +18,22 @@ macro_rules! message_success { }; } +pub fn async_message_warn(ui: Weak, msg: String) { + let _ = slint::invoke_from_event_loop(move || { + ui.unwrap() + .global::() + .invoke_show_message(slint::format!("{}", msg), "warning".into()); + }); +} + +pub fn async_message_success(ui: Weak, msg: String) { + let _ = slint::invoke_from_event_loop(move || { + ui.unwrap() + .global::() + .invoke_show_message(slint::format!("{}", msg), "success".into()); + }); +} + pub fn init(ui: &AppWindow) { let timer = Timer::default(); let ui_handle = ui.as_weak(); diff --git a/bitbox/src/logic/setting.rs b/bitbox/src/logic/setting.rs index eea74a6..f02897a 100644 --- a/bitbox/src/logic/setting.rs +++ b/bitbox/src/logic/setting.rs @@ -37,18 +37,24 @@ pub fn init(ui: &AppWindow) { .parse() .unwrap_or(18); config.ui.font_family = setting_config.ui.font_family.to_string(); - config.ui.win_width = setting_config - .ui - .win_width - .to_string() - .parse() - .unwrap_or(600); - config.ui.win_height = setting_config - .ui - .win_height - .to_string() - .parse() - .unwrap_or(800); + config.ui.win_width = u32::max( + setting_config + .ui + .win_width + .to_string() + .parse() + .unwrap_or(600), + 600, + ); + config.ui.win_height = u32::max( + setting_config + .ui + .win_height + .to_string() + .parse() + .unwrap_or(800), + 800, + ); config.ui.language = setting_config.ui.language.to_string(); @@ -81,8 +87,8 @@ fn init_setting_dialog(ui: Weak) { let mut setting_dialog = ui.global::().get_setting_dialog_config(); setting_dialog.ui.font_size = slint::format!("{}", ui_config.font_size); setting_dialog.ui.font_family = ui_config.font_family.into(); - setting_dialog.ui.win_width = slint::format!("{}", ui_config.win_width); - setting_dialog.ui.win_height = slint::format!("{}", ui_config.win_height); + setting_dialog.ui.win_width = slint::format!("{}", u32::max(ui_config.win_width, 600)); + setting_dialog.ui.win_height = slint::format!("{}", u32::max(ui_config.win_height, 800)); setting_dialog.ui.language = ui_config.language.into(); setting_dialog.proxy.enabled = socks5_config.enabled; diff --git a/bitbox/src/util/translator.rs b/bitbox/src/util/translator.rs index 7aca3c8..0789088 100644 --- a/bitbox/src/util/translator.rs +++ b/bitbox/src/util/translator.rs @@ -11,6 +11,8 @@ pub fn tr(text: &str) -> String { items.insert("原因", "Reason"); items.insert("删除成功", "Delete success"); items.insert("删除失败", "Delete failed"); + items.insert("添加成功", "Add success"); + items.insert("添加失败", "Add failed"); items.insert("复制失败", "Copy failed"); items.insert("复制成功", "Copy success"); items.insert("清空失败", "Delete failed"); @@ -22,6 +24,7 @@ pub fn tr(text: &str) -> String { items.insert("发送失败", "Send failed"); items.insert("下载成功", "Download success"); items.insert("下载失败", "Download failed"); + items.insert("加载失败", "Load failed"); items.insert("正在重试...", "Retrying..."); items.insert("正在下载...", "Downloading..."); items.insert("刷新...", "Flush..."); diff --git a/bitbox/ui/appwindow.slint b/bitbox/ui/appwindow.slint index dc85402..d7138cf 100644 --- a/bitbox/ui/appwindow.slint +++ b/bitbox/ui/appwindow.slint @@ -11,6 +11,7 @@ import { Blanket } from "./base/blanket.slint"; import { Panel } from "./panel/panel.slint"; import { Message } from "./base/message.slint"; import { SettingDialog } from "./dialog/setting/dialog.slint"; +import { AddressBookAddItemDialog } from "./dialog/address-book-add-item.slint"; import { AboutDialog } from "./dialog/about.slint"; import { HelpDialog } from "./dialog/help.slint"; import { OkCancelDialog } from "./dialog/ok-cancel.slint"; @@ -26,6 +27,8 @@ export component AppWindow inherits Window { forward-focus: fscope; title: "bitbox"; + property dialog-max-width: Math.min(root.width * 0.95, Theme.dialog-max-width); + init => { } fscope := FocusScope { @@ -60,23 +63,29 @@ export component AppWindow inherits Window { Store.about-dialog.show = false; } else if (help-dialog.visible) { Store.help-dialog.show = false; + } else if (address-book-add-item-dialog.visible) { + Store.is-show-address-book-add-item-dialog = false; } } - if setting-dialog.visible || oc-dialog.visible || about-dialog.visible || help-dialog.visible : low-modal := Blanket { } + if address-book-add-item-dialog.visible || setting-dialog.visible || oc-dialog.visible || about-dialog.visible || help-dialog.visible : low-modal := Blanket { } + + address-book-add-item-dialog := AddressBookAddItemDialog { + width: root.dialog-max-width; + } setting-dialog := SettingDialog { - width: root.width * 0.95; + width: root.dialog-max-width; } oc-dialog := OkCancelDialog {} about-dialog := AboutDialog { - width: root.width * 0.95; + width: root.dialog-max-width; } help-dialog := HelpDialog { - width: root.width * 0.95; + width: root.dialog-max-width; } } diff --git a/bitbox/ui/dialog/address-book-add-item.slint b/bitbox/ui/dialog/address-book-add-item.slint new file mode 100644 index 0000000..ad4f091 --- /dev/null +++ b/bitbox/ui/dialog/address-book-add-item.slint @@ -0,0 +1,72 @@ +import { LineEdit } from "std-widgets.slint"; +import { CDialog } from "../base/cdialog.slint"; +import { Label } from "../base/label.slint"; +import { Store } from "../store.slint"; +import { Util } from "../util.slint"; +import { Logic } from "../logic.slint"; +import { Theme } from "../theme.slint"; + +export component AddressBookAddItemDialog inherits CDialog { + in-out property name-text <=> name-lineedit.text; + in-out property address-text <=> address-lineedit.text; + + visible: Store.is-show-address-book-add-item-dialog; + title: Store.translator.address-book-add-item-dialog-title; + max-width: 500px; + + forward-focus: name-lineedit; + + callback close(); + + VerticalLayout { + padding: Theme.padding * 2; + spacing: Theme.spacing * 2; + + HorizontalLayout { + spacing: Theme.spacing * 2; + + VerticalLayout { + alignment: center; + name-text := Label { + width: Theme.default-label-width * 0.6; + text: Store.translator.name + ":"; + } + } + + name-lineedit := LineEdit { + accepted => { + root.ok-clicked(); + } + } + } + + HorizontalLayout { + spacing: Theme.spacing * 2; + + VerticalLayout { + alignment: center; + address-text := Label { + width: Theme.default-label-width * 0.6; + text: Store.translator.address + ":"; + } + } + + address-lineedit := LineEdit { } + } + } + + ok-clicked => { + if (name-lineedit.text != "" && address-lineedit.text != "") { + Logic.address-book-add-item(name-lineedit.text, address-lineedit.text); + } + name-lineedit.text = ""; address-lineedit.text = ""; + Store.is-show-address-book-add-item-dialog = false; + root.close(); + } + + cancel-clicked => { + name-lineedit.text = ""; address-lineedit.text = ""; + Store.is-show-address-book-add-item-dialog = false; + root.close(); + } +} diff --git a/bitbox/ui/dialog/setting/ui.slint b/bitbox/ui/dialog/setting/ui.slint index d2c7d4a..7e6f63c 100644 --- a/bitbox/ui/dialog/setting/ui.slint +++ b/bitbox/ui/dialog/setting/ui.slint @@ -31,8 +31,10 @@ export component UI inherits VerticalLayout { } HorizontalLayout { - spacing: Theme.spacing * 2; + alignment: space-between; + win-width-lineedit := LineEdit { + width: 40%; placeholder-text: Store.translator.setting-dialog-ui-win-W; } @@ -41,6 +43,7 @@ export component UI inherits VerticalLayout { } win-height-lineedit := LineEdit { + width: 40%; placeholder-text: Store.translator.setting-dialog-ui-win-H; } } diff --git a/bitbox/ui/images/qrcode.svg b/bitbox/ui/images/qrcode.svg new file mode 100644 index 0000000..0b03abf --- /dev/null +++ b/bitbox/ui/images/qrcode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/bitbox/ui/logic.slint b/bitbox/ui/logic.slint index c2c2395..7f284ca 100644 --- a/bitbox/ui/logic.slint +++ b/bitbox/ui/logic.slint @@ -14,8 +14,8 @@ export global Logic { callback flush-account(); callback switch-network(); - callback show-address-book-add-dialog(); - callback address_book-delete-item(string); // argument: uuid + callback address-book-add-item(string, string); // argument: name, address + callback address-book-delete-item(string); // argument: uuid callback address-book-item-address(string) -> string; // argument: uuid } diff --git a/bitbox/ui/panel/bodyer/tool/address-book.slint b/bitbox/ui/panel/bodyer/tool/address-book.slint index e783518..ff6aa1e 100644 --- a/bitbox/ui/panel/bodyer/tool/address-book.slint +++ b/bitbox/ui/panel/bodyer/tool/address-book.slint @@ -47,7 +47,18 @@ export component AddressBook inherits Rectangle { tip-pos: "left"; tip-text: Store.translator.tip-add; clicked => { - Logic.show-address-book-add-dialog(); + Store.is-show-address-book-add-item-dialog = true; + } + } + + IconBtn { + width: Theme.icon-size * 1.33; + icon-width: Theme.icon-size; + icon: @image-url("../../../images/qrcode.svg"); + tip-pos: "left"; + tip-text: Store.translator.tip-qrcode; + clicked => { + Store.is-show-address-book-qrcode-dialog = true; } } diff --git a/bitbox/ui/store.slint b/bitbox/ui/store.slint index 2c41711..07af304 100644 --- a/bitbox/ui/store.slint +++ b/bitbox/ui/store.slint @@ -73,6 +73,9 @@ export global Store { balance-usd: "1000000", }; + in-out property is-show-address-book-add-item-dialog: false; + in-out property is-show-address-book-qrcode-dialog: false; + in-out property<[AddressBookItem]> address-book-datas: [ { uuid: "uuid-1", diff --git a/bitbox/ui/theme.slint b/bitbox/ui/theme.slint index cc951bc..3eb7c1d 100644 --- a/bitbox/ui/theme.slint +++ b/bitbox/ui/theme.slint @@ -11,7 +11,7 @@ export global Theme { out property balance-height: 100px; out property border-radius: 4px; out property scroll-width: 12px; - out property max-image-width: 300px; + out property dialog-max-width: 600px; in-out property default-width: Store.setting-dialog-config.ui.win-width.to-float() * 1px; in-out property default-height: Store.setting-dialog-config.ui.win-height.to-float() * 1px; diff --git a/bitbox/ui/translator.slint b/bitbox/ui/translator.slint index 639ff65..3b1e85c 100644 --- a/bitbox/ui/translator.slint +++ b/bitbox/ui/translator.slint @@ -11,6 +11,7 @@ export struct Translation { about-btn: string, help-btn: string, name: string, + address: string, warning: string, delete-or-not: string, reset-or-not: string, @@ -56,6 +57,7 @@ export struct Translation { address-book-header-name: string, address-book-header-address: string, + address-book-add-item-dialog-title: string, tip-copy: string, tip-add: string, @@ -69,16 +71,17 @@ export struct Translation { tip-network: string, tip-main-network: string, tip-test-network: string, + tip-qrcode: string, tool-send: string, tool-receive: string, tool-activity: string, tool-address-book: string, + tip-help: string, tip-about: string, tip-setting: string, - tip-last-session-pos: string, tip-cur-session-pos: string, tip-edit: string, tip-up: string, @@ -108,6 +111,7 @@ export global Translator { about: is-cn ? "关 于" : "About", help: is-cn ? "帮 助" : "Help", name: is-cn ? "名 称" : "Name", + address: is-cn ? "地 址" : "Address", warning: is-cn ? "警 告" : "Warning", delete-or-not: is-cn ? "是否删除?" : "Are You Sure to Delete?", reset-or-not: is-cn ? "是否重置?" : "Reset or not?", @@ -148,10 +152,11 @@ export global Translator { no-message: is-cn ? "没有信息" : "No Message", btc-price: is-cn ? "BTC 价格" : "BTC Preice", - btc-per-byte-fee: is-cn ? "费用(s/B)" : "Fee(s/B)", + btc-per-byte-fee: is-cn ? "费用(sat/vB)" : "Fee(sat/vB)", address-book-header-name: is-cn ? "名称" : "Name", address-book-header-address: is-cn ? "地址" : "Address", + address-book-add-item-dialog-title: is-cn ? "添加地址" : "Add Address", tip-copy: is-cn ? "复制" : "Copy", tip-add: is-cn ? "添加" : "Add", @@ -165,6 +170,7 @@ export global Translator { tip-network: is-cn ? "网络" : "Network", tip-main-network: is-cn ? "主网络" : "Main Network", tip-test-network: is-cn ? "测试网络" : "Test Network", + tip-qrcode: is-cn ? "二维码" : "QrCode", tool-send: is-cn ? "发送" : "Send", tool-receive: is-cn ? "接收" : "Receive", @@ -175,7 +181,6 @@ export global Translator { tip-help: is-cn ? "帮助" : "Help", tip-about: is-cn ? "关于" : "About", tip-setting: is-cn ? "设置" : "Setting", - tip-last-session-pos: is-cn ? "上一次位置" : "Previous Position", tip-cur-session-pos: is-cn ? "当前位置" : "Current Position", tip-edit: is-cn ? "编辑" : "Edit", tip-up: is-cn ? "向上" : "Move Up",