This commit is contained in:
2026-03-25 22:59:31 +08:00
commit d391deb23b
81 changed files with 13012 additions and 0 deletions

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules
build
dist
out
.DS_Store
.eslintcache
*.log*

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

57
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,57 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

14
.idea/deployment.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
<serverData>
<paths name="WordPress-dev-EndFiled_v2">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
</component>
</project>

7
.idea/dictionaries/project.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>scrollarea</w>
</words>
</dictionary>
</component>

11
.idea/fan-tools.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/out" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="ts-external-references" level="project" />
</component>
</module>

View File

@@ -0,0 +1,26 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="pyside6" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N806" />
<option value="N802" />
<option value="N803" />
<option value="N801" />
<option value="N814" />
</list>
</option>
</inspection_tool>
</profile>
</component>

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{ts-external-references}" />
</component>
</project>

View File

@@ -0,0 +1,14 @@
<component name="libraryTable">
<library name="ts-external-references" type="javaScript">
<properties>
<sourceFilesUrls>
<item url="file://$PROJECT_DIR$/node_modules/buffer/index.d.ts" />
</sourceFilesUrls>
</properties>
<CLASSES>
<root url="file://$PROJECT_DIR$/node_modules/buffer/index.d.ts" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/fan-tools.iml" filepath="$PROJECT_DIR$/.idea/fan-tools.iml" />
</modules>
</component>
</project>

7
.idea/prettier.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
<option name="myRunOnSave" value="true" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

4
.idea/watcherTasks.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions" suppressed-tasks="SCSS" />
</project>

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json

4
.prettierrc.yaml Normal file
View File

@@ -0,0 +1,4 @@
singleQuote: true
semi: false
printWidth: 100
trailingComma: none

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

39
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,39 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": {
"order": 1
}
}
]
}

12
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"js/ts.tsdk.path": "node_modules\\typescript\\lib"
}

33
README.md Normal file
View File

@@ -0,0 +1,33 @@
# FanTools 芒果工具箱
<img height="200" src="/resources/FanTools-ICON.png" width="200" alt="FanTools logo"/>
使用 Electron 技术重新制作的芒果工具箱,再一次尝试。
这应该是芒果工具箱的第三个(?)迭代了~
## 推荐 IDE
创建项目的模板默认推荐使用 Visual Studio Code但是我还是更习惯 WebStorm。
仓库包含了 `.vscode``.idea` 目录,已被不时之需。
## Project Setup
### Install
```bash
$ npm install
```
### Development
```bash
$ npm run dev
```
### Build
```bash
$ npm run build:win
```

3
dev-app-update.yml Normal file
View File

@@ -0,0 +1,3 @@
provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: fan-tools-updater

45
electron-builder.yml Normal file
View File

@@ -0,0 +1,45 @@
appId: com.electron.app
productName: fan-tools
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
asarUnpack:
- resources/**
win:
executableName: fan-tools
nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/

50
electron.vite.config.ts Normal file
View File

@@ -0,0 +1,50 @@
import { resolve } from 'path'
import { defineConfig } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import svgLoader from 'vite-svg-loader'
export default defineConfig({
main: {
resolve: {
alias: {
'@my-type': resolve('src/my-type')
}
}
},
preload: {},
renderer: {
resolve: {
alias: {
'@renderer': resolve('src/renderer/src'),
'@my-type': resolve('src/my-type')
}
},
plugins: [
vue(),
Components({
resolvers: [NaiveUiResolver()],
dts: true
}),
svgLoader({
svgoConfig: {
plugins: [
{
name: 'removeAttrs',
params: {
attrs: ['width', 'height']
}
},
{
name: 'prefixIds',
params: {
prefix: true
}
}
]
}
})
]
}
})

40
eslint.config.mjs Normal file
View File

@@ -0,0 +1,40 @@
import { defineConfig } from 'eslint/config'
import tseslint from '@electron-toolkit/eslint-config-ts'
import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier'
import eslintPluginVue from 'eslint-plugin-vue'
import vueParser from 'vue-eslint-parser'
export default defineConfig(
{ ignores: ['**/node_modules', '**/dist', '**/out'] },
tseslint.configs.recommended,
eslintPluginVue.configs['flat/recommended'],
{
files: ['**/*.vue'],
languageOptions: {
parser: vueParser,
parserOptions: {
ecmaFeatures: {
jsx: true
},
extraFileExtensions: ['.vue'],
parser: tseslint.parser
}
}
},
{
files: ['**/*.{ts,mts,tsx,vue}'],
rules: {
'vue/require-default-prop': 'off',
'vue/multi-word-component-names': 'off',
'vue/block-lang': [
'error',
{
script: {
lang: 'ts'
}
}
]
}
},
eslintConfigPrettier
)

10449
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

65
package.json Normal file
View File

