diff --git a/public/assets/calculator.png b/public/assets/calculator.png new file mode 100644 index 00000000..033f6550 Binary files /dev/null and b/public/assets/calculator.png differ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 254a7993..660aa374 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -416,7 +416,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 7.1.3", "num-rational", "v_frame", ] @@ -723,6 +723,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chinese-number" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fccaef6346f6d6a741908d3b79fe97c2debe2fbb5eb3a7d00ff5981b52bb6c" +dependencies = [ + "chinese-variant", + "enum-ordinalize", + "num-bigint", + "num-traits", +] + +[[package]] +name = "chinese-variant" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7588475145507237ded760e52bf2f1085495245502033756d28ea72ade0e498b" + [[package]] name = "chrono" version = "0.4.40" @@ -770,6 +788,7 @@ dependencies = [ "applications", "async-trait", "base64 0.13.1", + "chinese-number", "dirs 5.0.1", "env_logger", "futures", @@ -780,7 +799,9 @@ dependencies = [ "hyper 0.14.32", "lazy_static", "log", + "meval", "notify", + "num2words", "once_cell", "ordered-float", "pizza-common", @@ -1476,6 +1497,26 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "enumflags2" version = "0.7.11" @@ -3272,6 +3313,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "meval" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9" +dependencies = [ + "fnv", + "nom 1.2.4", +] + [[package]] name = "mime" version = "0.3.17" @@ -3426,6 +3477,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" + [[package]] name = "nom" version = "7.1.3" @@ -3460,6 +3517,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "num-bigfloat" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793a9a2afbf2b4fc1b9a47de731031b06ed362a5ede3681fbfbaeb2ad4faaa13" + [[package]] name = "num-bigint" version = "0.4.6" @@ -3527,6 +3590,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num2words" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab78b987a0e1e6cf869a443f7f1c9dc696117d47780227e961e742d0b45d706" +dependencies = [ + "num-bigfloat", +] + [[package]] name = "num_enum" version = "0.7.3" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c45963ac..11b82117 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -72,6 +72,9 @@ tungstenite = "0.24.0" env_logger = "0.11.5" tokio-util = "0.7.14" tauri-plugin-windows-version = "2" +meval = "0.2" +chinese-number = "0.7" +num2words = "1" [target."cfg(target_os = \"macos\")".dependencies] tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" } diff --git a/src-tauri/src/common/document.rs b/src-tauri/src/common/document.rs index cf13bbd9..a75c3648 100644 --- a/src-tauri/src/common/document.rs +++ b/src-tauri/src/common/document.rs @@ -29,7 +29,7 @@ pub struct EditorInfo { pub timestamp: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Document { pub id: String, pub created: Option, @@ -55,32 +55,9 @@ pub struct Document { pub owner: Option, pub last_updated_by: Option, } + impl Document { - pub fn new(source: Option, id: String, category: String, name: String, url: String) -> Self { - Self { - id, - created: None, - updated: None, - source, - r#type: None, - category: Some(category), - subcategory: None, - categories: None, - rich_categories: None, - title: Some(name), - summary: None, - lang: None, - content: None, - icon: None, - thumbnail: None, - cover: None, - tags: None, - url: Some(url), - size: None, - metadata: None, - payload: None, - owner: None, - last_updated_by: None, - } + pub fn new(document: Document) -> Self { + document } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7b06645d..6233081b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -244,10 +244,12 @@ pub async fn init(app_handle: &AppHandle) { async fn init_app_search_source(app_handle: &AppHandle) -> Result<(), String> { let application_search = local::application::ApplicationSearchSource::new(app_handle.clone(), 1000f64).await?; + let calculator_search = local::calculator::CalculatorSource::new(2000f64); // Register the application search source let registry = app_handle.state::(); registry.register_source(application_search).await; + registry.register_source(calculator_search).await; Ok(()) } diff --git a/src-tauri/src/local/application.rs b/src-tauri/src/local/application.rs index b0bcd483..4f27ce0d 100644 --- a/src-tauri/src/local/application.rs +++ b/src-tauri/src/local/application.rs @@ -281,18 +281,20 @@ impl SearchSource for ApplicationSearchSource { let app_path_string = app_path.to_string_lossy().into_owned(); total_hits += 1; - let mut doc = Document::new( - Some(DataSourceReference { + + let mut doc = Document::new(Document { + id: app_path_string.clone(), + title: Some(app_name.clone()), + url: Some(app_path_string), + category: Some("Application".to_string()), + source: Some(DataSourceReference { r#type: Some(LOCAL_QUERY_SOURCE_TYPE.into()), name: Some(DATA_SOURCE_ID.into()), id: Some(DATA_SOURCE_ID.into()), icon: None, }), - app_path_string.clone(), - "Application".to_string(), - app_name.clone(), - app_path_string.clone(), - ); + ..Default::default() + }); // Attach icon if available if let Some(icon_path) = self.icons.get(app_name.as_str()) { diff --git a/src-tauri/src/local/calculator.rs b/src-tauri/src/local/calculator.rs new file mode 100644 index 00000000..69e553ff --- /dev/null +++ b/src-tauri/src/local/calculator.rs @@ -0,0 +1,163 @@ +use super::LOCAL_QUERY_SOURCE_TYPE; +use crate::common::{ + document::{DataSourceReference, Document}, + error::SearchError, + search::{QueryResponse, QuerySource, SearchQuery}, + traits::SearchSource, +}; +use async_trait::async_trait; +use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese}; +use num2words::Num2Words; +use serde_json::Value; +use std::collections::HashMap; + +const DATA_SOURCE_ID: &str = "Calculator"; + +pub struct CalculatorSource { + base_score: f64, +} + +impl CalculatorSource { + pub fn new(base_score: f64) -> Self { + CalculatorSource { base_score } + } +} + +fn parse_query(query: String) -> Value { + let mut query_json = serde_json::Map::new(); + + let operators = ["+", "-", "*", "/", "%"]; + + let found_operators: Vec<_> = query + .chars() + .filter(|c| operators.contains(&c.to_string().as_str())) + .collect(); + + if found_operators.len() == 1 { + let operation = match found_operators[0] { + '+' => "sum", + '-' => "subtract", + '*' => "multiply", + '/' => "divide", + '%' => "remainder", + _ => "expression", + }; + + query_json.insert("type".to_string(), Value::String(operation.to_string())); + } else { + query_json.insert("type".to_string(), Value::String("expression".to_string())); + } + + query_json.insert("value".to_string(), Value::String(query)); + + Value::Object(query_json) +} + +fn parse_result(num: f64) -> Value { + let mut result_json = serde_json::Map::new(); + + let to_zh = num + .to_chinese( + ChineseVariant::Simple, + ChineseCase::Upper, + ChineseCountMethod::TenThousand, + ) + .unwrap_or(num.to_string()); + + let to_en = Num2Words::new(num) + .to_words() + .map(|s| { + let mut chars = s.chars(); + let mut result = String::new(); + let mut capitalize = true; + + while let Some(c) = chars.next() { + if c == ' ' || c == '-' { + result.push(c); + capitalize = true; + } else if capitalize { + result.extend(c.to_uppercase()); + capitalize = false; + } else { + result.push(c); + } + } + + result + }) + .unwrap_or(num.to_string()); + + result_json.insert("value".to_string(), Value::String(num.to_string())); + result_json.insert("toZh".to_string(), Value::String(to_zh)); + result_json.insert("toEn".to_string(), Value::String(to_en)); + + Value::Object(result_json) +} + +#[async_trait] +impl SearchSource for CalculatorSource { + fn get_type(&self) -> QuerySource { + QuerySource { + r#type: LOCAL_QUERY_SOURCE_TYPE.into(), + name: hostname::get() + .unwrap_or(DATA_SOURCE_ID.into()) + .to_string_lossy() + .into(), + id: DATA_SOURCE_ID.into(), + } + } + + async fn search(&self, query: SearchQuery) -> Result { + let query_string = query + .query_strings + .get("query") + .unwrap_or(&"".to_string()) + .to_string(); + + if query_string.is_empty() || query_string.len() == 1 { + return Ok(QueryResponse { + source: self.get_type(), + hits: Vec::new(), + total_hits: 0, + }); + } + + match meval::eval_str(&query_string) { + Ok(num) => { + let mut payload: HashMap = HashMap::new(); + + let payload_query = parse_query(query_string); + let payload_result = parse_result(num); + + payload.insert("query".to_string(), payload_query); + payload.insert("result".to_string(), payload_result); + + let doc = Document::new(Document { + id: DATA_SOURCE_ID.to_string(), + category: Some(DATA_SOURCE_ID.to_string()), + payload: Some(payload), + source: Some(DataSourceReference { + r#type: Some(LOCAL_QUERY_SOURCE_TYPE.into()), + name: Some(DATA_SOURCE_ID.into()), + id: Some(DATA_SOURCE_ID.into()), + icon: None, + }), + ..Default::default() + }); + + return Ok(QueryResponse { + source: self.get_type(), + hits: vec![(doc, self.base_score)], + total_hits: 1, + }); + } + Err(_) => { + return Ok(QueryResponse { + source: self.get_type(), + hits: Vec::new(), + total_hits: 0, + }); + } + }; + } +} diff --git a/src-tauri/src/local/mod.rs b/src-tauri/src/local/mod.rs index 904cef0f..b027eb4b 100644 --- a/src-tauri/src/local/mod.rs +++ b/src-tauri/src/local/mod.rs @@ -1,4 +1,5 @@ pub mod application; +pub mod calculator; pub mod file_system; -pub const LOCAL_QUERY_SOURCE_TYPE: &str = "local"; \ No newline at end of file +pub const LOCAL_QUERY_SOURCE_TYPE: &str = "local"; diff --git a/src/components/Common/Icons/TypeIcon.tsx b/src/components/Common/Icons/TypeIcon.tsx index 4d72d410..c40de72c 100644 --- a/src/components/Common/Icons/TypeIcon.tsx +++ b/src/components/Common/Icons/TypeIcon.tsx @@ -17,9 +17,20 @@ function TypeIcon({ className = "w-5 h-5 flex-shrink-0", onClick = () => {}, }: TypeIconProps) { + console.log("item", item); const endpoint_http = useAppStore((state) => state.endpoint_http); const connectorSource = useFindConnectorIcon(item); + const isCalculator = item.id === "Calculator"; + + if (isCalculator) { + return ( + + icon + + ); + } + if (item?.source?.icon) { if ( item?.source?.icon.startsWith("http://") || diff --git a/src/components/Search/Calculator.tsx b/src/components/Search/Calculator.tsx new file mode 100644 index 00000000..ed4812f0 --- /dev/null +++ b/src/components/Search/Calculator.tsx @@ -0,0 +1,52 @@ +import { ChevronsRight } from "lucide-react"; +import { FC } from "react"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; + +interface CalculatorProps { + item: any; + isSelected: boolean; +} + +const Calculator: FC = (props) => { + const { item, isSelected } = props; + const { + payload: { query, result }, + } = item; + const { t, i18n } = useTranslation(); + + const renderItem = (result: string, description: string) => { + return ( +
+
+ {result} +
+
+ {description} +
+
+ ); + }; + + return ( +
+ {renderItem(query.value, t(`calculator.${query.type}`))} + + + + {renderItem( + result.value, + i18n.language === "zh" ? result.toZh : result.toEn + )} +
+ ); +}; + +export default Calculator; diff --git a/src/components/Search/ContextMenu.tsx b/src/components/Search/ContextMenu.tsx index 323ecadc..0304ed69 100644 --- a/src/components/Search/ContextMenu.tsx +++ b/src/components/Search/ContextMenu.tsx @@ -1,12 +1,7 @@ -import { - useClickAway, - useCreation, - useEventListener, - useReactive, -} from "ahooks"; +import { useClickAway, useCreation, useReactive } from "ahooks"; import clsx from "clsx"; -import { isNil } from "lodash-es"; -import { Link, SquareArrowOutUpRight } from "lucide-react"; +import { isNil, noop } from "lodash-es"; +import { Copy, Link, SquareArrowOutUpRight } from "lucide-react"; import { cloneElement, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; @@ -14,6 +9,7 @@ import { useOSKeyPress } from "@/hooks/useOSKeyPress"; import { useSearchStore } from "@/stores/searchStore"; import { copyToClipboard, OpenURLWithBrowser } from "@/utils"; import { isMac } from "@/utils/platform"; +import { CONTEXT_MENU_PANEL_ID } from "@/constants"; interface State { activeMenuIndex: number; @@ -26,7 +22,7 @@ interface ContextMenuProps { const ContextMenu = ({ hideCoco }: ContextMenuProps) => { const containerRef = useRef(null); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const visibleContextMenu = useSearchStore((state) => { return state.visibleContextMenu; @@ -43,16 +39,18 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => { const menus = useCreation(() => { if (isNil(selectedSearchContent)) return []; - return [ + const { url, category, payload } = selectedSearchContent; + const { query, result } = payload ?? {}; + + const menus = [ { name: "search.contextMenu.open", icon: , keys: isMac ? ["↩︎"] : ["Enter"], shortcut: "enter", + hide: category === "Calculator", clickEvent: () => { - OpenURLWithBrowser(selectedSearchContent?.url); - - setVisibleContextMenu(false); + OpenURLWithBrowser(url); hideCoco && hideCoco(); }, @@ -62,15 +60,50 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => { icon: , keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"], shortcut: isMac ? "meta.l" : "ctrl.l", - clickEvent: () => { - copyToClipboard(selectedSearchContent?.url); - - setVisibleContextMenu(false); + hide: category === "Calculator", + clickEvent() { + copyToClipboard(url); + }, + }, + { + name: "search.contextMenu.copyAnswer", + icon: , + keys: isMac ? ["↩︎"] : ["Enter"], + shortcut: "enter", + hide: category !== "Calculator", + clickEvent() { + copyToClipboard(result.value); + }, + }, + { + name: "search.contextMenu.copyUppercaseAnswer", + icon: , + keys: isMac ? ["⌘", "↩︎"] : ["Ctrl", "Enter"], + shortcut: "meta.enter", + hide: category !== "Calculator", + clickEvent() { + copyToClipboard(i18n.language === "zh" ? result.toZh : result.toEn); + }, + }, + { + name: "search.contextMenu.copyQuestionAndAnswer", + icon: , + keys: isMac ? ["⌘", "L"] : ["Ctrl", "L"], + shortcut: "meta.l", + hide: category !== "Calculator", + clickEvent() { + copyToClipboard(`${query.value} = ${result.value}`); }, }, ]; + + return menus.filter((item) => !item.hide); }, [selectedSearchContent]); + const shortcuts = useCreation(() => { + return menus.map((item) => item.shortcut); + }, [menus]); + const state = useReactive({ activeMenuIndex: 0, }); @@ -111,23 +144,20 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => { } }); - useOSKeyPress( - menus.map((item) => item.shortcut), - (_, key) => { - if (!visibleContextMenu) return; - - const item = menus.find((item) => item.shortcut === key); - - item?.clickEvent(); - } - ); - - useEventListener("keydown", (event) => { + useOSKeyPress(shortcuts, (_, key) => { if (!visibleContextMenu) return; - event.stopImmediatePropagation(); + const item = menus.find((item) => item.shortcut === key); + + handleClick(item?.clickEvent); }); + const handleClick = (click = noop) => { + click?.(); + + setVisibleContextMenu(false); + }; + return ( <> {visibleContextMenu && ( @@ -138,11 +168,12 @@ const ContextMenu = ({ hideCoco }: ContextMenuProps) => { setVisibleContextMenu(false); }} - > + /> )}
{ onMouseEnter={() => { state.activeMenuIndex = index; }} - onClick={clickEvent} + onClick={() => handleClick(clickEvent)} >
{cloneElement(icon, { className: "size-4" })} diff --git a/src/components/Search/DropdownList.tsx b/src/components/Search/DropdownList.tsx index 9dae0f22..3ad0f005 100644 --- a/src/components/Search/DropdownList.tsx +++ b/src/components/Search/DropdownList.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, useCallback } from "react"; +import { useEffect, useRef, useState, useCallback, MouseEvent } from "react"; import { CircleAlert, Bolt, X, ArrowBigRight } from "lucide-react"; import { isNil } from "lodash-es"; import { useUnmount } from "ahooks"; @@ -9,8 +9,10 @@ import IconWrapper from "@/components/Common/Icons/IconWrapper"; import TypeIcon from "@/components/Common/Icons/TypeIcon"; import SearchListItem from "./SearchListItem"; import { metaOrCtrlKey, isMetaOrCtrlKey } from "@/utils/keyboardUtils"; -import { OpenURLWithBrowser } from "@/utils/index"; +import { copyToClipboard, OpenURLWithBrowser } from "@/utils/index"; import VisibleKey from "@/components/Common/VisibleKey"; +import Calculator from "./Calculator"; +import { useShortcutsStore } from "@/stores/shortcutsStore"; type ISearchData = Record; @@ -46,6 +48,12 @@ function DropdownList({ return state.setSelectedSearchContent; }); + const hideArrowRight = (item: any) => { + const categories = ["Calculator"]; + + return categories.includes(item.category); + }; + useUnmount(() => { setSelectedSearchContent(void 0); }); @@ -66,6 +74,8 @@ function DropdownList({ } }, [isChatMode]); + const openPopover = useShortcutsStore((state) => state.openPopover); + const handleKeyDown = useCallback( (e: KeyboardEvent) => { // console.log( @@ -74,7 +84,7 @@ function DropdownList({ // showIndex, // e.key >= "0" && e.key <= "9" && showIndex // ); - if (!suggests.length) return; + if (!suggests.length || openPopover) return; if (e.key === "ArrowUp") { e.preventDefault(); @@ -100,7 +110,11 @@ function DropdownList({ if (e.key === "ArrowRight" && selectedItem !== null) { e.preventDefault(); + const item = globalItemIndexMap[selectedItem]; + + if (hideArrowRight(item)) return; + goToTwoPage(item); } @@ -109,6 +123,8 @@ function DropdownList({ const item = globalItemIndexMap[selectedItem]; if (item?.url) { OpenURLWithBrowser(item?.url); + } else { + copyToClipboard(item?.payload?.result?.value); } } @@ -161,6 +177,16 @@ function DropdownList({ setSourceData(item); } + const setVisibleContextMenu = useSearchStore( + (state) => state.setVisibleContextMenu + ); + + const handleContextMenu = (event: MouseEvent) => { + event.preventDefault(); + + setVisibleContextMenu(true); + }; + return (
{sourceName} - {items[0]?.source.name}
- { - e.stopPropagation(); - goToTwoPage(items[0]?.document); - }} - > - - - {showIndex && sourceName === selectedName ? ( -
- -
- ) : null} + {!hideArrowRight({ category: sourceName }) && ( + <> + { + e.stopPropagation(); + goToTwoPage(items[0]?.document); + }} + > + + + {showIndex && sourceName === selectedName && ( +
+ +
+ )} + + )}
) : null} @@ -210,22 +240,31 @@ function DropdownList({ const item = hit.document; globalItemIndexMap.push(item); globalIndex++; + + // TODO: 如果没有分类,计算器下面显示一个横线:https://lanhuapp.com/web/#/item/project/detailDetach?pid=fed58f5b-a117-4fe4-a521-c71f2e9b88c3&project_id=fed58f5b-a117-4fe4-a521-c71f2e9b88c3&image_id=a0afd01b-da7d-47c8-818b-90496fb28a71&fromEditor=true return ( - setSelectedItem(currentIndex)} - onItemClick={() => { - if (item?.url) { - OpenURLWithBrowser(item?.url); - } - }} - goToTwoPage={goToTwoPage} - itemRef={(el) => (itemRefs.current[currentIndex] = el)} - /> +
+ {hideArrowRight(item) ? ( +
setSelectedItem(currentIndex)}> + +
+ ) : ( + setSelectedItem(currentIndex)} + onItemClick={() => { + if (item?.url) { + OpenURLWithBrowser(item?.url); + } + }} + goToTwoPage={goToTwoPage} + itemRef={(el) => (itemRefs.current[currentIndex] = el)} + /> + )} +
); })}
diff --git a/src/components/Search/SearchListItem.tsx b/src/components/Search/SearchListItem.tsx index e889ece9..05ebf997 100644 --- a/src/components/Search/SearchListItem.tsx +++ b/src/components/Search/SearchListItem.tsx @@ -1,8 +1,7 @@ -import React, { MouseEvent } from "react"; +import React from "react"; import ItemIcon from "@/components/Common/Icons/ItemIcon"; import ListRight from "./ListRight"; -import { useSearchStore } from "@/stores/searchStore"; import { useAppStore } from "@/stores/appStore"; import { useIsMobile } from "@/hooks/useIsMobile"; @@ -32,16 +31,6 @@ const SearchListItem: React.FC = React.memo( }) => { const isTauri = useAppStore((state) => state.isTauri); - const setVisibleContextMenu = useSearchStore( - (state) => state.setVisibleContextMenu - ); - - const onContextMenu = (event: MouseEvent) => { - event.preventDefault(); - - setVisibleContextMenu(true); - }; - const isMobile = useIsMobile(); return ( @@ -54,13 +43,10 @@ const SearchListItem: React.FC = React.memo( ? "text-white bg-[var(--coco-primary-color)] hover:bg-[var(--coco-primary-color)]" : "text-[#333] dark:text-[#d8d8d8] mobile:bg-gray-200/80 mobile:dark:bg-gray-700/50" } ${showListRight ? "gap-7 mobile:gap-1" : ""}`} - onContextMenu={onContextMenu} >
diff --git a/src/constants/index.ts b/src/constants/index.ts index 7032c5d2..8c35b617 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,3 +1,5 @@ export const POPOVER_PANEL_SELECTOR = '[id^="headlessui-popover-panel"]'; export const HISTORY_PANEL_ID = "headlessui-popover-panel:history-panel"; + +export const CONTEXT_MENU_PANEL_ID = "headlessui-popover-panel:context-menu"; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b49e7439..6f9dd3d4 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -232,7 +232,10 @@ }, "contextMenu": { "open": "Open", - "copyLink": "Copy Link" + "copyLink": "Copy Link", + "copyAnswer": "Copy Answer", + "copyUppercaseAnswer": "Copy Answer (in Word)", + "copyQuestionAndAnswer": "Copy Question and Answer" } }, "assistant": { @@ -373,5 +376,13 @@ "cancel": "Cancel" } } + }, + "calculator": { + "sum": "Sum", + "subtract": "Difference", + "multiply": "Product", + "divide": "Divide", + "remainder": "Remainder", + "expression": "Expression" } } diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index d23dd08c..a04dd786 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -234,7 +234,10 @@ }, "contextMenu": { "open": "打开", - "copyLink": "复制链接" + "copyLink": "复制链接", + "copyAnswer": "复制答案", + "copyUppercaseAnswer": "复制答案(大写)", + "copyQuestionAndAnswer": "复制问题和答案" } }, "assistant": { @@ -374,5 +377,13 @@ "cancel": "取消" } } + }, + "calculator": { + "sum": "求和", + "subtract": "相减", + "multiply": "相乘", + "divide": "相除", + "remainder": "求余", + "expression": "表达式" } } diff --git a/src/stores/appStore.ts b/src/stores/appStore.ts index 58ba92b2..1ce67fba 100644 --- a/src/stores/appStore.ts +++ b/src/stores/appStore.ts @@ -28,7 +28,7 @@ export interface IServer { interface ErrorMessage { id: string; - type: 'error' | 'warning' | 'info'; + type: "error" | "warning" | "info"; message: string; timestamp: number; } @@ -38,7 +38,7 @@ export type IAppStore = { setShowTooltip: (showTooltip: boolean) => void; errors: ErrorMessage[]; - addError: (message: string, type?: 'error' | 'warning' | 'info') => void; + addError: (message: string, type?: "error" | "warning" | "info") => void; removeError: (id: string) => void; clearErrors: () => void; @@ -74,20 +74,23 @@ export const useAppStore = create()( showTooltip: true, setShowTooltip: (showTooltip: boolean) => set({ showTooltip }), errors: [], - addError: (message: string, type: 'error' | 'warning' | 'info' = 'error') => + addError: ( + message: string, + type: "error" | "warning" | "info" = "error" + ) => set((state) => { const newError = { id: Date.now().toString(), type, message, - timestamp: Date.now() + timestamp: Date.now(), }; const updatedErrors = [newError, ...state.errors].slice(0, 5); return { errors: updatedErrors }; }), removeError: (id: string) => set((state) => ({ - errors: state.errors.filter(error => error.id !== id) + errors: state.errors.filter((error) => error.id !== id), })), clearErrors: () => set({ errors: [] }), diff --git a/src/utils/index.ts b/src/utils/index.ts index 34b269cf..9616dc0b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,9 +1,13 @@ import { useEffect, useState } from "react"; import platformAdapter from "./platformAdapter"; +import { useAppStore } from "@/stores/appStore"; // 1 export async function copyToClipboard(text: string) { + const addError = useAppStore.getState().addError; + const language = useAppStore.getState().language; + try { if (window.__TAURI__) { window.__TAURI__.writeText(text); @@ -26,6 +30,8 @@ export async function copyToClipboard(text: string) { } document.body.removeChild(textArea); } + + addError(language === "zh" ? "复制成功" : "Copy Success", "info"); } // 2 @@ -95,4 +101,4 @@ export const isImage = (value: string) => { const regex = /\.(jpe?g|png|webp|avif|gif|svg|bmp|ico|tiff?|heic|apng)$/i; return regex.test(value); -}; \ No newline at end of file +};