This commit is contained in:
lincube
2026-02-13 17:01:37 +08:00
commit ef1ccd439b
88 changed files with 14682 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

1
.eslintcache Normal file

File diff suppressed because one or more lines are too long

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

26
.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
# CakePHP 3
/vendor/*
/config/app.php
/tmp/cache/models/*
!/tmp/cache/models/empty
/tmp/cache/persistent/*
!/tmp/cache/persistent/empty
/tmp/cache/views/*
!/tmp/cache/views/empty
/tmp/sessions/*
!/tmp/sessions/empty
/tmp/tests/*
!/tmp/tests/empty
/logs/*
!/logs/empty
# CakePHP 2
/app/tmp/*
/app/Config/core.php
/app/Config/database.php
/vendors/*
node_modules

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
shamefully-hoist=true

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

0
.trae/.ignore Normal file
View File

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
}
}
]
}

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

@@ -0,0 +1,11 @@
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

34
README.md Normal file
View File

@@ -0,0 +1,34 @@
# desktop
An Electron application with React and TypeScript
## Recommended IDE Setup
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
## Project Setup
### Install
```bash
$ pnpm install
```
### Development
```bash
$ pnpm dev
```
### Build
```bash
# For windows
$ pnpm build:win
# For macOS
$ pnpm build:mac
# For Linux
$ pnpm build:linux
```

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>

BIN
build/icon.icns Normal file

Binary file not shown.

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

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

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

45
electron-builder.yml Normal file
View File

@@ -0,0 +1,45 @@
appId: com.electron.app
productName: desktop
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: desktop
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/

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

@@ -0,0 +1,16 @@
import { resolve } from 'path'
import { defineConfig } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
main: {},
preload: {},
renderer: {
resolve: {
alias: {
'@renderer': resolve('src/renderer/src')
}
},
plugins: [vue()]
}
})

9
eslint.config.mjs Normal file
View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'eslint/config'
import tseslint from '@electron-toolkit/eslint-config-ts'
import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier'
export default defineConfig(
{ ignores: ['**/node_modules', '**/dist', '**/out', '**/*.vue'] },
tseslint.configs.recommended,
eslintConfigPrettier
)

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="currentColor" d="M5.102 3a.5.5 0 0 0-.433.25l-2.598 4.5a.5.5 0 0 0 0 .5l2.504 4.337a.827.827 0 0 0 1.504-.165l2.89-9.146a1.826 1.826 0 0 1 3.322-.363l2.504 4.337a1.5 1.5 0 0 1 0 1.5l-2.598 4.5a1.5 1.5 0 0 1-1.299.75H8.502a.5.5 0 0 1 0-1h2.396a.5.5 0 0 0 .433-.25l2.598-4.5a.5.5 0 0 0 0-.5l-2.504-4.337a.826.826 0 0 0-1.503.164l-2.89 9.146a1.827 1.827 0 0 1-3.323.364L1.205 8.75a1.5 1.5 0 0 1 0-1.5l2.598-4.5A1.5 1.5 0 0 1 5.102 2h2.4a.5.5 0 0 1 0 1z"/></svg>

After

Width:  |  Height:  |  Size: 553 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M9.08 18a3 3 0 0 0 .67-1.238l3.4-12.754A.01.01 0 0 1 13.158 4l.001-.004l-.006.004c.066-.203-.009-.53-.313-.602c-.357-.084-.555.092-.635.285l-.02.068l-3.402 12.753a2.015 2.015 0 0 1-3.652.556l-3.863-6.126a1.75 1.75 0 0 1 0-1.868l3.941-6.25A1.75 1.75 0 0 1 6.69 2h4.23a3 3 0 0 0-.667 1.235L6.85 15.99l-.005.007l-.002.006l.004-.002c-.067.203.008.53.312.602c.355.084.553-.09.634-.282q.013-.036.023-.074l3.401-12.753a2.01 2.01 0 0 1 3.645-.554l.003.005l.002.004l3.92 6.297a1.43 1.43 0 0 1 0 1.51l-3.996 6.42a1.75 1.75 0 0 1-1.486.825z"/></svg>

After

Width:  |  Height:  |  Size: 650 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M4.5 17a1.5 1.5 0 0 1-1.493-1.355L3 15.501v-11a1.5 1.5 0 0 1 1.356-1.493L4.5 3H9a1.5 1.5 0 0 1 1.493 1.355l.007.145v.254l2.189-2.269a1.5 1.5 0 0 1 2.007-.138l.116.101l2.757 2.725a1.5 1.5 0 0 1 .111 2.011l-.103.116l-2.311 2.2h.234a1.5 1.5 0 0 1 1.493 1.356L17 11v4.5a1.5 1.5 0 0 1-1.355 1.493L15.5 17zm5-6.5H4v5a.5.5 0 0 0 .326.47l.084.023l.09.008h5zm6 0h-5V16h5a.5.5 0 0 0 .492-.41L16 15.5V11a.5.5 0 0 0-.41-.491zm-5-2.79V9.5h1.79zM9 4H4.5a.5.5 0 0 0-.492.411L4 4.501v5h5.5v-5a.5.5 0 0 0-.326-.469L9.09 4.01zm5.122-.826a.5.5 0 0 0-.645-.053l-.068.06l-2.616 2.713a.5.5 0 0 0-.057.623l.063.078l2.616 2.615a.5.5 0 0 0 .62.07l.078-.061l2.758-2.627a.5.5 0 0 0 .054-.638l-.059-.069z"/></svg>

After

Width:  |  Height:  |  Size: 797 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M16 10A6 6 0 0 0 5.528 6H7.5a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5v-3a.5.5 0 0 1 1 0v1.601a7 7 0 1 1-1.98 4.361a.5.5 0 0 1 .998.076A6 6 0 1 0 16 10"/></svg>

After

Width:  |  Height:  |  Size: 265 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M12.5 17a.5.5 0 0 0 0-1H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h6.5a.5.5 0 0 0 0-1H6a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3zm1.146-10.854a.5.5 0 0 1 .708 0l3.5 3.5a.5.5 0 0 1 0 .708l-3.5 3.5a.5.5 0 0 1-.708-.708l2.647-2.646H7.5a.5.5 0 0 1 0-1h8.793l-2.647-2.646a.5.5 0 0 1 0-.708"/></svg>

After

Width:  |  Height:  |  Size: 383 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M10 2a8 8 0 1 1-3.613 15.14l-.121-.065l-3.645.91a.5.5 0 0 1-.62-.441v-.082l.014-.083l.91-3.644l-.063-.12a8 8 0 0 1-.83-2.887l-.025-.382L2 10q.001-.823.16-1.599V8.4a.506.506 0 0 1 .615-.394c.307.08.413.394.353.671l-.012.049a7.04 7.04 0 0 0 .778 4.7a.5.5 0 0 1 .063.272l-.014.094l-.756 3.021l3.024-.754a.5.5 0 0 1 .188-.01l.091.021l.087.039A7 7 0 1 0 4.255 6H6.5a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5v-3a.5.5 0 0 1 1 0v1.208A7.98 7.98 0 0 1 10 2m0 5.5a.5.5 0 0 0-1 0v4a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 0-1H10z"/></svg>

After

Width:  |  Height:  |  Size: 621 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M8.731 17.174a2.5 2.5 0 0 1 0-3.536l4.9-4.9a2.5 2.5 0 0 1 3.536 0l.8.8a7.995 7.995 0 1 0-8.441 8.44zm7.737-7.734l2.095 2.1a1.5 1.5 0 0 1 0 2.122l-3.6 3.6l-4.216-4.217l3.6-3.6a1.5 1.5 0 0 1 2.122 0zm-2.212 8.523l-4.216-4.217l-.6.6a1.5 1.5 0 0 0 0 2.122l2.1 2.095a1.5 1.5 0 0 0 1.117.438h4.092a.5.5 0 0 0 0-1h-2.531z"/></svg>

After

Width:  |  Height:  |  Size: 435 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M8.732 17.173a2.5 2.5 0 0 1-.288-.353A7.01 7.01 0 0 1 3 10a7.01 7.01 0 0 1 7-7a7.01 7.01 0 0 1 6.819 5.441q.19.13.354.29l.8.8A8 8 0 0 0 10 2a8.01 8.01 0 0 0-8 8a8 8 0 0 0 7.535 7.976zm.707-.707a1.5 1.5 0 0 1 0-2.122l4.9-4.9a1.5 1.5 0 0 1 2.122 0l2.095 2.095a1.5 1.5 0 0 1 0 2.122L14.216 18h2.531a.5.5 0 0 1 0 1h-4.092a1.5 1.5 0 0 1-1.121-.438zm5.612-6.319l-3.6 3.6l2.8 2.8l3.6-3.6a.5.5 0 0 0 0-.708l-2.095-2.095a.5.5 0 0 0-.705.003m-1.5 7.108l-2.8-2.8l-.6.6a.5.5 0 0 0 0 .708l2.095 2.095a.5.5 0 0 0 .708 0z"/></svg>

After

Width:  |  Height:  |  Size: 627 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M6.255 3.579a.478.478 0 0 0-.706-.227a8 8 0 0 0-2.2 2.2a.478.478 0 0 0 .228.706a.53.53 0 0 0 .638-.2a7 7 0 0 1 1.84-1.84a.53.53 0 0 0 .2-.639m2.44-.458a.53.53 0 0 1-.593-.31a.478.478 0 0 1 .338-.659a8 8 0 0 1 3.115-.001c.298.059.454.38.338.66a.53.53 0 0 1-.594.31a7 7 0 0 0-2.603 0m7.727 3.135a.53.53 0 0 1-.638-.2a7 7 0 0 0-1.842-1.842a.53.53 0 0 1-.201-.638a.478.478 0 0 1 .706-.228A8 8 0 0 1 16.65 5.55c.17.253.054.59-.227.706m.766 5.64a.53.53 0 0 1-.309-.593a7 7 0 0 0 0-2.606a.53.53 0 0 1 .31-.593c.28-.117.6.04.66.338a8 8 0 0 1 0 3.116a.478.478 0 0 1-.66.338m-9.086 5.292a.53.53 0 0 1 .594-.31a7 7 0 0 0 2.305.05v1.01a8 8 0 0 1-2.56-.09a.478.478 0 0 1-.339-.66m-4.525-3.446a.53.53 0 0 1 .638.2a7 7 0 0 0 1.84 1.841a.53.53 0 0 1 .2.638a.478.478 0 0 1-.706.228a8 8 0 0 1-2.2-2.202a.478.478 0 0 1 .228-.705M2.81 8.106a.53.53 0 0 1 .31.593a7 7 0 0 0 0 2.601a.53.53 0 0 1-.31.594a.478.478 0 0 1-.66-.338a8 8 0 0 1 0-3.112a.478.478 0 0 1 .66-.338m10.045 2.04a.5.5 0 0 0-.852.354l-.003 8a.5.5 0 0 0 .9.301l1.994-2.647l3.497.776a.5.5 0 0 0 .46-.843z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16m0 1a7 7 0 1 0 0 14a7 7 0 0 0 0-14m-.5 2a.5.5 0 0 1 .492.41L10 5.5V10h2.5a.5.5 0 0 1 .09.992L12.5 11h-3a.5.5 0 0 1-.492-.41L9 10.5v-5a.5.5 0 0 1 .5-.5"/></svg>

After

Width:  |  Height:  |  Size: 306 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M6.636 2.287A1 1 0 0 0 5 3.059v13.998c0 .927 1.15 1.355 1.756.655l3.524-4.073a1.5 1.5 0 0 1 1.134-.518h5.592c.938 0 1.36-1.176.636-1.772z"/></svg>

After

Width:  |  Height:  |  Size: 258 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M5 3.059a1 1 0 0 1 1.636-.772l11.006 9.062c.724.596.302 1.772-.636 1.772h-5.592a1.5 1.5 0 0 0-1.134.518l-3.524 4.073c-.606.7-1.756.271-1.756-.655zm12.006 9.062L6 3.059v13.998l3.524-4.072a2.5 2.5 0 0 1 1.89-.864z"/></svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M4 5c0-1.007.875-1.755 1.904-2.223C6.978 2.289 8.427 2 10 2s3.022.289 4.096.777C15.125 3.245 16 3.993 16 5v4.041a3 3 0 0 0-1 0V6.698a5 5 0 0 1-.904.525C13.022 7.711 11.573 8 10 8s-3.022-.289-4.096-.777A5 5 0 0 1 5 6.698V15c0 .374.356.875 1.318 1.313c.916.416 2.218.687 3.682.687q.533 0 1.031-.046c.047.334.146.663.3.975Q10.686 18 10 18c-1.573 0-3.022-.289-4.096-.777C4.875 16.755 4 16.007 4 15zm1 0c0 .374.356.875 1.318 1.313C7.234 6.729 8.536 7 10 7s2.766-.27 3.682-.687C14.644 5.875 15 5.373 15 5c0-.374-.356-.875-1.318-1.313C12.766 3.271 11.464 3 10 3s-2.766.27-3.682.687C5.356 4.125 5 4.627 5 5m12.5 7a2 2 0 1 1-4 0a2 2 0 0 1 4 0m1.5 4.5c0 1.245-1 2.5-3.5 2.5S12 17.75 12 16.5a1.5 1.5 0 0 1 1.5-1.5h4a1.5 1.5 0 0 1 1.5 1.5"/></svg>

After

Width:  |  Height:  |  Size: 847 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M10 7a3 3 0 1 0 0 6a3 3 0 0 0 0-6m-2 3a2 2 0 1 1 4 0a2 2 0 0 1-4 0m-.5-8a.5.5 0 0 1 .5.5V4h1.5V2.5a.5.5 0 0 1 1 0V4H12V2.5a.5.5 0 0 1 1 0V4h.5A2.5 2.5 0 0 1 16 6.5V7h1.5a.5.5 0 0 1 0 1H16v1.5h1.5a.5.5 0 0 1 0 1H16V12h1.5a.5.5 0 0 1 0 1H16v.5a2.5 2.5 0 0 1-2.5 2.5H13v1.5a.5.5 0 0 1-1 0V16h-1.5v1.5a.5.5 0 0 1-1 0V16H8v1.5a.5.5 0 0 1-1 0V16h-.5A2.5 2.5 0 0 1 4 13.5V13H2.5a.5.5 0 0 1 0-1H4v-1.5H2.5a.5.5 0 0 1 0-1H4V8H2.5a.5.5 0 0 1 0-1H4v-.5A2.5 2.5 0 0 1 6.5 4H7V2.5a.5.5 0 0 1 .5-.5M15 6.5A1.5 1.5 0 0 0 13.5 5h-7A1.5 1.5 0 0 0 5 6.5v7A1.5 1.5 0 0 0 6.5 15h7a1.5 1.5 0 0 0 1.5-1.5z"/></svg>

After

Width:  |  Height:  |  Size: 704 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M11.197 2.44a1.5 1.5 0 0 1 2.121 0l4.243 4.242a1.5 1.5 0 0 1 0 2.121L9.364 17H14.5a.5.5 0 1 1 0 1H7.82a1.5 1.5 0 0 1-1.14-.437L2.437 13.32a1.5 1.5 0 0 1 0-2.121zM9.781 15.168l-4.95-4.95l-1.687 1.687a.5.5 0 0 0 0 .707l4.243 4.243a.5.5 0 0 0 .707 0z"/></svg>

After

Width:  |  Height:  |  Size: 368 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M11.197 2.44a1.5 1.5 0 0 1 2.121 0l4.243 4.242a1.5 1.5 0 0 1 0 2.121L9.364 17H14.5a.5.5 0 1 1 0 1H7.82a1.5 1.5 0 0 1-1.14-.437L2.437 13.32a1.5 1.5 0 0 1 0-2.121zm1.414.706a.5.5 0 0 0-.707 0L5.538 9.512l4.95 4.95l6.366-6.366a.5.5 0 0 0 0-.707zM9.781 15.17l-4.95-4.95l-1.687 1.687a.5.5 0 0 0 0 .707l4.243 4.243a.5.5 0 0 0 .707 0z"/></svg>

After

Width:  |  Height:  |  Size: 448 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M2.44 11.2a1.5 1.5 0 0 0 0 2.122l4.242 4.242a1.5 1.5 0 0 0 2.121 0l.72-.72a5.5 5.5 0 0 1-.369-1.045l-1.058 1.058a.5.5 0 0 1-.707 0l-4.243-4.242a.5.5 0 0 1 0-.707l1.69-1.69l4.165 4.164a5.5 5.5 0 0 1 7.843-4.859l.72-.72a1.5 1.5 0 0 0 0-2.121l-4.242-4.243a1.5 1.5 0 0 0-2.122 0zM14.5 19a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9"/></svg>

After

Width:  |  Height:  |  Size: 440 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M2.44 11.2a1.5 1.5 0 0 0 0 2.122l4.242 4.242a1.5 1.5 0 0 0 2.121 0l.72-.72a5.5 5.5 0 0 1-.369-1.045l-1.058 1.058a.5.5 0 0 1-.707 0l-4.243-4.242a.5.5 0 0 1 0-.707l1.69-1.69l4.165 4.164q.015-.645.17-1.245L5.543 9.51l6.364-6.364a.5.5 0 0 1 .707 0l4.242 4.243a.5.5 0 0 1 0 .707L15.8 9.154a5.5 5.5 0 0 1 1.045.37l.72-.72a1.5 1.5 0 0 0 0-2.122l-4.242-4.243a1.5 1.5 0 0 0-2.122 0zM14.5 19a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9"/></svg>

After

Width:  |  Height:  |  Size: 538 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M10 4a6 6 0 1 1-5.982 5.538a.5.5 0 1 0-.998-.076Q3 9.73 3 10a7 7 0 1 0 2-4.899V3.5a.5.5 0 0 0-1 0v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 0-1H5.528A5.98 5.98 0 0 1 10 4m0 2.5a.5.5 0 0 0-1 0v4a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 0-1H10z"/></svg>

After

Width:  |  Height:  |  Size: 340 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M3.5 4A1.5 1.5 0 0 0 2 5.5v8A1.5 1.5 0 0 0 3.5 15h13a1.5 1.5 0 0 0 1.5-1.5v-8A1.5 1.5 0 0 0 16.5 4zm2.755 3.252a.752.752 0 1 1-1.505 0a.752.752 0 0 1 1.505 0m6 0a.752.752 0 1 1-1.505 0a.752.752 0 0 1 1.505 0M5 12.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m9.502-4.495a.752.752 0 1 1 0-1.505a.752.752 0 0 1 0 1.505m-7.504 2.5a.752.752 0 1 1 0-1.505a.752.752 0 0 1 0 1.505m3.757-.753a.752.752 0 1 1-1.505 0a.752.752 0 0 1 1.505 0m2.252.753a.752.752 0 1 1 0-1.505a.752.752 0 0 1 0 1.505M9.255 7.252a.752.752 0 1 1-1.505 0a.752.752 0 0 1 1.505 0"/></svg>

After

Width:  |  Height:  |  Size: 675 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M5 12.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m6.502-4.495a.752.752 0 1 0 0-1.505a.752.752 0 0 0 0 1.505m3.753-.753a.752.752 0 1 1-1.505 0a.752.752 0 0 1 1.505 0m-9.753.753a.752.752 0 1 0 0-1.505a.752.752 0 0 0 0 1.505M7.75 9.752a.752.752 0 1 1-1.505 0a.752.752 0 0 1 1.505 0m2.252.753a.752.752 0 1 0 0-1.505a.752.752 0 0 0 0 1.505m3.757-.753a.752.752 0 1 1-1.504 0a.752.752 0 0 1 1.504 0M8.503 8.005a.752.752 0 1 0 0-1.505a.752.752 0 0 0 0 1.505M2 5.5A1.5 1.5 0 0 1 3.5 4h13A1.5 1.5 0 0 1 18 5.5v8a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 2 13.5zM3.5 5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5z"/></svg>

After

Width:  |  Height:  |  Size: 763 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M5 2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zM4 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm9-2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zm-1 2a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1z"/></svg>

After

Width:  |  Height:  |  Size: 395 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M12.92 2.873a2.975 2.975 0 0 1 4.207 4.207l-.67.67l.336.336a2 2 0 0 1 0 2.828l-.94.94a.5.5 0 0 1-.707-.708l.94-.939a1 1 0 0 0 0-1.414l-.336-.336l-7.98 7.981a2.5 2.5 0 0 1-1.235.678l-3.926.873a.5.5 0 0 1-.597-.597l.878-3.95c.1-.452.328-.867.655-1.194z"/></svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M17.18 2.926a2.975 2.975 0 0 0-4.26-.054l-9.375 9.375a2.44 2.44 0 0 0-.655 1.194l-.878 3.95a.5.5 0 0 0 .597.597l3.926-.873a2.5 2.5 0 0 0 1.234-.678l7.98-7.98l.337.336a1 1 0 0 1 0 1.414l-.94.94a.5.5 0 0 0 .708.706l.939-.94a2 2 0 0 0 0-2.828l-.336-.336l.67-.67a2.975 2.975 0 0 0 .052-4.153m-3.553.653a1.975 1.975 0 0 1 2.793 2.793L7.062 15.73a1.5 1.5 0 0 1-.744.409l-3.16.702l.708-3.183a1.43 1.43 0 0 1 .387-.704z"/></svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M8.5 9A1.5 1.5 0 0 0 10 7.5v-4A1.5 1.5 0 0 0 8.5 2h-6A1.5 1.5 0 0 0 1 3.5v4A1.5 1.5 0 0 0 2.5 9zm0 1h-6q-.257 0-.5-.05v4.3A2.75 2.75 0 0 0 4.75 17h10.5A2.75 2.75 0 0 0 18 14.25v-6.5A2.75 2.75 0 0 0 15.25 5H11v2.5A2.5 2.5 0 0 1 8.5 10m3.854.646L15 13.293V11.5a.5.5 0 0 1 1 0v3.003a.5.5 0 0 1-.5.497h-3a.5.5 0 0 1 0-1h1.793l-2.647-2.646a.5.5 0 0 1 .708-.708"/></svg>

After

Width:  |  Height:  |  Size: 476 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M8.5 9A1.5 1.5 0 0 0 10 7.5v-4A1.5 1.5 0 0 0 8.5 2h-6A1.5 1.5 0 0 0 1 3.5v4a1.5 1.5 0 0 0 1 1.415l.019.006c.15.051.313.079.481.079zm6.75-3H11V5h4.25A2.75 2.75 0 0 1 18 7.75v6.5A2.75 2.75 0 0 1 15.25 17H4.75A2.75 2.75 0 0 1 2 14.25v-4.3q.243.05.5.05H3v4.25c0 .966.784 1.75 1.75 1.75h10.5A1.75 1.75 0 0 0 17 14.25v-6.5A1.75 1.75 0 0 0 15.25 6M14 12.293l-2.646-2.647a.5.5 0 0 0-.708.708L13.293 13H11.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 .5-.497V10.5a.5.5 0 0 0-1 0z"/></svg>

After

Width:  |  Height:  |  Size: 576 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M13.325 2.617a2 2 0 0 0-3.203.52l-1.73 3.459a1.5 1.5 0 0 1-.784.721l-3.59 1.436a1 1 0 0 0-.335 1.636L6.293 13L3 16.292V17h.707L7 13.706l2.61 2.61a1 1 0 0 0 1.636-.335l1.436-3.59a1.5 1.5 0 0 1 .722-.784l3.458-1.73a2 2 0 0 0 .52-3.203z"/></svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M10.122 3.137a2 2 0 0 1 3.203-.52l4.057 4.057a2 2 0 0 1-.52 3.203l-3.458 1.73a1.5 1.5 0 0 0-.722.784l-1.436 3.59a1 1 0 0 1-1.636.336L7 13.707l-3.293 3.292H3v-.707L6.293 13l-2.61-2.61a1 1 0 0 1 .335-1.636l3.59-1.436a1.5 1.5 0 0 0 .785-.721zm2.496.187a1 1 0 0 0-1.601.26l-1.73 3.459A2.5 2.5 0 0 1 7.98 8.246L4.39 9.682l5.927 5.928l1.436-3.59a2.5 2.5 0 0 1 1.204-1.308l3.458-1.73a1 1 0 0 0 .26-1.6z"/></svg>

After

Width:  |  Height:  |  Size: 516 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M17.22 8.687a1.498 1.498 0 0 1 0 2.626l-9.997 5.499A1.5 1.5 0 0 1 5 15.499V4.501a1.5 1.5 0 0 1 2.223-1.313zm-.482 1.75a.5.5 0 0 0 0-.875L6.741 4.063A.5.5 0 0 0 6 4.501v10.998a.5.5 0 0 0 .741.438z"/></svg>

After

Width:  |  Height:  |  Size: 316 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M16 8a2 2 0 1 1 0 4H4a2 2 0 1 1 0-4zm1 2a1 1 0 0 0-1-1H4a1 1 0 0 0 0 2h12a1 1 0 0 0 1-1M8 2a2 2 0 1 1 0 4H4a2 2 0 1 1 0-4zm1 2a1 1 0 0 0-1-1H4a1 1 0 0 0 0 2h4a1 1 0 0 0 1-1m5 12a2 2 0 0 0-2-2H4a2 2 0 1 0 0 4h8a2 2 0 0 0 2-2m-2-1a1 1 0 1 1 0 2H4a1 1 0 1 1 0-2z"/></svg>

After

Width:  |  Height:  |  Size: 380 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M7.75 17.25a.75.75 0 0 0 1.5 0V2.75a.75.75 0 0 0-1.5 0zm3 0a.75.75 0 0 0 1.5 0V2.75a.75.75 0 0 0-1.5 0z"/></svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M8 17.5a.5.5 0 0 0 1 0v-15a.5.5 0 0 0-1 0zm3 0a.5.5 0 0 0 1 0v-15a.5.5 0 0 0-1 0z"/></svg>

After

Width:  |  Height:  |  Size: 202 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M1.911 7.383a8.5 8.5 0 0 1 1.78-3.08a.5.5 0 0 1 .54-.135l1.918.686a1 1 0 0 0 1.32-.762l.366-2.006a.5.5 0 0 1 .388-.4a8.5 8.5 0 0 1 3.554 0a.5.5 0 0 1 .388.4l.366 2.006a1 1 0 0 0 1.32.762l1.919-.686a.5.5 0 0 1 .54.136a8.5 8.5 0 0 1 1.78 3.079a.5.5 0 0 1-.153.535l-1.555 1.32a1 1 0 0 0 0 1.524l1.555 1.32a.5.5 0 0 1 .152.535a8.5 8.5 0 0 1-1.78 3.08a.5.5 0 0 1-.54.135l-1.918-.686a1 1 0 0 0-1.32.762l-.366 2.007a.5.5 0 0 1-.388.399a8.5 8.5 0 0 1-3.554 0a.5.5 0 0 1-.388-.4l-.366-2.006a1 1 0 0 0-1.32-.762l-1.918.686a.5.5 0 0 1-.54-.136a8.5 8.5 0 0 1-1.78-3.079a.5.5 0 0 1 .152-.535l1.555-1.32a1 1 0 0 0 0-1.524l-1.555-1.32a.5.5 0 0 1-.152-.535m1.06-.006l1.294 1.098a2 2 0 0 1 0 3.05l-1.293 1.098c.292.782.713 1.51 1.244 2.152l1.596-.57q.155-.055.315-.085a2 2 0 0 1 2.326 1.609l.304 1.669a7.6 7.6 0 0 0 2.486 0l.304-1.67a1.998 1.998 0 0 1 2.641-1.524l1.596.571a7.5 7.5 0 0 0 1.245-2.152l-1.294-1.098a1.998 1.998 0 0 1 0-3.05l1.294-1.098a7.5 7.5 0 0 0-1.245-2.152l-1.596.57a2 2 0 0 1-2.64-1.524l-.305-1.669a7.6 7.6 0 0 0-2.486 0l-.304 1.669a2 2 0 0 1-2.64 1.525l-1.597-.571a7.5 7.5 0 0 0-1.244 2.152M7.502 10a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0m1 0a1.5 1.5 0 1 0 3 0a1.5 1.5 0 0 0-3 0"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M9.5 7v3h-3V7zm0 7v-3h-3v3zm4-7v3h-3V7zm0 7v-3h-3v3zM7 4V2.5A1.5 1.5 0 0 1 8.5 1h3A1.5 1.5 0 0 1 13 2.5V4h4.5a.5.5 0 0 1 .5.5v10a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 2 14.5v-10a.5.5 0 0 1 .5-.5zm1-1.5V4h4V2.5a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5m-5 12A1.5 1.5 0 0 0 4.5 16h11a1.5 1.5 0 0 0 1.5-1.5V5H3z"/></svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="M5 4a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3h5a3 3 0 0 0 3-3v-.321l3.037 2.097a1.25 1.25 0 0 0 1.96-1.029V6.252a1.25 1.25 0 0 0-1.96-1.028L13 7.32V7a3 3 0 0 0-3-3zm8 4.536l3.605-2.49a.25.25 0 0 1 .392.206v7.495a.25.25 0 0 1-.392.206L13 11.463zM3 7a2 2 0 0 1 2-2h5a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>

After

Width:  |  Height:  |  Size: 421 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="m17.331 3.461l.11.102l.102.11a1.93 1.93 0 0 1-.103 2.606l-3.603 3.617a1.9 1.9 0 0 1-.794.477l-1.96.591a.84.84 0 0 1-1.047-.567a.85.85 0 0 1 .005-.503l.621-1.942c.093-.289.252-.55.465-.765l3.612-3.625a1.904 1.904 0 0 1 2.592-.1M12.891 4H4.5A2.5 2.5 0 0 0 2 6.5v2.264a18 18 0 0 1 1.72-1.411c.647-.458 1.342-.86 1.979-1.026c.322-.085.662-.118.987-.042c.34.08.633.272.846.582c.463.674.126 1.404-.194 1.924c-.167.272-.374.556-.576.834l-.023.03c-.211.292-.421.582-.609.881c-.158.285-.2.622-.13.865q.054.174.166.265c.073.061.19.119.379.136c.33.03.759-.083 1.286-.272a.5.5 0 1 1 .338.94c-.52.188-1.14.38-1.714.328a1.66 1.66 0 0 1-.928-.363a1.5 1.5 0 0 1-.486-.753c-.16-.546-.05-1.165.224-1.648l.005-.009l.006-.01c.21-.337.443-.657.655-.948l.01-.014c.213-.291.399-.547.545-.785c.326-.53.298-.724.222-.835a.4.4 0 0 0-.25-.175c-.113-.026-.278-.024-.505.036c-.46.12-1.038.438-1.654.875c-.853.604-1.701 1.38-2.299 1.985V13.5A2.5 2.5 0 0 0 4.5 16h11a2.5 2.5 0 0 0 2.5-2.5V7.134l-3.455 3.468c-.338.34-.755.59-1.213.728l-1.96.591a1.84 1.84 0 0 1-2.295-1.238a1.85 1.85 0 0 1 .011-1.094l.622-1.942c.14-.44.383-.839.709-1.165z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="currentColor" d="m17.331 3.461l.11.102l.102.11a1.93 1.93 0 0 1-.103 2.606l-3.603 3.617a1.9 1.9 0 0 1-.794.477l-1.96.591a.84.84 0 0 1-1.047-.567a.85.85 0 0 1 .005-.503l.621-1.942c.093-.289.252-.55.465-.765l3.612-3.625a1.904 1.904 0 0 1 2.592-.1m-1.884.806l-3.611 3.626a.9.9 0 0 0-.221.363l-.533 1.664l1.672-.505c.14-.042.27-.12.374-.224l3.603-3.617a.93.93 0 0 0 .06-1.24l-.06-.065l-.064-.06a.904.904 0 0 0-1.22.058M12.891 4H5a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V7.134l-1 1.004V13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9.23c.573-.486 1.34-1.11 2.074-1.535c.41-.237.772-.39 1.062-.439c.281-.048.423.01.51.098a.33.33 0 0 1 .106.185a.6.6 0 0 1-.04.276c-.093.276-.31.602-.602 1.01l-.094.132c-.252.35-.538.747-.736 1.144c-.225.447-.392.995-.204 1.557c.17.508.498.845.926 1.011c.402.156.844.144 1.236.073c.785-.14 1.584-.552 2.02-.813a.5.5 0 0 0-.515-.858c-.399.24-1.075.578-1.681.687c-.303.054-.537.042-.698-.021c-.136-.053-.26-.153-.34-.395c-.062-.188-.03-.435.15-.793c.16-.32.396-.649.656-1.01l.093-.131c.276-.386.587-.832.737-1.273c.077-.229.122-.486.08-.753a1.32 1.32 0 0 0-.386-.736c-.397-.396-.914-.456-1.386-.376c-.462.079-.945.3-1.394.559c-.546.315-1.096.722-1.574 1.104V7a2 2 0 0 1 2-2h6.895z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

448
out/main/index.js Normal file
View File

@@ -0,0 +1,448 @@
"use strict";
const electron = require("electron");
const path = require("path");
const utils = require("@electron-toolkit/utils");
const elysia = require("elysia");
const fs = require("fs");
const child_process = require("child_process");
const util = require("util");
const node = require("@elysiajs/node");
const icon = path.join(__dirname, "../../resources/icon.png");
function getStartMenuPaths() {
const appData = process.env["APPDATA"];
const programData = process.env["ProgramData"] ?? process.env["PROGRAMDATA"];
return {
userProgramsPath: appData ? path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs") : null,
commonProgramsPath: programData ? path.join(programData, "Microsoft", "Windows", "Start Menu", "Programs") : null
};
}
function getStartMenuRoots() {
const { userProgramsPath, commonProgramsPath } = getStartMenuPaths();
return [userProgramsPath, commonProgramsPath].filter((p) => Boolean(p));
}
const execFileAsync$1 = util.promisify(child_process.execFile);
function getWindowsPowerShellExe() {
const systemRoot = process.env["SystemRoot"] ?? process.env["WINDIR"] ?? "C:\\Windows";
return path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
}
function buildPowerShellScript(roots) {
const rootsJson = JSON.stringify(roots).replace(/'/g, "''");
return [
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8",
'$ErrorActionPreference = "Stop"',
"$out = $null",
"try {",
`$roots = '${rootsJson}' | ConvertFrom-Json`,
"$wsh = New-Object -ComObject WScript.Shell",
"$entries = @()",
"foreach ($root in $roots) {",
" if (-not (Test-Path -LiteralPath $root)) { continue }",
" Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object {",
" $ext = $_.Extension.ToLowerInvariant()",
' if ($ext -ne ".lnk" -and $ext -ne ".url" -and $ext -ne ".appref-ms") { return }',
' $source = "unknown"',
' if ($root -like "*\\\\AppData\\\\Roaming*") { $source = "user" }',
' if ($root -like "*\\\\ProgramData*") { $source = "common" }',
' $rel = $_.FullName.Substring($root.Length).TrimStart("\\\\")',
' if ($ext -eq ".lnk") {',
" $sc = $wsh.CreateShortcut($_.FullName)",
" $entries += [pscustomobject]@{",
" id = $_.FullName",
" name = $_.BaseName",
' type = "lnk"',
" filePath = $_.FullName",
" relativePath = $rel",
" targetPath = $sc.TargetPath",
" arguments = $sc.Arguments",
" workingDirectory = $sc.WorkingDirectory",
" iconLocation = $sc.IconLocation",
" description = $sc.Description",
" source = $source",
" }",
" return",
" }",
' $t = "appref-ms"',
' if ($ext -eq ".url") { $t = "url" }',
" $entries += [pscustomobject]@{",
" id = $_.FullName",
" name = $_.BaseName",
" type = $t",
" filePath = $_.FullName",
" relativePath = $rel",
" source = $source",
" }",
" }",
"}",
"$usedStartApps = $false",
"try {",
" $startApps = Get-StartApps | Select-Object Name, AppID",
" if ($startApps -ne $null) {",
" $usedStartApps = $true",
" foreach ($a in $startApps) {",
" if ([string]::IsNullOrWhiteSpace($a.AppID)) { continue }",
" $entries += [pscustomobject]@{",
' id = "appsFolder:" + $a.AppID',
" name = $a.Name",
' type = "uwp"',
' filePath = "shell:AppsFolder\\\\" + $a.AppID',
' relativePath = "AppsFolder\\\\" + $a.Name',
" appUserModelId = $a.AppID",
' source = "appsfolder"',
" }",
" }",
" }",
"} catch { }",
"if (-not $usedStartApps) {",
" $shell = New-Object -ComObject Shell.Application",
' $appsFolder = $shell.NameSpace("shell:AppsFolder")',
" if ($appsFolder -ne $null) {",
" $appsFolder.Items() | ForEach-Object {",
" $aumid = $_.Path",
" if ([string]::IsNullOrWhiteSpace($aumid)) { return }",
" $name = $_.Name",
" $entries += [pscustomobject]@{",
' id = "appsFolder:" + $aumid',
" name = $name",
' type = "uwp"',
' filePath = "shell:AppsFolder\\\\" + $aumid',
' relativePath = "AppsFolder\\\\" + $name',
" appUserModelId = $aumid",
' source = "appsfolder"',
" }",
" }",
" }",
"}",
"$out = [pscustomobject]@{ ok = $true; entries = $entries }",
"} catch {",
" $out = [pscustomobject]@{ ok = $false; error = ($_ | Out-String); entries = @() }",
"}",
"$out | ConvertTo-Json -Depth 6"
].join("\n");
}
async function listWindowsStartMenuApps() {
if (process.platform !== "win32") return [];
const roots = getStartMenuRoots();
const script = buildPowerShellScript(roots);
const powershellExe = getWindowsPowerShellExe();
const { stdout } = await execFileAsync$1(
powershellExe,
["-NoProfile", "-NonInteractive", "-Sta", "-ExecutionPolicy", "Bypass", "-Command", script],
{ windowsHide: true, maxBuffer: 50 * 1024 * 1024, timeout: 6e4 }
);
const trimmed = stdout.trim();
if (!trimmed) return [];
const parsed = JSON.parse(trimmed);
if (typeof parsed === "object" && parsed !== null && "ok" in parsed) {
const ok = parsed.ok;
if (ok === false) {
const error = parsed.error;
throw new Error(typeof error === "string" ? error : "PowerShellFailed");
}
}
const rawEntries = typeof parsed === "object" && parsed !== null && "entries" in parsed ? parsed.entries : parsed;
const list = Array.isArray(rawEntries) ? rawEntries : rawEntries ? [rawEntries] : [];
const isEntry = (value) => {
if (typeof value !== "object" || value === null) return false;
const v = value;
return typeof v.id === "string" && typeof v.name === "string" && typeof v.type === "string" && typeof v.filePath === "string" && typeof v.relativePath === "string";
};
const seen = /* @__PURE__ */ new Set();
const result = [];
for (const item of list) {
if (!isEntry(item)) continue;
if (seen.has(item.id)) continue;
seen.add(item.id);
result.push(item);
}
result.sort((a, b) => a.name.localeCompare(b.name, "zh-CN"));
return result;
}
const execFileAsync = util.promisify(child_process.execFile);
function isUnderRoot(filePath, root) {
const normalizedFile = path.resolve(filePath).toLowerCase();
const normalizedRoot = path.resolve(root).toLowerCase();
return normalizedFile === normalizedRoot || normalizedFile.startsWith(normalizedRoot + "\\");
}
async function launchStartMenuEntry(filePath) {
if (process.platform !== "win32") return;
if (filePath.startsWith("shell:AppsFolder\\")) {
await execFileAsync("explorer.exe", [filePath], { windowsHide: true });
return;
}
const roots = getStartMenuRoots();
const allowed = roots.some((root) => isUnderRoot(filePath, root));
if (!allowed) {
throw new Error("PathNotAllowed");
}
const result = await electron.shell.openPath(filePath);
if (result) {
throw new Error(result);
}
}
function createEiysiaApp(deps) {
const iconCache = /* @__PURE__ */ new Map();
const appsCacheFilePath = path.join(electron.app.getPath("userData"), "apps-cache.json");
let cachedApps = [];
let cacheLoadPromise = null;
let refreshPromise = null;
const ensureAppsCacheLoaded = async () => {
if (cacheLoadPromise) return cacheLoadPromise;
cacheLoadPromise = (async () => {
try {
const raw = await fs.promises.readFile(appsCacheFilePath, "utf-8");
const parsed = JSON.parse(raw);
const apps = Array.isArray(parsed.apps) ? parsed.apps : [];
const parsedApps = apps.map((a) => {
const id = typeof a.id === "string" ? a.id : "";
const name = typeof a.name === "string" ? a.name : "";
const filePath = typeof a.filePath === "string" ? a.filePath : "";
const iconDataUrl = typeof a.iconDataUrl === "string" ? a.iconDataUrl : "";
if (!id || !name || !filePath || !iconDataUrl) return null;
return { id, name, filePath, iconDataUrl };
});
cachedApps = parsedApps.filter((a) => Boolean(a));
} catch {
cachedApps = [];
}
})();
return cacheLoadPromise;
};
const persistAppsCache = async (apps) => {
const payload = JSON.stringify(
{
version: 1,
updatedAt: Date.now(),
apps
},
null,
2
);
await fs.promises.writeFile(appsCacheFilePath, payload, "utf-8");
};
const refreshAppsCache = async () => {
if (refreshPromise) return refreshPromise;
refreshPromise = (async () => {
const cachedById = new Map(cachedApps.map((a) => [a.id, a]));
const entries = await listWindowsStartMenuApps();
let index = 0;
const limit = Math.max(4, Math.min(16, entries.length));
const nextApps = new Array(entries.length);
const workers = Array.from({ length: limit }, async () => {
while (true) {
const i = index;
index += 1;
if (i >= entries.length) return;
const e = entries[i];
const cached = cachedById.get(e.id);
if (cached && cached.name === e.name && cached.filePath === e.filePath && cached.iconDataUrl) {
nextApps[i] = cached;
continue;
}
const iconDataUrl = await getIconDataUrl(e);
nextApps[i] = { id: e.id, name: e.name, filePath: e.filePath, iconDataUrl };
}
});
await Promise.all(workers);
cachedApps = nextApps.filter(Boolean);
await persistAppsCache(cachedApps);
})().finally(() => {
refreshPromise = null;
});
return refreshPromise;
};
const placeholderIconDataUrl = (name, id) => {
const letter = (name.trim().charAt(0) || "?").toUpperCase();
let hash = 0;
for (let i = 0; i < id.length; i += 1) {
hash = hash * 31 + id.charCodeAt(i) | 0;
}
const hue = Math.abs(hash) % 360;
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><circle cx="32" cy="32" r="32" fill="hsl(${hue} 70% 45%)"/><text x="32" y="40" text-anchor="middle" font-family="Segoe UI, Arial" font-size="28" font-weight="700" fill="white">${letter.replace(
/[<>&]/g,
""
)}</text></svg>`;
return `data:image/svg+xml;base64,${Buffer.from(svg, "utf-8").toString("base64")}`;
};
const getIconDataUrl = async (entry) => {
const cached = iconCache.get(entry.id);
if (cached) return cached;
if (entry.type === "uwp" || entry.filePath.startsWith("shell:")) {
const dataUrl = placeholderIconDataUrl(entry.name, entry.id);
iconCache.set(entry.id, dataUrl);
return dataUrl;
}
if (!electron.app.isReady()) {
const dataUrl = placeholderIconDataUrl(entry.name, entry.id);
iconCache.set(entry.id, dataUrl);
return dataUrl;
}
try {
const tryPath = typeof entry.targetPath === "string" && entry.targetPath.trim() ? entry.targetPath : entry.filePath;
const rawIcon = await electron.app.getFileIcon(tryPath, { size: "large" }).catch(() => electron.app.getFileIcon(tryPath, { size: "normal" }));
let icon2 = rawIcon;
if (!icon2.isEmpty()) {
const { width, height } = icon2.getSize();
const target = 64;
if (width > 0 && height > 0 && (width < target || height < target)) {
icon2 = icon2.resize({ width: target, height: target, quality: "best" });
}
}
const dataUrl = icon2.isEmpty() ? placeholderIconDataUrl(entry.name, entry.id) : icon2.toDataURL();
iconCache.set(entry.id, dataUrl);
return dataUrl;
} catch {
const dataUrl = placeholderIconDataUrl(entry.name, entry.id);
iconCache.set(entry.id, dataUrl);
return dataUrl;
}
};
return new elysia.Elysia().get("/health", () => ({
ok: true,
time: Date.now()
})).onStart(() => {
void ensureAppsCacheLoaded().then(() => refreshAppsCache());
}).get("/apps/list", async () => {
try {
await ensureAppsCacheLoaded();
if (cachedApps.length === 0) {
await refreshAppsCache();
} else {
void refreshAppsCache();
}
return { apps: cachedApps, error: null };
} catch (error) {
const message = error instanceof Error ? error.message : "UnknownError";
console.error("[apps/list] failed:", message);
return { apps: [], error: message };
}
}).post("/apps/launch", async ({ body }) => {
const payload = body;
if (typeof payload.filePath !== "string") {
return new Response("BadRequest", { status: 400 });
}
try {
await launchStartMenuEntry(payload.filePath);
return { ok: true };
} catch (error) {
const message = error instanceof Error ? error.message : "UnknownError";
if (message === "PathNotAllowed") return new Response("Forbidden", { status: 403 });
return new Response("LaunchFailed", { status: 500 });
}
}).get("/backend/port", () => ({
port: deps.getHttpPort()
})).post("/app/minimize", () => {
deps.getMainWindow()?.minimize();
return { ok: true };
});
}
function registerEiysiaIpc(app) {
electron.ipcMain.handle(
"eiysia:request",
async (_event, payload) => {
const method = payload.method?.toUpperCase?.() ?? "GET";
const path2 = payload.path?.startsWith("/") ? payload.path : `/${payload.path ?? ""}`;
const headers = new Headers(payload.headers ?? {});
let body;
if (payload.body !== void 0) {
if (typeof payload.body === "string" || payload.body instanceof ArrayBuffer || ArrayBuffer.isView(payload.body)) {
body = payload.body;
} else {
body = JSON.stringify(payload.body);
if (!headers.has("content-type")) {
headers.set("content-type", "application/json");
}
}
}
const request = new Request(`http://eiysia.local${path2}`, {
method,
headers,
body
});
const response = await app.handle(request);
const bodyText = await response.text();
return {
status: response.status,
headers: Object.fromEntries(response.headers.entries()),
bodyText
};
}
);
}
function startEiysiaHttpServer(app) {
const serverApp = new elysia.Elysia({ adapter: node.node() }).use(app).listen({ hostname: "127.0.0.1", port: 0 });
let stopped = false;
const stop = () => {
if (stopped) return;
stopped = true;
try {
if (!serverApp.server) return;
const maybe = serverApp;
if (typeof maybe.stop === "function") maybe.stop();
else if (typeof maybe.close === "function") maybe.close();
} catch {
return;
}
};
return {
app: serverApp,
port: serverApp.server?.port ?? null,
stop
};
}
electron.app.commandLine.appendSwitch("touch-events", "enabled");
let mainWindow = null;
let eiysiaServerStop = null;
let eiysiaHttpPort = null;
function createWindow() {
const window = new electron.BrowserWindow({
show: false,
autoHideMenuBar: true,
fullscreen: true,
...process.platform === "linux" ? { icon } : {},
webPreferences: {
preload: path.join(__dirname, "../preload/index.js"),
sandbox: false
}
});
mainWindow = window;
window.on("ready-to-show", () => {
window.show();
});
window.webContents.setWindowOpenHandler((details) => {
electron.shell.openExternal(details.url);
return { action: "deny" };
});
if (utils.is.dev && process.env["ELECTRON_RENDERER_URL"]) {
window.loadURL(process.env["ELECTRON_RENDERER_URL"]);
} else {
window.loadFile(path.join(__dirname, "../renderer/index.html"));
}
}
electron.app.whenReady().then(() => {
utils.electronApp.setAppUserModelId("com.electron");
electron.app.on("browser-window-created", (_, window) => {
utils.optimizer.watchWindowShortcuts(window);
});
const eiysia = createEiysiaApp({
getMainWindow: () => mainWindow,
getHttpPort: () => eiysiaHttpPort
});
registerEiysiaIpc(eiysia);
const server = startEiysiaHttpServer(eiysia);
eiysiaHttpPort = server.port;
eiysiaServerStop = server.stop;
createWindow();
electron.app.on("activate", function() {
if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
electron.app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
electron.app.quit();
}
});
electron.app.on("before-quit", () => {
eiysiaServerStop?.();
eiysiaServerStop = null;
eiysiaHttpPort = null;
});

25
out/preload/index.js Normal file
View File

@@ -0,0 +1,25 @@
"use strict";
const electron = require("electron");
const preload = require("@electron-toolkit/preload");
const api = {
request: (payload) => electron.ipcRenderer.invoke("eiysia:request", payload),
call: async (payload) => {
const response = await electron.ipcRenderer.invoke("eiysia:request", payload);
const contentType = response.headers["content-type"] ?? response.headers["Content-Type"];
if (contentType?.includes("application/json")) {
return JSON.parse(response.bodyText);
}
return response.bodyText;
}
};
if (process.contextIsolated) {
try {
electron.contextBridge.exposeInMainWorld("electron", preload.electronAPI);
electron.contextBridge.exposeInMainWorld("api", api);
} catch (error) {
console.error(error);
}
} else {
window.electron = preload.electronAPI;
window.api = api;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,450 @@
:root {
--ev-c-white: #ffffff;
--ev-c-white-soft: #f8f8f8;
--ev-c-white-mute: #f2f2f2;
--ev-c-black: #1b1b1f;
--ev-c-black-soft: #222222;
--ev-c-black-mute: #282828;
--ev-c-gray-1: #515c67;
--ev-c-gray-2: #414853;
--ev-c-gray-3: #32363f;
--ev-c-text-1: rgba(255, 255, 245, 0.86);
--ev-c-text-2: rgba(235, 235, 245, 0.6);
--ev-c-text-3: rgba(235, 235, 245, 0.38);
--ev-button-alt-border: transparent;
--ev-button-alt-text: var(--ev-c-text-1);
--ev-button-alt-bg: var(--ev-c-gray-3);
--ev-button-alt-hover-border: transparent;
--ev-button-alt-hover-text: var(--ev-c-text-1);
--ev-button-alt-hover-bg: var(--ev-c-gray-2);
}
:root {
--color-background: var(--ev-c-black);
--color-background-soft: var(--ev-c-black-soft);
--color-background-mute: var(--ev-c-black-mute);
--color-text: var(--ev-c-text-1);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
ul {
list-style: none;
}
body {
min-height: 100vh;
color: var(--color-text);
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;
}
body {
margin: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%201422%20800'%20opacity='0.3'%3e%3cdefs%3e%3clinearGradient%20x1='50%25'%20y1='0%25'%20x2='50%25'%20y2='100%25'%20id='oooscillate-grad'%3e%3cstop%20stop-color='hsl(206,%2075%25,%2049%25)'%20stop-opacity='1'%20offset='0%25'%3e%3c/stop%3e%3cstop%20stop-color='hsl(331,%2090%25,%2056%25)'%20stop-opacity='1'%20offset='100%25'%3e%3c/stop%3e%3c/linearGradient%3e%3c/defs%3e%3cg%20stroke-width='1'%20stroke='url(%23oooscillate-grad)'%20fill='none'%20stroke-linecap='round'%3e%3cpath%20d='M%200%20448%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20448'%20opacity='0.05'%3e%3c/path%3e%3cpath%20d='M%200%20420%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20420'%20opacity='0.11'%3e%3c/path%3e%3cpath%20d='M%200%20392%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20392'%20opacity='0.18'%3e%3c/path%3e%3cpath%20d='M%200%20364%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20364'%20opacity='0.24'%3e%3c/path%3e%3cpath%20d='M%200%20336%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20336'%20opacity='0.30'%3e%3c/path%3e%3cpath%20d='M%200%20308%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20308'%20opacity='0.37'%3e%3c/path%3e%3cpath%20d='M%200%20280%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20280'%20opacity='0.43'%3e%3c/path%3e%3cpath%20d='M%200%20252%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20252'%20opacity='0.49'%3e%3c/path%3e%3cpath%20d='M%200%20224%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20224'%20opacity='0.56'%3e%3c/path%3e%3cpath%20d='M%200%20196%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20196'%20opacity='0.62'%3e%3c/path%3e%3cpath%20d='M%200%20168%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20168'%20opacity='0.68'%3e%3c/path%3e%3cpath%20d='M%200%20140%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20140'%20opacity='0.75'%3e%3c/path%3e%3cpath%20d='M%200%20112%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%20112'%20opacity='0.81'%3e%3c/path%3e%3cpath%20d='M%200%2084%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%2084'%20opacity='0.87'%3e%3c/path%3e%3cpath%20d='M%200%2056%20Q%20355.5%20-100%20711%20400%20Q%201066.5%20900%201422%2056'%20opacity='0.94'%3e%3c/path%3e%3c/g%3e%3c/svg%3e");
background-size: cover;
user-select: none;
}
#root {
width: 100%;
height: 100%;
}
.screen {
width: 100%;
height: 100%;
}
.touchButton {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.launcher {
position: fixed;
left: 16px;
top: 84px;
width: 520px;
height: calc(100vh - 168px);
padding: 12px;
background: rgba(0, 0, 0, 0.28);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
backdrop-filter: blur(12px);
box-sizing: border-box;
}
.home {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.openAppsButton {
cursor: pointer;
border-radius: 18px;
padding: 20px 28px;
border: 1px solid rgba(255, 255, 255, 0.16);
color: var(--ev-c-text-1);
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(12px);
font-size: 22px;
line-height: 26px;
font-weight: 800;
}
.openAppsButton:hover {
border-color: rgba(255, 255, 255, 0.26);
background: rgba(0, 0, 0, 0.45);
}
.appsPage {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
padding: 16px;
box-sizing: border-box;
}
.backButton {
cursor: pointer;
border-radius: 16px;
padding: 0 18px;
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--ev-c-text-1);
background: rgba(0, 0, 0, 0.35);
font-size: 18px;
line-height: 22px;
font-weight: 800;
height: 72px;
min-width: 120px;
}
.backButton:hover {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(0, 0, 0, 0.45);
}
.appsTilesArea {
position: fixed;
left: 16px;
right: 16px;
top: 88px;
bottom: 120px;
padding: 12px;
background: rgba(0, 0, 0, 0.28);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
backdrop-filter: blur(12px);
box-sizing: border-box;
overflow: hidden;
}
.appsList {
height: 100%;
overflow-x: auto;
overflow-y: hidden;
padding: 4px 6px;
box-sizing: border-box;
columns: 6 280px;
column-gap: 14px;
column-fill: auto;
touch-action: pan-x;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
scrollbar-width: none;
-ms-overflow-style: none;
}
.appsList::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
.appsLoading {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 700;
opacity: 0.9;
}
.appsBottomBar {
position: fixed;
left: 16px;
right: 16px;
bottom: 16px;
height: 92px;
padding: 10px 12px;
display: grid;
grid-template-columns: 140px 1fr 140px;
gap: 12px;
align-items: center;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
backdrop-filter: blur(12px);
box-sizing: border-box;
}
.appsSearch {
width: 100%;
height: 72px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.35);
color: var(--ev-c-text-1);
padding: 0 16px;
box-sizing: border-box;
outline: none;
font-size: 20px;
line-height: 24px;
user-select: text;
}
.appsExitButton {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
cursor: pointer;
border-radius: 16px;
padding: 0 18px;
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--ev-c-text-1);
background: rgba(0, 0, 0, 0.35);
font-size: 18px;
line-height: 22px;
font-weight: 800;
height: 72px;
min-width: 120px;
}
.appsExitButton:hover {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(0, 0, 0, 0.45);
}
.search {
width: 100%;
height: 38px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.35);
color: var(--ev-c-text-1);
padding: 0 12px;
box-sizing: border-box;
outline: none;
}
.error {
margin-top: 10px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 120, 120, 0.25);
background: rgba(120, 0, 0, 0.18);
color: var(--ev-c-text-1);
font-size: 12px;
line-height: 16px;
user-select: text;
white-space: pre-wrap;
word-break: break-word;
}
.errorHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 8px;
}
.errorTitle {
font-weight: 700;
font-size: 12px;
line-height: 16px;
}
.errorBody {
white-space: pre-wrap;
word-break: break-word;
}
.copyButton {
cursor: pointer;
border-radius: 10px;
padding: 6px 10px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(0, 0, 0, 0.25);
color: var(--ev-c-text-1);
font-size: 12px;
line-height: 14px;
}
.copyButton:hover {
border-color: rgba(255, 255, 255, 0.26);
background: rgba(0, 0, 0, 0.35);
}
.list {
margin-top: 12px;
height: calc(100% - 50px);
overflow: auto;
display: grid;
gap: 10px;
}
.groupHeader {
display: inline-block;
width: 100%;
box-sizing: border-box;
margin: 0 0 10px;
padding: 6px 10px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.2);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.4px;
opacity: 0.9;
break-inside: avoid;
-webkit-column-break-inside: avoid;
}
.item {
display: inline-flex;
align-items: center;
gap: 10px;
text-align: left;
cursor: pointer;
width: 100%;
box-sizing: border-box;
min-height: 72px;
padding: 12px 14px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.25);
color: var(--ev-c-text-1);
margin: 0 0 10px;
break-inside: avoid;
-webkit-column-break-inside: avoid;
}
.item:hover {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(0, 0, 0, 0.35);
}
.item:active {
border-color: rgba(255, 255, 255, 0.26);
background: rgba(0, 0, 0, 0.5);
}
.name {
font-weight: 700;
font-size: 18px;
line-height: 22px;
}
.icon {
width: 48px;
height: 48px;
border-radius: 10px;
flex: 0 0 auto;
}
.path {
margin-top: 4px;
font-size: 12px;
line-height: 16px;
opacity: 0.75;
word-break: break-all;
}
.clock {
position: fixed;
left: 16px;
top: 16px;
padding: 10px 12px;
font-size: 28px;
line-height: 28px;
font-weight: 700;
color: var(--ev-c-text-1);
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
backdrop-filter: blur(12px);
}
.exitButton {
position: fixed;
right: 16px;
bottom: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
cursor: pointer;
text-align: center;
font-weight: 600;
border-radius: 14px;
padding: 10px 16px;
line-height: 20px;
font-size: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--ev-c-text-1);
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(12px);
}
.buttonIcon {
width: 20px;
height: 20px;
flex: 0 0 auto;
}
.exitButton:hover {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(0, 0, 0, 0.45);
}