@@ -0,0 +1,65 @@
{
"name": "fan-tools",
"version": "1.0.0",
"description": "An Electron application with Vue and TypeScript",
"main": "./out/main/index.js",
"type": "module",
"author": "example.com",
"homepage": "https://electron-vite.org",
"scripts": {
"format": "prettier --write .",
"lint": "eslint --cache .",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"electron-updater": "^6.3.9",
"highlight.js": "^11.11.1",
"load-json-file": "^7.0.1",
"make-dir": "^5.1.0",
"pinia": "^3.0.4",
"vue-router": "^4.6.4",
"write-json-file": "^7.0.0"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.1.0",
"@electron-toolkit/tsconfig": "^2.0.0",
"@types/node": "^22.19.1",
"@vicons/antd": "^0.13.0",
"@vicons/carbon": "^0.13.0",
"@vicons/fa": "^0.13.0",
"@vicons/fluent": "^0.13.0",
"@vicons/ionicons4": "^0.13.0",
"@vicons/ionicons5": "^0.13.0",
"@vicons/material": "^0.13.0",
"@vicons/tabler": "^0.13.0",
"@vitejs/plugin-vue": "^6.0.2",
"electron": "^39.2.6",
"electron-builder": "^26.0.12",
"electron-vite": "^5.0.0",
"eslint": "^9.39.1",
"eslint-plugin-vue": "^10.6.2",
"naive-ui": "^2.44.1",
"prettier": "^3.7.4",
"sass-embedded": "^1.98.0",
"typescript": "^5.9.3",
"unplugin-vue-components": "^31.0.0",
"vite": "^7.2.6",
"vite-svg-loader": "^5.1.1",
"vue": "^3.5.25",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.1.6"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
resources/FanTools-ICON.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

326
src/main/code-launchpad.ts Normal file
View File

@@ -0,0 +1,326 @@
import type {
checkIDEResultDto,
checkIDEsResultDto,
checkIDEsVersionDto,
checkIDEVersionDto
} from '@my-type/settings'
import { execSync } from 'node:child_process'
import { BrowserWindow, screen, shell, Tray } from 'electron'
import { is } from '@electron-toolkit/utils'
import path from 'path'
import { loadJsonFile } from 'load-json-file'
import os from 'os'
import {
JetBrainsIDEDisplayNameEnum as JIN,
JetBrainsProductCode,
JetBrainsStateDto
} from '@my-type/jetbrains-state-tools'
import { settingsManager } from './settings'
import { codeLaunchpadIcon } from './resources'
import { isNodeError } from '@my-type/node-error'
import { JetBrainsDataProductDto } from '@my-type/jetbrains-data-products'
const JETBRAINS_TOOLBOX_STATE_JSON_PATH = path.join(
os.homedir(),
'AppData/Local/JetBrains/Toolbox/state.json'
)
/**
* 通过 where.exe 检查系统中的指定 IDE 是否安装可用。
* @param display IDE 的正式显示名称,例如 `Visual Studio Code`。
* @param command IDE 的命令行别名,例如 `code`。
* @param code JetBrains IDE的产品代号对于其他 IDE 为空字符串
*/
function checkIDE(
display: string,
command: string,
code: JetBrainsProductCode | '' = ''
): checkIDEResultDto | null {
try {
const paths = execSync(`where.exe ${command}`).toString().split('\n').slice(0, -1)
return { code, display: display, command: command, paths: paths }
} catch {
return null
}
}
/**
* 检查 JerBrains IDEs 是否安装可用。
*/
async function checkJetBrainsIDEs(): Promise<checkIDEsResultDto> {
// 构建数据结构的辅助函数
const _ = (command: string, code: JetBrainsProductCode): checkIDEResultDto => {
return {
code,
command,
display: JIN[code],
paths: []
}
}
const result: checkIDEsResultDto = {
pycharm: _('pycharm', 'PY'),
clion: _('clion', 'CL'),
webstorm: _('webstorm', 'WS'),
phpstorm: _('phpstorm', 'PS'),
idea: _('idea', 'IU')
}
// 优先从 JBTState.json 查找
if (settingsManager._settings?.codeLaunchpadIDESearchPolicy.includes('JBTState.json')) {
// 处理文件不存在,即 JetBrains Toolbox 相关问题
try {
const data: JetBrainsStateDto = await loadJsonFile(JETBRAINS_TOOLBOX_STATE_JSON_PATH)
for (const ide of data.tools) {
for (const resultElement of Object.values(result)) {
if (resultElement?.code === ide.productCode) {
resultElement.paths = [ide.launchCommand]
}
}
}
} catch (error) {
if (isNodeError(error)) {
if (error.code === 'ENOENT') {
console.log('由于 JBTState.json 不存在,回退到 where.exe。')
}
} else {
console.error(error)
}
}
}
// 从 where.exe 查找
// 此分支确保在设置中启用 where.exe 方式,以及 JBTState.json 方式没有结果(未找到任何,或出现错误)时进入
if (
settingsManager._settings?.codeLaunchpadIDESearchPolicy.includes('where.exe') &&
Object.entries(result).length === 0
) {
result['clion'] = checkIDE(JIN.clion, 'clion', 'CL')
result['pycharm'] = checkIDE(JIN.pycharm, 'pycharm', 'PY')
result['webstorm'] = checkIDE(JIN.webstorm, 'webstorm', 'WS')
result['phpstorm'] = checkIDE(JIN.phpstorm, 'phpstorm', 'PS')
result['idea'] = checkIDE(JIN.idea, 'idea', 'IU')
}
return result
}
/**
* 获取系统中的各 IDE 是否安装可用。
*/
export async function getIDEs(): Promise<checkIDEsResultDto> {
return global.installedIDEs === null ? await checkIDEs() : global.installedIDEs
}
/**
* 检查系统中的各 IDE 是否安装可用。
* 原理是运行 `where.exe ${command}` 并检查返回值。
*/
export async function checkIDEs(): Promise<checkIDEsResultDto> {
console.log('在系统中查找已安装的 IDE ...')
const vscodeIDEs = {
vscode: checkIDE('Visual Studio Code', 'code')
}
global.installedIDEs = { ...vscodeIDEs, ...(await checkJetBrainsIDEs()) }
return global.installedIDEs
}
/**
* 检查 VSCode 的版本信息,通过 code --version。
*/
async function checkVSCodeVersion(): Promise<checkIDEVersionDto> {
const install = execSync('code --version').toString().split('\n')[0]
let latest = 'unknown'
try {
latest = await fetch('https://update.code.visualstudio.com/api/releases/stable')
.then((res) => res.json())
.then((data) => data[0])
} catch (error) {
console.error('获取 VSCode 版本列表时出现错误。错误提供如下。')
console.error(error)
}
return { code: '', install, latest, display: 'Visual Studio Code' }
}
/**
* 检查 JetBrains IDE 的版本信息
*/
export async function checkJetBrainsIDEsVersion(): Promise<checkIDEsVersionDto> {
// 构建数据结构的辅助函数
const _ = (display: string, code: JetBrainsProductCode): checkIDEVersionDto => {
return {
code,
display,
install: 'unknown',
latest: 'unknown'
}
}
const result: checkIDEsVersionDto = {
pycharm: _('pycharm', 'PY'),
clion: _('clion', 'CL'),
webstorm: _('webstorm', 'WS'),
phpstorm: _('phpstorm', 'PS'),
idea: _('idea', 'IU')
}
// 尝试从 JBTState.json 获取已安装的 JetBrains IDEs 的版本
// TODO添加更多获取 JetBrains IDEs 已安装版本的方案
try {
const data: JetBrainsStateDto = await loadJsonFile(JETBRAINS_TOOLBOX_STATE_JSON_PATH)
for (const tool of data.tools) {
for (const resultElement of Object.values(result)) {
if (resultElement?.code === tool.productCode) {
resultElement.install = tool.displayVersion
}
}
}
} catch (error) {
if (isNodeError(error)) {
if (error.code === 'ENOENT') {
console.log('由于 JBTState.json 不存在,检查 JetBrains IDEs 已安装版本的任务失败。')
}
} else {
console.error(error)
}
}
// 从命令行获取 JetBrains IDE 的已安装版本的方案仍然在科研中
// 所以这个方法就暂时先结束了~
// 从网络接口获取 JetBrains IDEs 的版本列表
const codes: JetBrainsProductCode[] = []
for (const resultElement of Object.values(result)) {
codes.push(<JetBrainsProductCode>resultElement?.code)
}
try {
const params = new URLSearchParams({
type: 'release',
code: codes.join(',')
})
const data: JetBrainsDataProductDto[] = await fetch(
`https://data.services.jetbrains.com/products?${params}`
).then((res) => res.json())
for (const datum of data) {
for (const resultElement of Object.values(result)) {
if (resultElement?.code === datum.intellijProductCode) {
resultElement.latest = datum.releases[0].version
}
}
}
} catch (error) {
console.error('从 JetBrains IDEs 版本列表接口获取数据失败。错误提供如下。')
console.error(error)
}
return result
}
/**
* 获取各 IDE 的版本信息。
*/
export async function getIDEsVersion(): Promise<checkIDEsVersionDto> {
return global.installedIDEsVersion === null
? await checkIDEsVersion()
: global.installedIDEsVersion
}
/**
* 检查各 IDE 的版本信息。
*/
export async function checkIDEsVersion(): Promise<checkIDEsVersionDto> {
let result: checkIDEsVersionDto = {}
for (const ide in await getIDEs()) {
switch (ide) {
case 'vscode':
result['vscode'] = await checkVSCodeVersion()
break
}
}
result = { ...result, ...(await checkJetBrainsIDEsVersion()) }
global.installedIDEsVersion = result
return result
}
/**
* 创建代码启动台窗口。
* @return 布尔值,表明创建是否成功
*/
export function createCodeLaunchpadWindow(closeOnBlur: boolean): boolean {
// 不允许重复创建
if (global.codeLaunchpadWindow !== null) {
return false
}
const windowWidth = settingsManager._settings?.codeLaunchpadWidth
? settingsManager._settings?.codeLaunchpadWidth
: 460
const windowHeight = settingsManager._settings?.codeLaunchpadHeight
? settingsManager._settings?.codeLaunchpadHeight
: 760
const position = settingsManager._settings?.codeLaunchpadPosition
? settingsManager._settings.codeLaunchpadPosition
: 'left top'
const codeLaunchpadWindow = new BrowserWindow({
width: windowWidth,
height: windowHeight,
x:
position === 'left top' || position === 'left bottom'
? 0
: screen.getPrimaryDisplay().workArea.width - windowWidth,
y:
position === 'left top' || position === 'right top'
? 0
: screen.getPrimaryDisplay().workArea.height - windowHeight,
frame: false,
show: false,
autoHideMenuBar: true,
// 代码启动台需要置顶
alwaysOnTop: true,
resizable: false,
backgroundColor: '#1f1f1f',
icon: codeLaunchpadIcon,
webPreferences: {
preload: path.join(__dirname, '../preload/index.mjs'),
sandbox: false
}
})
codeLaunchpadWindow.on('ready-to-show', () => {
codeLaunchpadWindow.show()
})
// 如有必要,失去焦点时自动关闭
if (closeOnBlur) {
codeLaunchpadWindow.on('blur', () => {
codeLaunchpadWindow.close()
})
}
// 关闭代码启动台时
codeLaunchpadWindow.on('close', () => {
global.codeLaunchpadWindow = null
})
codeLaunchpadWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// 开发和生产环境的各自设置
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
codeLaunchpadWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/codeLaunchpad/IDEs')
} else {
codeLaunchpadWindow.loadFile(path.join(__dirname, '../renderer/index.html'), {
hash: '/codeLaunchpad/IDEs'
})
}
global.codeLaunchpadWindow = codeLaunchpadWindow
return true
}
export function createCodeLaunchpadTray(): Tray {
const tray = new Tray(codeLaunchpadIcon)
tray.on('click', () => createCodeLaunchpadWindow(true))
return tray
}

15
src/main/global.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
// noinspection ES6ConvertVarToLetConst,JSUnusedGlobalSymbols
import { checkIDEsResultDto, checkIDEsVersionDto } from '@my-type/settings'
import { BrowserWindow } from 'electron'
declare global {
// 准备关闭工具箱的信号
var isQuiting: boolean
// 代码启动台窗口实例,如果没有打开或已关闭则为 null
var codeLaunchpadWindow: BrowserWindow | null
// 检测到的已安装 IDE
var installedIDEs: checkIDEsResultDto | null
// IDE 版本
var installedIDEsVersion: checkIDEsVersionDto | null
}

130
src/main/index.ts Normal file
View File

@@ -0,0 +1,130 @@
import { app, BrowserWindow, ipcMain, Menu, shell, Tray } from 'electron'
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import { saveSettingsToLocal, updateSettingsFromLocal } from './settings'
import {
checkIDEs,
checkIDEsVersion,
createCodeLaunchpadTray,
createCodeLaunchpadWindow,
getIDEs,
getIDEsVersion
} from './code-launchpad'
import { fanToolsIcon } from './resources'
import path from 'path'
let mainWindow: BrowserWindow | null = null
let mainTray: Tray | null = null
let codeLaunchpadTray: Tray | null = null
/**
* 创建程序主窗口
*/
function createWindow(): BrowserWindow {
const window = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
backgroundColor: '#1f1f1f',
icon: fanToolsIcon,
webPreferences: {
preload: path.join(__dirname, '../preload/index.mjs'),
sandbox: false
}
})
window.on('ready-to-show', () => {
window!.show()
})
window.on('close', (event) => {
if (!global.isQuiting) {
event.preventDefault()
window.hide()
}
})
window.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// 开发和生产环境的各自设置
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
window.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/main/home')
} else {
window.loadFile(path.join(__dirname, '../renderer/index.html'), { hash: '/main/home' })
}
return window
}
function createTray(): Tray {
const tray = new Tray(fanToolsIcon)
tray.setToolTip('FanTools')
tray.setContextMenu(
Menu.buildFromTemplate([
{
label: '显示主窗口',
click: () => {
mainWindow?.show()
}
},
{
label: '退出工具箱',
click: () => {
global.isQuiting = true
app.quit()
}
}
])
)
tray.on('click', () => {
mainWindow?.show()
})
return tray
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
global.isQuiting = false
global.codeLaunchpadWindow = null
global.installedIDEs = null
global.installedIDEsVersion = null
mainWindow = createWindow()
mainTray = createTray()
codeLaunchpadTray = createCodeLaunchpadTray()
ipcMain.handle('settings:save', saveSettingsToLocal)
ipcMain.handle('settings:update', updateSettingsFromLocal)
ipcMain.handle('tools:openCodeLaunchpad', () => {
return createCodeLaunchpadWindow(false)
})
ipcMain.handle('tools:closeCodeLaunchpad', () => {
if (global.codeLaunchpadWindow !== null) {
global.codeLaunchpadWindow.close()
return true
}
return false
})
ipcMain.handle('codeLaunchpad:getIDEs', getIDEs)
ipcMain.handle('codeLaunchpad:checkIDEs', checkIDEs)
ipcMain.handle('codeLaunchpad:getIDEsVersion', getIDEsVersion)
ipcMain.handle('codeLaunchpad:checkIDEsVersion', checkIDEsVersion)
checkIDEs()
checkIDEsVersion()
})

4
src/main/resources.ts Normal file
View File

@@ -0,0 +1,4 @@
import fanToolsIcon from '../../resources/FanTools-ICON.png?asset'
import codeLaunchpadIcon from '../../resources/CodeLaunchpad-ICON.png?asset'
export { fanToolsIcon, codeLaunchpadIcon }

51
src/main/settings.ts Normal file
View File

@@ -0,0 +1,51 @@
import { settingsDto } from '@my-type/settings'
import { loadJsonFileSync } from 'load-json-file'
import { makeDirectory } from 'make-dir'
import os from 'os'
import path from 'path'
import { writeJsonFileSync } from 'write-json-file'
import IpcMainInvokeEvent = Electron.IpcMainInvokeEvent
// 主进程
const SETTINGS_DIR = await makeDirectory(path.join(os.homedir(), 'AppData/Local/FanTools/settings'))
const SETTINGS_PATH = path.join(SETTINGS_DIR, 'settings.json')
console.log('已加载 FanTools 设置目录:%s', SETTINGS_PATH)
class SettingsManager {
_settingsPath = SETTINGS_PATH
_settings: settingsDto | null = null
constructor() {
this.updateSettings()
console.log('SettingsManager 已启用。')
console.log(this._settings)
}
saveSettings(settings: settingsDto): void {
writeJsonFileSync(this._settingsPath, settings)
this._settings = settings
}
updateSettings(): settingsDto {
this._settings = loadJsonFileSync<settingsDto>(this._settingsPath)
return this._settings
}
}
export const settingsManager = new SettingsManager()
/**
* 保存从渲染进程发回的设置
*/
export function saveSettingsToLocal(_: IpcMainInvokeEvent, settings: settingsDto): boolean {
settingsManager.saveSettings(settings)
return true
}
/**
* 向渲染进程发送设置
*/
export function updateSettingsFromLocal(): settingsDto {
return settingsManager._settings === null
? settingsManager.updateSettings()
: settingsManager._settings
}

30
src/my-type/ide-icons.ts Normal file
View File

@@ -0,0 +1,30 @@
// @ts-ignore 直接导入 svg 图像作为 vue 组件
import VSCodeIcon from '@renderer/assets/vscode.svg?component'
// @ts-ignore 直接导入 svg 图像作为 vue 组件
import CLionIcon from '@renderer/assets/CLion_icon.svg?component'
// @ts-ignore 直接导入 svg 图像作为 vue 组件
import IDEAIcon from '@renderer/assets/IntelliJ_IDEA_icon.svg?component'
// @ts-ignore 直接导入 svg 图像作为 vue 组件
import PhpStormIcon from '@renderer/assets/PhpStorm_icon.svg?component'
// @ts-ignore 直接导入 svg 图像作为 vue 组件
import PyCharmIcon from '@renderer/assets/PyCharm_icon.svg?component'
// @ts-ignore 直接导入 svg 图像作为 vue 组件
import WebStormIcon from '@renderer/assets/WebStorm_icon.svg?component'
import { FunctionalComponent } from 'vue'
function _(
icon: FunctionalComponent,
description: string
): { icon: FunctionalComponent; description: string } {
return { icon, description }
}
export const ideIcons = {
vscode: _(VSCodeIcon, '全能的代码编辑器'),
clion: _(CLionIcon, '强大的 C 和 C++ IDE'),
idea: _(IDEAIcon, '强大的 Java IDE'),
phpstorm: _(PhpStormIcon, '强大的 PHP IDE'),
pycharm: _(PyCharmIcon, '强大的 Python IDE'),
webstorm: _(WebStormIcon, '强大的 Web IDE')
}

View File

@@ -0,0 +1,20 @@
import type { JetBrainsProductCode } from '@my-type/jetbrains-state-tools'
export interface JetBrainsDataProductReleaseDto {
date: string
type: 'release'
// e.g. 2025.3.3
version: string
// e.g. 2025.3
majorVersion: string
// e.g. 253.31033.139
build: string
// HTML 文本
whatsnew: string
}
export interface JetBrainsDataProductDto {
intellijProductCode: JetBrainsProductCode
// 认为 releases[0] 即为最新版吧
releases: JetBrainsDataProductReleaseDto[]
}

View File

