├── env.d.ts
├── public
├── logo.ico
├── logo.png
├── config.ts
└── configModel.ts
├── src
├── assets
│ ├── logo.png
│ ├── css
│ │ ├── global-dark.css
│ │ └── global-light.css
│ ├── logo.svg
│ ├── main.css
│ └── base.css
├── components
│ ├── ControlCenter
│ │ ├── HomeView.vue
│ │ ├── AboutView.vue
│ │ ├── NotesView.vue
│ │ ├── SettingsView.vue
│ │ └── PromptsView.vue
│ └── HelloWorld.vue
├── views
│ ├── PromptView.vue
│ ├── HomeView.vue
│ └── ControlCenterView.vue
├── stores
│ └── counter.ts
├── main.ts
├── App.vue
├── request
│ └── network.ts
├── renderer.d.ts
├── router
│ └── index.ts
├── components.d.ts
└── auto-imports.d.ts
├── tsconfig.config.json
├── tsconfig.json
├── index.html
├── .gitignore
├── .eslintrc.cjs
├── LICENSE
├── electron
├── preload.js
├── update
│ ├── helperUpdater.ts
│ └── autoUpdater.ts
├── prompt
│ └── prompt.ts
├── main.ts
└── menu.ts
├── vite.config.ts
├── README.md
└── package.json
/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
--------------------------------------------------------------------------------
/public/logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karenina-na/Claude-Desktop/HEAD/public/logo.ico
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karenina-na/Claude-Desktop/HEAD/public/logo.png
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karenina-na/Claude-Desktop/HEAD/src/assets/logo.png
--------------------------------------------------------------------------------
/src/components/ControlCenter/HomeView.vue:
--------------------------------------------------------------------------------
1 |
2 | Home
3 |
4 |
5 |
8 |
9 |
--------------------------------------------------------------------------------
/src/components/ControlCenter/AboutView.vue:
--------------------------------------------------------------------------------
1 |
2 | About
3 |
4 |
5 |
8 |
9 |
--------------------------------------------------------------------------------
/src/components/ControlCenter/NotesView.vue:
--------------------------------------------------------------------------------
1 |
2 | Notes
3 |
4 |
5 |
8 |
9 |
--------------------------------------------------------------------------------
/src/views/PromptView.vue:
--------------------------------------------------------------------------------
1 |
2 | This is a prompt view
3 |
4 |
5 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/views/HomeView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
--------------------------------------------------------------------------------
/tsconfig.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.json",
3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "types": ["node"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/assets/css/global-dark.css:
--------------------------------------------------------------------------------
1 | /*暗黑色调*/
2 | html.dark {
3 | transition: all 0.2s;
4 | background-color: #1d1e1f;
5 | color: #fff;
6 | --el-transition-duration: 0.2s;
7 | --el-transition-duration-fast: 0.2s;
8 | --el-transition-all: 0.2s;
9 | }
--------------------------------------------------------------------------------
/src/assets/css/global-light.css:
--------------------------------------------------------------------------------
1 | /*明亮色调*/
2 | html.light {
3 | transition: all 0.2s;
4 | background-color: #fff;
5 | color: #000;
6 | --el-transition-duration: 0.2s;
7 | --el-transition-duration-fast: 0.2s;
8 | --el-transition-all: 0.2s;
9 | }
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.json",
3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
4 | "compilerOptions": {
5 | "baseUrl": ".",
6 | "paths": {
7 | "@/*": ["./src/*"]
8 | }
9 | },
10 |
11 | "references": [
12 | {
13 | "path": "./tsconfig.config.json"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/src/stores/counter.ts:
--------------------------------------------------------------------------------
1 | import { ref, computed } from 'vue'
2 | import { defineStore } from 'pinia'
3 |
4 | export const useCounterStore = defineStore('counter', () => {
5 | const count = ref(0)
6 | const doubleCount = computed(() => count.value * 2)
7 | function increment() {
8 | count.value++
9 | }
10 |
11 | return { count, doubleCount, increment }
12 | })
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | .DS_Store
12 | dist
13 | dist-electron/*
14 | dist-client/*
15 | dist-ssr
16 | coverage
17 | *.local
18 |
19 | /cypress/videos/
20 | /cypress/screenshots/
21 |
22 | # Editor directories and files
23 | .vscode/*
24 | .idea
25 | *.suo
26 | *.ntvs*
27 | *.njsproj
28 | *.sln
29 | *.sw?
30 |
31 | # config
32 | config/*
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | require('@rushstack/eslint-patch/modern-module-resolution')
3 |
4 | module.exports = {
5 | root: true,
6 | 'extends': [
7 | 'plugin:vue/vue3-essential',
8 | 'eslint:recommended',
9 | '@vue/eslint-config-typescript'
10 | ],
11 | parserOptions: {
12 | ecmaVersion: 'latest'
13 | },
14 | "globals": {
15 | "Atomics": "readonly",
16 | "SharedArrayBuffer": "readonly",
17 | "process": true
18 | },
19 | }
20 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import { createPinia } from 'pinia'
3 | import 'element-plus/dist/index.css' //element-plus css
4 | import 'element-plus/theme-chalk/dark/css-vars.css' //element-plus dark theme
5 | // import './assets/css/global-light.css'
6 | // import './assets/css/global-dark.css'
7 | import router from './router'
8 | import axios from "axios";
9 | import App from './App.vue'
10 |
11 | const app = createApp(App)
12 |
13 | app.use(createPinia())
14 | app.use(router)
15 | app.config.globalProperties.$axios = axios
16 |
17 | app.mount('#app')
18 |
19 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
35 |
--------------------------------------------------------------------------------
/src/assets/main.css:
--------------------------------------------------------------------------------
1 | @import "./base.css";
2 |
3 | #app {
4 | max-width: 1280px;
5 | margin: 0 auto;
6 | padding: 2rem;
7 |
8 | font-weight: normal;
9 | }
10 |
11 | a,
12 | .green {
13 | text-decoration: none;
14 | color: hsla(160, 100%, 37%, 1);
15 | transition: 0.4s;
16 | }
17 |
18 | @media (hover: hover) {
19 | a:hover {
20 | background-color: hsla(160, 100%, 37%, 0.2);
21 | }
22 | }
23 |
24 | @media (min-width: 1024px) {
25 | body {
26 | display: flex;
27 | place-items: center;
28 | }
29 |
30 | #app {
31 | display: grid;
32 | grid-template-columns: 1fr 1fr;
33 | padding: 0 2rem;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/request/network.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const service = axios.create({
4 | baseURL: (process.env.VITE_NODE_MODE === 'production' ? process.env.VITE_THEMISE_URL + "" : "/api") + "/v1",
5 | timeout: 5000
6 | });
7 |
8 | // Request interceptors
9 | service.interceptors.request.use((config) => {
10 | const headers = new Headers();
11 | headers.append("Content-Type", "application/json");
12 | return config;
13 | }, error => {
14 | return Promise.reject(error);
15 | });
16 |
17 | // Response interceptors
18 | service.interceptors.response.use(response => {
19 | return response.data;
20 | }, error => {
21 | return Promise.reject(error);
22 | });
23 |
24 | export default service;
--------------------------------------------------------------------------------
/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
{{ msg }}
10 |
11 | You’ve successfully created a project with
12 | Vite +
13 | Vue 3. What's next?
14 |
15 |
16 |
17 |
18 |
41 |
--------------------------------------------------------------------------------
/src/renderer.d.ts:
--------------------------------------------------------------------------------
1 | // if you need to add some types for electron, you can add here
2 | // for electron ipcRenderer
3 | export interface IElectronAPI {
4 | quit: () => void,
5 | getConfigPath: () => Promise,
6 | openConfig: () => void,
7 | getUpdateInfo: () => Promise,
8 | setUpdateInfo: (arg: any) => Promise,
9 | resetUpdateInfo: () => Promise,
10 | getConfig: () => Promise,
11 | updateConfig: (config: any) => Promise,
12 | resetConfig: () => Promise,
13 | getPromptURL: () => Promise,
14 | openPrompt() : Promise,
15 | getPrompt: () => Promise,
16 | setPrompt: (prompt: any) => Promise,
17 | resetPrompt: () => Promise,
18 | syncPrompt: () => Promise,
19 | }
20 |
21 | declare global {
22 | interface Window {
23 | electronAPI: IElectronAPI
24 | }
25 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Wei Zixiang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/electron/preload.js:
--------------------------------------------------------------------------------
1 | // load node.js modules in the renderer process
2 | window.addEventListener('DOMContentLoaded', () => {
3 | // console.log('DOMContentLoaded')
4 |
5 | const { contextBridge, ipcRenderer } = require('electron')
6 |
7 | contextBridge.exposeInMainWorld('electronAPI', {
8 | getUpdateInfo: () => ipcRenderer.invoke('getUpdateInfo'),
9 | setUpdateInfo: (updateInfo) => ipcRenderer.invoke('setUpdateInfo', updateInfo),
10 | resetUpdateInfo: () => ipcRenderer.invoke('resetUpdateInfo'),
11 | quit: () => ipcRenderer.send('quit'),
12 | getConfigPath: () => ipcRenderer.invoke('getConfigPath'),
13 | getConfig: () => ipcRenderer.invoke('getConfig'),
14 | openConfig: () => ipcRenderer.send('openConfig'),
15 | updateConfig: (config) => ipcRenderer.invoke('updateConfig', config),
16 | resetConfig: () => ipcRenderer.invoke('resetConfig'),
17 | getPromptURL: () => ipcRenderer.invoke('getPromptURL'),
18 | openPrompt: () => ipcRenderer.send('openPrompt'),
19 | getPrompt: () => ipcRenderer.invoke('getPrompt'),
20 | setPrompt: (prompt) => ipcRenderer.invoke('setPrompt', prompt),
21 | resetPrompt: () => ipcRenderer.invoke('resetPrompt'),
22 | syncPrompt: () => ipcRenderer.invoke('syncPrompt'),
23 | })
24 | })
--------------------------------------------------------------------------------
/electron/update/helperUpdater.ts:
--------------------------------------------------------------------------------
1 | // help for auto update
2 |
3 | import { join } from 'path'
4 | import fs from 'fs'
5 | import { app } from 'electron'
6 | const dataPath = join(join(app.getPath('home'), '.claude'), 'updateInfo.json')
7 |
8 | function getLocalData(key?:any) {
9 | if (!fs.existsSync(dataPath)) {
10 | fs.writeFileSync(dataPath, JSON.stringify({}), { encoding: 'utf-8' })
11 | }
12 | let data = fs.readFileSync(dataPath, { encoding: 'utf-8' })
13 | let json = JSON.parse(data)
14 | return key ? json[key] : json
15 | }
16 |
17 | function setLocalData(key?:any, value?:any,) {
18 | let args = [...arguments]
19 | let data = fs.readFileSync(dataPath, { encoding: 'utf-8' })
20 | let json = JSON.parse(data)
21 | if (args.length === 0 || args[0] === null) {
22 | json = {}
23 | } else if (args.length === 1 && typeof key === 'object' && key) {
24 | json = {
25 | ...json,
26 | ...args[0],
27 | }
28 | } else {
29 | json[key] = value
30 | }
31 | fs.writeFileSync(dataPath, JSON.stringify(json), { encoding: 'utf-8' })
32 | }
33 |
34 | async function sleep(ms:any) {
35 | return new Promise((resolve:any) => {
36 | const timer = setTimeout(() => {
37 | resolve()
38 | clearTimeout(timer)
39 | }, ms)
40 | })
41 | }
42 |
43 | export {
44 | getLocalData,
45 | setLocalData,
46 | sleep,
47 | dataPath
48 | }
49 |
--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import {createRouter, createWebHashHistory} from 'vue-router'
2 |
3 | const router = createRouter({
4 | history: createWebHashHistory(import.meta.env.BASE_URL),
5 | routes: [{
6 | path: '/',
7 | name: 'home',
8 | component: () => import('@/views/HomeView.vue')
9 | }, {
10 | path: '/controlCenter',
11 | name: 'controlCenter',
12 | component: () => import('@/views/ControlCenterView.vue'),
13 | children: [{
14 | path: '/controlCenter',
15 | name: 'controlCenterHome',
16 | component: () => import('@/components/ControlCenter/HomeView.vue')
17 | },{
18 | path: '/controlCenter/settings',
19 | name: 'settings',
20 | component: () => import('@/components/ControlCenter/SettingsView.vue')
21 | },{
22 | path: '/controlCenter/prompts',
23 | name: 'prompts',
24 | component: () => import('@/components/ControlCenter/PromptsView.vue')
25 | },{
26 | path: '/controlCenter/notes',
27 | name: 'notes',
28 | component: () => import('@/components/ControlCenter/NotesView.vue')
29 | },{
30 | path: '/controlCenter/about',
31 | name: 'about',
32 | component: () => import('@/components/ControlCenter/AboutView.vue')
33 | }]
34 | }, {
35 | path: '/prompt',
36 | name: 'prompt',
37 | component: () => import('@/views/PromptView.vue')
38 | }]
39 | })
40 |
41 | export default router
42 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 |
3 | import { defineConfig } from 'vite'
4 | import vue from '@vitejs/plugin-vue'
5 | import electron from 'vite-plugin-electron'
6 | import Inspect from 'vite-plugin-inspect'
7 |
8 | import Icons from 'unplugin-icons/vite'
9 | import IconsResolver from 'unplugin-icons/resolver'
10 |
11 | import AutoImport from 'unplugin-auto-import/vite'
12 | import Components from 'unplugin-vue-components/vite'
13 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
14 |
15 | const pathSrc = path.resolve(__dirname, 'src')
16 |
17 | // https://vitejs.dev/config/
18 | export default defineConfig({
19 | base: "./",
20 | resolve: {
21 | alias: {
22 | '@': pathSrc
23 | }
24 | },
25 | plugins: [
26 | vue(),
27 | electron({
28 | entry: 'electron/main.ts',
29 | },
30 | ),
31 | AutoImport({
32 | imports: ['vue'],
33 | resolvers: [
34 | ElementPlusResolver(),
35 | IconsResolver({
36 | prefix: 'Icon',
37 | }),
38 | ],
39 | dts: path.resolve(pathSrc, 'auto-imports.d.ts'),
40 | }),
41 | Components({
42 | resolvers: [
43 | IconsResolver({
44 | enabledCollections: ['ep'],
45 | }),
46 | ElementPlusResolver(),
47 | ],
48 | dts: path.resolve(pathSrc, 'components.d.ts'),
49 | }),
50 | Icons({
51 | autoInstall: true,
52 | }),
53 | Inspect(),
54 | ],
55 | })
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
Claude
4 |
Claude Desktop App (Windows)
5 |
6 |
7 | ---
8 |
9 | **Claude to Desktop is an Electron-based desktop application for Claude2.**
10 |
11 | ✨ **It is an unofficial project, for personal learning and research purposes.**
12 |
13 | ⚗️ Claude2 is a powerful cloud-based note-taking application, and this project aims to bring its functionality to the desktop environment.
14 |
15 | ***
16 |
17 | ## 📦 To Use
18 |
19 | To clone and run this repository you'll need [Git](https://git-scm.com) and [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. From your command line:
20 |
21 | ```bash
22 | # Clone this repository
23 | git clone https://github.com/Karenina-na/Claude-Desktop.git
24 | # Go into the repository
25 | cd Claude-Desktop
26 | # Install dependencies
27 | npm install
28 | # Run the app
29 | npm run dev
30 | ```
31 |
32 | ## 📢 Announcement
33 |
34 | I am a novice programmer, brimming with passion for coding, constantly engaged in learning and advancing. This project is the fruit of my learning journey, albeit possibly containing some imperfections. However, I shall persistently refine it and strive to elevate its excellence.
35 |
36 | I wholeheartedly welcome everyone's suggestions, opinions, and critiques concerning this project. Should you have any queries or ideas, feel free to engage in a meaningful exchange with me. Together, we shall progress, learn in unison, and mutually inspire one another!"
37 |
38 | ## 🤝 contribute
39 |
40 | 1. Contribute to this endeavor, `Fork` the present undertaking.
41 | 2. Establish your distinctive branch of characteristics. (`git checkout -b feature/AmazingFeature`)
42 | 3. Submit your modifications forthwith. (`git commit -m 'Add some AmazingFeature'`)
43 | 4. Propagate your branch to the remote repository with due diligence. (`git push origin feature/AmazingFeature`)
44 | 5. Submit a formal pull request for consideration.
45 |
46 | ## License
47 |
48 | [MIT LICENSE](LICENSE)
49 |
50 |
51 | ## 📞 Contact Information
52 |
53 | Should you have any questions or concerns regarding the project, please feel free to contact me via the following methods:
54 |
55 | - Email: weizixiang0@outlook.com
56 |
--------------------------------------------------------------------------------
/src/assets/base.css:
--------------------------------------------------------------------------------
1 | /* color palette from */
2 | :root {
3 | --vt-c-white: #ffffff;
4 | --vt-c-white-soft: #f8f8f8;
5 | --vt-c-white-mute: #f2f2f2;
6 |
7 | --vt-c-black: #181818;
8 | --vt-c-black-soft: #222222;
9 | --vt-c-black-mute: #282828;
10 |
11 | --vt-c-indigo: #2c3e50;
12 |
13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
17 |
18 | --vt-c-text-light-1: var(--vt-c-indigo);
19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
20 | --vt-c-text-dark-1: var(--vt-c-white);
21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
22 | }
23 |
24 | /* semantic color variables for this project */
25 | :root {
26 | --color-background: var(--vt-c-white);
27 | --color-background-soft: var(--vt-c-white-soft);
28 | --color-background-mute: var(--vt-c-white-mute);
29 |
30 | --color-border: var(--vt-c-divider-light-2);
31 | --color-border-hover: var(--vt-c-divider-light-1);
32 |
33 | --color-heading: var(--vt-c-text-light-1);
34 | --color-text: var(--vt-c-text-light-1);
35 |
36 | --section-gap: 160px;
37 | }
38 |
39 | @media (prefers-color-scheme: dark) {
40 | :root {
41 | --color-background: var(--vt-c-black);
42 | --color-background-soft: var(--vt-c-black-soft);
43 | --color-background-mute: var(--vt-c-black-mute);
44 |
45 | --color-border: var(--vt-c-divider-dark-2);
46 | --color-border-hover: var(--vt-c-divider-dark-1);
47 |
48 | --color-heading: var(--vt-c-text-dark-1);
49 | --color-text: var(--vt-c-text-dark-2);
50 | }
51 | }
52 |
53 | *,
54 | *::before,
55 | *::after {
56 | box-sizing: border-box;
57 | margin: 0;
58 | position: relative;
59 | font-weight: normal;
60 | }
61 |
62 | body {
63 | min-height: 100vh;
64 | color: var(--color-text);
65 | background: var(--color-background);
66 | transition: color 0.5s, background-color 0.5s;
67 | line-height: 1.6;
68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
69 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
70 | font-size: 15px;
71 | text-rendering: optimizeLegibility;
72 | -webkit-font-smoothing: antialiased;
73 | -moz-osx-font-smoothing: grayscale;
74 | }
75 |
--------------------------------------------------------------------------------
/public/config.ts:
--------------------------------------------------------------------------------
1 | // create config file
2 | import path from "path";
3 | import { app} from "electron";
4 |
5 | import fs from "fs";
6 | import { dialog } from "electron";
7 | import configModel from "./configModel";
8 | import logger from "electron-log";
9 |
10 |
11 | // create and load config file
12 | function ConfigFactory(){
13 | let config: configModel;
14 |
15 | // config dir
16 | const configDir = path.join(app.getPath('home'), '.claude');
17 | // config file
18 | const configFile = path.join(configDir, 'config.json');
19 |
20 | // error dialog
21 | const error = (message:string) => {
22 | dialog.showErrorBox('Error', message)
23 | logger.error(message)
24 | }
25 |
26 | try{
27 | fs.statSync(configDir)
28 | // config dir exists
29 | try{
30 | fs.statSync(configFile)
31 | // config file exists
32 | config = new configModel()
33 | Object.assign(config, JSON.parse(fs.readFileSync(configFile).toString()))
34 | }catch(err){
35 | // config file not exists
36 | config = new configModel()
37 | fs.writeFileSync(configFile, JSON.stringify(config, null, 1))
38 | }
39 | }catch(err){
40 | // config dir not exists
41 | try{
42 | fs.mkdirSync(configDir)
43 | // mkdir success
44 | config = new configModel()
45 | fs.writeFileSync(configFile, JSON.stringify(config, null, 1))
46 | }catch(err){
47 |
48 | // mkdir failed
49 | error(err.message)
50 | }
51 | }
52 |
53 | return config;
54 | }
55 |
56 | // load and update config file
57 | function ConfigUpdate(config: configModel){
58 | // config dir
59 | const configDir = path.join(app.getPath('home'), '.claude');
60 | // config file
61 | const configFile = path.join(configDir, 'config.json');
62 |
63 | // error dialog
64 | const error = (message:string) => {
65 | dialog.showErrorBox('Error', message)
66 | logger.error(message)
67 | }
68 |
69 | try{
70 | fs.statSync(configDir)
71 | // config dir exists
72 | try{
73 | fs.statSync(configFile)
74 | // config file exists
75 | fs.writeFileSync(configFile, JSON.stringify(config, null, 1))
76 | }catch(err){
77 | // config file not exists
78 | error(err.message)
79 | }
80 | }catch(err){
81 | // config dir not exists
82 | error(err.message)
83 | }
84 | }
85 |
86 | export {ConfigFactory, ConfigUpdate}
--------------------------------------------------------------------------------
/src/components.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* prettier-ignore */
3 | // @ts-nocheck
4 | // Generated by unplugin-vue-components
5 | // Read more: https://github.com/vuejs/core/pull/3399
6 | export {}
7 |
8 | declare module 'vue' {
9 | export interface GlobalComponents {
10 | AboutView: typeof import('./components/ControlCenter/AboutView.vue')['default']
11 | ElAside: typeof import('element-plus/es')['ElAside']
12 | ElButton: typeof import('element-plus/es')['ElButton']
13 | ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
14 | ElContainer: typeof import('element-plus/es')['ElContainer']
15 | ElIcon: typeof import('element-plus/es')['ElIcon']
16 | ElInput: typeof import('element-plus/es')['ElInput']
17 | ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
18 | ElMain: typeof import('element-plus/es')['ElMain']
19 | ElPagination: typeof import('element-plus/es')['ElPagination']
20 | ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
21 | ElRadio: typeof import('element-plus/es')['ElRadio']
22 | ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
23 | ElRate: typeof import('element-plus/es')['ElRate']
24 | ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
25 | ElSwitch: typeof import('element-plus/es')['ElSwitch']
26 | ElTable: typeof import('element-plus/es')['ElTable']
27 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
28 | ElTabPane: typeof import('element-plus/es')['ElTabPane']
29 | ElTabs: typeof import('element-plus/es')['ElTabs']
30 | ElTooltip: typeof import('element-plus/es')['ElTooltip']
31 | HelloWorld: typeof import('./components/HelloWorld.vue')['default']
32 | HomeView: typeof import('./components/ControlCenter/HomeView.vue')['default']
33 | IEpCheck: typeof import('~icons/ep/check')['default']
34 | IEpClose: typeof import('~icons/ep/close')['default']
35 | IEpDArrowLeft: typeof import('~icons/ep/d-arrow-left')['default']
36 | IEpDArrowRight: typeof import('~icons/ep/d-arrow-right')['default']
37 | IEpEdit: typeof import('~icons/ep/edit')['default']
38 | IEpMagicStick: typeof import('~icons/ep/magic-stick')['default']
39 | IEpSetting: typeof import('~icons/ep/setting')['default']
40 | IEpWarning: typeof import('~icons/ep/warning')['default']
41 | NotesView: typeof import('./components/ControlCenter/NotesView.vue')['default']
42 | PromptsView: typeof import('./components/ControlCenter/PromptsView.vue')['default']
43 | RouterLink: typeof import('vue-router')['RouterLink']
44 | RouterView: typeof import('vue-router')['RouterView']
45 | SettingsView: typeof import('./components/ControlCenter/SettingsView.vue')['default']
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "claude-desktop",
3 | "version": "1.0.0",
4 | "private": true,
5 | "author": {
6 | "name": "Karenina-na",
7 | "email": "weizixiang0@outlook.com"
8 | },
9 | "description": "An Claude desktop application built with Vite + Vue 3.x + Electron 13.x",
10 | "main": "dist-electron/main.js",
11 | "scripts": {
12 | "dev": "vite",
13 | "build": "vue-tsc --noEmit && vite build && electron-builder",
14 | "preview": "vite preview --port 4173",
15 | "build-only": "vite build",
16 | "type-check": "vue-tsc --noEmit",
17 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
18 | },
19 | "dependencies": {
20 | "@element-plus/icons-vue": "^2.1.0",
21 | "@vueuse/core": "^10.3.0",
22 | "axios": "^1.4.0",
23 | "electron-log": "^4.4.8",
24 | "electron-updater": "^6.1.1",
25 | "element-plus": "^2.3.8",
26 | "perfect-scrollbar": "^1.5.5",
27 | "pinia": "^2.0.21",
28 | "vite-plugin-inspect": "^0.7.35",
29 | "vue": "^3.2.38",
30 | "vue-router": "^4.1.5"
31 | },
32 | "devDependencies": {
33 | "@iconify-json/ep": "^1.1.11",
34 | "@rushstack/eslint-patch": "^1.1.4",
35 | "@types/node": "^20.4.2",
36 | "@vitejs/plugin-vue": "^3.0.3",
37 | "@vue/eslint-config-typescript": "^11.0.0",
38 | "@vue/tsconfig": "^0.4.0",
39 | "electron": "^25.3.1",
40 | "electron-builder": "^24.4.0",
41 | "eslint": "^8.22.0",
42 | "eslint-plugin-vue": "^9.3.0",
43 | "npm-run-all": "^4.1.5",
44 | "typescript": "~5.1.6",
45 | "unplugin-auto-import": "^0.16.6",
46 | "unplugin-icons": "^0.16.5",
47 | "unplugin-vue-components": "^0.25.1",
48 | "vite": "^3.0.9",
49 | "vite-plugin-electron": "^0.12.0",
50 | "vue-tsc": "^1.8.5"
51 | },
52 | "build": {
53 | "appId": "Karenina-na.Claude",
54 | "artifactName": "${productName}-setup-${version}.${ext}",
55 | "productName": "Claude-Desktop",
56 | "copyright": "Copyright © 2023 Karenina-na",
57 | "directories": {
58 | "output": "dist-client"
59 | },
60 | "publish": [
61 | {
62 | "provider": "github",
63 | "owner": "Karenina-na",
64 | "repo": "Claude-Desktop",
65 | "releaseType": "release",
66 | "protocol": "https"
67 | }
68 | ],
69 | "win": {
70 | "icon": "./public/logo.ico",
71 | "target": [
72 | {
73 | "target": "nsis",
74 | "arch": [
75 | "x64"
76 | ]
77 | }
78 | ]
79 | },
80 | "nsis": {
81 | "oneClick": "false",
82 | "perMachine": "true",
83 | "allowToChangeInstallationDirectory": "true",
84 | "installerIcon": "./public/logo.ico",
85 | "uninstallerIcon": "./public/logo.ico",
86 | "installerHeaderIcon": "./public/logo.ico"
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/public/configModel.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import {app} from "electron";
3 | import fs from "fs";
4 |
5 | class Config{
6 | private _theme: string;
7 | private _stay_on_top: boolean;
8 | private _main_width: number;
9 | private _main_height: number;
10 | private _tray: boolean;
11 | private _tray_width: number;
12 | private _tray_height: number;
13 | private _ua_tray: string;
14 | private _prompt_path: string;
15 |
16 | constructor(){
17 | this._theme = "system";
18 | this._stay_on_top = false;
19 | this._main_width = 1080;
20 | this._main_height = 768;
21 | this._tray = true;
22 | this._tray_width = 400;
23 | this._tray_height = 600;
24 | this._ua_tray = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1";
25 | this._prompt_path = path.join(path.join(app.getPath('home'), '.claude'), 'prompt');
26 |
27 | // create prompt dir
28 | try{
29 | fs.statSync(path.join(app.getPath('home'), '.claude', 'prompt'))
30 | }catch(err){
31 | fs.mkdirSync(path.join(app.getPath('home'), '.claude', 'prompt'))
32 | }
33 | }
34 |
35 | get theme(): string {
36 | return this._theme;
37 | }
38 |
39 | set theme(value: string) {
40 | this._theme = value;
41 | }
42 |
43 | get stay_on_top(): boolean {
44 | return this._stay_on_top;
45 | }
46 |
47 | set stay_on_top(value: boolean) {
48 | this._stay_on_top = value;
49 | }
50 |
51 | get main_width(): number {
52 | return this._main_width;
53 | }
54 |
55 | set main_width(value: number) {
56 | this._main_width = value;
57 | }
58 |
59 | get main_height(): number {
60 | return this._main_height;
61 | }
62 |
63 | set main_height(value: number) {
64 | this._main_height = value;
65 | }
66 |
67 | get tray(): boolean {
68 | return this._tray;
69 | }
70 |
71 | set tray(value: boolean) {
72 | this._tray = value;
73 | }
74 |
75 | get tray_width(): number {
76 | return this._tray_width;
77 | }
78 |
79 | set tray_width(value: number) {
80 | this._tray_width = value;
81 | }
82 |
83 | get tray_height(): number {
84 | return this._tray_height;
85 | }
86 |
87 | set tray_height(value: number) {
88 | this._tray_height = value;
89 | }
90 |
91 | get ua_tray(): string {
92 | return this._ua_tray;
93 | }
94 |
95 | set ua_tray(value: string) {
96 | this._ua_tray = value;
97 | }
98 |
99 | get prompt_path(): string {
100 | return this._prompt_path;
101 | }
102 |
103 | set prompt_path(value: string) {
104 | this._prompt_path = value;
105 | }
106 |
107 | }
108 |
109 | export default Config;
--------------------------------------------------------------------------------
/src/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* prettier-ignore */
3 | // @ts-nocheck
4 | // noinspection JSUnusedGlobalSymbols
5 | // Generated by unplugin-auto-import
6 | export {}
7 | declare global {
8 | const EffectScope: typeof import('vue')['EffectScope']
9 | const ElNotification: typeof import('element-plus/es')['ElNotification']
10 | const computed: typeof import('vue')['computed']
11 | const createApp: typeof import('vue')['createApp']
12 | const customRef: typeof import('vue')['customRef']
13 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
14 | const defineComponent: typeof import('vue')['defineComponent']
15 | const effectScope: typeof import('vue')['effectScope']
16 | const getCurrentInstance: typeof import('vue')['getCurrentInstance']
17 | const getCurrentScope: typeof import('vue')['getCurrentScope']
18 | const h: typeof import('vue')['h']
19 | const inject: typeof import('vue')['inject']
20 | const isProxy: typeof import('vue')['isProxy']
21 | const isReactive: typeof import('vue')['isReactive']
22 | const isReadonly: typeof import('vue')['isReadonly']
23 | const isRef: typeof import('vue')['isRef']
24 | const markRaw: typeof import('vue')['markRaw']
25 | const nextTick: typeof import('vue')['nextTick']
26 | const onActivated: typeof import('vue')['onActivated']
27 | const onBeforeMount: typeof import('vue')['onBeforeMount']
28 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
29 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
30 | const onDeactivated: typeof import('vue')['onDeactivated']
31 | const onErrorCaptured: typeof import('vue')['onErrorCaptured']
32 | const onMounted: typeof import('vue')['onMounted']
33 | const onRenderTracked: typeof import('vue')['onRenderTracked']
34 | const onRenderTriggered: typeof import('vue')['onRenderTriggered']
35 | const onScopeDispose: typeof import('vue')['onScopeDispose']
36 | const onServerPrefetch: typeof import('vue')['onServerPrefetch']
37 | const onUnmounted: typeof import('vue')['onUnmounted']
38 | const onUpdated: typeof import('vue')['onUpdated']
39 | const provide: typeof import('vue')['provide']
40 | const reactive: typeof import('vue')['reactive']
41 | const readonly: typeof import('vue')['readonly']
42 | const ref: typeof import('vue')['ref']
43 | const resolveComponent: typeof import('vue')['resolveComponent']
44 | const shallowReactive: typeof import('vue')['shallowReactive']
45 | const shallowReadonly: typeof import('vue')['shallowReadonly']
46 | const shallowRef: typeof import('vue')['shallowRef']
47 | const toRaw: typeof import('vue')['toRaw']
48 | const toRef: typeof import('vue')['toRef']
49 | const toRefs: typeof import('vue')['toRefs']
50 | const toValue: typeof import('vue')['toValue']
51 | const triggerRef: typeof import('vue')['triggerRef']
52 | const unref: typeof import('vue')['unref']
53 | const useAttrs: typeof import('vue')['useAttrs']
54 | const useCssModule: typeof import('vue')['useCssModule']
55 | const useCssVars: typeof import('vue')['useCssVars']
56 | const useSlots: typeof import('vue')['useSlots']
57 | const watch: typeof import('vue')['watch']
58 | const watchEffect: typeof import('vue')['watchEffect']
59 | const watchPostEffect: typeof import('vue')['watchPostEffect']
60 | const watchSyncEffect: typeof import('vue')['watchSyncEffect']
61 | }
62 | // for type re-export
63 | declare global {
64 | // @ts-ignore
65 | export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
66 | }
67 |
--------------------------------------------------------------------------------
/electron/update/autoUpdater.ts:
--------------------------------------------------------------------------------
1 | // src/main/autoUpdater.js
2 |
3 | import { app, dialog } from 'electron'
4 | import { autoUpdater } from 'electron-updater'
5 | import logger from 'electron-log'
6 | import { name } from '../../package.json'
7 | import { getLocalData, setLocalData, sleep } from './helperUpdater'
8 |
9 | export async function autoUpdateInit() {
10 | autoUpdater.logger = logger
11 | autoUpdater.disableWebInstaller = false
12 | autoUpdater.autoDownload = false // Please write this as "false." When written as "true," I am encountering a permission issue and unable to determine the exact cause.
13 | autoUpdater.on('error', (error) => {
14 | logger.error(['check update error', error])
15 | })
16 | // Upon detecting available updates, it will be triggered to automatically initiate the download process for the updates.
17 | autoUpdater.on('update-available', (info) => {
18 | logger.info('Updates have been detected. Initiating the download of the new version.')
19 | logger.info(info)
20 | const { version } = info
21 | askUpdate(version)
22 | })
23 | // Upon detecting the absence of available updates, it will be triggered accordingly.
24 | autoUpdater.on('update-not-available', () => {
25 | logger.info('No updates have been detected.')
26 | })
27 | // Implementing the delta download logic during the application's startup.
28 | autoUpdater.on('download-progress', async (progress) => {
29 | logger.info(progress)
30 | })
31 | // Upon the completion of the update download process, it will be triggered accordingly.
32 | autoUpdater.on('update-downloaded', (res) => {
33 | logger.info('Download complete! Prompting to install updates.')
34 | logger.info(res)
35 | // To use 'dialog,' it must be created after the 'BrowserWindow' has been created.
36 | dialog.showMessageBox({
37 | type: 'info',
38 | buttons: ['OK'],
39 | title: 'Upgrade notification.!',
40 | message: 'The latest application has been downloaded for you. \n' +
41 | 'Click \'OK\' to replace it with the newest version immediately!',
42 | textWidth: 250,
43 | })
44 | .then(() => {
45 | logger.info('Exit the application, installation will begin!')
46 | // Restart the application and install the update after downloading.
47 | // It should only be called after the 'update-downloaded' event has been emitted.
48 | autoUpdater.quitAndInstall()
49 | })
50 | })
51 |
52 | await sleep(3000)
53 | // Each time it starts, it will automatically perform a version check and update itself accordingly,
54 | // either through scheduled updates or any other suitable method.
55 | await autoUpdater.checkForUpdates()
56 | }
57 |
58 | async function askUpdate(version:any) {
59 | logger.info(`The latest version. ${version}`)
60 | let { updater } = getLocalData()
61 | let { auto, version: ver, skip } = updater || {}
62 | logger.info(
63 | JSON.stringify({
64 | ...updater,
65 | ver: ver,
66 | })
67 | )
68 | if (skip && version === ver) return
69 | if (auto) {
70 | // Proceed with the update download without further inquiry.
71 | await autoUpdater.downloadUpdate()
72 | } else {
73 | const { response, checkboxChecked } = await dialog.showMessageBox({
74 | type: 'question',
75 | title: `${name} software update notification.`,
76 | buttons: ['Closing', 'Skipping this version.', 'Installing the update.'],
77 | message: `The latest version is ${version},the current version is ${app.getVersion()},Is it necessary to download updates now ?`,
78 | defaultId: 2,
79 | cancelId: -1,
80 | checkboxLabel: 'Automatic updates installation.',
81 | checkboxChecked: false,
82 | textWidth: 250,
83 | })
84 | if ([1, 2].includes(response)) {
85 | let updaterData = {
86 | version: version,
87 | skip: response === 1,
88 | auto: checkboxChecked,
89 | }
90 | setLocalData({
91 | updater: {
92 | ...updaterData,
93 | },
94 | })
95 | if (response === 2) await autoUpdater.downloadUpdate()
96 | logger.info(['Updating process.', JSON.stringify(updaterData)])
97 | } else {
98 | logger.info(['Updating process.', 'Disable update notifications.'])
99 | }
100 | }
101 | }
102 |
103 |
--------------------------------------------------------------------------------
/electron/prompt/prompt.ts:
--------------------------------------------------------------------------------
1 | import {join} from 'path'
2 | import fs from 'fs'
3 | import logger from 'electron-log'
4 | import https from "https";
5 |
6 | const promptUrl = 'https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv'
7 | // const promptPath = join(app.getPath('home'), '.claude', 'prompt')
8 | // const promptInfo = join(promptPath, 'promptInfo.json')
9 |
10 |
11 | function downloadAndGetPrompt(promptPath:string) {
12 | const promptInfo = join(promptPath, 'promptInfo.json')
13 | // csv
14 | if (!fs.existsSync(join(promptPath, 'prompts.csv'))) {
15 | downloadPrompt(promptPath)
16 | }
17 |
18 | let promptList = []
19 |
20 | // make promptInfo.json
21 | if (fs.existsSync(join(promptPath, 'prompts.csv')) && !fs.existsSync(promptInfo)) {
22 | let data = fs.readFileSync(join(promptPath, 'prompts.csv'), { encoding: 'utf-8' })
23 | let lines = data.split('\n')
24 | for (let i = 1; i < lines.length; i++) {
25 | let line = lines[i]
26 | // fix bug for csv
27 | let [act, prompt] = line.split('","')
28 | if (act == undefined || prompt == undefined) {
29 | continue
30 | }
31 | let info = {
32 | CMD: act.toLowerCase().replace(/ /g, '_').replace(/"/g, ''),
33 | ACT: act.replace(/"/g, ''),
34 | PROMPT: prompt.replace(/"/g, ''),
35 | ENABLE: true,
36 | }
37 | promptList.push(info)
38 | }
39 | fs.writeFileSync(promptInfo, JSON.stringify(promptList, null, 4))
40 | }else if (fs.existsSync(promptInfo)) {
41 |
42 | // json exists
43 | promptList = JSON.parse(fs.readFileSync(promptInfo, { encoding: 'utf-8' }))
44 | }
45 | return promptList
46 | }
47 |
48 | function downloadPrompt(promptPath:string) {
49 | const file = fs.createWriteStream(join(promptPath, 'prompts.csv.temp'))
50 | logger.info('downloading prompts.csv to prompts.csv.temp')
51 | https.get(promptUrl, response=> {
52 | response.pipe(file)
53 |
54 | // finish
55 | file.on('finish', ()=> {
56 | file.close()
57 | logger.info('download prompts.csv.temp success')
58 | // rename
59 | fs.renameSync(join(promptPath, 'prompts.csv.temp'), join(promptPath, 'prompts.csv'))
60 | logger.info('rename prompts.csv.temp to prompts.csv success')
61 | })
62 |
63 | // error
64 | file.on('error', err=> {
65 | logger.error(err)
66 | })
67 |
68 | }).on('error', err=> {
69 | logger.error(err)
70 | })
71 | }
72 |
73 | function setPromptInfo(promptPath:string,promptList:any[]) {
74 | const promptInfo = join(promptPath, 'promptInfo.json')
75 | fs.writeFileSync(promptInfo, JSON.stringify(promptList, null, 4))
76 | }
77 |
78 | function resetPromptInfo(promptPath:string) {
79 | const promptInfo = join(promptPath, 'promptInfo.json')
80 | let promptList = []
81 | if (fs.existsSync(join(promptPath, 'prompts.csv'))) {
82 | // update json
83 | let data = fs.readFileSync(join(promptPath, 'prompts.csv'), { encoding: 'utf-8' })
84 | let lines = data.split('\n')
85 | for (let i = 1; i < lines.length; i++) {
86 | let line = lines[i]
87 | // fix bug for csv
88 | let [act, prompt] = line.split('","')
89 | if (act == undefined || prompt == undefined) {
90 | continue
91 | }
92 | let info = {
93 | CMD: act.toLowerCase().replace(/ /g, '_').replace(/"/g, ''),
94 | ACT: act.replace(/"/g, ''),
95 | PROMPT: prompt.replace(/"/g, ''),
96 | ENABLE: true,
97 | }
98 | promptList.push(info)
99 | }
100 | fs.writeFileSync(promptInfo, JSON.stringify(promptList, null, 4))
101 | }else{
102 | // download csv
103 | downloadPrompt(promptPath)
104 | }
105 | return promptList
106 | }
107 |
108 | async function syncPromptInfo(promptPath:string) {
109 | const promptInfo = join(promptPath, 'promptInfo.json')
110 | // download
111 | await downloadPrompt(promptPath)
112 | // update json
113 | let promptList = []
114 | if (fs.existsSync(join(promptPath, 'prompts.csv'))) {
115 | // update json
116 | let data = fs.readFileSync(join(promptPath, 'prompts.csv'), { encoding: 'utf-8' })
117 | let lines = data.split('\n')
118 | for (let i = 1; i < lines.length; i++) {
119 | let line = lines[i]
120 | // fix bug for csv
121 | let [act, prompt] = line.split('","')
122 | if (act == undefined || prompt == undefined) {
123 | continue
124 | }
125 | let info = {
126 | CMD: act.toLowerCase().replace(/ /g, '_').replace(/"/g, ''),
127 | ACT: act.replace(/"/g, ''),
128 | PROMPT: prompt.replace(/"/g, ''),
129 | ENABLE: true,
130 | }
131 | promptList.push(info)
132 | }
133 | fs.writeFileSync(promptInfo, JSON.stringify(promptList, null, 4))
134 | }
135 | }
136 |
137 | export {
138 | promptUrl,
139 | downloadAndGetPrompt,
140 | setPromptInfo,
141 | resetPromptInfo,
142 | syncPromptInfo,
143 | }
--------------------------------------------------------------------------------
/electron/main.ts:
--------------------------------------------------------------------------------
1 | import {app, BrowserWindow, ipcMain, Menu, protocol, shell, Tray} from 'electron'
2 | import menuTemplate from "./menu";
3 | import {ConfigFactory, ConfigUpdate} from "../public/config"
4 | import path, {join} from "path";
5 | import Config from "../public/configModel";
6 | import {autoUpdateInit} from "./update/autoUpdater";
7 | import {getLocalData, setLocalData} from "./update/helperUpdater";
8 | import { promptUrl, downloadAndGetPrompt, setPromptInfo, resetPromptInfo, syncPromptInfo,} from "./prompt/prompt";
9 | import logger from 'electron-log'
10 |
11 | // log
12 | logger.transports.file.maxSize = 1002430 // 10M
13 | logger.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}';
14 | logger.transports.file.resolvePath = () => join(path.join(app.getPath('home'), '.claude'), 'logs/claude.log')
15 |
16 | app.commandLine.appendSwitch("--ignore-certificate-errors", "true");
17 | // Scheme must be registered before the app is ready
18 | protocol.registerSchemesAsPrivileged([
19 | { scheme: "app", privileges: { secure: true, standard: true } }
20 | ]);
21 |
22 | // config
23 | const config = ConfigFactory();
24 |
25 | app.whenReady().then(() => {
26 |
27 | const win= new BrowserWindow({
28 | title: 'Claude',
29 | width: config.main_width,
30 | height: config.main_height,
31 | center: true,
32 | icon: "public/logo.png",
33 | webPreferences: {
34 | nodeIntegration: true,
35 | nodeIntegrationInWorker: true,
36 | webSecurity: false,
37 | }
38 | })
39 |
40 | // set user agent
41 | let session = win.webContents.session;
42 | session.setUserAgent(config.ua_tray);
43 |
44 | // load url
45 | win.loadURL('https://claude.ai/chat/')
46 |
47 | // tray
48 | createTray(win);
49 |
50 | // dev tools
51 | if (!app.isPackaged) {
52 | // win.webContents.openDevTools({ mode: 'detach' });
53 | }
54 |
55 | // stay on top
56 | if (config.stay_on_top) {
57 | if (win) {
58 | win.setAlwaysOnTop(true);
59 | }
60 | }
61 |
62 | })
63 |
64 | // create menu
65 | app.on('ready',() =>{
66 | const m = Menu.buildFromTemplate(menuTemplate(config));
67 | Menu.setApplicationMenu(m);
68 | });
69 |
70 | // create Tray
71 | function createTray(win: BrowserWindow){
72 | // Tray
73 | const tray: Tray = new Tray(path.join(__dirname, '../public/logo.png'));
74 | let ControlCenterWin: BrowserWindow | null = null;
75 | const contextMenu = Menu.buildFromTemplate([{
76 | label: 'Control Center',
77 | click: () => {
78 | if (ControlCenterWin) {
79 | ControlCenterWin.focus();
80 | return;
81 | }
82 | // Create the browser window.
83 | ControlCenterWin = new BrowserWindow({
84 | title: 'Control Center',
85 | width: 1000,
86 | height: 650,
87 | resizable: false,
88 | icon: "public/logo.png",
89 | modal: true,
90 | center: true,
91 | parent: BrowserWindow.getFocusedWindow(),
92 | webPreferences: {
93 | nodeIntegration: true,
94 | nodeIntegrationInWorker: true,
95 | webSecurity: false,
96 | preload: path.join(__dirname, '../electron/preload.js')
97 | },
98 | })
99 | if (app.isPackaged) {
100 | ControlCenterWin.loadFile(path.join(__dirname, '../dist/index.html'), { hash: 'controlCenter' })
101 | } else {
102 | ControlCenterWin.loadURL('http://localhost:5173/#/controlCenter')
103 | }
104 | // Emitted when the window is closed.
105 | ControlCenterWin.on('closed', () => {
106 | ControlCenterWin = null
107 | })
108 | }
109 | },
110 | { label: 'Show Window', click: () => win.show() },
111 | { label: 'Reload', click: () => win.reload() },
112 | { type: 'separator' },
113 | { label: 'Quit', click: () => app.quit() }
114 | ])
115 |
116 | tray.setContextMenu(contextMenu)
117 | tray.on('click', () => {
118 | if (win.isVisible()) {
119 | win.hide();
120 | }else{
121 | win.show();
122 | }
123 | });
124 | }
125 |
126 | // Quit when all windows are closed.
127 | app.on('window-all-closed', () => {
128 | if (process.platform !== 'darwin') app.quit()
129 | })
130 |
131 | // render operation
132 | app.whenReady().then(() => {
133 |
134 | const { ipcMain } = require('electron')
135 |
136 | // quit
137 | ipcMain.on('quit', () => {
138 | app.quit()
139 | })
140 |
141 | // get config path
142 | ipcMain.handle('getConfigPath', () => {
143 | return path.join(app.getPath('home'), '.claude')
144 | })
145 |
146 | // open config
147 | ipcMain.on('openConfig', () => {
148 | const configDir = path.join(app.getPath('home'), '.claude');
149 | const { shell } = require('electron')
150 | shell.openPath(configDir).then(r => logger.info(r))
151 | })
152 |
153 | // get update info
154 | ipcMain.handle('getUpdateInfo', () => {
155 | let { updater } = getLocalData()
156 | let { auto, version: ver, skip } = updater || {}
157 | if (auto) {
158 | // file exists
159 | return { auto, ver, skip }
160 | }else{
161 | // file not exists
162 | let updaterData = {
163 | version: app.getVersion(),
164 | skip: false,
165 | auto: false,
166 | }
167 | setLocalData({
168 | updater: {
169 | ...updaterData,
170 | },
171 | })
172 | return { auto: false, version: app.getVersion() , skip: false }
173 | }
174 | })
175 |
176 | // set update info
177 | ipcMain.handle('setUpdateInfo', (event, arg) => {
178 | setLocalData({
179 | updater: {
180 | ...arg,
181 | },
182 | })
183 | return "ok";
184 | })
185 |
186 | // reset update info
187 | ipcMain.handle('resetUpdateInfo', () => {
188 | setLocalData({
189 | updater: {
190 | version: app.getVersion(),
191 | skip: false,
192 | auto: false,
193 | },
194 | })
195 | return "ok"
196 | })
197 |
198 | // get config
199 | ipcMain.handle('getConfig', () => {
200 | return ConfigFactory();
201 | })
202 |
203 | // update config
204 | ipcMain.handle('updateConfig', (event, arg) => {
205 | ConfigUpdate(arg);
206 | return "ok";
207 | })
208 |
209 | // reset config
210 | ipcMain.handle('resetConfig', () => {
211 | const config = new Config();
212 | ConfigUpdate(config);
213 | return "ok"
214 | })
215 |
216 | // get prompt url
217 | ipcMain.handle('getPromptURL', () => {
218 | return promptUrl;
219 | })
220 |
221 | // open prompt
222 | ipcMain.on('openPrompt', () => {
223 | const { shell } = require('electron')
224 | shell.openExternal(config.prompt_path).then(r => logger.info(r))
225 | })
226 |
227 | // get prompt
228 | ipcMain.handle('getPrompt', () => {
229 | return downloadAndGetPrompt(config.prompt_path);
230 | })
231 |
232 | // set prompt
233 | ipcMain.handle('setPrompt', (event, arg) => {
234 | setPromptInfo(config.prompt_path, arg);
235 | return "ok";
236 | })
237 |
238 | // reset prompt
239 | ipcMain.handle('resetPrompt', () => {
240 | return resetPromptInfo(config.prompt_path);
241 | })
242 |
243 | // sync prompt
244 | ipcMain.handle('syncPrompt', () => {
245 | syncPromptInfo(config.prompt_path).then(r => logger.info(r)).catch(e => logger.error(e));
246 | return "ok"
247 | })
248 |
249 | // auto update
250 | autoUpdateInit().then((r: any) => logger.info(r));
251 |
252 | })
--------------------------------------------------------------------------------
/src/views/ControlCenterView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |

12 |
13 |
14 |
15 |
16 |
Claude
17 |
{{version}}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
39 |
40 |
41 | Settings
42 |
43 |
44 |
45 |
46 |
50 |
51 |
52 | Prompts
53 |
54 |
55 |
56 |
57 |
61 |
62 |
63 | Notes
64 |
65 |
66 |
67 |
68 |
72 |
73 |
74 | About
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
142 |
143 |
270 |
--------------------------------------------------------------------------------
/electron/menu.ts:
--------------------------------------------------------------------------------
1 | import {app, BrowserWindow, dialog, MenuItemConstructorOptions, nativeTheme} from 'electron'
2 | import configModel from "../public/configModel";
3 | import path from "path";
4 | import {ConfigFactory, ConfigUpdate} from "../public/config";
5 | import {autoUpdater} from "electron-updater";
6 | import logger from "electron-log";
7 |
8 | let PromptWin: BrowserWindow | null = null;
9 | let ControlCenterWin: BrowserWindow | null = null;
10 |
11 | export default function createMenu(config: configModel) {
12 |
13 | // theme: light=0, dark=1, system=2
14 | let theme: number = 2;
15 | switch (config.theme){
16 | case 'light':
17 | nativeTheme.themeSource = 'light';
18 | theme = 0;
19 | break;
20 | case 'dark':
21 | nativeTheme.themeSource = 'dark';
22 | theme = 1;
23 | break;
24 | case 'system':
25 | nativeTheme.themeSource = 'system';
26 | theme = 2;
27 | break;
28 | }
29 |
30 | const menuTemplate: Array = [{
31 | label: 'Claude',
32 | submenu: [
33 | {
34 | label: 'About Claude', click: () => {
35 | dialog.showMessageBox({
36 | icon: 'public/logo.png',
37 | title: 'About Claude',
38 | // package.json version
39 | message: `Version ${app.getVersion()}`,
40 | detail: 'Claude is a desktop app for the Claude chatbot.',
41 | buttons: ['OK']
42 | });
43 | }
44 | },
45 | {label: 'Check for Updates', click: () => {autoUpdater.checkForUpdates().then(r => logger.info(r)).catch(e => logger.error(e))},
46 | },
47 | {role: 'minimize', label: 'Hide', accelerator: 'ctrl+H'},
48 | {type: 'separator'},
49 | {role: 'quit', label: 'Quit', accelerator: 'ctrl+W'}
50 | ]
51 | }, {
52 | label: 'Preferences',
53 | submenu: [
54 | {
55 | label: 'Prompt',
56 | accelerator: 'ctrl+O',
57 | click: () => {
58 | if (PromptWin) {
59 | PromptWin.focus();
60 | return;
61 | }
62 | // Create the browser window.
63 | PromptWin = new BrowserWindow({
64 | title: 'Prompt Center',
65 | width: 800,
66 | height: 600,
67 | icon: "public/logo.png",
68 | modal: true,
69 | center: true,
70 | parent: BrowserWindow.getFocusedWindow(),
71 | webPreferences: {
72 | nodeIntegration: true,
73 | nodeIntegrationInWorker: true,
74 | webSecurity: false,
75 | preload: path.join(__dirname, '../electron/preload.js'),
76 | },
77 | titleBarStyle: 'hidden',
78 | titleBarOverlay: {
79 | color: '#000000',
80 | symbolColor: '#74b1be',
81 | height: 30,
82 | }
83 | })
84 | if (app.isPackaged) {
85 | PromptWin.loadFile(path.join(__dirname, '../dist/index.html'), { hash: 'prompt' })
86 | } else {
87 | PromptWin.loadURL('http://localhost:5173/#/prompt')
88 | }
89 | // Emitted when the window is closed.
90 | PromptWin.on('closed', () => {
91 | PromptWin = null
92 | })
93 | }
94 | },{
95 | label: 'Control Center',
96 | accelerator: 'ctrl+shift+P',
97 | click: () => {
98 | // exist
99 | if (ControlCenterWin) {
100 | ControlCenterWin.focus();
101 | return;
102 | }
103 | // Create the browser window.
104 | ControlCenterWin = new BrowserWindow({
105 | title: 'Control Center',
106 | width: 1000,
107 | height: 650,
108 | resizable: false,
109 | icon: "public/logo.png",
110 | modal: true,
111 | center: true,
112 | parent: BrowserWindow.getFocusedWindow(),
113 | webPreferences: {
114 | nodeIntegration: true,
115 | nodeIntegrationInWorker: true,
116 | webSecurity: false,
117 | preload: path.join(__dirname, '../electron/preload.js')
118 | },
119 | })
120 | if (app.isPackaged) {
121 | ControlCenterWin.loadFile(path.join(__dirname, '../dist/index.html'), { hash: 'controlCenter' })
122 | } else {
123 | ControlCenterWin.loadURL('http://localhost:5173/#/controlCenter')
124 | }
125 | // Emitted when the window is closed.
126 | ControlCenterWin.on('closed', () => {
127 | ControlCenterWin = null
128 | })
129 | }
130 | },
131 | {type: 'separator'},
132 | {
133 | label: 'Stay On Top', type: 'checkbox', checked: config.stay_on_top, click: () => {
134 | let con = ConfigFactory();
135 | con.stay_on_top = !con.stay_on_top;
136 | ConfigUpdate(con);
137 | const win = BrowserWindow.getFocusedWindow()
138 | if (win) {
139 | win.setAlwaysOnTop(con.stay_on_top);
140 | }
141 | }, accelerator: 'ctrl+T'
142 | },
143 | {
144 | label: 'Theme', submenu: [
145 | {label: 'Light', type: 'radio', checked: theme == 0,click: () => {
146 | let con = ConfigFactory();
147 | con.theme = 'light';
148 | ConfigUpdate(con);
149 | nativeTheme.themeSource = 'light';
150 | }},
151 | {label: 'Dark', type: 'radio', checked: theme == 1, click: () => {
152 | let con = ConfigFactory();
153 | con.theme = 'dark';
154 | ConfigUpdate(con);
155 | nativeTheme.themeSource = 'dark';
156 | }},
157 | {label: 'System', type: 'radio', checked: theme == 2, click: () => {
158 | let con = ConfigFactory();
159 | con.theme = 'system';
160 | ConfigUpdate(con);
161 | nativeTheme.themeSource = 'system';
162 | }},
163 | ]
164 | },]
165 | }, {
166 | label: 'Window',
167 | submenu: [
168 | {role: 'minimize'},
169 | {role: 'togglefullscreen'}
170 | ]
171 | }, {
172 | label: 'Edit',
173 | submenu: [
174 | {role: 'undo'},
175 | {role: 'redo'},
176 | {type: 'separator'},
177 | {role: 'copy'},
178 | {role: 'paste'},
179 | {type: 'separator'},
180 | {role: 'selectAll'},
181 | ]
182 | }, {
183 | label: 'View',
184 | submenu: [
185 | {role: 'reload'},
186 | {role: 'forceReload'},
187 | {type: 'separator'},
188 | {role: 'resetZoom'},
189 | {role: 'zoomIn'},
190 | {role: 'zoomOut'},
191 | ]
192 | }, {
193 | role: 'help',
194 | submenu: [
195 | {
196 | label: 'Learn More',
197 | click: async () => {
198 | const {shell} = require('electron')
199 | await shell.openExternal('https://github.com/Karenina-na/Claude-Desktop')
200 | }
201 | },
202 | {
203 | label: 'About Author',
204 | click: async () => {
205 | const {shell} = require('electron')
206 | await shell.openExternal('https://github.com/Karenina-na')
207 | }
208 | },
209 | {type: 'separator'},
210 | {label: 'Toggle Dev Tools', accelerator: 'ctrl+shift+I', click: () => {
211 | const win = BrowserWindow.getFocusedWindow()
212 | if (win) {
213 | win.webContents.openDevTools({ mode: 'detach'});
214 | }
215 | }
216 | },
217 | ]
218 | }
219 | ]
220 |
221 | return menuTemplate;
222 | }
--------------------------------------------------------------------------------
/src/components/ControlCenter/SettingsView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Config Path: {{ configPath }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Stay On Top:
13 |
18 |
19 |
20 |
21 |
22 | Enable Tray:
23 |
28 |
29 |
30 |
31 |
32 | Theme:
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | User Agent:
43 |
52 |
53 |
54 |
55 |
56 | Prompt Path:
57 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | Default Width:
72 |
81 |
82 |
83 |
84 |
85 | Default Height:
86 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | Auto Update:
105 |
110 |
111 |
112 |
113 |
114 | Skip Next Version:
115 |
120 |
121 |
122 |
123 |
124 |
Current Version:
125 |
{{ updateInfo.version }}
126 |
127 |
128 |
129 |
130 |
131 |
132 |
140 |
141 | Submit
142 |
143 |
144 |
145 |
146 | Cancel
147 |
148 |
149 |
157 |
158 | Reset to defaults
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
333 |
334 |
--------------------------------------------------------------------------------
/src/components/ControlCenter/PromptsView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
URL: {{ promptURL }}
6 |
Cache: {{ promptDir }}
7 |
8 |
9 |
10 |
11 | Enable
12 |
13 |
14 | Disable
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
30 |
31 | Sync
32 |
33 |
34 |
35 |
36 |
44 |
45 | Submit
46 |
47 |
48 |
49 |
50 | Cancel
51 |
52 |
53 |
61 |
62 | Reset to defaults
63 |
64 |
65 |
66 |
67 |
68 |
69 |
76 |
77 |
78 |
79 |
80 | {{ scope.row.CMD }}
81 |
82 |
83 |
84 | {{ scope.row.ACT }}
85 |
86 |
87 |
88 |
89 |
96 |
97 |
98 |
99 |
100 |
101 |
106 | {{ scope.row.PROMPT }}
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | {{ scope.row.PROMPT }}
120 |
121 |
122 |
123 |
124 |
125 |
126 |
138 |
139 |
140 |
416 |
417 |
--------------------------------------------------------------------------------