├── src ├── main │ ├── db │ │ ├── index.ts │ │ ├── ipc.ts │ │ ├── tables.ts │ │ └── query.ts │ ├── windows.ts │ ├── ipc.ts │ ├── createWindow.ts │ ├── shortCut.ts │ ├── index.ts │ └── platformApps.ts ├── renderer │ ├── src │ │ ├── env.d.ts │ │ ├── data.ts │ │ ├── assets │ │ │ ├── tailwind.css │ │ │ ├── global.scss │ │ │ ├── base.css │ │ │ ├── wavy-lines.svg │ │ │ ├── main.css │ │ │ └── electron.svg │ │ ├── hooks │ │ │ ├── useCategory │ │ │ │ ├── styles.module.scss │ │ │ │ └── index.tsx │ │ │ ├── useCode.ts │ │ │ ├── useShortCut.ts │ │ │ ├── useContent.ts │ │ │ ├── useWindowHidden.ts │ │ │ ├── useIgnoreMouseEvents.ts │ │ │ ├── useSelect.ts │ │ │ └── useSearch.ts │ │ ├── pages │ │ │ ├── Category │ │ │ │ ├── CategoryLoader.ts │ │ │ │ ├── styles.module.scss │ │ │ │ ├── index.tsx │ │ │ │ ├── category.scss │ │ │ │ └── CategoryAction.ts │ │ │ ├── Tools │ │ │ │ ├── ToolsLoader.ts │ │ │ │ ├── tools.scss │ │ │ │ ├── ToolsAction.ts │ │ │ │ └── index.tsx │ │ │ ├── QuickView │ │ │ │ ├── QuickViewLoader.ts │ │ │ │ ├── quickView.scss │ │ │ │ ├── QuickViewAction.ts │ │ │ │ └── index.tsx │ │ │ ├── Setting │ │ │ │ ├── SettingLoader.ts │ │ │ │ ├── styles.scss │ │ │ │ ├── SettingAction.ts │ │ │ │ └── index.tsx │ │ │ ├── Welcome │ │ │ │ └── index.tsx │ │ │ ├── Content │ │ │ │ ├── ContentLoader.ts │ │ │ │ ├── ContentAction.ts │ │ │ │ ├── content.scss │ │ │ │ └── index.tsx │ │ │ └── ContentList │ │ │ │ ├── ContentListLoader.ts │ │ │ │ ├── ContentListAction.ts │ │ │ │ ├── contentList.scss │ │ │ │ └── index.tsx │ │ ├── utils.ts │ │ ├── components │ │ │ ├── ContentItem │ │ │ │ ├── styles.module.scss │ │ │ │ └── index.tsx │ │ │ ├── QuickViewItem │ │ │ │ ├── styles.module.scss │ │ │ │ ├── quickViewItem.scss │ │ │ │ └── index.tsx │ │ │ ├── Result │ │ │ │ ├── styles.scss │ │ │ │ ├── styled.tsx │ │ │ │ └── index.tsx │ │ │ ├── ShowContent │ │ │ │ ├── styled.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styles.scss │ │ │ ├── FooterMenu │ │ │ │ └── index.tsx │ │ │ ├── Error.tsx │ │ │ ├── CategoryItem │ │ │ │ ├── styles.module.scss │ │ │ │ └── index.tsx │ │ │ ├── QuickNav │ │ │ │ ├── styles.module.scss │ │ │ │ └── index.tsx │ │ │ ├── ToolItem │ │ │ │ ├── toolItem.scss │ │ │ │ └── index.tsx │ │ │ ├── ContentSearch │ │ │ │ └── index.tsx │ │ │ └── Search │ │ │ │ └── index.tsx │ │ ├── main.tsx │ │ ├── context │ │ │ └── CodeContext.tsx │ │ ├── layouts │ │ │ ├── Home │ │ │ │ └── index.tsx │ │ │ └── Config │ │ │ │ ├── styles.module.scss │ │ │ │ └── index.tsx │ │ ├── store │ │ │ └── useStore.ts │ │ └── router │ │ │ └── index.tsx │ └── index.html └── preload │ ├── index.d.ts │ └── index.ts ├── .eslintignore ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── resources └── icon.png ├── .prettierrc.yaml ├── .prettierignore ├── postcss.config.js ├── tsconfig.json ├── .npmrc ├── .editorconfig ├── tailwind.config.js ├── tsconfig.node.json ├── .eslintrc.cjs ├── tsconfig.web.json ├── electron.vite.config.ts ├── electron.vite.config.1710940922578.mjs ├── electron.vite.config.1716131513253.mjs ├── README.md ├── types.d.ts └── package.json /src/main/db/index.ts: -------------------------------------------------------------------------------- 1 | import './ipc' 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | -------------------------------------------------------------------------------- /src/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .DS_Store 5 | *.log* 6 | rapidle.db 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shadowDragons/rapidle/HEAD/resources/icon.png -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: false 3 | printWidth: 100 4 | trailingComma: none 5 | -------------------------------------------------------------------------------- /src/renderer/src/data.ts: -------------------------------------------------------------------------------- 1 | export interface DataType { 2 | id: number 3 | content: string 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /src/renderer/src/assets/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useCategory/styles.module.scss: -------------------------------------------------------------------------------- 1 | .darging { 2 | @apply bg-slate-400 text-white; 3 | } 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Category/CategoryLoader.ts: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | return window.api.sql('select * from categories order by id desc ', 'findAll') 3 | } 4 | -------------------------------------------------------------------------------- /src/renderer/src/utils.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | export const formatDate = (date: string | number | Date) => { 4 | return dayjs(date).format('YY/MM/DD') 5 | } 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | electron_mirror=https://npmmirror.com/mirrors/electron/ 2 | electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ 3 | shamefully-hoist=true 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /src/renderer/src/pages/Tools/ToolsLoader.ts: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | const sql = `SELECT * FROM tools` 3 | const sqlParams: string[] = [] 4 | return window.api.sql(sql, 'findAll', sqlParams) 5 | } 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {} 6 | }, 7 | plugins: [] 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/src/pages/QuickView/QuickViewLoader.ts: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | const sql = `SELECT * FROM quick_view` 3 | const sqlParams: string[] = [] 4 | return window.api.sql(sql, 'findAll', sqlParams) 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Setting/SettingLoader.ts: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | const config = (await window.api.sql(`select * from config where id=1`, 'findOne', {})) as { 3 | content: string 4 | } 5 | 6 | return JSON.parse(config.content) as ConfigDataType 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/src/components/ContentItem/styles.module.scss: -------------------------------------------------------------------------------- 1 | .link { 2 | @apply truncate py-[6px] px-1 cursor-pointer flex items-center gap-1 border-b mx-2 my-1 hover:bg-slate-100; 3 | } 4 | 5 | .active { 6 | @apply bg-blue-600 text-white rounded-md border-none hover:bg-blue-600; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", 3 | "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*","types.d.ts"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["electron-vite/node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/src/components/QuickViewItem/styles.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | @apply w-screen space-x-10 text-sm border-b px-2; 3 | display: flex; 4 | } 5 | 6 | .container > div { 7 | @apply h-5 my-10; 8 | } 9 | 10 | .container > div input { 11 | @apply h-10 px-2; 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/src/components/Result/styles.scss: -------------------------------------------------------------------------------- 1 | .result { 2 | @apply bg-slate-50 px-3 rounded-bl-lg rounded-br-lg -mt-[7px] pb-2; 3 | 4 | div { 5 | @apply text-slate-700 truncate px-2 py-1 rounded-lg; 6 | &.active { 7 | @apply bg-orange-400 text-white; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/src/components/Result/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | interface Props { 3 | isActive: boolean 4 | } 5 | export const Houdunren = styled.div` 6 | background-color: ${(props) => (props.isActive ? 'rebeccapurple' : '')}; 7 | color: ${(props) => (props.isActive ? '#fff' : '')}; 8 | ` 9 | -------------------------------------------------------------------------------- /src/renderer/src/components/ShowContent/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | interface Props { 3 | isActive: boolean 4 | } 5 | export const Houdunren = styled.div` 6 | background-color: ${(props) => (props.isActive ? 'rebeccapurple' : '')}; 7 | color: ${(props) => (props.isActive ? '#fff' : '')}; 8 | ` 9 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useCode.ts: -------------------------------------------------------------------------------- 1 | import { CodeContext } from '@renderer/context/CodeContext' 2 | import { useContext } from 'react' 3 | 4 | export default () => { 5 | const context = useContext(CodeContext) 6 | if (!context?.data) { 7 | throw new Error('CodeContext.provider 未正确定义') 8 | } 9 | return { ...context } 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 效率工具 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useShortCut.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useStore } from '@renderer/store/useStore' 3 | 4 | export function useShortcut() { 5 | const config = useStore((s) => s.config) 6 | 7 | useEffect(() => { 8 | if (config.shortCut) { 9 | window.api.shortCut(config.shortCut) 10 | } 11 | }, [config.shortCut]) 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Welcome/index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from '@icon-park/react' 2 | 3 | export const Welcome = () => { 4 | return ( 5 | 6 | 7 | 效率工具 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:react/recommended', 5 | 'plugin:react/jsx-runtime', 6 | '@electron-toolkit/eslint-config-ts/recommended', 7 | '@electron-toolkit/eslint-config-prettier' 8 | ], 9 | rules: { 10 | '@typescript-eslint/explicit-function-return-type': 'off', 11 | '@typescript-eslint/no-explicit-any': 'off' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/src/main.tsx: -------------------------------------------------------------------------------- 1 | import '@renderer/assets/global.scss' 2 | import '@renderer/assets/tailwind.css' 3 | import React from 'react' 4 | import ReactDOM from 'react-dom/client' 5 | import { RouterProvider } from 'react-router-dom' 6 | import router from './router' 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /src/renderer/src/assets/global.scss: -------------------------------------------------------------------------------- 1 | body { 2 | @apply w-screen h-screen; 3 | } 4 | * { 5 | opacity: 0.995; 6 | } 7 | 8 | .drag { 9 | -webkit-app-region: drag; 10 | } 11 | 12 | .nodrag, 13 | input, 14 | select, 15 | textarea, 16 | option { 17 | -webkit-app-region: no-drag; 18 | } 19 | 20 | .contextMenu { 21 | @apply rounded-md; 22 | button { 23 | @apply bg-[#2c3e50] text-white px-3 py-1 hover:bg-[#34495e]; 24 | & > div { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/src/components/FooterMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import { Add } from '@icon-park/react' 2 | import { useSubmit } from 'react-router-dom' 3 | 4 | export const FooterMenu = () => { 5 | const submit = useSubmit() 6 | return ( 7 | 8 | { 13 | submit(null, { method: 'POST' }) 14 | }} 15 | /> 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Content/ContentLoader.ts: -------------------------------------------------------------------------------- 1 | export default async ({ params }) => { 2 | const contentSql = `SELECT * FROM contents WHERE id = ?` 3 | const contentParams = [params.id] 4 | const content = await window.api.sql(contentSql, 'findOne', contentParams) 5 | 6 | const categoriesSql = 'SELECT * FROM categories ORDER BY id DESC' 7 | const categories = await window.api.sql(categoriesSql, 'findAll') 8 | 9 | return { 10 | content, 11 | categories 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "src/renderer/src/env.d.ts", 5 | "src/renderer/src/**/*", 6 | "src/renderer/src/**/*.tsx", 7 | "src/preload/*.d.ts", 8 | "types.d.ts" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "jsx": "react-jsx", 13 | "baseUrl": ".", 14 | "paths": { 15 | "@renderer/*": [ 16 | "src/renderer/src/*" 17 | ] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Setting/styles.scss: -------------------------------------------------------------------------------- 1 | .setting-page { 2 | @apply bg-gray-100 min-h-screen p-8; 3 | 4 | h1 { 5 | @apply text-2xl font-bold text-center mb-8 text-gray-800; 6 | } 7 | 8 | .setting-card { 9 | @apply mb-8 shadow-md; 10 | 11 | .ant-card-head-title { 12 | @apply text-lg font-semibold text-gray-700; 13 | } 14 | 15 | .ant-input { 16 | @apply text-base; 17 | } 18 | } 19 | 20 | .save-button { 21 | @apply block mx-auto; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useContent.ts: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom' 2 | 3 | export default () => { 4 | const navigate = useNavigate() 5 | const updateContentCategory = async (id: number, cateogry_id: number) => { 6 | await window.api.sql(`update contents set category_id=@cateogry_id where id=@id`, 'update', { 7 | id, 8 | cateogry_id 9 | }) 10 | navigate(`/config/category/contentList/${cateogry_id}/content/${id}`) 11 | } 12 | 13 | return { updateContentCategory } 14 | } 15 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 3 | import react from '@vitejs/plugin-react' 4 | 5 | export default defineConfig({ 6 | main: { 7 | plugins: [externalizeDepsPlugin()] 8 | }, 9 | preload: { 10 | plugins: [externalizeDepsPlugin()] 11 | }, 12 | renderer: { 13 | resolve: { 14 | alias: { 15 | '@renderer': resolve('src/renderer/src') 16 | } 17 | }, 18 | plugins: [react()] 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /src/renderer/src/components/Error.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from '@renderer/store/useStore' 2 | import { Alert } from 'antd' 3 | import { useEffect } from 'react' 4 | 5 | function Error() { 6 | const { error, setError } = useStore((state) => state) 7 | useEffect(() => { 8 | const id = setTimeout(() => setError(''), 2000) 9 | return () => clearTimeout(id) 10 | }, [error]) 11 | 12 | if (!error) return <>> 13 | 14 | return ( 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | export default Error 22 | -------------------------------------------------------------------------------- /src/renderer/src/context/CodeContext.tsx: -------------------------------------------------------------------------------- 1 | import { DataType } from '@renderer/data' 2 | import { Dispatch, ReactNode, SetStateAction, createContext, useState } from 'react' 3 | 4 | interface ContextProps { 5 | data: DataType[] 6 | setData: Dispatch> 7 | } 8 | export const CodeContext = createContext(undefined) 9 | interface Props { 10 | children: ReactNode 11 | } 12 | export const CodeProvider = ({ children }: Props) => { 13 | const [data, setData] = useState([]) 14 | return {children} 15 | } 16 | -------------------------------------------------------------------------------- /electron.vite.config.1710940922578.mjs: -------------------------------------------------------------------------------- 1 | // electron.vite.config.ts 2 | import { resolve } from 'path' 3 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 4 | import react from '@vitejs/plugin-react' 5 | var electron_vite_config_default = defineConfig({ 6 | main: { 7 | plugins: [externalizeDepsPlugin()] 8 | }, 9 | preload: { 10 | plugins: [externalizeDepsPlugin()] 11 | }, 12 | renderer: { 13 | resolve: { 14 | alias: { 15 | '@renderer': resolve('src/renderer/src') 16 | } 17 | }, 18 | plugins: [react()] 19 | } 20 | }) 21 | export { electron_vite_config_default as default } 22 | -------------------------------------------------------------------------------- /electron.vite.config.1716131513253.mjs: -------------------------------------------------------------------------------- 1 | // electron.vite.config.ts 2 | import { resolve } from 'path' 3 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 4 | import react from '@vitejs/plugin-react' 5 | var electron_vite_config_default = defineConfig({ 6 | main: { 7 | plugins: [externalizeDepsPlugin()] 8 | }, 9 | preload: { 10 | plugins: [externalizeDepsPlugin()] 11 | }, 12 | renderer: { 13 | resolve: { 14 | alias: { 15 | '@renderer': resolve('src/renderer/src') 16 | } 17 | }, 18 | plugins: [react()] 19 | } 20 | }) 21 | export { electron_vite_config_default as default } 22 | -------------------------------------------------------------------------------- /src/renderer/src/components/ShowContent/index.tsx: -------------------------------------------------------------------------------- 1 | import './styles.scss' 2 | import { useStore } from '@renderer/store/useStore' 3 | import ReactMarkdown from 'react-markdown' 4 | import remarkGfm from 'remark-gfm' 5 | 6 | export default function ShowContent() { 7 | const content = useStore((state) => state.content) 8 | 9 | return ( 10 | 11 | {children} 15 | }} 16 | > 17 | {content} 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Setting/SettingAction.ts: -------------------------------------------------------------------------------- 1 | export default async ({ request }) => { 2 | const formData = await request.formData() 3 | const data = Object.fromEntries(formData) 4 | 5 | try { 6 | const isRegister = await window.api.shortCut(data.shortCut) 7 | 8 | if (isRegister) { 9 | const sql = `UPDATE config SET content = ? WHERE id = 1` 10 | const params = [JSON.stringify(data)] 11 | await window.api.sql(sql, 'updateConfig', params) 12 | await window.api.initTable() 13 | } 14 | 15 | return {} 16 | } catch (error) { 17 | console.error('Error updating settings:', error) 18 | return { error: '更新设置时发生错误。' } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/src/pages/ContentList/ContentListLoader.ts: -------------------------------------------------------------------------------- 1 | export default async ({ params, request }) => { 2 | const url = new URL(request.url) 3 | const searchWord = url.searchParams.get('searchWord') 4 | const { cid } = params 5 | let sql = `SELECT * FROM contents` 6 | const sqlParams: string[] = [] 7 | 8 | if (searchWord) { 9 | sql += ` WHERE title LIKE ? ORDER BY id DESC` 10 | sqlParams.push(`%${searchWord}%`) 11 | return window.api.sql(sql, 'findAll', sqlParams) 12 | } 13 | 14 | if (cid !== undefined) { 15 | sql += ` WHERE category_id = ?` 16 | sqlParams.push(cid.toString()) 17 | } 18 | 19 | sql += ' ORDER BY id DESC' 20 | return window.api.sql(sql, 'findAll', sqlParams) 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useWindowHidden.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useStore } from '@renderer/store/useStore' 3 | 4 | export function useWindowHidden() { 5 | const { setData, setTotalPage, setContent, setPage, setSearch } = useStore() 6 | 7 | useEffect(() => { 8 | const handleWindowHidden = () => { 9 | setData([]) 10 | setTotalPage(1) 11 | setContent('') 12 | setPage(1) 13 | setSearch('') 14 | } 15 | 16 | window.electron.ipcRenderer.on('window-hidden', handleWindowHidden) 17 | 18 | return () => { 19 | window.electron.ipcRenderer.removeListener('window-hidden', handleWindowHidden) 20 | } 21 | }, [setData, setTotalPage, setContent, setPage]) 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Content/ContentAction.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from 'react-router-dom' 2 | 3 | export default async ({ request }) => { 4 | const formData = await request.formData() 5 | const data = Object.fromEntries(formData) 6 | 7 | try { 8 | const sql = `UPDATE contents SET title = ?, content = ?, category_id = ?, type = ? WHERE id = ?` 9 | const params2 = [data.title, data.content, data.category_id, data.type, data.id] 10 | await window.api.sql(sql, 'update', params2) 11 | 12 | return redirect(`/config/category/contentList/${data.category_id}/content/${data.id}`) 13 | } catch (error) { 14 | console.error('Error updating content:', error) 15 | return { error: '更新内容时发生错误。' } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Electron: Main", 8 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", 9 | "runtimeArgs": ["--remote-debugging-port=9223", "."], 10 | "windows": { 11 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" 12 | } 13 | }, 14 | { 15 | "name": "Electron: Renderer", 16 | "type": "chrome", 17 | "request": "attach", 18 | "port": 9223, 19 | "webRoot": "${workspaceFolder}", 20 | "timeout": 30000 21 | } 22 | ], 23 | "compounds": [ 24 | { 25 | "name": "Electron: All", 26 | "configurations": ["Electron: Main", "Electron: Renderer"] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rapidle 2 | 3 | 一个简约的桌面效率工具,支持记忆片段、快速访问链接、第三方 API 请求和快速启动系统应用。 4 | 5 |  6 | 7 | ## 功能特点 8 | 9 | - 🚀 即时访问:随时随地召唤 Rapidle 10 | - 📝 记忆片段:快速笔记和代码片段 11 | - 🔗 快速访问链接:书签和访问常用网址 12 | - 🛠️ API 集成:连接第三方工具 13 | - ⚡ 系统启动器:快速打开系统应用 14 | 15 | ## 即将推出 16 | 17 | - 📋 剪贴板历史 18 | - 🔄 多设备同步 19 | - 📸 截图和图片粘贴支持 20 | 21 | ## 快速开始 22 | 23 | ### 安装 24 | 25 | ```bash 26 | pnpm install 27 | ``` 28 | 29 | ### 开发 30 | 31 | ```bash 32 | pnpm run dev 33 | ``` 34 | 35 | ### 构建 36 | 37 | macOS 版本: 38 | 39 | ```bash 40 | pnpm run build:mac 41 | ``` 42 | 43 | Windows 版本: 44 | 45 | ```bash 46 | pnpm run build:windows 47 | ``` 48 | 49 | ## 贡献 50 | 51 | 欢迎贡献!请随时提交 Pull Request。 52 | 53 | ⭐ 如果您觉得这个项目有用,请考虑在 GitHub 上给它一个星标!您的支持帮助我们成长和改进项目。 54 | 55 | ## 许可证 56 | 57 | MIT License © 2025-PRESENT 58 | -------------------------------------------------------------------------------- /src/renderer/src/components/CategoryItem/styles.module.scss: -------------------------------------------------------------------------------- 1 | $height: 30px; 2 | @mixin commonLink { 3 | @apply px-2 py-2 truncate cursor-pointer block mx-1 my-1 rounded-md; 4 | height: $height; 5 | -webkit-user-drag: none; 6 | } 7 | .link { 8 | @apply flex items-center gap-2 px-3 py-2 rounded-md text-gray-700 9 | hover:bg-gray-100 transition-colors; 10 | 11 | svg { 12 | @apply text-gray-500; 13 | } 14 | 15 | span { 16 | @apply text-sm font-medium truncate; 17 | } 18 | } 19 | 20 | .active { 21 | @apply flex items-center gap-2 px-3 py-2 rounded-md 22 | bg-blue-50 text-blue-700; 23 | 24 | svg { 25 | @apply text-blue-600; 26 | } 27 | 28 | span { 29 | @apply text-sm font-medium truncate; 30 | } 31 | } 32 | 33 | .input { 34 | @apply px-3 py-2; 35 | 36 | input { 37 | @apply w-full px-3 py-1.5 text-sm border rounded-md 38 | focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/src/components/QuickNav/styles.module.scss: -------------------------------------------------------------------------------- 1 | @mixin commonLink { 2 | @apply px-2 py-2 truncate cursor-pointer block mx-1; 3 | } 4 | 5 | .quicknav { 6 | @apply sticky top-[0] bg-white; 7 | margin: -0.25rem -0.25rem 0.25rem -0.25rem; 8 | z-index: 15; 9 | 10 | &::before { 11 | content: ''; 12 | @apply absolute inset-0 bg-white; 13 | margin: -0.5px; 14 | z-index: -1; 15 | } 16 | 17 | .quicknav_content { 18 | @apply p-1 border-b; 19 | } 20 | } 21 | 22 | .link { 23 | @apply flex items-center gap-1 px-2 py-1.5 rounded-md text-gray-700 24 | hover:bg-gray-100 transition-colors; 25 | 26 | svg { 27 | @apply text-gray-500; 28 | } 29 | 30 | span { 31 | @apply text-sm font-medium truncate; 32 | } 33 | } 34 | 35 | .active { 36 | @apply flex items-center gap-1 px-2 py-1.5 rounded-md 37 | bg-blue-50 text-blue-700; 38 | 39 | svg { 40 | @apply text-blue-600; 41 | } 42 | 43 | span { 44 | @apply text-sm font-medium truncate; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/src/components/ShowContent/styles.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | @apply bg-slate-50 px-3 rounded-bl-lg rounded-br-lg -mt-[7px] pb-2; 3 | 4 | pre { 5 | @apply text-slate-700 break-words whitespace-pre-wrap px-2 py-1 rounded-lg; 6 | } 7 | 8 | // 基本的 Markdown 样式 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6 { 15 | margin-top: 24px; 16 | margin-bottom: 16px; 17 | font-weight: 600; 18 | line-height: 1.25; 19 | } 20 | 21 | p { 22 | margin-top: 0; 23 | margin-bottom: 16px; 24 | } 25 | 26 | ul, 27 | ol { 28 | padding-left: 2em; 29 | margin-top: 0; 30 | margin-bottom: 16px; 31 | } 32 | 33 | code { 34 | padding: 0.2em 0.4em; 35 | margin: 0; 36 | font-size: 85%; 37 | background-color: rgba(27, 31, 35, 0.05); 38 | border-radius: 3px; 39 | } 40 | 41 | pre { 42 | padding: 16px; 43 | overflow: auto; 44 | font-size: 85%; 45 | line-height: 1.45; 46 | background-color: #f6f8fa; 47 | border-radius: 3px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/db/ipc.ts: -------------------------------------------------------------------------------- 1 | import { IpcMainInvokeEvent } from 'electron/main' 2 | import { dialog, ipcMain } from 'electron' 3 | import * as query from './query' 4 | import { initTable } from './tables' 5 | 6 | ipcMain.handle( 7 | 'sql', 8 | async (_event: IpcMainInvokeEvent, sql: string, type: SqlActionType, params = []) => { 9 | try { 10 | const result = await query[type](sql, params) 11 | return result 12 | } catch (error) { 13 | console.error('SQL错误:', error) 14 | throw error 15 | } 16 | } 17 | ) 18 | 19 | ipcMain.handle('initTable', async () => { 20 | try { 21 | initTable() 22 | } catch (error) { 23 | console.error('SQL错误:', error) 24 | throw error 25 | } 26 | }) 27 | 28 | ipcMain.handle('selectDatabaseDirectory', async () => { 29 | const res = await dialog.showOpenDialog({ 30 | title: '选择目录', 31 | properties: ['openDirectory', 'createDirectory'] 32 | }) 33 | return res.canceled ? '' : res.filePaths[0] 34 | }) 35 | 36 | ipcMain.on('initTable', () => { 37 | initTable() 38 | }) 39 | -------------------------------------------------------------------------------- /src/renderer/src/components/ToolItem/toolItem.scss: -------------------------------------------------------------------------------- 1 | .tool-item { 2 | @apply bg-white rounded-lg; 3 | 4 | .tool-form { 5 | @apply flex items-center gap-3; 6 | 7 | .ant-form-item { 8 | @apply mb-0; 9 | } 10 | 11 | .name { 12 | @apply w-1/6; 13 | 14 | input { 15 | @apply rounded-md; 16 | } 17 | } 18 | 19 | .abbr { 20 | @apply w-1/12; 21 | 22 | input { 23 | @apply rounded-md; 24 | } 25 | } 26 | 27 | .url { 28 | @apply w-2/3; 29 | 30 | input { 31 | @apply rounded-md; 32 | } 33 | } 34 | 35 | .actions { 36 | @apply w-1/12 flex justify-end; 37 | 38 | button { 39 | @apply flex items-center justify-center w-8 h-8 rounded-md 40 | hover:bg-gray-100 transition-colors; 41 | 42 | &.save-btn { 43 | @apply text-blue-600 hover:bg-blue-50; 44 | } 45 | 46 | &.delete-btn { 47 | @apply text-red-600 hover:bg-red-50; 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/src/components/QuickViewItem/quickViewItem.scss: -------------------------------------------------------------------------------- 1 | .search-item { 2 | @apply bg-white rounded-lg; 3 | 4 | .search-form { 5 | @apply flex items-center gap-3; 6 | 7 | .ant-form-item { 8 | @apply mb-0; 9 | } 10 | 11 | .name { 12 | @apply w-1/6; 13 | 14 | input { 15 | @apply rounded-md; 16 | } 17 | } 18 | 19 | .abbr { 20 | @apply w-1/12; 21 | 22 | input { 23 | @apply rounded-md; 24 | } 25 | } 26 | 27 | .url { 28 | @apply w-2/3; 29 | 30 | input { 31 | @apply rounded-md; 32 | } 33 | } 34 | 35 | .actions { 36 | @apply w-1/12 flex justify-end; 37 | 38 | button { 39 | @apply flex items-center justify-center w-8 h-8 rounded-md 40 | hover:bg-gray-100 transition-colors; 41 | 42 | &.save-btn { 43 | @apply text-blue-600 hover:bg-blue-50; 44 | } 45 | 46 | &.delete-btn { 47 | @apply text-red-600 hover:bg-red-50; 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/preload/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronAPI } from '@electron-toolkit/preload' 2 | 3 | declare global { 4 | interface Window { 5 | electron: ElectronAPI 6 | api: { 7 | shortCut: (shortCut: string) => Promise 8 | dbChange: (config: ConfigDataType) => void 9 | setIgnoreMouseEvents: (ignore: boolean, options?: { forward: boolean }) => void 10 | openConfigWindow: () => void 11 | sql: (sql: string, type: SqlActionType, params?: Record) => Promise 12 | openWindow: (name: WindowNameType) => void 13 | closeWindow: (name: WindowNameType) => void 14 | selectDatabaseDirectory: () => Promise 15 | setDatabaseDirectory: (path: string) => void 16 | initTable: () => void 17 | openExternal: (url: string) => void 18 | getInstalledApps: (keyword: string) => Promise 19 | launchApp: (exce: string) => void 20 | getProcessList: (keyword: string) => Promise 21 | closeProcess: (processName: string) => void 22 | getPlatform: () => Promise 23 | restartApp: () => void 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Category/styles.module.scss: -------------------------------------------------------------------------------- 1 | .category { 2 | display: flex; 3 | height: 100%; 4 | width: 100%; 5 | 6 | .category-list { 7 | width: 250px; 8 | border-right: 1px solid #eee; 9 | overflow-y: auto; 10 | background: #fff; 11 | 12 | .category-header { 13 | padding: 1rem; 14 | border-bottom: 1px solid #eee; 15 | 16 | h2 { 17 | font-size: 1rem; 18 | font-weight: 500; 19 | margin-bottom: 0.5rem; 20 | } 21 | 22 | .add-category-btn { 23 | display: flex; 24 | align-items: center; 25 | gap: 0.5rem; 26 | padding: 0.5rem; 27 | border-radius: 0.375rem; 28 | background: #f3f4f6; 29 | color: #374151; 30 | font-size: 0.875rem; 31 | width: 100%; 32 | 33 | &:hover { 34 | background: #e5e7eb; 35 | } 36 | 37 | span { 38 | font-size: 0.875rem; 39 | } 40 | } 41 | } 42 | 43 | // 分类列表容器 44 | > div:last-child { 45 | padding: 0.5rem; 46 | } 47 | } 48 | 49 | .category-content { 50 | flex: 1; 51 | display: flex; 52 | overflow: hidden; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/renderer/src/pages/ContentList/ContentListAction.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from 'react-router-dom' 2 | 3 | export default async ({ request }) => { 4 | const formData = await request.formData() 5 | const data = Object.fromEntries(formData) 6 | 7 | // 获取当前时间的函数 8 | const getCurrentTimestamp = () => { 9 | return new Date().toISOString().slice(0, 19).replace('T', ' ') 10 | } 11 | try { 12 | switch (request.method) { 13 | case 'POST': { 14 | const sql = `INSERT INTO contents (title, content, category_id, created_at) VALUES (?, ?, ?, ?)` 15 | const params = ['未命名片段', '', data.category_id, getCurrentTimestamp()] 16 | const result = await window.api.sql(sql, 'insert', params) 17 | const id = result 18 | return redirect(`content/${id}`) 19 | } 20 | case 'DELETE': { 21 | const sql = `DELETE FROM contents WHERE id = ?` 22 | const params = [data.id] 23 | await window.api.sql(sql, 'del', params) 24 | return {} 25 | } 26 | } 27 | } catch (error) { 28 | console.error('Error performing content action:', error) 29 | return { error: '处理请求时发生错误。' } 30 | } 31 | 32 | return {} 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/src/components/Result/index.tsx: -------------------------------------------------------------------------------- 1 | import useSelect from '@renderer/hooks/useSelect' 2 | import classNames from 'classnames' 3 | import './styles.scss' 4 | 5 | export default function Result() { 6 | const { data, id, selectItem } = useSelect() 7 | 8 | const getItemTitle = (item: any) => { 9 | const types = { 10 | isTool: '工具', 11 | isApp: '应用', 12 | isQuickView: '快速访问', 13 | isProcess: '进程', 14 | isSnippet: '片段' 15 | } 16 | 17 | let title = item.title 18 | if (item.isSnippet) { 19 | title = `${item.categorie_name} - ${item.title}` 20 | } 21 | 22 | for (const [key, prefix] of Object.entries(types)) { 23 | if (item[key]) { 24 | return `${prefix} - ${title}` 25 | } 26 | } 27 | return title 28 | } 29 | 30 | return ( 31 | 0 })}> 32 | {data.map((item) => ( 33 | selectItem(item.id)} 37 | > 38 | {getItemTitle(item)} 39 | 40 | ))} 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Tools/tools.scss: -------------------------------------------------------------------------------- 1 | .tools-page { 2 | @apply h-full flex flex-col bg-white; 3 | 4 | .page-header { 5 | @apply sticky top-0 bg-white border-b z-20; 6 | 7 | .header-content { 8 | @apply px-6 py-4 flex items-center justify-between; 9 | 10 | h2 { 11 | @apply text-lg font-medium text-gray-800; 12 | } 13 | 14 | .add-btn { 15 | @apply flex items-center gap-1.5 px-3 py-1.5 16 | text-sm font-medium text-white bg-blue-600 17 | rounded-md hover:bg-blue-700 transition-colors; 18 | } 19 | } 20 | } 21 | 22 | .table-container { 23 | @apply flex-1 overflow-y-auto px-6 pb-6 relative; 24 | 25 | .table-header { 26 | @apply flex items-center py-3 text-sm font-medium text-gray-500 border-b 27 | sticky top-0 bg-white z-10 -mx-6 px-6; 28 | 29 | .col-name { 30 | @apply w-1/6; 31 | } 32 | .col-abbr { 33 | @apply w-1/12; 34 | } 35 | .col-url { 36 | @apply w-2/3; 37 | } 38 | .col-action { 39 | @apply w-1/12 text-right; 40 | } 41 | } 42 | 43 | .table-body { 44 | @apply space-y-2 pt-3; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/src/components/QuickNav/index.tsx: -------------------------------------------------------------------------------- 1 | import { AllApplication, FolderFailed } from '@icon-park/react' 2 | import { NavLink } from 'react-router-dom' 3 | import styles from './styles.module.scss' 4 | export const QuickNav = () => { 5 | return ( 6 | 7 | 8 | (isActive ? styles.active : styles.link)} 12 | > 13 | 14 | 15 | 所有片段 16 | 17 | 18 | (isActive ? styles.active : styles.link)} 22 | > 23 | 24 | 25 | 未分类 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/src/pages/QuickView/quickView.scss: -------------------------------------------------------------------------------- 1 | .quickview-page { 2 | @apply h-full flex flex-col bg-white; 3 | 4 | .page-header { 5 | @apply sticky top-0 bg-white border-b z-20; 6 | 7 | .header-content { 8 | @apply px-6 py-4 flex items-center justify-between; 9 | 10 | h2 { 11 | @apply text-lg font-medium text-gray-800; 12 | } 13 | 14 | .add-btn { 15 | @apply flex items-center gap-1.5 px-3 py-1.5 16 | text-sm font-medium text-white bg-blue-600 17 | rounded-md hover:bg-blue-700 transition-colors; 18 | } 19 | } 20 | } 21 | 22 | .table-container { 23 | @apply flex-1 overflow-y-auto px-6 pb-6 relative; 24 | 25 | .table-header { 26 | @apply flex items-center py-3 text-sm font-medium text-gray-500 border-b 27 | sticky top-0 bg-white z-10 -mx-6 px-6; 28 | 29 | .col-name { 30 | @apply w-1/6; 31 | } 32 | .col-abbr { 33 | @apply w-1/12; 34 | } 35 | .col-url { 36 | @apply w-2/3; 37 | } 38 | .col-action { 39 | @apply w-1/12 text-right; 40 | } 41 | } 42 | 43 | .table-body { 44 | @apply space-y-2 pt-3; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | type SqlActionType = 'findAll' | 'findOne' | 'insert' | 'update' | 'del' | 'config' | 'updateConfig' 2 | 3 | type CategoryType = { 4 | id: number 5 | name: string 6 | created_at: string 7 | } 8 | 9 | type ContentType = { 10 | id: number 11 | title: string 12 | category_id: number 13 | content: string 14 | created_at: string 15 | type: number 16 | } 17 | 18 | type AppInfo = { 19 | id: number 20 | title: string 21 | exec: string 22 | } 23 | 24 | type HomeSearchItem = { 25 | id: number 26 | title: string 27 | content: string 28 | categorie_name: string 29 | exec: string 30 | type: number 31 | cnt: number 32 | isApp: boolean 33 | isTool: boolean 34 | isQuickView: boolean 35 | isProcess: boolean 36 | isSnippet: boolean 37 | } 38 | 39 | type ConfigType = { 40 | id: number 41 | content: string 42 | } 43 | 44 | type ConfigDataType = { 45 | shortCut: string 46 | databaseDirectory: string 47 | } 48 | 49 | interface ToolType { 50 | id: number 51 | name: string 52 | abbr: string 53 | url: string 54 | } 55 | type QuickViewType = { 56 | id: number 57 | name: string 58 | abbr: string 59 | url: string 60 | create_at: string 61 | } 62 | 63 | type WindowNameType = 'search' | 'config' | 'code' | 'content' 64 | -------------------------------------------------------------------------------- /src/renderer/src/components/ContentSearch/index.tsx: -------------------------------------------------------------------------------- 1 | import { Search, Add } from '@icon-park/react' 2 | import { Form, useSubmit } from 'react-router-dom' 3 | 4 | export const ContentSearch = () => { 5 | const submit = useSubmit() 6 | 7 | return ( 8 | 9 | 10 | 11 | 12 | submit(e.target.form)} 18 | /> 19 | 20 | { 24 | submit(null, { method: 'POST' }) 25 | }} 26 | > 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/main/windows.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, IpcMainEvent, IpcMainInvokeEvent } from 'electron' 2 | import { createWindow, OptionsType } from './createWindow' 3 | 4 | export const config = { 5 | search: { 6 | id: 0, 7 | options: { 8 | initShow: true, 9 | hash: '', 10 | openDevTools: false 11 | } 12 | }, 13 | code: { 14 | id: 0, 15 | options: { 16 | initShow: false, 17 | openDevTools: false, 18 | width: 1060, 19 | height: 600, 20 | frame: true, 21 | transparent: false, 22 | hash: '/#config/category' 23 | } 24 | }, 25 | config: { 26 | id: 0, 27 | options: { 28 | initShow: false, 29 | openDevTools: false, 30 | width: 505, 31 | height: 350, 32 | frame: true, 33 | transparent: false, 34 | hash: '/#config' 35 | } 36 | } 37 | } as Record 38 | 39 | export const getByNameWindow = (name: WindowNameType) => { 40 | let win = BrowserWindow.fromId(config[name].id) 41 | if (!win) { 42 | win = createWindow(config[name].options) 43 | config[name].id = win.id 44 | } 45 | return win 46 | } 47 | 48 | export const getWindowByEvent = (event: IpcMainEvent | IpcMainInvokeEvent) => { 49 | return BrowserWindow.fromWebContents(event.sender)! 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Tools/ToolsAction.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from 'react-router-dom' 2 | 3 | export default async ({ request }) => { 4 | const formData = await request.formData() 5 | const data = Object.fromEntries(formData) 6 | 7 | const getCurrentTimestamp = () => { 8 | return new Date().toISOString().slice(0, 19).replace('T', ' ') 9 | } 10 | try { 11 | switch (request.method) { 12 | case 'POST': { 13 | const sql = `INSERT INTO tools (name, abbr, url, created_at) VALUES (?, ?, ?, ?)` 14 | const params = ['未命名', '', '', getCurrentTimestamp()] 15 | await window.api.sql(sql, 'insert', params) 16 | break 17 | } 18 | case 'DELETE': { 19 | const sql = `DELETE FROM tools WHERE id = ?` 20 | const params = [data.id] 21 | await window.api.sql(sql, 'del', params) 22 | break 23 | } 24 | case 'PUT': { 25 | const sql = `UPDATE tools SET name = ?, abbr = ?, url = ? WHERE id = ?` 26 | const params = [data.name, data.abbr, data.url, data.id] 27 | await window.api.sql(sql, 'update', params) 28 | break 29 | } 30 | } 31 | 32 | return redirect('/config/tools') 33 | } catch (error) { 34 | console.error('Error performing tools action:', error) 35 | return { error: '处理请求时发生错误。' } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/renderer/src/pages/QuickView/QuickViewAction.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from 'react-router-dom' 2 | 3 | export default async ({ request }) => { 4 | const formData = await request.formData() 5 | const data = Object.fromEntries(formData) 6 | 7 | const getCurrentTimestamp = () => { 8 | return new Date().toISOString().slice(0, 19).replace('T', ' ') 9 | } 10 | 11 | try { 12 | switch (request.method) { 13 | case 'POST': { 14 | const sql = `INSERT INTO quick_view (name, abbr, url, created_at) VALUES (?, ?, ?, ?)` 15 | const params = ['未命名', '', '', getCurrentTimestamp()] 16 | await window.api.sql(sql, 'insert', params) 17 | 18 | break 19 | } 20 | case 'DELETE': { 21 | const sql = `DELETE FROM quick_view WHERE id = ?` 22 | const params = [data.id] 23 | await window.api.sql(sql, 'del', params) 24 | break 25 | } 26 | case 'PUT': { 27 | const sql = `UPDATE quick_view SET name = ?, abbr = ?, url = ? WHERE id = ?` 28 | const params = [data.name, data.abbr, data.url, data.id] 29 | await window.api.sql(sql, 'update', params) 30 | break 31 | } 32 | } 33 | 34 | return redirect('/config/quickView') 35 | } catch (error) { 36 | console.error('Error performing quick view action:', error) 37 | return { error: '处理请求时发生错误。' } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Category/index.tsx: -------------------------------------------------------------------------------- 1 | import { CategoryItem } from '@renderer/components/CategoryItem' 2 | import { QuickNav } from '@renderer/components/QuickNav' 3 | import { useLoaderData, Outlet, useFetcher } from 'react-router-dom' 4 | import { IconFolderPlus } from '@tabler/icons-react' 5 | import './category.scss' 6 | import styles from './styles.module.scss' 7 | 8 | export const Category = () => { 9 | const categories = useLoaderData() as CategoryType[] 10 | const fetcher = useFetcher() 11 | 12 | const handleAddCategory = () => { 13 | fetcher.submit({ name: '新分类' }, { method: 'POST' }) 14 | } 15 | 16 | return ( 17 | 18 | 19 | 20 | 分类管理 21 | 22 | 23 | 新建分类 24 | 25 | 26 | 27 | 28 | 29 | {categories.map((category) => ( 30 | 31 | ))} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Category/category.scss: -------------------------------------------------------------------------------- 1 | .category-page { 2 | @apply h-full w-full flex; 3 | 4 | .categories-section { 5 | @apply w-[160px] bg-white border-r flex flex-col; 6 | flex-shrink: 0; 7 | z-index: 10; 8 | 9 | .categories-header { 10 | @apply px-4 py-3 border-b flex items-center justify-between sticky top-0 bg-white; 11 | z-index: 12; 12 | 13 | h2 { 14 | @apply text-base font-medium text-gray-800; 15 | } 16 | 17 | .add-category-btn { 18 | @apply flex items-center gap-1 px-2 py-1 text-sm text-blue-600 19 | hover:bg-blue-50 rounded-md transition-colors; 20 | 21 | &:hover { 22 | @apply text-blue-700; 23 | } 24 | } 25 | } 26 | 27 | .categories-list { 28 | @apply flex-1 overflow-y-auto px-3 py-2 space-y-1.5 relative; 29 | z-index: 11; 30 | 31 | &::-webkit-scrollbar { 32 | @apply w-1.5; 33 | } 34 | 35 | &::-webkit-scrollbar-track { 36 | @apply bg-transparent; 37 | } 38 | 39 | &::-webkit-scrollbar-thumb { 40 | @apply bg-gray-300 rounded-full; 41 | 42 | &:hover { 43 | @apply bg-gray-400; 44 | } 45 | } 46 | } 47 | } 48 | 49 | .content-section { 50 | @apply flex-1 bg-white overflow-y-auto; 51 | display: grid; 52 | grid-template-columns: minmax(0, 1fr); 53 | place-items: start center; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/renderer/src/components/Search/index.tsx: -------------------------------------------------------------------------------- 1 | import { Setting } from '@icon-park/react' 2 | import useSearch from '@renderer/hooks/useSearch' 3 | import { Input } from 'antd' 4 | import { useEffect, useRef } from 'react' 5 | import type { InputRef } from 'antd' 6 | 7 | export default function Search() { 8 | const { handleSearch, search } = useSearch() 9 | const inputRef = useRef(null) 10 | 11 | useEffect(() => { 12 | const focusInput = () => { 13 | if (inputRef.current) { 14 | inputRef.current.focus() 15 | } 16 | } 17 | 18 | window.electron.ipcRenderer.on('focus-input', focusInput) 19 | 20 | return () => { 21 | window.electron.ipcRenderer.removeAllListeners('focus-input') 22 | } 23 | }, []) 24 | return ( 25 | 26 | 27 | handleSearch(e.target.value, 1)} 32 | autoFocus 33 | className="flex-grow" 34 | /> 35 | window.api.openWindow('code')} 41 | /> 42 | 43 | 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Category/CategoryAction.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from 'react-router-dom' 2 | 3 | export default async ({ request }) => { 4 | const formData = await request.formData() 5 | const data = Object.fromEntries(formData) 6 | 7 | const getCurrentTimestamp = () => { 8 | return new Date().toISOString().slice(0, 19).replace('T', ' ') 9 | } 10 | try { 11 | switch (request.method) { 12 | case 'POST': { 13 | const sql = `INSERT INTO categories (name, created_at) VALUES (?, ?)` 14 | const params = ['未命名', getCurrentTimestamp()] 15 | await window.api.sql(sql, 'insert', params) 16 | break 17 | } 18 | case 'DELETE': { 19 | const deleteSql = `DELETE FROM categories WHERE id = ?` 20 | const updateSql = `UPDATE contents SET category_id = 0 WHERE category_id = ?` 21 | const params = [data.id] 22 | await window.api.sql(deleteSql, 'del', params) 23 | await window.api.sql(updateSql, 'update', params) 24 | return redirect('/config/category') 25 | } 26 | case 'PUT': { 27 | const sql = `UPDATE categories SET name = ? WHERE id = ?` 28 | const params = [data.name, data.id] 29 | await window.api.sql(sql, 'update', params) 30 | break 31 | } 32 | } 33 | 34 | return redirect('/config/category/contentList') 35 | } catch (error) { 36 | console.error('Error performing category action:', error) 37 | return { error: '处理请求时发生错误。' } 38 | } 39 | 40 | return {} 41 | } 42 | -------------------------------------------------------------------------------- /src/main/ipc.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain, IpcMainEvent, shell } from 'electron' 2 | import { getByNameWindow, getWindowByEvent } from './windows' 3 | import { 4 | closeProcess, 5 | getInstalledApps, 6 | getPlatform, 7 | getProcessList, 8 | launchApp 9 | } from './platformApps' 10 | 11 | ipcMain.on('openWindow', (_event: IpcMainEvent, name: WindowNameType) => { 12 | getByNameWindow(name).show() 13 | }) 14 | 15 | ipcMain.on('closeWindow', (_event: IpcMainEvent, name: WindowNameType) => { 16 | getByNameWindow(name).hide() 17 | }) 18 | 19 | ipcMain.on( 20 | 'setIgnoreMouseEvents', 21 | (event: IpcMainEvent, ignore: boolean, options?: { forward: boolean }) => { 22 | getWindowByEvent(event).setIgnoreMouseEvents(ignore, options) 23 | } 24 | ) 25 | 26 | ipcMain.handle('openExternal', async (_, url: string) => { 27 | await shell.openExternal(url) 28 | }) 29 | 30 | ipcMain.handle('getInstalledApps', async (_, keyword: string) => { 31 | return await getInstalledApps(keyword) 32 | }) 33 | 34 | ipcMain.handle('getProcessList', async (_, keyword: string) => { 35 | return await getProcessList(keyword) 36 | }) 37 | 38 | ipcMain.on('closeProcess', async (_, processName: string) => { 39 | closeProcess(processName) 40 | }) 41 | 42 | ipcMain.handle('getPlatform', () => { 43 | return getPlatform() 44 | }) 45 | 46 | ipcMain.on('launchApp', async (_, exec: string) => { 47 | launchApp(exec) 48 | }) 49 | 50 | ipcMain.on('restart-app', () => { 51 | app.relaunch({ args: process.argv.slice(1).concat(['--relaunch']) }) 52 | app.quit() 53 | }) 54 | -------------------------------------------------------------------------------- /src/renderer/src/components/ContentItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { Delete } from '@icon-park/react' 2 | import dayjs from 'dayjs' 3 | import { useContextMenu } from 'mantine-contextmenu' 4 | import { NavLink, useSubmit, useNavigate } from 'react-router-dom' 5 | import styles from './styles.module.scss' 6 | interface Props { 7 | content: ContentType 8 | } 9 | export const ContentItem = ({ content }: Props) => { 10 | const submit = useSubmit() 11 | const { showContextMenu } = useContextMenu() 12 | const navigate = useNavigate() 13 | 14 | const handleClick = () => { 15 | navigate(`content/${content.id}`) 16 | } 17 | 18 | return ( 19 | { 23 | return [isActive ? styles.active : '', styles.link].join(' ') 24 | }} 25 | onDragStart={(e) => { 26 | e.dataTransfer.setData('id', String(content.id)) 27 | }} 28 | onContextMenu={showContextMenu( 29 | [ 30 | { 31 | key: 'remove', 32 | icon: , 33 | title: '删除片段', 34 | onClick: () => { 35 | submit({ id: content.id }, { method: 'DELETE' }) 36 | } 37 | } 38 | ], 39 | { className: 'contextMenu' } 40 | )} 41 | onClick={handleClick} 42 | > 43 | {content.title} 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Setting/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useStore } from '@renderer/store/useStore' 3 | import { Form, useSubmit } from 'react-router-dom' 4 | import { Input, Card } from 'antd' 5 | import './styles.scss' 6 | 7 | export const Setting = () => { 8 | const [keys, setKeys] = useState([]) 9 | const config = useStore((s) => s.config) 10 | const setConfig = useStore((s) => s.setConfig) 11 | const submit = useSubmit() 12 | 13 | const handleKeyDown = (e) => { 14 | if (e.metaKey || e.ctrlKey || e.altKey) { 15 | const code = e.code.replace(/Left|Right|Key|Digit/, '') 16 | if (keys.includes(code)) return 17 | const newKeys = [...keys, code] 18 | setKeys(newKeys) 19 | if (code.match(/^(\w|Space)$/gi)) { 20 | const newShortcut = newKeys.join('+') 21 | setKeys([]) 22 | const updatedConfig = { ...config, shortCut: newShortcut } 23 | setConfig(updatedConfig) 24 | window.api.shortCut(newShortcut) 25 | submit(updatedConfig, { method: 'post' }) 26 | } 27 | } 28 | } 29 | 30 | return ( 31 | 32 | 33 | 软件配置 34 | 35 | 43 | 44 | 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/src/components/QuickViewItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Form, Input, Button } from 'antd' 3 | import { DeleteOutlined, SaveOutlined } from '@ant-design/icons' 4 | import { useFetcher } from 'react-router-dom' 5 | import './quickViewItem.scss' 6 | 7 | export const QuickViewItem = ({ search, onDelete }: { search: any; onDelete: any }) => { 8 | const [form] = Form.useForm() 9 | const [isEditing, setIsEditing] = useState(false) 10 | const fetcher = useFetcher() 11 | 12 | const handleSubmit = (values) => { 13 | fetcher.submit({ ...values, id: search.id }, { method: 'PUT' }) 14 | setIsEditing(false) 15 | } 16 | 17 | return ( 18 | 19 | 20 | 21 | setIsEditing(true)} /> 22 | 23 | 24 | setIsEditing(true)} /> 25 | 26 | 27 | setIsEditing(true)} /> 28 | 29 | 30 | {isEditing ? ( 31 | } /> 32 | ) : ( 33 | } /> 34 | )} 35 | 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useCategory/index.tsx: -------------------------------------------------------------------------------- 1 | import { Delete } from '@icon-park/react' 2 | import { useContextMenu } from 'mantine-contextmenu' 3 | import { useSubmit } from 'react-router-dom' 4 | import styles from './styles.module.scss' 5 | import useContent from '../useContent' 6 | import { DragEvent } from 'react' 7 | export default (category: CategoryType) => { 8 | const submit = useSubmit() 9 | const { showContextMenu } = useContextMenu() 10 | const { updateContentCategory } = useContent() 11 | const contextMenu = () => { 12 | return showContextMenu( 13 | [ 14 | { 15 | key: 'remove', 16 | icon: , 17 | title: '删除分类', 18 | onClick: () => { 19 | submit({ id: category.id }, { method: 'DELETE' }) 20 | } 21 | } 22 | ], 23 | { className: 'contextMenu' } 24 | ) 25 | } 26 | 27 | const dragHandle = { 28 | onDragOver: (e: DragEvent) => { 29 | e.preventDefault() 30 | e!.dataTransfer!.dropEffect = 'move' 31 | const el = e.currentTarget as HTMLDivElement 32 | el.classList.add(styles.darging) 33 | }, 34 | onDragLeave: (e: DragEvent) => { 35 | const el = e.currentTarget as HTMLDivElement 36 | el.classList.remove(styles.darging) 37 | }, 38 | onDrop: (e: DragEvent) => { 39 | const el = e.currentTarget as HTMLDivElement 40 | el.classList.remove(styles.darging) 41 | const id = e!.dataTransfer!.getData('id') 42 | updateContentCategory(Number(id), category.id) 43 | } 44 | } 45 | 46 | return { contextMenu, dragHandle } 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/src/layouts/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import isPropValid from '@emotion/is-prop-valid' 2 | import Error from '@renderer/components/Error' 3 | import Result from '@renderer/components/Result' 4 | import Search from '@renderer/components/Search' 5 | import ShowContent from '@renderer/components/ShowContent' 6 | import useIgnoreMouseEvents from '@renderer/hooks/useIgnoreMouseEvents' 7 | import { useShortcut } from '@renderer/hooks/useShortCut' 8 | import { useWindowHidden } from '@renderer/hooks/useWindowHidden' 9 | import { useEffect, useRef } from 'react' 10 | import { StyleSheetManager } from 'styled-components' 11 | 12 | function Home(): JSX.Element { 13 | const mainRef = useRef(null) 14 | const { setIgnoreMouseEvents, platform } = useIgnoreMouseEvents() 15 | useWindowHidden() 16 | useShortcut() 17 | useEffect(() => { 18 | let cleanup: (() => void) | undefined 19 | 20 | if (platform !== null) { 21 | cleanup = setIgnoreMouseEvents(mainRef) 22 | } 23 | 24 | return () => { 25 | if (cleanup) { 26 | cleanup() 27 | } 28 | } 29 | }, [setIgnoreMouseEvents, platform]) 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ) 44 | } 45 | export default Home 46 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useIgnoreMouseEvents.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useCallback, useEffect, useState } from 'react' 2 | 3 | type Platform = 'aix' | 'darwin' | 'freebsd' | 'linux' | 'openbsd' | 'sunos' | 'win32' | string 4 | 5 | export default () => { 6 | const [platform, setPlatform] = useState(null) 7 | 8 | useEffect(() => { 9 | window.api.getPlatform().then((result: string) => { 10 | setPlatform(result as Platform) 11 | }) 12 | }, []) 13 | 14 | const setIgnoreMouseEvents = useCallback( 15 | (el: MutableRefObject) => { 16 | if (!el.current || !platform) return 17 | 18 | const handleMouseOver = () => { 19 | window.api.setIgnoreMouseEvents(false) 20 | } 21 | 22 | const handleBodyMouseOver = (e: MouseEvent) => { 23 | if (e.target === document.body) { 24 | window.api.setIgnoreMouseEvents(true, { forward: true }) 25 | } 26 | } 27 | 28 | if (platform === 'win32' || platform === 'darwin') { 29 | el.current.addEventListener('mouseover', handleMouseOver) 30 | document.body?.addEventListener('mouseover', handleBodyMouseOver) 31 | } else { 32 | window.api.setIgnoreMouseEvents(false) 33 | } 34 | 35 | return () => { 36 | if (platform === 'win32' || platform === 'darwin') { 37 | el.current?.removeEventListener('mouseover', handleMouseOver) 38 | document.body?.removeEventListener('mouseover', handleBodyMouseOver) 39 | } else { 40 | window.api.setIgnoreMouseEvents(false) 41 | } 42 | } 43 | }, 44 | [platform] 45 | ) 46 | 47 | return { setIgnoreMouseEvents, platform } 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/src/assets/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ev-c-white: #ffffff; 3 | --ev-c-white-soft: #f8f8f8; 4 | --ev-c-white-mute: #f2f2f2; 5 | 6 | --ev-c-black: #1b1b1f; 7 | --ev-c-black-soft: #222222; 8 | --ev-c-black-mute: #282828; 9 | 10 | --ev-c-gray-1: #515c67; 11 | --ev-c-gray-2: #414853; 12 | --ev-c-gray-3: #32363f; 13 | 14 | --ev-c-text-1: rgba(255, 255, 245, 0.86); 15 | --ev-c-text-2: rgba(235, 235, 245, 0.6); 16 | --ev-c-text-3: rgba(235, 235, 245, 0.38); 17 | 18 | --ev-button-alt-border: transparent; 19 | --ev-button-alt-text: var(--ev-c-text-1); 20 | --ev-button-alt-bg: var(--ev-c-gray-3); 21 | --ev-button-alt-hover-border: transparent; 22 | --ev-button-alt-hover-text: var(--ev-c-text-1); 23 | --ev-button-alt-hover-bg: var(--ev-c-gray-2); 24 | } 25 | 26 | :root { 27 | --color-background: var(--ev-c-black); 28 | --color-background-soft: var(--ev-c-black-soft); 29 | --color-background-mute: var(--ev-c-black-mute); 30 | 31 | --color-text: var(--ev-c-text-1); 32 | } 33 | 34 | *, 35 | *::before, 36 | *::after { 37 | box-sizing: border-box; 38 | margin: 0; 39 | font-weight: normal; 40 | } 41 | 42 | ul { 43 | list-style: none; 44 | } 45 | 46 | body { 47 | min-height: 100vh; 48 | color: var(--color-text); 49 | background: var(--color-background); 50 | line-height: 1.6; 51 | font-family: 52 | Inter, 53 | -apple-system, 54 | BlinkMacSystemFont, 55 | 'Segoe UI', 56 | Roboto, 57 | Oxygen, 58 | Ubuntu, 59 | Cantarell, 60 | 'Fira Sans', 61 | 'Droid Sans', 62 | 'Helvetica Neue', 63 | sans-serif; 64 | text-rendering: optimizeLegibility; 65 | -webkit-font-smoothing: antialiased; 66 | -moz-osx-font-smoothing: grayscale; 67 | } 68 | -------------------------------------------------------------------------------- /src/renderer/src/layouts/Config/styles.module.scss: -------------------------------------------------------------------------------- 1 | .config { 2 | display: flex; 3 | height: 100%; 4 | 5 | .content { 6 | flex: 1; 7 | display: flex; 8 | overflow: hidden; 9 | } 10 | } 11 | 12 | .config_layout { 13 | @apply h-screen w-screen flex; 14 | } 15 | 16 | .sidebar { 17 | @apply w-[160px] bg-slate-800 text-white flex flex-col; 18 | 19 | .logo { 20 | @apply flex items-center gap-2.5 px-5 py-3 border-b border-slate-700; 21 | 22 | span { 23 | @apply text-base font-medium; 24 | } 25 | } 26 | 27 | .nav_links { 28 | @apply flex-1 py-4 space-y-2 px-3; 29 | } 30 | } 31 | 32 | .link { 33 | @apply flex items-center gap-2 px-3 py-2 rounded-md text-slate-300; 34 | 35 | span { 36 | @apply text-sm font-medium; 37 | } 38 | } 39 | 40 | .active { 41 | @apply flex items-center gap-3 px-4 py-3 rounded-lg bg-blue-600 text-white; 42 | 43 | span { 44 | @apply text-sm font-medium; 45 | } 46 | } 47 | 48 | .sync_button { 49 | @apply flex items-center justify-center gap-2 mx-4 mb-6 px-4 py-2 rounded-lg 50 | text-sm font-medium transition-colors; 51 | 52 | &.idle { 53 | @apply bg-blue-600 text-white hover:bg-blue-700; 54 | } 55 | 56 | &.syncing { 57 | @apply bg-yellow-600 text-white cursor-not-allowed; 58 | 59 | .spinning { 60 | animation: spin 1s linear infinite; 61 | } 62 | } 63 | 64 | &.completed { 65 | @apply bg-green-600 text-white; 66 | } 67 | 68 | &.error { 69 | @apply bg-red-600 text-white; 70 | } 71 | } 72 | 73 | .content { 74 | @apply flex-1 bg-slate-100 p-8 overflow-y-auto; 75 | } 76 | 77 | @keyframes spin { 78 | from { 79 | transform: rotate(0deg); 80 | } 81 | to { 82 | transform: rotate(360deg); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/renderer/src/pages/ContentList/contentList.scss: -------------------------------------------------------------------------------- 1 | .content-list { 2 | flex: 1; 3 | display: flex; 4 | height: 100%; 5 | position: relative; 6 | background: #fff; 7 | 8 | .list-container { 9 | width: 100%; 10 | height: 100%; 11 | overflow-y: auto; 12 | padding: 1rem; 13 | 14 | .list-header { 15 | margin-bottom: 1rem; 16 | display: flex; 17 | gap: 1rem; 18 | align-items: center; 19 | 20 | .search-box { 21 | flex: 1; 22 | position: relative; 23 | 24 | .search-icon { 25 | position: absolute; 26 | left: 0.75rem; 27 | top: 50%; 28 | transform: translateY(-50%); 29 | color: #9ca3af; 30 | } 31 | 32 | input { 33 | width: 100%; 34 | padding: 0.5rem 0.75rem 0.5rem 2.5rem; 35 | border: 1px solid #e5e7eb; 36 | border-radius: 0.375rem; 37 | outline: none; 38 | font-size: 0.875rem; 39 | 40 | &:focus { 41 | border-color: #2563eb; 42 | } 43 | 44 | &::placeholder { 45 | color: #9ca3af; 46 | } 47 | } 48 | } 49 | 50 | .add-content-btn { 51 | display: flex; 52 | align-items: center; 53 | gap: 0.5rem; 54 | padding: 0.5rem 1rem; 55 | border-radius: 0.375rem; 56 | background: #f3f4f6; 57 | color: #374151; 58 | font-size: 0.875rem; 59 | 60 | &:hover { 61 | background: #e5e7eb; 62 | } 63 | } 64 | } 65 | } 66 | 67 | .content-container { 68 | width: 100%; 69 | height: 100%; 70 | position: absolute; 71 | top: 0; 72 | left: 0; 73 | background: #fff; 74 | overflow-y: auto; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Content/content.scss: -------------------------------------------------------------------------------- 1 | @mixin common() { 2 | @apply outline-none bg-slate-50; 3 | } 4 | 5 | .content-page { 6 | padding: 1rem; 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | gap: 1rem; 11 | overflow-x: hidden; 12 | 13 | input[type='text'] { 14 | width: 100%; 15 | padding: 0.75rem; 16 | border: 1px solid #eee; 17 | border-radius: 0.375rem; 18 | outline: none; 19 | font-size: 1.125rem; 20 | 21 | &:focus { 22 | border-color: #2563eb; 23 | } 24 | } 25 | 26 | .options { 27 | display: flex; 28 | gap: 1rem; 29 | 30 | .select { 31 | display: flex; 32 | align-items: center; 33 | gap: 0.5rem; 34 | 35 | label { 36 | font-size: 0.875rem; 37 | color: #4b5563; 38 | } 39 | 40 | select { 41 | padding: 0.5rem 0.75rem; 42 | border: 1px solid #eee; 43 | border-radius: 0.375rem; 44 | outline: none; 45 | font-size: 0.875rem; 46 | 47 | &:focus { 48 | border-color: #2563eb; 49 | } 50 | } 51 | } 52 | } 53 | 54 | textarea { 55 | flex: 1; 56 | width: 100%; 57 | padding: 0.75rem; 58 | border: 1px solid #eee; 59 | border-radius: 0.375rem; 60 | outline: none; 61 | resize: none; 62 | font-size: 1rem; 63 | line-height: 1.5; 64 | min-height: 300px; 65 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 66 | 'Courier New', monospace; 67 | 68 | &:focus { 69 | border-color: #2563eb; 70 | } 71 | } 72 | } 73 | 74 | .back-button { 75 | margin: 1rem; 76 | padding: 0.5rem 1rem; 77 | background: #f3f4f6; 78 | border-radius: 0.375rem; 79 | font-size: 0.875rem; 80 | color: #374151; 81 | 82 | &:hover { 83 | background: #e5e7eb; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/renderer/src/assets/wavy-lines.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/createWindow.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, BrowserWindowConstructorOptions, shell } from 'electron' 2 | import { resolve } from 'path' 3 | import icon from '../../resources/icon.png?asset' 4 | import { is } from '@electron-toolkit/utils' 5 | import url from 'node:url' 6 | export interface OptionsType extends Partial { 7 | openDevTools?: boolean 8 | hash?: string 9 | initShow?: boolean 10 | } 11 | 12 | export function createWindow(options: OptionsType): BrowserWindow { 13 | const preloadPath = resolve(__dirname, '../preload/index.js') 14 | 15 | const win = new BrowserWindow( 16 | Object.assign( 17 | { 18 | width: 500, 19 | height: 350, 20 | center: true, 21 | show: false, 22 | frame: false, 23 | transparent: true, 24 | alwaysOnTop: false, 25 | autoHideMenuBar: true, 26 | ...(process.platform === 'linux' ? { icon } : {}), 27 | webPreferences: { 28 | preload: preloadPath, 29 | sandbox: false, 30 | webSecurity: true 31 | } 32 | }, 33 | options 34 | ) 35 | ) 36 | 37 | if (is.dev && options.openDevTools) { 38 | win.webContents.openDevTools({ mode: 'detach' }) 39 | } 40 | 41 | win.on('ready-to-show', () => { 42 | options.initShow && win.show() 43 | }) 44 | 45 | win.webContents.setWindowOpenHandler((details) => { 46 | shell.openExternal(details.url) 47 | return { action: 'deny' } 48 | }) 49 | 50 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 51 | win.loadURL(process.env['ELECTRON_RENDERER_URL'] + options.hash) 52 | } else { 53 | win.loadURL( 54 | url.format({ 55 | pathname: resolve(__dirname, '../renderer/index.html'), 56 | protocol: 'file', 57 | slashes: true, 58 | hash: options.hash?.substring(1) 59 | }) 60 | ) 61 | } 62 | 63 | return win 64 | } 65 | -------------------------------------------------------------------------------- /src/main/shortCut.ts: -------------------------------------------------------------------------------- 1 | import { app, dialog, globalShortcut, ipcMain, IpcMainInvokeEvent } from 'electron' 2 | import { config } from './db/query' 3 | import { getByNameWindow } from './windows' 4 | 5 | ipcMain.handle('shortCut', async (_event: IpcMainInvokeEvent, shortCut: string) => { 6 | if (!shortCut) { 7 | return true 8 | } 9 | return registerSearchShortCut(shortCut) 10 | }) 11 | 12 | function registerSearchShortCut(shortCut: string) { 13 | globalShortcut.unregisterAll() 14 | if (!shortCut || typeof shortCut !== 'string') { 15 | dialog.showErrorBox('温馨提示', '快捷键格式不正确') 16 | return false 17 | } 18 | 19 | try { 20 | if (globalShortcut.isRegistered(shortCut)) { 21 | dialog.showErrorBox('温馨提示', '快捷键注册失败,请检查该快捷键是否已被其他程序占用') 22 | return false 23 | } 24 | 25 | const win = getByNameWindow('search') 26 | const registered = globalShortcut.register(shortCut, () => { 27 | if (win.isVisible()) { 28 | win.hide() 29 | } else { 30 | win.show() 31 | win.webContents.send('focus-input') 32 | } 33 | }) 34 | 35 | if (!registered) { 36 | dialog.showErrorBox('温馨提示', '快捷键注册失败,请检查快捷键格式是否正确') 37 | return false 38 | } 39 | 40 | return true 41 | } catch (error) { 42 | console.error('Failed to register global shortcut:', error) 43 | dialog.showErrorBox('温馨提示', '快捷键注册失败,请检查快捷键格式是否正确') 44 | return false 45 | } 46 | } 47 | 48 | app.on('will-quit', () => { 49 | globalShortcut.unregisterAll() 50 | }) 51 | 52 | export const registerAppGlobShortcut = async () => { 53 | try { 54 | const configData = await config() 55 | if (configData && configData.shortCut) { 56 | registerSearchShortCut(configData.shortCut) 57 | } else { 58 | console.warn('No valid shortcut found in config, using default') 59 | registerSearchShortCut('Alt+Space') 60 | } 61 | } catch (error) { 62 | console.error('Failed to register global shortcut:', error) 63 | registerSearchShortCut('Alt+Space') 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/renderer/src/components/ToolItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Input, Button, Form } from 'antd' 3 | import { DeleteOutlined } from '@ant-design/icons' 4 | import { useFetcher } from 'react-router-dom' 5 | import './toolItem.scss' 6 | 7 | interface ToolItemProps { 8 | tool: ToolType 9 | onDelete: () => void 10 | } 11 | 12 | export const ToolItem: React.FC = ({ tool, onDelete }) => { 13 | const [form] = Form.useForm() 14 | const fetcher = useFetcher() 15 | const [editingField, setEditingField] = useState(null) 16 | 17 | const handleDoubleClick = (field: string) => { 18 | setEditingField(field) 19 | } 20 | 21 | const handleBlur = () => { 22 | if (editingField) { 23 | const values = form.getFieldsValue() 24 | fetcher.submit({ id: tool.id, ...values }, { method: 'PUT' }) 25 | setEditingField(null) 26 | } 27 | } 28 | 29 | return ( 30 | 31 | 32 | 33 | handleDoubleClick('name')} 36 | onBlur={handleBlur} 37 | /> 38 | 39 | 40 | handleDoubleClick('abbr')} 43 | onBlur={handleBlur} 44 | /> 45 | 46 | 47 | handleDoubleClick('url')} 50 | onBlur={handleBlur} 51 | /> 52 | 53 | 54 | } danger /> 55 | 56 | 57 | 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { electronApp, optimizer } from '@electron-toolkit/utils' 2 | import { app, BrowserWindow, ipcMain, session } from 'electron' 3 | import './windows' 4 | import './ipc' 5 | import './db' 6 | import { registerAppGlobShortcut } from './shortCut' 7 | import { getByNameWindow } from './windows' 8 | import path from 'node:path' 9 | import { initDB } from './db/query' 10 | import { initTable } from './db/tables' 11 | 12 | let mainWindow: BrowserWindow | null = null 13 | 14 | app.whenReady().then(() => { 15 | initDB() 16 | initTable() 17 | registerAppGlobShortcut() 18 | mainWindow = getByNameWindow('search') 19 | 20 | electronApp.setAppUserModelId('com.electron') 21 | 22 | app.on('browser-window-created', (_, window) => { 23 | optimizer.watchWindowShortcuts(window) 24 | }) 25 | 26 | ipcMain.on('ping', () => console.log('pong')) 27 | app.on('activate', function () { 28 | if (BrowserWindow.getAllWindows().length === 0) getByNameWindow('search') 29 | }) 30 | if (process.platform === 'darwin') { 31 | app.dock.setIcon(path.resolve(__dirname, '../resources/icon.png')) 32 | } 33 | if (mainWindow) { 34 | mainWindow.once('ready-to-show', () => { 35 | mainWindow?.show() 36 | mainWindow?.webContents.send('focus-input') 37 | }) 38 | mainWindow.on('restore', () => { 39 | mainWindow?.webContents.send('focus-input') 40 | }) 41 | mainWindow.on('focus', () => { 42 | mainWindow?.webContents.send('focus-input') 43 | }) 44 | mainWindow.on('closed', () => { 45 | mainWindow = null 46 | }) 47 | mainWindow.on('hide', () => { 48 | mainWindow?.webContents.send('window-hidden') 49 | }) 50 | mainWindow.on('blur', () => { 51 | if (mainWindow && !mainWindow.isDestroyed()) { 52 | mainWindow.hide() 53 | } 54 | }) 55 | } 56 | }) 57 | 58 | app.on('ready', async () => { 59 | await session.defaultSession.clearCache() 60 | }) 61 | 62 | app.on('window-all-closed', () => { 63 | if (process.platform !== 'darwin') { 64 | app.quit() 65 | } 66 | }) 67 | -------------------------------------------------------------------------------- /src/renderer/src/pages/ContentList/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLoaderData, Outlet, useLocation, useSubmit } from 'react-router-dom' 2 | import { ContentItem } from '@renderer/components/ContentItem' 3 | import { IconPlus, IconSearch } from '@tabler/icons-react' 4 | import { useState } from 'react' 5 | import './contentList.scss' 6 | 7 | export const ContentList = () => { 8 | const contents = useLoaderData() as ContentType[] 9 | const location = useLocation() 10 | const submit = useSubmit() 11 | const showingContent = location.pathname.includes('/content/') 12 | const [searchText, setSearchText] = useState('') 13 | 14 | const handleAddContent = () => { 15 | const categoryId = location.pathname.split('/').pop() || '0' 16 | submit({ category_id: categoryId }, { method: 'POST' }) 17 | } 18 | 19 | const filteredContents = contents.filter( 20 | (content) => 21 | content.title.toLowerCase().includes(searchText.toLowerCase()) || 22 | content.content.toLowerCase().includes(searchText.toLowerCase()) 23 | ) 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | setSearchText(e.target.value)} 36 | /> 37 | 38 | 39 | 40 | 新建片段 41 | 42 | 43 | {filteredContents.map((content) => ( 44 | 45 | ))} 46 | 47 | 48 | 49 | 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import { Form, useLoaderData, useSubmit, useNavigate } from 'react-router-dom' 2 | import './content.scss' 3 | 4 | export const Content = () => { 5 | const submit = useSubmit() 6 | const { content, categories } = useLoaderData() as { 7 | content: ContentType 8 | categories: CategoryType[] 9 | } 10 | const navigate = useNavigate() 11 | 12 | return ( 13 | 14 | navigate('..')} className="back-button"> 15 | 返回列表 16 | 17 | 18 | 19 | submit(e.target.form)} 25 | /> 26 | 27 | 28 | 29 | 分类: 30 | submit(e.target.form)} 34 | > 35 | 未分类 36 | {categories.map((category) => ( 37 | 38 | {category.name} 39 | 40 | ))} 41 | 42 | 43 | 44 | 45 | 类型: 46 | submit(e.target.form)}> 47 | 直接复制 48 | 查看 49 | 50 | 51 | 52 | 53 | submit(e.target.form)} 58 | /> 59 | 60 | 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /src/main/db/tables.ts: -------------------------------------------------------------------------------- 1 | import { insert, findOne } from './query' 2 | 3 | export async function initTable() { 4 | const createTableQueries = [ 5 | ` 6 | CREATE TABLE IF NOT EXISTS categories ( 7 | id INTEGER PRIMARY KEY AUTOINCREMENT, 8 | name TEXT NOT NULL, 9 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 10 | ) 11 | `, 12 | ` 13 | CREATE TABLE IF NOT EXISTS contents ( 14 | id INTEGER PRIMARY KEY AUTOINCREMENT, 15 | title TEXT NOT NULL, 16 | content TEXT NOT NULL, 17 | category_id INTEGER, 18 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 19 | type tinyint NOT NULL DEFAULT 1 20 | ) 21 | `, 22 | ` 23 | CREATE TABLE IF NOT EXISTS config ( 24 | id INTEGER PRIMARY KEY AUTOINCREMENT, 25 | content TEXT NOT NULL 26 | ) 27 | `, 28 | ` 29 | CREATE TABLE IF NOT EXISTS quick_view ( 30 | id INTEGER PRIMARY KEY AUTOINCREMENT, 31 | name TEXT NOT NULL, 32 | url TEXT NOT NULL, 33 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 34 | abbr varchar(100) DEFAULT NULL 35 | ) 36 | `, 37 | ` 38 | CREATE TABLE IF NOT EXISTS tools ( 39 | id INTEGER PRIMARY KEY AUTOINCREMENT, 40 | name TEXT NOT NULL, 41 | url TEXT NOT NULL, 42 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 43 | abbr varchar(100) DEFAULT NULL 44 | ) 45 | ` 46 | ] 47 | for (const query of createTableQueries) { 48 | try { 49 | await insert(query) 50 | } catch (error) { 51 | console.error('创建数据表失败:', error) 52 | } 53 | } 54 | 55 | // 检查config表是否有数据,如果没有则创建一条id=1的默认记录 56 | try { 57 | const configExists = await findOne('SELECT id FROM config WHERE id = 1') 58 | if (!configExists) { 59 | const defaultConfig = { 60 | shortCut: 'Alt+Space', 61 | databaseDirectory: '/' 62 | } 63 | await insert('INSERT INTO config (id, content) VALUES (1, ?)', [ 64 | JSON.stringify(defaultConfig) 65 | ]) 66 | console.log('已创建默认配置') 67 | } 68 | } catch (error) { 69 | console.error('检查或创建默认配置失败:', error) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/renderer/src/store/useStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { createJSONStorage, persist } from 'zustand/middleware' 3 | interface StateProps { 4 | config: ConfigDataType 5 | setConfig: (config: ConfigDataType) => void 6 | data: HomeSearchItem[] 7 | setData: (data: HomeSearchItem[]) => void 8 | search: string 9 | setSearch: (search: string) => void 10 | error: string 11 | setError: (message: string) => void 12 | id: number 13 | setId: (id: number) => void 14 | editCategoryId: number 15 | setEditCategoryId: (id: number) => void 16 | content: string 17 | setContent: (content: string) => void 18 | page: number 19 | setPage: (page: number) => void 20 | totalPage: number 21 | setTotalPage: (totalPage: number) => void 22 | platform: string 23 | setPlatform: (platform: string) => void 24 | param: string 25 | setParam: (param: string) => void 26 | } 27 | 28 | export const useStore = create( 29 | persist( 30 | (set) => ({ 31 | config: { 32 | shortCut: '', 33 | dbType: 'sqlite', 34 | mysqlHost: '', 35 | mysqlPort: '', 36 | mysqlDatabase: '', 37 | mysqlUsername: '', 38 | mysqlPassword: '', 39 | databaseDirectory: '' 40 | }, 41 | setConfig: (config) => set({ config }), 42 | data: [], 43 | setData: (data) => set({ data }), 44 | search: '', 45 | setSearch: (content) => set({ search: content }), 46 | content: '', 47 | setContent: (content) => set({ content: content }), 48 | error: '', 49 | setError: (message) => set({ error: message }), 50 | id: 0, 51 | setId: (id) => set({ id }), 52 | editCategoryId: 0, 53 | setEditCategoryId: (editCategoryId) => set({ editCategoryId }), 54 | page: 1, 55 | setPage: (page) => set({ page }), 56 | totalPage: 1, 57 | setTotalPage: (totalPage) => set({ totalPage }), 58 | platform: '', 59 | setPlatform: (platform) => set({ platform }), 60 | param: '', 61 | setParam: (param) => set({ param: param }) 62 | }), 63 | { 64 | name: 'rapidle-storage', 65 | storage: createJSONStorage(() => localStorage) 66 | } 67 | ) 68 | ) 69 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron' 2 | import { electronAPI } from '@electron-toolkit/preload' 3 | 4 | const api = { 5 | shortCut: (shortCut: string) => { 6 | return ipcRenderer.invoke('shortCut', shortCut) 7 | }, 8 | dbChange: (config: ConfigDataType) => { 9 | return ipcRenderer.invoke('dbChange', config) 10 | }, 11 | setIgnoreMouseEvents: (ignore: boolean, options?: { forward: boolean }) => { 12 | ipcRenderer.send('setIgnoreMouseEvents', ignore, options) 13 | }, 14 | openConfigWindow: () => { 15 | ipcRenderer.send('openConfigWindow') 16 | }, 17 | sql: (sql: string, type: SqlActionType, params = {}) => { 18 | return ipcRenderer.invoke('sql', sql, type, params) 19 | }, 20 | openWindow: (name: WindowNameType) => { 21 | ipcRenderer.send('openWindow', name) 22 | }, 23 | closeWindow: (name: WindowNameType) => { 24 | ipcRenderer.send('closeWindow', name) 25 | }, 26 | selectDatabaseDirectory: () => { 27 | return ipcRenderer.invoke('selectDatabaseDirectory') 28 | }, 29 | setDatabaseDirectory: (path: string) => { 30 | ipcRenderer.send('setDatabaseDirectory', path) 31 | }, 32 | initTable: () => { 33 | ipcRenderer.send('initTable') 34 | }, 35 | openExternal: (url: string) => { 36 | ipcRenderer.invoke('openExternal', url) 37 | }, 38 | getInstalledApps: (keyword: string) => { 39 | return ipcRenderer.invoke('getInstalledApps', keyword) 40 | }, 41 | getPlatform: () => { 42 | return ipcRenderer.invoke('getPlatform') 43 | }, 44 | launchApp: (exec: string) => { 45 | ipcRenderer.send('launchApp', exec) 46 | }, 47 | getProcessList: (keyword: string) => { 48 | return ipcRenderer.invoke('getProcessList', keyword) 49 | }, 50 | closeProcess: (processName: string) => { 51 | ipcRenderer.send('closeProcess', processName) 52 | }, 53 | restartApp: () => ipcRenderer.send('restart-app') 54 | } 55 | 56 | if (process.contextIsolated) { 57 | try { 58 | contextBridge.exposeInMainWorld('electron', electronAPI) 59 | contextBridge.exposeInMainWorld('api', api) 60 | } catch (error) { 61 | console.error(error) 62 | } 63 | } else { 64 | // @ts-ignore (define in dts) 65 | window.electron = electronAPI 66 | // @ts-ignore (define in dts) 67 | window.api = api 68 | } 69 | -------------------------------------------------------------------------------- /src/renderer/src/layouts/Config/index.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink, Outlet } from 'react-router-dom' 2 | import { MantineProvider } from '@mantine/core' 3 | import { ContextMenuProvider } from 'mantine-contextmenu' 4 | import { IconCode, IconSearch, IconTools, IconSettings } from '@tabler/icons-react' 5 | 6 | import '@mantine/core/styles.layer.css' 7 | import 'mantine-contextmenu/styles.layer.css' 8 | import styles from './styles.module.scss' 9 | 10 | export default function Config() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | Rapidle 19 | 20 | 21 | 22 | (isActive ? styles.active : styles.link)} 25 | > 26 | 27 | 片段管理 28 | 29 | 30 | (isActive ? styles.active : styles.link)} 33 | > 34 | 35 | 快速访问 36 | 37 | 38 | (isActive ? styles.active : styles.link)} 41 | > 42 | 43 | 工具设置 44 | 45 | 46 | (isActive ? styles.active : styles.link)} 49 | > 50 | 51 | 系统设置 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /src/renderer/src/components/CategoryItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { Delete } from '@icon-park/react' 2 | import { useContextMenu } from 'mantine-contextmenu' 3 | import { NavLink, useSubmit } from 'react-router-dom' 4 | import { useState, useRef, useEffect } from 'react' 5 | import styles from './styles.module.scss' 6 | 7 | interface Props { 8 | category: CategoryType 9 | } 10 | 11 | export const CategoryItem = ({ category }: Props) => { 12 | const submit = useSubmit() 13 | const { showContextMenu } = useContextMenu() 14 | const [isEditing, setIsEditing] = useState(false) 15 | const [editName, setEditName] = useState(category.name) 16 | const inputRef = useRef(null) 17 | 18 | useEffect(() => { 19 | if (isEditing && inputRef.current) { 20 | inputRef.current.focus() 21 | inputRef.current.select() 22 | } 23 | }, [isEditing]) 24 | 25 | const handleDoubleClick = (e: React.MouseEvent) => { 26 | e.preventDefault() 27 | setIsEditing(true) 28 | } 29 | 30 | const handleSubmit = () => { 31 | if (editName.trim() && editName !== category.name) { 32 | submit({ id: category.id, name: editName.trim() }, { method: 'PUT' }) 33 | } 34 | setIsEditing(false) 35 | } 36 | 37 | const handleKeyDown = (e: React.KeyboardEvent) => { 38 | if (e.key === 'Enter') { 39 | handleSubmit() 40 | } else if (e.key === 'Escape') { 41 | setEditName(category.name) 42 | setIsEditing(false) 43 | } 44 | } 45 | 46 | return ( 47 | { 50 | return [isActive ? styles.active : '', styles.link].join(' ') 51 | }} 52 | onContextMenu={showContextMenu( 53 | [ 54 | { 55 | key: 'remove', 56 | icon: , 57 | title: '删除分类', 58 | onClick: () => { 59 | if (confirm('确定要删除这个分类吗?该分类下的所有片段也将被删除。')) { 60 | submit({ id: category.id }, { method: 'DELETE' }) 61 | } 62 | } 63 | } 64 | ], 65 | { className: 'contextMenu' } 66 | )} 67 | onDoubleClick={handleDoubleClick} 68 | > 69 | {isEditing ? ( 70 | setEditName(e.target.value)} 75 | onBlur={handleSubmit} 76 | onKeyDown={handleKeyDown} 77 | /> 78 | ) : ( 79 | {category.name} 80 | )} 81 | 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /src/renderer/src/pages/Tools/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLoaderData, useFetcher } from 'react-router-dom' 2 | import { ToolItem } from '@renderer/components/ToolItem' 3 | import { IconPlus, IconHelp } from '@tabler/icons-react' 4 | import './tools.scss' 5 | import { useState } from 'react' 6 | 7 | export const Tools = () => { 8 | const tools = useLoaderData() as ToolType[] 9 | const fetcher = useFetcher() 10 | const [showTooltip, setShowTooltip] = useState(false) 11 | 12 | const addNewTool = async () => { 13 | await fetcher.submit({}, { method: 'POST' }) 14 | } 15 | 16 | const handleDelete = (id: number) => { 17 | fetcher.submit({ id }, { method: 'DELETE' }) 18 | } 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 工具设置 26 | 27 | setShowTooltip(true)} 31 | onMouseLeave={() => setShowTooltip(false)} 32 | /> 33 | {showTooltip && ( 34 | 46 | IP地理位置查询配置示例: 47 | 48 | 名称:IP地理位置 49 | 50 | 缩写:ip 51 | 52 | 链接:http://ip-api.com/json/{'{keyword}'} 53 | 54 | )} 55 | 56 | 57 | 58 | 59 | 添加新工具 60 | 61 | 62 | 63 | 64 | 65 | 66 | 名称 67 | 缩写 68 | 链接 69 | 操作 70 | 71 | 72 | 73 | {tools.map((tool) => ( 74 | handleDelete(tool.id)} /> 75 | ))} 76 | 77 | 78 | 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /src/renderer/src/pages/QuickView/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLoaderData, useFetcher } from 'react-router-dom' 2 | import './quickView.scss' 3 | import { QuickViewItem } from '@renderer/components/QuickViewItem' 4 | import { IconPlus, IconHelp } from '@tabler/icons-react' 5 | import { useState } from 'react' 6 | 7 | export const QuickView = () => { 8 | const searchs = useLoaderData() as QuickViewType[] 9 | const fetcher = useFetcher() 10 | const [showTooltip, setShowTooltip] = useState(false) 11 | 12 | const addNewRow = async () => { 13 | await fetcher.submit({}, { method: 'POST' }) 14 | } 15 | 16 | const handleDelete = (id: number) => { 17 | fetcher.submit({ id }, { method: 'DELETE' }) 18 | } 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 快速访问 26 | 27 | setShowTooltip(true)} 31 | onMouseLeave={() => setShowTooltip(false)} 32 | /> 33 | {showTooltip && ( 34 | 46 | Google搜索配置示例: 47 | 48 | 名称:Google 49 | 50 | 缩写:g 51 | 52 | 链接:https://www.google.com/search?q={'{keyword}'} 53 | 54 | )} 55 | 56 | 57 | 58 | 59 | 添加新行 60 | 61 | 62 | 63 | 64 | 65 | 66 | 名称 67 | 缩写 68 | 链接 69 | 操作 70 | 71 | 72 | 73 | {searchs.map((search) => ( 74 | handleDelete(search.id)} 78 | /> 79 | ))} 80 | 81 | 82 | 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /src/renderer/src/router/index.tsx: -------------------------------------------------------------------------------- 1 | import Config from '@renderer/layouts/Config' 2 | import Home from '@renderer/layouts/Home' 3 | import { Category } from '@renderer/pages/Category' 4 | import CategoryAction from '@renderer/pages/Category/CategoryAction' 5 | import CategoryLoader from '@renderer/pages/Category/CategoryLoader' 6 | import { Content } from '@renderer/pages/Content' 7 | import ContentAction from '@renderer/pages/Content/ContentAction' 8 | import ContentLoader from '@renderer/pages/Content/ContentLoader' 9 | import { ContentList } from '@renderer/pages/ContentList' 10 | import ContentListAction from '@renderer/pages/ContentList/ContentListAction' 11 | import ContentListLoader from '@renderer/pages/ContentList/ContentListLoader' 12 | import { QuickView } from '@renderer/pages/QuickView' 13 | import QuickViewAction from '@renderer/pages/QuickView/QuickViewAction' 14 | import QuickViewLoader from '@renderer/pages/QuickView/QuickViewLoader' 15 | import { Setting } from '@renderer/pages/Setting' 16 | import SettingAction from '@renderer/pages/Setting/SettingAction' 17 | import SettingLoader from '@renderer/pages/Setting/SettingLoader' 18 | import { Welcome } from '@renderer/pages/Welcome' 19 | import { createHashRouter } from 'react-router-dom' 20 | import { Tools } from '@renderer/pages/Tools' 21 | import ToolsLoader from '@renderer/pages/Tools/ToolsLoader' 22 | import ToolsAction from '@renderer/pages/Tools/ToolsAction' 23 | 24 | const router = createHashRouter([ 25 | { 26 | path: '/', 27 | element: 28 | }, 29 | { 30 | path: 'config', 31 | element: , 32 | children: [ 33 | { 34 | index: true, 35 | element: , 36 | loader: CategoryLoader, 37 | action: CategoryAction 38 | }, 39 | { 40 | path: 'setting', 41 | element: , 42 | loader: SettingLoader, 43 | action: SettingAction 44 | }, 45 | { 46 | path: 'quickView', 47 | element: , 48 | loader: QuickViewLoader, 49 | action: QuickViewAction 50 | }, 51 | { 52 | path: 'category', 53 | element: , 54 | loader: CategoryLoader, 55 | action: CategoryAction, 56 | children: [ 57 | { 58 | path: 'contentList/:cid?', 59 | element: , 60 | loader: ContentListLoader, 61 | action: ContentListAction, 62 | children: [ 63 | { 64 | index: true, 65 | element: 66 | }, 67 | { 68 | path: 'content/:id', 69 | element: , 70 | loader: ContentLoader, 71 | action: ContentAction 72 | } 73 | ] 74 | } 75 | ] 76 | }, 77 | { 78 | path: 'tools', 79 | element: , 80 | loader: ToolsLoader, 81 | action: ToolsAction 82 | } 83 | ] 84 | } 85 | ]) 86 | export default router 87 | -------------------------------------------------------------------------------- /src/main/db/query.ts: -------------------------------------------------------------------------------- 1 | import { Database } from 'better-sqlite3' 2 | import BetterSQLite3 from 'better-sqlite3' 3 | import { app } from 'electron' 4 | import fs from 'fs' 5 | import path from 'path' 6 | 7 | interface DatabaseWrapper { 8 | findAll(sql: string, params?: any): Promise | any[] 9 | findOne(sql: string, params?: any): Promise | any 10 | insert(sql: string, params?: any): Promise | number 11 | update(sql: string, params?: any): Promise | number 12 | del(sql: string, params?: any): Promise | number 13 | close(): Promise | void 14 | } 15 | 16 | class SQLiteWrapper implements DatabaseWrapper { 17 | private db: Database 18 | 19 | constructor() { 20 | const dbPath = path.join(app.getPath('userData'), 'rapidle.db') 21 | if (!fs.existsSync(dbPath)) { 22 | fs.writeFileSync(dbPath, '') 23 | } 24 | this.db = new BetterSQLite3(dbPath) 25 | } 26 | 27 | findAll(sql: string, params: any = {}): any[] { 28 | return this.db.prepare(sql).all(params) 29 | } 30 | 31 | findOne(sql: string, params: any = {}): any { 32 | return this.db.prepare(sql).get(params) 33 | } 34 | 35 | insert(sql: string, params: any = {}): number { 36 | return this.db.prepare(sql).run(params).lastInsertRowid as number 37 | } 38 | 39 | update(sql: string, params: any = {}): number { 40 | return this.db.prepare(sql).run(params).changes 41 | } 42 | 43 | del(sql: string, params: any = {}): number { 44 | return this.db.prepare(sql).run(params).changes 45 | } 46 | 47 | close(): void { 48 | this.db.close() 49 | } 50 | } 51 | 52 | let dbWrapper: DatabaseWrapper 53 | 54 | export const initDB = () => { 55 | dbWrapper = new SQLiteWrapper() 56 | } 57 | 58 | export const findAll = (sql: string, params?: any): Promise | any[] => { 59 | return dbWrapper.findAll(sql, params) 60 | } 61 | 62 | export const findOne = (sql: string, params?: any): Promise | any => { 63 | return dbWrapper.findOne(sql, params) 64 | } 65 | 66 | export const insert = (sql: string, params?: any): Promise | number => { 67 | return dbWrapper.insert(sql, params) 68 | } 69 | 70 | export const update = (sql: string, params?: any): Promise | number => { 71 | return dbWrapper.update(sql, params) 72 | } 73 | 74 | export const updateConfig = async (sql: string, params?: any): Promise => { 75 | return dbWrapper.update(sql, params) 76 | } 77 | 78 | export const del = (sql: string, params?: any): Promise | number => { 79 | return dbWrapper.del(sql, params) 80 | } 81 | 82 | export const config = async (): Promise => { 83 | const result = await findOne('SELECT content FROM config WHERE id = 1') 84 | if (result && result.content) { 85 | try { 86 | return JSON.parse(result.content) 87 | } catch (error) { 88 | console.error('解析配置失败:', error) 89 | return getDefaultConfig() 90 | } 91 | } 92 | return getDefaultConfig() 93 | } 94 | 95 | function getDefaultConfig(): ConfigDataType { 96 | return { 97 | shortCut: 'Alt+Space', 98 | databaseDirectory: '/' 99 | } 100 | } 101 | 102 | export const closeDB = (): Promise | void => { 103 | return dbWrapper.close() 104 | } 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rapidle", 3 | "version": "1.0.0", 4 | "description": "一个基于 Electron、React 和 TypeScript 的效率工具应用", 5 | "author": { 6 | "name": "Junexus", 7 | "email": "shadowdragon4399@gmail.com" 8 | }, 9 | "main": "./out/main/index.js", 10 | "homepage": "https://byte.ink", 11 | "repository": "https://github.com/shadowDragons/rapidle", 12 | "scripts": { 13 | "format": "prettier --write .", 14 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", 15 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", 16 | "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", 17 | "typecheck": "npm run typecheck:node && npm run typecheck:web", 18 | "start": "electron-vite preview", 19 | "dev": "electron-vite dev", 20 | "build": "npm run typecheck && electron-vite build", 21 | "postinstall": "electron-builder install-app-deps", 22 | "build:unpack": "npm run build && electron-builder --dir", 23 | "build:win": "cross-env NODE_ENV=production electron-builder --win --config", 24 | "build:mac": "electron-vite build && electron-builder --mac", 25 | "build:linux": "electron-vite build && electron-builder --linux" 26 | }, 27 | "dependencies": { 28 | "@electron-toolkit/preload": "^3.0.0", 29 | "@electron-toolkit/utils": "^3.0.0", 30 | "@emotion/is-prop-valid": "^1.2.2", 31 | "@icon-park/react": "^1.4.2", 32 | "@remix-run/router": "^1.15.3", 33 | "@tabler/icons-react": "^3.24.0", 34 | "@types/better-sqlite3": "^7.6.10", 35 | "@types/mockjs": "^1.0.10", 36 | "antd": "^5.16.2", 37 | "better-sqlite3": "^9.5.0", 38 | "classnames": "^2.5.1", 39 | "clsx": "^2.1.1", 40 | "dayjs": "^1.11.11", 41 | "dotenv": "^16.4.5", 42 | "electron-updater": "^6.2.1", 43 | "localforage": "^1.10.0", 44 | "lucide-react": "^0.446.0", 45 | "mantine-contextmenu": "^7.9.1", 46 | "match-sorter": "^6.3.4", 47 | "mockjs": "^1.1.0", 48 | "mysql": "^2.18.1", 49 | "mysql2": "^3.10.2", 50 | "react-markdown": "^9.0.1", 51 | "react-router-dom": "^6.22.3", 52 | "remark-gfm": "^4.0.0", 53 | "sort-by": "^1.2.0", 54 | "styled-components": "^6.1.8", 55 | "uuid": "^10.0.0", 56 | "zustand": "^4.5.2" 57 | }, 58 | "devDependencies": { 59 | "@electron-toolkit/eslint-config-prettier": "^2.0.0", 60 | "@electron-toolkit/eslint-config-ts": "^1.0.1", 61 | "@electron-toolkit/tsconfig": "^1.0.1", 62 | "@types/node": "^18.19.9", 63 | "@types/react": "^18.2.48", 64 | "@types/react-dom": "^18.2.18", 65 | "@types/uuid": "^10.0.0", 66 | "@vitejs/plugin-react": "^4.2.1", 67 | "autoprefixer": "^10.4.18", 68 | "cross-env": "^7.0.3", 69 | "dotenv-cli": "^7.4.1", 70 | "electron": "^28.2.0", 71 | "electron-builder": "^24.9.1", 72 | "electron-rebuild": "^3.2.9", 73 | "electron-vite": "^2.0.0", 74 | "eslint": "^8.56.0", 75 | "eslint-plugin-react": "^7.33.2", 76 | "postcss": "^8.4.35", 77 | "prettier": "^3.2.4", 78 | "react": "^18.2.0", 79 | "react-dom": "^18.2.0", 80 | "sass": "^1.72.0", 81 | "tailwindcss": "^3.4.1", 82 | "typescript": "^5.3.3", 83 | "vite": "^5.0.12" 84 | }, 85 | "build": { 86 | "win": { 87 | "target": [ 88 | "portable" 89 | ], 90 | "icon": "resources/icon.png" 91 | }, 92 | "linux": { 93 | "target": [ 94 | "AppImage", 95 | "deb" 96 | ], 97 | "category": "开发工具", 98 | "icon": "resources/icon.png" 99 | }, 100 | "mac": { 101 | "icon": "resources/icon.png", 102 | "extendInfo": { 103 | "NSMicrophoneUsageDescription": "我们需要访问您的麦克风以支持音频功能。", 104 | "NSCameraUsageDescription": "我们需要访问您的摄像头以支持视频功能。" 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/renderer/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | body { 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | overflow: hidden; 8 | background-image: url('./wavy-lines.svg'); 9 | background-size: cover; 10 | user-select: none; 11 | } 12 | 13 | code { 14 | font-weight: 600; 15 | padding: 3px 5px; 16 | border-radius: 2px; 17 | background-color: var(--color-background-mute); 18 | font-family: 19 | ui-monospace, 20 | SFMono-Regular, 21 | SF Mono, 22 | Menlo, 23 | Consolas, 24 | Liberation Mono, 25 | monospace; 26 | font-size: 85%; 27 | } 28 | 29 | #root { 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | flex-direction: column; 34 | margin-bottom: 80px; 35 | } 36 | 37 | .logo { 38 | margin-bottom: 20px; 39 | -webkit-user-drag: none; 40 | height: 128px; 41 | width: 128px; 42 | will-change: filter; 43 | transition: filter 300ms; 44 | } 45 | 46 | .logo:hover { 47 | filter: drop-shadow(0 0 1.2em #6988e6aa); 48 | } 49 | 50 | .creator { 51 | font-size: 14px; 52 | line-height: 16px; 53 | color: var(--ev-c-text-2); 54 | font-weight: 600; 55 | margin-bottom: 10px; 56 | } 57 | 58 | .text { 59 | font-size: 28px; 60 | color: var(--ev-c-text-1); 61 | font-weight: 700; 62 | line-height: 32px; 63 | text-align: center; 64 | margin: 0 10px; 65 | padding: 16px 0; 66 | } 67 | 68 | .tip { 69 | font-size: 16px; 70 | line-height: 24px; 71 | color: var(--ev-c-text-2); 72 | font-weight: 600; 73 | } 74 | 75 | .react { 76 | background: -webkit-linear-gradient(315deg, #087ea4 55%, #7c93ee); 77 | background-clip: text; 78 | -webkit-background-clip: text; 79 | -webkit-text-fill-color: transparent; 80 | font-weight: 700; 81 | } 82 | 83 | .ts { 84 | background: -webkit-linear-gradient(315deg, #3178c6 45%, #f0dc4e); 85 | background-clip: text; 86 | -webkit-background-clip: text; 87 | -webkit-text-fill-color: transparent; 88 | font-weight: 700; 89 | } 90 | 91 | .actions { 92 | display: flex; 93 | padding-top: 32px; 94 | margin: -6px; 95 | flex-wrap: wrap; 96 | justify-content: flex-start; 97 | } 98 | 99 | .action { 100 | flex-shrink: 0; 101 | padding: 6px; 102 | } 103 | 104 | .action a { 105 | cursor: pointer; 106 | text-decoration: none; 107 | display: inline-block; 108 | border: 1px solid transparent; 109 | text-align: center; 110 | font-weight: 600; 111 | white-space: nowrap; 112 | border-radius: 20px; 113 | padding: 0 20px; 114 | line-height: 38px; 115 | font-size: 14px; 116 | border-color: var(--ev-button-alt-border); 117 | color: var(--ev-button-alt-text); 118 | background-color: var(--ev-button-alt-bg); 119 | } 120 | 121 | .action a:hover { 122 | border-color: var(--ev-button-alt-hover-border); 123 | color: var(--ev-button-alt-hover-text); 124 | background-color: var(--ev-button-alt-hover-bg); 125 | } 126 | 127 | .versions { 128 | position: absolute; 129 | bottom: 30px; 130 | margin: 0 auto; 131 | padding: 15px 0; 132 | font-family: 'Menlo', 'Lucida Console', monospace; 133 | display: inline-flex; 134 | overflow: hidden; 135 | align-items: center; 136 | border-radius: 22px; 137 | background-color: #202127; 138 | backdrop-filter: blur(24px); 139 | } 140 | 141 | .versions li { 142 | display: block; 143 | float: left; 144 | border-right: 1px solid var(--ev-c-gray-1); 145 | padding: 0 20px; 146 | font-size: 14px; 147 | line-height: 14px; 148 | opacity: 0.8; 149 | &:last-child { 150 | border: none; 151 | } 152 | } 153 | 154 | @media (max-width: 720px) { 155 | .text { 156 | font-size: 20px; 157 | } 158 | } 159 | 160 | @media (max-width: 620px) { 161 | .versions { 162 | display: none; 163 | } 164 | } 165 | 166 | @media (max-width: 350px) { 167 | .tip, 168 | .actions { 169 | display: none; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useSelect.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from '@renderer/store/useStore' 2 | import { useCallback, useEffect } from 'react' 3 | import useSearch from './useSearch' 4 | 5 | export default () => { 6 | const { data, setData, setSearch, setId, id, setContent, page, totalPage, param } = useStore( 7 | (state) => state 8 | ) 9 | 10 | const { handleSearch, search } = useSearch() 11 | const handleKeyEvent = useCallback( 12 | (e: KeyboardEvent) => { 13 | switch (e.code) { 14 | case 'ArrowUp': 15 | { 16 | const index = data.findIndex((item) => item.id == id) 17 | const item = data.find((item) => item.id == id) 18 | if (!data[index - 1]) { 19 | if (page != 1) { 20 | const nextPage = page - 1 21 | handleSearch(search, nextPage) 22 | } 23 | } 24 | 25 | setId(data[index - 1]?.id || data[data.length - 1].id) 26 | if (item?.isTool) { 27 | setSearch('@' + item.title + ' ') 28 | } 29 | } 30 | break 31 | case 'ArrowDown': { 32 | const index = data.findIndex((item) => item.id == id) 33 | const item = data.find((item) => item.id == id) 34 | if (!data[index + 1]) { 35 | if (index + 1 == 6) { 36 | if (page == totalPage) { 37 | handleSearch(search, 1) 38 | } else { 39 | const nextPage = page + 1 40 | handleSearch(search, nextPage) 41 | } 42 | } else if (page != 1) { 43 | handleSearch(search, 1) 44 | } 45 | } 46 | setId(data[index + 1]?.id || data[0].id) 47 | if (item?.isTool) { 48 | setSearch('@' + item.title + ' ') 49 | } 50 | break 51 | } 52 | case 'Enter': { 53 | selectItem(id) 54 | break 55 | } 56 | case 'Escape': { 57 | window.api.closeWindow('search') 58 | } 59 | } 60 | }, 61 | [data, id, param] 62 | ) 63 | 64 | async function selectItem(id: number) { 65 | const item = data.find((item) => item.id == id) 66 | const selectContent = item?.content 67 | if (item) { 68 | if (item.isApp) { 69 | window.api.launchApp(item.exec) 70 | window.api.closeWindow('search') 71 | } else if (item.isProcess) { 72 | window.api.closeProcess(item.title) 73 | window.api.closeWindow('search') 74 | } else if (item.isQuickView) { 75 | const url = item.content.replace('{keyword}', param) 76 | await window.api.openExternal(url) 77 | } else if (item.isTool) { 78 | try { 79 | const encodedParam = encodeURIComponent(param) 80 | const url = item.content.replace('{keyword}', encodedParam) 81 | 82 | let response 83 | try { 84 | response = await fetch(url) 85 | } catch (fetchError) { 86 | if (fetchError instanceof Error) { 87 | throw new Error('获取数据失败:' + fetchError.message) 88 | } else { 89 | throw new Error('获取数据失败:发生未知错误') 90 | } 91 | } 92 | 93 | if (!response.ok) { 94 | throw new Error(`HTTP错误!状态码:${response.status}`) 95 | } 96 | 97 | let jsonData 98 | try { 99 | jsonData = await response.json() 100 | } catch (jsonError) { 101 | console.error('解析JSON时出错:', jsonError) 102 | if (jsonError instanceof Error) { 103 | throw new Error('解析JSON失败:' + jsonError.message) 104 | } else { 105 | throw new Error('解析JSON失败:发生未知错误') 106 | } 107 | } 108 | 109 | const formattedJsonString = JSON.stringify(jsonData, null, 2) 110 | 111 | setContent(formattedJsonString) 112 | } catch (error) { 113 | console.error('获取或处理数据时出错:', error) 114 | if (error instanceof Error) { 115 | setContent('访问API或处理数据时出错:' + error.message) 116 | } else { 117 | setContent('访问API或处理数据时出错:发生未知错误') 118 | } 119 | } 120 | } else if (item.type == 2) { 121 | if (selectContent) { 122 | setContent(selectContent) 123 | } 124 | } else { 125 | if (selectContent) { 126 | await navigator.clipboard.writeText(selectContent) 127 | } 128 | window.api.closeWindow('search') 129 | } 130 | } 131 | 132 | setData([]) 133 | setSearch('') 134 | } 135 | 136 | useEffect(() => { 137 | document.addEventListener('keydown', handleKeyEvent) 138 | return () => { 139 | document.removeEventListener('keydown', handleKeyEvent) 140 | } 141 | }, [handleKeyEvent]) 142 | 143 | useEffect(() => { 144 | setId(data[0]?.id || 0) 145 | }, [data]) 146 | return { data, id, selectItem } 147 | } 148 | -------------------------------------------------------------------------------- /src/renderer/src/assets/electron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/useSearch.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from '@renderer/store/useStore' 2 | 3 | export default () => { 4 | const { setData, setContent, setPage, setTotalPage, setParam } = useStore((s) => s) 5 | const { search, setSearch } = useStore() 6 | 7 | const handleAppSearch = async (keyword: string) => { 8 | const data = await window.api.getInstalledApps(keyword.replace(/^a:/, '')) 9 | setData(data as HomeSearchItem[]) 10 | setTotalPage(1) 11 | } 12 | 13 | const handleProcessSearch = async (keyword: string) => { 14 | const data = await window.api.getProcessList(keyword.replace(/^k:/, '')) 15 | setData(data as HomeSearchItem[]) 16 | setTotalPage(1) 17 | } 18 | 19 | const handleQuickViewSearch = async (keyword: string, page: number) => { 20 | const limit = (page - 1) * 6 21 | const offset = 6 22 | const keywords = keyword.replace(/^q:/, '').split(' ') 23 | const sql = `SELECT id,name as title,url as content FROM quick_view WHERE abbr like ? or name like ? limit ?,?` 24 | const params = [`%${keywords[0]}%`, `%${keywords[0]}%`, limit, offset] 25 | const data: HomeSearchItem[] = await window.api.sql(sql, 'findAll', params) 26 | 27 | const dataWithQuickView = data.map((item) => ({ ...item, isQuickView: true })) 28 | setData(dataWithQuickView) 29 | setParam(keywords.slice(1).join(' ')) 30 | setContent('') 31 | setPage(page) 32 | 33 | const sql_count = `SELECT count(*) as cnt FROM quick_view a WHERE (abbr LIKE ? or name LIKE ?)` 34 | const data_count: HomeSearchItem = await window.api.sql(sql_count, 'findOne', params) 35 | setTotalPage(Math.ceil(data_count.cnt / 6)) 36 | } 37 | 38 | const handleToolSearch = async (keyword: string, page: number) => { 39 | const limit = (page - 1) * 6 40 | const offset = 6 41 | const keywords = keyword.replace(/^t:/, '').split(' ') 42 | const sql = `SELECT id,name as title,url as content FROM tools WHERE abbr like ? or name like ? limit ?,?` 43 | const params = [`%${keywords[0]}%`, `%${keywords[0]}%`, limit, offset] 44 | const data: HomeSearchItem[] = await window.api.sql(sql, 'findAll', params) 45 | 46 | const dataWithTool = data.map((item) => ({ ...item, isTool: true })) 47 | setData(dataWithTool) 48 | setParam(keywords.slice(1).join(' ')) 49 | setContent('') 50 | setPage(page) 51 | 52 | const sql_count = `SELECT count(*) as cnt FROM tools a WHERE (abbr LIKE ? or name LIKE ?)` 53 | const data_count: HomeSearchItem = await window.api.sql(sql_count, 'findOne', params) 54 | setTotalPage(Math.ceil(data_count.cnt / 6)) 55 | } 56 | 57 | const handleContentSearch = async (keyword: string, page: number) => { 58 | const limit = (page - 1) * 6 59 | const offset = 6 60 | 61 | const toolsSql = `SELECT id, name as title, url as content, '' as type 62 | FROM tools WHERE abbr LIKE ? or name LIKE ?` 63 | const quickViewSql = `SELECT id, name as title, url as content, '' as type 64 | FROM quick_view WHERE abbr LIKE ? or name LIKE ?` 65 | const snippetSql = `SELECT a.id, a.title, a.content, a.type as type, b.name as categorie_name 66 | FROM contents a 67 | LEFT JOIN categories b on a.category_id = b.id 68 | WHERE a.title LIKE ? or b.name LIKE ?` 69 | 70 | const params = [`%${keyword}%`, `%${keyword}%`] 71 | 72 | const toolsData: HomeSearchItem[] = await window.api 73 | .sql(toolsSql, 'findAll', params) 74 | .then((data: any) => data.map((item) => ({ ...item, id: `tool_${item.id}` }))) 75 | 76 | const quickViewData: HomeSearchItem[] = await window.api 77 | .sql(quickViewSql, 'findAll', params) 78 | .then((data: any) => data.map((item) => ({ ...item, id: `quick_${item.id}` }))) 79 | 80 | const snippetData: HomeSearchItem[] = await window.api 81 | .sql(snippetSql, 'findAll', params) 82 | .then((data: any) => data.map((item) => ({ ...item, id: `snippet_${item.id}` }))) 83 | 84 | const appsData: HomeSearchItem[] = await window.api 85 | .getInstalledApps(keyword) 86 | .then((data: any) => data.map((item) => ({ ...item, id: `app_${item.id}` }))) 87 | 88 | const allData = [ 89 | ...toolsData.map((item) => ({ ...item, isTool: true })), 90 | ...quickViewData.map((item) => ({ ...item, isQuickView: true })), 91 | ...appsData.map((item) => ({ ...item, isApp: true })), 92 | ...snippetData.map((item) => ({ ...item, isSnippet: true })) 93 | ] 94 | 95 | const totalItems = allData.length 96 | const pagedData = allData.slice(limit, limit + offset) 97 | 98 | setData(pagedData) 99 | setContent('') 100 | setPage(page) 101 | setTotalPage(Math.ceil(totalItems / 6)) 102 | } 103 | 104 | const handleSearch = async (keyword: string, page: number) => { 105 | setSearch(keyword) 106 | const keywords = keyword.split(' ') 107 | const tmpParam = keywords.slice(1).join(' ') 108 | setParam(tmpParam) 109 | 110 | if (keyword.includes(' ')) { 111 | return 112 | } 113 | 114 | if (search != keyword) { 115 | page = 1 116 | } 117 | 118 | if (keyword.startsWith('a:')) { 119 | await handleAppSearch(keyword) 120 | } else if (keyword.startsWith('k:')) { 121 | await handleProcessSearch(keyword) 122 | } else if (keyword.startsWith('q:')) { 123 | await handleQuickViewSearch(keyword, page) 124 | } else if (keyword.startsWith('t:')) { 125 | await handleToolSearch(keyword, page) 126 | } else if (keyword.length > 0) { 127 | await handleContentSearch(keyword, page) 128 | } else { 129 | setData([]) 130 | setTotalPage(1) 131 | setContent('') 132 | setPage(1) 133 | return 134 | } 135 | } 136 | 137 | return { search, handleSearch } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/platformApps.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import { promisify } from 'util' 3 | import * as os from 'os' 4 | 5 | const execAsync = promisify(exec) 6 | 7 | async function getMacApps(keyword: string): Promise { 8 | try { 9 | const { stdout } = await execAsync('ls /Applications') 10 | 11 | const searchApps = stdout.split('\n').filter((app) => app.endsWith('.app')) 12 | 13 | let idCounter = 0 // 初始化计数器 14 | 15 | const apps = searchApps.map((app) => ({ 16 | id: ++idCounter, 17 | title: app.replace('.app', ''), 18 | exec: `open "/Applications/${app}"`, 19 | isApp: true 20 | })) 21 | 22 | if (!keyword || keyword.trim() === '') { 23 | return apps.slice(0, 6) 24 | } 25 | 26 | const lowerKeyword = keyword.toLowerCase() 27 | 28 | const matchedApps = apps 29 | .filter((app) => app.title.toLowerCase().includes(lowerKeyword)) 30 | .slice(0, 6) 31 | 32 | return matchedApps 33 | } catch (error) { 34 | console.error('Error getting Mac apps:', error) 35 | return [] 36 | } 37 | } 38 | async function getWindowsApps(keyword: string): Promise { 39 | try { 40 | const { stdout } = await execAsync( 41 | 'powershell -command "& {$OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-StartApps | ConvertTo-Json}"' 42 | ) 43 | let idCounter = 0 44 | 45 | const apps: AppInfo[] = JSON.parse(stdout).map((app) => ({ 46 | id: ++idCounter, 47 | title: app.Name, 48 | exec: `powershell -command "Start-Process '${app.AppID}'"`, 49 | isApp: true 50 | })) 51 | 52 | if (!keyword || keyword.trim() === '') { 53 | return apps.slice(0, 6) 54 | } 55 | 56 | const lowerKeyword = keyword.toLowerCase() 57 | 58 | const matchedApps = apps 59 | .filter((app) => app.title.toLowerCase().includes(lowerKeyword)) 60 | .slice(0, 6) 61 | 62 | return matchedApps 63 | } catch (error) { 64 | console.error('Error getting Windows apps:', error) 65 | return [] 66 | } 67 | } 68 | async function getLinuxApps(keyword: string): Promise { 69 | try { 70 | const { stdout } = await execAsync('ls /usr/share/applications/*.desktop') 71 | const desktopFiles = stdout.split('\n').filter(Boolean) 72 | let idCounter = 0 // 初始化计数器 73 | const apps: AppInfo[] = await Promise.all( 74 | desktopFiles.map(async (file) => { 75 | const { stdout: nameStdout } = await execAsync(`grep -m 1 "^Name=" "${file}"`) 76 | const { stdout: execStdout } = await execAsync(`grep -m 1 "^Exec=" "${file}"`) 77 | 78 | return { 79 | id: ++idCounter, 80 | title: nameStdout.replace('Name=', '').trim(), 81 | exec: execStdout.replace('Exec=', '').trim(), 82 | isApp: true 83 | } 84 | }) 85 | ) 86 | 87 | const validApps = apps.filter((app) => app.title && app.exec).slice(0, 6) 88 | 89 | if (!keyword || keyword.trim() === '') { 90 | return validApps.slice(0, 6) 91 | } 92 | 93 | const lowerKeyword = keyword.toLowerCase() 94 | 95 | const matchedApps = validApps.filter((app) => app.title.toLowerCase().includes(lowerKeyword)) 96 | return matchedApps 97 | } catch (error) { 98 | return [] 99 | } 100 | } 101 | export async function getInstalledApps(keyword: string): Promise { 102 | const platform = os.platform() 103 | switch (platform) { 104 | case 'darwin': 105 | return getMacApps(keyword) 106 | case 'win32': 107 | return getWindowsApps(keyword) 108 | case 'linux': 109 | return getLinuxApps(keyword) 110 | default: 111 | console.warn(`Platform ${platform} is not supported for app listing.`) 112 | return [] 113 | } 114 | } 115 | 116 | export function getPlatform(): string { 117 | return os.platform() 118 | } 119 | 120 | export async function launchApp(exec: string): Promise { 121 | try { 122 | await execAsync(exec) 123 | } catch (error) { 124 | console.error('Error launching app:', error) 125 | throw error 126 | } 127 | } 128 | 129 | export async function getProcessList(keyword: string) { 130 | return new Promise((resolve, reject) => { 131 | let command 132 | let parser: (line: string) => { title: string; id: string; isProcess: boolean } 133 | switch (process.platform) { 134 | case 'win32': 135 | command = 'tasklist /fo csv /nh' 136 | parser = (line) => { 137 | const [title, id] = line.split(',').map((item) => item.replace(/"/g, '')) 138 | return { title, id, isProcess: true } 139 | } 140 | break 141 | case 'darwin': 142 | command = 'ps -axco pid,comm' 143 | parser = (line) => { 144 | const [id, title] = line.trim().split(/\s+/) 145 | return { title, id, isProcess: true } 146 | } 147 | break 148 | case 'linux': 149 | command = 'ps -eo pid,comm --no-headers' 150 | parser = (line) => { 151 | const [id, title] = line.trim().split(/\s+/) 152 | return { title, id, isProcess: true } 153 | } 154 | break 155 | default: 156 | reject('Unsupported operating system') 157 | return 158 | } 159 | 160 | exec(command, (error, stdout, stderr) => { 161 | if (error) { 162 | reject(`Error: ${error.message}`) 163 | return 164 | } 165 | if (stderr) { 166 | reject(`Error: ${stderr}`) 167 | return 168 | } 169 | 170 | const processes = stdout 171 | .split('\n') 172 | .filter((line) => line.trim() !== '') 173 | .map(parser) 174 | .filter((process) => process.title.toLowerCase().includes(keyword.toLowerCase())) 175 | 176 | resolve(processes) 177 | }) 178 | }) 179 | } 180 | 181 | export async function closeProcess(processName: string) { 182 | return new Promise((resolve, reject) => { 183 | let command 184 | switch (process.platform) { 185 | case 'win32': 186 | command = `taskkill /F /IM "${processName}"` 187 | break 188 | case 'darwin': 189 | case 'linux': 190 | command = `pkill "${processName}"` 191 | break 192 | default: 193 | reject('Unsupported operating system') 194 | return 195 | } 196 | 197 | exec(command, (error) => { 198 | if (error) { 199 | reject(`Error closing ${processName}: ${error.message}`) 200 | return 201 | } 202 | resolve(`${processName} closed successfully`) 203 | }) 204 | }) 205 | } 206 | --------------------------------------------------------------------------------
{children}