@@ -0,0 +1,27 @@
export type JetBrainsProductCode = 'PY' | 'IU' | 'WS' | 'CL' | 'PS'
export interface JetBrainsStateToolDto {
channelId: string
toolId: string
productCode: JetBrainsProductCode
tag: string
displayName: string
displayVersion: string
buildNumber: string
installLocation: string
launchCommand: string
}
export interface JetBrainsStateDto {
version: 1
appVersion: string
tools: JetBrainsStateToolDto[]
}
export enum JetBrainsIDEDisplayNameEnum {
pycharm = 'PyCharm',
idea = 'IntelliJ IDEA',
clion = 'CLion',
phpstorm = 'PhpStorm',
webstorm = 'WebStorm'
}

View File

@@ -0,0 +1,3 @@
export function isNodeError(error: unknown): error is NodeJS.ErrnoException {
return error instanceof Error && 'code' in error
}

39
src/my-type/settings.d.ts vendored Normal file
View File

@@ -0,0 +1,39 @@
import type { JetBrainsProductCode } from '@my-type/jetbrains-state-tools'
export type screenPosition = 'left top' | 'left bottom' | 'right top' | 'right bottom'
export type keyboardShortcut = 'alt+c' | 'alt+space'
export type ideSearchPolicy = 'where.exe' | 'JBTState.json' | 'global.search'
export interface settingsDto {
isStayInTray: boolean
isAutoLaunchAtStartUp: boolean
isCodeLaunchpadEnabled: boolean
isCodeLaunchpadInTray: boolean
isCodeLaunchpadShortcutEnabled: boolean
codeLaunchpadShortcut: keyboardShortcut
codeLaunchpadPosition: screenPosition
codeLaunchpadWidth: number
codeLaunchpadHeight: number
codeLaunchpadIDESearchPolicy: ideSearchPolicy[]
}
export interface checkIDEResultDto {
// 仅针对 JetBrains 系列 IDEs 的产品代码,对于其他 IDE 统一为空字符串
code: JetBrainsProductCode | ''
display: string
command: string
paths: string[]
}
export type checkIDEsResultDto = Record<string, checkIDEResultDto | null>
export interface checkIDEVersionDto {
// 仅针对 JetBrains 系列 IDEs 的产品代码,对于其他 IDE 统一为空字符串
code: JetBrainsProductCode | ''
display: string
install: string
latest: string
}
export type checkIDEsVersionDto = Record<string, checkIDEVersionDto | null>

23
src/preload/index.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import { settingsDto, checkIDEsResultDto } from '@my-type/settings'
import { checkIDEsVersionDto } from "../my-type/settings";
// 此处只有签名
declare global {
interface Window {
electron: ElectronAPI
api: {
_saveSettings: (settings: settingsDto) => Promise<boolean>
_updateSettings: () => Promise<settingsDto>
_openCodeLaunchpad: () => Promise<boolean>
_closeCodeLaunchpad: () => Promise<boolean>
}
codeLaunchpad: {
_getIDEs: () => Promise<checkIDEsResultDto>
_checkIDEs: () => Promise<checkIDEsResultDto>
_getIDEsVersion: () => Promise<checkIDEsVersionDto>
_checkIDEsVersion: () => Promise<checkIDEsVersionDto>
}
}
}

39
src/preload/index.ts Normal file
View File

@@ -0,0 +1,39 @@
import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
import { settingsDto } from '@my-type/settings'
// Custom APIs for renderer
// 在此添加新的进程间通信 API
const api = {
_saveSettings: (settings: settingsDto) => ipcRenderer.invoke('settings:save', settings),
_updateSettings: () => ipcRenderer.invoke('settings:update'),
_openCodeLaunchpad: () => ipcRenderer.invoke('tools:openCodeLaunchpad'),
_closeCodeLaunchpad: () => ipcRenderer.invoke('tools:closeCodeLaunchpad')
}
const codeLaunchpadApi = {
_getIDEs: () => ipcRenderer.invoke('codeLaunchpad:getIDEs'),
_checkIDEs: () => ipcRenderer.invoke('codeLaunchpad:checkIDEs'),
_getIDEsVersion: () => ipcRenderer.invoke('codeLaunchpad:getIDEsVersion'),
_checkIDEsVersion: () => ipcRenderer.invoke('codeLaunchpad:checkIDEsVersion')
}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
contextBridge.exposeInMainWorld('codeLaunchpad', codeLaunchpadApi)
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
// @ts-ignore (define in dts)
window.codeLaunchpad = codeLaunchpadApi
}

51
src/renderer/components.d.ts vendored Normal file
View File

@@ -0,0 +1,51 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
CoderJson: typeof import('./src/components/CoderJson.vue')['default']
DetectedIDECard: typeof import('./src/components/DetectedIDECard.vue')['default']
DetectedIDECardList: typeof import('./src/components/DetectedIDECardList.vue')['default']
DetectedIDEVersionCard: typeof import('./src/components/DetectedIDEVersionCard.vue')['default']
DetectedIDEVersionCardList: typeof import('./src/components/DetectedIDEVersionCardList.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCode: typeof import('naive-ui')['NCode']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex']
NFloatButton: typeof import('naive-ui')['NFloatButton']
NFloatButtonGroup: typeof import('naive-ui')['NFloatButtonGroup']
NH1: typeof import('naive-ui')['NH1']
NH2: typeof import('naive-ui')['NH2']
NH3: typeof import('naive-ui')['NH3']
NH4: typeof import('naive-ui')['NH4']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NP: typeof import('naive-ui')['NP']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
NSwitch: typeof import('naive-ui')['NSwitch']
NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText']
NTooltip: typeof import('naive-ui')['NTooltip']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SaveSettingsButton: typeof import('./src/components/SaveSettingsButton.vue')['default']
SettingCard: typeof import('./src/components/SettingCard.vue')['default']
SidebarRouter: typeof import('./src/components/SidebarRouter.vue')['default']
Versions: typeof import('./src/components/Versions.vue')['default']
}
}

24
src/renderer/index.html Normal file
View File

@@ -0,0 +1,24 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Electron</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
<style>
div#app {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

15
src/renderer/src/App.vue Normal file
View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import { darkTheme, dateZhCN, zhCN } from 'naive-ui'
import hljs from 'highlight.js'
import javascript from 'highlight.js/lib/languages/javascript'
import json from 'highlight.js/lib/languages/json'
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('json', json)
</script>
<template>
<n-config-provider :date-locale="dateZhCN" :hljs :locale="zhCN" :theme="darkTheme">
<router-view />
</n-config-provider>
</template>

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const activeKey = ref('/codeLaunchpad/IDEs')
const router = useRouter()
const codeLaunchpadMenuOptions = [
{
label: 'IDEs',
key: '/codeLaunchpad/IDEs'
},
{
label: '项目',
key: '/codeLaunchpad/projects'
}
]
function handleUpdateValue(key: string): void {
router.push(key)
}
</script>
<template>
<div class="codeLaunchpad-container">
<n-menu
v-model:value="activeKey"
:options="codeLaunchpadMenuOptions"
mode="horizontal"
@update:value="handleUpdateValue"
/>
<div class="scrollarea">
<router-view />
</div>
</div>
</template>
<style lang="scss" scoped>
div.codeLaunchpad-container {
display: flex;
flex-direction: column;
overflow: hidden;
width: 100vw;
height: 100vh;
}
</style>

View File

@@ -0,0 +1,39 @@
<script lang="ts" setup>
import SidebarRouter from '@renderer/components/SidebarRouter.vue'
import SaveSettingsButton from '@renderer/components/SaveSettingsButton.vue'
</script>
<template>
<div class="everything-container">
<div class="sidebar-router-container scrollarea">
<SidebarRouter />
</div>
<div class="content-container scrollarea">
<n-message-provider placement="bottom">
<RouterView />
<SaveSettingsButton />
</n-message-provider>
</div>
</div>
</template>
<style lang="scss">
div.everything-container {
display: flex;
flex-direction: row;
overflow: hidden;
width: 100vw;
height: 100vh;
div.sidebar-router-container {
flex: 0;
min-width: 200px;
}
div.content-container {
flex: 1;
padding: 20px 10px 20px 0;
overflow-x: hidden;
}
}
</style>

