Skip to content

Commit af70639

Browse files
authored
feat: add application management to the plugin (#374)
* feat: add application management to the plugin * refactor: add dark color mode support * docs: update changelog * style: add a full note
1 parent bd5015e commit af70639

13 files changed

Lines changed: 549 additions & 36 deletions

File tree

docs/content.en/docs/release-notes/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Information about release notes of Coco Server is provided here.
2121
- feat: mobile terminal adaptation about style #348
2222
- feat: service list popup box supports keyboard-only operation #359
2323
- feat: networked search data sources support search and keyboard-only operation #367
24+
- feat: add application management to the plugin #374
2425

2526
### Bug fix
2627

src-tauri/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ pub fn run() {
132132
server::attachment::get_attachment,
133133
server::attachment::delete_attachment,
134134
server::transcription::transcription,
135-
local::application::get_default_search_paths
135+
local::application::get_default_search_paths,
136+
local::application::list_app_with_metadata_in,
136137
])
137138
.setup(|app| {
138139
let registry = SearchSourceRegistry::default();
@@ -412,4 +413,4 @@ async fn get_app_search_source<R: Runtime>(app_handle: AppHandle<R>) -> Result<(
412413
#[tauri::command]
413414
async fn show_settings(app_handle: AppHandle) {
414415
open_settings(&app_handle);
415-
}
416+
}

src-tauri/src/local/application.rs

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,28 @@ use tauri_plugin_fs_pro::{icon, name};
1414

1515
#[tauri::command]
1616
pub fn get_default_search_paths() -> Vec<String> {
17-
let paths = applications::get_default_search_paths();
18-
let mut ret = Vec::with_capacity(paths.len());
19-
for search_path in paths {
20-
let path_string = search_path
21-
.into_os_string()
22-
.into_string()
23-
.expect("path should be UTF-8 encoded");
24-
25-
ret.push(path_string);
26-
}
17+
#[cfg(target_os = "macos")]
18+
return vec![
19+
"/Applications".into(),
20+
"/System/Applications".into(),
21+
"/System/Library/CoreServices".into(),
22+
];
23+
24+
#[cfg(not(target_os = "macos"))]
25+
{
26+
let paths = applications::get_default_search_paths();
27+
let mut ret = Vec::with_capacity(paths.len());
28+
for search_path in paths {
29+
let path_string = search_path
30+
.into_os_string()
31+
.into_string()
32+
.expect("path should be UTF-8 encoded");
33+
34+
ret.push(path_string);
35+
}
2736

28-
ret
37+
ret
38+
}
2939
}
3040

3141
/// List apps that are in the `search_path`.
@@ -35,27 +45,24 @@ fn list_app_in(search_path: Vec<String>) -> Result<Vec<App>, String> {
3545
.into_iter()
3646
.map(PathBuf::from)
3747
.collect::<Vec<_>>();
48+
3849
let apps = applications::get_all_apps(&search_path).map_err(|err| err.to_string())?;
3950

4051
Ok(apps
4152
.into_iter()
42-
.filter(|app| app.icon_path.is_none())
53+
.filter(|app| !app.icon_path.is_none())
4354
.collect())
4455
}
4556

4657
#[derive(serde::Serialize)]
58+
#[serde(rename_all = "camelCase")]
4759
pub struct AppMetadata {
48-
#[serde(rename = "Name")]
4960
name: String,
50-
#[serde(rename = "Where")]
5161
r#where: PathBuf,
52-
#[serde(rename = "Size")]
5362
size: u64,
54-
#[serde(rename = "Created")]
63+
icon: PathBuf,
5564
created: u128,
56-
#[serde(rename = "Modified")]
5765
modified: u128,
58-
#[serde(rename = "Last opened")]
5966
last_opened: u128,
6067
}
6168

@@ -65,16 +72,18 @@ pub struct AppMetadata {
6572
///
6673
/// ```json
6774
/// {
68-
/// "Name": "Finder",
69-
/// "Where": "/System/Library/CoreServices",
70-
/// "Size": 49283072,
71-
/// "Created": 1744625204,
72-
/// "Modified": 1744625204,
73-
/// "Last opened": 1744625250
75+
/// "name": "Finder",
76+
/// "where": "/System/Library/CoreServices",
77+
/// "size": 49283072,
78+
/// "icon": "/xxx.png",
79+
/// "created": 1744625204,
80+
/// "modified": 1744625204,
81+
/// "lastOpened": 1744625250
7482
/// }
7583
/// ```
7684
#[tauri::command]
77-
pub async fn list_app_with_metadata_in(
85+
pub async fn list_app_with_metadata_in<R: Runtime>(
86+
app_handle: AppHandle<R>,
7887
search_path: Vec<String>,
7988
) -> Result<Vec<AppMetadata>, String> {
8089
let apps = list_app_in(search_path)?;
@@ -101,12 +110,21 @@ pub async fn list_app_with_metadata_in(
101110
app_path_clone
102111
};
103112

113+
let icon = if cfg!(target_os = "linux") {
114+
app.icon_path.clone().unwrap_or(PathBuf::from(""))
115+
} else {
116+
icon(app_handle.clone(), app_path.clone(), Some(256))
117+
.await
118+
.map_err(|err| err.to_string())?
119+
};
120+
104121
let raw_app_metadata = tauri_plugin_fs_pro::metadata(app_path.clone(), None).await?;
105122

106123
let app_metadata = AppMetadata {
107124
name: app_name,
108125
r#where: app_path_where,
109126
size: raw_app_metadata.size,
127+
icon,
110128
created: raw_app_metadata.created_at,
111129
modified: raw_app_metadata.modified_at,
112130
last_opened: raw_app_metadata.accessed_at,
@@ -132,11 +150,7 @@ impl ApplicationSearchSource {
132150
let application_paths = Trie::new();
133151
let mut icons = HashMap::new();
134152

135-
let default_search_path = if cfg!(target_os = "macos") {
136-
vec!["/Applications".into(), "/System/Applications".into(), "/System/Library/CoreServices".into()]
137-
} else {
138-
applications::get_default_search_paths()
139-
};
153+
let default_search_path = applications::get_default_search_paths();
140154
let mut ctx = AppInfoContext::new(default_search_path);
141155
ctx.refresh_apps().map_err(|err| err.to_string())?; // must refresh apps before getting them
142156
let apps = ctx.get_all_apps();
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { cloneElement, FC, useContext, useState } from "react";
2+
import { ChevronRight } from "lucide-react";
3+
import clsx from "clsx";
4+
import SettingsToggle from "@/components/Settings/SettingsToggle";
5+
import { ExtensionsContext, Plugin } from "../..";
6+
7+
interface AccordionProps extends Plugin {}
8+
9+
const Accordion: FC<AccordionProps> = (props) => {
10+
const {
11+
id,
12+
icon,
13+
title,
14+
type = "Extension",
15+
alias = "-",
16+
hotKey = "-",
17+
enabled = true,
18+
content,
19+
} = props;
20+
const { activeId, setActiveId } = useContext(ExtensionsContext);
21+
22+
const [expand, setExpand] = useState(false);
23+
24+
return (
25+
<div>
26+
<div
27+
className={clsx("flex items-center h-8 -mx-2 px-2 text-sm rounded-md", {
28+
"bg-[#f0f6fe] dark:bg-gray-700": id === activeId,
29+
})}
30+
onClick={() => {
31+
setActiveId(id);
32+
}}
33+
>
34+
<div className="w-[220px] flex items-center gap-1">
35+
<div className="size-4">
36+
{content && (
37+
<ChevronRight
38+
onClick={(event) => {
39+
event.stopPropagation();
40+
41+
setExpand((prev) => !prev);
42+
}}
43+
className={clsx("size-full transition cursor-pointer", {
44+
"rotate-90": expand,
45+
})}
46+
/>
47+
)}
48+
</div>
49+
50+
{cloneElement(icon, { className: "size-4" })}
51+
52+
<span>{title}</span>
53+
</div>
54+
55+
<div className="flex-1 flex items-center text-[#999]">
56+
<div className="flex-1">{type}</div>
57+
<div className="flex-1">{alias}</div>
58+
<div className="flex-1">{hotKey}</div>
59+
<div className="flex-1 flex items-center justify-end">
60+
<SettingsToggle
61+
label=""
62+
checked={enabled}
63+
className="scale-75"
64+
onChange={() => {}}
65+
/>
66+
</div>
67+
</div>
68+
</div>
69+
70+
{expand && <div className="text-sm">{content}</div>}
71+
</div>
72+
);
73+
};
74+
75+
export default Accordion;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import SettingsToggle from "@/components/Settings/SettingsToggle";
2+
import { useApplicationsStore } from "@/stores/applications";
3+
import platformAdapter from "@/utils/platformAdapter";
4+
import { useContext } from "react";
5+
import { ExtensionsContext } from "../../..";
6+
import clsx from "clsx";
7+
8+
const Applications = () => {
9+
const { activeId, setActiveId } = useContext(ExtensionsContext);
10+
11+
const allApps = useApplicationsStore((state) => state.allApps);
12+
const disabledApps = useApplicationsStore((state) => state.disabledApps);
13+
const setDisabledApps = useApplicationsStore((state) => {
14+
return state.setDisabledApps;
15+
});
16+
17+
return allApps.map((app) => {
18+
const { name, icon } = app;
19+
20+
return (
21+
<div
22+
key={name}
23+
className={clsx("flex items-center h-8 -mx-2 pl-10 pr-2 rounded-md", {
24+
"bg-[#f0f6fe] dark:bg-gray-700": name === activeId,
25+
})}
26+
onClick={() => {
27+
setActiveId(name);
28+
}}
29+
>
30+
<div className="flex items-center gap-1 w-[180px] pr-2 overflow-hidden">
31+
<img src={platformAdapter.convertFileSrc(icon)} className="size-5" />
32+
33+
<span className="text-sm truncate">{name}</span>
34+
</div>
35+
36+
<div className="flex-1 flex items-center text-[#999] ">
37+
<div className="flex-1">Application</div>
38+
<div className="flex-1">Add Alias</div>
39+
<div className="flex-1">Record Hotkey</div>
40+
<div className="flex-1 flex items-center justify-end">
41+
<SettingsToggle
42+
label=""
43+
checked={!disabledApps.includes(name)}
44+
className="scale-75"
45+
onChange={() => {
46+
if (disabledApps.includes(name)) {
47+
setDisabledApps(disabledApps.filter((app) => app !== name));
48+
} else {
49+
setDisabledApps([...disabledApps, name]);
50+
}
51+
}}
52+
/>
53+
</div>
54+
</div>
55+
</div>
56+
);
57+
});
58+
};
59+
60+
export default Applications;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { FC } from "react";
2+
import { Application } from "@/stores/applications";
3+
import { filesize } from "filesize";
4+
import dayjs from "dayjs";
5+
import { useTranslation } from "react-i18next";
6+
7+
interface AppProps {
8+
current: Application;
9+
}
10+
11+
const App: FC<AppProps> = (props) => {
12+
const { name, where, size, created, modified, lastOpened } = props.current;
13+
const { t } = useTranslation();
14+
15+
const metadata = [
16+
{
17+
label: t("settings.extensions.application.name"),
18+
value: name,
19+
},
20+
{
21+
label: t("settings.extensions.application.where"),
22+
value: where,
23+
},
24+
{
25+
label: t("settings.extensions.application.type"),
26+
value: t("settings.extensions.application.typeValue"),
27+
},
28+
{
29+
label: t("settings.extensions.application.size"),
30+
value: filesize(size, { standard: "jedec", spacer: "" }),
31+
},
32+
{
33+
label: t("settings.extensions.application.created"),
34+
value: dayjs(created).format("YYYY/MM/DD HH:mm:ss"),
35+
},
36+
{
37+
label: t("settings.extensions.application.modified"),
38+
value: dayjs(modified).format("YYYY/MM/DD HH:mm:ss"),
39+
},
40+
{
41+
label: t("settings.extensions.application.lastOpened"),
42+
value: dayjs(lastOpened).format("YYYY/MM/DD HH:mm:ss"),
43+
},
44+
];
45+
46+
return (
47+
<ul className="flex flex-col gap-2">
48+
{metadata.map((item) => {
49+
const { label, value } = item;
50+
51+
return (
52+
<li key={label} className="flex items-center justify-between gap-2">
53+
<span className="opacity-70">{label}</span>
54+
<span className="truncate max-w-[240px]">{value}</span>
55+
</li>
56+
);
57+
})}
58+
</ul>
59+
);
60+
};
61+
62+
export default App;

0 commit comments

Comments
 (0)