18
out/renderer/index.html Normal file
View File

@@ -0,0 +1,18 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<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:"
/>
<script type="module" crossorigin src="./assets/index-C9aAt2YI.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DAQiHmWm.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

52
package.json Normal file
View File

@@ -0,0 +1,52 @@
{
"name": "desktop",
"version": "1.0.0",
"description": "An Electron application with Vue 3 and TypeScript",
"main": "./out/main/index.js",
"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": "pnpm run typecheck:node && pnpm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "pnpm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "pnpm run build && electron-builder --dir",
"build:win": "pnpm run build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@elysiajs/node": "^1.4.4",
"electron-updater": "^6.3.9",
"elysia": "^1.4.25"
},
"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",
"@vitejs/plugin-vue": "^6.0.0",
"electron": "^39.2.6",
"electron-builder": "^26.0.12",
"electron-vite": "^5.0.0",
"eslint": "^9.39.1",
"prettier": "^3.7.4",
"typescript": "^5.9.3",
"vite": "^7.2.6",
"vue": "^3.5.0",
"vue-tsc": "^2.2.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"electron",
"esbuild"
]
}
}

5091
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

3
src/app_list/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { listWindowsStartMenuApps } from './list'
export { launchStartMenuEntry } from './launch'
export type { WindowsStartMenuAppEntry, WindowsStartMenuEntryType } from './types'