View File

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" viewBox="0 0 64 64">
<defs>
<linearGradient id="a" x1="3.98138" x2="62.6868" y1="4.22048" y2="62.9254" gradientUnits="userSpaceOnUse">
<stop offset=".29" stop-color="#009AE5"/>
<stop offset=".7" stop-color="#00D980"/>
</linearGradient>
<linearGradient id="b" x1="56.3788" x2="2.75258" y1="-.71738" y2="24.1455" gradientUnits="userSpaceOnUse">
<stop offset=".3" stop-color="#FF318C"/>
<stop offset=".54" stop-color="#009AE5"/>
</linearGradient>
</defs>
<path fill="url(#a)" d="M64 59.9255V25.9572c0-1.6291-.9711-3.1017-2.4681-3.7434L24.0413 6.14572c-.5068-.21702-1.0531-.32931-1.6046-.32931H4.07273C1.82342 5.81641 0 7.63982 0 9.88913V27.8551c0 .8047.238545 1.5913.685382 2.2609L22.0887 62.1847c.7552 1.1322 2.0265 1.8118 3.3874 1.8118l34.4512.0017c2.2493 0 4.0727-1.8234 4.0727-4.0727Z"/>
<path fill="url(#b)" d="M58.1818 14.5193V4.07273C58.1818 1.82342 56.3584 0 54.1091 0H41.0164c-.1925 0-.3851.013964-.576.040727L3.49673 5.3184C1.49004 5.60465 0 7.32334 0 9.34982V25.0228c0 2.2499 1.824 4.0733 4.07389 4.0728l18.53851-.0047c.4375 0 .8721-.0704 1.2869-.2089l31.4979-10.4995c1.6629-.5544 2.7846-2.1108 2.7846-3.8638v.0006Z"/>
<path fill="#FF318C" d="m58.1814 15.9476-.0017-11.87545C58.1797 1.82342 56.3562 0 54.1069 0H42.6003c-1.1886 0-2.3185.519564-3.0923 1.42196L6.79872 39.5834c-.63243.7383-.98036 1.6786-.98036 2.6508v11.8755c0 2.2493 1.82342 4.0727 4.07273 4.0727H21.3994c1.1887 0 2.3186-.5196 3.0924-1.422L57.201 18.599c.6331-.7383.9804-1.6786.9804-2.6514Z"/>
<path fill="#000" d="M52 12H12v40h40V12Z"/>
<path fill="#fff" d="M20.0471 31.161c1.1594.668 2.4537 1.0029 3.8828 1.0029v-.0011c1.2086 0 2.3183-.2241 3.328-.6721 1.0097-.448 1.8486-1.0731 2.5172-1.8771.6754-.8108 1.1274-1.7388 1.3548-2.784h-3.0508c-.1995.54-.4977 1.0172-.896 1.4292-.3915.4057-.864.7188-1.4189.9388-.5543.22-1.1588.3309-1.8131.3309-.8817 0-1.6783-.22-2.3892-.6612-.7114-.4411-1.2697-1.0451-1.6748-1.8131-.3983-.7743-.5972-1.6463-.5972-2.6132 0-.9668.1989-1.8348.5972-2.6028.4057-.7749.964-1.3829 1.6748-1.824.7109-.4412 1.5075-.6612 2.3892-.6612.6537 0 1.2583.1109 1.8131.3309.5549.22 1.0274.5371 1.4189.9491.3983.4057.6965.8789.896 1.4189h3.0508c-.228-1.0451-.6794-1.9691-1.3548-2.7731-.6686-.8109-1.5075-1.44-2.5172-1.888-1.0097-.448-2.1194-.672-3.328-.672-1.4297 0-2.724.3371-3.8828 1.0131-1.1595.668-2.0697 1.5931-2.7309 2.7731-.6611 1.1732-.992 2.4852-.992 3.936 0 1.4509.3309 2.7669.992 3.9469.6617 1.1731 1.572 2.0971 2.7309 2.7731Z"/>
<path fill="#fff" d="M36.1248 29.2833V16.9742h-2.9012v14.9331h9.984v-2.624h-7.0828Z"/>
<path fill="#fff" d="M16.9941 43.9988h16v3h-16v-3Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" viewBox="0 0 64 64">
<defs>
<linearGradient id="a" x1="-.717383" x2="24.1455" y1="7.61946" y2="61.2456" gradientUnits="userSpaceOnUse">
<stop offset=".1" stop-color="#FC801D"/>
<stop offset=".59" stop-color="#FE2857"/>
</linearGradient>
<linearGradient id="b" x1="4.22243" x2="62.9273" y1="60.0186" y2="1.31316" gradientUnits="userSpaceOnUse">
<stop offset=".21" stop-color="#FE2857"/>
<stop offset=".7" stop-color="#007EFF"/>
</linearGradient>
</defs>
<path fill="#FF8100" d="m15.9476 5.81641-11.87545.00174C1.82284 5.81815 0 7.64157 0 9.89088V21.3975c0 1.1887.519564 2.3185 1.42196 3.0924L39.5828 57.1997c.7384.6324 1.6786.9803 2.6508.9803h11.8755c2.2493 0 4.0727-1.8234 4.0727-4.0727V42.599c0-1.1887-.5195-2.3186-1.4219-3.0924L18.599 6.79735c-.7383-.63302-1.6786-.98036-2.6514-.98036v-.00058Z"/>
<path fill="url(#a)" d="M14.5193 5.81641H4.07273C1.82342 5.81641 0 7.63982 0 9.88913V22.9818c0 .1926.013964.3852.040727.576L5.31782 60.5015c.28683 2.0067 2.00494 3.4967 4.032 3.4967H25.0228c2.2499 0 4.0733-1.824 4.0728-4.0739l-.0047-18.5384c0-.4376-.0704-.8722-.2089-1.287L18.3825 8.60099c-.5544-1.66284-2.1108-2.78458-3.8638-2.78458h.0006Z"/>
<path fill="url(#b)" d="M59.9275 0H25.9592c-1.6291 0-3.1017.971054-3.7435 2.46807L6.14767 39.9587c-.21702.5068-.32931 1.0531-.32931 1.6046v18.364C5.81836 62.1766 7.64178 64 9.89109 64H27.8571c.8046 0 1.5912-.2385 2.2609-.6854l32.0687-21.4033c1.1322-.7552 1.8117-2.0265 1.8117-3.3874l.0018-34.45117C64.0002 1.82342 62.1768 0 59.9275 0Z"/>
<path fill="#000" d="M52 12H12v40h40V12Z"/>
<path fill="#fff" d="M17 29.3856h2.9788v-9.7712H17V17h8.839v2.6144h-2.9788v9.7712h2.9788V32H17v-2.6144Z"/>
<path fill="#fff" d="M27.3389 29.3002h2.1538c.4354 0 .8233-.0928 1.1625-.2784.3392-.1857.6016-.4481.7872-.7873.1857-.3392.2785-.7265.2785-1.1625V17h2.9249v10.2748c0 .9001-.2074 1.7092-.6216 2.4271-.4143.7179-.9855 1.2805-1.7143 1.6873-.7288.4074-1.5464.6108-2.4534.6108h-2.5176v-2.6998Z"/>
<path fill="#fff" d="M17 44h16v3H17v-3Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" viewBox="0 0 64 64">
<defs>
<linearGradient id="a" x1="56.3788" x2="2.75258" y1="-.717381" y2="24.146" gradientUnits="userSpaceOnUse">
<stop offset=".16" stop-color="#D249FC"/>
<stop offset=".55" stop-color="#FF2D90"/>
</linearGradient>
<linearGradient id="b" x1="3.98196" x2="62.6868" y1="4.22048" y2="62.9259" gradientUnits="userSpaceOnUse">
<stop offset=".3" stop-color="#FF2D90"/>
<stop offset=".7" stop-color="#7256FF"/>
</linearGradient>
</defs>
<path fill="#D249FC" d="m58.182 15.9476-.0018-11.87545C58.1797 1.82342 56.3562 0 54.1075 0H42.6009c-1.1886 0-2.3185.519564-3.0924 1.42196L6.79872 39.5834c-.63243.7383-.98036 1.6786-.98036 2.6508v11.8755c0 2.2493 1.82342 4.0727 4.07273 4.0727H21.3994c1.1887 0 2.3186-.5196 3.0924-1.422L57.2011 18.599c.633-.7383.9803-1.6786.9803-2.6514h.0006Z"/>
<path fill="url(#a)" d="M58.1818 14.5193V4.07273C58.1818 1.82342 56.3584 0 54.1091 0H41.0164c-.1925 0-.3851.013964-.576.040727L3.49673 5.3184C1.49062 5.60465 0 7.32334 0 9.34982V25.0228c0 2.2499 1.824 4.0733 4.07389 4.0728l18.53851-.0047c.4375 0 .8721-.0704 1.2869-.2089l31.4979-10.4995c1.6629-.5544 2.7846-2.1108 2.7846-3.8638v.0006Z"/>
<path fill="url(#b)" d="M64 59.9255V25.9572c0-1.6291-.9711-3.1017-2.4681-3.7434L24.0413 6.14572c-.5068-.21702-1.0531-.32931-1.6046-.32931H4.07273C1.82342 5.81641 0 7.63982 0 9.88913V27.8551c0 .8047.238545 1.5913.685382 2.2609L22.0887 62.1847c.7552 1.1322 2.0265 1.8118 3.3874 1.8118l34.4512.0017c2.2493 0 4.0727-1.8234 4.0727-4.0727Z"/>
<path fill="#000" d="M52 12H12v40h40V12Z"/>
<path fill="#fff" fill-rule="evenodd" d="M23.2922 17.3302h-6.2969v14.6705h2.8607v-5.5436h3.3532c1.0475 0 1.9665-.1887 2.7558-.5659.7966-.384 1.4079-.9223 1.834-1.614.4328-.6916.6495-1.5089.6495-2.41 0-.901-.2133-1.6936-.6394-2.3785-.4194-.6849-1.02-1.2154-1.8026-1.5926-.7826-.3773-1.6875-.5659-2.7143-.5659Zm1.0683 6.4867c-.3564.1746-.7752.2622-1.2574.2622h-3.2487v-4.3698h3.2487c.4822 0 .901.0904 1.2574.2723.3633.174.6423.4255.8382.7545.1959.3205.2936.7124.2936 1.1525 0 .4401-.0983.828-.2936 1.1632-.1954.3289-.4749.5832-.8382.7651Z" clip-rule="evenodd"/>
<path fill="#fff" d="M32.1424 31.6762c.8594.384 1.8267.5765 2.9029.5765 1.0823 0 2.0529-.192 2.9124-.5759.8595-.384 1.5297-.9184 2.012-1.6033.4822-.6916.723-1.4776.723-2.3578 0-.7124-.1538-1.3687-.4609-1.9699-.307-.608-.7438-1.1149-1.3097-1.5196-.5585-.4048-1.2047-.6743-1.9384-.8067l-2.913-.5451c-.4614-.0904-.8213-.2729-1.0795-.5451-.2588-.279-.3879-.6221-.3879-1.0268 0-.3497.1049-.6563.3143-.9223.2094-.2729.503-.4862.8803-.6395.3772-.1532.7932-.2307 1.2681-.2307.475 0 .901.0797 1.2783.2408.3772.1533.6708.3706.8802.6496.2167.279.3251.6012.3251.9638h2.8607c-.0067-.8381-.2442-1.578-.7123-2.2213-.4609-.6501-1.1003-1.157-1.9177-1.5197-.8106-.3632-1.7324-.5451-2.7457-.5451-1.0133 0-1.925.1847-2.735.5552-.8039.3711-1.436.887-1.8969 1.5511-.4542.6641-.681 1.4214-.681 2.2742 0 .6916.1398 1.3203.4194 1.8862.2795.5591.6775 1.0268 1.1946 1.404.5237.3705 1.1384.622 1.8441.7545l3.0073.5765c.4957.0983.8836.3077 1.1632.6288.2863.3143.4294.7062.4294 1.1738 0 .3773-.1151.7124-.3458 1.006-.2301.2936-.5518.5238-.9639.6916-.4053.1611-.8662.2408-1.3832.2408-.5445 0-1.0341-.0903-1.4669-.2722-.4266-.1819-.7651-.4334-1.0166-.7545-.2442-.3284-.3666-.7017-.3666-1.1211h-2.8715c.0141.8949.2656 1.6909.7545 2.3892.489.6917 1.1632 1.23 2.0227 1.614Z"/>
<path fill="#fff" d="M16.9941 44.0015h16v3h-16v-3Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" viewBox="0 0 64 64">
<defs>
<linearGradient id="a" x1="7.62141" x2="61.2476" y1="64.7192" y2="39.8558" gradientUnits="userSpaceOnUse">
<stop offset=".1" stop-color="#00D886"/>
<stop offset=".59" stop-color="#F0EB18"/>
</linearGradient>
<linearGradient id="b" x1="60.0186" x2="1.31317" y1="59.7778" y2="1.07229" gradientUnits="userSpaceOnUse">
<stop offset=".3" stop-color="#F0EB18"/>
<stop offset=".7" stop-color="#00C4F4"/>
</linearGradient>
</defs>
<path fill="#00D886" d="m5.81934 48.0512.00174 11.8755c0 2.2493 1.82342 4.0721 4.07273 4.0721H21.4004c1.1887 0 2.3186-.5196 3.0924-1.422L57.202 24.4154c.6325-.7383.9804-1.6786.9804-2.6508V9.88913c0-2.24931-1.8234-4.07272-4.0727-4.07272H42.6013c-1.1887 0-2.3185.51956-3.0924 1.42196L6.7997 45.3998c-.63302.7384-.98036 1.6786-.98036 2.6514Z"/>
<path fill="url(#a)" d="M5.81836 49.4825v10.4466c0 2.2493 1.82342 4.0727 4.07273 4.0727H22.9837c.1926 0 .3852-.0139.576-.0407l36.9438-5.2771c2.0066-.2868 3.4967-2.0049 3.4967-4.032V38.979c0-2.2499-1.824-4.0733-4.0739-4.0727l-18.5385.0046c-.4375 0-.8721.0704-1.287.2089L8.60294 45.6193c-1.66284.5544-2.78458 2.1108-2.78458 3.8638v-.0006Z"/>
<path fill="url(#b)" d="M0 4.07273V38.041c0 1.6291.971054 3.1017 2.46807 3.7434L39.9587 57.8525c.5068.217 1.0531.3293 1.6046.3293h18.364c2.2493 0 4.0727-1.8234 4.0727-4.0727v-17.966c0-.8046-.2385-1.5912-.6854-2.2609L41.9119 1.81353C41.1561.681309 39.8854.001745 38.5245.001745L4.07273 0C1.82342 0 0 1.82342 0 4.07273Z"/>
<path fill="#000" d="M52 12H12v40h40V12Z"/>
<path fill="#fff" fill-rule="evenodd" d="M23.5363 16.9676h-6.4407v15.0055h2.9261v-5.6702h3.4296c1.0715 0 2.0115-.1929 2.8188-.5788.8148-.3927 1.4401-.9434 1.8759-1.6508.4427-.7074.6643-1.5434.6643-2.465 0-.9216-.2182-1.7324-.654-2.4329-.4289-.7005-1.0433-1.2431-1.8437-1.629-.8005-.3859-1.7261-.5788-2.7763-.5788Zm1.0927 6.6349c-.3646.1785-.7929.2681-1.2862.2681h-3.3229v-4.4695h3.3229c.4933 0 .9216.0924 1.2862.2784.3715.178.6569.4353.8573.7718.2004.3278.3003.7286.3003 1.1788 0 .4502-.1005.8469-.3003 1.1897-.1998.3365-.4858.5966-.8573.7827Z" clip-rule="evenodd"/>
<path fill="#fff" d="M33.3821 31.2232c1.1651.6713 2.4656 1.0077 3.9017 1.0077v-.0011c1.2144 0 2.3295-.2251 3.3441-.6753 1.0146-.4501 1.8575-1.0783 2.5294-1.8862.6787-.8148 1.1328-1.7473 1.3614-2.7975H41.453c-.2004.5426-.5001 1.0221-.9003 1.4361-.3933.4076-.8688.7223-1.4257.9434-.557.221-1.1645.3324-1.822.3324-.886 0-1.6864-.221-2.4007-.6643-.7149-.4433-1.2759-1.0502-1.683-1.8219-.4002-.7781-.6-1.6543-.6-2.6259 0-.9715.1998-1.8437.6-2.6154.4077-.7786.9681-1.3896 1.683-1.8329.7143-.4432 1.5147-.6643 2.4007-.6643.6569 0 1.2644.1114 1.822.3325.5575.221 1.0324.5397 1.4257.9537.4002.4077.6999.8831.9003 1.4257h3.0657c-.2291-1.0502-.6827-1.9787-1.3614-2.7866-.6719-.8147-1.5148-1.4469-2.5294-1.8971-1.0146-.4502-2.1297-.6753-3.3441-.6753-1.4367 0-2.7372.3388-3.9017 1.0181-1.165.6712-2.0797 1.6009-2.7441 2.7866-.6643 1.1788-.9968 2.4972-.9968 3.955 0 1.4579.3325 2.7803.9968 3.966.6649 1.1789 1.5791 2.1073 2.7441 2.7866Z"/>
<path fill="#fff" d="M16.9941 44.001h16v3h-16v-3Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" viewBox="0 0 64 64">
<defs>
<linearGradient id="a" x1="7.62141" x2="61.2476" y1="64.7192" y2="39.8564" gradientUnits="userSpaceOnUse">
<stop offset=".22" stop-color="#F0EB18"/>
<stop offset=".59" stop-color="#00C4F4"/>
</linearGradient>
<linearGradient id="b" x1="60.0186" x2="1.31316" y1="59.7777" y2="1.07287" gradientUnits="userSpaceOnUse">
<stop offset=".19" stop-color="#00C4F4"/>
<stop offset=".83" stop-color="#007DFE"/>
</linearGradient>
</defs>
<path fill="#F0EB18" d="m5.81836 48.0512.00174 11.8755c0 2.2493 1.82342 4.0721 4.07273 4.0721H21.3994c1.1887 0 2.3186-.5196 3.0924-1.422l32.7098-38.1614c.6325-.7383.9804-1.6786.9804-2.6508V9.88913c0-2.24931-1.8234-4.07272-4.0727-4.07272H42.6009c-1.1887 0-2.3185.51956-3.0924 1.42196L6.79872 45.3998c-.63302.7383-.98036 1.6786-.98036 2.6514Z"/>
<path fill="url(#a)" d="M5.81836 49.4825v10.4466c0 2.2493 1.82342 4.0727 4.07273 4.0727H22.9837c.1926 0 .3852-.014.576-.0407l36.9437-5.2771c2.0067-.2868 3.4968-2.005 3.4968-4.032V38.979c0-2.2499-1.824-4.0733-4.0739-4.0727l-18.5385.0046c-.4375 0-.8721.0704-1.287.2089L8.60294 45.6193c-1.66284.5544-2.78458 2.1108-2.78458 3.8638v-.0006Z"/>
<path fill="url(#b)" d="M0 4.07273V38.041c0 1.6291.971054 3.1017 2.46807 3.7434L39.9587 57.8525c.5067.217 1.0531.3293 1.6046.3293h18.364c2.2493 0 4.0727-1.8234 4.0727-4.0727v-17.966c0-.8046-.2386-1.5913-.6854-2.2609L41.9113 1.81353C41.1561.681309 39.8854.001745 38.5239.001745L4.07273 0C1.82342 0 0 1.82342 0 4.07273Z"/>
<path fill="#000" d="M52 12H12v40h40V12Z"/>
<path fill="#fff" d="m21.2377 27.961-2.4224-10.9532h-3.0321l3.6224 15.0044h3.3226l2.5613-10.8998 2.5292 10.8998h3.333l3.6441-15.0044h-2.9689L29.4797 27.961l-2.7008-10.9532h-2.9581L21.2377 27.961Z"/>
<path fill="#fff" d="M38.2224 31.6804c.8429.3927 1.8074.5896 2.8937.5896s2.0515-.1998 2.8944-.6005c.8428-.4008 1.4968-.9503 1.9613-1.6507.4714-.7074.7073-1.5049.7073-2.3903 0-.7222-.1607-1.3865-.4822-1.9934-.3216-.6149-.7717-1.1294-1.3505-1.5434-.5718-.422-1.2252-.7073-1.9613-.8572l-2.551-.5575c-.4644-.1137-.8325-.3066-1.1041-.5787-.2646-.2722-.3967-.615-.3967-1.0289 0-.364.0965-.6815.2894-.9537.1929-.2785.461-.4932.8038-.643.3497-.1568.7539-.236 1.2109-.236.457 0 .8607.0815 1.2109.2463.3571.1567.6321.3818.8251.6752.1929.2854.2893.6109.2893.9755h2.9259c-.0074-.8572-.236-1.614-.6861-2.272-.4432-.6648-1.0645-1.1833-1.8649-1.5542-.8003-.3715-1.7253-.5575-2.7329-.5575-1.0077 0-1.908.1929-2.7009.5788-.7929.3789-1.4107.9106-1.8539 1.5967-.4427.6792-.6643 1.4469-.6643 2.3041 0 .7073.1464 1.3441.4392 1.9079.2934.5644.7039 1.0398 1.2327 1.4256.5288.379 1.1506.6471 1.8649.8038l2.6474.5897c.5076.1223.9037.3393 1.1897.654.2928.3146.4392.7113.4392 1.1896 0 .3859-.1074.7286-.3215 1.0289-.2073.3003-.5036.5357-.8894.7074-.3789.1648-.8004.2463-1.3292.2463-.5288 0-.9967-.0925-1.4038-.2785-.4071-.186-.7251-.4432-.9537-.7716-.2285-.3279-.3427-.7114-.3427-1.1466h-2.9368c.0143.9215.2572 1.7362.7286 2.4436.4788.7073 1.1362 1.2579 1.9722 1.6507Z"/>
<path fill="#fff" d="M16.9932 44h16v3h-16v-3Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,37 @@
:root {
--color-background: #1f1f1f;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
ul {
list-style: none;
}
body {
min-height: 100vh;
background: var(--color-background);
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -0,0 +1,10 @@
<svg viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="64" cy="64" r="64" fill="#2F3242"/>
<ellipse cx="63.9835" cy="23.2036" rx="4.48794" ry="4.495" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path d="M51.3954 39.5028C52.3733 39.6812 53.3108 39.033 53.4892 38.055C53.6676 37.0771 53.0194 36.1396 52.0414 35.9612L51.3954 39.5028ZM28.6153 43.5751L30.1748 44.4741L30.1748 44.4741L28.6153 43.5751ZM28.9393 60.9358C29.4332 61.7985 30.5329 62.0976 31.3957 61.6037C32.2585 61.1098 32.5575 60.0101 32.0636 59.1473L28.9393 60.9358ZM37.6935 66.7457C37.025 66.01 35.8866 65.9554 35.1508 66.6239C34.415 67.2924 34.3605 68.4308 35.029 69.1666L37.6935 66.7457ZM53.7489 81.7014L52.8478 83.2597L53.7489 81.7014ZM96.9206 89.515C97.7416 88.9544 97.9526 87.8344 97.3919 87.0135C96.8313 86.1925 95.7113 85.9815 94.8904 86.5422L96.9206 89.515ZM52.0414 35.9612C46.4712 34.9451 41.2848 34.8966 36.9738 35.9376C32.6548 36.9806 29.0841 39.1576 27.0559 42.6762L30.1748 44.4741C31.5693 42.0549 34.1448 40.3243 37.8188 39.4371C41.5009 38.5479 46.1547 38.5468 51.3954 39.5028L52.0414 35.9612ZM27.0559 42.6762C24.043 47.9029 25.2781 54.5399 28.9393 60.9358L32.0636 59.1473C28.6579 53.1977 28.1088 48.0581 30.1748 44.4741L27.0559 42.6762ZM35.029 69.1666C39.6385 74.24 45.7158 79.1355 52.8478 83.2597L54.6499 80.1432C47.8081 76.1868 42.0298 71.5185 37.6935 66.7457L35.029 69.1666ZM52.8478 83.2597C61.344 88.1726 70.0465 91.2445 77.7351 92.3608C85.359 93.4677 92.2744 92.6881 96.9206 89.515L94.8904 86.5422C91.3255 88.9767 85.4902 89.849 78.2524 88.7982C71.0793 87.7567 62.809 84.8612 54.6499 80.1432L52.8478 83.2597ZM105.359 84.9077C105.359 81.4337 102.546 78.6127 99.071 78.6127V82.2127C100.553 82.2127 101.759 83.4166 101.759 84.9077H105.359ZM99.071 78.6127C95.5956 78.6127 92.7831 81.4337 92.7831 84.9077H96.3831C96.3831 83.4166 97.5892 82.2127 99.071 82.2127V78.6127ZM92.7831 84.9077C92.7831 88.3817 95.5956 91.2027 99.071 91.2027V87.6027C97.5892 87.6027 96.3831 86.3988 96.3831 84.9077H92.7831ZM99.071 91.2027C102.546 91.2027 105.359 88.3817 105.359 84.9077H101.759C101.759 86.3988 100.553 87.6027 99.071 87.6027V91.2027Z" fill="#A2ECFB"/>
<path d="M91.4873 65.382C90.8456 66.1412 90.9409 67.2769 91.7002 67.9186C92.4594 68.5603 93.5951 68.465 94.2368 67.7058L91.4873 65.382ZM99.3169 43.6354L97.7574 44.5344L99.3169 43.6354ZM84.507 35.2412C83.513 35.2282 82.6967 36.0236 82.6838 37.0176C82.6708 38.0116 83.4661 38.8279 84.4602 38.8409L84.507 35.2412ZM74.9407 39.8801C75.9127 39.6716 76.5315 38.7145 76.323 37.7425C76.1144 36.7706 75.1573 36.1517 74.1854 36.3603L74.9407 39.8801ZM53.7836 46.3728L54.6847 47.931L53.7836 46.3728ZM25.5491 80.9047C25.6932 81.8883 26.6074 82.5688 27.5911 82.4247C28.5747 82.2806 29.2552 81.3664 29.1111 80.3828L25.5491 80.9047ZM94.2368 67.7058C97.8838 63.3907 100.505 58.927 101.752 54.678C103.001 50.4213 102.9 46.2472 100.876 42.7365L97.7574 44.5344C99.1494 46.9491 99.3603 50.0419 98.2974 53.6644C97.2323 57.2945 94.9184 61.3223 91.4873 65.382L94.2368 67.7058ZM100.876 42.7365C97.9119 37.5938 91.7082 35.335 84.507 35.2412L84.4602 38.8409C91.1328 38.9278 95.7262 41.0106 97.7574 44.5344L100.876 42.7365ZM74.1854 36.3603C67.4362 37.8086 60.0878 40.648 52.8826 44.8146L54.6847 47.931C61.5972 43.9338 68.5948 41.2419 74.9407 39.8801L74.1854 36.3603ZM52.8826 44.8146C44.1366 49.872 36.9669 56.0954 32.1491 62.3927C27.3774 68.63 24.7148 75.2115 25.5491 80.9047L29.1111 80.3828C28.4839 76.1026 30.4747 70.5062 35.0084 64.5802C39.496 58.7143 46.2839 52.7889 54.6847 47.931L52.8826 44.8146Z" fill="#A2ECFB"/>
<path d="M49.0825 87.2295C48.7478 86.2934 47.7176 85.8059 46.7816 86.1406C45.8455 86.4753 45.358 87.5055 45.6927 88.4416L49.0825 87.2295ZM78.5635 96.4256C79.075 95.5732 78.7988 94.4675 77.9464 93.9559C77.0941 93.4443 75.9884 93.7205 75.4768 94.5729L78.5635 96.4256ZM79.5703 85.1795C79.2738 86.1284 79.8027 87.1379 80.7516 87.4344C81.7004 87.7308 82.71 87.2019 83.0064 86.2531L79.5703 85.1795ZM84.3832 64.0673H82.5832H84.3832ZM69.156 22.5301C68.2477 22.1261 67.1838 22.535 66.7799 23.4433C66.3759 24.3517 66.7848 25.4155 67.6931 25.8194L69.156 22.5301ZM45.6927 88.4416C47.5994 93.7741 50.1496 98.2905 53.2032 101.505C56.2623 104.724 59.9279 106.731 63.9835 106.731V103.131C61.1984 103.131 58.4165 101.765 55.8131 99.0249C53.2042 96.279 50.8768 92.2477 49.0825 87.2295L45.6927 88.4416ZM63.9835 106.731C69.8694 106.731 74.8921 102.542 78.5635 96.4256L75.4768 94.5729C72.0781 100.235 68.0122 103.131 63.9835 103.131V106.731ZM83.0064 86.2531C85.0269 79.7864 86.1832 72.1831 86.1832 64.0673H82.5832C82.5832 71.8536 81.4723 79.0919 79.5703 85.1795L83.0064 86.2531ZM86.1832 64.0673C86.1832 54.1144 84.4439 44.922 81.4961 37.6502C78.5748 30.4436 74.3436 24.8371 69.156 22.5301L67.6931 25.8194C71.6364 27.5731 75.3846 32.1564 78.1598 39.0026C80.9086 45.7836 82.5832 54.507 82.5832 64.0673H86.1832Z" fill="#A2ECFB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M103.559 84.9077C103.559 82.4252 101.55 80.4127 99.071 80.4127C96.5924 80.4127 94.5831 82.4252 94.5831 84.9077C94.5831 87.3902 96.5924 89.4027 99.071 89.4027C101.55 89.4027 103.559 87.3902 103.559 84.9077V84.9077Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.8143 89.4027C31.2929 89.4027 33.3023 87.3902 33.3023 84.9077C33.3023 82.4252 31.2929 80.4127 28.8143 80.4127C26.3357 80.4127 24.3264 82.4252 24.3264 84.9077C24.3264 87.3902 26.3357 89.4027 28.8143 89.4027V89.4027V89.4027Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.8501 68.0857C62.6341 68.5652 60.451 67.1547 59.9713 64.9353C59.4934 62.7159 60.9007 60.5293 63.1167 60.0489C65.3326 59.5693 67.5157 60.9798 67.9954 63.1992C68.4742 65.4186 67.066 67.6052 64.8501 68.0857Z" fill="#A2ECFB"/>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,54 @@
@import './base.css';
body {
background-image: url('./wavy-lines.svg');
background-size: cover;
-webkit-user-select: none;
user-select: none;
height: 100vh;
width: 100vw;
overflow: hidden;
}
.no-margin-bottom {
margin-bottom: 0;
}
// 滚动区域
div.scrollarea {
overflow: auto;
}
// 默认卡片的 margin 间距
.default-card {
margin: 10px 0;
}
// 卡片的 header-extra 插槽内的输入标签
.card-header-extra-input {
width: 120px;
}
.card-header-extra-input-large {
width: 300px;
}
.n-icon svg {
width: 100%;
height: 100%;
}
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background-color: #393939;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background-color: #74c072;
border-radius: 4px;
}

View File

@@ -0,0 +1,41 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.9119 99.3171C72.4869 99.9307 74.2828 99.8914 75.8725 99.1264L96.4608 89.2197C98.6242 88.1787 100 85.9892 100 83.5872V16.4133C100 14.0113 98.6243 11.8218 96.4609 10.7808L75.8725 0.873756C73.7862 -0.130129 71.3446 0.11576 69.5135 1.44695C69.252 1.63711 69.0028 1.84943 68.769 2.08341L29.3551 38.0415L12.1872 25.0096C10.589 23.7965 8.35363 23.8959 6.86933 25.2461L1.36303 30.2549C-0.452552 31.9064 -0.454633 34.7627 1.35853 36.417L16.2471 50.0001L1.35853 63.5832C-0.454633 65.2374 -0.452552 68.0938 1.36303 69.7453L6.86933 74.7541C8.35363 76.1043 10.589 76.2037 12.1872 74.9905L29.3551 61.9587L68.769 97.9167C69.3925 98.5406 70.1246 99.0104 70.9119 99.3171ZM75.0152 27.2989L45.1091 50.0001L75.0152 72.7012V27.2989Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<path d="M96.4614 10.7962L75.8569 0.875542C73.4719 -0.272773 70.6217 0.211611 68.75 2.08333L1.29858 63.5832C-0.515693 65.2373 -0.513607 68.0937 1.30308 69.7452L6.81272 74.754C8.29793 76.1042 10.5347 76.2036 12.1338 74.9905L93.3609 13.3699C96.086 11.3026 100 13.2462 100 16.6667V16.4275C100 14.0265 98.6246 11.8378 96.4614 10.7962Z" fill="#0065A9"/>
<g filter="url(#filter0_d)">
<path d="M96.4614 89.2038L75.8569 99.1245C73.4719 100.273 70.6217 99.7884 68.75 97.9167L1.29858 36.4169C-0.515693 34.7627 -0.513607 31.9063 1.30308 30.2548L6.81272 25.246C8.29793 23.8958 10.5347 23.7964 12.1338 25.0095L93.3609 86.6301C96.086 88.6974 100 86.7538 100 83.3334V83.5726C100 85.9735 98.6246 88.1622 96.4614 89.2038Z" fill="#007ACC"/>
</g>
<g filter="url(#filter1_d)">
<path d="M75.8578 99.1263C73.4721 100.274 70.6219 99.7885 68.75 97.9166C71.0564 100.223 75 98.5895 75 95.3278V4.67213C75 1.41039 71.0564 -0.223106 68.75 2.08329C70.6219 0.211402 73.4721 -0.273666 75.8578 0.873633L96.4587 10.7807C98.6234 11.8217 100 14.0112 100 16.4132V83.5871C100 85.9891 98.6234 88.1786 96.4586 89.2196L75.8578 99.1263Z" fill="#1F9CF0"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.25">
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.8511 99.3171C72.4261 99.9306 74.2221 99.8913 75.8117 99.1264L96.4 89.2197C98.5634 88.1787 99.9392 85.9892 99.9392 83.5871V16.4133C99.9392 14.0112 98.5635 11.8217 96.4001 10.7807L75.8117 0.873695C73.7255 -0.13019 71.2838 0.115699 69.4527 1.44688C69.1912 1.63705 68.942 1.84937 68.7082 2.08335L29.2943 38.0414L12.1264 25.0096C10.5283 23.7964 8.29285 23.8959 6.80855 25.246L1.30225 30.2548C-0.513334 31.9064 -0.515415 34.7627 1.29775 36.4169L16.1863 50L1.29775 63.5832C-0.515415 65.2374 -0.513334 68.0937 1.30225 69.7452L6.80855 74.754C8.29285 76.1042 10.5283 76.2036 12.1264 74.9905L29.2943 61.9586L68.7082 97.9167C69.3317 98.5405 70.0638 99.0104 70.8511 99.3171ZM74.9544 27.2989L45.0483 50L74.9544 72.7012V27.2989Z" fill="url(#paint0_linear)"/>
</g>
</g>
<defs>
<filter id="filter0_d" x="-8.39411" y="15.8291" width="116.727" height="92.2456" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset/>
<feGaussianBlur stdDeviation="4.16667"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter1_d" x="60.4167" y="-8.07558" width="47.9167" height="116.151" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset/>
<feGaussianBlur stdDeviation="4.16667"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<linearGradient id="paint0_linear" x1="49.9392" y1="0.257812" x2="49.9392" y2="99.7423" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1422 800" opacity="0.3">
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="oooscillate-grad">
<stop stop-color="hsl(206, 75%, 49%)" stop-opacity="1" offset="0%"></stop>
<stop stop-color="hsl(331, 90%, 56%)" stop-opacity="1" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke-width="1" stroke="url(#oooscillate-grad)" fill="none" stroke-linecap="round">
<path d="M 0 448 Q 355.5 -100 711 400 Q 1066.5 900 1422 448" opacity="0.05"></path>
<path d="M 0 420 Q 355.5 -100 711 400 Q 1066.5 900 1422 420" opacity="0.11"></path>
<path d="M 0 392 Q 355.5 -100 711 400 Q 1066.5 900 1422 392" opacity="0.18"></path>
<path d="M 0 364 Q 355.5 -100 711 400 Q 1066.5 900 1422 364" opacity="0.24"></path>
<path d="M 0 336 Q 355.5 -100 711 400 Q 1066.5 900 1422 336" opacity="0.30"></path>
<path d="M 0 308 Q 355.5 -100 711 400 Q 1066.5 900 1422 308" opacity="0.37"></path>
<path d="M 0 280 Q 355.5 -100 711 400 Q 1066.5 900 1422 280" opacity="0.43"></path>
<path d="M 0 252 Q 355.5 -100 711 400 Q 1066.5 900 1422 252" opacity="0.49"></path>
<path d="M 0 224 Q 355.5 -100 711 400 Q 1066.5 900 1422 224" opacity="0.56"></path>
<path d="M 0 196 Q 355.5 -100 711 400 Q 1066.5 900 1422 196" opacity="0.62"></path>
<path d="M 0 168 Q 355.5 -100 711 400 Q 1066.5 900 1422 168" opacity="0.68"></path>
<path d="M 0 140 Q 355.5 -100 711 400 Q 1066.5 900 1422 140" opacity="0.75"></path>
<path d="M 0 112 Q 355.5 -100 711 400 Q 1066.5 900 1422 112" opacity="0.81"></path>
<path d="M 0 84 Q 355.5 -100 711 400 Q 1066.5 900 1422 84" opacity="0.87"></path>
<path d="M 0 56 Q 355.5 -100 711 400 Q 1066.5 900 1422 56" opacity="0.94"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { Component, onMounted } from 'vue'
import { checkIDEResultDto, checkIDEVersionDto } from '@my-type/settings'
defineProps<{
icon: Component
alia: string
ideInfo: checkIDEResultDto
ideVersion: checkIDEVersionDto | null
}>()
onMounted(() => {})
</script>
<template>
<n-card>
<template #header>
<div class="ide-card-header">
<n-icon :component="icon" size="34" />
<n-h2>{{ ideInfo.display }}</n-h2>
</div>
</template>
<template #header-extra>
<n-flex>
<n-tag size="large" type="info">
{{ ideVersion?.install ? ideVersion?.install : '检查版本中' }}
</n-tag>
<n-button secondary type="primary">运行</n-button>
</n-flex>
</template>
<template #default>
<slot />
</template>
</n-card>
</template>
<style lang="scss" scoped>
div.ide-card-header {
display: flex;
flex-direction: row;
h2.n-h2 {
margin: 0 12px;
}
}
</style>

View File

@@ -0,0 +1,77 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { useMessage } from 'naive-ui'
const message = useMessage()
const defaultInput =
'{"name":"Code Space","author":1,"url":"https://code.mangofanfan.cn","users":[{"name":"MangoFanFanw","age":18,"male":true}]}'
const codingHumanFriendly = ref(true)
const inputValue = ref(defaultInput)
const displayValue = ref('')
const errorTip = ref('')
function generate(humanFriendly: boolean): void {
try {
displayValue.value = JSON.stringify(JSON.parse(inputValue.value), null, humanFriendly ? 4 : 0)
errorTip.value = ''
} catch (error) {
if (error instanceof SyntaxError) {
message.error('JSON 解析错误,请复制完整并检查是否合法。')
displayValue.value = ''
errorTip.value = error.message
}
}
}
onMounted(() => {
generate(true)
})
</script>
<template>
<n-card class="default-card" title="json 格式化/压缩">
<template #header-extra>
<n-flex align="center" justify="center">
<n-switch v-model:value="codingHumanFriendly" size="large">
<template #checked> 转换为人类友好格式 </template>
<template #unchecked> 转换为机器友好格式 </template>
</n-switch>
<n-button secondary type="primary" @click="() => generate(codingHumanFriendly)"
>生成</n-button
>
<n-button
secondary
type="warning"
@click="
() => {
inputValue = defaultInput
codingHumanFriendly = true
generate(true)
}
"
>重置</n-button
>
<n-button secondary type="info">复制结果</n-button>
</n-flex>
</template>
<template #default>
<n-flex vertical>
<n-input v-model:value="inputValue" spellcheck="false" type="textarea" />
<div class="scrollarea code-area">
<n-code :code="displayValue" language="json" />
</div>
<n-alert v-if="errorTip !== ''" type="error">
{{ errorTip }}
</n-alert>
</n-flex>
</template>
</n-card>
</template>
<style scoped>
div.code-area {
max-height: 600px;
}
</style>

View File

@@ -0,0 +1,40 @@
<script lang="ts" setup>
import { checkIDEResultDto, checkIDEVersionDto } from '@my-type/settings'
import { ideIcons } from '@my-type/ide-icons'
defineProps<{
info: checkIDEResultDto | null
version: checkIDEVersionDto | null
name: string
}>()
</script>
<template>
<n-card v-if="info !== null">
<template #header>
<n-flex justify="left">
<n-icon :component="ideIcons[name]['icon']" size="28"></n-icon>
<n-h4>{{ info.display }}</n-h4>
<n-tag type="success">{{ info.command }}</n-tag>
</n-flex>
</template>
<template #header-extra>
<n-tag>{{ ideIcons[name]['description'] }}</n-tag>
</template>
<template #default>
<n-flex vertical>
<n-tag v-for="path in info.paths" :key="path" type="info">{{ path }}</n-tag>
<n-flex>
<n-tag type="success">已安装 {{ version?.install ? version?.install : 'Unknown' }}</n-tag>
<n-tag type="warning">最新 {{ version?.latest ? version?.latest : 'Unknown' }}</n-tag>
</n-flex>
</n-flex>
</template>
</n-card>
</template>
<style lang="scss" scoped>
.n-h4 {
margin: 0 12px;
}
</style>

View File

@@ -0,0 +1,31 @@
<script lang="ts" setup>
import { checkIDEsResultDto, checkIDEsVersionDto } from '@my-type/settings'
import DetectedIDECard from '@renderer/components/DetectedIDECard.vue'
const props = defineProps<{
ides: checkIDEsResultDto | null
versions: checkIDEsVersionDto
}>()
</script>
<template>
<div class="scrollarea detected-ide-card-list">
<n-flex v-if="ides !== null" vertical>
<DetectedIDECard
v-for="(info, name) in props.ides"
:key="name"
:info
:name
:version="versions[name]"
/>
</n-flex>
<n-empty v-else description="什么都木有捏" size="large" />
</div>
</template>
<style scoped>
div.detected-ide-card-list {
max-height: 600px;
padding-right: 10px;
}
</style>

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import { useSettings } from '@renderer/stores'
import { useMessage } from 'naive-ui'
import { SaveEdit20Filled as SaveIcon } from '@vicons/fluent'
import { RefreshOutline as RefreshIcon } from '@vicons/ionicons5'
import { onMounted } from 'vue'
const settings = useSettings()
const message = useMessage()
function browserSaveSettings(): void {
settings.save().then((result: boolean) => {
if (result) {
message.success('设置已成功保存~')
} else {
message.error('未知错误,保存失败…')
}
})
}
function updateSettings(): void {
settings.update().then((result: boolean) => {
if (result) {
message.info('已将设置与磁盘同步。')
} else {
message.warning('还是没在磁盘中找到已保存的设置,这不合理。')
}
})
}
onMounted(async () => {
if (!(await settings.update())) {
message.info('未在磁盘中找到已保存的设置,这合理吗?')
}
})
</script>
<template>
<n-float-button-group :right="16" :bottom="16" shape="circle">
<n-tooltip placement="left">
<template #trigger>
<n-float-button @click="updateSettings">
<n-icon>
<RefreshIcon />
</n-icon>
</n-float-button>
</template>
从磁盘中重新获取设置一般用来撤销未保存的更改或者同步你刚刚在磁盘中手动修改的设置
</n-tooltip>
<n-tooltip placement="left">
<template #trigger>
<n-float-button type="primary" @click="browserSaveSettings">
<n-icon>
<SaveIcon />
</n-icon>
</n-float-button>
</template>
保存设置到磁盘
</n-tooltip>
</n-float-button-group>
</template>

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
const props = defineProps<{
title: string
message: string
showSwitch?: boolean
disabled?: boolean
}>()
const booleanValue = defineModel<boolean | undefined>('value', { required: false })
</script>
<template>
<n-card :title="props.title" class="default-card">
<template v-if="props.showSwitch" #header-extra>
<n-switch v-model:value="booleanValue" :disabled size="large" />
</template>
<n-p>{{ props.message }}</n-p>
<slot />
</n-card>
</template>
<style></style>

View File

@@ -0,0 +1,34 @@
<script lang="ts" setup>
import { MenuOption } from 'naive-ui'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const activeKey = ref('/main/home')
const router = useRouter()
const menuOptions: MenuOption[] = [
{
label: '主页',
key: '/main/home'
},
{
label: '工具',
children: [
{ label: '代码启动台', key: '/main/tools/CodeLaunchpad' },
{ label: '数据编解码', key: '/main/tools/DataCoder' }
]
},
{
label: '设置',
key: '/main/settings'
}
]
function handleUpdateValue(key: string): void {
router.push(key)
}
</script>
<template>
<n-menu v-model:value="activeKey" :options="menuOptions" @update:value="handleUpdateValue" />
</template>

View File

@@ -0,0 +1,40 @@
<script setup lang="ts">
import { reactive } from 'vue'
const versions = reactive({ ...window.electron.process.versions })
</script>
<template>
<ul class="versions">
<li class="electron-version">Electron v{{ versions.electron }}</li>
<li class="chrome-version">Chromium v{{ versions.chrome }}</li>
<li class="node-version">Node v{{ versions.node }}</li>
</ul>
</template>
<style>
.versions {
margin: 0 auto;
padding: 15px 0;
font-family: 'Menlo', 'Lucida Console', monospace;
display: inline-flex;
overflow: hidden;
align-items: center;
border-radius: 22px;
background-color: #202127;
backdrop-filter: blur(24px);
}
.versions li {
display: block;
float: left;
border-right: 1px solid var(--ev-c-gray-1);
padding: 0 20px;
font-size: 14px;
line-height: 14px;
opacity: 0.8;
&:last-child {
border: none;
}
}
</style>

2
src/renderer/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-svg-loader" />

43
src/renderer/src/main.ts Normal file
View File

@@ -0,0 +1,43 @@
import './assets/main.scss'
import naive from 'naive-ui'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createRouter, createWebHashHistory } from 'vue-router'
import App from './App.vue'
import MainPage from './pages/MainPage.vue'
import SettingsPage from './pages/SettingsPage.vue'
import CodeLaunchpadPage from './pages/CodeLaunchpadPage.vue'
import MainApp from '@renderer/MainApp.vue'
import CodeLaunchpadApp from '@renderer/CodeLaunchpadApp.vue'
import IDEs from '@renderer/pages-code-launchpad/IDEs.vue'
import Projects from '@renderer/pages-code-launchpad/Projects.vue'
import DataCoderPage from '@renderer/pages/DataCoderPage.vue'
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/main',
component: MainApp,
children: [
{ path: 'home', component: MainPage },
{ path: 'tools/CodeLaunchpad', component: CodeLaunchpadPage },
{ path: 'tools/DataCoder', component: DataCoderPage },
{ path: 'settings', component: SettingsPage }
]
},
{
path: '/codeLaunchpad',
component: CodeLaunchpadApp,
children: [
{ path: 'IDEs', component: IDEs },
{ path: 'projects', component: Projects }
]
}
]
})
const pinia = createPinia()
createApp(App).use(pinia).use(naive).use(router).mount('#app')

