From 4b2ca066ba5cc4ff0fdcd514f54c48c3cc964134 Mon Sep 17 00:00:00 2001 From: blueJpg <2238288979@qq.com> Date: Sat, 6 Jul 2024 15:23:13 +0800 Subject: [PATCH] [+] add home page --- src/db/def.rs | 2 +- src/logic/history.rs | 44 ++++++---- src/logic/tr.rs | 1 + ui/base/account-balance.slint | 41 +++++++++ ui/base/elevated-btn.slint | 2 +- ui/base/icon-btn.slint | 14 +++- ui/base/list-tile.slint | 1 + ui/base/token-list.slint | 21 +++++ ui/base/token-tile.slint | 105 +++++++++++++++++++++++ ui/base/transaction-tile.slint | 9 +- ui/base/widgets.slint | 12 ++- ui/images/management.svg | 1 + ui/logic.slint | 13 ++- ui/panel/bodyer/history.slint | 8 ++ ui/panel/bodyer/home.slint | 142 ++++++++++++++++++++++++++++++-- ui/panel/bodyer/recipient.slint | 1 + ui/store.slint | 13 ++- ui/theme.slint | 1 + 18 files changed, 393 insertions(+), 38 deletions(-) create mode 100644 ui/base/account-balance.slint create mode 100644 ui/base/token-list.slint create mode 100644 ui/base/token-tile.slint create mode 100644 ui/images/management.svg diff --git a/src/db/def.rs b/src/db/def.rs index b38d56b..88059c5 100644 --- a/src/db/def.rs +++ b/src/db/def.rs @@ -102,7 +102,7 @@ impl SerializeAs for TranStatus { let status = match source { TransactionTileStatus::Success => "Success", TransactionTileStatus::Pending => "Pending", - TransactionTileStatus::Error => "Error", + _ => "Error", }; serializer.serialize_str(status) diff --git a/src/logic/history.rs b/src/logic/history.rs index 98ddca7..3989c77 100644 --- a/src/logic/history.rs +++ b/src/logic/history.rs @@ -14,7 +14,7 @@ use crate::{ }; use anyhow::Result; use cutil::time::local_now; -use slint::{ComponentHandle, Model, SharedString, VecModel, Weak}; +use slint::{ComponentHandle, Model, SharedString, VecModel}; use std::str::FromStr; use uuid::Uuid; use wallet::{ @@ -152,14 +152,16 @@ pub fn init(ui: &AppWindow) { let ui_handle = ui.as_weak(); ui.global::() - .on_update_history_status(move |uuid, status| { + .on_update_history_status(move |uuid, status, is_update_db| { let ui = ui_handle.unwrap(); if let Some((index, mut entry)) = get_entry(&ui, &uuid) { entry.status = status; store_history_entries!(ui).set_row_data(index, entry.clone()); - _update_entry(entry.into()); + if is_update_db { + _update_entry(entry.into()); + } } }); @@ -167,9 +169,12 @@ pub fn init(ui: &AppWindow) { ui.global::() .on_refresh_all_pending_and_error_history(move || { let ui = ui_handle.unwrap(); - message_info!(ui, tr("正在刷新...")); - for item in get_pending_and_error_entries(&ui).into_iter() { - _refresh_pending_and_error_history(ui.as_weak(), item); + for (index, item) in get_pending_and_error_entries(&ui).into_iter().enumerate() { + if index == 0 { + message_info!(ui, tr("正在刷新...")); + } + + _refresh_pending_and_error_history(&ui, item); } }); @@ -240,8 +245,16 @@ fn _remove_entry(uuid: SharedString) { }); } -fn _refresh_pending_and_error_history(ui_handle: Weak, item: UIHistoryEntry) { +fn _refresh_pending_and_error_history(ui: &AppWindow, item: UIHistoryEntry) { + ui.global::().invoke_update_history_status( + item.uuid.clone(), + TransactionTileStatus::Loading, + false, + ); + let rpc_url_ty = RpcUrlType::from_str(&item.network).unwrap_or(RpcUrlType::Main); + + let ui_handle = ui.as_weak(); match Signature::from_str(&item.hash) { Ok(signature) => { tokio::spawn(async move { @@ -259,17 +272,14 @@ fn _refresh_pending_and_error_history(ui_handle: Weak, item: UIHistor } }; - if status != item.status { - let ui_handle = ui_handle.clone(); - _ = slint::invoke_from_event_loop(move || { - ui_handle - .unwrap() - .global::() - .invoke_update_history_status(item.uuid, status); - }); - } + let ui = ui_handle.clone(); + _ = slint::invoke_from_event_loop(move || { + ui.unwrap() + .global::() + .invoke_update_history_status(item.uuid, status, true); + }); - async_message_success(ui_handle, tr("刷新成功")); + async_message_success(ui_handle, tr("刷新完成")); }); } Err(e) => message_warn!(ui_handle.unwrap(), e.to_string()), diff --git a/src/logic/tr.rs b/src/logic/tr.rs index 94405f4..38796df 100644 --- a/src/logic/tr.rs +++ b/src/logic/tr.rs @@ -235,6 +235,7 @@ pub fn tr(text: &str) -> String { items.insert("移除账户", "Remove account"); items.insert("更改密码", "Change password"); items.insert("位组记词", "mnemonics"); + items.insert("刷新完成", "Refresh finished"); items.insert( "创建账户和使用组记词恢复账户", diff --git a/ui/base/account-balance.slint b/ui/base/account-balance.slint new file mode 100644 index 0000000..cb4e905 --- /dev/null +++ b/ui/base/account-balance.slint @@ -0,0 +1,41 @@ +import { Theme, Icons } from "../theme.slint"; +import { IconBtn } from "icon-btn.slint"; +import { TextBtn } from "btn.slint"; +import { Label } from "label.slint"; +import { Link } from "link.slint"; + +export component AccountBalance inherits Rectangle { + in-out property account-name <=> account.text; + in-out property balance <=> balance-label.text; + + callback account-clicked <=> account.clicked; + callback copy <=> copy-btn.clicked; + + VerticalLayout { + padding-top: Theme.padding * 5; + padding-bottom: Theme.padding * 5; + spacing: Theme.spacing * 2; + alignment: LayoutAlignment.start; + + HorizontalLayout { + alignment: LayoutAlignment.center; + spacing: Theme.spacing * 2; + + account := Link { + horizontal-alignment: TextHorizontalAlignment.center; + font-weight: 0.5; + font-size: Theme.title4-font-size; + } + + copy-btn := IconBtn { + icon: Icons.copy; + } + } + + balance-label := Label { + horizontal-alignment: TextHorizontalAlignment.center; + font-weight: 0.5; + font-size: Theme.title1-font-size * 2; + } + } +} diff --git a/ui/base/elevated-btn.slint b/ui/base/elevated-btn.slint index 8754ec8..bf31414 100644 --- a/ui/base/elevated-btn.slint +++ b/ui/base/elevated-btn.slint @@ -11,7 +11,7 @@ export component ElevatedBtn inherits Rectangle { drop-shadow-color: Theme.secondary-background-drop-shadow; in-out property icon <=> btn.icon; - in-out property enable-rotation-animation <=> btn.enable-rotation-animation; + in-out property rotation-type <=> btn.rotation-type; callback clicked <=> btn.clicked; diff --git a/ui/base/icon-btn.slint b/ui/base/icon-btn.slint index 51f1848..4422135 100644 --- a/ui/base/icon-btn.slint +++ b/ui/base/icon-btn.slint @@ -2,6 +2,12 @@ import { Palette } from "std-widgets.slint"; import { Theme } from "../theme.slint"; import { Label } from "./label.slint"; +export enum RotationType { + Auto, + Click, + None, +} + export component IconBtn inherits Rectangle { in-out property icon <=> img.source; in-out property colorize <=> img.colorize; @@ -9,17 +15,18 @@ export component IconBtn inherits Rectangle { in-out property icon-size <=> img.width; in-out property icon-rotation-angle <=> img.rotation-angle; in-out property mouse-cursor <=> touch.mouse-cursor; - in-out property enabled-toucharea<=> touch.enabled; + in-out property enabled-toucharea <=> touch.enabled; out property has-hover <=> touch.has-hover; in-out property show-icon-hover-background: true; in-out property is-ltr: true; - in-out property enable-rotation-animation: false; + in-out property rotation-type: RotationType.None; in-out property use-auto-size: false; in-out property text; in-out property font-size: Theme.title4-font-size; in-out property text-color: root.colorize; + in-out property icon-rotation-iteration-count; callback clicked(); @@ -65,6 +72,7 @@ export component IconBtn inherits Rectangle { animate rotation-angle { duration: Theme.default-animate-duration; easing: ease-in-out; + iteration-count: root.rotation-type == RotationType.Auto ? -1 : root.icon-rotation-iteration-count; } } @@ -80,7 +88,7 @@ export component IconBtn inherits Rectangle { touch := TouchArea { mouse-cursor: MouseCursor.pointer; clicked => { - if (enable-rotation-animation) { + if (RotationType.Click == root.rotation-type) { if (icon-rotation-angle == 0) { icon-rotation-angle = 360deg; } else { diff --git a/ui/base/list-tile.slint b/ui/base/list-tile.slint index 6c32cec..8a7e2e7 100644 --- a/ui/base/list-tile.slint +++ b/ui/base/list-tile.slint @@ -27,6 +27,7 @@ import { Label } from "./label.slint"; export component ListTile inherits Rectangle { height: vbox.preferred-height; + background: Theme.base-background; callback left-clicked(); callback right-clicked(); diff --git a/ui/base/token-list.slint b/ui/base/token-list.slint new file mode 100644 index 0000000..1de8726 --- /dev/null +++ b/ui/base/token-list.slint @@ -0,0 +1,21 @@ +import { Theme } from "../theme.slint"; +import { TokenTile, TokenTileEntry } from "token-tile.slint"; +import { SettingDetailInner } from "setting-detail.slint"; + + +export component TokenList inherits SettingDetailInner { + in-out property <[TokenTileEntry]> entries; + + vbox-alignment: LayoutAlignment.start; + vbox-spacing: Theme.spacing * 2; + + callback clicked(TokenTileEntry); + + for item[index] in entries: TokenTile { + entry: item; + + clicked => { + root.clicked(item); + } + } +} diff --git a/ui/base/token-tile.slint b/ui/base/token-tile.slint new file mode 100644 index 0000000..c26e64f --- /dev/null +++ b/ui/base/token-tile.slint @@ -0,0 +1,105 @@ +import { Switch } from "std-widgets.slint"; +import { Theme } from "../theme.slint"; +import { ListTile } from "list-tile.slint"; +import { Label } from "label.slint"; +import { IconBtn } from "icon-btn.slint"; + +export struct TokenTileEntry { + uuid: string, + icon: image, + symbol: string, + mint-address: string, + balance: string, + balance-usdt: string, +} + +component Tile inherits Rectangle { + in-out property entry; + + in property icon-size: Theme.icon-size; + in property icon-colorize; + + in property symbol-font-size: Theme.title3-font-size; + in property balance-font-size: Theme.default-font-size; + in property balance-usdt-font-size: Theme.title3-font-size; + + out property has-hover: ta.has-hover; + + callback clicked <=> ta.clicked; + + height: hbox.preferred-height; + background: has-hover ? Theme.base-background.darker(3%) : Theme.base-background; + border-radius: Theme.border-radius; + drop-shadow-blur: Theme.padding * 2; + drop-shadow-color: Theme.base-background-drop-shadow; + + ta := TouchArea { + mouse-cursor: MouseCursor.pointer; + } + + hbox := HorizontalLayout { + padding: Theme.padding * 2; + spacing: Theme.spacing * 2; + alignment: LayoutAlignment.space-between; + + HorizontalLayout { + horizontal-stretch: 1; + spacing: Theme.spacing * 2; + + VerticalLayout { + alignment: LayoutAlignment.center; + Rectangle { + width: root.icon-size + Theme.padding * 4; + height: self.width; + + img := Image { + height: root.icon-size; + width: self.height; + source: entry.icon; + colorize: root.icon-colorize; + } + } + } + + VerticalLayout { + alignment: LayoutAlignment.space-between; + spacing: Theme.spacing * 2; + padding-top: Theme.padding; + padding-bottom: Theme.padding; + + Label { + font-size: root.symbol-font-size; + text: entry.symbol; + wrap: TextWrap.no-wrap; + overflow: TextOverflow.elide; + font-weight: 0.5; + } + + Label { + font-size: root.balance-font-size; + text: entry.balance; + wrap: TextWrap.no-wrap; + overflow: TextOverflow.elide; + } + } + } + + @children + } +} + +export component TokenTile inherits Tile { + Label { + font-size: root.balance-usdt-font-size; + text: root.entry.balance-usdt; + } +} + +export component TokenTileWithSwitch inherits Rectangle { + in-out property checked <=> sw.checked; + + callback toggled <=> sw.toggled; + + sw := Switch { + } +} diff --git a/ui/base/transaction-tile.slint b/ui/base/transaction-tile.slint index 53820f2..5b10868 100644 --- a/ui/base/transaction-tile.slint +++ b/ui/base/transaction-tile.slint @@ -1,11 +1,12 @@ import { Theme, Icons } from "../theme.slint"; -import { IconBtn } from "./icon-btn.slint"; +import { IconBtn, RotationType } from "./icon-btn.slint"; import { Label } from "./label.slint"; export enum TransactionTileStatus { Success, Pending, Error, + Loading, } export struct TransactionTileEntry { @@ -34,6 +35,8 @@ export component TransactionTile inherits Rectangle { return Icons.success-big-fill; } else if (status == TransactionTileStatus.Pending) { return Icons.pending-fill; + } else if (status == TransactionTileStatus.Loading) { + return Icons.loading; } else { return Icons.error-fill; } @@ -44,6 +47,8 @@ export component TransactionTile inherits Rectangle { return Theme.success-color; } else if (status == TransactionTileStatus.Pending) { return Theme.warning-color; + } else if (status == TransactionTileStatus.Loading) { + return Theme.secondary-brand-color; } else { return Theme.danger-color; } @@ -102,6 +107,8 @@ export component TransactionTile inherits Rectangle { icon-size: root.icon-size; colorize: root.icon-colorize; icon: root.icon; + rotation-type: TransactionTileStatus.Loading == entry.status ? RotationType.Auto : RotationType.None; + icon-rotation-angle: TransactionTileStatus.Loading == entry.status ? 240deg * animation-tick() / 0.5s : 0; } } } diff --git a/ui/base/widgets.slint b/ui/base/widgets.slint index c970420..4001758 100644 --- a/ui/base/widgets.slint +++ b/ui/base/widgets.slint @@ -1,6 +1,6 @@ import { Toast } from "toast.slint"; import { TabBtn } from "tab-btn.slint"; -import { IconBtn } from "icon-btn.slint"; +import { IconBtn, RotationType } from "icon-btn.slint"; import { Label } from "label.slint"; import { Tag } from "tag.slint"; import { CenterLayout } from "center-layout.slint"; @@ -40,11 +40,15 @@ import { About, AboutSetting } from "about.slint"; import { Help } from "help.slint"; import { ResetPassword } from "reset-password.slint"; import { Banner } from "banner.slint"; +import { AccountBalance } from "account-balance.slint"; +import { TokenTile, TokenTileEntry } from "token-tile.slint"; +import { TokenList } from "token-list.slint"; export { Toast, TabBtn, IconBtn, + RotationType, ElevatedBtn, Label, CenterLayout, @@ -100,5 +104,9 @@ export { AboutSetting, Help, ResetPassword, - Banner + Banner, + AccountBalance, + TokenTile, + TokenTileEntry, + TokenList } diff --git a/ui/images/management.svg b/ui/images/management.svg new file mode 100644 index 0000000..88aecfd --- /dev/null +++ b/ui/images/management.svg @@ -0,0 +1 @@ + diff --git a/ui/logic.slint b/ui/logic.slint index d04bd71..f089254 100644 --- a/ui/logic.slint +++ b/ui/logic.slint @@ -66,8 +66,8 @@ export global Logic { // (uuid) -> void callback remove-history(string); - // (uuid, status) -> void - callback update-history-status(string, TransactionTileStatus); + // (uuid, status, is-update-db) -> void + callback update-history-status(string, TransactionTileStatus, bool); callback refresh-all-pending-and-error-history(); @@ -80,6 +80,15 @@ export global Logic { // (network) -> void callback switch-history-network(string); + // (network, address) -> void + callback address-balance(string, string); + + // (network, address) -> void + callback open-tokens-management(string, string); + + // (network, uuid) -> void + callback open-send-token(string, string); + callback update-cache-size(); callback remove-all-cache(); diff --git a/ui/panel/bodyer/history.slint b/ui/panel/bodyer/history.slint index 378ed7b..37f73b5 100644 --- a/ui/panel/bodyer/history.slint +++ b/ui/panel/bodyer/history.slint @@ -26,6 +26,14 @@ export global HistorySetting { balance: "$100.12", time: "2024-12-04 12:06:09", status: TransactionTileStatus.Error, + }, + { + uuid: "uuid-4", + hash: "hash-1", + hash: "hash-1", + balance: "$100.12", + time: "2024-12-04 12:06:09", + status: TransactionTileStatus.Loading, } ]; } diff --git a/ui/panel/bodyer/home.slint b/ui/panel/bodyer/home.slint index 6cfdca0..f474c41 100644 --- a/ui/panel/bodyer/home.slint +++ b/ui/panel/bodyer/home.slint @@ -1,9 +1,66 @@ import { Theme, Icons } from "../../theme.slint"; -import { Store } from "../../store.slint"; +import { Store, HomeIndex } from "../../store.slint"; import { Logic } from "../../logic.slint"; -import { Head, Banner } from "../../base/widgets.slint"; +import { AccountBalance, Head, IconBtn, TextBtn, Banner, Label, Link, SettingDetail, SettingDetailInner, TokenTile, TokenTileEntry, TokenList } from "../../base/widgets.slint"; import { TestModeBanner } from "test-mode-banner.slint"; +export global TokensSetting { + in-out property <[TokenTileEntry]> entries: [ + { + uuid: "uuid-1", + icon: Icons.account, + symbol: "SOL", + balance: "100.34", + balance-usdt: "$12321.34" + }, + { + uuid: "uuid-2", + icon: Icons.account, + symbol: "USDT", + balance: "100.34", + balance-usdt: "$100.34", + }, + { + uuid: "uuid-3", + icon: Icons.account, + symbol: "DOG", + balance: "120.34", + balance-usdt: "$1100.4", + }, + ]; + + in-out property <[TokenTileEntry]> all-account-tokens:[ + { + uuid: "uuid-1", + icon: Icons.account, + symbol: "SOL", + balance: "100.34", + balance-usdt: "$12321.34" + }, + { + uuid: "uuid-2", + icon: Icons.account, + symbol: "USDT", + balance: "100.34", + balance-usdt: "$100.34", + }, + { + uuid: "uuid-3", + icon: Icons.account, + symbol: "DOG", + balance: "120.34", + balance-usdt: "$1100.4", + }, + { + uuid: "uuid-3", + icon: Icons.account, + symbol: "BOOM", + balance: "130.34", + balance-usdt: "$1130.8", + }, + ]; +} + component TopHead inherits Head { icon: Icons.crypto; icon-width: Theme.icon-size * 1.2; @@ -11,16 +68,85 @@ component TopHead inherits Head { hbox-alignment: LayoutAlignment.start; } -component Body inherits Rectangle { } +component AccountDetail inherits AccountBalance { + account-name: Store.current-account.name; + balance: Store.current-account.balance; + + account-clicked => { + Logic.open-account-detail(Logic.get-current-network(), Store.current-account.pubkey); + } + + copy => { + Logic.copy-to-clipboard(Store.current-account.pubkey); + } +} + +component ManagementBtn inherits VerticalLayout { + padding-bottom: Theme.padding * 2; + + btn := TextBtn { + icon: Icons.management; + text: Logic.tr(Store.is-cn, "管理代币"); + clicked => { + Logic.open-tokens-management(Logic.get-current-network(), Store.current-account.pubkey); + Store.current-home-index = HomeIndex.Management; + } + } +} + +component Management inherits SettingDetail { + title: Logic.tr(Store.is-cn, "管理通证"); + SettingDetailInner { } +} -export component Home inherits VerticalLayout { +component Send inherits SettingDetail { + in-out property entry; + + title: Logic.tr(Store.is-cn, "发送代币"); + SettingDetailInner { } +} + +component Body inherits Rectangle { + VerticalLayout { + spacing: Theme.spacing * 2; + + AccountDetail { } + + TokenList { + entries: TokensSetting.entries; + clicked(entry) => { + Logic.open-send-token(Logic.get-current-network(), entry.uuid); + Store.current-home-index = HomeIndex.Send; + } + } + + ManagementBtn { } + } +} + +export component Home inherits Rectangle { private property network: Logic.get-current-network(); + private property current-entry; + + if Store.current-home-index == HomeIndex.Home: VerticalLayout { + head := TopHead { } - head := TopHead { } + if network != "main": TestModeBanner { + network: root.network; + } - if network != "main": TestModeBanner { - network: root.network; + Body { } } - body := Body { } + if Store.current-home-index == HomeIndex.Management: Management { + back => { + Store.current-home-index = HomeIndex.Home; + } + } + + if Store.current-home-index == HomeIndex.Send: Send { + back => { + Store.current-home-index = HomeIndex.Home; + } + } } diff --git a/ui/panel/bodyer/recipient.slint b/ui/panel/bodyer/recipient.slint index 072185d..6a01b77 100644 --- a/ui/panel/bodyer/recipient.slint +++ b/ui/panel/bodyer/recipient.slint @@ -21,6 +21,7 @@ component Body inherits SettingDetailInner { Label { font-size: Theme.title3-font-size; + font-weight: 0.5; text: Store.current-account.name; } } diff --git a/ui/store.slint b/ui/store.slint index fd0abd8..45795b8 100644 --- a/ui/store.slint +++ b/ui/store.slint @@ -30,6 +30,12 @@ export enum SetupIndex { SetPassword, } +export enum HomeIndex { + Home, + Send, + Management, +} + export struct AccountEntry { uuid: string, name: string, @@ -57,8 +63,9 @@ export struct SettingDeveloperMode { } export global Store { + in-out property current-home-index: HomeIndex.Home; in-out property current-setup-index: SetupIndex.SetupType; - in-out property current-tab-index: TabIndex.History; + in-out property current-tab-index: TabIndex.Home; private property current-tab-index-tmp: TabIndex.None; in-out property current-setting-detail-index: SettingDetailIndex.Home; @@ -68,7 +75,7 @@ export global Store { pubkey: "pubkey-test", derive-index: 0, avatar-index: 0, - balance: "100.21", + balance: "$100.21", }; in-out property <[AccountEntry]> accounts: [ @@ -78,7 +85,7 @@ export global Store { pubkey: "pubkey-test", derive-index: 0, avatar-index: 0, - balance: "100.21", + balance: "$100.21", } ]; diff --git a/ui/theme.slint b/ui/theme.slint index 5c4f537..e66b984 100644 --- a/ui/theme.slint +++ b/ui/theme.slint @@ -144,6 +144,7 @@ export global Icons { out property reset-in-trash: @image-url("./images/reset-in-trash.svg"); out property debug-light : @image-url("./images/debug-light.svg"); out property add-light : @image-url("./images/add-light.svg"); + out property management : @image-url("./images/management.svg"); out property wechat-fill: @image-url("./images/wechat-fill.svg"); out property wechat-light: @image-url("./images/wechat-light.svg");