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:
SteveLauC
2025-05-09 17:54:58 +08:00
committed by GitHub
parent 4895322397
commit 8d2528e521
34 changed files with 4319 additions and 1990 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

1666
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly-2024-10-29"

View File

@@ -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,
}
}
}

View File

@@ -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;

View File

@@ -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,
})
}
}

View 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,
}

File diff suppressed because it is too large Load Diff

View 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")
}

View File

@@ -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,

View File

@@ -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));
}

View File

@@ -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
})); }));
} }

View File

@@ -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}`;

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View 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;

View File

@@ -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;
};

View File

@@ -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>

View File

@@ -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;

View File

@@ -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"
} }
} }
}, },

View File

@@ -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": "计算器"
} }
} }
}, },

View File

@@ -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>
); );

View File

@@ -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>

View File

@@ -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[]) => {

View File

@@ -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,
}),
} }
) )
); );

View File

@@ -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
View 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: () => ({}),
}
)
)
);

View File

@@ -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