View File

@@ -0,0 +1,38 @@
<script lang="ts" setup>
import IDECard from '@renderer/components-code-launchpad/IDECard.vue'
import { useIDEs } from '@renderer/stores'
import { ideIcons } from '@my-type/ide-icons'
import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
const IDEs = useIDEs()
const { ides, versions } = storeToRefs(IDEs)
onMounted(() => {
IDEs.getIDEs()
IDEs.getVersions()
})
</script>
<template>
<n-flex size="small" vertical>
<div v-for="(ide, alia) in ides" :key="alia" class="ide-card">
<IDECard
v-if="ide !== null"
:alia
:icon="ideIcons[alia]['icon']"
:ide-info="ide"
:ide-version="versions[alia]"
>
{{ ideIcons[alia]['description'] }}
</IDECard>
</div>
</n-flex>
</template>
<style scoped>
div.ide-card {
margin: 0 4px;
}
</style>

View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template></template>
<style scoped></style>

View File

@@ -0,0 +1,29 @@
<script lang="ts" setup>
import { useIDEs } from '@renderer/stores'
import { onMounted } from 'vue'
import CodeLaunchpadPageView from '@renderer/pages/CodeLaunchpadPageView.vue'
import CodeLaunchpadPageIDEs from '@renderer/pages/CodeLaunchpadPageIDEs.vue'
import CodeLaunchpadPageDisplay from '@renderer/pages/CodeLaunchpadPageDisplay.vue'
const IDEs = useIDEs()
onMounted(() => {
IDEs.getIDEs()
IDEs.getVersions()
})
</script>
<template>
<n-h2>代码启动台</n-h2>
<n-alert title="这是什么?" type="info">
在你的 Windows 系统托盘中添加一个代码启动台允许你快速地运行 Visual StudioVSCode JetBrains
IDEs以及用它们打开代码项目<br />该工具的目标是提供一个适用性更广的 JetBrains Toolbox
</n-alert>
<n-alert title="本页面的工具设置需要保存" type="success"> 保存按钮在窗口右下角 </n-alert>
<CodeLaunchpadPageView />
<CodeLaunchpadPageDisplay />
<CodeLaunchpadPageIDEs />
</template>

