Compare commits

..

1 Commits
master ... dev

Author SHA1 Message Date
f410df3bad commit one version
小范围测试开始
2025-07-04 13:18:41 +08:00
12 changed files with 722 additions and 159 deletions

View File

@ -11,7 +11,10 @@ iced = { git = "https://github.com/iced-rs/iced.git", features = [
"image", "image",
"sipper", "sipper",
"tokio", "tokio",
"unconditional-rendering"
] } ] }
iced_renderer = {version = "0.13.0",features = ["wgpu","tiny-skia"]}
iced_aw = {git = "https://github.com/iced-rs/iced_aw.git",features = ["menu"]}
tokio = { version = "1.45.1", features = ["full"] } tokio = { version = "1.45.1", features = ["full"] }
reqwest = "0.12.20" reqwest = "0.12.20"
tracing-subscriber = { version = "0.3.19", features = [ tracing-subscriber = { version = "0.3.19", features = [
@ -49,7 +52,9 @@ num_enum = "0.7.4"
trace = "0.1.7" trace = "0.1.7"
tracing = "0.1.41" tracing = "0.1.41"
log = "0.4.27" log = "0.4.27"
dirs = "6.0.0"
[build-dependencies] [build-dependencies]
embed-resource = "3.0.3" embed-resource = "3.0.3"

View File

@ -14,7 +14,9 @@
## 待办 ## 待办
* 下载成功的日志以Toast消息形式呈现
* 软件更新以消息弹窗的形式呈现
* 关注iced_aw项目该项目与现有iced版本并不兼容期待更新
## 周期性待办 ## 周期性待办
@ -23,3 +25,8 @@
## 忐忑 ## 忐忑
* 本软件的3D封装下载调用了jlc的api不知道哪天就收到了某函所以暂时只在本站开源了在未想办法解决掉该可能引起纠纷的事项之前不想广泛传播所以也请各位道友手下留情不要随意传播本软件 * 本软件的3D封装下载调用了jlc的api不知道哪天就收到了某函所以暂时只在本站开源了在未想办法解决掉该可能引起纠纷的事项之前不想广泛传播所以也请各位道友手下留情不要随意传播本软件
## 笔记
* 如需窗口透明可以参考gradient例程
* 加载动画参见loading_spinners例程

View File

@ -1,18 +1,11 @@
#![windows_subsystem = "windows"]
mod ui; mod ui;
mod utils; mod utils;
mod widgets; mod widgets;
fn main() { fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
unsafe {
use utils::winsparkle::*;
win_sparkle_set_appcast_url(
"https://dl.wuembed.com/hardware_tk/appcast.xml\0".as_ptr() as *const i8
);
// win_sparkle_set_eddsa_public_key(
// "pXr0FyLTCvtX2BP7d/i3Ot8T9hL+ODBQforwfBp2oLo=\0".as_ptr() as *const i8
// );
win_sparkle_init();
}
ui::main_window::main_window(); ui::main_window::main_window();
} }

View File

@ -1,18 +1,27 @@
use iced::{alignment::Horizontal, widget::Column, Length, Task}; use iced::{Length, Task, alignment::Horizontal, widget::Column};
use tracing::info; use tracing::info;
use crate::utils::winsparkle; use crate::widgets::toast;
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::ui::main_window::{MainWindowMsg, TabContent}; use crate::ui::main_window::{MainWindowMsg, TabContent};
pub struct HomePage { pub struct HomePage {
step_dir: String, pub step_dir: String,
pub theme: iced::Theme, pub theme: iced::Theme,
} }
impl Default for HomePage { impl Default for HomePage {
fn default() -> Self { fn default() -> Self {
let mut step_dir = String::new();
if let Ok(dir) = crate::utils::app_settings::get_step_dir(){
step_dir = dir;
}else{
if let Some(path) = dirs::download_dir(){
step_dir = path.to_str().unwrap().to_string();
}else{
step_dir = ".\\".to_string();
}
};
Self { Self {
step_dir: crate::utils::app_settings::get_step_dir().unwrap(), step_dir,
theme: Default::default(), theme: Default::default(),
} }
} }
@ -27,6 +36,38 @@ pub enum HomePageMsg {
} }
impl TabContent for HomePage { impl TabContent for HomePage {
type TabMessage = HomePageMsg;
fn update(&mut self, msg: Self::TabMessage) -> Task<MainWindowMsg> {
match msg {
HomePageMsg::Nothing => {
info!("This way ok.");
}
HomePageMsg::OpenStepDir => {
info!("To open the dir.");
let _ = std::process::Command::new(r"explorer.exe")
.arg(self.step_dir.as_str())
.output()
.unwrap()
.stdout;
}
HomePageMsg::ChooseStepDir => {
info!("To choose the step dir.");
if let Some(path) = rfd::FileDialog::new().pick_folder() {
let path = path.to_str();
if let Some(path) = path {
self.step_dir = path.to_string();
let _ = crate::utils::app_settings::set_step_dir(path.to_string().as_str());
}
}
}
HomePageMsg::CheckUpdate => {
info!("To check update.");
}
}
Task::none()
}
fn content(&self) -> iced::Element<'_, MainWindowMsg> { fn content(&self) -> iced::Element<'_, MainWindowMsg> {
let info = iced::widget::row![ let info = iced::widget::row![
iced::widget::text("版本:"), iced::widget::text("版本:"),
@ -64,41 +105,6 @@ impl TabContent for HomePage {
.spacing(5.0) .spacing(5.0)
.into() .into()
} }
type TabMessage = HomePageMsg;
fn update(&mut self, msg: Self::TabMessage) -> Task<MainWindowMsg> {
match msg {
HomePageMsg::Nothing => {
info!("This way ok.");
}
HomePageMsg::OpenStepDir => {
info!("To open the dir.");
let _ = std::process::Command::new(r"explorer.exe")
.arg(self.step_dir.as_str())
.output()
.unwrap()
.stdout;
}
HomePageMsg::ChooseStepDir => {
info!("To choose the step dir.");
if let Some(path) = rfd::FileDialog::new().pick_folder() {
let path = path.to_str();
if let Some(path) = path {
self.step_dir = path.to_string();
let _ = crate::utils::app_settings::set_step_dir(path.to_string().as_str());
}
}
}
HomePageMsg::CheckUpdate => {
info!("To check update.");
unsafe{
winsparkle::win_sparkle_check_update_with_ui_and_install();
}
}
}
Task::none()
}
} }
impl HomePage { impl HomePage {
pub fn set_theme(&mut self, theme: iced::Theme) { pub fn set_theme(&mut self, theme: iced::Theme) {

View File

@ -1,9 +1,11 @@
use crate::ui::main_window::{MainWindowMsg, TabContent}; use crate::ui::main_window::{MainWindowMsg, TabContent};
use crate::utils::step_downloader::{self as downloader, FetchResultItem, SearchResultItem}; use crate::utils::step_downloader::{self as downloader, FetchResultItem, SearchResultItem};
use crate::widgets::toast;
#[allow(unused_imports)] #[allow(unused_imports)]
use anyhow::Result; use anyhow::Result;
use iced::widget::{Row, button, keyed_column}; use iced::widget::{Row, button, keyed_column};
use iced::{Element, Length, Task, alignment::Horizontal, widget::Column}; use iced::{Element, Length, Task, alignment::Horizontal, widget::Column};
use tokio::io::AsyncWriteExt;
use tracing::info; use tracing::info;
pub struct JlcDownloader { pub struct JlcDownloader {
@ -16,10 +18,10 @@ pub struct JlcDownloader {
msg_disp: String, msg_disp: String,
step_data: String, step_data: String,
theme: iced::Theme, theme: iced::Theme,
item_clickable:bool, item_clickable: bool,
current_step_content:String, current_step_content: String,
current_step_name:String, current_step_name: String,
download_log:Vec<String>, pub step_save_dir: String,
} }
impl JlcDownloader { impl JlcDownloader {
pub fn set_theme(&mut self, theme: iced::Theme) { pub fn set_theme(&mut self, theme: iced::Theme) {
@ -50,7 +52,7 @@ impl JlcDownloader {
let theme = self.theme.clone(); let theme = self.theme.clone();
let palette = theme.extended_palette(); let palette = theme.extended_palette();
let info = iced::widget::column![ let info = iced::widget::column![
iced::widget::text(format!("元件:{}",item.name)), iced::widget::text(format!("元件:{}", item.name)),
iced::widget::text(format!("编号:{}", item.code)), iced::widget::text(format!("编号:{}", item.code)),
iced::widget::text(format!("模型:{}", item.model)), iced::widget::text(format!("模型:{}", item.model)),
] ]
@ -61,15 +63,15 @@ impl JlcDownloader {
.push(iced::widget::Image::new(img.clone()).width(Length::FillPortion(1))); .push(iced::widget::Image::new(img.clone()).width(Length::FillPortion(1)));
} }
btn_content = btn_content.push(info); btn_content = btn_content.push(info);
if self.item_clickable{ if self.item_clickable {
let attr = DownloadAttr{ let attr = DownloadAttr {
name:item.name.clone(), name: item.name.clone(),
id:item.model_id.clone(), id: item.model_id.clone(),
}; };
iced::widget::Button::new(btn_content).on_press(MainWindowMsg::JlcDownloader( iced::widget::Button::new(btn_content).on_press(MainWindowMsg::JlcDownloader(
JlcDownloaderMsg::PartClicked(attr), JlcDownloaderMsg::PartClicked(attr),
)) ))
}else{ } else {
iced::widget::Button::new(btn_content) iced::widget::Button::new(btn_content)
} }
} }
@ -89,7 +91,7 @@ impl Default for JlcDownloader {
item_clickable: true, item_clickable: true,
current_step_content: "".to_string(), current_step_content: "".to_string(),
current_step_name: "".to_string(), current_step_name: "".to_string(),
download_log: vec![], step_save_dir: String::new(),
} }
} }
} }
@ -108,10 +110,10 @@ pub enum JlcDownloaderMsg {
OpenDatasheet(String), OpenDatasheet(String),
SearchPart, SearchPart,
} }
#[derive(Debug,Clone,PartialEq,Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
struct DownloadAttr{ struct DownloadAttr {
name:String, name: String,
id:String, id: String,
} }
impl TabContent for JlcDownloader { impl TabContent for JlcDownloader {
type TabMessage = JlcDownloaderMsg; type TabMessage = JlcDownloaderMsg;
@ -179,12 +181,60 @@ impl TabContent for JlcDownloader {
}); });
} }
JlcDownloaderMsg::StepFetched(s) => { JlcDownloaderMsg::StepFetched(s) => {
self.download_log.push(format!("3D for {} 下载成功",self.current_step_name)); // MainWindowMsg::Toast(toast::Toast{title:"下载成功".into(),body:format!("{}的模型下载成功",s),status:toast::Status::Success});
self.item_clickable = true; self.item_clickable = true;
let model_name = self.current_step_name.clone();
let mut step_dir = String::new();
if let Ok(dir) = crate::utils::app_settings::get_step_dir() {
step_dir = dir;
} else {
step_dir = self.step_save_dir.clone();
}
return Task::perform(
async move {
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let file_path = format!("{step_dir}\\{model_name}.step");
if let Ok(mut f) = tokio::fs::File::create(file_path.clone()).await{
if f.write_all(s.as_str().as_bytes()).await.is_ok(){
return (true,file_path);
}
}
return (false,file_path)
},
|(rst,path)| {
let title = match rst{
true=>"下载成功".to_string(),
false=>"下载失败".to_string(),
};
let body = match rst{
true=>format!("Step模型存储到{path}"),
false=> "Step模型下载成功但无法保存请检查是否有写入权限".to_string(),
};
let status = match rst{
true=>toast::Status::Success,
false=>toast::Status::Danger,
};
MainWindowMsg::Toast(toast::Toast {
title,
body,
status
})
},
);
} }
JlcDownloaderMsg::StepFetchErr(e) => { JlcDownloaderMsg::StepFetchErr(e) => {
self.item_clickable = true; self.item_clickable = true;
info!("JlcDownloaderMsg::StepFetchError({:?})", e); let model_name = self.current_step_name.clone();
return Task::perform(
async { tokio::time::sleep(tokio::time::Duration::from_millis(10)) },
move |_| {
MainWindowMsg::Toast(toast::Toast {
title: "下载失败".into(),
body: format!("{}的模型下载失败", model_name),
status: toast::Status::Danger,
})
},
);
} }
JlcDownloaderMsg::OpenDatasheet(url) => { JlcDownloaderMsg::OpenDatasheet(url) => {
todo!("To open the url!"); todo!("To open the url!");
@ -212,10 +262,7 @@ impl TabContent for JlcDownloader {
results = results.push(self.create_fetched_item_button(&item, 1000)); results = results.push(self.create_fetched_item_button(&item, 1000));
} }
let mut logs = iced::widget::column!["预览暂不可用期待后期iced更新","日志:"]; let mut logs = iced::widget::column!["预览暂不可用期待后期iced更新",];
for item in self.download_log.iter(){
logs = logs.push(iced::widget::text(item.clone()));
}
let body = iced::widget::row![ let body = iced::widget::row![
iced::widget::column![ iced::widget::column![
iced::widget::text(self.msg_disp.clone()).height(Length::Shrink), iced::widget::text(self.msg_disp.clone()).height(Length::Shrink),

View File

@ -3,11 +3,12 @@ use crate::ui::home_page::HomePage;
use crate::ui::home_page::HomePageMsg; use crate::ui::home_page::HomePageMsg;
use crate::ui::jlc_downloader::JlcDownloaderMsg; use crate::ui::jlc_downloader::JlcDownloaderMsg;
use crate::ui::part_viewer::PartViewerMsg; use crate::ui::part_viewer::PartViewerMsg;
use iced::color;
use iced::Subscription; use iced::Subscription;
use iced::Task; use iced::Task;
use iced::color;
use tracing::info; use tracing::info;
use tracing::warn; use tracing::warn;
use crate::widgets::toast;
#[allow(unused_imports)] #[allow(unused_imports)]
use super::db_browser; use super::db_browser;
@ -17,16 +18,16 @@ use super::home_page;
use super::jlc_downloader; use super::jlc_downloader;
#[allow(unused_imports)] #[allow(unused_imports)]
use super::part_viewer; use super::part_viewer;
use iced::Theme;
#[allow(unused_imports)] #[allow(unused_imports)]
use iced::widget as w; use iced::widget as w;
use iced::widget::button; use iced::widget::button;
use iced::widget::row; use iced::widget::row;
use iced::Theme;
#[allow(unused_imports)] #[allow(unused_imports)]
use iced::{ use iced::{
alignment::{Horizontal, Vertical}, widget::{column, Column, Container, Text}, Element, Length,
Element, alignment::{Horizontal, Vertical},
Length, widget::{Column, Container, Text, column},
}; };
use std::fmt::Display; use std::fmt::Display;
@ -40,6 +41,7 @@ struct MainWindow {
db_browser: crate::ui::db_browser::DbBrowser, db_browser: crate::ui::db_browser::DbBrowser,
part_viewer: crate::ui::part_viewer::PartViewer, part_viewer: crate::ui::part_viewer::PartViewer,
explain: bool, explain: bool,
toasts:Vec<toast::Toast>,
} }
impl Default for MainWindow { impl Default for MainWindow {
fn default() -> Self { fn default() -> Self {
@ -51,6 +53,7 @@ impl Default for MainWindow {
} }
let mut jlc_downloader = crate::ui::jlc_downloader::JlcDownloader::default(); let mut jlc_downloader = crate::ui::jlc_downloader::JlcDownloader::default();
jlc_downloader.set_theme(theme.clone()); jlc_downloader.set_theme(theme.clone());
jlc_downloader.step_save_dir = home_page.step_dir.clone();
Self { Self {
title: "HardwareToolkit".into(), title: "HardwareToolkit".into(),
theme, theme,
@ -60,6 +63,7 @@ impl Default for MainWindow {
db_browser: Default::default(), db_browser: Default::default(),
part_viewer: Default::default(), part_viewer: Default::default(),
explain: false, explain: false,
toasts:Default::default(),
} }
} }
} }
@ -90,6 +94,8 @@ pub enum MainWindowMsg {
DbBrowser(DbBrowserMsg), DbBrowser(DbBrowserMsg),
PartViewer(PartViewerMsg), PartViewer(PartViewerMsg),
Explain(bool), Explain(bool),
Toast(toast::Toast),
CloseToast(usize),
Nothing, Nothing,
} }
@ -220,6 +226,14 @@ impl MainWindow {
self.explain = explain; self.explain = explain;
Task::none() Task::none()
} }
MainWindowMsg::Toast(t)=>{
self.toasts.push(t.clone());
Task::none()
}
MainWindowMsg::CloseToast(i)=>{
self.toasts.remove(i);
Task::none()
}
} }
} }
fn view(&self) -> Element<'_, MainWindowMsg> { fn view(&self) -> Element<'_, MainWindowMsg> {
@ -262,7 +276,9 @@ impl MainWindow {
} }
}; };
// let content = iced::widget::Button::new("Click").on_press(MainWindowMsg::Nothing); // let content = iced::widget::Button::new("Click").on_press(MainWindowMsg::Nothing);
column![h, v].into()
//let content = column![h, v].into();
toast::Manager::new(column![h,v], &self.toasts, MainWindowMsg::CloseToast).timeout(8).into()
} }
} }

