Compare commits
No commits in common. "f410df3badd347dd1947b863d18d752880f04f59" and "3a7b0f22a881c9279a7c021c7d137fcb1d00cfdc" have entirely different histories.
f410df3bad
...
3a7b0f22a8
@ -11,10 +11,7 @@ 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 = [
|
||||||
@ -52,9 +49,7 @@ 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"
|
||||||
|
|
||||||
|
|||||||
@ -14,9 +14,7 @@
|
|||||||
|
|
||||||
|
|
||||||
## 待办
|
## 待办
|
||||||
* 下载成功的日志以Toast消息形式呈现
|
|
||||||
* 软件更新以消息弹窗的形式呈现
|
|
||||||
* 关注iced_aw项目,该项目与现有iced版本并不兼容,期待更新
|
|
||||||
|
|
||||||
|
|
||||||
## 周期性待办
|
## 周期性待办
|
||||||
@ -25,8 +23,3 @@
|
|||||||
|
|
||||||
## 忐忑
|
## 忐忑
|
||||||
* 本软件的3D封装下载调用了jlc的api,不知道哪天就收到了某函,所以暂时只在本站开源了,在未想办法解决掉该可能引起纠纷的事项之前不想广泛传播,所以也请各位道友手下留情,不要随意传播本软件
|
* 本软件的3D封装下载调用了jlc的api,不知道哪天就收到了某函,所以暂时只在本站开源了,在未想办法解决掉该可能引起纠纷的事项之前不想广泛传播,所以也请各位道友手下留情,不要随意传播本软件
|
||||||
|
|
||||||
## 笔记
|
|
||||||
* 如需窗口透明可以参考gradient例程
|
|
||||||
* 加载动画参见loading_spinners例程
|
|
||||||
|
|
||||||
|
|||||||
13
src/main.rs
13
src/main.rs
@ -1,11 +1,18 @@
|
|||||||
#![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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,17 @@
|
|||||||
use iced::{Length, Task, alignment::Horizontal, widget::Column};
|
use iced::{Length, Task, alignment::Horizontal, widget::Column};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
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 {
|
||||||
pub step_dir: String,
|
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,
|
step_dir: crate::utils::app_settings::get_step_dir().unwrap(),
|
||||||
theme: Default::default(),
|
theme: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,38 +26,6 @@ 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("版本:"),
|
||||||
@ -105,9 +63,36 @@ impl TabContent for HomePage {
|
|||||||
.spacing(5.0)
|
.spacing(5.0)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
impl HomePage {
|
type TabMessage = HomePageMsg;
|
||||||
pub fn set_theme(&mut self, theme: iced::Theme) {
|
|
||||||
self.theme = theme;
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,120 +1,26 @@
|
|||||||
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, JlcSearchResultItem};
|
||||||
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::{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;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct JlcDownloader {
|
pub struct JlcDownloader {
|
||||||
search_word: String,
|
search_word: String,
|
||||||
search_status: String,
|
search_results: Vec<JlcSearchResultItem>,
|
||||||
search_available: bool,
|
search_error: String,
|
||||||
search_index: usize,
|
|
||||||
result_list: Vec<SearchResultItem>,
|
|
||||||
fetch_list: Vec<FetchResultItem>,
|
|
||||||
msg_disp: String,
|
|
||||||
step_data: String,
|
|
||||||
theme: iced::Theme,
|
|
||||||
item_clickable: bool,
|
|
||||||
current_step_content: String,
|
|
||||||
current_step_name: String,
|
|
||||||
pub step_save_dir: String,
|
|
||||||
}
|
|
||||||
impl JlcDownloader {
|
|
||||||
pub fn set_theme(&mut self, theme: iced::Theme) {
|
|
||||||
self.theme = theme;
|
|
||||||
}
|
|
||||||
fn fetch_something(&mut self) -> Task<MainWindowMsg> {
|
|
||||||
info!("Fetching data : {}", self.search_index);
|
|
||||||
self.msg_disp = format!("Fetching : {}", self.search_index);
|
|
||||||
return if self.search_index < self.result_list.len() {
|
|
||||||
Task::perform(
|
|
||||||
downloader::fetch_item(self.result_list[self.search_index].clone()),
|
|
||||||
|x| match x {
|
|
||||||
Ok(v) => MainWindowMsg::JlcDownloader(JlcDownloaderMsg::ItemFetchResult(v)),
|
|
||||||
Err(e) => MainWindowMsg::JlcDownloader(JlcDownloaderMsg::ItemFetchError(
|
|
||||||
e.to_string(),
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Task::none()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
fn create_fetched_item_button(
|
|
||||||
&self,
|
|
||||||
item: &FetchResultItem,
|
|
||||||
width: u32,
|
|
||||||
) -> iced::widget::Button<'_, MainWindowMsg> {
|
|
||||||
let theme = self.theme.clone();
|
|
||||||
let palette = theme.extended_palette();
|
|
||||||
let info = iced::widget::column![
|
|
||||||
iced::widget::text(format!("元件:{}", item.name)),
|
|
||||||
iced::widget::text(format!("编号:{}", item.code)),
|
|
||||||
iced::widget::text(format!("模型:{}", item.model)),
|
|
||||||
]
|
|
||||||
.width(Length::FillPortion(2));
|
|
||||||
let mut btn_content = Row::new().spacing(10);
|
|
||||||
for img in item.imgs.clone().iter() {
|
|
||||||
btn_content = btn_content
|
|
||||||
.push(iced::widget::Image::new(img.clone()).width(Length::FillPortion(1)));
|
|
||||||
}
|
|
||||||
btn_content = btn_content.push(info);
|
|
||||||
if self.item_clickable {
|
|
||||||
let attr = DownloadAttr {
|
|
||||||
name: item.name.clone(),
|
|
||||||
id: item.model_id.clone(),
|
|
||||||
};
|
|
||||||
iced::widget::Button::new(btn_content).on_press(MainWindowMsg::JlcDownloader(
|
|
||||||
JlcDownloaderMsg::PartClicked(attr),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
iced::widget::Button::new(btn_content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Default for JlcDownloader {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
search_word: "".to_string(),
|
|
||||||
search_status: "".to_string(),
|
|
||||||
search_available: true,
|
|
||||||
search_index: 0,
|
|
||||||
result_list: vec![],
|
|
||||||
fetch_list: vec![],
|
|
||||||
msg_disp: "".to_string(),
|
|
||||||
step_data: "".to_string(),
|
|
||||||
theme: Default::default(),
|
|
||||||
item_clickable: true,
|
|
||||||
current_step_content: "".to_string(),
|
|
||||||
current_step_name: "".to_string(),
|
|
||||||
step_save_dir: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
pub enum JlcDownloaderMsg {
|
pub enum JlcDownloaderMsg {
|
||||||
Nothing,
|
Nothing,
|
||||||
KeywordChanged(String),
|
KeywordChanged(String),
|
||||||
ItemFetchResult(FetchResultItem),
|
KeywordSearchResult(Vec<JlcSearchResultItem>),
|
||||||
ItemFetchError(String),
|
|
||||||
KeywordSearchResult(Vec<SearchResultItem>),
|
|
||||||
KeywordSearchError(String),
|
KeywordSearchError(String),
|
||||||
PartClicked(DownloadAttr),
|
|
||||||
StepFetched(String),
|
|
||||||
StepFetchErr(String),
|
|
||||||
OpenDatasheet(String),
|
|
||||||
SearchPart,
|
SearchPart,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
struct DownloadAttr {
|
|
||||||
name: String,
|
|
||||||
id: String,
|
|
||||||
}
|
|
||||||
impl TabContent for JlcDownloader {
|
impl TabContent for JlcDownloader {
|
||||||
type TabMessage = JlcDownloaderMsg;
|
type TabMessage = JlcDownloaderMsg;
|
||||||
|
|
||||||
@ -125,17 +31,10 @@ impl TabContent for JlcDownloader {
|
|||||||
}
|
}
|
||||||
JlcDownloaderMsg::KeywordChanged(k) => {
|
JlcDownloaderMsg::KeywordChanged(k) => {
|
||||||
self.search_word = k;
|
self.search_word = k;
|
||||||
self.search_status.clear();
|
|
||||||
}
|
}
|
||||||
JlcDownloaderMsg::SearchPart => {
|
JlcDownloaderMsg::SearchPart => {
|
||||||
self.search_status = "Searching...".to_string();
|
return Task::perform(downloader::search_keyword(self.search_word.clone()), |x| {
|
||||||
self.search_index = 0;
|
match x {
|
||||||
self.search_available = false;
|
|
||||||
self.fetch_list.clear();
|
|
||||||
self.result_list.clear();
|
|
||||||
return Task::perform(
|
|
||||||
downloader::search_keyword(self.search_word.clone(), 1, 20),
|
|
||||||
|x| match x {
|
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
MainWindowMsg::JlcDownloader(JlcDownloaderMsg::KeywordSearchResult(v))
|
MainWindowMsg::JlcDownloader(JlcDownloaderMsg::KeywordSearchResult(v))
|
||||||
}
|
}
|
||||||
@ -143,101 +42,16 @@ impl TabContent for JlcDownloader {
|
|||||||
let e = format!("{e:?}");
|
let e = format!("{e:?}");
|
||||||
MainWindowMsg::JlcDownloader(JlcDownloaderMsg::KeywordSearchError(e))
|
MainWindowMsg::JlcDownloader(JlcDownloaderMsg::KeywordSearchError(e))
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
JlcDownloaderMsg::KeywordSearchResult(rest) => {
|
|
||||||
self.search_status.clear();
|
|
||||||
self.search_available = true;
|
|
||||||
self.result_list.clear();
|
|
||||||
self.result_list = rest;
|
|
||||||
self.search_index = 0;
|
|
||||||
return self.fetch_something();
|
|
||||||
}
|
|
||||||
JlcDownloaderMsg::KeywordSearchError(e) => {
|
|
||||||
self.search_status = e;
|
|
||||||
self.search_available = true;
|
|
||||||
}
|
|
||||||
JlcDownloaderMsg::ItemFetchResult(item) => {
|
|
||||||
self.fetch_list.push(item);
|
|
||||||
//开始fetch下一条
|
|
||||||
self.search_index += 1;
|
|
||||||
return self.fetch_something();
|
|
||||||
}
|
|
||||||
JlcDownloaderMsg::ItemFetchError(e) => {
|
|
||||||
info!("JlcDownloaderMsg::ItemFetchError({:?})", e);
|
|
||||||
self.search_index += 1;
|
|
||||||
return self.fetch_something();
|
|
||||||
}
|
|
||||||
JlcDownloaderMsg::PartClicked(id) => {
|
|
||||||
self.current_step_name = id.name.clone();
|
|
||||||
info!("JlcDownloaderMsg::PartClicked {:?}", id);
|
|
||||||
self.item_clickable = false;
|
|
||||||
return Task::perform(downloader::download_step(id.id), |x| match x {
|
|
||||||
Ok(s) => MainWindowMsg::JlcDownloader(JlcDownloaderMsg::StepFetched(s)),
|
|
||||||
Err(e) => MainWindowMsg::JlcDownloader(JlcDownloaderMsg::ItemFetchError(
|
|
||||||
e.to_string(),
|
|
||||||
)),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
JlcDownloaderMsg::StepFetched(s) => {
|
JlcDownloaderMsg::KeywordSearchResult(rest) => {
|
||||||
// MainWindowMsg::Toast(toast::Toast{title:"下载成功".into(),body:format!("{}的模型下载成功",s),status:toast::Status::Success});
|
info!("JlcDownloaderMsg::KeywordSearchResult");
|
||||||
self.item_clickable = true;
|
self.search_error.clear();
|
||||||
let model_name = self.current_step_name.clone();
|
self.search_results = rest;
|
||||||
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::KeywordSearchError(e) => {
|
||||||
self.item_clickable = true;
|
self.search_error = 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) => {
|
|
||||||
todo!("To open the url!");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
@ -245,40 +59,20 @@ impl TabContent for JlcDownloader {
|
|||||||
|
|
||||||
fn content(&self) -> iced::Element<'_, MainWindowMsg> {
|
fn content(&self) -> iced::Element<'_, MainWindowMsg> {
|
||||||
let h = iced::widget::row![
|
let h = iced::widget::row![
|
||||||
|
iced::widget::text("搜索元件:").align_y(iced::Alignment::Center),
|
||||||
iced::widget::text_input("元件名或嘉立创编号", &self.search_word)
|
iced::widget::text_input("元件名或嘉立创编号", &self.search_word)
|
||||||
.on_input(MainWindowMsg::SearchKeywordChanged)
|
.on_input(MainWindowMsg::SearchKeywordChanged),
|
||||||
.width(iced::FillPortion(6)),
|
iced::widget::button("搜索")
|
||||||
if !self.search_available {
|
.on_press(MainWindowMsg::JlcDownloader(JlcDownloaderMsg::SearchPart)),
|
||||||
iced::widget::button("搜索").width(iced::FillPortion(1))
|
|
||||||
} else {
|
|
||||||
iced::widget::button("搜索")
|
|
||||||
.width(iced::FillPortion(1))
|
|
||||||
.on_press(MainWindowMsg::JlcDownloader(JlcDownloaderMsg::SearchPart))
|
|
||||||
},
|
|
||||||
iced::widget::horizontal_space().width(iced::FillPortion(1)),
|
|
||||||
];
|
|
||||||
let mut results = iced::widget::column![];
|
|
||||||
for item in self.fetch_list.iter() {
|
|
||||||
results = results.push(self.create_fetched_item_button(&item, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut logs = iced::widget::column!["预览暂不可用,期待后期iced更新",];
|
|
||||||
let body = iced::widget::row![
|
|
||||||
iced::widget::column![
|
|
||||||
iced::widget::text(self.msg_disp.clone()).height(Length::Shrink),
|
|
||||||
iced::widget::scrollable(iced::widget::column![results.spacing(10),]),
|
|
||||||
]
|
|
||||||
.width(iced::Length::FillPortion(6)),
|
|
||||||
// logs.width(iced::Length::FillPortion(4)),
|
|
||||||
iced::widget::scrollable(logs).width(iced::Length::FillPortion(4)),
|
|
||||||
]
|
]
|
||||||
.height(Length::Fill);
|
.height(Length::Shrink)
|
||||||
Column::new()
|
.spacing(40);
|
||||||
|
let c = Column::new()
|
||||||
.align_x(Horizontal::Left)
|
.align_x(Horizontal::Left)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.push(h)
|
.push(h);
|
||||||
.push(body)
|
|
||||||
.into()
|
return c.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,10 +5,8 @@ use crate::ui::jlc_downloader::JlcDownloaderMsg;
|
|||||||
use crate::ui::part_viewer::PartViewerMsg;
|
use crate::ui::part_viewer::PartViewerMsg;
|
||||||
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;
|
||||||
@ -40,8 +38,6 @@ struct MainWindow {
|
|||||||
jlc_downloader: crate::ui::jlc_downloader::JlcDownloader,
|
jlc_downloader: crate::ui::jlc_downloader::JlcDownloader,
|
||||||
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,
|
|
||||||
toasts:Vec<toast::Toast>,
|
|
||||||
}
|
}
|
||||||
impl Default for MainWindow {
|
impl Default for MainWindow {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
@ -51,25 +47,25 @@ impl Default for MainWindow {
|
|||||||
theme = Theme::ALL[saved_theme as usize % Theme::ALL.len()].clone();
|
theme = Theme::ALL[saved_theme as usize % Theme::ALL.len()].clone();
|
||||||
home_page.theme = theme.clone();
|
home_page.theme = theme.clone();
|
||||||
}
|
}
|
||||||
let mut jlc_downloader = crate::ui::jlc_downloader::JlcDownloader::default();
|
|
||||||
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,
|
||||||
curr_tab: Default::default(),
|
curr_tab: Default::default(),
|
||||||
home_page,
|
home_page,
|
||||||
jlc_downloader,
|
jlc_downloader: Default::default(),
|
||||||
db_browser: Default::default(),
|
db_browser: Default::default(),
|
||||||
part_viewer: Default::default(),
|
part_viewer: Default::default(),
|
||||||
explain: false,
|
|
||||||
toasts:Default::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl MainWindow {
|
impl MainWindow {
|
||||||
pub fn new() -> (Self, Task<MainWindowMsg>) {
|
pub fn new() -> (Self, Task<MainWindowMsg>) {
|
||||||
(Self::default(), Task::batch([iced::widget::focus_next()]))
|
(
|
||||||
|
Self::default(),
|
||||||
|
Task::batch([
|
||||||
|
iced::widget::focus_next(),
|
||||||
|
]),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,9 +89,6 @@ pub enum MainWindowMsg {
|
|||||||
JlcDownloader(JlcDownloaderMsg),
|
JlcDownloader(JlcDownloaderMsg),
|
||||||
DbBrowser(DbBrowserMsg),
|
DbBrowser(DbBrowserMsg),
|
||||||
PartViewer(PartViewerMsg),
|
PartViewer(PartViewerMsg),
|
||||||
Explain(bool),
|
|
||||||
Toast(toast::Toast),
|
|
||||||
CloseToast(usize),
|
|
||||||
Nothing,
|
Nothing,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,14 +182,14 @@ impl MainWindow {
|
|||||||
btn.on_press(MainWindowMsg::TabSelected(tab)).into()
|
btn.on_press(MainWindowMsg::TabSelected(tab)).into()
|
||||||
}
|
}
|
||||||
fn update(&mut self, msg: MainWindowMsg) -> Task<MainWindowMsg> {
|
fn update(&mut self, msg: MainWindowMsg) -> Task<MainWindowMsg> {
|
||||||
|
info!("Process the msg: {msg:?}");
|
||||||
match msg {
|
match msg {
|
||||||
MainWindowMsg::ThemeChanged(theme) => {
|
MainWindowMsg::ThemeChanged(theme) => {
|
||||||
self.theme = theme.clone();
|
self.theme = theme.clone();
|
||||||
if let Some(idx) = Theme::ALL.iter().position(|x| x == &theme) {
|
if let Some(idx) = Theme::ALL.iter().position(|x| x == &theme) {
|
||||||
crate::utils::app_settings::set_curr_theme(idx as u32).unwrap();
|
crate::utils::app_settings::set_curr_theme(idx as u32).unwrap();
|
||||||
}
|
}
|
||||||
self.home_page.set_theme(theme);
|
Task::none()
|
||||||
self.home_page.update(HomePageMsg::Nothing)
|
|
||||||
}
|
}
|
||||||
MainWindowMsg::TitleChanged(title) => {
|
MainWindowMsg::TitleChanged(title) => {
|
||||||
self.title = title;
|
self.title = title;
|
||||||
@ -222,18 +215,6 @@ impl MainWindow {
|
|||||||
MainWindowMsg::SearchKeywordChanged(k) => self
|
MainWindowMsg::SearchKeywordChanged(k) => self
|
||||||
.jlc_downloader
|
.jlc_downloader
|
||||||
.update(JlcDownloaderMsg::KeywordChanged(k)),
|
.update(JlcDownloaderMsg::KeywordChanged(k)),
|
||||||
MainWindowMsg::Explain(explain) => {
|
|
||||||
self.explain = explain;
|
|
||||||
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> {
|
||||||
@ -241,44 +222,16 @@ impl MainWindow {
|
|||||||
self.create_tab_btn(TabId::HomePage),
|
self.create_tab_btn(TabId::HomePage),
|
||||||
self.create_tab_btn(TabId::JlcDownloader),
|
self.create_tab_btn(TabId::JlcDownloader),
|
||||||
self.create_tab_btn(TabId::DbBrowser),
|
self.create_tab_btn(TabId::DbBrowser),
|
||||||
self.create_tab_btn(TabId::PartViewer),
|
self.create_tab_btn(TabId::PartViewer)
|
||||||
iced::widget::horizontal_space().width(Length::Fill),
|
|
||||||
iced::widget::checkbox("Explain", self.explain).on_toggle(MainWindowMsg::Explain),
|
|
||||||
];
|
];
|
||||||
let v = match self.curr_tab {
|
let v = match self.curr_tab {
|
||||||
TabId::HomePage => {
|
TabId::HomePage => self.home_page.view(),
|
||||||
if self.explain {
|
TabId::JlcDownloader => self.jlc_downloader.view(),
|
||||||
self.home_page.view().explain(color!(0xff0000))
|
TabId::DbBrowser => self.db_browser.view(),
|
||||||
} else {
|
TabId::PartViewer => self.part_viewer.view(),
|
||||||
self.home_page.view()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TabId::JlcDownloader => {
|
|
||||||
if self.explain {
|
|
||||||
self.jlc_downloader.view().explain(color!(0xff0000))
|
|
||||||
} else {
|
|
||||||
self.jlc_downloader.view()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TabId::DbBrowser => {
|
|
||||||
if self.explain {
|
|
||||||
self.db_browser.view().explain(color!(0xff0000))
|
|
||||||
} else {
|
|
||||||
self.db_browser.view()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TabId::PartViewer => {
|
|
||||||
if self.explain {
|
|
||||||
self.part_viewer.view().explain(color!(0xff0000))
|
|
||||||
} else {
|
|
||||||
self.part_viewer.view()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,77 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -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 gitea;
|
pub mod winsparkle;
|
||||||
|
|||||||
@ -1,206 +1,16 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use image::EncodableLayout;
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Debug,Clone,Eq,PartialEq)]
|
||||||
pub struct SearchResultItem {
|
pub struct JlcSearchResultItem{
|
||||||
name: String,
|
pub chip_name:String,
|
||||||
code: String,
|
pub imgs_url:Vec<String>,
|
||||||
has_device: String,
|
|
||||||
img_urls: Vec<String>,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
|
||||||
pub struct FetchResultItem {
|
|
||||||
pub imgs: Vec<iced::widget::image::Handle>,
|
|
||||||
pub name: String,
|
|
||||||
pub model: String,
|
|
||||||
pub code: String,
|
|
||||||
pub model_id: String,
|
|
||||||
pub datasheet: String,
|
|
||||||
}
|
|
||||||
/// 访问一次api,得到需要的部分数据
|
|
||||||
pub async fn search_keyword(
|
|
||||||
keyword: String,
|
|
||||||
cur_page: u32,
|
|
||||||
page_size: u32,
|
|
||||||
) -> Result<Vec<SearchResultItem>> {
|
|
||||||
let mut form_maps = std::collections::HashMap::new();
|
|
||||||
form_maps.insert("keyword", keyword);
|
|
||||||
let cur_page = format!("{cur_page}");
|
|
||||||
let page_size = format!("{page_size}");
|
|
||||||
form_maps.insert("curPage", cur_page);
|
|
||||||
form_maps.insert("pageSize", page_size);
|
|
||||||
let resp = reqwest::Client::new()
|
|
||||||
.post("https://pro.lceda.cn/api/eda/product/search")
|
|
||||||
.form(&form_maps)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let text = resp.text().await?;
|
|
||||||
let j: KeywordSearchRoot = serde_json::from_str(&text)?;
|
|
||||||
let mut r = Vec::new();
|
|
||||||
for i in j.result.productList.iter() {
|
|
||||||
if let Some(has) = i.hasDevice.clone() {
|
|
||||||
let mut imgs = Vec::new();
|
|
||||||
if let Some(s) = i.image.clone() {
|
|
||||||
for img in s.imgs() {
|
|
||||||
imgs.push(img.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let ii = SearchResultItem {
|
|
||||||
name: i.model.clone(),
|
|
||||||
code: i.code.clone(),
|
|
||||||
has_device: has,
|
|
||||||
img_urls: imgs,
|
|
||||||
};
|
|
||||||
r.push(ii);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(r)
|
|
||||||
}
|
|
||||||
pub async fn download_step(id: String) -> Result<String> {
|
|
||||||
let url = format!("https://modules.lceda.cn/qAxj6KHrDKw4blvCG8QJPs7Y/{id}");
|
|
||||||
let url = url.as_str();
|
|
||||||
let resp = reqwest::get(url).await?;
|
|
||||||
let text = resp.text().await?;
|
|
||||||
Ok(text)
|
|
||||||
}
|
|
||||||
pub async fn fetch_item(item: SearchResultItem) -> Result<FetchResultItem> {
|
|
||||||
let h = search_has_device(&item.has_device).await?;
|
|
||||||
let mut has_device = String::new();
|
|
||||||
let v: serde_json::Value = serde_json::from_str(h.as_str())?;
|
|
||||||
let id = v["result"][0]["attributes"]["3D Model"].clone();
|
|
||||||
if let serde_json::Value::String(id) = id {
|
|
||||||
has_device = id.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut title = String::new();
|
|
||||||
let mut footprint = String::new();
|
|
||||||
let mut datasheet = String::new();
|
|
||||||
|
|
||||||
if let serde_json::Value::String(t) = v["result"][0]["title"].clone() {
|
|
||||||
title = t;
|
|
||||||
}
|
|
||||||
if let serde_json::Value::String(t) = v["result"][0]["attributes"]["Supplier Footprint"].clone()
|
|
||||||
{
|
|
||||||
footprint = t;
|
|
||||||
}
|
|
||||||
if let serde_json::Value::String(t) = v["result"][0]["attributes"]["Datasheet"].clone() {
|
|
||||||
datasheet = t;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut step_id = String::new();
|
|
||||||
let resp = search_model_id(has_device.as_str()).await?;
|
|
||||||
let mut model = String::new();
|
|
||||||
let v: serde_json::Value = serde_json::from_str(resp.as_str())?;
|
|
||||||
info!("In search_model_id: The v is : {v:#?}");
|
|
||||||
let data_str: serde_json::Value = v["result"][0]["dataStr"].clone();
|
|
||||||
|
|
||||||
if let serde_json::Value::String(data_str) = data_str {
|
|
||||||
let data_str: DataStr = serde_json::from_str(data_str.as_str())?;
|
|
||||||
if let Some(m) = data_str.model {
|
|
||||||
step_id = m;
|
|
||||||
} else {
|
|
||||||
return Err(anyhow::Error::msg("Failed to get model"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut imgs = Vec::new();
|
|
||||||
for img_url in item.img_urls {
|
|
||||||
info!("Fetching img url: {}", img_url);
|
|
||||||
let img = reqwest::get(img_url).await?.bytes().await?;
|
|
||||||
// let img = image::load_from_memory(&img)?;
|
|
||||||
let img = iced::widget::image::Handle::from_bytes(img.clone());
|
|
||||||
imgs.push(img);
|
|
||||||
}
|
|
||||||
let rst = FetchResultItem {
|
|
||||||
name: item.name.clone(),
|
|
||||||
model_id: step_id,
|
|
||||||
model: footprint,
|
|
||||||
code: item.code,
|
|
||||||
imgs,
|
|
||||||
datasheet,
|
|
||||||
};
|
|
||||||
Ok(rst)
|
|
||||||
}
|
|
||||||
async fn search_has_device(has_device: &str) -> Result<String> {
|
|
||||||
let mut form_maps = std::collections::HashMap::new();
|
|
||||||
form_maps.insert("uuids[]", has_device);
|
|
||||||
let url = "https://pro.lceda.cn/api/devices/searchByIds";
|
|
||||||
let resp = reqwest::Client::new()
|
|
||||||
.post(url)
|
|
||||||
.form(&form_maps)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let text = resp.text().await?;
|
|
||||||
Ok(text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn search_model_id(uuid: &str) -> Result<String> {
|
pub async fn search_keyword(keyword:String)->Result<Vec<JlcSearchResultItem>>{
|
||||||
let mut form_maps = std::collections::HashMap::new();
|
if keyword.is_empty(){
|
||||||
form_maps.insert("uuids[]", uuid);
|
return Err(anyhow::anyhow!("No keyword found"));
|
||||||
form_maps.insert("dataStr", "yes");
|
|
||||||
let url = "https://pro.lceda.cn/api/components/searchByIds?forceOnline=1";
|
|
||||||
|
|
||||||
let resp = reqwest::Client::new()
|
|
||||||
.post(url)
|
|
||||||
.form(&form_maps)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
let text = resp.text().await?;
|
|
||||||
return Ok(text);
|
|
||||||
}
|
|
||||||
#[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
|
|
||||||
struct KeywordSearchRoot {
|
|
||||||
code: u32,
|
|
||||||
success: bool,
|
|
||||||
result: KeywordSearchResult,
|
|
||||||
}
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq)]
|
|
||||||
struct KeywordSearchResult {
|
|
||||||
total: u32,
|
|
||||||
productList: Vec<KeywordSearchListItem>,
|
|
||||||
}
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq)]
|
|
||||||
struct KeywordSearchListItem {
|
|
||||||
id: u32,
|
|
||||||
code: String,
|
|
||||||
image: Option<KeywordChipImages>,
|
|
||||||
model: String,
|
|
||||||
hasDevice: Option<String>,
|
|
||||||
}
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq)]
|
|
||||||
struct KeywordChipImages(String);
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl KeywordChipImages {
|
|
||||||
pub fn imgs(&self) -> Vec<String> {
|
|
||||||
let urls = self.0.clone();
|
|
||||||
let list = urls.split("<$>");
|
|
||||||
let mut rst = Vec::new();
|
|
||||||
for i in list {
|
|
||||||
rst.push(i.to_string());
|
|
||||||
}
|
|
||||||
rst
|
|
||||||
}
|
}
|
||||||
}
|
Err(anyhow::Error::msg("Search error"))
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
}
|
||||||
struct KeywordDevInfo {
|
|
||||||
uuid: String,
|
|
||||||
description: String,
|
|
||||||
title: String,
|
|
||||||
images: Vec<String>,
|
|
||||||
attributes: KeywordAttributes,
|
|
||||||
}
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
struct KeywordAttributes {
|
|
||||||
Datasheet: Option<String>,
|
|
||||||
}
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
struct DataStr {
|
|
||||||
model: Option<String>,
|
|
||||||
src: Option<String>,
|
|
||||||
_type: Option<String>,
|
|
||||||
}
|
|
||||||
409
src/utils/step_downloader.rs.old.rs
Normal file
409
src/utils/step_downloader.rs.old.rs
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use image::DynamicImage;
|
||||||
|
use log::info;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender, channel};
|
||||||
|
|
||||||
|
use crate::utils::lazy;
|
||||||
|
|
||||||
|
pub struct StepDownloader {
|
||||||
|
status: SearchStatus,
|
||||||
|
rx_status: Receiver<SearchStatus>,
|
||||||
|
tx_cmd: Sender<SearchCmd>,
|
||||||
|
rx_total: Receiver<SearchTotalInfo>,
|
||||||
|
rx_item: Receiver<SearchItemInfo>,
|
||||||
|
}
|
||||||
|
impl Default for StepDownloader {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SearchTotalInfo {
|
||||||
|
total: u32,
|
||||||
|
curr_page: u32,
|
||||||
|
}
|
||||||
|
pub struct SearchItemInfo {
|
||||||
|
name: String,
|
||||||
|
imgs: Vec<DynamicImage>,
|
||||||
|
stp_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StepDownloader {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let status = SearchStatus::Ready;
|
||||||
|
let (rx_status, tx_cmd, rx_total, rx_item) = Self::search_process();
|
||||||
|
Self {
|
||||||
|
status,
|
||||||
|
rx_status,
|
||||||
|
tx_cmd,
|
||||||
|
rx_total,
|
||||||
|
rx_item,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StepDownloader {
|
||||||
|
fn search_process() -> (
|
||||||
|
Receiver<SearchStatus>,
|
||||||
|
Sender<SearchCmd>,
|
||||||
|
Receiver<SearchTotalInfo>,
|
||||||
|
Receiver<SearchItemInfo>,
|
||||||
|
) {
|
||||||
|
log::info!("Create the search_process.");
|
||||||
|
let (tx_status, rx_status) = channel(5);
|
||||||
|
let (tx_cmd, mut rx_cmd) = channel(5);
|
||||||
|
let (tx_total, mut rx_total) = channel(10);
|
||||||
|
let (tx_item, mut rx_item) = channel(10);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
if let Some(cmd) = rx_cmd.recv().await {
|
||||||
|
match cmd {
|
||||||
|
SearchCmd::Search(keyword, cur, lim) => {
|
||||||
|
log::info!("To search : {}", keyword.clone());
|
||||||
|
if tx_status.send(SearchStatus::Searching).await.is_err() {
|
||||||
|
log::error!("Failed to send the searching state.")
|
||||||
|
}
|
||||||
|
match Self::search_keyword(&keyword, cur, lim).await {
|
||||||
|
Ok(r) => {
|
||||||
|
//搜索到了Root列表
|
||||||
|
let total = SearchTotalInfo {
|
||||||
|
total: r.result.total,
|
||||||
|
curr_page: cur,
|
||||||
|
};
|
||||||
|
tx_total.send(total).await.unwrap();
|
||||||
|
|
||||||
|
//TODO: complete the search process.
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to search by keyword : {e:?}");
|
||||||
|
if tx_status.send(SearchStatus::Error).await.is_err() {
|
||||||
|
log::error!("Failed to send tx_status:Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::info!("Search for {} done.", keyword.clone());
|
||||||
|
if tx_status.send(SearchStatus::Done).await.is_err() {
|
||||||
|
log::error!("Failed to send the searching state.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SearchCmd::Stop => {
|
||||||
|
log::warn!(
|
||||||
|
"Received cmd to Stop the search process in first rx_cmd.recv, This may cause error!"
|
||||||
|
);
|
||||||
|
if tx_status.send(SearchStatus::Error).await.is_err() {
|
||||||
|
log::error!("Failed to send tx_status:Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SearchCmd::Exit => {
|
||||||
|
log::warn!(
|
||||||
|
"Received cmd to Exit the search_process, that means to exit the whole Application, Whatch that!"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(rx_status, tx_cmd, rx_total, rx_item)
|
||||||
|
}
|
||||||
|
pub fn get_status(&mut self) -> SearchStatus {
|
||||||
|
if let Ok(status) = self.rx_status.try_recv() {
|
||||||
|
self.status = status;
|
||||||
|
}
|
||||||
|
self.status.clone()
|
||||||
|
}
|
||||||
|
pub fn search(&self, keyword: &str, cur_page: u32, page_lim: u32) {
|
||||||
|
if let Ok(()) =
|
||||||
|
self.tx_cmd
|
||||||
|
.try_send(SearchCmd::Search(keyword.to_owned(), cur_page, page_lim))
|
||||||
|
{
|
||||||
|
log::info!("Success send the search {keyword} cmd.");
|
||||||
|
} else {
|
||||||
|
log::warn!("Failed to send the search {keyword} cmd.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn exit(&self) {
|
||||||
|
let _ = self.tx_cmd.try_send(SearchCmd::Exit);
|
||||||
|
}
|
||||||
|
pub fn stop(&self) {
|
||||||
|
let _ = self.tx_cmd.try_send(SearchCmd::Stop);
|
||||||
|
}
|
||||||
|
async fn search_has_device(has_device: &str) -> Result<String> {
|
||||||
|
let mut form_maps = std::collections::HashMap::new();
|
||||||
|
form_maps.insert("uuids[]", has_device);
|
||||||
|
let url = "https://pro.lceda.cn/api/devices/searchByIds";
|
||||||
|
let resp = reqwest::Client::new()
|
||||||
|
.post(url)
|
||||||
|
.form(&form_maps)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let text = resp.text().await?;
|
||||||
|
let v: serde_json::Value = serde_json::from_str(text.as_str())?;
|
||||||
|
let id = v["result"][0]["attributes"]["3D Model"].clone();
|
||||||
|
if let serde_json::Value::String(id) = id {
|
||||||
|
return Ok(id.clone());
|
||||||
|
}
|
||||||
|
Err(anyhow::Error::msg("Failed to parse the json."))
|
||||||
|
}
|
||||||
|
async fn search_model_id(uuid: &str) -> Result<String> {
|
||||||
|
let mut form_maps = std::collections::HashMap::new();
|
||||||
|
form_maps.insert("uuids[]", uuid);
|
||||||
|
form_maps.insert("dataStr", "yes");
|
||||||
|
let url = "https://pro.lceda.cn/api/components/searchByIds?forceOnline=1";
|
||||||
|
|
||||||
|
let resp = reqwest::Client::new()
|
||||||
|
.post(url)
|
||||||
|
.form(&form_maps)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let text = resp.text().await?;
|
||||||
|
let v: serde_json::Value = serde_json::from_str(text.as_str())?;
|
||||||
|
info!("In search_model_id: The v is : {v:#?}");
|
||||||
|
let data_str: serde_json::Value = v["result"][0]["dataStr"].clone();
|
||||||
|
info!("The data str Value: {data_str:#?}");
|
||||||
|
if let serde_json::Value::String(data_str) = data_str {
|
||||||
|
let data_str: DataStr = serde_json::from_str(data_str.as_str())?;
|
||||||
|
if let Some(model) = data_str.model {
|
||||||
|
return Ok(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow::Error::msg("Failed to parse the json."))
|
||||||
|
}
|
||||||
|
async fn download_step(id: &str) -> Result<String> {
|
||||||
|
let url = format!("https://modules.lceda.cn/qAxj6KHrDKw4blvCG8QJPs7Y/{id}");
|
||||||
|
let url = url.as_str();
|
||||||
|
let resp = reqwest::get(url).await?;
|
||||||
|
let text = resp.text().await?;
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
async fn search_keyword(
|
||||||
|
keyword: &str,
|
||||||
|
cur_page: u32,
|
||||||
|
page_size: u32,
|
||||||
|
) -> Result<KeywordSearchRoot> {
|
||||||
|
let mut form_maps = std::collections::HashMap::new();
|
||||||
|
form_maps.insert("keyword", keyword);
|
||||||
|
let cur_page = &format!("{cur_page}");
|
||||||
|
let page_size = &format!("{page_size}");
|
||||||
|
form_maps.insert("curPage", cur_page);
|
||||||
|
form_maps.insert("pageSize", page_size);
|
||||||
|
let resp = reqwest::Client::new()
|
||||||
|
.post("https://pro.lceda.cn/api/eda/product/search")
|
||||||
|
.form(&form_maps)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let text = resp.text().await?;
|
||||||
|
let j: KeywordSearchRoot = serde_json::from_str(&text)?;
|
||||||
|
Ok(j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum SearchCmd {
|
||||||
|
Search(String, u32, u32),
|
||||||
|
Stop,
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum SearchStatus {
|
||||||
|
Ready,
|
||||||
|
Error,
|
||||||
|
Searching,
|
||||||
|
Done,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod StepDownloaderTest {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use log::info;
|
||||||
|
use reqwest;
|
||||||
|
|
||||||
|
// #[tokio::test]
|
||||||
|
// pub async fn test_search() {
|
||||||
|
// env_logger::try_init().unwrap();
|
||||||
|
// log::info!("To test search `STM32`");
|
||||||
|
// let d = StepDownloader::default();
|
||||||
|
// d.search("STM32");
|
||||||
|
// d.exit();
|
||||||
|
// }
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_search_func() {
|
||||||
|
assert!(env_logger::try_init().is_ok());
|
||||||
|
log::info!("Start test for call search_keyword function.");
|
||||||
|
test_search("STM32").await;
|
||||||
|
// test_search("STEM32F103").await;
|
||||||
|
// test_search("TPS54302").await;
|
||||||
|
}
|
||||||
|
async fn test_search_uuids(d: &str) {
|
||||||
|
let rst = super::StepDownloader::search_has_device(d).await;
|
||||||
|
match rst {
|
||||||
|
Ok(v) => {
|
||||||
|
log::info!("Get the uuids: {}", v.clone());
|
||||||
|
match super::StepDownloader::search_model_id(v.clone().as_str()).await {
|
||||||
|
Ok(i) => {
|
||||||
|
log::info!("Get the model_id: {}", i.clone());
|
||||||
|
match super::StepDownloader::download_step(i.as_str()).await {
|
||||||
|
Ok(t) => {
|
||||||
|
info!("Success to get the content.");
|
||||||
|
|
||||||
|
let mut file = std::fs::OpenOptions::new()
|
||||||
|
.read(false)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.append(false)
|
||||||
|
.open("web_temp/first_step.step")
|
||||||
|
.unwrap();
|
||||||
|
file.write_all(t.as_bytes()).unwrap();
|
||||||
|
let _ = file.flush();
|
||||||
|
}
|
||||||
|
Err(e) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to call search_model_id: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to call search_has_device: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn test_search(k: &str) {
|
||||||
|
let rst = super::StepDownloader::search_keyword(k, 1, 10).await;
|
||||||
|
match rst {
|
||||||
|
Ok(v) => {
|
||||||
|
log::info!("Search done! ========================");
|
||||||
|
log::info!("The result:\r\n\r\n {v:#?}");
|
||||||
|
let item = &v.result.productList[0];
|
||||||
|
let uuids = item.hasDevice.clone();
|
||||||
|
if let Some(v) = uuids {
|
||||||
|
test_search_uuids(v.as_str()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error occured.");
|
||||||
|
log::error!("Error : {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #[tokio::test]
|
||||||
|
pub async fn test_post() {
|
||||||
|
if env_logger::try_init().is_ok() {
|
||||||
|
log::info!("The env_logger::try_init done.");
|
||||||
|
}
|
||||||
|
log::info!("Start test");
|
||||||
|
match reqwest::get("https://www.baidu.com").await {
|
||||||
|
Ok(r) => {
|
||||||
|
log::info!("Get www.baidu.com : {r:?}");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to get www.baidu.com : {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::info!("==========================================");
|
||||||
|
let mut form = std::collections::HashMap::new();
|
||||||
|
form.insert("keyword", "STM32");
|
||||||
|
form.insert("currPage", "1");
|
||||||
|
form.insert("pageSize", "10");
|
||||||
|
let resp = reqwest::Client::new()
|
||||||
|
.post("https://pro.lceda.cn/api/eda/product/search")
|
||||||
|
.form(&form)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match resp {
|
||||||
|
Ok(mut r) => {
|
||||||
|
if std::fs::create_dir("web_temp").is_ok() {
|
||||||
|
log::info!("Created the dir: web_temp");
|
||||||
|
} else {
|
||||||
|
log::warn!("Failed to create the dir: web_temp");
|
||||||
|
}
|
||||||
|
let mut file = std::fs::OpenOptions::new()
|
||||||
|
.read(false)
|
||||||
|
.write(true)
|
||||||
|
// .create(true)
|
||||||
|
.append(false)
|
||||||
|
.open("web_temp/first_resp.json")
|
||||||
|
.unwrap();
|
||||||
|
let enc = r.headers();
|
||||||
|
info!("Get the headers: {enc:?}");
|
||||||
|
info!("-------------------------------------------");
|
||||||
|
info!("Get the status: {:?}", r.status());
|
||||||
|
info!("-------------------------------------------");
|
||||||
|
info!("Get the http version: {:?}", r.version());
|
||||||
|
info!("-------------------------------------------");
|
||||||
|
info!("Get the url : {:?}", r.url());
|
||||||
|
// let text = r.text_with_charset("utf-8").await.unwrap();
|
||||||
|
// file.write_all(text.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
let chunk = r.chunk().await.unwrap();
|
||||||
|
if let Some(data) = chunk {
|
||||||
|
let v = data.to_vec();
|
||||||
|
let data = v.as_slice();
|
||||||
|
file.write_all(data).unwrap();
|
||||||
|
log::info!("All content has been wrote to {file:?}");
|
||||||
|
} else {
|
||||||
|
log::warn!("Failed to parse and save data to file.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error occured: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct KeywordSearchRoot {
|
||||||
|
code: u32,
|
||||||
|
success: bool,
|
||||||
|
result: KeywordSearchResult,
|
||||||
|
}
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct KeywordSearchResult {
|
||||||
|
total: u32,
|
||||||
|
productList: Vec<KeywordSearchListItem>,
|
||||||
|
}
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct KeywordSearchListItem {
|
||||||
|
id: u32,
|
||||||
|
code: String,
|
||||||
|
image: Option<KeywordChipImages>,
|
||||||
|
hasDevice: Option<String>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct KeywordChipImages(String);
|
||||||
|
impl KeywordChipImages {
|
||||||
|
pub fn imgs(&self) -> Vec<String> {
|
||||||
|
let urls = self.0.clone();
|
||||||
|
let list = urls.split("<$>");
|
||||||
|
let mut rst = Vec::new();
|
||||||
|
for i in list {
|
||||||
|
rst.push(i.to_string());
|
||||||
|
}
|
||||||
|
rst
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct KeywordDevInfo {
|
||||||
|
uuid: String,
|
||||||
|
description: String,
|
||||||
|
title: String,
|
||||||
|
images: Vec<String>,
|
||||||
|
attributes: KeywordAttributes,
|
||||||
|
}
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct KeywordAttributes {
|
||||||
|
Datasheet: Option<String>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
struct DataStr {
|
||||||
|
model: Option<String>,
|
||||||
|
src: Option<String>,
|
||||||
|
_type: Option<String>,
|
||||||
|
}
|
||||||
68
src/utils/winsparkle.rs
Normal file
68
src/utils/winsparkle.rs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
||||||
@ -1,2 +1 @@
|
|||||||
pub mod tab_header;
|
pub mod tab_header;
|
||||||
pub(crate) mod toast;
|
|
||||||
|
|||||||
@ -1,479 +0,0 @@
|
|||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
||||||
<channel>
|
|
||||||
<title>
|
|
||||||
硬件工具箱
|
|
||||||
</title>
|
|
||||||
<link>
|
|
||||||
https://dl.wuembed.com/hardware_tk/appcast.xml
|
|
||||||
</link>
|
|
||||||
<language>
|
|
||||||
en
|
|
||||||
</language>
|
|
||||||
<item>
|
|
||||||
<title>
|
|
||||||
Version {{VERSION}}
|
|
||||||
</title>
|
|
||||||
<sparkle:releaseNotesLink>
|
|
||||||
https://dl.wuembed.com/hardware_tk/{{VERSION}}.html
|
|
||||||
</sparkle:releaseNotesLink>
|
|
||||||
<enclosure url="https://dl.wuembed.com/hardware_tk/hardware_tk_{{VERSION}}.exe"
|
|
||||||
sparkle:version="{{VERSION}}"
|
|
||||||
length="{{LENGTH}}"
|
|
||||||
type="application/octet-stream"
|
|
||||||
sparkle:dsaSignature="{{SIGNATURE}}" />
|
|
||||||
</item>
|
|
||||||
</channel>
|
|
||||||
</rss>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user