View File

@@ -0,0 +1,63 @@
<script lang="ts" setup>
import SettingCard from '@renderer/components/SettingCard.vue'
import { useSettings } from '@renderer/stores'
import { storeToRefs } from 'pinia'
const { codeLaunchpadPosition, codeLaunchpadWidth, codeLaunchpadHeight } =
storeToRefs(useSettings())
const positionOptions = [
{
label: '左上',
value: 'left top'
},
{
label: '左下',
value: 'left bottom'
},
{
label: '右上',
value: 'right top'
},
{
label: '右下',
value: 'right bottom'
}
]
</script>
<template>
<n-h3>显示</n-h3>
<n-p>也许你想要稍微定制一下代码启动台窗口的显示效果</n-p>
<setting-card message="默认效果我觉得就很奈斯。" title="代码启动台窗口定制">
<n-flex vertical>
<n-card title="窗口位置">
<template #header-extra>
<n-select
v-model:value="codeLaunchpadPosition"
:options="positionOptions"
class="card-header-extra-input"
/>
</template>
</n-card>
<n-card title="窗口尺寸">
<template #header-extra>
<n-flex>
<n-input-number
v-model:value="codeLaunchpadWidth"
class="card-header-extra-input"
placeholder="width"
/>
<n-input-number
v-model:value="codeLaunchpadHeight"
class="card-header-extra-input"
placeholder="height"
/>
</n-flex>
</template>
</n-card>
</n-flex>
</setting-card>
</template>
<style scoped></style>