77
src/utils/gitea.rs Normal file
View File

@ -0,0 +1,77 @@
use reqwest;
const GET_URL:&str = "https://git.wuembed.com/api/v1/repos/z/hardware_toolkit/releases/latest";
/// example json content:
///
/// {
// "id": 3,
// "tag_name": "0.0.3",
// "target_commitish": "master",
// "name": "测试发布标题",
// "body": "测试版本描述",
// "url": "https://git.wuembed.com/api/v1/repos/z/hardware_toolkit/releases/3",
// "html_url": "https://git.wuembed.com/z/hardware_toolkit/releases/tag/0.0.3",
// "tarball_url": "https://git.wuembed.com/z/hardware_toolkit/archive/0.0.3.tar.gz",
// "zipball_url": "https://git.wuembed.com/z/hardware_toolkit/archive/0.0.3.zip",
// "upload_url": "https://git.wuembed.com/api/v1/repos/z/hardware_toolkit/releases/3/assets",
// "draft": false,
// "prerelease": false,
// "created_at": "2025-07-04T01:23:07+08:00",
// "published_at": "2025-07-04T01:23:07+08:00",
// "author": {
// "id": 1,
// "login": "z",
// "login_name": "",
// "source_id": 0,
// "full_name": "",
// "email": "z@noreply.localhost",
// "avatar_url": "https://git.wuembed.com/avatars/8b2edd24e3e5e88d3e78b5d40989440439931b3bcf7f3702ce87e5b345080c72",
// "html_url": "https://git.wuembed.com/z",
// "language": "",
// "is_admin": false,
// "last_login": "0001-01-01T00:00:00Z",
// "created": "2025-06-18T16:21:24+08:00",
// "restricted": false,
// "active": false,
// "prohibit_login": false,
// "location": "",
// "website": "",
// "description": "",
// "visibility": "public",
// "followers_count": 0,
// "following_count": 0,
// "starred_repos_count": 0,
// "username": "z"
// },
// "assets": [
// {
// "id": 2,
// "name": "hardware_toolkit.exe",
// "size": 41979904,
// "download_count": 0,
// "created_at": "2025-07-04T01:23:05+08:00",
// "uuid": "a1bc2d0d-bf43-4eb5-b28b-26bdd413c4ce",
// "browser_download_url": "https://git.wuembed.com/z/hardware_toolkit/releases/download/0.0.3/hardware_toolkit.exe"
// }
// ]
// }
#[derive(serde::Serialize,serde::Deserialize)]
struct Assets{
id:u32,
name:String,
size:usize,
download_count:u32,
created_at:String,
browser_download_url:String,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct ReleaseTag{
// 对标于版本号
tag_name:String,
// 对标于发布标题
name:String,
// 对标于版本描述
body:String,
}

View File

@ -1,4 +1,4 @@
pub mod app_settings; pub mod app_settings;
pub mod lazy; pub mod lazy;
pub mod step_downloader; pub mod step_downloader;
pub mod winsparkle; pub mod gitea;

View File

@ -14,10 +14,10 @@ pub struct SearchResultItem {
pub struct FetchResultItem { pub struct FetchResultItem {
pub imgs: Vec<iced::widget::image::Handle>, pub imgs: Vec<iced::widget::image::Handle>,
pub name: String, pub name: String,
pub model:String, pub model: String,
pub code :String, pub code: String,
pub model_id: String, pub model_id: String,
pub datasheet:String, pub datasheet: String,
} }
/// 访问一次api得到需要的部分数据 /// 访问一次api得到需要的部分数据
pub async fn search_keyword( pub async fn search_keyword(
@ -49,7 +49,7 @@ pub async fn search_keyword(
} }
let ii = SearchResultItem { let ii = SearchResultItem {
name: i.model.clone(), name: i.model.clone(),
code:i.code.clone(), code: i.code.clone(),
has_device: has, has_device: has,
img_urls: imgs, img_urls: imgs,
}; };
@ -78,13 +78,14 @@ pub async fn fetch_item(item: SearchResultItem) -> Result<FetchResultItem> {
let mut footprint = String::new(); let mut footprint = String::new();
let mut datasheet = String::new(); let mut datasheet = String::new();
if let serde_json::Value::String(t) = v["result"][0]["title"].clone(){ if let serde_json::Value::String(t) = v["result"][0]["title"].clone() {
title = t; title = t;
} }
if let serde_json::Value::String(t) = v["result"][0]["attributes"]["Supplier Footprint"].clone(){ if let serde_json::Value::String(t) = v["result"][0]["attributes"]["Supplier Footprint"].clone()
{
footprint = t; footprint = t;
} }
if let serde_json::Value::String(t) = v["result"][0]["attributes"]["Datasheet"].clone(){ if let serde_json::Value::String(t) = v["result"][0]["attributes"]["Datasheet"].clone() {
datasheet = t; datasheet = t;
} }
@ -99,7 +100,7 @@ pub async fn fetch_item(item: SearchResultItem) -> Result<FetchResultItem> {
let data_str: DataStr = serde_json::from_str(data_str.as_str())?; let data_str: DataStr = serde_json::from_str(data_str.as_str())?;
if let Some(m) = data_str.model { if let Some(m) = data_str.model {
step_id = m; step_id = m;
}else{ } else {
return Err(anyhow::Error::msg("Failed to get model")); return Err(anyhow::Error::msg("Failed to get model"));
} }
} }
@ -115,8 +116,8 @@ pub async fn fetch_item(item: SearchResultItem) -> Result<FetchResultItem> {
let rst = FetchResultItem { let rst = FetchResultItem {
name: item.name.clone(), name: item.name.clone(),
model_id: step_id, model_id: step_id,
model:footprint, model: footprint,
code:item.code, code: item.code,
imgs, imgs,
datasheet, datasheet,
}; };
@ -147,8 +148,7 @@ async fn search_model_id(uuid: &str) -> Result<String> {
.send() .send()
.await?; .await?;
let text = resp.text().await?; let text = resp.text().await?;
info!("In search_model_id: The v is : {}",text.clone()); return Ok(text);
return Ok(text)
} }
#[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] #[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
struct KeywordSearchRoot { struct KeywordSearchRoot {

View File

@ -1,68 +0,0 @@
// lib.rs
// winsparkle-sys
#![cfg(target_os = "windows")]
// Link to the WinSparkle.dll
#[cfg_attr(target_os = "windows", link(name = "WinSparkle", kind = "dylib"))]
#[allow(dead_code)]
unsafe extern "C" {
/// Initialize WinSparkle.
///
/// This initializes WinSparkle and should be called at application startup.
pub fn win_sparkle_init();
/// Clean up after WinSparkle.
///
/// Should be called at application shutdown.
/// Cancels any pending update checks and shuts down its helper threads.
pub fn win_sparkle_cleanup();
/// Set the URL for the appcast file.
///
/// This specifies the URL where WinSparkle will look for updates.
pub fn win_sparkle_set_appcast_url(url: *const i8);
/// Set DSA public key.
///
/// Only PEM format is supported.
/// Public key will be used to verify DSA signatures of the update file.
/// PEM data will be set only if it contains valid DSA public key.
///
/// If this function isn't called by the app, public key is obtained from
/// Windows resource named "DSAPub" of type "DSAPEM".
///
/// returns 1 if valid DSA public key provided, 0 otherwise.
pub fn win_sparkle_set_dsa_pub_pem(dsa_pub_pem: *const i8) -> i32;
/// Set EDDSA public key.
/// The function above should not be used.
pub unsafe fn win_sparkle_set_eddsa_public_key(eddsa_pub_pem: *const i8) -> i32;
/// Set the path in the registry where WinSparkle will store its settings.
///
/// This sets the path where WinSparkle will store its settings in the registry.
pub fn win_sparkle_set_registry_path(path: *const i8);
/// Set the callback function for handling shutdown requests.
///
/// This sets the callback function that WinSparkle will call when it receives a
/// request to shut down the application during an update.
pub fn win_sparkle_set_shutdown_request_callback(callback: Option<extern "C" fn() -> ()>);
/// Check for updates with the WinSparkle UI.
///
/// This checks for updates and displays the WinSparkle UI if an update is available.
pub fn win_sparkle_check_update_with_ui();
/// Check for updates with the WinSparkle UI
/// and immediately install the update if one is available.
pub fn win_sparkle_check_update_with_ui_and_install();
/// Check for updates.
///
/// No progress UI is shown to the user when checking.
/// If an update is available, the usual "update available" UI is shown.
/// This function is *not* completely UI-less.
pub fn win_sparkle_check_update_without_ui();
}

View File

@ -1 +1,2 @@
pub mod tab_header; pub mod tab_header;
pub(crate) mod toast;

479
src/widgets/toast.rs Normal file
View File

@ -0,0 +1,479 @@
use std::fmt;
use iced::advanced::layout::{self, Layout};
use iced::advanced::overlay;
use iced::advanced::renderer;
use iced::advanced::widget::{self, Operation, Tree};
use iced::advanced::{Clipboard, Shell, Widget};
use iced::mouse;
use iced::theme;
use iced::time::{self, Duration, Instant};
use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, text};
use iced::window;
use iced::{
Alignment, Center, Element, Event, Fill, Length, Point, Rectangle, Renderer, Size, Theme,
Vector,
};
pub const DEFAULT_TIMEOUT: u64 = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Status {
#[default]
Primary,
Secondary,
Success,
Danger,
}
impl Status {
pub const ALL: &'static [Self] = &[Self::Primary, Self::Secondary, Self::Success, Self::Danger];
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Status::Primary => "Primary",
Status::Secondary => "Secondary",
Status::Success => "Success",
Status::Danger => "Danger",
}
.fmt(f)
}
}
#[derive(Debug, Clone, Default)]
pub struct Toast {
pub title: String,
pub body: String,
pub status: Status,
}
pub struct Manager<'a, Message> {
content: Element<'a, Message>,
toasts: Vec<Element<'a, Message>>,
timeout_secs: u64,
on_close: Box<dyn Fn(usize) -> Message + 'a>,
}
impl<'a, Message> Manager<'a, Message>
where
Message: 'a + Clone,
{
pub fn new(
content: impl Into<Element<'a, Message>>,
toasts: &'a [Toast],
on_close: impl Fn(usize) -> Message + 'a,
) -> Self {
let toasts = toasts
.iter()
.enumerate()
.map(|(index, toast)| {
container(column![
container(
row![
text(toast.title.as_str()),
horizontal_space(),
button("X").on_press((on_close)(index)).padding(3),
]
.align_y(Center)
)
.width(Fill)
.padding(5)
.style(match toast.status {
Status::Primary => primary,
Status::Secondary => secondary,
Status::Success => success,
Status::Danger => danger,
}),
horizontal_rule(1),
container(text(toast.body.as_str()))
.width(Fill)
.padding(5)
.style(container::rounded_box),
])
.max_width(600)
.into()
})
.collect();
Self {
content: content.into(),
toasts,
timeout_secs: DEFAULT_TIMEOUT,
on_close: Box::new(on_close),
}
}
pub fn timeout(self, seconds: u64) -> Self {
Self {
timeout_secs: seconds,
..self
}
}
}
impl<Message> Widget<Message, Theme, Renderer> for Manager<'_, Message> {
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(
&self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content
.as_widget()
.layout(&mut tree.children[0], renderer, limits)
}
fn tag(&self) -> widget::tree::Tag {
struct Marker;
widget::tree::Tag::of::<Marker>()
}
fn state(&self) -> widget::tree::State {
widget::tree::State::new(Vec::<Option<Instant>>::new())
}
fn children(&self) -> Vec<Tree> {
std::iter::once(Tree::new(&self.content))
.chain(self.toasts.iter().map(Tree::new))
.collect()
}
fn diff(&self, tree: &mut Tree) {
let instants = tree.state.downcast_mut::<Vec<Option<Instant>>>();
// Invalidating removed instants to None allows us to remove
// them here so that diffing for removed / new toast instants
// is accurate
instants.retain(Option::is_some);
match (instants.len(), self.toasts.len()) {
(old, new) if old > new => {
instants.truncate(new);
}
(old, new) if old < new => {
instants.extend(std::iter::repeat_n(Some(Instant::now()), new - old));
}
_ => {}
}
tree.diff_children(
&std::iter::once(&self.content)
.chain(self.toasts.iter())
.collect::<Vec<_>>(),
);
}
fn operate(
&self,
state: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.content
.as_widget()
.operate(&mut state.children[0], layout, renderer, operation);
});
}
fn update(
&mut self,
state: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
&mut state.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn draw(
&self,
state: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&state.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn mouse_interaction(
&self,
state: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&state.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn overlay<'b>(
&'b mut self,
state: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
let instants = state.state.downcast_mut::<Vec<Option<Instant>>>();
let (content_state, toasts_state) = state.children.split_at_mut(1);
let content = self.content.as_widget_mut().overlay(
&mut content_state[0],
layout,
renderer,
viewport,
translation,
);
let toasts = (!self.toasts.is_empty()).then(|| {
overlay::Element::new(Box::new(Overlay {
position: layout.bounds().position() + translation,
viewport: *viewport,
toasts: &mut self.toasts,
state: toasts_state,
instants,
on_close: &self.on_close,
timeout_secs: self.timeout_secs,
}))
});
let overlays = content.into_iter().chain(toasts).collect::<Vec<_>>();
(!overlays.is_empty()).then(|| overlay::Group::with_children(overlays).overlay())
}
}
struct Overlay<'a, 'b, Message> {
position: Point,
viewport: Rectangle,
toasts: &'b mut [Element<'a, Message>],
state: &'b mut [Tree],
instants: &'b mut [Option<Instant>],
on_close: &'b dyn Fn(usize) -> Message,
timeout_secs: u64,
}
impl<Message> overlay::Overlay<Message, Theme, Renderer> for Overlay<'_, '_, Message> {
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
let limits = layout::Limits::new(Size::ZERO, bounds);
layout::flex::resolve(
layout::flex::Axis::Vertical,
renderer,
&limits,
Fill,
Fill,
10.into(),
10.0,
Alignment::End,
self.toasts,
self.state,
)
.translate(Vector::new(self.position.x, self.position.y))
}
fn update(
&mut self,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) {
if let Event::Window(window::Event::RedrawRequested(now)) = &event {
self.instants
.iter_mut()
.enumerate()
.for_each(|(index, maybe_instant)| {
if let Some(instant) = maybe_instant.as_mut() {
let remaining =
time::seconds(self.timeout_secs).saturating_sub(instant.elapsed());
if remaining == Duration::ZERO {
maybe_instant.take();
shell.publish((self.on_close)(index));
} else {
shell.request_redraw_at(*now + remaining);
}
}
});
}
let viewport = layout.bounds();
for (((child, state), layout), instant) in self
.toasts
.iter_mut()
.zip(self.state.iter_mut())
.zip(layout.children())
.zip(self.instants.iter_mut())
{
let mut local_messages = vec![];
let mut local_shell = Shell::new(&mut local_messages);
child.as_widget_mut().update(
state,
event,
layout,
cursor,
renderer,
clipboard,
&mut local_shell,
&viewport,
);
if !local_shell.is_empty() {
instant.take();
}
shell.merge(local_shell, std::convert::identity);
}
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
) {
let viewport = layout.bounds();
for ((child, state), layout) in self
.toasts
.iter()
.zip(self.state.iter())
.zip(layout.children())
{
child
.as_widget()
.draw(state, renderer, theme, style, layout, cursor, &viewport);
}
}
fn operate(
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
operation.container(None, layout.bounds(), &mut |operation| {
self.toasts
.iter()
.zip(self.state.iter_mut())
.zip(layout.children())
.for_each(|((child, state), layout)| {
child
.as_widget()
.operate(state, layout, renderer, operation);
});
});
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
) -> mouse::Interaction {
self.toasts
.iter()
.zip(self.state.iter())
.zip(layout.children())
.map(|((child, state), layout)| {
child
.as_widget()
.mouse_interaction(state, layout, cursor, &self.viewport, renderer)
.max(
cursor
.is_over(layout.bounds())
.then_some(mouse::Interaction::Idle)
.unwrap_or_default(),
)
})
.max()
.unwrap_or_default()
}
}
impl<'a, Message> From<Manager<'a, Message>> for Element<'a, Message>
where
Message: 'a,
{
fn from(manager: Manager<'a, Message>) -> Self {
Element::new(manager)
}
}
fn styled(pair: theme::palette::Pair) -> container::Style {
container::Style {
background: Some(pair.color.into()),
text_color: pair.text.into(),
..Default::default()
}
}
fn primary(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
styled(palette.primary.weak)
}
fn secondary(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
styled(palette.secondary.weak)
}
fn success(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
styled(palette.success.weak)
}
fn danger(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
styled(palette.danger.weak)
}