diff --git a/src/main.rs b/src/main.rs index 9434358..e41572e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,9 +9,9 @@ fn main() { 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_set_eddsa_public_key( + // "pXr0FyLTCvtX2BP7d/i3Ot8T9hL+ODBQforwfBp2oLo=\0".as_ptr() as *const i8 + // ); win_sparkle_init(); } ui::main_window::main_window(); diff --git a/src/ui/home_page.rs b/src/ui/home_page.rs index 2705442..549b70b 100644 --- a/src/ui/home_page.rs +++ b/src/ui/home_page.rs @@ -1,5 +1,6 @@ -use iced::{Length, Task, alignment::Horizontal, widget::Column}; +use iced::{alignment::Horizontal, widget::Column, Length, Task}; use tracing::info; +use crate::utils::winsparkle; #[allow(unused_imports)] use crate::ui::main_window::{MainWindowMsg, TabContent}; @@ -91,8 +92,16 @@ impl TabContent for HomePage { } HomePageMsg::CheckUpdate => { info!("To check update."); + unsafe{ + winsparkle::win_sparkle_check_update_with_ui_and_install(); + } } } Task::none() } } +impl HomePage { + pub fn set_theme(&mut self, theme: iced::Theme) { + self.theme = theme; + } +} diff --git a/src/ui/jlc_downloader.rs b/src/ui/jlc_downloader.rs index 4f81a75..c827560 100644 --- a/src/ui/jlc_downloader.rs +++ b/src/ui/jlc_downloader.rs @@ -1,26 +1,118 @@ use crate::ui::main_window::{MainWindowMsg, TabContent}; -use crate::utils::step_downloader::{self as downloader, JlcSearchResultItem}; +use crate::utils::step_downloader::{self as downloader, FetchResultItem, SearchResultItem}; #[allow(unused_imports)] use anyhow::Result; -use iced::{Length, Task, alignment::Horizontal, widget::Column}; +use iced::widget::{Row, button, keyed_column}; +use iced::{Element, Length, Task, alignment::Horizontal, widget::Column}; use tracing::info; -#[derive(Default)] pub struct JlcDownloader { search_word: String, - search_results: Vec, - search_error: String, + search_status: String, + search_available: bool, + search_index: usize, + result_list: Vec, + fetch_list: Vec, + msg_disp: String, + step_data: String, + theme: iced::Theme, + item_clickable:bool, + current_step_content:String, + current_step_name:String, + download_log:Vec, +} +impl JlcDownloader { + pub fn set_theme(&mut self, theme: iced::Theme) { + self.theme = theme; + } + fn fetch_something(&mut self) -> Task { + 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(), + download_log: vec![], + } + } } #[allow(dead_code)] #[derive(Debug, Clone, Eq, PartialEq)] pub enum JlcDownloaderMsg { Nothing, KeywordChanged(String), - KeywordSearchResult(Vec), + ItemFetchResult(FetchResultItem), + ItemFetchError(String), + KeywordSearchResult(Vec), KeywordSearchError(String), + PartClicked(DownloadAttr), + StepFetched(String), + StepFetchErr(String), + OpenDatasheet(String), SearchPart, } - +#[derive(Debug,Clone,PartialEq,Eq)] +struct DownloadAttr{ + name:String, + id:String, +} impl TabContent for JlcDownloader { type TabMessage = JlcDownloaderMsg; @@ -31,10 +123,17 @@ impl TabContent for JlcDownloader { } JlcDownloaderMsg::KeywordChanged(k) => { self.search_word = k; + self.search_status.clear(); } JlcDownloaderMsg::SearchPart => { - return Task::perform(downloader::search_keyword(self.search_word.clone()), |x| { - match x { + self.search_status = "Searching...".to_string(); + self.search_index = 0; + 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) => { MainWindowMsg::JlcDownloader(JlcDownloaderMsg::KeywordSearchResult(v)) } @@ -42,16 +141,53 @@ impl TabContent for JlcDownloader { let e = format!("{e:?}"); MainWindowMsg::JlcDownloader(JlcDownloaderMsg::KeywordSearchError(e)) } - } - }); + }, + ); } JlcDownloaderMsg::KeywordSearchResult(rest) => { - info!("JlcDownloaderMsg::KeywordSearchResult"); - self.search_error.clear(); - self.search_results = 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_error = 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) => { + self.download_log.push(format!("3D for {} 下载成功",self.current_step_name)); + self.item_clickable = true; + } + JlcDownloaderMsg::StepFetchErr(e) => { + self.item_clickable = true; + info!("JlcDownloaderMsg::StepFetchError({:?})", e); + } + JlcDownloaderMsg::OpenDatasheet(url) => { + todo!("To open the url!"); } } Task::none() @@ -59,20 +195,43 @@ impl TabContent for JlcDownloader { fn content(&self) -> iced::Element<'_, MainWindowMsg> { let h = iced::widget::row![ - iced::widget::text("搜索元件:").align_y(iced::Alignment::Center), iced::widget::text_input("元件名或嘉立创编号", &self.search_word) - .on_input(MainWindowMsg::SearchKeywordChanged), - iced::widget::button("搜索") - .on_press(MainWindowMsg::JlcDownloader(JlcDownloaderMsg::SearchPart)), + .on_input(MainWindowMsg::SearchKeywordChanged) + .width(iced::FillPortion(6)), + if !self.search_available { + 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更新","日志:"]; + for item in self.download_log.iter(){ + logs = logs.push(iced::widget::text(item.clone())); + } + 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::Shrink) - .spacing(40); - let c = Column::new() + .height(Length::Fill); + Column::new() .align_x(Horizontal::Left) .width(Length::Fill) .height(Length::Fill) - .push(h); - - return c.into(); + .push(h) + .push(body) + .into() } } diff --git a/src/ui/main_window.rs b/src/ui/main_window.rs index 15957ea..0ccb381 100644 --- a/src/ui/main_window.rs +++ b/src/ui/main_window.rs @@ -3,6 +3,7 @@ use crate::ui::home_page::HomePage; use crate::ui::home_page::HomePageMsg; use crate::ui::jlc_downloader::JlcDownloaderMsg; use crate::ui::part_viewer::PartViewerMsg; +use iced::color; use iced::Subscription; use iced::Task; use tracing::info; @@ -16,16 +17,16 @@ use super::home_page; use super::jlc_downloader; #[allow(unused_imports)] use super::part_viewer; -use iced::Theme; #[allow(unused_imports)] use iced::widget as w; use iced::widget::button; use iced::widget::row; +use iced::Theme; #[allow(unused_imports)] use iced::{ - Element, Length, - alignment::{Horizontal, Vertical}, - widget::{Column, Container, Text, column}, + alignment::{Horizontal, Vertical}, widget::{column, Column, Container, Text}, + Element, + Length, }; use std::fmt::Display; @@ -38,6 +39,7 @@ struct MainWindow { jlc_downloader: crate::ui::jlc_downloader::JlcDownloader, db_browser: crate::ui::db_browser::DbBrowser, part_viewer: crate::ui::part_viewer::PartViewer, + explain: bool, } impl Default for MainWindow { fn default() -> Self { @@ -47,25 +49,23 @@ impl Default for MainWindow { theme = Theme::ALL[saved_theme as usize % Theme::ALL.len()].clone(); home_page.theme = theme.clone(); } + let mut jlc_downloader = crate::ui::jlc_downloader::JlcDownloader::default(); + jlc_downloader.set_theme(theme.clone()); Self { title: "HardwareToolkit".into(), theme, curr_tab: Default::default(), home_page, - jlc_downloader: Default::default(), + jlc_downloader, db_browser: Default::default(), part_viewer: Default::default(), + explain: false, } } } impl MainWindow { pub fn new() -> (Self, Task) { - ( - Self::default(), - Task::batch([ - iced::widget::focus_next(), - ]), - ) + (Self::default(), Task::batch([iced::widget::focus_next()])) } } @@ -89,6 +89,7 @@ pub enum MainWindowMsg { JlcDownloader(JlcDownloaderMsg), DbBrowser(DbBrowserMsg), PartViewer(PartViewerMsg), + Explain(bool), Nothing, } @@ -182,14 +183,14 @@ impl MainWindow { btn.on_press(MainWindowMsg::TabSelected(tab)).into() } fn update(&mut self, msg: MainWindowMsg) -> Task { - info!("Process the msg: {msg:?}"); match msg { MainWindowMsg::ThemeChanged(theme) => { self.theme = theme.clone(); if let Some(idx) = Theme::ALL.iter().position(|x| x == &theme) { crate::utils::app_settings::set_curr_theme(idx as u32).unwrap(); } - Task::none() + self.home_page.set_theme(theme); + self.home_page.update(HomePageMsg::Nothing) } MainWindowMsg::TitleChanged(title) => { self.title = title; @@ -215,6 +216,10 @@ impl MainWindow { MainWindowMsg::SearchKeywordChanged(k) => self .jlc_downloader .update(JlcDownloaderMsg::KeywordChanged(k)), + MainWindowMsg::Explain(explain) => { + self.explain = explain; + Task::none() + } } } fn view(&self) -> Element<'_, MainWindowMsg> { @@ -222,13 +227,39 @@ impl MainWindow { self.create_tab_btn(TabId::HomePage), self.create_tab_btn(TabId::JlcDownloader), 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 { - TabId::HomePage => self.home_page.view(), - TabId::JlcDownloader => self.jlc_downloader.view(), - TabId::DbBrowser => self.db_browser.view(), - TabId::PartViewer => self.part_viewer.view(), + TabId::HomePage => { + if self.explain { + self.home_page.view().explain(color!(0xff0000)) + } else { + 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); column![h, v].into() diff --git a/src/utils/step_downloader.rs b/src/utils/step_downloader.rs index 5061a5e..0062746 100644 --- a/src/utils/step_downloader.rs +++ b/src/utils/step_downloader.rs @@ -1,16 +1,206 @@ +use anyhow::Result; +use image::EncodableLayout; #[allow(unused_imports)] use tracing::{error, info, warn}; -use anyhow::Result; -#[derive(Debug,Clone,Eq,PartialEq)] -pub struct JlcSearchResultItem{ - pub chip_name:String, - pub imgs_url:Vec, +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct SearchResultItem { + name: String, + code: String, + has_device: String, + img_urls: Vec, +} +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct FetchResultItem { + pub imgs: Vec, + 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> { + 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 { + 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 { + 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 { + 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) } -pub async fn search_keyword(keyword:String)->Result>{ - if keyword.is_empty(){ - return Err(anyhow::anyhow!("No keyword found")); +async fn search_model_id(uuid: &str) -> Result { + 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?; + info!("In search_model_id: The v is : {}",text.clone()); + 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, +} +#[allow(non_snake_case)] +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq)] +struct KeywordSearchListItem { + id: u32, + code: String, + image: Option, + model: String, + hasDevice: Option, +} +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq)] +struct KeywordChipImages(String); +#[allow(dead_code)] +impl KeywordChipImages { + pub fn imgs(&self) -> Vec { + 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")) -} \ No newline at end of file +} +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct KeywordDevInfo { + uuid: String, + description: String, + title: String, + images: Vec, + attributes: KeywordAttributes, +} +#[allow(non_snake_case)] +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct KeywordAttributes { + Datasheet: Option, +} +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct DataStr { + model: Option, + src: Option, + _type: Option, +} diff --git a/src/utils/step_downloader.rs.old.rs b/src/utils/step_downloader.rs.old.rs deleted file mode 100644 index 843eb4b..0000000 --- a/src/utils/step_downloader.rs.old.rs +++ /dev/null @@ -1,409 +0,0 @@ -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, - tx_cmd: Sender, - rx_total: Receiver, - rx_item: Receiver, -} -impl Default for StepDownloader { - fn default() -> Self { - Self::new() - } -} - -pub struct SearchTotalInfo { - total: u32, - curr_page: u32, -} -pub struct SearchItemInfo { - name: String, - imgs: Vec, - 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, - Sender, - Receiver, - Receiver, - ) { - 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 { - 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 { - 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 { - 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 { - 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, -} -#[allow(non_snake_case)] -#[derive(Debug, serde::Serialize, serde::Deserialize)] -struct KeywordSearchListItem { - id: u32, - code: String, - image: Option, - hasDevice: Option, -} -#[derive(Debug, serde::Serialize, serde::Deserialize)] -struct KeywordChipImages(String); -impl KeywordChipImages { - pub fn imgs(&self) -> Vec { - 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, - attributes: KeywordAttributes, -} -#[allow(non_snake_case)] -#[derive(Debug, serde::Serialize, serde::Deserialize)] -struct KeywordAttributes { - Datasheet: Option, -} -#[derive(Debug, serde::Serialize, serde::Deserialize)] -struct DataStr { - model: Option, - src: Option, - _type: Option, -} diff --git a/website/appcast.xml.template.xml b/website/appcast.xml.template.xml new file mode 100644 index 0000000..ebd113e --- /dev/null +++ b/website/appcast.xml.template.xml @@ -0,0 +1,27 @@ + + + + + 硬件工具箱 + + + https://dl.wuembed.com/hardware_tk/appcast.xml + + + en + + + + Version {{VERSION}} + + + https://dl.wuembed.com/hardware_tk/{{VERSION}}.html + + + + +