View File

@@ -0,0 +1,83 @@
<script lang="ts" setup>
import DetectedIDECardList from '@renderer/components/DetectedIDECardList.vue'
import { useIDEs, useSettings } from '@renderer/stores'
import { storeToRefs } from 'pinia'
const IDEs = useIDEs()
const { ides, versions } = storeToRefs(IDEs)
const { codeLaunchpadIDESearchPolicy } = storeToRefs(useSettings())
const IDESearchMethodOptions = [
{
label: 'where.exe 命令',
value: 'where.exe'
},
{
label: 'JetBrains Toolbox state.json',
value: 'JBTState.json'
},
{
label: '全盘搜索安装目录',
value: 'just.search'
}
]
</script>
<template>
<n-h3>IDE设置</n-h3>
<n-p>你可能需要检查工具箱是否成功识别了你已安装的 IDE以及在识别失败时手动指定</n-p>
<n-card title="已安装的 IDE">
<template #header-extra>
<n-button
@click="
() => {
IDEs.checkIDEs()
IDEs.checkVersions()
}
"
>重新检测</n-button
>
</template>
<template #default>
<DetectedIDECardList :ides="ides" :versions />
</template>
<template #action>
<n-p
>工具箱使用一些不受到官方支持的方法来获取已安装的 IDE
以及其版本这是因为并没有能够达到相同目的的官方解决方案</n-p
>
<n-p>
工具箱需要连接到互联网以从在线接口获取已安装的 IDE 的最新版本如果获取失败将显示
<n-tag type="warning">最新 unknown</n-tag>
</n-p>
</template>
</n-card>
<n-p>你期望工具箱如何查找已安装的 IDE 以及识别安装的版本</n-p>
<n-card class="default-card" title="IDE 本地查找策略">
<template #header-extra>
<n-select
v-model:value="codeLaunchpadIDESearchPolicy"
:options="IDESearchMethodOptions"
class="card-header-extra-input-large"
multiple
/>
</template>
<template #default>
<n-p>
<n-code inline>where.exe</n-code>
命令可用于查找可执行文件的位置但需求环境变量正确设置
</n-p>
<n-p>
JetBrains Toolbox 会在其数据目录下的
<n-code inline>state.json</n-code>
中存储已安装的所有 JetBrains IDE 的信息但需要你使用其来管理你的 JetBrains IDEs也仅支持
JetBrains IDEs
</n-p>
<n-p>全盘搜索正如其名理论上不会遗漏但不够稳定和快速</n-p>
<n-p>对于查找到的每一个 IDE工具箱将从合适的网络接口尝试获取其最新版本</n-p>
</template>
</n-card>
</template>
<style scoped></style>

