mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
refactor: use pizza_engine for app search (#346)
* refactor: use pizza_engine for app search * refactor: do not break the build when pizza_engine is unavailable
This commit is contained in:
50
package.json
50
package.json
@@ -19,36 +19,36 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.0.0",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.2",
|
||||||
"@tauri-apps/api": "^2.4.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
"@tauri-apps/plugin-autostart": "~2.2.0",
|
"@tauri-apps/plugin-autostart": "~2.2.0",
|
||||||
"@tauri-apps/plugin-deep-link": "^2.2.0",
|
"@tauri-apps/plugin-deep-link": "^2.2.1",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||||
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
|
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
|
||||||
"@tauri-apps/plugin-http": "~2.0.2",
|
"@tauri-apps/plugin-http": "~2.0.2",
|
||||||
"@tauri-apps/plugin-os": "^2.2.1",
|
"@tauri-apps/plugin-os": "^2.2.1",
|
||||||
"@tauri-apps/plugin-process": "^2.2.0",
|
"@tauri-apps/plugin-process": "^2.2.1",
|
||||||
"@tauri-apps/plugin-shell": "^2.2.0",
|
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||||
"@tauri-apps/plugin-updater": "github:infinilabs/tauri-plugin-updater#v2",
|
"@tauri-apps/plugin-updater": "github:infinilabs/tauri-plugin-updater#v2",
|
||||||
"@tauri-apps/plugin-websocket": "~2.3.0",
|
"@tauri-apps/plugin-websocket": "~2.3.0",
|
||||||
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-window": "2.0.0-alpha.1",
|
||||||
"@wavesurfer/react": "^1.0.9",
|
"@wavesurfer/react": "^1.0.11",
|
||||||
"ahooks": "^3.8.4",
|
"ahooks": "^3.8.4",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.9.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.5.0",
|
||||||
"filesize": "^10.1.6",
|
"filesize": "^10.1.6",
|
||||||
"i18next": "^23.16.8",
|
"i18next": "^23.16.8",
|
||||||
"i18next-browser-languagedetector": "^8.0.4",
|
"i18next-browser-languagedetector": "^8.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "^0.461.0",
|
"lucide-react": "^0.461.0",
|
||||||
"mermaid": "^11.5.0",
|
"mermaid": "^11.6.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hotkeys-hook": "^4.6.1",
|
"react-hotkeys-hook": "^4.6.2",
|
||||||
"react-i18next": "^15.4.1",
|
"react-i18next": "^15.5.1",
|
||||||
"react-markdown": "^9.1.0",
|
"react-markdown": "^9.1.0",
|
||||||
"react-router-dom": "^6.30.0",
|
"react-router-dom": "^6.30.0",
|
||||||
"react-window": "^1.8.11",
|
"react-window": "^1.8.11",
|
||||||
@@ -58,25 +58,25 @@
|
|||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"tauri-plugin-fs-pro-api": "^2.4.0",
|
"tauri-plugin-fs-pro-api": "^2.4.0",
|
||||||
"tauri-plugin-macos-permissions-api": "^2.2.0",
|
"tauri-plugin-macos-permissions-api": "^2.3.0",
|
||||||
"tauri-plugin-screenshots-api": "^2.1.0",
|
"tauri-plugin-screenshots-api": "^2.2.0",
|
||||||
"tauri-plugin-windows-version-api": "^2.0.0",
|
"tauri-plugin-windows-version-api": "^2.0.0",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"wavesurfer.js": "^7.9.3",
|
"wavesurfer.js": "^7.9.5",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.4.0",
|
"@tauri-apps/cli": "^2.5.0",
|
||||||
"@types/dom-speech-recognition": "^0.0.4",
|
"@types/dom-speech-recognition": "^0.0.4",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^22.13.11",
|
"@types/node": "^22.15.17",
|
||||||
"@types/react": "^18.3.19",
|
"@types/react": "^18.3.21",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^18.3.7",
|
||||||
"@types/react-katex": "^3.0.4",
|
"@types/react-katex": "^3.0.4",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
@@ -85,8 +85,8 @@
|
|||||||
"sass": "^1.87.0",
|
"sass": "^1.87.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"tsup": "^8.4.0",
|
"tsup": "^8.4.0",
|
||||||
"tsx": "^4.19.3",
|
"tsx": "^4.19.4",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^5.4.14"
|
"vite": "^5.4.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1735
pnpm-lock.yaml
generated
1735
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1666
src-tauri/Cargo.lock
generated
1666
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -20,9 +20,15 @@ tauri-build = { version = "2", features = ["default"] }
|
|||||||
default = ["desktop"]
|
default = ["desktop"]
|
||||||
desktop = []
|
desktop = []
|
||||||
cargo-clippy = []
|
cargo-clippy = []
|
||||||
|
# If enabled, dependency `pizza-engine` will be pulled in. Since it is still
|
||||||
|
# private, you need access to the repo to enable this feature, or Coco-AI won't
|
||||||
|
# compile.
|
||||||
|
use_pizza_engine = ["dep:pizza-engine"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pizza-common = { git = "https://github.com/infinilabs/pizza-common", branch = "main" }
|
pizza-common = { git = "https://github.com/infinilabs/pizza-common", branch = "main" }
|
||||||
|
pizza-engine = { git = "https://github.com/infinilabs/pizza", features = ["query_string_parser", "persistence"], optional = true }
|
||||||
|
|
||||||
|
|
||||||
tauri = { version = "2", features = ["protocol-asset", "macos-private-api", "tray-icon", "image-ico", "image-png", "unstable"] }
|
tauri = { version = "2", features = ["protocol-asset", "macos-private-api", "tray-icon", "image-ico", "image-png", "unstable"] }
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
@@ -42,7 +48,7 @@ tauri-plugin-drag = "2"
|
|||||||
tauri-plugin-macos-permissions = "2"
|
tauri-plugin-macos-permissions = "2"
|
||||||
tauri-plugin-fs-pro = "2"
|
tauri-plugin-fs-pro = "2"
|
||||||
tauri-plugin-screenshots = "2"
|
tauri-plugin-screenshots = "2"
|
||||||
applications = { git = "https://github.com/infinilabs/applications-rs", rev = "fb8f475993a2a774ce08d7a58f9f2ac264248a24" }
|
applications = { git = "https://github.com/infinilabs/applications-rs", rev = "1f62cd25651733bf8dc961c2382a39335a26ffe7" }
|
||||||
|
|
||||||
tokio-native-tls = "0.3" # For wss connections
|
tokio-native-tls = "0.3" # For wss connections
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
@@ -61,7 +67,6 @@ hostname = "0.3"
|
|||||||
plist = "1.7"
|
plist = "1.7"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
fuzzy_prefix_search = "0.2"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
|
|||||||
2
src-tauri/rust-toolchain.toml
Normal file
2
src-tauri/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "nightly-2024-10-29"
|
||||||
@@ -55,39 +55,3 @@ pub struct Document {
|
|||||||
pub owner: Option<UserInfo>,
|
pub owner: Option<UserInfo>,
|
||||||
pub last_updated_by: Option<EditorInfo>,
|
pub last_updated_by: Option<EditorInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Document {
|
|
||||||
pub fn new(
|
|
||||||
source: Option<DataSourceReference>,
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use crate::server::servers::{load_or_insert_default_server, load_servers_token};
|
|||||||
use autostart::{change_autostart, enable_autostart};
|
use autostart::{change_autostart, enable_autostart};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
use std::sync::OnceLock;
|
||||||
use tauri::async_runtime::block_on;
|
use tauri::async_runtime::block_on;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use tauri::ActivationPolicy;
|
use tauri::ActivationPolicy;
|
||||||
@@ -30,6 +31,10 @@ lazy_static! {
|
|||||||
static ref PREVIOUS_MONITOR_NAME: Mutex<Option<String>> = Mutex::new(None);
|
static ref PREVIOUS_MONITOR_NAME: Mutex<Option<String>> = Mutex::new(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// To allow us to access tauri's `AppHandle` when its context is inaccessible,
|
||||||
|
/// store it globally. It will be set in `init()`.
|
||||||
|
pub(crate) static GLOBAL_TAURI_APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn change_window_height(handle: AppHandle, height: u32) {
|
async fn change_window_height(handle: AppHandle, height: u32) {
|
||||||
let window: WebviewWindow = handle.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
|
let window: WebviewWindow = handle.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
|
||||||
@@ -134,13 +139,29 @@ pub fn run() {
|
|||||||
server::attachment::get_attachment,
|
server::attachment::get_attachment,
|
||||||
server::attachment::delete_attachment,
|
server::attachment::delete_attachment,
|
||||||
server::transcription::transcription,
|
server::transcription::transcription,
|
||||||
local::application::get_default_search_paths,
|
|
||||||
local::application::list_app_with_metadata_in,
|
|
||||||
util::open,
|
util::open,
|
||||||
server::system_settings::get_system_settings,
|
server::system_settings::get_system_settings,
|
||||||
simulate_mouse_click
|
simulate_mouse_click,
|
||||||
|
local::get_disabled_local_query_sources,
|
||||||
|
local::enable_local_query_source,
|
||||||
|
local::disable_local_query_source,
|
||||||
|
local::application::get_app_list,
|
||||||
|
local::application::get_app_search_path,
|
||||||
|
local::application::get_app_metadata,
|
||||||
|
local::application::set_app_alias,
|
||||||
|
local::application::register_app_hotkey,
|
||||||
|
local::application::unregister_app_hotkey,
|
||||||
|
local::application::disable_app_search,
|
||||||
|
local::application::enable_app_search,
|
||||||
|
local::application::add_app_search_path,
|
||||||
|
local::application::remove_app_search_path,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
GLOBAL_TAURI_APP_HANDLE
|
||||||
|
.set(app_handle.clone())
|
||||||
|
.expect("variable already initialized");
|
||||||
|
|
||||||
let registry = SearchSourceRegistry::default();
|
let registry = SearchSourceRegistry::default();
|
||||||
|
|
||||||
app.manage(registry); // Store registry in Tauri's app state
|
app.manage(registry); // Store registry in Tauri's app state
|
||||||
@@ -234,19 +255,8 @@ pub async fn init<R: Runtime>(app_handle: &AppHandle<R>) {
|
|||||||
crate::server::servers::try_register_server_to_search_source(app_handle.clone(), &server)
|
crate::server::servers::try_register_server_to_search_source(app_handle.clone(), &server)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async fn init_app_search_source<R: Runtime>(app_handle: &AppHandle<R>) -> Result<(), String> {
|
local::start_pizza_engine_runtime();
|
||||||
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::<SearchSourceRegistry>();
|
|
||||||
registry.register_source(application_search).await;
|
|
||||||
registry.register_source(calculator_search).await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -402,7 +412,7 @@ fn open_settings(app: &tauri::AppHandle) {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||||
init_app_search_source(&app_handle).await?;
|
local::init_local_search_source(&app_handle).await?;
|
||||||
let _ = server::connector::refresh_all_connectors(&app_handle).await;
|
let _ = server::connector::refresh_all_connectors(&app_handle).await;
|
||||||
let _ = server::datasource::refresh_all_datasources(&app_handle).await;
|
let _ = server::datasource::refresh_all_datasources(&app_handle).await;
|
||||||
|
|
||||||
|
|||||||
@@ -1,313 +0,0 @@
|
|||||||
use crate::common::document::{DataSourceReference, Document};
|
|
||||||
use crate::common::error::SearchError;
|
|
||||||
use crate::common::search::{QueryResponse, QuerySource, SearchQuery};
|
|
||||||
use crate::common::traits::SearchSource;
|
|
||||||
use crate::local::LOCAL_QUERY_SOURCE_TYPE;
|
|
||||||
use applications::App;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use fuzzy_prefix_search::Trie;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tauri::{AppHandle, Runtime};
|
|
||||||
use tauri_plugin_fs_pro::{icon, metadata, name, IconOptions};
|
|
||||||
|
|
||||||
const DATA_SOURCE_ID: &str = "Applications";
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_default_search_paths() -> Vec<String> {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
return vec![
|
|
||||||
"/Applications".into(),
|
|
||||||
"/System/Applications".into(),
|
|
||||||
"/System/Library/CoreServices".into(),
|
|
||||||
];
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
{
|
|
||||||
let paths = applications::get_default_search_paths();
|
|
||||||
let mut ret = Vec::with_capacity(paths.len());
|
|
||||||
for search_path in paths {
|
|
||||||
let path_string = search_path
|
|
||||||
.into_os_string()
|
|
||||||
.into_string()
|
|
||||||
.expect("path should be UTF-8 encoded");
|
|
||||||
|
|
||||||
ret.push(path_string);
|
|
||||||
}
|
|
||||||
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to return `app`'s path.
|
|
||||||
///
|
|
||||||
/// * Windows: return the path to application's exe
|
|
||||||
/// * macOS: return the path to the `.app` bundle
|
|
||||||
/// * Linux: return the path to the `.desktop` file
|
|
||||||
fn get_app_path(app: &App) -> PathBuf {
|
|
||||||
if cfg!(target_os = "windows") {
|
|
||||||
assert!(
|
|
||||||
app.icon_path.is_some(),
|
|
||||||
"we only accept Applications with icons"
|
|
||||||
);
|
|
||||||
app.app_path_exe
|
|
||||||
.as_ref()
|
|
||||||
.expect("icon is Some, exe path should be Some as well")
|
|
||||||
.to_path_buf()
|
|
||||||
} else {
|
|
||||||
app.app_desktop_path.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to return `app`'s path.
|
|
||||||
///
|
|
||||||
/// * Windows/macOS: extract `app_path`'s file name and remove the file extension
|
|
||||||
/// * Linux: return the name specified in `.desktop` file
|
|
||||||
async fn get_app_name(app: &App) -> String {
|
|
||||||
if cfg!(target_os = "linux") {
|
|
||||||
app.name.clone()
|
|
||||||
} else {
|
|
||||||
let app_path = get_app_path(app);
|
|
||||||
name(app_path.clone()).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to return an absolute path to `app`'s icon.
|
|
||||||
///
|
|
||||||
/// On macOS/Windows, we cache icons in our data directory using the `icon()` function.
|
|
||||||
async fn get_app_icon_path<R: Runtime>(
|
|
||||||
tauri_app_handle: &AppHandle<R>,
|
|
||||||
app: &App,
|
|
||||||
) -> Result<PathBuf, String> {
|
|
||||||
if cfg!(target_os = "linux") {
|
|
||||||
let icon_path = app
|
|
||||||
.icon_path
|
|
||||||
.as_ref()
|
|
||||||
.expect("We only accept applications with icons")
|
|
||||||
.to_path_buf();
|
|
||||||
|
|
||||||
Ok(icon_path)
|
|
||||||
} else {
|
|
||||||
let app_path = get_app_path(app);
|
|
||||||
let options = IconOptions {
|
|
||||||
size: Some(256),
|
|
||||||
save_path: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
icon(tauri_app_handle.clone(), app_path, Some(options))
|
|
||||||
.await
|
|
||||||
.map_err(|err| err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return all the Apps found under `search_path`.
|
|
||||||
///
|
|
||||||
/// Note: apps with no icons will be filtered out.
|
|
||||||
fn list_app_in(search_path: Vec<String>) -> Result<Vec<App>, String> {
|
|
||||||
let search_path = search_path
|
|
||||||
.into_iter()
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let apps = applications::get_all_apps(&search_path).map_err(|err| err.to_string())?;
|
|
||||||
|
|
||||||
Ok(apps
|
|
||||||
.into_iter()
|
|
||||||
.filter(|app| app.icon_path.is_some())
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct AppMetadata {
|
|
||||||
name: String,
|
|
||||||
r#where: PathBuf,
|
|
||||||
size: u64,
|
|
||||||
icon: PathBuf,
|
|
||||||
created: u128,
|
|
||||||
modified: u128,
|
|
||||||
last_opened: u128,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List apps that are in the `search_path`.
|
|
||||||
///
|
|
||||||
/// Different from `list_app_in()`, every app is JSON object containing its metadata, e.g.:
|
|
||||||
///
|
|
||||||
/// ```json
|
|
||||||
/// {
|
|
||||||
/// "name": "Finder",
|
|
||||||
/// "where": "/System/Library/CoreServices",
|
|
||||||
/// "size": 49283072,
|
|
||||||
/// "icon": "/xxx.png",
|
|
||||||
/// "created": 1744625204,
|
|
||||||
/// "modified": 1744625204,
|
|
||||||
/// "lastOpened": 1744625250
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn list_app_with_metadata_in<R: Runtime>(
|
|
||||||
app_handle: AppHandle<R>,
|
|
||||||
search_path: Vec<String>,
|
|
||||||
) -> Result<Vec<AppMetadata>, String> {
|
|
||||||
let apps = list_app_in(search_path)?;
|
|
||||||
|
|
||||||
let mut apps_with_meta = Vec::with_capacity(apps.len());
|
|
||||||
|
|
||||||
// name version where Type(hardcoded Application) Size Created Modify
|
|
||||||
for app in apps.iter() {
|
|
||||||
let app_path = get_app_path(app);
|
|
||||||
let app_name = get_app_name(app).await;
|
|
||||||
let app_path_where = {
|
|
||||||
let mut app_path_clone = app_path.clone();
|
|
||||||
let truncated = app_path_clone.pop();
|
|
||||||
if !truncated {
|
|
||||||
panic!("every app file should live somewhere");
|
|
||||||
}
|
|
||||||
|
|
||||||
app_path_clone
|
|
||||||
};
|
|
||||||
let icon = get_app_icon_path(&app_handle, app).await?;
|
|
||||||
|
|
||||||
let raw_app_metadata = metadata(app_path.clone(), None).await?;
|
|
||||||
|
|
||||||
let app_metadata = AppMetadata {
|
|
||||||
name: app_name,
|
|
||||||
r#where: app_path_where,
|
|
||||||
size: raw_app_metadata.size,
|
|
||||||
icon,
|
|
||||||
created: raw_app_metadata.created_at,
|
|
||||||
modified: raw_app_metadata.modified_at,
|
|
||||||
last_opened: raw_app_metadata.accessed_at,
|
|
||||||
};
|
|
||||||
|
|
||||||
apps_with_meta.push(app_metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(apps_with_meta)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ApplicationSearchSource {
|
|
||||||
base_score: f64,
|
|
||||||
// app name -> app icon path
|
|
||||||
icons: HashMap<String, PathBuf>,
|
|
||||||
application_paths: Trie<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApplicationSearchSource {
|
|
||||||
pub async fn new<R: Runtime>(
|
|
||||||
app_handle: AppHandle<R>,
|
|
||||||
base_score: f64,
|
|
||||||
) -> Result<Self, String> {
|
|
||||||
let application_paths = Trie::new();
|
|
||||||
let mut icons = HashMap::new();
|
|
||||||
|
|
||||||
let default_search_path = get_default_search_paths();
|
|
||||||
let apps = list_app_in(default_search_path)?;
|
|
||||||
|
|
||||||
for app in &apps {
|
|
||||||
let app_path = get_app_path(app);
|
|
||||||
let app_name = get_app_name(app).await;
|
|
||||||
let app_icon_path = get_app_icon_path(&app_handle, app).await?;
|
|
||||||
|
|
||||||
if app_name.is_empty() || app_name.eq("Coco-AI") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
application_paths.insert(&app_name, app_path);
|
|
||||||
icons.insert(app_name, app_icon_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ApplicationSearchSource {
|
|
||||||
base_score,
|
|
||||||
icons,
|
|
||||||
application_paths,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl SearchSource for ApplicationSearchSource {
|
|
||||||
fn get_type(&self) -> QuerySource {
|
|
||||||
QuerySource {
|
|
||||||
r#type: LOCAL_QUERY_SOURCE_TYPE.into(),
|
|
||||||
name: hostname::get()
|
|
||||||
.unwrap_or("My Computer".into())
|
|
||||||
.to_string_lossy()
|
|
||||||
.into(),
|
|
||||||
id: DATA_SOURCE_ID.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn search(&self, query: SearchQuery) -> Result<QueryResponse, SearchError> {
|
|
||||||
let query_string = query
|
|
||||||
.query_strings
|
|
||||||
.get("query")
|
|
||||||
.unwrap_or(&"".to_string())
|
|
||||||
.to_lowercase();
|
|
||||||
|
|
||||||
if query_string.is_empty() {
|
|
||||||
return Ok(QueryResponse {
|
|
||||||
source: self.get_type(),
|
|
||||||
hits: Vec::new(),
|
|
||||||
total_hits: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut total_hits = 0;
|
|
||||||
let mut hits = Vec::new();
|
|
||||||
|
|
||||||
let query_string_len = query_string.len();
|
|
||||||
let mut results = self
|
|
||||||
.application_paths
|
|
||||||
.search_within_distance_scored(&query_string, query_string_len - 1);
|
|
||||||
|
|
||||||
// Check for NaN or extreme score values and handle them properly
|
|
||||||
results.sort_by(|a, b| {
|
|
||||||
// If either score is NaN, consider them equal (you can customize this logic as needed)
|
|
||||||
if a.score.is_nan() || b.score.is_nan() {
|
|
||||||
std::cmp::Ordering::Equal
|
|
||||||
} else {
|
|
||||||
// Otherwise, compare the scores as usual
|
|
||||||
b.score
|
|
||||||
.partial_cmp(&a.score)
|
|
||||||
.unwrap_or(std::cmp::Ordering::Equal)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if !results.is_empty() {
|
|
||||||
for result in results {
|
|
||||||
let app_name = result.word;
|
|
||||||
let app_path = result.data.first().unwrap().clone();
|
|
||||||
let app_path_string = app_path.to_string_lossy().into_owned();
|
|
||||||
|
|
||||||
total_hits += 1;
|
|
||||||
|
|
||||||
let mut doc = Document::new(
|
|
||||||
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(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Attach icon if available
|
|
||||||
if let Some(icon_path) = self.icons.get(app_name.as_str()) {
|
|
||||||
doc.icon = Some(icon_path.as_os_str().to_str().unwrap().to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
hits.push((doc, self.base_score + result.score as f64));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(QueryResponse {
|
|
||||||
source: self.get_type(),
|
|
||||||
hits,
|
|
||||||
total_hits,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
38
src-tauri/src/local/application/mod.rs
Normal file
38
src-tauri/src/local/application/mod.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[cfg(feature = "use_pizza_engine")]
|
||||||
|
mod with_feature;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "use_pizza_engine"))]
|
||||||
|
mod without_feature;
|
||||||
|
|
||||||
|
#[cfg(feature = "use_pizza_engine")]
|
||||||
|
pub use with_feature::*;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "use_pizza_engine"))]
|
||||||
|
pub use without_feature::*;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AppEntry {
|
||||||
|
path: String,
|
||||||
|
name: String,
|
||||||
|
icon_path: String,
|
||||||
|
alias: String,
|
||||||
|
hotkey: String,
|
||||||
|
is_disabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AppMetadata {
|
||||||
|
name: String,
|
||||||
|
r#where: String,
|
||||||
|
size: u64,
|
||||||
|
icon: String,
|
||||||
|
created: u128,
|
||||||
|
modified: u128,
|
||||||
|
last_opened: u128,
|
||||||
|
}
|
||||||
1109
src-tauri/src/local/application/with_feature.rs
Normal file
1109
src-tauri/src/local/application/with_feature.rs
Normal file
File diff suppressed because it is too large
Load Diff
121
src-tauri/src/local/application/without_feature.rs
Normal file
121
src-tauri/src/local/application/without_feature.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
use crate::common::error::SearchError;
|
||||||
|
use crate::common::search::{QueryResponse, QuerySource, SearchQuery};
|
||||||
|
use crate::common::traits::SearchSource;
|
||||||
|
use crate::local::LOCAL_QUERY_SOURCE_TYPE;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use tauri::{AppHandle, Runtime};
|
||||||
|
use super::AppEntry;
|
||||||
|
use super::AppMetadata;
|
||||||
|
|
||||||
|
pub(crate) const QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME: &str = "Applications";
|
||||||
|
|
||||||
|
pub struct ApplicationSearchSource;
|
||||||
|
|
||||||
|
impl ApplicationSearchSource {
|
||||||
|
pub async fn init<R: Runtime>(_app_handle: AppHandle<R>) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SearchSource for ApplicationSearchSource {
|
||||||
|
fn get_type(&self) -> QuerySource {
|
||||||
|
QuerySource {
|
||||||
|
r#type: LOCAL_QUERY_SOURCE_TYPE.into(),
|
||||||
|
name: hostname::get()
|
||||||
|
.unwrap_or("My Computer".into())
|
||||||
|
.to_string_lossy()
|
||||||
|
.into(),
|
||||||
|
id: QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn search(&self, _query: SearchQuery) -> Result<QueryResponse, SearchError> {
|
||||||
|
Ok(QueryResponse {
|
||||||
|
source: self.get_type(),
|
||||||
|
hits: Vec::new(),
|
||||||
|
total_hits: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn set_app_alias(_app_path: String, _alias: String) -> Result<(), String> {
|
||||||
|
unreachable!("app list should be empty, there is no way this can be invoked")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn register_app_hotkey<R: Runtime>(
|
||||||
|
_tauri_app_handle: AppHandle<R>,
|
||||||
|
_app_path: String,
|
||||||
|
_hotkey: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
unreachable!("app list should be empty, there is no way this can be invoked")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn unregister_app_hotkey<R: Runtime>(
|
||||||
|
_tauri_app_handle: AppHandle<R>,
|
||||||
|
_app_path: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
unreachable!("app list should be empty, there is no way this can be invoked")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn disable_app_search<R: Runtime>(
|
||||||
|
_tauri_app_handle: AppHandle<R>,
|
||||||
|
_app_path: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// no-op
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn enable_app_search<R: Runtime>(
|
||||||
|
_tauri_app_handle: AppHandle<R>,
|
||||||
|
_app_path: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// no-op
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn add_app_search_path<R: Runtime>(
|
||||||
|
_tauri_app_handle: AppHandle<R>,
|
||||||
|
_search_path: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// no-op
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn remove_app_search_path<R: Runtime>(
|
||||||
|
_tauri_app_handle: AppHandle<R>,
|
||||||
|
_search_path: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// no-op
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_app_search_path<R: Runtime>(_tauri_app_handle: AppHandle<R>) -> Vec<String> {
|
||||||
|
// Return an empty list
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_app_list<R: Runtime>(
|
||||||
|
_tauri_app_handle: AppHandle<R>,
|
||||||
|
) -> Result<Vec<AppEntry>, String> {
|
||||||
|
// Return an empty list
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_app_metadata<R: Runtime>(
|
||||||
|
_tauri_app_handle: AppHandle<R>,
|
||||||
|
_app_path: String,
|
||||||
|
) -> Result<AppMetadata, String> {
|
||||||
|
unreachable!("app list should be empty, there is no way this can be invoked")
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ use num2words::Num2Words;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
const DATA_SOURCE_ID: &str = "Calculator";
|
pub(crate) const DATA_SOURCE_ID: &str = "Calculator";
|
||||||
|
|
||||||
pub struct CalculatorSource {
|
pub struct CalculatorSource {
|
||||||
base_score: f64,
|
base_score: f64,
|
||||||
|
|||||||
@@ -2,4 +2,163 @@ pub mod application;
|
|||||||
pub mod calculator;
|
pub mod calculator;
|
||||||
pub mod file_system;
|
pub mod file_system;
|
||||||
|
|
||||||
|
use std::any::Any;
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use crate::common::register::SearchSourceRegistry;
|
||||||
|
use serde_json::Value as Json;
|
||||||
|
use tauri::{AppHandle, Manager, Runtime};
|
||||||
|
use tauri_plugin_store::StoreExt;
|
||||||
|
|
||||||
pub const LOCAL_QUERY_SOURCE_TYPE: &str = "local";
|
pub const LOCAL_QUERY_SOURCE_TYPE: &str = "local";
|
||||||
|
pub const TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE: &str = "local_query_source_enabled_state";
|
||||||
|
|
||||||
|
trait SearchSourceState {
|
||||||
|
#[cfg_attr(not(feature = "use_pizza_engine"), allow(unused))]
|
||||||
|
fn as_mut_any(&mut self) -> &mut dyn Any;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
trait Task: Send + Sync {
|
||||||
|
fn search_source_id(&self) -> &'static str;
|
||||||
|
|
||||||
|
async fn exec(&mut self, state: &mut Option<Box<dyn SearchSourceState>>);
|
||||||
|
}
|
||||||
|
|
||||||
|
static RUNTIME_TX: OnceLock<tokio::sync::mpsc::UnboundedSender<Box<dyn Task>>> = OnceLock::new();
|
||||||
|
|
||||||
|
pub(crate) fn start_pizza_engine_runtime() {
|
||||||
|
std::thread::spawn(|| {
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
|
||||||
|
let main = async {
|
||||||
|
let mut states: HashMap<String, Option<Box<dyn SearchSourceState>>> = HashMap::new();
|
||||||
|
|
||||||
|
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
RUNTIME_TX.set(tx).unwrap();
|
||||||
|
|
||||||
|
while let Some(mut task) = rx.recv().await {
|
||||||
|
let opt_search_source_state = match states.entry(task.search_source_id().into()) {
|
||||||
|
Entry::Occupied(o) => o.into_mut(),
|
||||||
|
Entry::Vacant(v) => v.insert(None),
|
||||||
|
};
|
||||||
|
task.exec(opt_search_source_state).await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rt.block_on(main);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn init_local_search_source<R: Runtime>(
|
||||||
|
app_handle: &AppHandle<R>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let enabled_status_store = app_handle
|
||||||
|
.store(TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if enabled_status_store.is_empty() {
|
||||||
|
enabled_status_store.set(
|
||||||
|
application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME,
|
||||||
|
Json::Bool(true),
|
||||||
|
);
|
||||||
|
enabled_status_store.set(calculator::DATA_SOURCE_ID, Json::Bool(true));
|
||||||
|
}
|
||||||
|
let registry = app_handle.state::<SearchSourceRegistry>();
|
||||||
|
|
||||||
|
application::ApplicationSearchSource::init(app_handle.clone()).await?;
|
||||||
|
|
||||||
|
for (id, enabled) in enabled_status_store.entries() {
|
||||||
|
let enabled = match enabled {
|
||||||
|
Json::Bool(b) => b,
|
||||||
|
_ => unreachable!("enabled state should be stored as a boolean"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
if id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||||
|
registry
|
||||||
|
.register_source(application::ApplicationSearchSource)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == calculator::DATA_SOURCE_ID {
|
||||||
|
let calculator_search = calculator::CalculatorSource::new(2000f64);
|
||||||
|
registry.register_source(calculator_search).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_disabled_local_query_sources<R: Runtime>(app_handle: AppHandle<R>) -> Vec<String> {
|
||||||
|
let enabled_status_store = app_handle
|
||||||
|
.store(TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"tauri store [{}] should exist and be loaded, but that's not true due to error [{}]",
|
||||||
|
TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE, e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let mut disabled_local_query_sources = Vec::new();
|
||||||
|
|
||||||
|
for (id, enabled) in enabled_status_store.entries() {
|
||||||
|
let enabled = match enabled {
|
||||||
|
Json::Bool(b) => b,
|
||||||
|
_ => unreachable!("enabled state should be stored as a boolean"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !enabled {
|
||||||
|
disabled_local_query_sources.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disabled_local_query_sources
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn enable_local_query_source<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
query_source_id: String,
|
||||||
|
) {
|
||||||
|
let registry = app_handle.state::<SearchSourceRegistry>();
|
||||||
|
if query_source_id == application::QUERYSOURCE_ID_DATASOURCE_ID_DATASOURCE_NAME {
|
||||||
|
let application_search = application::ApplicationSearchSource;
|
||||||
|
registry.register_source(application_search).await;
|
||||||
|
}
|
||||||
|
if query_source_id == calculator::DATA_SOURCE_ID {
|
||||||
|
let calculator_search = calculator::CalculatorSource::new(2000f64);
|
||||||
|
registry.register_source(calculator_search).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let enabled_status_store = app_handle
|
||||||
|
.store(TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"tauri store [{}] should exist and be loaded, but that's not true due to error [{}]",
|
||||||
|
TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE, e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
enabled_status_store.set(query_source_id, Json::Bool(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn disable_local_query_source<R: Runtime>(
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
query_source_id: String,
|
||||||
|
) {
|
||||||
|
let registry = app_handle.state::<SearchSourceRegistry>();
|
||||||
|
registry.remove_source(&query_source_id).await;
|
||||||
|
|
||||||
|
let enabled_status_store = app_handle
|
||||||
|
.store(TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"tauri store [{}] should exist and be loaded, but that's not true due to error [{}]",
|
||||||
|
TAURI_STORE_LOCAL_QUERY_SOURCE_ENABLED_STATE, e
|
||||||
|
)
|
||||||
|
});
|
||||||
|
enabled_status_store.set(query_source_id, Json::Bool(false));
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ pub async fn query_coco_fusion<R: Runtime>(
|
|||||||
timeout(timeout_duration, async {
|
timeout(timeout_duration, async {
|
||||||
query_source_clone.search(query).await
|
query_source_clone.search(query).await
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const sourceData = useSearchStore((state) => state.sourceData);
|
const sourceData = useSearchStore((state) => state.sourceData);
|
||||||
const isTauri = useAppStore((state) => state.isTauri);
|
const isTauri = useAppStore((state) => state.isTauri);
|
||||||
const queryTimeout = useConnectStore((state) => state.querySourceTimeout);
|
const querySourceTimeout = useConnectStore((state) => {
|
||||||
|
return state.querySourceTimeout;
|
||||||
|
});
|
||||||
|
|
||||||
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
const [selectedItem, setSelectedItem] = useState<number | null>(null);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
@@ -43,6 +45,11 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
const [isKeyboardMode, setIsKeyboardMode] = useState(false);
|
const [isKeyboardMode, setIsKeyboardMode] = useState(false);
|
||||||
|
|
||||||
|
const querySourceTimeoutRef = useRef(querySourceTimeout);
|
||||||
|
useEffect(() => {
|
||||||
|
querySourceTimeoutRef.current = querySourceTimeout;
|
||||||
|
}, [querySourceTimeout]);
|
||||||
|
|
||||||
const { data, loading } = useInfiniteScroll(
|
const { data, loading } = useInfiniteScroll(
|
||||||
async (d) => {
|
async (d) => {
|
||||||
const from = d?.list?.length || 0;
|
const from = d?.list?.length || 0;
|
||||||
@@ -65,7 +72,7 @@ export const DocumentList: React.FC<DocumentListProps> = ({
|
|||||||
from: from,
|
from: from,
|
||||||
size: PAGE_SIZE,
|
size: PAGE_SIZE,
|
||||||
queryStrings: queryStrings,
|
queryStrings: queryStrings,
|
||||||
queryTimeout,
|
queryTimeout: querySourceTimeoutRef.current,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let url = `/query/_search?query=${queryStrings.query}&datasource=${queryStrings.datasource}&from=${from}&size=${PAGE_SIZE}`;
|
let url = `/query/_search?query=${queryStrings.query}&datasource=${queryStrings.datasource}&from=${from}&size=${PAGE_SIZE}`;
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ function Search({
|
|||||||
setWindowAlwaysOnTop,
|
setWindowAlwaysOnTop,
|
||||||
}: SearchProps) {
|
}: SearchProps) {
|
||||||
const sourceData = useSearchStore((state) => state.sourceData);
|
const sourceData = useSearchStore((state) => state.sourceData);
|
||||||
const queryTimeout = useConnectStore((state) => state.querySourceTimeout);
|
const querySourceTimeout = useConnectStore((state) => {
|
||||||
|
return state.querySourceTimeout;
|
||||||
|
});
|
||||||
|
|
||||||
const [IsError, setIsError] = useState<any[]>([]);
|
const [IsError, setIsError] = useState<any[]>([]);
|
||||||
const [suggests, setSuggests] = useState<any[]>([]);
|
const [suggests, setSuggests] = useState<any[]>([]);
|
||||||
@@ -54,6 +56,11 @@ function Search({
|
|||||||
|
|
||||||
const mainWindowRef = useRef<HTMLDivElement>(null);
|
const mainWindowRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const querySourceTimeoutRef = useRef(querySourceTimeout);
|
||||||
|
useEffect(() => {
|
||||||
|
querySourceTimeoutRef.current = querySourceTimeout;
|
||||||
|
}, [querySourceTimeout]);
|
||||||
|
|
||||||
const getSuggest = useCallback(
|
const getSuggest = useCallback(
|
||||||
async (searchInput: string) => {
|
async (searchInput: string) => {
|
||||||
if (!searchInput) return;
|
if (!searchInput) return;
|
||||||
@@ -65,7 +72,7 @@ function Search({
|
|||||||
from: 0,
|
from: 0,
|
||||||
size: 10,
|
size: 10,
|
||||||
queryStrings: { query: searchInput },
|
queryStrings: { query: searchInput },
|
||||||
queryTimeout: queryTimeout,
|
queryTimeout: querySourceTimeoutRef.current,
|
||||||
});
|
});
|
||||||
if (response && typeof response === "object" && "failed" in response) {
|
if (response && typeof response === "object" && "failed" in response) {
|
||||||
const failedResult = response as any;
|
const failedResult = response as any;
|
||||||
|
|||||||
@@ -110,25 +110,23 @@ function SearchChat({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
if (!mounted) return;
|
|
||||||
await initializeListeners();
|
await initializeListeners();
|
||||||
await initializeListeners_auth();
|
await initializeListeners_auth();
|
||||||
await platformAdapter.invokeBackend("get_app_search_source");
|
await platformAdapter.invokeBackend("get_app_search_source");
|
||||||
if (theme && mounted) {
|
|
||||||
setTheme(theme);
|
platformAdapter.emitEvent("search-source-loaded");
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
return () => {
|
|
||||||
mounted = false;
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!theme) return;
|
||||||
|
|
||||||
|
setTheme(theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
const chatAIRef = useRef<ChatAIRef>(null);
|
const chatAIRef = useRef<ChatAIRef>(null);
|
||||||
|
|
||||||
const changeMode = useCallback(async (value: boolean) => {
|
const changeMode = useCallback(async (value: boolean) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ChevronRight } from "lucide-react";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
||||||
import { ExtensionsContext, Plugin } from "../..";
|
import { ExtensionsContext, Plugin } from "../..";
|
||||||
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
|
|
||||||
interface AccordionProps extends Plugin {}
|
interface AccordionProps extends Plugin {}
|
||||||
|
|
||||||
@@ -14,11 +15,10 @@ const Accordion: FC<AccordionProps> = (props) => {
|
|||||||
type = "Extension",
|
type = "Extension",
|
||||||
alias = "-",
|
alias = "-",
|
||||||
hotKey = "-",
|
hotKey = "-",
|
||||||
enabled = true,
|
|
||||||
content,
|
content,
|
||||||
} = props;
|
} = props;
|
||||||
const { activeId, setActiveId } = useContext(ExtensionsContext);
|
const { activeId, setActiveId, disabledExtensions, setDisabledExtensions } =
|
||||||
|
useContext(ExtensionsContext);
|
||||||
const [expand, setExpand] = useState(false);
|
const [expand, setExpand] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,12 +56,37 @@ const Accordion: FC<AccordionProps> = (props) => {
|
|||||||
<div className="flex-1">{type}</div>
|
<div className="flex-1">{type}</div>
|
||||||
<div className="flex-1">{alias}</div>
|
<div className="flex-1">{alias}</div>
|
||||||
<div className="flex-1">{hotKey}</div>
|
<div className="flex-1">{hotKey}</div>
|
||||||
<div className="flex-1 flex items-center justify-end">
|
<div
|
||||||
|
className="flex-1 flex items-center justify-end"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
label=""
|
label=""
|
||||||
checked={enabled}
|
checked={!disabledExtensions.includes(id)}
|
||||||
className="scale-75"
|
className="scale-75"
|
||||||
onChange={() => {}}
|
onChange={(value) => {
|
||||||
|
console.log("value", value);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
setDisabledExtensions(
|
||||||
|
disabledExtensions.filter((extensionId) => {
|
||||||
|
return extensionId !== id;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
platformAdapter.invokeBackend("enable_local_query_source", {
|
||||||
|
querySourceId: id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setDisabledExtensions(disabledExtensions.concat(id));
|
||||||
|
|
||||||
|
platformAdapter.invokeBackend("disable_local_query_source", {
|
||||||
|
querySourceId: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,36 +1,102 @@
|
|||||||
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
import SettingsToggle from "@/components/Settings/SettingsToggle";
|
||||||
import { useApplicationsStore } from "@/stores/applicationsStore";
|
import { Application, useApplicationsStore } from "@/stores/applicationsStore";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { ExtensionsContext } from "../../..";
|
import { ExtensionsContext } from "../../..";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useDebounceFn } from "ahooks";
|
||||||
|
import SettingsInput from "@/components/Settings/SettingsInput";
|
||||||
|
import Shortcut from "../../Shortcut";
|
||||||
|
|
||||||
const Applications = () => {
|
const Applications = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { activeId, setActiveId } = useContext(ExtensionsContext);
|
const { activeId, setActiveId } = useContext(ExtensionsContext);
|
||||||
|
|
||||||
const allApps = useApplicationsStore((state) => state.allApps);
|
const allApps = useApplicationsStore((state) => state.allApps);
|
||||||
const disabledApps = useApplicationsStore((state) => state.disabledApps);
|
const setAllApps = useApplicationsStore((state) => state.setAllApps);
|
||||||
const setDisabledApps = useApplicationsStore((state) => {
|
|
||||||
return state.setDisabledApps;
|
const handleDisable = (app: Application) => {
|
||||||
});
|
const { path, isDisabled } = app;
|
||||||
|
|
||||||
|
const nextApps = allApps.map((item) => {
|
||||||
|
if (item.path !== path) return item;
|
||||||
|
|
||||||
|
return { ...item, isDisabled: !isDisabled };
|
||||||
|
});
|
||||||
|
setAllApps(nextApps);
|
||||||
|
|
||||||
|
if (isDisabled) {
|
||||||
|
platformAdapter.invokeBackend("enable_app_search", {
|
||||||
|
appPath: path,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
platformAdapter.invokeBackend("disable_app_search", {
|
||||||
|
appPath: path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { run: handleAlias } = useDebounceFn(
|
||||||
|
(app: Application, alias: string) => {
|
||||||
|
const { path } = app;
|
||||||
|
|
||||||
|
platformAdapter.invokeBackend("set_app_alias", {
|
||||||
|
appPath: path,
|
||||||
|
alias,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextApps = allApps.map((item) => {
|
||||||
|
if (item.path !== path) return item;
|
||||||
|
|
||||||
|
return { ...item, alias };
|
||||||
|
});
|
||||||
|
|
||||||
|
setAllApps(nextApps);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleHotkey = (app: Application, hotkey: string) => {
|
||||||
|
const { path } = app;
|
||||||
|
|
||||||
|
if (hotkey) {
|
||||||
|
platformAdapter.invokeBackend("register_app_hotkey", {
|
||||||
|
appPath: path,
|
||||||
|
hotkey,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
platformAdapter.invokeBackend("unregister_app_hotkey", {
|
||||||
|
appPath: path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextApps = allApps.map((item) => {
|
||||||
|
if (item.path !== path) return item;
|
||||||
|
|
||||||
|
return { ...item, hotkey };
|
||||||
|
});
|
||||||
|
|
||||||
|
setAllApps(nextApps);
|
||||||
|
};
|
||||||
|
|
||||||
return allApps.map((app) => {
|
return allApps.map((app) => {
|
||||||
const { name, icon } = app;
|
const { name, path, iconPath, isDisabled, alias, hotkey } = app;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={name}
|
key={path}
|
||||||
className={clsx("flex items-center h-8 -mx-2 pl-10 pr-2 rounded-md", {
|
className={clsx("flex items-center h-8 -mx-2 pl-10 pr-2 rounded-md", {
|
||||||
"bg-[#f0f6fe] dark:bg-gray-700": name === activeId,
|
"bg-[#f0f6fe] dark:bg-gray-700": path === activeId,
|
||||||
})}
|
})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveId(name);
|
setActiveId(path);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1 w-[180px] pr-2 overflow-hidden">
|
<div className="flex items-center gap-1 w-[180px] pr-2 overflow-hidden">
|
||||||
<img src={platformAdapter.convertFileSrc(icon)} className="size-5" />
|
<img
|
||||||
|
src={platformAdapter.convertFileSrc(iconPath)}
|
||||||
|
className="size-5"
|
||||||
|
/>
|
||||||
|
|
||||||
<span className="text-sm truncate">{name}</span>
|
<span className="text-sm truncate">{name}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,24 +105,43 @@ const Applications = () => {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
{t("settings.extensions.application.title")}
|
{t("settings.extensions.application.title")}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div
|
||||||
{t("settings.extensions.application.hits.addAlias")}
|
className="flex-1"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SettingsInput
|
||||||
|
defaultValue={alias}
|
||||||
|
placeholder={t("settings.extensions.application.hits.addAlias")}
|
||||||
|
className="!w-[90%] !h-6 border-transparent rounded-[4px]"
|
||||||
|
onChange={(event) => {
|
||||||
|
handleAlias(app, event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div
|
||||||
{t("settings.extensions.application.hits.recordHotkey")}
|
className="flex-1"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Shortcut
|
||||||
|
value={hotkey}
|
||||||
|
placeholder={t(
|
||||||
|
"settings.extensions.application.hits.recordHotkey"
|
||||||
|
)}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleHotkey(app, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex items-center justify-end">
|
<div className="flex-1 flex items-center justify-end">
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
label=""
|
label=""
|
||||||
checked={!disabledApps.includes(name)}
|
checked={!isDisabled}
|
||||||
className="scale-75"
|
className="scale-75"
|
||||||
onChange={() => {
|
onChange={() => handleDisable(app)}
|
||||||
if (disabledApps.includes(name)) {
|
|
||||||
setDisabledApps(disabledApps.filter((app) => app !== name));
|
|
||||||
} else {
|
|
||||||
setDisabledApps([...disabledApps, name]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,47 +1,66 @@
|
|||||||
import { FC } from "react";
|
import { useContext, useMemo, useState } from "react";
|
||||||
import { Application } from "@/stores/applicationsStore";
|
import { ApplicationMetadata } from "@/stores/applicationsStore";
|
||||||
import { filesize } from "filesize";
|
import { filesize } from "filesize";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useAsyncEffect } from "ahooks";
|
||||||
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
|
import { ExtensionsContext } from "../../..";
|
||||||
|
|
||||||
interface AppProps {
|
const App = () => {
|
||||||
current: Application;
|
|
||||||
}
|
|
||||||
|
|
||||||
const App: FC<AppProps> = (props) => {
|
|
||||||
const { name, where, size, created, modified, lastOpened } = props.current;
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { activeId } = useContext(ExtensionsContext);
|
||||||
|
|
||||||
const metadata = [
|
const [appMetadata, setAppMetadata] = useState<ApplicationMetadata>();
|
||||||
{
|
|
||||||
label: t("settings.extensions.application.details.name"),
|
useAsyncEffect(async () => {
|
||||||
value: name,
|
const appMetadata =
|
||||||
},
|
await platformAdapter.invokeBackend<ApplicationMetadata>(
|
||||||
{
|
"get_app_metadata",
|
||||||
label: t("settings.extensions.application.details.where"),
|
{
|
||||||
value: where,
|
appPath: activeId,
|
||||||
},
|
}
|
||||||
{
|
);
|
||||||
label: t("settings.extensions.application.details.type"),
|
|
||||||
value: t("settings.extensions.application.details.typeValue"),
|
setAppMetadata(appMetadata);
|
||||||
},
|
}, [activeId]);
|
||||||
{
|
|
||||||
label: t("settings.extensions.application.details.size"),
|
const metadata = useMemo(() => {
|
||||||
value: filesize(size, { standard: "jedec", spacer: "" }),
|
if (!appMetadata) return [];
|
||||||
},
|
|
||||||
{
|
const { name, where, size, created, modified, lastOpened } = appMetadata;
|
||||||
label: t("settings.extensions.application.details.created"),
|
|
||||||
value: dayjs(created).format("YYYY/MM/DD HH:mm:ss"),
|
return [
|
||||||
},
|
{
|
||||||
{
|
label: t("settings.extensions.application.details.name"),
|
||||||
label: t("settings.extensions.application.details.modified"),
|
value: name,
|
||||||
value: dayjs(modified).format("YYYY/MM/DD HH:mm:ss"),
|
},
|
||||||
},
|
{
|
||||||
{
|
label: t("settings.extensions.application.details.where"),
|
||||||
label: t("settings.extensions.application.details.lastOpened"),
|
value: where,
|
||||||
value: dayjs(lastOpened).format("YYYY/MM/DD HH:mm:ss"),
|
},
|
||||||
},
|
{
|
||||||
];
|
label: t("settings.extensions.application.details.type"),
|
||||||
|
value: t("settings.extensions.application.details.typeValue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("settings.extensions.application.details.size"),
|
||||||
|
value: filesize(size, { standard: "jedec", spacer: "" }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("settings.extensions.application.details.created"),
|
||||||
|
value: dayjs(created).format("YYYY/MM/DD HH:mm:ss"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("settings.extensions.application.details.modified"),
|
||||||
|
value: dayjs(modified).format("YYYY/MM/DD HH:mm:ss"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("settings.extensions.application.details.lastOpened"),
|
||||||
|
value: dayjs(lastOpened).format("YYYY/MM/DD HH:mm:ss"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [appMetadata]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="flex flex-col gap-2 p-0">
|
<ul className="flex flex-col gap-2 p-0">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useApplicationsStore } from "@/stores/applicationsStore";
|
import { useApplicationsStore } from "@/stores/applicationsStore";
|
||||||
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import { Button } from "@headlessui/react";
|
import { Button } from "@headlessui/react";
|
||||||
import { castArray, union } from "lodash-es";
|
import { castArray } from "lodash-es";
|
||||||
import { Folder, SquareArrowOutUpRight, X } from "lucide-react";
|
import { Folder, SquareArrowOutUpRight, X } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -9,8 +10,9 @@ const Applications = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const searchPaths = useApplicationsStore((state) => state.searchPaths);
|
const searchPaths = useApplicationsStore((state) => state.searchPaths);
|
||||||
const setSearchPaths = useApplicationsStore((state) => state.setSearchPaths);
|
const setSearchPaths = useApplicationsStore((state) => state.setSearchPaths);
|
||||||
|
const addError = useAppStore((state) => state.addError);
|
||||||
|
|
||||||
const selectDirectory = async () => {
|
const handleAdd = async () => {
|
||||||
const selected = await platformAdapter.openFileDialog({
|
const selected = await platformAdapter.openFileDialog({
|
||||||
directory: true,
|
directory: true,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
@@ -18,7 +20,49 @@ const Applications = () => {
|
|||||||
|
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
|
|
||||||
setSearchPaths(union(searchPaths, castArray(selected)));
|
const selectedPaths = castArray(selected).filter((selectedPath) => {
|
||||||
|
if (searchPaths.includes(selectedPath)) {
|
||||||
|
addError(
|
||||||
|
t("settings.extensions.application.hits.pathDuplication", {
|
||||||
|
replace: [selectedPath],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isChildPath = searchPaths.some((item) => {
|
||||||
|
return selectedPath.startsWith(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isChildPath) {
|
||||||
|
addError(
|
||||||
|
t("settings.extensions.application.hits.pathIncluded", {
|
||||||
|
replace: [selectedPath],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
setSearchPaths(searchPaths.concat(selectedPaths));
|
||||||
|
|
||||||
|
for await (const path of selectedPaths) {
|
||||||
|
await platformAdapter.invokeBackend("add_app_search_path", {
|
||||||
|
searchPath: path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (path: string) => {
|
||||||
|
setSearchPaths(searchPaths.filter((item) => item !== path));
|
||||||
|
|
||||||
|
platformAdapter.invokeBackend("remove_app_search_path", {
|
||||||
|
searchPath: path,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -35,7 +79,7 @@ const Applications = () => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full h-8 my-4 text-[#0087FF] border border-[#EEF0F3] hover:border-[#0087FF] dark:border-gray-700 rounded-md transition"
|
className="w-full h-8 my-4 text-[#0087FF] border border-[#EEF0F3] hover:border-[#0087FF] dark:border-gray-700 rounded-md transition"
|
||||||
onClick={selectDirectory}
|
onClick={handleAdd}
|
||||||
>
|
>
|
||||||
{t("settings.extensions.application.button.addDirectories")}
|
{t("settings.extensions.application.button.addDirectories")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -60,9 +104,7 @@ const Applications = () => {
|
|||||||
|
|
||||||
<X
|
<X
|
||||||
className="size-4 cursor-pointer"
|
className="size-4 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => handleRemove(item)}
|
||||||
setSearchPaths(searchPaths.filter((path) => path !== item));
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
143
src/components/Settings/Extensions/components/Shortcut/index.tsx
Normal file
143
src/components/Settings/Extensions/components/Shortcut/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { find, isEmpty, map, remove, some, split } from "lodash-es";
|
||||||
|
import { useRef, type FC, type KeyboardEvent, type MouseEvent } from "react";
|
||||||
|
import { type Key, keys, modifierKeys, standardKeys } from "./keyboard";
|
||||||
|
import { CircleX } from "lucide-react";
|
||||||
|
import { useFocusWithin, useHover, useReactive } from "ahooks";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
interface ShortcutProps {
|
||||||
|
value?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
isSystem?: boolean;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
value: Key[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Shortcut: FC<ShortcutProps> = (props) => {
|
||||||
|
const { value = "", placeholder, isSystem = true, onChange } = props;
|
||||||
|
|
||||||
|
const separator = isSystem ? "+" : ".";
|
||||||
|
const keyFiled = isSystem ? "tauriKey" : "hookKey";
|
||||||
|
|
||||||
|
const parseValue = () => {
|
||||||
|
if (!value) return [];
|
||||||
|
|
||||||
|
return split(value, separator).map((key) => {
|
||||||
|
return find(keys, { [keyFiled]: key })!;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = useReactive<State>({
|
||||||
|
value: parseValue(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const isHovering = useHover(containerRef);
|
||||||
|
|
||||||
|
const isFocusing = useFocusWithin(containerRef, {
|
||||||
|
onFocus: () => {
|
||||||
|
state.value = [];
|
||||||
|
},
|
||||||
|
onBlur: () => {
|
||||||
|
if (!isValidShortcut()) {
|
||||||
|
state.value = parseValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isValidShortcut = () => {
|
||||||
|
if (state.value?.[0]?.eventKey?.startsWith("F")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasModifierKey = some(state.value, ({ eventKey }) => {
|
||||||
|
return some(modifierKeys, { eventKey });
|
||||||
|
});
|
||||||
|
const hasStandardKey = some(state.value, ({ eventKey }) => {
|
||||||
|
return some(standardKeys, { eventKey });
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasModifierKey && hasStandardKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEventKey = (event: KeyboardEvent) => {
|
||||||
|
let { key, code } = event;
|
||||||
|
|
||||||
|
key = key.replace("Meta", "Command");
|
||||||
|
|
||||||
|
const isModifierKey = some(modifierKeys, { eventKey: key });
|
||||||
|
|
||||||
|
return isModifierKey ? key : code;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
const nextValue = map(state.value, keyFiled).join(separator);
|
||||||
|
|
||||||
|
onChange?.(nextValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
const eventKey = getEventKey(event);
|
||||||
|
|
||||||
|
const matched = find(keys, { eventKey });
|
||||||
|
const isInvalid = !matched;
|
||||||
|
const isDuplicate = some(state.value, { eventKey });
|
||||||
|
|
||||||
|
if (isInvalid || isDuplicate) return;
|
||||||
|
|
||||||
|
state.value.push(matched);
|
||||||
|
|
||||||
|
if (isValidShortcut()) {
|
||||||
|
containerRef.current?.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyUp = (event: KeyboardEvent) => {
|
||||||
|
remove(state.value, { eventKey: getEventKey(event) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
state.value = [];
|
||||||
|
|
||||||
|
handleChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
tabIndex={0}
|
||||||
|
className="relative flex items-center h-6 px-2 rounded-[4px] border border-transparent hover:border-[#0072FF] focus:border-[#0072FF] transition"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
>
|
||||||
|
{isEmpty(state.value) ? (
|
||||||
|
<div className="whitespace-nowrap">{placeholder}</div>
|
||||||
|
) : (
|
||||||
|
<div className="font-bold text-primary">
|
||||||
|
{map(state.value, "symbol").join(" ")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CircleX
|
||||||
|
size={16}
|
||||||
|
className={clsx(
|
||||||
|
"absolute right-2 hover:text-[#0072FF] cursor-pointer transition",
|
||||||
|
{
|
||||||
|
hidden: isFocusing || !isHovering || isEmpty(state.value),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onMouseDown={handleClear}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Shortcut;
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
import { isMac } from "@/utils/platform";
|
||||||
|
import { defaults } from "lodash-es";
|
||||||
|
|
||||||
|
export interface Key {
|
||||||
|
eventKey: string;
|
||||||
|
hookKey?: string;
|
||||||
|
tauriKey?: string;
|
||||||
|
symbol?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modifierKeys: Key[] = [
|
||||||
|
{
|
||||||
|
eventKey: "Shift",
|
||||||
|
symbol: isMac ? "⇧" : "Shift",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Control",
|
||||||
|
hookKey: "ctrl",
|
||||||
|
symbol: isMac ? "⌃" : "Ctrl",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Alt",
|
||||||
|
symbol: isMac ? "⌥" : "Alt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Command",
|
||||||
|
hookKey: "meta",
|
||||||
|
symbol: isMac ? "⌘" : "Super",
|
||||||
|
},
|
||||||
|
].map((item) => {
|
||||||
|
const { eventKey } = item;
|
||||||
|
|
||||||
|
defaults<Key, Partial<Key>>(item, {
|
||||||
|
hookKey: eventKey.toLowerCase(),
|
||||||
|
tauriKey: eventKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const standardKeys: Key[] = [
|
||||||
|
// 第一排
|
||||||
|
{
|
||||||
|
eventKey: "Escape",
|
||||||
|
hookKey: "esc",
|
||||||
|
symbol: isMac ? "⎋" : "Esc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "F1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "F2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "F3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "F4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "F5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "F6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "F7",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "F8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "F9",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "F10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "F11",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "F12",
|
||||||
|
}, // 第二排
|
||||||
|
{
|
||||||
|
eventKey: "Backquote",
|
||||||
|
hookKey: "graveaccent",
|
||||||
|
symbol: "`",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Digit1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Digit2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Digit3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Digit4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Digit5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Digit6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Digit7",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Digit8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Digit9",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Digit0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Minus",
|
||||||
|
hookKey: "dash",
|
||||||
|
tauriKey: "-",
|
||||||
|
symbol: "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Equal",
|
||||||
|
hookKey: "equalsign",
|
||||||
|
tauriKey: "=",
|
||||||
|
symbol: "=",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Backspace",
|
||||||
|
symbol: isMac ? "⌫" : void 0,
|
||||||
|
},
|
||||||
|
// 第三排
|
||||||
|
{
|
||||||
|
eventKey: "Tab",
|
||||||
|
symbol: isMac ? "⇥" : void 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyQ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyW",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyY",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyU",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyI",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyO",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "BracketLeft",
|
||||||
|
hookKey: "openbracket",
|
||||||
|
symbol: "[",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "BracketRight",
|
||||||
|
hookKey: "closebracket",
|
||||||
|
symbol: "]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Backslash",
|
||||||
|
symbol: "\\",
|
||||||
|
},
|
||||||
|
// 第四排
|
||||||
|
{
|
||||||
|
eventKey: "KeyA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyG",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyH",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyJ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Semicolon",
|
||||||
|
symbol: ";",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Quote",
|
||||||
|
hookKey: "singlequote",
|
||||||
|
symbol: "'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Enter",
|
||||||
|
symbol: isMac ? "↩︎" : void 0,
|
||||||
|
},
|
||||||
|
// 第五排
|
||||||
|
{
|
||||||
|
eventKey: "KeyZ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyX",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyV",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "KeyM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Comma",
|
||||||
|
symbol: ",",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Period",
|
||||||
|
symbol: ".",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "Slash",
|
||||||
|
hookKey: "forwardslash",
|
||||||
|
symbol: "/",
|
||||||
|
},
|
||||||
|
// 第六排
|
||||||
|
{
|
||||||
|
eventKey: "Space",
|
||||||
|
symbol: isMac ? "␣" : void 0,
|
||||||
|
},
|
||||||
|
// 方向键
|
||||||
|
{
|
||||||
|
eventKey: "ArrowUp",
|
||||||
|
hookKey: "uparrow",
|
||||||
|
symbol: "↑",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "ArrowDown",
|
||||||
|
hookKey: "downarrow",
|
||||||
|
symbol: "↓",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "ArrowLeft",
|
||||||
|
hookKey: "leftarrow",
|
||||||
|
symbol: "←",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventKey: "ArrowRight",
|
||||||
|
hookKey: "rightarrow",
|
||||||
|
symbol: "→",
|
||||||
|
},
|
||||||
|
].map((item) => {
|
||||||
|
const { eventKey } = item;
|
||||||
|
|
||||||
|
defaults<Key, Partial<Key>>(item, {
|
||||||
|
hookKey: eventKey.toLowerCase(),
|
||||||
|
symbol: eventKey,
|
||||||
|
tauriKey: eventKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eventKey.startsWith("Digit") || eventKey.startsWith("Key")) {
|
||||||
|
item.tauriKey = item.symbol = eventKey.slice(-1);
|
||||||
|
|
||||||
|
item.hookKey = item.tauriKey.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const keys = modifierKeys.concat(standardKeys);
|
||||||
|
|
||||||
|
export const getKeySymbol = (key: string) => {
|
||||||
|
const fields = ["tauriKey", "hookKey"] as const;
|
||||||
|
|
||||||
|
const matched = keys.find((entry) => {
|
||||||
|
return fields.some((field) => entry[field] === key);
|
||||||
|
});
|
||||||
|
|
||||||
|
return matched?.symbol ?? key;
|
||||||
|
};
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Folder } from "lucide-react";
|
import { Calculator, Folder } from "lucide-react";
|
||||||
import { noop } from "lodash-es";
|
import { noop } from "lodash-es";
|
||||||
|
|
||||||
import Accordion from "./components/Accordion";
|
import Accordion from "./components/Accordion";
|
||||||
@@ -14,6 +14,9 @@ import ApplicationsDetail from "./components/Details/Applications";
|
|||||||
import { useApplicationsStore } from "@/stores/applicationsStore";
|
import { useApplicationsStore } from "@/stores/applicationsStore";
|
||||||
import Application from "./components/Details/Application";
|
import Application from "./components/Details/Application";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useMount } from "ahooks";
|
||||||
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
|
import { useExtensionStore } from "@/stores/extension";
|
||||||
|
|
||||||
export interface Plugin {
|
export interface Plugin {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -30,14 +33,34 @@ export interface Plugin {
|
|||||||
interface ExtensionsContextType {
|
interface ExtensionsContextType {
|
||||||
activeId?: string;
|
activeId?: string;
|
||||||
setActiveId: (id: string) => void;
|
setActiveId: (id: string) => void;
|
||||||
|
disabledExtensions: string[];
|
||||||
|
setDisabledExtensions: (ids: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExtensionsContext = createContext<ExtensionsContextType>({
|
export const ExtensionsContext = createContext<ExtensionsContextType>({
|
||||||
setActiveId: noop,
|
setActiveId: noop,
|
||||||
|
disabledExtensions: [],
|
||||||
|
setDisabledExtensions: noop,
|
||||||
});
|
});
|
||||||
|
|
||||||
const Extensions = () => {
|
const Extensions = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const disabledExtensions = useExtensionStore((state) => {
|
||||||
|
return state.disabledExtensions;
|
||||||
|
});
|
||||||
|
const setDisabledExtensions = useExtensionStore((state) => {
|
||||||
|
return state.setDisabledExtensions;
|
||||||
|
});
|
||||||
|
|
||||||
|
useMount(async () => {
|
||||||
|
const disabledExtensions = await platformAdapter.invokeBackend<string[]>(
|
||||||
|
"get_disabled_local_query_sources"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("disabledExtensions", disabledExtensions);
|
||||||
|
|
||||||
|
setDisabledExtensions(disabledExtensions);
|
||||||
|
});
|
||||||
|
|
||||||
const allApps = useApplicationsStore((state) => {
|
const allApps = useApplicationsStore((state) => {
|
||||||
return state.allApps;
|
return state.allApps;
|
||||||
@@ -45,13 +68,18 @@ const Extensions = () => {
|
|||||||
|
|
||||||
const presetPlugins: Plugin[] = [
|
const presetPlugins: Plugin[] = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "Applications",
|
||||||
icon: <Folder />,
|
icon: <Folder />,
|
||||||
title: t("settings.extensions.application.title"),
|
title: t("settings.extensions.application.title"),
|
||||||
type: "Group",
|
type: "Group",
|
||||||
content: <ApplicationsContent />,
|
content: <ApplicationsContent />,
|
||||||
detail: <ApplicationsDetail />,
|
detail: <ApplicationsDetail />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "Calculator",
|
||||||
|
icon: <Calculator />,
|
||||||
|
title: t("settings.extensions.calculator.title"),
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// id: "2",
|
// id: "2",
|
||||||
// icon: <File />,
|
// icon: <File />,
|
||||||
@@ -69,7 +97,7 @@ const Extensions = () => {
|
|||||||
|
|
||||||
const currentApp = useMemo(() => {
|
const currentApp = useMemo(() => {
|
||||||
return allApps.find((app) => {
|
return allApps.find((app) => {
|
||||||
return app.name === activeId;
|
return app.path === activeId;
|
||||||
});
|
});
|
||||||
}, [activeId, allApps]);
|
}, [activeId, allApps]);
|
||||||
|
|
||||||
@@ -78,6 +106,8 @@ const Extensions = () => {
|
|||||||
value={{
|
value={{
|
||||||
activeId,
|
activeId,
|
||||||
setActiveId,
|
setActiveId,
|
||||||
|
disabledExtensions,
|
||||||
|
setDisabledExtensions,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex h-[calc(100vh-128px)] -mx-6 gap-4">
|
<div className="flex h-[calc(100vh-128px)] -mx-6 gap-4">
|
||||||
@@ -123,7 +153,7 @@ const Extensions = () => {
|
|||||||
|
|
||||||
{currentPlugin?.detail}
|
{currentPlugin?.detail}
|
||||||
|
|
||||||
{currentApp && <Application current={currentApp} />}
|
{currentApp && <Application />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ExtensionsContext.Provider>
|
</ExtensionsContext.Provider>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const UpdateApp = ({ checkUpdate, relaunchApp }: UpdateAppProps) => {
|
|||||||
const checkUpdateStatus = useCallback(async () => {
|
const checkUpdateStatus = useCallback(async () => {
|
||||||
const update = await checkUpdate();
|
const update = await checkUpdate();
|
||||||
|
|
||||||
if (update?.available) {
|
if (update) {
|
||||||
setUpdateInfo(update);
|
setUpdateInfo(update);
|
||||||
|
|
||||||
if (skipVersion === update.version) return;
|
if (skipVersion === update.version) return;
|
||||||
|
|||||||
@@ -200,7 +200,9 @@
|
|||||||
"title": "Applications",
|
"title": "Applications",
|
||||||
"hits": {
|
"hits": {
|
||||||
"addAlias": "Add Alias",
|
"addAlias": "Add Alias",
|
||||||
"recordHotkey": "Record Hotkey"
|
"recordHotkey": "Record Hotkey",
|
||||||
|
"pathDuplication": "Path \"{{0}}\" is already in search scope.",
|
||||||
|
"pathIncluded": "Path \"{{0}}\" is already covered by another search directory."
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"addDirectories": "Add Directories"
|
"addDirectories": "Add Directories"
|
||||||
@@ -217,6 +219,9 @@
|
|||||||
"modified": "Modified",
|
"modified": "Modified",
|
||||||
"lastOpened": "Last Opened"
|
"lastOpened": "Last Opened"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"calculator": {
|
||||||
|
"title": "Calculator"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -200,7 +200,9 @@
|
|||||||
"title": "应用程序",
|
"title": "应用程序",
|
||||||
"hits": {
|
"hits": {
|
||||||
"addAlias": "添加别名",
|
"addAlias": "添加别名",
|
||||||
"recordHotkey": "录制热键"
|
"recordHotkey": "录制热键",
|
||||||
|
"pathDuplication": "路径 \"{{0}}\" 已存在于搜索范围中。",
|
||||||
|
"pathIncluded": "路径 \"{{0}}\" 已被其他搜索目录包含。"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"addDirectories": "添加目录"
|
"addDirectories": "添加目录"
|
||||||
@@ -217,6 +219,9 @@
|
|||||||
"modified": "修改时间",
|
"modified": "修改时间",
|
||||||
"lastOpened": "上次打开时间"
|
"lastOpened": "上次打开时间"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"calculator": {
|
||||||
|
"title": "计算器"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { RouterProvider } from "react-router-dom";
|
import { RouterProvider } from "react-router-dom";
|
||||||
|
|
||||||
@@ -8,7 +7,5 @@ import "./i18n";
|
|||||||
import "./main.css";
|
import "./main.css";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<RouterProvider router={router} />
|
||||||
<RouterProvider router={router} />
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import Footer from "@/components/Common/UI/SettingsFooter";
|
|||||||
import { useTray } from "@/hooks/useTray";
|
import { useTray } from "@/hooks/useTray";
|
||||||
import Advanced from "@/components/Settings/Advanced";
|
import Advanced from "@/components/Settings/Advanced";
|
||||||
import Extensions from "@/components/Settings/Extensions";
|
import Extensions from "@/components/Settings/Extensions";
|
||||||
import { useAsyncEffect, useMount } from "ahooks";
|
import { Application, useApplicationsStore } from "@/stores/applicationsStore";
|
||||||
import { useApplicationsStore } from "@/stores/applicationsStore";
|
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
|
|
||||||
const tabIndexMap: { [key: string]: number } = {
|
const tabIndexMap: { [key: string]: number } = {
|
||||||
@@ -26,9 +25,9 @@ const tabIndexMap: { [key: string]: number } = {
|
|||||||
|
|
||||||
function SettingsPage() {
|
function SettingsPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const searchPaths = useApplicationsStore((state) => state.searchPaths);
|
|
||||||
const setSearchPaths = useApplicationsStore((state) => state.setSearchPaths);
|
const setSearchPaths = useApplicationsStore((state) => state.setSearchPaths);
|
||||||
const setAllApps = useApplicationsStore((state) => state.setAllApps);
|
const setAllApps = useApplicationsStore((state) => state.setAllApps);
|
||||||
|
const allApps = useApplicationsStore((state) => state.allApps);
|
||||||
|
|
||||||
useTray();
|
useTray();
|
||||||
|
|
||||||
@@ -60,30 +59,33 @@ function SettingsPage() {
|
|||||||
document.body.style.overflow = defaultIndex !== 1 ? "auto" : "hidden";
|
document.body.style.overflow = defaultIndex !== 1 ? "auto" : "hidden";
|
||||||
}, [defaultIndex]);
|
}, [defaultIndex]);
|
||||||
|
|
||||||
useMount(async () => {
|
useEffect(() => {
|
||||||
if (searchPaths.length > 0) return;
|
platformAdapter.listenEvent("search-source-loaded", async () => {
|
||||||
|
const apps = await platformAdapter.invokeBackend<Application[]>(
|
||||||
|
"get_app_list"
|
||||||
|
);
|
||||||
|
|
||||||
const paths = await platformAdapter.invokeBackend<string[]>(
|
const sortedApps = apps.sort((a, b) => {
|
||||||
"get_default_search_paths"
|
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
||||||
);
|
});
|
||||||
|
|
||||||
setSearchPaths(paths);
|
setAllApps(sortedApps);
|
||||||
});
|
|
||||||
|
|
||||||
useAsyncEffect(async () => {
|
const paths = await platformAdapter.invokeBackend<string[]>(
|
||||||
if (searchPaths.length === 0) {
|
"get_app_search_path"
|
||||||
return setAllApps([]);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const apps = await platformAdapter.invokeBackend<any[]>(
|
setSearchPaths(paths);
|
||||||
"list_app_with_metadata_in",
|
});
|
||||||
{
|
|
||||||
searchPath: searchPaths,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
setAllApps(apps);
|
platformAdapter.listenEvent("new-apps", ({ payload }) => {
|
||||||
}, [searchPaths]);
|
const nextApps = allApps.concat(payload).sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
||||||
|
});
|
||||||
|
|
||||||
|
setAllApps(nextApps);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export type IAppStore = {
|
|||||||
setLanguage: (language: string) => void;
|
setLanguage: (language: string) => void;
|
||||||
isPinned: boolean;
|
isPinned: boolean;
|
||||||
setIsPinned: (isPinned: boolean) => void;
|
setIsPinned: (isPinned: boolean) => void;
|
||||||
initializeListeners: () => void;
|
initializeListeners: () => Promise<() => void>;
|
||||||
|
|
||||||
showCocoShortcuts: string[];
|
showCocoShortcuts: string[];
|
||||||
setShowCocoShortcuts: (showCocoShortcuts: string[]) => void;
|
setShowCocoShortcuts: (showCocoShortcuts: string[]) => void;
|
||||||
@@ -130,10 +130,14 @@ export const useAppStore = create<IAppStore>()(
|
|||||||
isPinned: false,
|
isPinned: false,
|
||||||
setIsPinned: (isPinned: boolean) => set({ isPinned }),
|
setIsPinned: (isPinned: boolean) => set({ isPinned }),
|
||||||
initializeListeners: () => {
|
initializeListeners: () => {
|
||||||
platformAdapter.listenEvent(ENDPOINT_CHANGE_EVENT, (event: any) => {
|
return platformAdapter.listenEvent(
|
||||||
const { endpoint, endpoint_http, endpoint_websocket } = event.payload;
|
ENDPOINT_CHANGE_EVENT,
|
||||||
set({ endpoint, endpoint_http, endpoint_websocket });
|
(event: any) => {
|
||||||
});
|
const { endpoint, endpoint_http, endpoint_websocket } =
|
||||||
|
event.payload;
|
||||||
|
set({ endpoint, endpoint_http, endpoint_websocket });
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
showCocoShortcuts: [],
|
showCocoShortcuts: [],
|
||||||
setShowCocoShortcuts: (showCocoShortcuts: string[]) => {
|
setShowCocoShortcuts: (showCocoShortcuts: string[]) => {
|
||||||
|
|||||||
@@ -2,10 +2,19 @@ import { create } from "zustand";
|
|||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
export interface Application {
|
export interface Application {
|
||||||
|
path: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
iconPath: string;
|
||||||
|
alias: string;
|
||||||
|
hotkey: string;
|
||||||
|
isDisabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationMetadata {
|
||||||
|
name: string;
|
||||||
|
where: string;
|
||||||
size: number;
|
size: number;
|
||||||
icon: string;
|
icon: string;
|
||||||
where: string;
|
|
||||||
created: number;
|
created: number;
|
||||||
modified: number;
|
modified: number;
|
||||||
lastOpened: number;
|
lastOpened: number;
|
||||||
@@ -16,8 +25,6 @@ export type IUpdateStore = {
|
|||||||
setAllApps: (appApps: Application[]) => void;
|
setAllApps: (appApps: Application[]) => void;
|
||||||
searchPaths: string[];
|
searchPaths: string[];
|
||||||
setSearchPaths: (searchPaths: string[]) => void;
|
setSearchPaths: (searchPaths: string[]) => void;
|
||||||
disabledApps: string[];
|
|
||||||
setDisabledApps: (disabledApps: string[]) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useApplicationsStore = create<IUpdateStore>()(
|
export const useApplicationsStore = create<IUpdateStore>()(
|
||||||
@@ -31,17 +38,10 @@ export const useApplicationsStore = create<IUpdateStore>()(
|
|||||||
setSearchPaths: (searchPaths: string[]) => {
|
setSearchPaths: (searchPaths: string[]) => {
|
||||||
return set({ searchPaths });
|
return set({ searchPaths });
|
||||||
},
|
},
|
||||||
disabledApps: [],
|
|
||||||
setDisabledApps: (disabledApps: string[]) => {
|
|
||||||
return set({ disabledApps });
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "applications-store",
|
name: "applications-store",
|
||||||
partialize: (state) => ({
|
partialize: () => ({}),
|
||||||
searchPaths: state.searchPaths,
|
|
||||||
disabledApps: state.disabledApps,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import { produce } from 'immer'
|
import { produce } from "immer";
|
||||||
|
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
|
|
||||||
const AUTH_CHANGE_EVENT = 'auth-changed';
|
const AUTH_CHANGE_EVENT = "auth-changed";
|
||||||
const USERINFO_CHANGE_EVENT = 'userInfo-changed';
|
const USERINFO_CHANGE_EVENT = "userInfo-changed";
|
||||||
|
|
||||||
export type Plan = {
|
export type Plan = {
|
||||||
upgraded: boolean;
|
upgraded: boolean;
|
||||||
@@ -33,7 +33,7 @@ export type IAuthStore = {
|
|||||||
userInfo: userInfoMapProp;
|
userInfo: userInfoMapProp;
|
||||||
setAuth: (auth: AuthProp | undefined, key: string) => void;
|
setAuth: (auth: AuthProp | undefined, key: string) => void;
|
||||||
resetAuth: (key: string) => void;
|
resetAuth: (key: string) => void;
|
||||||
initializeListeners: () => void;
|
initializeListeners: () => Promise<() => void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAuthStore = create<IAuthStore>()(
|
export const useAuthStore = create<IAuthStore>()(
|
||||||
@@ -44,59 +44,62 @@ export const useAuthStore = create<IAuthStore>()(
|
|||||||
setAuth: async (auth, key) => {
|
setAuth: async (auth, key) => {
|
||||||
set(
|
set(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
draft.auth[key] = auth
|
draft.auth[key] = auth;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await platformAdapter.emitEvent(AUTH_CHANGE_EVENT, {
|
await platformAdapter.emitEvent(AUTH_CHANGE_EVENT, {
|
||||||
auth: {
|
auth: {
|
||||||
[key]: auth
|
[key]: auth,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetAuth: async (key: string) => {
|
resetAuth: async (key: string) => {
|
||||||
set(
|
set(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
draft.auth[key] = undefined
|
draft.auth[key] = undefined;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await platformAdapter.emitEvent(AUTH_CHANGE_EVENT, {
|
await platformAdapter.emitEvent(AUTH_CHANGE_EVENT, {
|
||||||
auth: {
|
auth: {
|
||||||
[key]: undefined
|
[key]: undefined,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setUserInfo: async (userInfo: any, key: string) => {
|
setUserInfo: async (userInfo: any, key: string) => {
|
||||||
set(
|
set(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
draft.userInfo[key] = userInfo
|
draft.userInfo[key] = userInfo;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await platformAdapter.emitEvent(USERINFO_CHANGE_EVENT, {
|
await platformAdapter.emitEvent(USERINFO_CHANGE_EVENT, {
|
||||||
userInfo: {
|
userInfo: {
|
||||||
[key]: userInfo
|
[key]: userInfo,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
initializeListeners: () => {
|
initializeListeners: async () => {
|
||||||
platformAdapter.listenEvent(AUTH_CHANGE_EVENT, (event: any) => {
|
await platformAdapter.listenEvent(AUTH_CHANGE_EVENT, (event: any) => {
|
||||||
const { auth } = event.payload;
|
const { auth } = event.payload;
|
||||||
set({ auth });
|
set({ auth });
|
||||||
});
|
});
|
||||||
|
|
||||||
platformAdapter.listenEvent(USERINFO_CHANGE_EVENT, (event: any) => {
|
return platformAdapter.listenEvent(
|
||||||
const { userInfo } = event.payload;
|
USERINFO_CHANGE_EVENT,
|
||||||
set({ userInfo });
|
(event: any) => {
|
||||||
});
|
const { userInfo } = event.payload;
|
||||||
|
set({ userInfo });
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "auth-store",
|
name: "auth-store",
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
auth: state.auth,
|
auth: state.auth,
|
||||||
userInfo: state.userInfo
|
userInfo: state.userInfo,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
24
src/stores/extension.ts
Normal file
24
src/stores/extension.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { persist, subscribeWithSelector } from "zustand/middleware";
|
||||||
|
|
||||||
|
export type IExtensionStore = {
|
||||||
|
disabledExtensions: string[];
|
||||||
|
setDisabledExtensions: (disabledExtensions: string[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useExtensionStore = create<IExtensionStore>()(
|
||||||
|
subscribeWithSelector(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
disabledExtensions: [],
|
||||||
|
setDisabledExtensions: (disabledExtensions) => {
|
||||||
|
return set({ disabledExtensions });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "extension-store",
|
||||||
|
partialize: () => ({}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IAppearanceStore } from "@/stores/appearance";
|
import { IAppearanceStore } from "@/stores/appearance";
|
||||||
|
import { Application } from "@/stores/applicationsStore";
|
||||||
import { IConnectStore } from "@/stores/connectStore";
|
import { IConnectStore } from "@/stores/connectStore";
|
||||||
import { IShortcutsStore } from "@/stores/shortcutsStore";
|
import { IShortcutsStore } from "@/stores/shortcutsStore";
|
||||||
import { IStartupStore } from "@/stores/startupStore";
|
import { IStartupStore } from "@/stores/startupStore";
|
||||||
@@ -40,6 +41,8 @@ export interface EventPayloads {
|
|||||||
"change-shortcuts-store": IShortcutsStore;
|
"change-shortcuts-store": IShortcutsStore;
|
||||||
"change-connect-store": IConnectStore;
|
"change-connect-store": IConnectStore;
|
||||||
"change-appearance-store": IAppearanceStore;
|
"change-appearance-store": IAppearanceStore;
|
||||||
|
"search-source-loaded": any;
|
||||||
|
"new-apps": Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window operation interface
|
// Window operation interface
|
||||||
|
|||||||
Reference in New Issue
Block a user