Skip to content

Commit 44b5f84

Browse files
authored
feat: added support for the calculator function (#399)
* feat: added support for the calculator function * chore: deletion of duplicate files * refactor: rust implements the conversion logic * refactor: optimize string handling logic for number to word conversion * refactor: adjusting styles to improve text overflow * feat: adding tips * feat(utils): adjust copy success message according to language settings * feat(TypeIcon): add support for Calculator icons * refactor(Search): refactoring context menu logic and component structure
1 parent 77e6b58 commit 44b5f84

18 files changed

Lines changed: 495 additions & 123 deletions

File tree

public/assets/calculator.png

845 Bytes
Loading

src-tauri/Cargo.lock

Lines changed: 73 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ tungstenite = "0.24.0"
7272
env_logger = "0.11.5"
7373
tokio-util = "0.7.14"
7474
tauri-plugin-windows-version = "2"
75+
meval = "0.2"
76+
chinese-number = "0.7"
77+
num2words = "1"
7578

7679
[target."cfg(target_os = \"macos\")".dependencies]
7780
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }

src-tauri/src/common/document.rs

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub struct EditorInfo {
2929
pub timestamp: Option<String>,
3030
}
3131

32-
#[derive(Debug, Clone, Serialize, Deserialize)]
32+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
3333
pub struct Document {
3434
pub id: String,
3535
pub created: Option<String>,
@@ -55,32 +55,9 @@ pub struct Document {
5555
pub owner: Option<UserInfo>,
5656
pub last_updated_by: Option<EditorInfo>,
5757
}
58+
5859
impl Document {
59-
pub fn new(source: Option<DataSourceReference>, id: String, category: String, name: String, url: String) -> Self {
60-
Self {
61-
id,
62-
created: None,
63-
updated: None,
64-
source,
65-
r#type: None,
66-
category: Some(category),
67-
subcategory: None,
68-
categories: None,
69-
rich_categories: None,
70-
title: Some(name),
71-
summary: None,
72-
lang: None,
73-
content: None,
74-
icon: None,
75-
thumbnail: None,
76-
cover: None,
77-
tags: None,
78-
url: Some(url),
79-
size: None,
80-
metadata: None,
81-
payload: None,
82-
owner: None,
83-
last_updated_by: None,
84-
}
60+
pub fn new(document: Document) -> Self {
61+
document
8562
}
8663
}

src-tauri/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,12 @@ pub async fn init<R: Runtime>(app_handle: &AppHandle<R>) {
244244
async fn init_app_search_source<R: Runtime>(app_handle: &AppHandle<R>) -> Result<(), String> {
245245
let application_search =
246246
local::application::ApplicationSearchSource::new(app_handle.clone(), 1000f64).await?;
247+
let calculator_search = local::calculator::CalculatorSource::new(2000f64);
247248

248249
// Register the application search source
249250
let registry = app_handle.state::<SearchSourceRegistry>();
250251
registry.register_source(application_search).await;
252+
registry.register_source(calculator_search).await;
251253

252254
Ok(())
253255
}

src-tauri/src/local/application.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -281,18 +281,20 @@ impl SearchSource for ApplicationSearchSource {
281281
let app_path_string = app_path.to_string_lossy().into_owned();
282282

283283
total_hits += 1;
284-
let mut doc = Document::new(
285-
Some(DataSourceReference {
284+
285+
let mut doc = Document::new(Document {
286+
id: app_path_string.clone(),
287+
title: Some(app_name.clone()),
288+
url: Some(app_path_string),
289+
category: Some("Application".to_string()),
290+
source: Some(DataSourceReference {
286291
r#type: Some(LOCAL_QUERY_SOURCE_TYPE.into()),
287292
name: Some(DATA_SOURCE_ID.into()),
288293
id: Some(DATA_SOURCE_ID.into()),
289294
icon: None,
290295
}),
291-
app_path_string.clone(),
292-
"Application".to_string(),
293-
app_name.clone(),
294-
app_path_string.clone(),
295-
);
296+
..Default::default()
297+
});
296298

297299
// Attach icon if available
298300
if let Some(icon_path) = self.icons.get(app_name.as_str()) {

src-tauri/src/local/calculator.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
use super::LOCAL_QUERY_SOURCE_TYPE;
2+
use crate::common::{
3+
document::{DataSourceReference, Document},
4+
error::SearchError,
5+
search::{QueryResponse, QuerySource, SearchQuery},
6+
traits::SearchSource,
7+
};
8+
use async_trait::async_trait;
9+
use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese};
10+
use num2words::Num2Words;
11+
use serde_json::Value;
12+
use std::collections::HashMap;
13+
14+
const DATA_SOURCE_ID: &str = "Calculator";
15+
16+
pub struct CalculatorSource {
17+
base_score: f64,
18+
}
19+
20+
impl CalculatorSource {
21+
pub fn new(base_score: f64) -> Self {
22+
CalculatorSource { base_score }
23+
}
24+
}
25+
26+
fn parse_query(query: String) -> Value {
27+
let mut query_json = serde_json::Map::new();
28+
29+
let operators = ["+", "-", "*", "/", "%"];
30+
31+
let found_operators: Vec<_> = query
32+
.chars()
33+
.filter(|c| operators.contains(&c.to_string().as_str()))
34+
.collect();
35+
36+
if found_operators.len() == 1 {
37+
let operation = match found_operators[0] {
38+
'+' => "sum",
39+
'-' => "subtract",
40+
'*' => "multiply",
41+
'/' => "divide",
42+
'%' => "remainder",
43+
_ => "expression",
44+
};
45+
46+
query_json.insert("type".to_string(), Value::String(operation.to_string()));
47+
} else {
48+
query_json.insert("type".to_string(), Value::String("expression".to_string()));
49+
}
50+
51+
query_json.insert("value".to_string(), Value::String(query));
52+
53+
Value::Object(query_json)
54+
}
55+
56+
fn parse_result(num: f64) -> Value {
57+
let mut result_json = serde_json::Map::new();
58+
59+
let to_zh = num
60+
.to_chinese(
61+
ChineseVariant::Simple,
62+
ChineseCase::Upper,
63+
ChineseCountMethod::TenThousand,
64+
)
65+
.unwrap_or(num.to_string());
66+
67+
let to_en = Num2Words::new(num)
68+
.to_words()
69+
.map(|s| {
70+
let mut chars = s.chars();
71+
let mut result = String::new();
72+
let mut capitalize = true;
73+
74+
while let Some(c) = chars.next() {
75+
if c == ' ' || c == '-' {
76+
result.push(c);
77+
capitalize = true;
78+
} else if capitalize {
79+
result.extend(c.to_uppercase());
80+
capitalize = false;
81+
} else {
82+
result.push(c);
83+
}
84+
}
85+
86+
result
87+
})
88+
.unwrap_or(num.to_string());
89+
90+
result_json.insert("value".to_string(), Value::String(num.to_string()));
91+
result_json.insert("toZh".to_string(), Value::String(to_zh));
92+
result_json.insert("toEn".to_string(), Value::String(to_en));
93+
94+
Value::Object(result_json)
95+
}
96+
97+
#[async_trait]
98+
impl SearchSource for CalculatorSource {
99+
fn get_type(&self) -> QuerySource {
100+
QuerySource {
101+
r#type: LOCAL_QUERY_SOURCE_TYPE.into(),
102+
name: hostname::get()
103+
.unwrap_or(DATA_SOURCE_ID.into())
104+
.to_string_lossy()
105+
.into(),
106+
id: DATA_SOURCE_ID.into(),
107+
}
108+
}
109+
110+
async fn search(&self, query: SearchQuery) -> Result<QueryResponse, SearchError> {
111+
let query_string = query
112+
.query_strings
113+
.get("query")
114+
.unwrap_or(&"".to_string())
115+
.to_string();
116+
117+
if query_string.is_empty() || query_string.len() == 1 {
118+
return Ok(QueryResponse {
119+
source: self.get_type(),
120+
hits: Vec::new(),
121+
total_hits: 0,
122+
});
123+
}
124+
125+
match meval::eval_str(&query_string) {
126+
Ok(num) => {
127+
let mut payload: HashMap<String, Value> = HashMap::new();
128+
129+
let payload_query = parse_query(query_string);
130+
let payload_result = parse_result(num);
131+
132+
payload.insert("query".to_string(), payload_query);
133+
payload.insert("result".to_string(), payload_result);
134+
135+
let doc = Document::new(Document {
136+
id: DATA_SOURCE_ID.to_string(),
137+
category: Some(DATA_SOURCE_ID.to_string()),
138+
payload: Some(payload),
139+
source: Some(DataSourceReference {
140+
r#type: Some(LOCAL_QUERY_SOURCE_TYPE.into()),
141+
name: Some(DATA_SOURCE_ID.into()),
142+
id: Some(DATA_SOURCE_ID.into()),
143+
icon: None,
144+
}),
145+
..Default::default()
146+
});
147+
148+
return Ok(QueryResponse {
149+
source: self.get_type(),
150+
hits: vec![(doc, self.base_score)],
151+
total_hits: 1,
152+
});
153+
}
154+
Err(_) => {
155+
return Ok(QueryResponse {
156+
source: self.get_type(),
157+
hits: Vec::new(),
158+
total_hits: 0,
159+
});
160+
}
161+
};
162+
}
163+
}

0 commit comments

Comments
 (0)