View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
import SettingCard from '@renderer/components/SettingCard.vue'
import { storeToRefs } from 'pinia'
import { useSettings } from '@renderer/stores'
import { useMessage } from 'naive-ui'
const message = useMessage()
const {
isCodeLaunchpadEnabled,
isCodeLaunchpadInTray,
isCodeLaunchpadShortcutEnabled,
codeLaunchpadShortcut
} = storeToRefs(useSettings())
const keyShortcutOptions = [
{
label: 'Alt + C',
value: 'alt+c'
},
{
label: 'Alt + Space',
value: 'alt+space'
}
]
async function openCodeLaunchpad(): Promise<void> {
if (await window.api._openCodeLaunchpad()) {
message.success('如你所愿,代码启动台已开启。')
} else {
message.error('啊哦?出问题了,请检查。')
}
}
async function closeCodeLaunchpad(): Promise<void> {
if (await window.api._closeCodeLaunchpad()) {
message.success('如你所愿,代码启动台已关闭。')
} else {
message.error('代码启动台真的打开了吗?')
}
}
</script>
<template>
<n-h3>总览</n-h3>
<setting-card
v-model:value="isCodeLaunchpadEnabled"
message="总开关"
show-switch
title="代码启动台"
>
<n-space>
<n-button @click="openCodeLaunchpad">我现在就要打开代码启动台</n-button>
<n-button @click="closeCodeLaunchpad">关闭已打开的代码启动台</n-button>
</n-space>
</setting-card>
<n-p>使用上面的按钮打开的代码启动台只能通过上面的按钮再将其关闭方便你测试效果</n-p>
<n-p>除此之外使用下面的托盘图标或快捷键打开的代码启动台都将在失去焦点后自动关闭</n-p>
<setting-card
v-model:value="isCodeLaunchpadInTray"
message="工具箱启动后,在系统托盘显示代码启动台图标"
show-switch
title="在 Windows 系统托盘显示"
/>
<n-p>如果不在系统托盘显示则只能通过快捷键打开代码启动台</n-p>
<n-p>我感觉还是在系统托盘显示比较方便</n-p>
<setting-card
v-model:value="isCodeLaunchpadShortcutEnabled"
message="通过此全局快捷键让你能在任何地方唤起代码启动台,尽量避免与其他软件的快捷键产生冲突"
show-switch
title="唤起快捷键"
>
<n-p
>设置一个快捷键吧我觉得这些按键比较顺手由于我还不是很会写这种东西所以暂时不让你自己设置</n-p
>
<n-select v-model:value="codeLaunchpadShortcut" :options="keyShortcutOptions" />
</setting-card>
<n-p>如果不设置或禁用唤起快捷键则只能通过系统托盘图标打开代码启动台</n-p>
<n-p>那如果我两个都禁用呢那你不如把总开关一起关掉算惹</n-p>
</template>
<style scoped></style>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import CoderJson from '@renderer/components/CoderJson.vue'
</script>
<template>
<n-h2>数据编解码</n-h2>
<n-alert title="这是什么?" type="info">
一个简单的用来编解码/序列化反序列化数据的小工具可能有用<br />
此工具没有配置需求一般来说此工具不能完全代替其他专业的软件和工具
</n-alert>
<CoderJson />
</template>
<style scoped></style>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup></script>
<template>
<n-h1 prefix="bar"><n-text type="success">FanTools - 芒果工具箱</n-text></n-h1>
<n-h2>主页</n-h2>
<n-p
>FanTools或者芒果工具箱是一个使用 Electron 技术开发的 Windows
桌面应用程序提供一些工具功能从名称上也能看得出来</n-p
>
<n-p>本窗口是工具箱本体一些工具直接在本窗口中工作另有一些功能拥有它们的独立窗口</n-p>
<n-p
>窗口右下角的浮动按钮是<n-text strong type="success">保存</n-text
>设置需要保存之后才能生效部分设置还需要重启工具箱来生效如果你改动了设置记得保存</n-p
>
</template>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import SettingCard from '@renderer/components/SettingCard.vue'
import { useSettings } from '@renderer/stores'
const settings = useSettings()
</script>
<template>
<n-h2>设置</n-h2>
<n-card title="激活芒果工具箱!">
<template #header-extra>
<n-tag type="success">重要</n-tag>
</template>
<template #default>
<n-p>为运行工具箱提供的功能工具箱需要在后台存活以及设置开机启动</n-p>
<n-p>我将之称为<n-text strong type="success">激活</n-text></n-p>
<setting-card
v-model:value="settings.isStayInTray"
message="关闭工具箱本体窗口(也就是此窗口)时,不退出工具箱"
show-switch
title="保留工具箱在系统托盘中"
/>
<setting-card
v-model:value="settings.isAutoLaunchAtStartUp"
:disabled="!settings.isStayInTray"
message="工具箱本体窗口不会打开,只有托盘图标会被添加,必须在允许保留系统托盘时有效"
show-switch
title="开机时自动启动"
/>
</template>
</n-card>
</template>

103
src/renderer/src/stores.ts Normal file
View File

@@ -0,0 +1,103 @@
import { defineStore } from 'pinia'
import { ref, toRaw } from 'vue'
import type {
checkIDEsResultDto,
checkIDEsVersionDto,
ideSearchPolicy,
keyboardShortcut,
screenPosition
} from '@my-type/settings'
export const useSettings = defineStore('settings', () => {
const isStayInTray = ref(false)
const isAutoLaunchAtStartUp = ref(false)
const isCodeLaunchpadEnabled = ref(false)
const isCodeLaunchpadInTray = ref(false)
const isCodeLaunchpadShortcutEnabled = ref(false)
const codeLaunchpadShortcut = ref<keyboardShortcut>('alt+c')
const codeLaunchpadPosition = ref<screenPosition>('right bottom')
const codeLaunchpadWidth = ref(460)
const codeLaunchpadHeight = ref(760)
const codeLaunchpadIDESearchPolicy = ref<ideSearchPolicy[]>(['where.exe', 'JBTState.json'])
/**
* 保存当前 store 中存储的设置。不改变当前 store。
* @returns 布尔值,表明是否成功。
*/
async function save(): Promise<boolean> {
return await window.api._saveSettings({
isStayInTray: isStayInTray.value,
isAutoLaunchAtStartUp: isAutoLaunchAtStartUp.value,
isCodeLaunchpadEnabled: isCodeLaunchpadEnabled.value,
isCodeLaunchpadInTray: isCodeLaunchpadInTray.value,
isCodeLaunchpadShortcutEnabled: isCodeLaunchpadShortcutEnabled.value,
codeLaunchpadShortcut: codeLaunchpadShortcut.value,
codeLaunchpadPosition: codeLaunchpadPosition.value,
codeLaunchpadWidth: codeLaunchpadWidth.value,
codeLaunchpadHeight: codeLaunchpadHeight.value,
codeLaunchpadIDESearchPolicy: toRaw(codeLaunchpadIDESearchPolicy.value)
})
}
/**
* 从主进程更新保存在硬盘中的设置,直接修改该 store 中的设置,相当于撤销未保存的更改。
* @returns 布尔值,表明是否成功。
*/
async function update(): Promise<boolean> {
const result = await window.api._updateSettings()
isStayInTray.value = result.isStayInTray
isAutoLaunchAtStartUp.value = result.isAutoLaunchAtStartUp
isCodeLaunchpadEnabled.value = result.isCodeLaunchpadEnabled
isCodeLaunchpadInTray.value = result.isCodeLaunchpadInTray
isCodeLaunchpadShortcutEnabled.value = result.isCodeLaunchpadShortcutEnabled
codeLaunchpadShortcut.value = result.codeLaunchpadShortcut
codeLaunchpadPosition.value = result.codeLaunchpadPosition
codeLaunchpadWidth.value = result.codeLaunchpadWidth
codeLaunchpadHeight.value = result.codeLaunchpadHeight
codeLaunchpadIDESearchPolicy.value = result.codeLaunchpadIDESearchPolicy
return true
}
return {
isStayInTray,
isAutoLaunchAtStartUp,
isCodeLaunchpadEnabled,
isCodeLaunchpadInTray,
isCodeLaunchpadShortcutEnabled,
codeLaunchpadShortcut,
codeLaunchpadPosition,
codeLaunchpadWidth,
codeLaunchpadHeight,
codeLaunchpadIDESearchPolicy,
save,
update
}
})
export const useIDEs = defineStore('IDEs', () => {
const ides = ref<checkIDEsResultDto>({})
const versions = ref<checkIDEsVersionDto>({})
async function getIDEs(): Promise<void> {
ides.value = await window.codeLaunchpad._getIDEs()
for (const key in ides.value) {
if (versions.value[key] === undefined) {
versions.value[key] = null
}
}
}
async function checkIDEs(): Promise<void> {
ides.value = await window.codeLaunchpad._checkIDEs()
}
async function getVersions(): Promise<void> {
versions.value = await window.codeLaunchpad._getIDEsVersion()
}
async function checkVersions(): Promise<void> {
versions.value = await window.codeLaunchpad._checkIDEsVersion()
}
return { ides, versions, getIDEs, checkIDEs, getVersions, checkVersions }
})

11
tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.web.json"
}
]
}

22
tsconfig.node.json Normal file
View File

@@ -0,0 +1,22 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": [
"electron.vite.config.*",
"src/main/**/*",
"src/preload/**/*",
"src/my-type/*.d.ts",
"src/my-type/*.ts"
],
"compilerOptions": {
"composite": true,
"types": [
"electron-vite/node"
],
"baseUrl": ".",
"paths": {
"@my-type/*": [
"src/my-type/*"
]
}
}
}

32
tsconfig.web.json Normal file
View File

@@ -0,0 +1,32 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
"include": [
"src/renderer/src/env.d.ts",
"src/renderer/src/**/*",
"src/renderer/src/**/*.vue",
"src/preload/*.d.ts",
"src/my-type/*.d.ts",
"src/my-type/*.ts"
],
"compilerOptions": {
"strict": true,
"composite": true,
"plugins": [
{
"name": "@vue/typescript-plugin"
}
],
"types": [
"naive-ui/volar"
],
"baseUrl": ".",
"paths": {
"@renderer/*": [
"src/renderer/src/*"
],
"@my-type/*": [
"src/my-type/*"
]
}
}
}