├── 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 | ![rapidle-demo.gif](https://85b83fbf.cloudflare-imgbed-3ws.pages.dev/file/1737688882415_rapidle-demo.gif) 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 | 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 | 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 |
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 |
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 | 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 | 17 |
18 | 19 | submit(e.target.form)} 25 | /> 26 | 27 |
28 |
29 | 30 | 42 |
43 | 44 |
45 | 46 | 50 |
51 |
52 | 53 |