33
src/app_list/launch.ts Normal file
View File

@@ -0,0 +1,33 @@
import { shell } from 'electron'
import { execFile } from 'child_process'
import { resolve } from 'path'
import { promisify } from 'util'
import { getStartMenuRoots } from './paths'
const execFileAsync = promisify(execFile)
function isUnderRoot(filePath: string, root: string): boolean {
const normalizedFile = resolve(filePath).toLowerCase()
const normalizedRoot = resolve(root).toLowerCase()
return normalizedFile === normalizedRoot || normalizedFile.startsWith(normalizedRoot + '\\')
}
export async function launchStartMenuEntry(filePath: string): Promise<void> {
if (process.platform !== 'win32') return
if (filePath.startsWith('shell:AppsFolder\\')) {
await execFileAsync('explorer.exe', [filePath], { windowsHide: true })
return
}
const roots = getStartMenuRoots()
const allowed = roots.some((root) => isUnderRoot(filePath, root))
if (!allowed) {
throw new Error('PathNotAllowed')
}
const result = await shell.openPath(filePath)
if (result) {
throw new Error(result)
}
}

167
src/app_list/list.ts Normal file
View File

@@ -0,0 +1,167 @@
import { execFile } from 'child_process'
import { join } from 'path'
import { promisify } from 'util'
import { getStartMenuRoots } from './paths'
import type { WindowsStartMenuAppEntry } from './types'
const execFileAsync = promisify(execFile)
function getWindowsPowerShellExe(): string {
const systemRoot = process.env['SystemRoot'] ?? process.env['WINDIR'] ?? 'C:\\Windows'
return join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
}
function buildPowerShellScript(roots: string[]): string {
const rootsJson = JSON.stringify(roots).replace(/'/g, "''")
return [
'[Console]::OutputEncoding = [System.Text.Encoding]::UTF8',
'$ErrorActionPreference = "Stop"',
'$out = $null',
'try {',
`$roots = '${rootsJson}' | ConvertFrom-Json`,
'$wsh = New-Object -ComObject WScript.Shell',
'$entries = @()',
'foreach ($root in $roots) {',
' if (-not (Test-Path -LiteralPath $root)) { continue }',
' Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object {',
' $ext = $_.Extension.ToLowerInvariant()',
' if ($ext -ne ".lnk" -and $ext -ne ".url" -and $ext -ne ".appref-ms") { return }',
' $source = "unknown"',
' if ($root -like "*\\\\AppData\\\\Roaming*") { $source = "user" }',
' if ($root -like "*\\\\ProgramData*") { $source = "common" }',
' $rel = $_.FullName.Substring($root.Length).TrimStart("\\\\")',
' if ($ext -eq ".lnk") {',
' $sc = $wsh.CreateShortcut($_.FullName)',
' $entries += [pscustomobject]@{',
' id = $_.FullName',
' name = $_.BaseName',
' type = "lnk"',
' filePath = $_.FullName',
' relativePath = $rel',
' targetPath = $sc.TargetPath',
' arguments = $sc.Arguments',
' workingDirectory = $sc.WorkingDirectory',
' iconLocation = $sc.IconLocation',
' description = $sc.Description',
' source = $source',
' }',
' return',
' }',
' $t = "appref-ms"',
' if ($ext -eq ".url") { $t = "url" }',
' $entries += [pscustomobject]@{',
' id = $_.FullName',
' name = $_.BaseName',
' type = $t',
' filePath = $_.FullName',
' relativePath = $rel',
' source = $source',
' }',
' }',
'}',
'$usedStartApps = $false',
'try {',
' $startApps = Get-StartApps | Select-Object Name, AppID',
' if ($startApps -ne $null) {',
' $usedStartApps = $true',
' foreach ($a in $startApps) {',
' if ([string]::IsNullOrWhiteSpace($a.AppID)) { continue }',
' $entries += [pscustomobject]@{',
' id = "appsFolder:" + $a.AppID',
' name = $a.Name',
' type = "uwp"',
' filePath = "shell:AppsFolder\\\\" + $a.AppID',
' relativePath = "AppsFolder\\\\" + $a.Name',
' appUserModelId = $a.AppID',
' source = "appsfolder"',
' }',
' }',
' }',
'} catch { }',
'if (-not $usedStartApps) {',
' $shell = New-Object -ComObject Shell.Application',
' $appsFolder = $shell.NameSpace("shell:AppsFolder")',
' if ($appsFolder -ne $null) {',
' $appsFolder.Items() | ForEach-Object {',
' $aumid = $_.Path',
' if ([string]::IsNullOrWhiteSpace($aumid)) { return }',
' $name = $_.Name',
' $entries += [pscustomobject]@{',
' id = "appsFolder:" + $aumid',
' name = $name',
' type = "uwp"',
' filePath = "shell:AppsFolder\\\\" + $aumid',
' relativePath = "AppsFolder\\\\" + $name',
' appUserModelId = $aumid',
' source = "appsfolder"',
' }',
' }',
' }',
'}',
'$out = [pscustomobject]@{ ok = $true; entries = $entries }',
'} catch {',
' $out = [pscustomobject]@{ ok = $false; error = ($_ | Out-String); entries = @() }',
'}',
'$out | ConvertTo-Json -Depth 6'
].join('\n')
}
export async function listWindowsStartMenuApps(): Promise<WindowsStartMenuAppEntry[]> {
if (process.platform !== 'win32') return []
const roots = getStartMenuRoots()
const script = buildPowerShellScript(roots)
const powershellExe = getWindowsPowerShellExe()
const { stdout } = await execFileAsync(
powershellExe,
['-NoProfile', '-NonInteractive', '-Sta', '-ExecutionPolicy', 'Bypass', '-Command', script],
{ windowsHide: true, maxBuffer: 50 * 1024 * 1024, timeout: 60_000 }
)
const trimmed = stdout.trim()
if (!trimmed) return []
const parsed: unknown = JSON.parse(trimmed)
if (typeof parsed === 'object' && parsed !== null && 'ok' in parsed) {
const ok = (parsed as { ok?: unknown }).ok
if (ok === false) {
const error = (parsed as { error?: unknown }).error
throw new Error(typeof error === 'string' ? error : 'PowerShellFailed')
}
}
const rawEntries: unknown =
typeof parsed === 'object' && parsed !== null && 'entries' in parsed
? (parsed as { entries?: unknown }).entries
: parsed
const list: unknown[] = Array.isArray(rawEntries) ? rawEntries : rawEntries ? [rawEntries] : []
const isEntry = (value: unknown): value is WindowsStartMenuAppEntry => {
if (typeof value !== 'object' || value === null) return false
const v = value as Record<string, unknown>
return (
typeof v.id === 'string' &&
typeof v.name === 'string' &&
typeof v.type === 'string' &&
typeof v.filePath === 'string' &&
typeof v.relativePath === 'string'
)
}
const seen = new Set<string>()
const result: WindowsStartMenuAppEntry[] = []
for (const item of list) {
if (!isEntry(item)) continue
if (seen.has(item.id)) continue
seen.add(item.id)
result.push(item)
}
result.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
return result
}

25
src/app_list/paths.ts Normal file
View File

@@ -0,0 +1,25 @@
import { join } from 'path'
export interface StartMenuPaths {
userProgramsPath: string | null
commonProgramsPath: string | null
}
export function getStartMenuPaths(): StartMenuPaths {
const appData = process.env['APPDATA']
const programData = process.env['ProgramData'] ?? process.env['PROGRAMDATA']
return {
userProgramsPath: appData
? join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs')
: null,
commonProgramsPath: programData
? join(programData, 'Microsoft', 'Windows', 'Start Menu', 'Programs')
: null
}
}
export function getStartMenuRoots(): string[] {
const { userProgramsPath, commonProgramsPath } = getStartMenuPaths()
return [userProgramsPath, commonProgramsPath].filter((p): p is string => Boolean(p))
}

16
src/app_list/types.ts Normal file
View File

@@ -0,0 +1,16 @@
export type WindowsStartMenuEntryType = 'lnk' | 'url' | 'appref-ms' | 'uwp' | 'other'
export interface WindowsStartMenuAppEntry {
id: string
name: string
type: WindowsStartMenuEntryType
filePath: string
relativePath: string
appUserModelId?: string | null
targetPath?: string | null
arguments?: string | null
workingDirectory?: string | null
iconLocation?: string | null
description?: string | null
source: 'user' | 'common' | 'appsfolder' | 'unknown'
}

226
src/eiysia/app.ts Normal file
View File

@@ -0,0 +1,226 @@
import { app as electronApp, BrowserWindow } from 'electron'
import { Elysia } from 'elysia'
import { promises as fs } from 'fs'
import { join } from 'path'
import { launchStartMenuEntry, listWindowsStartMenuApps } from '../app_list'
export interface EiysiaDependencies {
getMainWindow: () => BrowserWindow | null
getHttpPort: () => number | null
}
export function createEiysiaApp(deps: EiysiaDependencies): {
handle: (request: Request) => Response | Promise<Response>
} {
type CachedApp = {
id: string
name: string
filePath: string
iconDataUrl: string
}
const iconCache = new Map<string, string>()
const appsCacheFilePath = join(electronApp.getPath('userData'), 'apps-cache.json')
let cachedApps: CachedApp[] = []
let cacheLoadPromise: Promise<void> | null = null
let refreshPromise: Promise<void> | null = null
const ensureAppsCacheLoaded = async (): Promise<void> => {
if (cacheLoadPromise) return cacheLoadPromise
cacheLoadPromise = (async () => {
try {
const raw = await fs.readFile(appsCacheFilePath, 'utf-8')
const parsed = JSON.parse(raw) as {
apps?: Array<{ id?: unknown; name?: unknown; filePath?: unknown; iconDataUrl?: unknown }>
}
const apps = Array.isArray(parsed.apps) ? parsed.apps : []
const parsedApps = apps.map((a) => {
const id = typeof a.id === 'string' ? a.id : ''
const name = typeof a.name === 'string' ? a.name : ''
const filePath = typeof a.filePath === 'string' ? a.filePath : ''
const iconDataUrl = typeof a.iconDataUrl === 'string' ? a.iconDataUrl : ''
if (!id || !name || !filePath || !iconDataUrl) return null
return { id, name, filePath, iconDataUrl }
})
cachedApps = parsedApps.filter((a): a is CachedApp => Boolean(a))
} catch {
cachedApps = []
}
})()
return cacheLoadPromise
}
const persistAppsCache = async (apps: CachedApp[]): Promise<void> => {
const payload = JSON.stringify(
{
version: 1,
updatedAt: Date.now(),
apps
},
null,
2
)
await fs.writeFile(appsCacheFilePath, payload, 'utf-8')
}
const refreshAppsCache = async (): Promise<void> => {
if (refreshPromise) return refreshPromise
refreshPromise = (async () => {
const cachedById = new Map(cachedApps.map((a) => [a.id, a]))
const entries = await listWindowsStartMenuApps()
let index = 0
const limit = Math.max(4, Math.min(16, entries.length))
const nextApps: CachedApp[] = new Array(entries.length)
const workers = Array.from({ length: limit }, async () => {
while (true) {
const i = index
index += 1
if (i >= entries.length) return
const e = entries[i]
const cached = cachedById.get(e.id)
if (
cached &&
cached.name === e.name &&
cached.filePath === e.filePath &&
cached.iconDataUrl
) {
nextApps[i] = cached
continue
}
const iconDataUrl = await getIconDataUrl(e)
nextApps[i] = { id: e.id, name: e.name, filePath: e.filePath, iconDataUrl }
}
})
await Promise.all(workers)
cachedApps = nextApps.filter(Boolean)
await persistAppsCache(cachedApps)
})().finally(() => {
refreshPromise = null
})
return refreshPromise
}
const placeholderIconDataUrl = (name: string, id: string): string => {
const letter = (name.trim().charAt(0) || '?').toUpperCase()
let hash = 0
for (let i = 0; i < id.length; i += 1) {
hash = (hash * 31 + id.charCodeAt(i)) | 0
}
const hue = Math.abs(hash) % 360
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><circle cx="32" cy="32" r="32" fill="hsl(${hue} 70% 45%)"/><text x="32" y="40" text-anchor="middle" font-family="Segoe UI, Arial" font-size="28" font-weight="700" fill="white">${letter.replace(
/[<>&]/g,
''
)}</text></svg>`
return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf-8').toString('base64')}`
}
const getIconDataUrl = async (entry: {
id: string
name: string
type: string
filePath: string
targetPath?: string | null
}): Promise<string> => {
const cached = iconCache.get(entry.id)
if (cached) return cached
if (entry.type === 'uwp' || entry.filePath.startsWith('shell:')) {
const dataUrl = placeholderIconDataUrl(entry.name, entry.id)
iconCache.set(entry.id, dataUrl)
return dataUrl
}
if (!electronApp.isReady()) {
const dataUrl = placeholderIconDataUrl(entry.name, entry.id)
iconCache.set(entry.id, dataUrl)
return dataUrl
}
try {
const tryPath =
typeof entry.targetPath === 'string' && entry.targetPath.trim()
? entry.targetPath
: entry.filePath
const rawIcon = await electronApp
.getFileIcon(tryPath, { size: 'large' })
.catch(() => electronApp.getFileIcon(tryPath, { size: 'normal' }))
let icon = rawIcon
if (!icon.isEmpty()) {
const { width, height } = icon.getSize()
const target = 64
if (width > 0 && height > 0 && (width < target || height < target)) {
icon = icon.resize({ width: target, height: target, quality: 'best' })
}
}
const dataUrl = icon.isEmpty()
? placeholderIconDataUrl(entry.name, entry.id)
: icon.toDataURL()
iconCache.set(entry.id, dataUrl)
return dataUrl
} catch {
const dataUrl = placeholderIconDataUrl(entry.name, entry.id)
iconCache.set(entry.id, dataUrl)
return dataUrl
}
}
return new Elysia()
.get('/health', () => ({
ok: true,
time: Date.now()
}))
.onStart(() => {
void ensureAppsCacheLoaded().then(() => refreshAppsCache())
})
.get('/apps/list', async () => {
try {
await ensureAppsCacheLoaded()
if (cachedApps.length === 0) {
await refreshAppsCache()
} else {
void refreshAppsCache()
}
return { apps: cachedApps, error: null as string | null }
} catch (error) {
const message = error instanceof Error ? error.message : 'UnknownError'
console.error('[apps/list] failed:', message)
return { apps: [], error: message }
}
})
.post('/apps/launch', async ({ body }) => {
const payload = body as { filePath?: unknown }
if (typeof payload.filePath !== 'string') {
return new Response('BadRequest', { status: 400 })
}
try {
await launchStartMenuEntry(payload.filePath)
return { ok: true }
} catch (error) {
const message = error instanceof Error ? error.message : 'UnknownError'
if (message === 'PathNotAllowed') return new Response('Forbidden', { status: 403 })
return new Response('LaunchFailed', { status: 500 })
}
})
.get('/backend/port', () => ({
port: deps.getHttpPort()
}))
.post('/app/minimize', () => {
deps.getMainWindow()?.minimize()
return { ok: true }
})
}

3
src/eiysia/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { createEiysiaApp } from './app'
export { registerEiysiaIpc } from './ipc'
export { startEiysiaHttpServer } from './server'

59
src/eiysia/ipc.ts Normal file
View File

@@ -0,0 +1,59 @@
import { ipcMain } from 'electron'
export interface EiysiaIpcRequest {
method: string
path: string
headers?: Record<string, string>
body?: unknown
}
export interface EiysiaIpcResponse {
status: number
headers: Record<string, string>
bodyText: string
}
export function registerEiysiaIpc(app: {
handle: (request: Request) => Response | Promise<Response>
}): void {
ipcMain.handle(
'eiysia:request',
async (_event, payload: EiysiaIpcRequest): Promise<EiysiaIpcResponse> => {
const method = payload.method?.toUpperCase?.() ?? 'GET'
const path = payload.path?.startsWith('/') ? payload.path : `/${payload.path ?? ''}`
const headers = new Headers(payload.headers ?? {})
let body: BodyInit | undefined
if (payload.body !== undefined) {
if (
typeof payload.body === 'string' ||
payload.body instanceof ArrayBuffer ||
ArrayBuffer.isView(payload.body)
) {
body = payload.body as BodyInit
} else {
body = JSON.stringify(payload.body)
if (!headers.has('content-type')) {
headers.set('content-type', 'application/json')
}
}
}
const request = new Request(`http://eiysia.local${path}`, {
method,
headers,
body
})
const response = await app.handle(request)
const bodyText = await response.text()
return {
status: response.status,
headers: Object.fromEntries(response.headers.entries()),
bodyText
}
}
)
}

39
src/eiysia/server.ts Normal file
View File

@@ -0,0 +1,39 @@
import { Elysia } from 'elysia'
import { node } from '@elysiajs/node'
export interface EiysiaServerHandle {
app: unknown
port: number | null
stop: () => void
}
export function startEiysiaHttpServer(app: unknown): EiysiaServerHandle {
const serverApp = new Elysia({ adapter: node() })
.use(app as never)
.listen({ hostname: '127.0.0.1', port: 0 })
let stopped = false
const stop = (): void => {
if (stopped) return
stopped = true
try {
if (!serverApp.server) return
const maybe = serverApp as unknown as {
stop?: () => Promise<void> | void
close?: () => void
}
if (typeof maybe.stop === 'function') maybe.stop()
else if (typeof maybe.close === 'function') maybe.close()
} catch {
return
}
}
return {
app: serverApp,
port: serverApp.server?.port ?? null,
stop
}
}

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

@@ -0,0 +1,96 @@
import { app, shell, BrowserWindow } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import { createEiysiaApp, registerEiysiaIpc, startEiysiaHttpServer } from '../eiysia'
app.commandLine.appendSwitch('touch-events', 'enabled')
let mainWindow: BrowserWindow | null = null
let eiysiaServerStop: (() => void) | null = null
let eiysiaHttpPort: number | null = null
function createWindow(): void {
// Create the browser window.
const window = new BrowserWindow({
show: false,
autoHideMenuBar: true,
fullscreen: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
mainWindow = window
window.on('ready-to-show', () => {
window.show()
})
window.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
window.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
window.loadFile(join(__dirname, '../renderer/index.html'))
}
}
// 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)
})
const eiysia = createEiysiaApp({
getMainWindow: () => mainWindow,
getHttpPort: () => eiysiaHttpPort
})
registerEiysiaIpc(eiysia)
const server = startEiysiaHttpServer(eiysia)
eiysiaHttpPort = server.port
eiysiaServerStop = server.stop
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('before-quit', () => {
eiysiaServerStop?.()
eiysiaServerStop = null
eiysiaHttpPort = null
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

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

@@ -0,0 +1,23 @@
import { ElectronAPI } from '@electron-toolkit/preload'
export interface AppApi {
request: (payload: {
method: string
path: string
headers?: Record<string, string>
body?: unknown
}) => Promise<{ status: number; headers: Record<string, string>; bodyText: string }>
call: <T = unknown>(payload: {
method: string
path: string
headers?: Record<string, string>
body?: unknown
}) => Promise<T>
}
declare global {
interface Window {
electron: ElectronAPI
api: AppApi
}
}

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

@@ -0,0 +1,46 @@
import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer
const api = {
request: (payload: {
method: string
path: string
headers?: Record<string, string>
body?: unknown
}): Promise<{ status: number; headers: Record<string, string>; bodyText: string }> =>
ipcRenderer.invoke('eiysia:request', payload),
call: async <T = unknown>(payload: {
method: string
path: string
headers?: Record<string, string>
body?: unknown
}): Promise<T> => {
const response: { status: number; headers: Record<string, string>; bodyText: string } =
await ipcRenderer.invoke('eiysia:request', payload)
const contentType = response.headers['content-type'] ?? response.headers['Content-Type']
if (contentType?.includes('application/json')) {
return JSON.parse(response.bodyText) as T
}
return response.bodyText as T
}
}
// 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)
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
}

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

@@ -0,0 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<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:"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

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

@@ -0,0 +1,345 @@
<template>
<div class="screen">
<div class="clock">{{ timeText }}</div>
<div v-if="page === 'home'" class="home">
<button
class="openAppsButton touchButton"
type="button"
@pointerup="openApps"
@keydown.enter.prevent="openApps"
@keydown.space.prevent="openApps"
>
应用列表
</button>
</div>
<div v-else class="appsPage">
<div class="appsTilesArea">
<div v-if="isLoadingApps" class="appsLoading">加载中...</div>
<div v-else-if="loadError" class="error">
<div class="errorHeader">
<div class="errorTitle">加载失败</div>
<button class="copyButton touchButton" type="button" @pointerup="copyLoadError">复制错误</button>
</div>
<div class="errorBody">{{ loadError }}</div>
</div>
<div v-else class="appsList" @scroll="handleAppsListScroll">
<template v-for="group in groupedApps" :key="group.key">
<div class="groupHeader">{{ group.label }}</div>
<button
v-for="app in group.items"
:key="app.id"
class="item touchButton"
type="button"
@pointerdown="handleAppPointerDown($event, app.filePath)"
@pointermove="handleAppPointerMove($event)"
@pointerup="handleAppPointerUp($event)"
@pointercancel="handleAppPointerCancel($event)"
@keydown.enter.prevent="launch(app.filePath)"
@keydown.space.prevent="launch(app.filePath)"
>
<img class="icon" :src="app.iconDataUrl" alt="" />
<div class="name">{{ app.name }}</div>
</button>
</template>
</div>
</div>
<div class="appsBottomBar">
<button class="backButton touchButton" type="button" @pointerup="closeApps">返回</button>
<input v-model="query" class="appsSearch" type="text" placeholder="搜索应用..." />
<button class="appsExitButton touchButton" type="button" @pointerup="handleExit">
<svg
class="buttonIcon"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M8.5 9A1.5 1.5 0 0 0 10 7.5v-4A1.5 1.5 0 0 0 8.5 2h-6A1.5 1.5 0 0 0 1 3.5v4a1.5 1.5 0 0 0 1 1.415l.019.006c.15.051.313.079.481.079zm6.75-3H11V5h4.25A2.75 2.75 0 0 1 18 7.75v6.5A2.75 2.75 0 0 1 15.25 17H4.75A2.75 2.75 0 0 1 2 14.25v-4.3q.243.05.5.05H3v4.25c0 .966.784 1.75 1.75 1.75h10.5A1.75 1.75 0 0 0 17 14.25v-6.5A1.75 1.75 0 0 0 15.25 6M14 12.293l-2.646-2.647a.5.5 0 0 0-.708.708L13.293 13H11.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 .5-.497V10.5a.5.5 0 0 0-1 0z"
/>
</svg>
Windows
</button>
</div>
</div>
<button v-if="page === 'home'" class="exitButton touchButton" type="button" @pointerup="handleExit">
<svg
class="buttonIcon"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M8.5 9A1.5 1.5 0 0 0 10 7.5v-4A1.5 1.5 0 0 0 8.5 2h-6A1.5 1.5 0 0 0 1 3.5v4a1.5 1.5 0 0 0 1 1.415l.019.006c.15.051.313.079.481.079zm6.75-3H11V5h4.25A2.75 2.75 0 0 1 18 7.75v6.5A2.75 2.75 0 0 1 15.25 17H4.75A2.75 2.75 0 0 1 2 14.25v-4.3q.243.05.5.05H3v4.25c0 .966.784 1.75 1.75 1.75h10.5A1.75 1.75 0 0 0 17 14.25v-6.5A1.75 1.75 0 0 0 15.25 6M14 12.293l-2.646-2.647a.5.5 0 0 0-.708.708L13.293 13H11.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 .5-.497V10.5a.5.5 0 0 0-1 0z"
/>
</svg>
Windows
</button>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
type Page = 'home' | 'apps'
const now = ref(new Date())
const page = ref<Page>('home')
const apps = ref<Array<{ id: string; name: string; filePath: string; iconDataUrl: string }>>([])
const query = ref('')
const loadError = ref<string | null>(null)
const isLoadingApps = ref(false)
const isAppsScrolling = ref(false)
let timerId: number | undefined
let appsScrollTimerId: number | undefined
let launchTimerId: number | undefined
const activeAppPointer = ref<{
pointerId: number
startX: number
startY: number
startedAt: number
moved: boolean
filePath: string
element: HTMLElement | null
} | null>(null)
onMounted(() => {
timerId = window.setInterval(() => {
now.value = new Date()
}, 1000)
})
const handleAppsListScroll = (): void => {
isAppsScrolling.value = true
if (appsScrollTimerId) window.clearTimeout(appsScrollTimerId)
appsScrollTimerId = window.setTimeout(() => {
isAppsScrolling.value = false
}, 140)
}
const loadApps = async (): Promise<void> => {
if (isLoadingApps.value) return
isLoadingApps.value = true
loadError.value = null
try {
const result = await window.api.call<{
apps: Array<{ id: string; name: string; filePath: string; iconDataUrl: string }>
error: string | null
}>({
method: 'GET',
path: '/apps/list'
})
apps.value = result.apps ?? []
loadError.value = result.error ?? null
} catch (error) {
const message = error instanceof Error ? error.message : 'UnknownError'
apps.value = []
loadError.value = message
} finally {
isLoadingApps.value = false
}
}
const openApps = async (): Promise<void> => {
page.value = 'apps'
if (apps.value.length === 0 || loadError.value) {
await loadApps()
}
}
const closeApps = (): void => {
page.value = 'home'
query.value = ''
}
onBeforeUnmount(() => {
if (timerId) window.clearInterval(timerId)
if (appsScrollTimerId) window.clearTimeout(appsScrollTimerId)
if (launchTimerId) window.clearTimeout(launchTimerId)
})
const timeText = computed(() => {
return new Intl.DateTimeFormat('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(now.value)
})
const filteredApps = computed(() => {
const q = query.value.trim().toLowerCase()
if (!q) return apps.value
return apps.value.filter((a) => a.name.toLowerCase().includes(q))
})
const collator =
Intl.Collator.supportedLocalesOf(['zh-Hans-CN-u-co-pinyin']).length > 0
? new Intl.Collator(['zh-Hans-CN-u-co-pinyin', 'en'], {
numeric: true,
sensitivity: 'base'
})
: new Intl.Collator(['zh-CN', 'en'], { numeric: true, sensitivity: 'base' })
const pinyinInitialBoundaries: Array<{ letter: string; char: string }> = [
{ letter: 'A', char: '阿' },
{ letter: 'B', char: '八' },
{ letter: 'C', char: '嚓' },
{ letter: 'D', char: '搭' },
{ letter: 'E', char: '蛾' },
{ letter: 'F', char: '发' },
{ letter: 'G', char: '噶' },
{ letter: 'H', char: '哈' },
{ letter: 'J', char: '击' },
{ letter: 'K', char: '喀' },
{ letter: 'L', char: '垃' },
{ letter: 'M', char: '妈' },
{ letter: 'N', char: '拿' },
{ letter: 'O', char: '哦' },
{ letter: 'P', char: '啪' },
{ letter: 'Q', char: '期' },
{ letter: 'R', char: '然' },
{ letter: 'S', char: '撒' },
{ letter: 'T', char: '塌' },
{ letter: 'W', char: '挖' },
{ letter: 'X', char: '昔' },
{ letter: 'Y', char: '压' },
{ letter: 'Z', char: '匝' }
]
const groupKeyForName = (name: string): string => {
const first = name.trim().charAt(0)
if (!first) return '#'
const upper = first.toUpperCase()
if (upper >= 'A' && upper <= 'Z') return upper
if (upper >= '0' && upper <= '9') return '#'
if (/[\u3400-\u9fff]/.test(first)) {
for (let i = 0; i < pinyinInitialBoundaries.length; i += 1) {
const current = pinyinInitialBoundaries[i]
const next = pinyinInitialBoundaries[i + 1]
if (!next) return current.letter
if (collator.compare(first, next.char) < 0) return current.letter
}
}
return '#'
}
const groupedApps = computed(() => {
const sorted = [...filteredApps.value].sort((a, b) => collator.compare(a.name, b.name))
const groups: Array<{
key: string
label: string
items: Array<{ id: string; name: string; filePath: string; iconDataUrl: string }>
}> = []
for (const app of sorted) {
const key = groupKeyForName(app.name)
const last = groups[groups.length - 1]
if (!last || last.key !== key) {
groups.push({ key, label: key, items: [app] })
} else {
last.items.push(app)
}
}
const alpha = groups.filter((g) => g.key !== '#')
const other = groups.filter((g) => g.key === '#')
return [...alpha, ...other]
})
const launch = async (filePath: string): Promise<void> => {
await window.api.call({ method: 'POST', path: '/apps/launch', body: { filePath } })
}
const handleAppPointerDown = (event: PointerEvent, filePath: string): void => {
if (launchTimerId) window.clearTimeout(launchTimerId)
launchTimerId = undefined
const element = event.currentTarget instanceof HTMLElement ? event.currentTarget : null
activeAppPointer.value = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
startedAt: performance.now(),
moved: false,
filePath,
element
}
try {
element?.setPointerCapture(event.pointerId)
} catch {}
}
const handleAppPointerMove = (event: PointerEvent): void => {
const state = activeAppPointer.value
if (!state || state.pointerId !== event.pointerId) return
if (state.moved) return
const dx = Math.abs(event.clientX - state.startX)
const dy = Math.abs(event.clientY - state.startY)
if (dx >= 12 || dy >= 12) state.moved = true
}
const handleAppPointerCancel = (event: PointerEvent): void => {
const state = activeAppPointer.value
if (!state || state.pointerId !== event.pointerId) return
activeAppPointer.value = null
}
const handleAppPointerUp = (event: PointerEvent): void => {
const state = activeAppPointer.value
if (!state || state.pointerId !== event.pointerId) return
activeAppPointer.value = null
try {
state.element?.releasePointerCapture(event.pointerId)
} catch {}
const elapsed = performance.now() - state.startedAt
if (state.moved) return
if (isAppsScrolling.value) return
if (elapsed > 900) return
const launchDelayMs = 180
launchTimerId = window.setTimeout(async () => {
if (isAppsScrolling.value) return
await launch(state.filePath)
}, launchDelayMs)
}
const handleExit = async (): Promise<void> => {
await window.api.call({ method: 'POST', path: '/app/minimize' })
}
const copyLoadError = async (): Promise<void> => {
if (!loadError.value) return
const text = loadError.value
try {
await navigator.clipboard.writeText(text)
} catch {
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.left = '-1000px'
textarea.style.top = '-1000px'
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
}
</script>

View File

@@ -0,0 +1,67 @@
:root {
--ev-c-white: #ffffff;
--ev-c-white-soft: #f8f8f8;
--ev-c-white-mute: #f2f2f2;
--ev-c-black: #1b1b1f;
--ev-c-black-soft: #222222;
--ev-c-black-mute: #282828;
--ev-c-gray-1: #515c67;
--ev-c-gray-2: #414853;
--ev-c-gray-3: #32363f;
--ev-c-text-1: rgba(255, 255, 245, 0.86);
--ev-c-text-2: rgba(235, 235, 245, 0.6);
--ev-c-text-3: rgba(235, 235, 245, 0.38);
--ev-button-alt-border: transparent;
--ev-button-alt-text: var(--ev-c-text-1);
--ev-button-alt-bg: var(--ev-c-gray-3);
--ev-button-alt-hover-border: transparent;
--ev-button-alt-hover-text: var(--ev-c-text-1);
--ev-button-alt-hover-bg: var(--ev-c-gray-2);
}
:root {
--color-background: var(--ev-c-black);
--color-background-soft: var(--ev-c-black-soft);
--color-background-mute: var(--ev-c-black-mute);
--color-text: var(--ev-c-text-1);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
ul {
list-style: none;
}
body {
min-height: 100vh;
color: var(--color-text);
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,384 @@
@import './base.css';
body {
margin: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
background-image: url('./wavy-lines.svg');
background-size: cover;
user-select: none;
}
#root {
width: 100%;
height: 100%;
}
.screen {
width: 100%;
height: 100%;
}
.touchButton {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.launcher {
position: fixed;
left: 16px;
top: 84px;
width: 520px;
height: calc(100vh - 168px);
padding: 12px;
background: rgba(0, 0, 0, 0.28);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
backdrop-filter: blur(12px);
box-sizing: border-box;
}
.home {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.openAppsButton {
cursor: pointer;
border-radius: 18px;
padding: 20px 28px;
border: 1px solid rgba(255, 255, 255, 0.16);
color: var(--ev-c-text-1);
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(12px);
font-size: 22px;
line-height: 26px;
font-weight: 800;
}
.openAppsButton:hover {
border-color: rgba(255, 255, 255, 0.26);
background: rgba(0, 0, 0, 0.45);
}
.appsPage {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
padding: 16px;
box-sizing: border-box;
}
.backButton {
cursor: pointer;
border-radius: 16px;
padding: 0 18px;
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--ev-c-text-1);
background: rgba(0, 0, 0, 0.35);
font-size: 18px;
line-height: 22px;
font-weight: 800;
height: 72px;
min-width: 120px;
}
.backButton:hover {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(0, 0, 0, 0.45);
}
.appsTilesArea {
position: fixed;
left: 16px;
right: 16px;
top: 88px;
bottom: 120px;
padding: 12px;
background: rgba(0, 0, 0, 0.28);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
backdrop-filter: blur(12px);
box-sizing: border-box;
overflow: hidden;
}
.appsList {
height: 100%;
overflow-x: auto;
overflow-y: hidden;
padding: 4px 6px;
box-sizing: border-box;
columns: 6 280px;
column-gap: 14px;
column-fill: auto;
touch-action: pan-x;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
scrollbar-width: none;
-ms-overflow-style: none;
}
.appsList::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
.appsLoading {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 700;
opacity: 0.9;
}
.appsBottomBar {
position: fixed;
left: 16px;
right: 16px;
bottom: 16px;
height: 92px;
padding: 10px 12px;
display: grid;
grid-template-columns: 140px 1fr 140px;
gap: 12px;
align-items: center;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
backdrop-filter: blur(12px);
box-sizing: border-box;
}
.appsSearch {
width: 100%;
height: 72px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.35);
color: var(--ev-c-text-1);
padding: 0 16px;
box-sizing: border-box;
outline: none;
font-size: 20px;
line-height: 24px;
user-select: text;
}
.appsExitButton {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
cursor: pointer;
border-radius: 16px;
padding: 0 18px;
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--ev-c-text-1);
background: rgba(0, 0, 0, 0.35);
font-size: 18px;
line-height: 22px;
font-weight: 800;
height: 72px;
min-width: 120px;
}
.appsExitButton:hover {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(0, 0, 0, 0.45);
}
.search {
width: 100%;
height: 38px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.35);
color: var(--ev-c-text-1);
padding: 0 12px;
box-sizing: border-box;
outline: none;
}
.error {
margin-top: 10px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(255, 120, 120, 0.25);
background: rgba(120, 0, 0, 0.18);
color: var(--ev-c-text-1);
font-size: 12px;
line-height: 16px;
user-select: text;
white-space: pre-wrap;
word-break: break-word;
}
.errorHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 8px;
}
.errorTitle {
font-weight: 700;
font-size: 12px;
line-height: 16px;
}
.errorBody {
white-space: pre-wrap;
word-break: break-word;
}
.copyButton {
cursor: pointer;
border-radius: 10px;
padding: 6px 10px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(0, 0, 0, 0.25);
color: var(--ev-c-text-1);
font-size: 12px;
line-height: 14px;
}
.copyButton:hover {
border-color: rgba(255, 255, 255, 0.26);
background: rgba(0, 0, 0, 0.35);
}
.list {
margin-top: 12px;
height: calc(100% - 50px);
overflow: auto;
display: grid;
gap: 10px;
}
.groupHeader {
display: inline-block;
width: 100%;
box-sizing: border-box;
margin: 0 0 10px;
padding: 6px 10px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.2);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.4px;
opacity: 0.9;
break-inside: avoid;
-webkit-column-break-inside: avoid;
}
.item {
display: inline-flex;
align-items: center;
gap: 10px;
text-align: left;
cursor: pointer;
width: 100%;
box-sizing: border-box;
min-height: 72px;
padding: 12px 14px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.25);
color: var(--ev-c-text-1);
margin: 0 0 10px;
break-inside: avoid;
-webkit-column-break-inside: avoid;
}
.item:hover {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(0, 0, 0, 0.35);
}
.item:active {
border-color: rgba(255, 255, 255, 0.26);
background: rgba(0, 0, 0, 0.5);
}
.name {
font-weight: 700;
font-size: 18px;
line-height: 22px;
}
.icon {
width: 48px;
height: 48px;
border-radius: 10px;
flex: 0 0 auto;
}
.path {
margin-top: 4px;
font-size: 12px;
line-height: 16px;
opacity: 0.75;
word-break: break-all;
}
.clock {
position: fixed;
left: 16px;
top: 16px;
padding: 10px 12px;
font-size: 28px;
line-height: 28px;
font-weight: 700;
color: var(--ev-c-text-1);
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
backdrop-filter: blur(12px);
}
.exitButton {
position: fixed;
right: 16px;
bottom: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
cursor: pointer;
text-align: center;
font-weight: 600;
border-radius: 14px;
padding: 10px 16px;
line-height: 20px;
font-size: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--ev-c-text-1);
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(12px);
}
.buttonIcon {
width: 20px;
height: 20px;
flex: 0 0 auto;
}
.exitButton:hover {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(0, 0, 0, 0.45);
}

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

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

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, unknown>
export default component
}

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

@@ -0,0 +1,6 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#root')

4
tsconfig.json Normal file
View File

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

8
tsconfig.node.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/eiysia/**/*", "src/app_list/**/*"],
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"]
}
}

18
tsconfig.web.json Normal file
View File

@@ -0,0 +1,18 @@
{
"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"
],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@renderer/*": [
"src/renderer/src/*"
]
}
}
}