Skip to content

Commit

Permalink
feat: 在 Windows 和 MacOS 上支持显示剪贴板文件内容对应的系统图标 (#899)
Browse files Browse the repository at this point in the history
  • Loading branch information
ayangweb authored Dec 14, 2024
1 parent 7724af4 commit 3370f50
Show file tree
Hide file tree
Showing 11 changed files with 1,228 additions and 470 deletions.
969 changes: 790 additions & 179 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@tauri-apps/plugin-process": "^2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-sql": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.3.0",
"@unocss/reset": "^0.63.6",
"ahooks": "^3.8.4",
"antd": "^5.22.4",
Expand All @@ -56,7 +56,7 @@
"react-router-dom": "6.27.0",
"rehype-raw": "^7.0.0",
"rtf.js": "^3.0.9",
"tauri-plugin-fs-pro-api": "^2.2.0",
"tauri-plugin-fs-pro-api": "^2.3.0",
"tauri-plugin-macos-permissions-api": "^2.0.4",
"valtio": "^2.1.2"
},
Expand All @@ -74,12 +74,12 @@
"@tauri-apps/cli": "^2.1.0",
"@types/is-url": "^1.2.32",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.10.1",
"@types/react": "^18.3.14",
"@types/react-dom": "^18.3.3",
"@types/node": "^22.10.2",
"@types/react": "^18.3.16",
"@types/react-dom": "^18.3.5",
"@unocss/preset-rem-to-px": "^0.63.6",
"@vitejs/plugin-react": "^4.3.4",
"lint-staged": "^15.2.10",
"lint-staged": "^15.2.11",
"npm-run-all": "^4.1.5",
"release-it": "^17.10.0",
"sass": "^1.82.0",
Expand Down
539 changes: 291 additions & 248 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
"fs:allow-write-text-file",
"fs:allow-copy-file",
"fs:allow-remove",
"fs:allow-create",
"fs:allow-write",
"fs:allow-exists",
"fs:allow-mkdir",
"fs:allow-write-file",
{
"identifier": "fs:scope",
"allow": ["**/*"]
Expand Down
1 change: 1 addition & 0 deletions src/pages/Backup/components/SavePath/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const SavePath: FC<{ state: State }> = (props) => {
await transfer(getSaveDataPath(), dstPath, {
includes: [
await fullName(getSaveImagePath()),
await fullName(getSaveIconPath()),
await fullName(await getSaveDatabasePath()),
],
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { exists, mkdir, writeFile } from "@tauri-apps/plugin-fs";
import { Flex } from "antd";
import clsx from "clsx";
import type { FC } from "react";
import { type Metadata, icon, metadata } from "tauri-plugin-fs-pro-api";
import Image from "../../../Image";

interface FileProps {
path: string;
count: number;
}

interface State extends Partial<Metadata> {
iconPath?: string;
}

const File: FC<FileProps> = (props) => {
const { path, count } = props;

const state = useReactive<State>({});

useAsyncEffect(async () => {
try {
const data = await metadata(path, { omitSize: true });

Object.assign(state, data);

if (isLinux()) return;

state.iconPath = await getIconPath();
} catch {
Object.assign(state, {
fullName: path.split("/").pop(),
});
}
}, [path]);

const getIconPath = async () => {
const iconPath = joinPath(getSaveIconPath(), `${getIconName()}.png`);

const existed = await exists(iconPath);

if (existed) return iconPath;

await mkdir(getSaveIconPath(), { recursive: true });

const bytes = await icon(path, 256);

await writeFile(iconPath, bytes);

return iconPath;
};

const getIconName = () => {
const { isDir, extname, name, fullName } = state;

const isMacApp = isMac() && extname === "app";
const isWinApp = isWin() && extname === "exe";

if (isMacApp || isWinApp) {
return fullName;
}

if (isDir) {
return "__ECOPASTE_DIRECTORY__";
}

if (!extname) {
return name;
}

return extname;
};

const renderContent = () => {
const height = 100 / Math.min(count, 3);

if (state.isExist && count === 1) {
if (isImage(path)) {
return <Image value={path} />;
}
}

return (
<Flex align="center" gap={4} style={{ height: `${height}%` }}>
{state.isExist && <Image value={state.iconPath} className="h-full" />}

<span
className={clsx("truncate", {
"text-danger line-through": !state.isExist,
})}
>
{state.fullName}
</span>
</Flex>
);
};

return renderContent();
};

export default File;
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
import type { HistoryTablePayload } from "@/types/database";
import type { FC } from "react";
import Image from "../Image";
import File from "./components/File";

const Files: FC<HistoryTablePayload> = (props) => {
const { value } = props;

const paths: string[] = JSON.parse(value);

const renderContent = () => {
if (paths.length === 1) {
const [path] = paths;

if (isImage(path)) {
return <Image value={path} />;
}

return path;
}

return paths.join("\n");
};

return renderContent();
return paths.map((path) => {
return <File key={path} path={path} count={paths.length} />;
});
};

export default memo(Files);
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ import type { HistoryTablePayload } from "@/types/database";
import { convertFileSrc } from "@tauri-apps/api/core";
import type { FC } from "react";

const Image: FC<Partial<HistoryTablePayload>> = (props) => {
const { value = "" } = props;
interface ImageProps extends Partial<HistoryTablePayload> {
className?: string;
}

const Image: FC<ImageProps> = (props) => {
const { value = "", className = "max-h-full" } = props;

const [src, setSrc] = useState("");

useMount(async () => {
useAsyncEffect(async () => {
if (!value) return;

const src = await convertFileSrc(value);

setSrc(src);
});
}, [value]);

return <img src={src} className="max-h-full" />;
return <img src={src} className={className} />;
};

export default memo(Image);
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,8 @@ const Item: FC<ItemProps> = (props) => {
<div className="relative flex-1 select-auto overflow-hidden break-words children:transition">
<div
className={clsx(
"absolute inset-0 line-clamp-4 opacity-100 group-hover:opacity-0",
{
"opacity-0!": !note,
},
"pointer-events-none absolute inset-0 line-clamp-4 opacity-100 group-hover:opacity-0",
{ "opacity-0!": !note },
)}
>
<Icon
Expand Down
22 changes: 12 additions & 10 deletions src/plugins/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,19 +368,21 @@ export const getClipboardSubtype = async (data: ClipboardPayload) => {
try {
const { value } = data;

let subtype: ClipboardPayload["subtype"];

if (isURL(value)) {
subtype = "url";
} else if (isEmail(value)) {
subtype = "email";
} else if (isColor(value)) {
subtype = "color";
} else if (await exists(value)) {
subtype = "path";
return "url";
}

if (isEmail(value)) {
return "email";
}

return subtype;
if (isColor(value)) {
return "color";
}

if (await exists(value)) {
return "path";
}
} catch {
return;
}
Expand Down
7 changes: 7 additions & 0 deletions src/utils/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,10 @@ export const getSaveWindowStatePath = async () => {

return joinPath(await appDataDir(), `.window-state.${extname}`);
};

/**
* 获取存储系统图标的路径
*/
export const getSaveIconPath = () => {
return joinPath(getSaveDataPath(), "icons");
};

0 comments on commit 3370f50

Please sign in to comment.