├── .github └── workflows │ ├── release.yml │ └── rolling-release.yml ├── .gitignore ├── GUI.for.SingBox.code-workspace ├── LICENSE ├── README.md ├── bridge ├── bridge.go ├── exec.go ├── exec_others.go ├── exec_windows.go ├── io.go ├── mmdb.go ├── net.go ├── notification.go ├── scheduledtasks.go ├── server.go ├── tray.go ├── types.go └── utils.go ├── build ├── README.md ├── appicon.png ├── darwin │ ├── Info.dev.plist │ └── Info.plist └── windows │ ├── icon.ico │ ├── info.json │ └── wails.exe.manifest ├── docs └── imgs │ ├── dark.png │ └── light.png ├── frontend ├── .editorconfig ├── .env ├── .gitattributes ├── .gitignore ├── .prettierrc.json ├── .vscode │ └── extensions.json ├── README.md ├── env.d.ts ├── eslint.config.js ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public │ ├── favicon.ico │ ├── icons │ │ ├── tray_normal_dark.ico │ │ ├── tray_normal_light.ico │ │ ├── tray_proxy_dark.ico │ │ ├── tray_proxy_light.ico │ │ ├── tray_tun_dark.ico │ │ └── tray_tun_light.ico │ └── imgs │ │ ├── notify_error.png │ │ ├── notify_success.png │ │ ├── tray_normal_dark.png │ │ ├── tray_normal_light.png │ │ ├── tray_proxy_dark.png │ │ ├── tray_proxy_light.png │ │ ├── tray_tun_dark.png │ │ └── tray_tun_light.png ├── src │ ├── App.vue │ ├── api │ │ └── kernel.ts │ ├── assets │ │ ├── logo.png │ │ ├── main.less │ │ ├── theme.less │ │ └── variables.less │ ├── bridge │ │ ├── app.ts │ │ ├── exec.ts │ │ ├── index.ts │ │ ├── io.ts │ │ ├── mmdb.ts │ │ ├── net.ts │ │ ├── scheduledTasks.ts │ │ └── server.ts │ ├── components │ │ ├── Button │ │ │ └── index.vue │ │ ├── Card │ │ │ └── index.vue │ │ ├── CheckBox │ │ │ └── index.vue │ │ ├── CodeViewer │ │ │ └── index.vue │ │ ├── Confirm │ │ │ └── index.vue │ │ ├── CustomAction │ │ │ └── index.vue │ │ ├── Divider │ │ │ └── index.vue │ │ ├── Dropdown │ │ │ └── index.vue │ │ ├── Empty │ │ │ └── index.vue │ │ ├── Icon │ │ │ ├── AddIcon.vue │ │ │ ├── ArrowDownIcon.vue │ │ │ ├── ArrowLeftIcon.vue │ │ │ ├── ArrowRightIcon.vue │ │ │ ├── Clear2Icon.vue │ │ │ ├── ClearIcon.vue │ │ │ ├── CloseIcon.vue │ │ │ ├── CodeIcon.vue │ │ │ ├── CollapseIcon.vue │ │ │ ├── DeleteIcon.vue │ │ │ ├── DisabledIcon.vue │ │ │ ├── DragIcon.vue │ │ │ ├── EditIcon.vue │ │ │ ├── EmptyIcon.vue │ │ │ ├── ErrorIcon.vue │ │ │ ├── ExpandIcon.vue │ │ │ ├── FileIcon.vue │ │ │ ├── FilterIcon.vue │ │ │ ├── FolderIcon.vue │ │ │ ├── ForbiddenIcon.vue │ │ │ ├── GithubIcon.vue │ │ │ ├── GrantIcon.vue │ │ │ ├── LinkIcon.vue │ │ │ ├── LoadingIcon.vue │ │ │ ├── LogIcon.vue │ │ │ ├── Maximize2Icon.vue │ │ │ ├── MaximizeIcon.vue │ │ │ ├── MessageErrorIcon.vue │ │ │ ├── MessageInfoIcon.vue │ │ │ ├── MessageSuccessIcon.vue │ │ │ ├── MessageWarnIcon.vue │ │ │ ├── MinimizeIcon.vue │ │ │ ├── MoreIcon.vue │ │ │ ├── OverviewIcon.vue │ │ │ ├── PauseIcon.vue │ │ │ ├── PinFillIcon.vue │ │ │ ├── PinIcon.vue │ │ │ ├── PlayIcon.vue │ │ │ ├── PluginsIcon.vue │ │ │ ├── PreviewIcon.vue │ │ │ ├── ProfilesIcon.vue │ │ │ ├── RefreshIcon.vue │ │ │ ├── ResetIcon.vue │ │ │ ├── RestartAppIcon.vue │ │ │ ├── RestartIcon.vue │ │ │ ├── RollbackIcon.vue │ │ │ ├── RulesetsIcon.vue │ │ │ ├── ScheduledTasksIcon.vue │ │ │ ├── SelectedIcon.vue │ │ │ ├── Settings2Icon.vue │ │ │ ├── SettingsIcon.vue │ │ │ ├── SpeedTestIcon.vue │ │ │ ├── StopIcon.vue │ │ │ ├── SubscriptionsIcon.vue │ │ │ ├── TelegramIcon.vue │ │ │ ├── icons.ts │ │ │ └── index.vue │ │ ├── Input │ │ │ └── index.vue │ │ ├── InputList │ │ │ └── index.vue │ │ ├── InterfaceSelect │ │ │ └── index.vue │ │ ├── KeyValueEditor │ │ │ └── index.vue │ │ ├── MainPage.vue │ │ ├── Menu │ │ │ └── index.vue │ │ ├── Message │ │ │ └── index.vue │ │ ├── Modal │ │ │ ├── index.ts │ │ │ └── index.vue │ │ ├── NavigationBar.vue │ │ ├── Pagination │ │ │ └── index.vue │ │ ├── Picker │ │ │ └── index.vue │ │ ├── Progress │ │ │ └── index.vue │ │ ├── Prompt │ │ │ └── index.vue │ │ ├── Radio │ │ │ └── index.vue │ │ ├── Select │ │ │ └── index.vue │ │ ├── Switch │ │ │ └── index.vue │ │ ├── Table │ │ │ └── index.vue │ │ ├── Tabs │ │ │ └── index.vue │ │ ├── Tag │ │ │ └── index.vue │ │ ├── Tips │ │ │ └── index.vue │ │ ├── TitleBar.vue │ │ ├── TrafficChart │ │ │ └── index.vue │ │ └── index.ts │ ├── constant │ │ ├── app.ts │ │ ├── kernel.ts │ │ └── profile.ts │ ├── directives │ │ ├── index.ts │ │ ├── menu.ts │ │ └── tips.ts │ ├── enums │ │ ├── app.ts │ │ └── kernel.ts │ ├── globalMethods.ts │ ├── hooks │ │ ├── index.ts │ │ ├── useBool.ts │ │ └── useCoreBranch.ts │ ├── lang │ │ ├── index.ts │ │ └── locale │ │ │ ├── en.ts │ │ │ ├── fa.ts │ │ │ ├── ru.ts │ │ │ └── zh.ts │ ├── main.ts │ ├── router │ │ ├── index.ts │ │ ├── router.d.ts │ │ └── routes.ts │ ├── stores │ │ ├── app.ts │ │ ├── appSettings.ts │ │ ├── env.ts │ │ ├── index.ts │ │ ├── kernelApi.ts │ │ ├── logs.ts │ │ ├── plugins.ts │ │ ├── profiles.ts │ │ ├── rulesets.ts │ │ ├── scheduledtasks.ts │ │ └── subscribes.ts │ ├── types │ │ ├── app.d.ts │ │ ├── global.d.ts │ │ ├── kernel.d.ts │ │ ├── profile.d.ts │ │ └── typescript.d.ts │ ├── utils │ │ ├── command.ts │ │ ├── completion.ts │ │ ├── env.ts │ │ ├── format.ts │ │ ├── generator.ts │ │ ├── helper.ts │ │ ├── index.ts │ │ ├── interaction.ts │ │ ├── is.ts │ │ ├── others.ts │ │ ├── profilesUpgrader.ts │ │ ├── request.ts │ │ ├── restorer.ts │ │ ├── tray.ts │ │ └── websockets.ts │ └── views │ │ ├── AboutView.vue │ │ ├── CommandView.vue │ │ ├── HomeView │ │ ├── components │ │ │ ├── CommonController.vue │ │ │ ├── ConnectionsController.vue │ │ │ ├── GroupsController.vue │ │ │ ├── KernelLogs.vue │ │ │ ├── LogsController.vue │ │ │ ├── OverView.vue │ │ │ └── QuickStart.vue │ │ └── index.vue │ │ ├── PluginsView │ │ ├── components │ │ │ ├── PluginChangelog.vue │ │ │ ├── PluginConfiguration.vue │ │ │ ├── PluginForm.vue │ │ │ ├── PluginHub.vue │ │ │ └── PluginView.vue │ │ └── index.vue │ │ ├── ProfilesView │ │ ├── components │ │ │ ├── DnsConfig.vue │ │ │ ├── DnsRulesConfig.vue │ │ │ ├── DnsServersConfig.vue │ │ │ ├── GeneralConfig.vue │ │ │ ├── InboundsConfig.vue │ │ │ ├── MixinAndScriptConfig.vue │ │ │ ├── OutboundsConfig.vue │ │ │ ├── ProfileForm.vue │ │ │ ├── RouteConfig.vue │ │ │ ├── RouteRulesConfig.vue │ │ │ └── RouteRulesetConfig.vue │ │ └── index.vue │ │ ├── RulesetsView │ │ ├── components │ │ │ ├── RulesetForm.vue │ │ │ ├── RulesetHub.vue │ │ │ └── RulesetView.vue │ │ └── index.vue │ │ ├── ScheduledTasksView │ │ ├── components │ │ │ ├── ScheduledTaskForm.vue │ │ │ └── ScheduledTasksLogs.vue │ │ └── index.vue │ │ ├── SettingsView │ │ ├── components │ │ │ ├── CoreSettings.vue │ │ │ ├── GeneralSettings.vue │ │ │ └── components │ │ │ │ ├── BranchDetail.vue │ │ │ │ └── SwitchBranch.vue │ │ └── index.vue │ │ ├── SplashView.vue │ │ └── SubscribesView │ │ ├── components │ │ ├── ProxiesEditor.vue │ │ ├── ProxiesView.vue │ │ ├── SubscribeForm.vue │ │ └── SubscribeScript.vue │ │ └── index.vue ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── wailsjs │ ├── go │ ├── bridge │ │ ├── App.d.ts │ │ └── App.js │ └── models.ts │ └── runtime │ ├── package.json │ ├── runtime.d.ts │ └── runtime.js ├── go.mod ├── go.sum ├── main.go └── wails.json /.github/workflows/rolling-release.yml: -------------------------------------------------------------------------------- 1 | name: Rolling Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "frontend/**" 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | Build: 13 | permissions: write-all 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Set up pnpm 20 | uses: pnpm/action-setup@v4 21 | with: 22 | version: 9 23 | - name: Set up Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: "latest" 27 | cache: "pnpm" 28 | cache-dependency-path: frontend/pnpm-lock.yaml 29 | - name: Install dependencies 30 | run: cd frontend && pnpm install 31 | - name: Build Frontend 32 | run: cd frontend && pnpm build-only 33 | - name: Create a compressed file 34 | run: | 35 | git rev-parse --short HEAD | tr -d '\n' > frontend/dist/version.txt 36 | cd frontend 37 | mv dist rolling-release 38 | zip -r rolling-release.zip rolling-release 39 | - name: Generate Changelog 40 | run: | 41 | set +e 42 | LAST_COMMIT=$(curl -L https://github.com/GUI-for-Cores/GUI.for.SingBox/releases/download/rolling-release/version.txt) 43 | echo -e "## Change log\n\n> Update time: $(TZ='Asia/Shanghai' date "+%Y-%m-%d %H:%M:%S")\n" > changelog.md 44 | git log $LAST_COMMIT..HEAD --pretty=format:"* %s" >> changelog.md 45 | if [ $? -ne 0 ]; then 46 | echo "No changes found since last commit." >> changelog.md 47 | fi 48 | set -e 49 | - name: Create Release and Upload Assets 50 | uses: svenstaro/upload-release-action@v2 51 | with: 52 | repo_token: ${{ secrets.GITHUB_TOKEN }} 53 | file: ./{frontend/{rolling-release.zip,rolling-release/version.txt},changelog.md} 54 | file_glob: true 55 | tag: rolling-release 56 | release_name: rolling-release 57 | overwrite: true 58 | draft: false 59 | prerelease: true 60 | body: | 61 | Rolling release built by GitHub Actions. 62 | To use this version, please install the "Rolling Release Assistant" plugin and enable "Enable Rolling Release" within the app. 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/bin 2 | frontend/dist 3 | 4 | .DS_Store -------------------------------------------------------------------------------- /GUI.for.SingBox.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "frontend" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | GUI.for.SingBox 3 |

GUI.for.SingBox

4 |

A GUI program developed by vue3 + wails.

5 |
6 | 7 | ## Preview 8 | 9 |
10 | 11 | 12 |
13 | 14 | ## Document 15 | 16 | [Community](https://gui-for-cores.github.io/guide/gfs/community) 17 | 18 | ## Build 19 | 20 | 1、Build Environment 21 | 22 | - Node.js [link](https://nodejs.org/en) 23 | 24 | - pnpm :`npm i -g pnpm` 25 | 26 | - Go [link](https://go.dev/) 27 | 28 | - Wails [link](https://wails.io/) :`go install github.com/wailsapp/wails/v2/cmd/wails@latest` 29 | 30 | 2、Pull and Build 31 | 32 | ```bash 33 | git clone https://github.com/GUI-for-Cores/GUI.for.SingBox.git 34 | 35 | cd GUI.for.SingBox/frontend 36 | 37 | pnpm install 38 | 39 | pnpm build 40 | 41 | cd .. 42 | 43 | wails build 44 | ``` 45 | 46 | ## Stargazers over time 47 | 48 | [![Stargazers over time](https://starchart.cc/GUI-for-Cores/GUI.for.SingBox.svg)](https://starchart.cc/GUI-for-Cores/GUI.for.SingBox) 49 | -------------------------------------------------------------------------------- /bridge/exec_others.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package bridge 4 | 5 | import ( 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | func SetCmdWindowHidden(cmd *exec.Cmd) { 12 | } 13 | 14 | func SendExitSignal(process *os.Process) error { 15 | return process.Signal(syscall.SIGINT) 16 | } 17 | -------------------------------------------------------------------------------- /bridge/exec_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package bridge 4 | 5 | import ( 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | func SetCmdWindowHidden(cmd *exec.Cmd) { 14 | cmd.SysProcAttr = &syscall.SysProcAttr{ 15 | CreationFlags: windows.CREATE_UNICODE_ENVIRONMENT | windows.CREATE_NEW_PROCESS_GROUP, 16 | HideWindow: true, 17 | } 18 | } 19 | 20 | func SendExitSignal(p *os.Process) error { 21 | kernel32DLL, err := windows.LoadDLL("kernel32.dll") 22 | if err != nil { 23 | return err 24 | } 25 | defer kernel32DLL.Release() 26 | 27 | freeConsoleProc, err := kernel32DLL.FindProc("FreeConsole") 28 | if err == nil { 29 | if result, _, err := freeConsoleProc.Call(); result == 0 { 30 | if err != windows.ERROR_INVALID_HANDLE { 31 | return err 32 | } 33 | } 34 | } 35 | 36 | attachConsoleProc, err := kernel32DLL.FindProc("AttachConsole") 37 | if err != nil { 38 | return err 39 | } 40 | if result, _, err := attachConsoleProc.Call(uintptr(p.Pid)); result == 0 { 41 | if err != windows.ERROR_ACCESS_DENIED { 42 | return err 43 | } 44 | } 45 | 46 | setConsoleCtrlHandlerProc, err := kernel32DLL.FindProc("SetConsoleCtrlHandler") 47 | if err != nil { 48 | return err 49 | } 50 | if result, _, err := setConsoleCtrlHandlerProc.Call(0, 1); result == 0 { 51 | return err 52 | } 53 | 54 | generateConsoleCtrlEventProc, err := kernel32DLL.FindProc("GenerateConsoleCtrlEvent") 55 | if err != nil { 56 | return err 57 | } 58 | if result, _, err := generateConsoleCtrlEventProc.Call(windows.CTRL_BREAK_EVENT, uintptr(p.Pid)); result == 0 { 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /bridge/mmdb.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "log" 7 | "net" 8 | "sync" 9 | 10 | "github.com/oschwald/geoip2-golang" 11 | ) 12 | 13 | type MMDBInstance = struct { 14 | Refs map[string]bool 15 | Reader *geoip2.Reader 16 | } 17 | 18 | var ( 19 | mu sync.RWMutex 20 | mmdbMap = make(map[string]*MMDBInstance) 21 | ) 22 | 23 | func (a *App) OpenMMDB(path string, id string) FlagResult { 24 | log.Printf("OpenMMDB: %s -> %s", id, path) 25 | 26 | mu.Lock() 27 | defer mu.Unlock() 28 | 29 | if db, exists := mmdbMap[path]; exists { 30 | db.Refs[id] = true 31 | return FlagResult{true, "Success"} 32 | } 33 | 34 | reader, err := geoip2.Open(GetPath(path)) 35 | if err != nil { 36 | return FlagResult{false, "Failed to open mmdb: " + err.Error()} 37 | } 38 | 39 | mmdbMap[path] = &MMDBInstance{ 40 | Refs: map[string]bool{id: true}, 41 | Reader: reader, 42 | } 43 | 44 | return FlagResult{true, "Success"} 45 | } 46 | 47 | func (a *App) CloseMMDB(path string, id string) FlagResult { 48 | log.Printf("CloseMMDB: %s -> %s", id, path) 49 | 50 | mu.Lock() 51 | defer mu.Unlock() 52 | 53 | db, exists := mmdbMap[path] 54 | 55 | if !exists { 56 | return FlagResult{false, "Database not open: " + path} 57 | } 58 | 59 | if !db.Refs[id] { 60 | return FlagResult{false, "Reference not found for: " + id} 61 | } 62 | 63 | delete(db.Refs, id) 64 | 65 | if len(db.Refs) == 0 { 66 | if err := db.Reader.Close(); err != nil { 67 | return FlagResult{false, "Failed to close reader: " + err.Error()} 68 | } 69 | delete(mmdbMap, path) 70 | } 71 | 72 | return FlagResult{true, "Success"} 73 | } 74 | 75 | func (a *App) QueryMMDB(path string, ip string, dataType string) FlagResult { 76 | log.Printf("QueryMMDB: %s -> %s", path, ip) 77 | 78 | parsedIP := net.ParseIP(ip) 79 | if parsedIP == nil { 80 | return FlagResult{false, "Invalid IP address"} 81 | } 82 | 83 | mu.RLock() 84 | db, exists := mmdbMap[path] 85 | mu.RUnlock() 86 | 87 | if !exists { 88 | return FlagResult{false, "Database not open: " + path} 89 | } 90 | 91 | record, err := queryByType(db.Reader, parsedIP, dataType) 92 | if err != nil { 93 | return FlagResult{false, err.Error()} 94 | } 95 | 96 | bytes, err := json.Marshal(record) 97 | if err != nil { 98 | return FlagResult{false, err.Error()} 99 | } 100 | 101 | return FlagResult{true, string(bytes)} 102 | } 103 | 104 | func queryByType(reader *geoip2.Reader, ip net.IP, dataType string) (any, error) { 105 | switch dataType { 106 | case "ASN": 107 | return reader.ASN(ip) 108 | case "AnonymousIP": 109 | return reader.AnonymousIP(ip) 110 | case "City": 111 | return reader.City(ip) 112 | case "ConnectionType": 113 | return reader.ConnectionType(ip) 114 | case "Country": 115 | return reader.Country(ip) 116 | case "Domain": 117 | return reader.Domain(ip) 118 | case "Enterprise": 119 | return reader.Enterprise(ip) 120 | default: 121 | return nil, errors.New("Unsupported query type: " + dataType) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /bridge/notification.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "github.com/gen2brain/beeep" 5 | ) 6 | 7 | func (a *App) Notify(title string, message string, icon string) FlagResult { 8 | fullPath := GetPath(icon) 9 | 10 | err := beeep.Notify(title, message, fullPath) 11 | if err != nil { 12 | return FlagResult{false, err.Error()} 13 | } 14 | 15 | return FlagResult{true, "Success"} 16 | } 17 | -------------------------------------------------------------------------------- /bridge/scheduledtasks.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | 7 | "github.com/robfig/cron/v3" 8 | "github.com/wailsapp/wails/v2/pkg/runtime" 9 | ) 10 | 11 | var tasks cron.Cron 12 | 13 | func InitScheduledTasks() { 14 | tasks = *cron.New(cron.WithSeconds()) 15 | tasks.Start() 16 | } 17 | 18 | func (a *App) AddScheduledTask(spec string, event string) FlagResult { 19 | log.Printf("AddScheduledTask: %s %s", spec, event) 20 | 21 | id, err := tasks.AddFunc(spec, func() { 22 | runtime.EventsEmit(a.Ctx, event) 23 | }) 24 | if err != nil { 25 | return FlagResult{false, err.Error()} 26 | } 27 | 28 | return FlagResult{true, strconv.Itoa(int(id))} 29 | } 30 | 31 | func (a *App) RemoveScheduledTask(id int) { 32 | log.Printf("RemoveScheduledTask: %d", id) 33 | 34 | tasks.Remove(cron.EntryID(id)) 35 | } 36 | 37 | func (a *App) ValidateCron(spec string) FlagResult { 38 | log.Printf("ValidateCron: %s", spec) 39 | 40 | id, err := tasks.AddFunc(spec, nil) 41 | if err != nil { 42 | return FlagResult{false, err.Error()} 43 | } 44 | 45 | tasks.Remove(id) 46 | 47 | return FlagResult{true, "Success"} 48 | } 49 | -------------------------------------------------------------------------------- /bridge/tray.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/energye/systray" 8 | "github.com/wailsapp/wails/v2/pkg/runtime" 9 | ) 10 | 11 | func CreateTray(a *App, icon []byte) (trayStart, trayEnd func()) { 12 | return systray.RunWithExternalLoop(func() { 13 | systray.SetIcon(icon) 14 | systray.SetTooltip("GUI.for.Cores") 15 | 16 | systray.SetOnRClick(func(menu systray.IMenu) { menu.ShowMenu() }) 17 | systray.SetOnClick(func(menu systray.IMenu) { 18 | if Env.OS == "darwin" { 19 | menu.ShowMenu() 20 | } else { 21 | a.ShowMainWindow() 22 | } 23 | }) 24 | 25 | // Ensure the tray is still available if rolling-release fails 26 | addClickMenuItem("Show", "Show", func() { a.ShowMainWindow() }) 27 | addClickMenuItem("Restart", "Restart", func() { a.RestartApp() }) 28 | addClickMenuItem("Exit", "Exit", func() { a.ExitApp() }) 29 | }, nil) 30 | } 31 | 32 | func (a *App) UpdateTrayMenus(menus []MenuItem) { 33 | log.Printf("UpdateTrayMenus") 34 | 35 | systray.ResetMenu() 36 | 37 | for _, menu := range menus { 38 | createMenuItem(menu, a, nil) 39 | } 40 | } 41 | 42 | func addClickMenuItem(title, tooltip string, action func()) *systray.MenuItem { 43 | m := systray.AddMenuItem(title, tooltip) 44 | m.Click(action) 45 | return m 46 | } 47 | 48 | func createMenuItem(menu MenuItem, a *App, parent *systray.MenuItem) { 49 | if menu.Hidden { 50 | return 51 | } 52 | switch menu.Type { 53 | case "item": 54 | var m *systray.MenuItem 55 | if parent == nil { 56 | m = systray.AddMenuItem(menu.Text, menu.Tooltip) 57 | } else { 58 | m = parent.AddSubMenuItem(menu.Text, menu.Tooltip) 59 | } 60 | 61 | m.Click(func() { go runtime.EventsEmit(a.Ctx, "onMenuItemClick", menu.Event) }) 62 | 63 | if menu.Checked { 64 | m.Check() 65 | } 66 | 67 | for _, child := range menu.Children { 68 | createMenuItem(child, a, m) 69 | } 70 | case "separator": 71 | systray.AddSeparator() 72 | } 73 | } 74 | 75 | func (a *App) UpdateTray(tray TrayContent) { 76 | if tray.Icon != "" { 77 | ico, err := os.ReadFile(GetPath(tray.Icon)) 78 | if err == nil { 79 | systray.SetIcon(ico) 80 | } 81 | } 82 | if tray.Title != "" { 83 | systray.SetTitle(tray.Title) 84 | runtime.WindowSetTitle(a.Ctx, tray.Title) 85 | } 86 | if tray.Tooltip != "" { 87 | systray.SetTooltip(tray.Tooltip) 88 | } 89 | } 90 | 91 | func (a *App) ExitApp() { 92 | systray.Quit() 93 | runtime.Quit(a.Ctx) 94 | os.Exit(0) 95 | } 96 | -------------------------------------------------------------------------------- /bridge/types.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/wailsapp/wails/v2/pkg/menu" 8 | ) 9 | 10 | // App struct 11 | type App struct { 12 | Ctx context.Context 13 | AppMenu *menu.Menu 14 | } 15 | 16 | type EnvResult struct { 17 | IsStartup bool `json:"-"` 18 | FromTaskSch bool `json:"-"` 19 | AppName string `json:"appName"` 20 | BasePath string `json:"basePath"` 21 | OS string `json:"os"` 22 | ARCH string `json:"arch"` 23 | X64Level int `json:"x64Level"` 24 | } 25 | 26 | type RequestOptions struct { 27 | Proxy string 28 | Insecure bool 29 | Redirect bool 30 | Timeout int 31 | CancelId string 32 | FileField string 33 | } 34 | 35 | type ExecOptions struct { 36 | StopOutputKeyword string `json:"stopOutputKeyword"` 37 | Convert bool `json:"convert"` 38 | Env map[string]string `json:"env"` 39 | } 40 | 41 | type IOOptions struct { 42 | Mode string // Binary / Text 43 | } 44 | 45 | type FlagResult struct { 46 | Flag bool `json:"flag"` 47 | Data string `json:"data"` 48 | } 49 | 50 | type ServerOptions struct { 51 | Cert string `json:"Cert"` 52 | Key string `json:"Key"` 53 | StaticPath string `json:"StaticPath"` 54 | StaticRoute string `json:"StaticRoute"` 55 | UploadPath string `json:"UploadPath"` 56 | UploadRoute string `json:"UploadRoute"` 57 | MaxUploadSize int64 `json:"MaxUploadSize"` 58 | } 59 | 60 | type HTTPResult struct { 61 | Flag bool `json:"flag"` 62 | Status int `json:"status"` 63 | Headers http.Header `json:"headers"` 64 | Body string `json:"body"` 65 | } 66 | 67 | type AppConfig struct { 68 | WindowStartState int `yaml:"windowStartState"` 69 | WebviewGpuPolicy int `yaml:"webviewGpuPolicy"` 70 | Width int `yaml:"width"` 71 | Height int `yaml:"height"` 72 | MultipleInstance bool `yaml:"multipleInstance"` 73 | RollingRelease bool `yaml:"rollingRelease" default:"true"` 74 | StartHidden bool 75 | } 76 | 77 | type TrayContent struct { 78 | Icon string `json:"icon"` 79 | Title string `json:"title"` 80 | Tooltip string `json:"tooltip"` 81 | } 82 | 83 | type WriteTracker struct { 84 | Total int64 85 | Progress int64 86 | LastEmitted int64 87 | EmitThreshold int64 88 | ProgressChange string 89 | App *App 90 | } 91 | 92 | type MenuItem struct { 93 | Type string `json:"type"` // Menu Type: item / separator 94 | Text string `json:"text"` 95 | Tooltip string `json:"tooltip"` 96 | Event string `json:"event"` 97 | Children []MenuItem `json:"children"` 98 | Hidden bool `json:"hidden"` 99 | Checked bool `json:"checked"` 100 | } 101 | -------------------------------------------------------------------------------- /bridge/utils.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "time" 11 | 12 | "golang.org/x/text/encoding/simplifiedchinese" 13 | ) 14 | 15 | func GetPath(path string) string { 16 | if !filepath.IsAbs(path) { 17 | path = filepath.Join(Env.BasePath, path) 18 | } 19 | return filepath.Clean(path) 20 | } 21 | 22 | func GetProxy(_proxy string) func(*http.Request) (*url.URL, error) { 23 | proxy := http.ProxyFromEnvironment 24 | 25 | if _proxy != "" { 26 | proxyUrl, err := url.Parse(_proxy) 27 | if err == nil { 28 | proxy = http.ProxyURL(proxyUrl) 29 | } 30 | } 31 | 32 | return proxy 33 | } 34 | 35 | func GetTimeout(timeout int) time.Duration { 36 | if timeout <= 0 { 37 | return 15 * time.Second 38 | } 39 | return time.Duration(timeout) * time.Second 40 | } 41 | 42 | func GetHeader(headers map[string]string) http.Header { 43 | header := make(http.Header, len(headers)) 44 | for key, value := range headers { 45 | header.Set(key, value) 46 | } 47 | return header 48 | } 49 | 50 | func ConvertByte2String(byte []byte) string { 51 | decodeBytes, _ := simplifiedchinese.GB18030.NewDecoder().Bytes(byte) 52 | return string(decodeBytes) 53 | } 54 | 55 | func RollingRelease(next http.Handler) http.Handler { 56 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 57 | if !Config.RollingRelease { 58 | next.ServeHTTP(w, r) 59 | return 60 | } 61 | 62 | url := r.URL.Path 63 | if url == "/" { 64 | url = "/index.html" 65 | } 66 | 67 | log.Printf("[Rolling Release] %v %v\n", r.Method, url) 68 | 69 | file := GetPath("data/rolling-release" + url) 70 | 71 | bytes, err := os.ReadFile(file) 72 | if err != nil { 73 | next.ServeHTTP(w, r) 74 | return 75 | } 76 | 77 | ext := path.Ext(url) 78 | mime := "application/octet-stream" 79 | 80 | switch ext { 81 | case ".html": 82 | mime = "text/html" 83 | case ".ico": 84 | mime = "image/x-icon" 85 | case ".png": 86 | mime = "image/png" 87 | case ".css": 88 | mime = "text/css" 89 | case ".js": 90 | mime = "text/javascript" 91 | } 92 | 93 | w.Header().Set("Content-Type", mime) 94 | w.Write(bytes) 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /build/README.md: -------------------------------------------------------------------------------- 1 | # Build Directory 2 | 3 | The build directory is used to house all the build files and assets for your application. 4 | 5 | The structure is: 6 | 7 | * bin - Output directory 8 | * darwin - macOS specific files 9 | * windows - Windows specific files 10 | 11 | ## Mac 12 | 13 | The `darwin` directory holds files specific to Mac builds. 14 | These may be customised and used as part of the build. To return these files to the default state, simply delete them 15 | and 16 | build with `wails build`. 17 | 18 | The directory contains the following files: 19 | 20 | - `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`. 21 | - `Info.dev.plist` - same as the main plist file but used when building using `wails dev`. 22 | 23 | ## Windows 24 | 25 | The `windows` directory contains the manifest and rc files used when building with `wails build`. 26 | These may be customised for your application. To return these files to the default state, simply delete them and 27 | build with `wails build`. 28 | 29 | - `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to 30 | use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file 31 | will be created using the `appicon.png` file in the build directory. 32 | - `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`. 33 | - `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer, 34 | as well as the application itself (right click the exe -> properties -> details) 35 | - `wails.exe.manifest` - The main application manifest file. -------------------------------------------------------------------------------- /build/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/build/appicon.png -------------------------------------------------------------------------------- /build/darwin/Info.dev.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundlePackageType 5 | APPL 6 | CFBundleName 7 | {{.Info.ProductName}} 8 | CFBundleExecutable 9 | {{.Name}} 10 | CFBundleIdentifier 11 | com.wails.{{.Name}} 12 | CFBundleVersion 13 | {{.Info.ProductVersion}} 14 | CFBundleGetInfoString 15 | {{.Info.Comments}} 16 | CFBundleShortVersionString 17 | {{.Info.ProductVersion}} 18 | CFBundleIconFile 19 | iconfile 20 | LSMinimumSystemVersion 21 | 10.13.0 22 | NSHighResolutionCapable 23 | true 24 | NSHumanReadableCopyright 25 | {{.Info.Copyright}} 26 | {{if .Info.FileAssociations}} 27 | CFBundleDocumentTypes 28 | 29 | {{range .Info.FileAssociations}} 30 | 31 | CFBundleTypeExtensions 32 | 33 | {{.Ext}} 34 | 35 | CFBundleTypeName 36 | {{.Name}} 37 | CFBundleTypeRole 38 | {{.Role}} 39 | CFBundleTypeIconFile 40 | {{.IconName}} 41 | 42 | {{end}} 43 | 44 | {{end}} 45 | {{if .Info.Protocols}} 46 | CFBundleURLTypes 47 | 48 | {{range .Info.Protocols}} 49 | 50 | CFBundleURLName 51 | com.wails.{{.Scheme}} 52 | CFBundleURLSchemes 53 | 54 | {{.Scheme}} 55 | 56 | CFBundleTypeRole 57 | {{.Role}} 58 | 59 | {{end}} 60 | 61 | {{end}} 62 | NSAppTransportSecurity 63 | 64 | NSAllowsLocalNetworking 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /build/darwin/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundlePackageType 5 | APPL 6 | CFBundleName 7 | {{.Info.ProductName}} 8 | CFBundleExecutable 9 | {{.Name}} 10 | CFBundleIdentifier 11 | com.wails.{{.Name}} 12 | CFBundleVersion 13 | {{.Info.ProductVersion}} 14 | CFBundleGetInfoString 15 | {{.Info.Comments}} 16 | CFBundleShortVersionString 17 | {{.Info.ProductVersion}} 18 | CFBundleIconFile 19 | iconfile 20 | LSMinimumSystemVersion 21 | 10.13.0 22 | NSHighResolutionCapable 23 | true 24 | NSHumanReadableCopyright 25 | {{.Info.Copyright}} 26 | LSUIElement 27 | true 28 | {{if .Info.FileAssociations}} 29 | CFBundleDocumentTypes 30 | 31 | {{range .Info.FileAssociations}} 32 | 33 | CFBundleTypeExtensions 34 | 35 | {{.Ext}} 36 | 37 | CFBundleTypeName 38 | {{.Name}} 39 | CFBundleTypeRole 40 | {{.Role}} 41 | CFBundleTypeIconFile 42 | {{.IconName}} 43 | 44 | {{end}} 45 | 46 | {{end}} 47 | {{if .Info.Protocols}} 48 | CFBundleURLTypes 49 | 50 | {{range .Info.Protocols}} 51 | 52 | CFBundleURLName 53 | com.wails.{{.Scheme}} 54 | CFBundleURLSchemes 55 | 56 | {{.Scheme}} 57 | 58 | CFBundleTypeRole 59 | {{.Role}} 60 | 61 | {{end}} 62 | 63 | {{end}} 64 | 65 | 66 | -------------------------------------------------------------------------------- /build/windows/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/build/windows/icon.ico -------------------------------------------------------------------------------- /build/windows/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "fixed": { 3 | "file_version": "{{.Info.ProductVersion}}" 4 | }, 5 | "info": { 6 | "0000": { 7 | "ProductVersion": "{{.Info.ProductVersion}}", 8 | "CompanyName": "{{.Info.CompanyName}}", 9 | "FileDescription": "{{.Info.ProductName}}", 10 | "LegalCopyright": "{{.Info.Copyright}}", 11 | "ProductName": "{{.Info.ProductName}}", 12 | "Comments": "{{.Info.Comments}}" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /build/windows/wails.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | true/pm 12 | permonitorv2,permonitor 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/imgs/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/docs/imgs/dark.png -------------------------------------------------------------------------------- /docs/imgs/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/docs/imgs/light.png -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}] 2 | charset = utf-8 3 | indent_size = 2 4 | indent_style = space 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | end_of_line = lf 9 | max_line_length = 100 10 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | VITE_APP_TITLE = GUI.for.SingBox 2 | VITE_APP_VERSION = v1.9.7 3 | VITE_APP_PROJECT_URL = https://github.com/GUI-for-Cores/GUI.for.SingBox 4 | VITE_APP_VERSION_API = https://api.github.com/repos/GUI-for-Cores/GUI.for.SingBox/releases/latest 5 | VITE_APP_TG_GROUP = https://t.me/GUI_for_Cores 6 | VITE_APP_TG_CHANNEL = https://t.me/GUI_for_Cores_Channel -------------------------------------------------------------------------------- /frontend/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | 32 | package.json.md5 33 | components.d.ts -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "dbaeumer.vscode-eslint", 5 | "EditorConfig.EditorConfig", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | ## Project Setup 2 | 3 | ```sh 4 | pnpm install 5 | ``` 6 | 7 | ### Compile and Hot-Reload for Development 8 | 9 | ```sh 10 | pnpm dev 11 | ``` 12 | 13 | ### Type-Check, Compile and Minify for Production 14 | 15 | ```sh 16 | pnpm build 17 | ``` 18 | 19 | ### Lint with [ESLint](https://eslint.org/) 20 | 21 | ```sh 22 | pnpm lint 23 | ``` 24 | -------------------------------------------------------------------------------- /frontend/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_APP_TITLE: string 5 | readonly VITE_APP_VERSION: string 6 | readonly VITE_APP_PROJECT_URL: string 7 | readonly VITE_APP_TG_GROUP: string 8 | readonly VITE_APP_TG_CHANNEL: string 9 | readonly VITE_APP_VERSION_API: string 10 | } 11 | 12 | interface ImportMeta { 13 | readonly env: ImportMetaEnv 14 | } 15 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' 2 | import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' 3 | import { globalIgnores } from 'eslint/config' 4 | import importPlugin from 'eslint-plugin-import' 5 | import pluginVue from 'eslint-plugin-vue' 6 | export default defineConfigWithVueTs( 7 | { 8 | name: 'app/files-to-lint', 9 | files: ['**/*.{ts,mts,tsx,vue}'], 10 | }, 11 | 12 | globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**', '**/wailsjs/**']), 13 | 14 | pluginVue.configs['flat/essential'], 15 | vueTsConfigs.recommended, 16 | { 17 | plugins: { import: importPlugin }, 18 | rules: { 19 | 'import/order': [ 20 | 'error', 21 | { 22 | groups: [ 23 | 'builtin', 24 | 'external', 25 | 'internal', 26 | ['parent', 'sibling', 'index'], 27 | 'type', 28 | 'object', 29 | ], 30 | pathGroups: [ 31 | { 32 | pattern: '@/components/**', 33 | group: 'internal', 34 | position: 'after', 35 | }, 36 | { 37 | pattern: '@/**', 38 | group: 'internal', 39 | position: 'before', 40 | }, 41 | ], 42 | pathGroupsExcludedImportTypes: ['builtin', 'type'], 43 | 'newlines-between': 'always', 44 | alphabetize: { 45 | order: 'asc', 46 | caseInsensitive: true, 47 | }, 48 | }, 49 | ], 50 | }, 51 | }, 52 | { 53 | rules: { 54 | '@typescript-eslint/no-explicit-any': ['off'], 55 | '@typescript-eslint/no-unused-expressions': ['off'], 56 | 'vue/multi-word-component-names': [ 57 | 'error', 58 | { 59 | ignores: ['index'], 60 | }, 61 | ], 62 | }, 63 | }, 64 | skipFormatting, 65 | ) 66 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %VITE_APP_TITLE% 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "run-p type-check \"build-only {@}\" --", 9 | "preview": "vite preview", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --build", 12 | "lint": "eslint . --fix", 13 | "format": "prettier --write src/" 14 | }, 15 | "dependencies": { 16 | "@codemirror/autocomplete": "6.18.6", 17 | "@codemirror/commands": "^6.8.1", 18 | "@codemirror/lang-javascript": "^6.2.4", 19 | "@codemirror/lang-json": "^6.0.1", 20 | "@codemirror/lang-yaml": "6.1.2", 21 | "@codemirror/lint": "^6.8.5", 22 | "@codemirror/merge": "^6.10.1", 23 | "@codemirror/state": "^6.5.2", 24 | "@codemirror/theme-one-dark": "^6.1.2", 25 | "@codemirror/view": "^6.37.1", 26 | "codemirror": "6.0.1", 27 | "marked": "^15.0.12", 28 | "pinia": "^3.0.3", 29 | "prettier": "^3.5.3", 30 | "vue": "^3.5.16", 31 | "vue-draggable-plus": "^0.6.0", 32 | "vue-i18n": "^11.1.5", 33 | "vue-router": "^4.5.1", 34 | "yaml": "^2.8.0" 35 | }, 36 | "devDependencies": { 37 | "@tsconfig/node22": "^22.0.2", 38 | "@types/node": "^22.15.30", 39 | "@vitejs/plugin-vue": "^5.2.4", 40 | "@vue/eslint-config-prettier": "^10.2.0", 41 | "@vue/eslint-config-typescript": "^14.5.0", 42 | "@vue/tsconfig": "^0.7.0", 43 | "eslint": "^9.28.0", 44 | "eslint-plugin-import": "^2.31.0", 45 | "eslint-plugin-vue": "^10.2.0", 46 | "less": "^4.3.0", 47 | "npm-run-all2": "^8.0.4", 48 | "typescript": "~5.8.3", 49 | "unplugin-vue-components": "^28.7.0", 50 | "vite": "^6.3.5", 51 | "vue-tsc": "^2.2.10" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/icons/tray_normal_dark.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/icons/tray_normal_dark.ico -------------------------------------------------------------------------------- /frontend/public/icons/tray_normal_light.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/icons/tray_normal_light.ico -------------------------------------------------------------------------------- /frontend/public/icons/tray_proxy_dark.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/icons/tray_proxy_dark.ico -------------------------------------------------------------------------------- /frontend/public/icons/tray_proxy_light.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/icons/tray_proxy_light.ico -------------------------------------------------------------------------------- /frontend/public/icons/tray_tun_dark.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/icons/tray_tun_dark.ico -------------------------------------------------------------------------------- /frontend/public/icons/tray_tun_light.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/icons/tray_tun_light.ico -------------------------------------------------------------------------------- /frontend/public/imgs/notify_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/imgs/notify_error.png -------------------------------------------------------------------------------- /frontend/public/imgs/notify_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/imgs/notify_success.png -------------------------------------------------------------------------------- /frontend/public/imgs/tray_normal_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/imgs/tray_normal_dark.png -------------------------------------------------------------------------------- /frontend/public/imgs/tray_normal_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/imgs/tray_normal_light.png -------------------------------------------------------------------------------- /frontend/public/imgs/tray_proxy_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/imgs/tray_proxy_dark.png -------------------------------------------------------------------------------- /frontend/public/imgs/tray_proxy_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/imgs/tray_proxy_light.png -------------------------------------------------------------------------------- /frontend/public/imgs/tray_tun_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/imgs/tray_tun_dark.png -------------------------------------------------------------------------------- /frontend/public/imgs/tray_tun_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/public/imgs/tray_tun_light.png -------------------------------------------------------------------------------- /frontend/src/api/kernel.ts: -------------------------------------------------------------------------------- 1 | import { useAppSettingsStore, useProfilesStore } from '@/stores' 2 | import { Request } from '@/utils/request' 3 | 4 | import type { CoreApiConfig, CoreApiProxies, CoreApiConnections } from '@/types/kernel' 5 | 6 | export enum Api { 7 | Configs = '/configs', 8 | Memory = '/memory', 9 | Proxies = '/proxies', 10 | ProxyDelay = '/proxies/{0}/delay', 11 | Connections = '/connections', 12 | Traffic = '/traffic', 13 | Logs = '/logs', 14 | } 15 | 16 | const setupKernelApi = () => { 17 | const appSettingsStore = useAppSettingsStore() 18 | const profilesStore = useProfilesStore() 19 | 20 | const profile = profilesStore.getProfileById(appSettingsStore.app.kernel.profile) 21 | 22 | let base = 'http://127.0.0.1:20123' 23 | let bearer = '' 24 | 25 | if (profile) { 26 | const controller = profile.experimental.clash_api.external_controller || '127.0.0.1:20123' 27 | const [, port = 20123] = controller.split(':') 28 | base = `http://127.0.0.1:${port}` 29 | bearer = profile.experimental.clash_api.secret 30 | } 31 | 32 | request.base = base 33 | request.bearer = bearer 34 | } 35 | 36 | const request = new Request({ beforeRequest: setupKernelApi, timeout: 60 * 1000 }) 37 | 38 | export const getConfigs = () => request.get(Api.Configs) 39 | 40 | export const setConfigs = (body = {}) => request.patch(Api.Configs, body) 41 | 42 | export const getProxies = () => request.get(Api.Proxies) 43 | 44 | export const getConnections = () => request.get(Api.Connections) 45 | 46 | export const deleteConnection = (id: string) => request.delete(Api.Connections + '/' + id) 47 | 48 | export const useProxy = (group: string, proxy: string) => { 49 | return request.put(Api.Proxies + '/' + group, { name: proxy }) 50 | } 51 | 52 | export const getProxyDelay = (proxy: string, url: string) => { 53 | return request.get>(Api.ProxyDelay.replace('{0}', proxy), { 54 | url, 55 | timeout: 5000, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GUI-for-Cores/GUI.for.SingBox/bd3a6f998b2385faf99571cc895c4fe7ff66677f/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/bridge/app.ts: -------------------------------------------------------------------------------- 1 | import * as App from '@wails/go/bridge/App' 2 | 3 | import type { TrayContent } from '@/types/app' 4 | 5 | export const RestartApp = App.RestartApp 6 | 7 | export const ExitApp = App.ExitApp 8 | 9 | export const ShowMainWindow = App.ShowMainWindow 10 | 11 | export const UpdateTray = async (tray: TrayContent) => { 12 | const { icon = '', title = '', tooltip = '' } = tray 13 | await App.UpdateTray({ icon, title, tooltip }) 14 | } 15 | 16 | export const UpdateTrayMenus = App.UpdateTrayMenus 17 | 18 | export const Notify = async (title: string, message: string, icon = '') => { 19 | const icons: Record = { 20 | success: 'data/.cache/imgs/notify_success.png', 21 | error: 'data/.cache/imgs/notify_error.png', 22 | } 23 | await App.Notify(title, message, icons[icon] || 'data/.cache/imgs/tray_normal_dark.png') 24 | } 25 | 26 | export const GetEnv = App.GetEnv 27 | 28 | export const IsStartup = App.IsStartup 29 | 30 | export const GetInterfaces = async () => { 31 | const { flag, data } = await App.GetInterfaces() 32 | if (!flag) { 33 | throw data 34 | } 35 | return data.split('|') 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/bridge/exec.ts: -------------------------------------------------------------------------------- 1 | import * as App from '@wails/go/bridge/App' 2 | import { EventsOn, EventsOff } from '@wails/runtime/runtime' 3 | 4 | import { sampleID } from '@/utils' 5 | 6 | type ExecOptions = { 7 | convert: boolean 8 | env: Record 9 | stopOutputKeyword: string 10 | } 11 | 12 | export const Exec = async (path: string, args: string[], options: Partial = {}) => { 13 | const { flag, data } = await App.Exec( 14 | path, 15 | args, 16 | Object.assign({}, { convert: false, env: {}, stopOutputKeyword: '' }, options), 17 | ) 18 | if (!flag) { 19 | throw data 20 | } 21 | return data 22 | } 23 | 24 | export const ExecBackground = async ( 25 | path: string, 26 | args: string[], 27 | onOut: (out: string) => void, 28 | onEnd: () => void, 29 | options: Partial = {}, 30 | ) => { 31 | const outEvent = sampleID() 32 | const endEvent = sampleID() 33 | const { flag, data } = await App.ExecBackground( 34 | path, 35 | args, 36 | outEvent, 37 | endEvent, 38 | Object.assign({}, { convert: false, env: {}, stopOutputKeyword: '' }, options), 39 | ) 40 | if (!flag) { 41 | throw data 42 | } 43 | 44 | EventsOn(outEvent, (out: string) => { 45 | onOut && onOut(out) 46 | }) 47 | 48 | EventsOn(endEvent, () => { 49 | onEnd && onEnd() 50 | EventsOff(outEvent) 51 | EventsOff(endEvent) 52 | }) 53 | 54 | return Number(data) 55 | } 56 | 57 | export const ProcessInfo = async (pid: number) => { 58 | const { flag, data } = await App.ProcessInfo(pid) 59 | if (!flag) { 60 | throw data 61 | } 62 | return data 63 | } 64 | 65 | export const KillProcess = async (pid: number) => { 66 | const { flag, data } = await App.KillProcess(pid) 67 | if (!flag) { 68 | throw data 69 | } 70 | return data 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/bridge/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@wails/runtime/runtime' 2 | export * from './io' 3 | export * from './net' 4 | export * from './exec' 5 | export * from './app' 6 | export * from './server' 7 | export * from './mmdb' 8 | export * from './scheduledTasks' 9 | -------------------------------------------------------------------------------- /frontend/src/bridge/io.ts: -------------------------------------------------------------------------------- 1 | import * as App from '@wails/go/bridge/App' 2 | 3 | type IOOptions = { 4 | Mode?: 'Binary' | 'Text' 5 | } 6 | 7 | export const Writefile = async (path: string, content: string, options: IOOptions = {}) => { 8 | const { flag, data } = await App.Writefile(path, content, { Mode: 'Text', ...options }) 9 | if (!flag) { 10 | throw data 11 | } 12 | return data 13 | } 14 | 15 | export const Readfile = async (path: string, options: IOOptions = {}) => { 16 | const { flag, data } = await App.Readfile(path, { Mode: 'Text', ...options }) 17 | if (!flag) { 18 | throw data 19 | } 20 | return data 21 | } 22 | 23 | export const Movefile = async (source: string, target: string) => { 24 | const { flag, data } = await App.Movefile(source, target) 25 | if (!flag) { 26 | throw data 27 | } 28 | return data 29 | } 30 | 31 | export const Removefile = async (path: string) => { 32 | const { flag, data } = await App.Removefile(path) 33 | if (!flag) { 34 | throw data 35 | } 36 | return data 37 | } 38 | 39 | export const Copyfile = async (source: string, target: string) => { 40 | const { flag, data } = await App.Copyfile(source, target) 41 | if (!flag) { 42 | throw data 43 | } 44 | return data 45 | } 46 | 47 | export const FileExists = async (path: string) => { 48 | const { flag, data } = await App.FileExists(path) 49 | if (!flag) { 50 | throw data 51 | } 52 | return data === 'true' 53 | } 54 | 55 | export const AbsolutePath = async (path: string) => { 56 | const { flag, data } = await App.AbsolutePath(path) 57 | if (!flag) { 58 | throw data 59 | } 60 | return data 61 | } 62 | 63 | export const Makedir = async (path: string) => { 64 | const { flag, data } = await App.Makedir(path) 65 | if (!flag) { 66 | throw data 67 | } 68 | return data 69 | } 70 | 71 | export const Readdir = async (path: string) => { 72 | const { flag, data } = await App.Readdir(path) 73 | if (!flag) { 74 | throw data 75 | } 76 | return data 77 | .split('|') 78 | .filter((v) => v) 79 | .map((v) => { 80 | const [name, size, isDir] = v.split(',') 81 | return { name, size: Number(size), isDir: isDir === 'true' } 82 | }) 83 | } 84 | 85 | export const UnzipZIPFile = async (path: string, output: string) => { 86 | const { flag, data } = await App.UnzipZIPFile(path, output) 87 | if (!flag) { 88 | throw data 89 | } 90 | return data 91 | } 92 | 93 | export const UnzipGZFile = async (path: string, output: string) => { 94 | const { flag, data } = await App.UnzipGZFile(path, output) 95 | if (!flag) { 96 | throw data 97 | } 98 | return data 99 | } 100 | 101 | export const UnzipTarGZFile = async (path: string, output: string) => { 102 | const { flag, data } = await App.UnzipTarGZFile(path, output) 103 | if (!flag) { 104 | throw data 105 | } 106 | return data 107 | } 108 | -------------------------------------------------------------------------------- /frontend/src/bridge/mmdb.ts: -------------------------------------------------------------------------------- 1 | import * as App from '@wails/go/bridge/App' 2 | 3 | type QueryType = 4 | | 'ASN' 5 | | 'AnonymousIP' 6 | | 'City' 7 | | 'ConnectionType' 8 | | 'Country' 9 | | 'Domain' 10 | | 'Enterprise' 11 | 12 | export const OpenMMDB = async (path: string, id: string) => { 13 | const { flag, data } = await App.OpenMMDB(path, id) 14 | if (!flag) { 15 | throw data 16 | } 17 | return { 18 | close: () => CloseMMDB(path, id), 19 | query: (ip: string, type: QueryType) => QueryMMDB(path, ip, type), 20 | } 21 | } 22 | 23 | export const CloseMMDB = async (path: string, id: string) => { 24 | const { flag, data } = await App.CloseMMDB(path, id) 25 | if (!flag) { 26 | throw data 27 | } 28 | return data 29 | } 30 | 31 | export const QueryMMDB = async (path: string, ip: string, type: QueryType = 'Country') => { 32 | const { flag, data } = await App.QueryMMDB(path, ip, type) 33 | if (!flag) { 34 | throw data 35 | } 36 | return JSON.parse(data) 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/bridge/scheduledTasks.ts: -------------------------------------------------------------------------------- 1 | import * as App from '@wails/go/bridge/App' 2 | 3 | export const AddScheduledTask = async (cron: string, event: string) => { 4 | const { flag, data } = await App.AddScheduledTask(cron, event) 5 | if (!flag) { 6 | throw data 7 | } 8 | return Number(data) 9 | } 10 | 11 | export const RemoveScheduledTask = async (id: number) => { 12 | await App.RemoveScheduledTask(id) 13 | } 14 | 15 | export const ValidateCron = async (cron: string) => { 16 | const { flag, data } = await App.ValidateCron(cron) 17 | if (!flag) { 18 | throw data 19 | } 20 | return data 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/bridge/server.ts: -------------------------------------------------------------------------------- 1 | import * as App from '@wails/go/bridge/App' 2 | import { EventsOn, EventsEmit, EventsOff } from '@wails/runtime/runtime' 3 | 4 | type RequestType = { 5 | id: string 6 | method: string 7 | url: string 8 | headers: Record 9 | body: string 10 | } 11 | 12 | type ResponseType = { 13 | status: number 14 | headers: Record 15 | body: string 16 | options: { mode: 'Binary' | 'Text' } 17 | } 18 | 19 | type ServerOptions = { 20 | Cert?: string 21 | Key?: string 22 | StaticPath?: string 23 | StaticRoute?: string 24 | UploadPath?: string 25 | UploadRoute?: string 26 | MaxUploadSize?: number 27 | } 28 | 29 | type HttpServerHandler = ( 30 | req: RequestType, 31 | res: { 32 | end: ( 33 | status: ResponseType['status'], 34 | headers: ResponseType['headers'], 35 | body: ResponseType['body'], 36 | options: ResponseType['options'], 37 | ) => void 38 | }, 39 | ) => Promise 40 | 41 | export const StartServer = async ( 42 | address: string, 43 | id: string, 44 | handler: HttpServerHandler, 45 | options: ServerOptions = {}, 46 | ) => { 47 | const _options: Required = { 48 | Cert: '', 49 | Key: '', 50 | StaticPath: '', // default: /static 51 | StaticRoute: '/static/', 52 | UploadPath: '', // default: /upload 53 | UploadRoute: '/upload', 54 | MaxUploadSize: 50 * 1024 * 1024, // 50MB 55 | ...options, 56 | } 57 | const { flag, data } = await App.StartServer(address, id, _options) 58 | if (!flag) { 59 | throw data 60 | } 61 | 62 | EventsOn(id, async (...args) => { 63 | const [id, method, url, headers, body] = args 64 | try { 65 | await handler( 66 | { 67 | id, 68 | method, 69 | url, 70 | headers: Object.entries(headers).reduce((p, c: any) => ({ ...p, [c[0]]: c[1][0] }), {}), 71 | body, 72 | }, 73 | { 74 | end: (status, headers, body, options = { mode: 'Text' }) => { 75 | EventsEmit(id, status, JSON.stringify(headers), body, JSON.stringify(options)) 76 | }, 77 | }, 78 | ) 79 | } catch (err: any) { 80 | console.log('Server handler err:', err, id) 81 | EventsEmit( 82 | id, 83 | 500, 84 | JSON.stringify({ 'Content-Type': 'text/plain; charset=utf-8' }), 85 | err.message || err, 86 | JSON.stringify({ Mode: 'Text' }), 87 | ) 88 | } 89 | }) 90 | return { close: () => StopServer(id) } 91 | } 92 | 93 | export const StopServer = async (serverID: string) => { 94 | const { flag, data } = await App.StopServer(serverID) 95 | if (!flag) { 96 | throw data 97 | } 98 | EventsOff(serverID) 99 | return data 100 | } 101 | 102 | export const ListServer = async () => { 103 | const { flag, data } = await App.ListServer() 104 | if (!flag) { 105 | throw data 106 | } 107 | return data.split('|').filter((id) => id.length) 108 | } 109 | -------------------------------------------------------------------------------- /frontend/src/components/Button/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 38 | 39 | 123 | -------------------------------------------------------------------------------- /frontend/src/components/Card/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | 34 | 76 | -------------------------------------------------------------------------------- /frontend/src/components/CheckBox/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 46 | 47 | 88 | -------------------------------------------------------------------------------- /frontend/src/components/CustomAction/index.vue: -------------------------------------------------------------------------------- 1 | 44 | 61 | -------------------------------------------------------------------------------- /frontend/src/components/Divider/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /frontend/src/components/Dropdown/index.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 82 | 83 | 115 | -------------------------------------------------------------------------------- /frontend/src/components/Empty/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/AddIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/ArrowDownIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/ArrowLeftIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/ArrowRightIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/Clear2Icon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/ClearIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/CloseIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/CodeIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/CollapseIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/DeleteIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/DisabledIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/DragIcon.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/EditIcon.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/EmptyIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/ErrorIcon.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/ExpandIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/FileIcon.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/FilterIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/FolderIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/ForbiddenIcon.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/GithubIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/GrantIcon.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/LinkIcon.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/LoadingIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/LogIcon.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/Maximize2Icon.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/MaximizeIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/MessageErrorIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/MessageInfoIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/MessageSuccessIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/MessageWarnIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/MinimizeIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/MoreIcon.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/OverviewIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/PauseIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/PinFillIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/PinIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/PlayIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/PluginsIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/PreviewIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/ProfilesIcon.vue: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/RefreshIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/ResetIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/RestartAppIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/RestartIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/RollbackIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/RulesetsIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/ScheduledTasksIcon.vue: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/SelectedIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/Settings2Icon.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/SettingsIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/SpeedTestIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/StopIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/SubscriptionsIcon.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/TelegramIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/icons.ts: -------------------------------------------------------------------------------- 1 | const icons = [ 2 | 'link', 3 | 'loading', 4 | 'selected', 5 | 'disabled', 6 | 'pin', 7 | 'pinFill', 8 | 'minimize', 9 | 'maximize', 10 | 'maximize2', 11 | 'close', 12 | 'arrowLeft', 13 | 'arrowDown', 14 | 'arrowRight', 15 | 'speedTest', 16 | 'empty', 17 | 'github', 18 | 'forbidden', 19 | 'telegram', 20 | 'expand', 21 | 'collapse', 22 | 'refresh', 23 | 'error', 24 | 'reset', 25 | 'folder', 26 | 'restartApp', 27 | 'log', 28 | 'settings', 29 | 'stop', 30 | 'restart', 31 | 'messageSuccess', 32 | 'messageError', 33 | 'messageWarn', 34 | 'messageInfo', 35 | 'pause', 36 | 'play', 37 | 'clear', 38 | 'clear2', 39 | 'drag', 40 | 'more', 41 | 'add', 42 | 'filter', 43 | 'edit', 44 | 'delete', 45 | 'file', 46 | 'code', 47 | 'overview', 48 | 'profiles', 49 | 'subscriptions', 50 | 'rulesets', 51 | 'plugins', 52 | 'scheduledTasks', 53 | 'settings2', 54 | 'grant', 55 | 'preview', 56 | 'rollback', 57 | ] as const 58 | 59 | export default icons 60 | -------------------------------------------------------------------------------- /frontend/src/components/Icon/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /frontend/src/components/InputList/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 57 | 58 | 83 | -------------------------------------------------------------------------------- /frontend/src/components/InterfaceSelect/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 38 | -------------------------------------------------------------------------------- /frontend/src/components/KeyValueEditor/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 53 | 54 | 66 | -------------------------------------------------------------------------------- /frontend/src/components/MainPage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /frontend/src/components/Message/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 47 | 48 | 75 | -------------------------------------------------------------------------------- /frontend/src/components/Modal/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, defineComponent, h, computed, type VNode, shallowRef } from 'vue' 2 | 3 | import Modal, { type Props } from './index.vue' 4 | 5 | export const useModal = (options: Partial) => { 6 | const open = ref(false) 7 | const props = ref(options) 8 | const component = shallowRef() 9 | 10 | const modal = defineComponent({ 11 | setup() { 12 | const mergedProps = computed(() => ({ 13 | ...props.value, 14 | open: open.value, 15 | 'onUpdate:open': (val: boolean) => (open.value = val), 16 | })) 17 | return () => h(Modal, mergedProps.value, () => component.value) 18 | }, 19 | }) 20 | 21 | const api = { 22 | open: () => (open.value = true), 23 | close: () => (open.value = false), 24 | setProps(options: Partial & Recordable) { 25 | props.value = options 26 | return this 27 | }, 28 | setComponent(comp: VNode) { 29 | component.value = comp 30 | return this 31 | }, 32 | } 33 | 34 | return [modal, api] as const 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/components/NavigationBar.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 32 | 38 | -------------------------------------------------------------------------------- /frontend/src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 62 | -------------------------------------------------------------------------------- /frontend/src/components/Progress/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 43 | 44 | 65 | -------------------------------------------------------------------------------- /frontend/src/components/Prompt/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 59 | 60 | 76 | -------------------------------------------------------------------------------- /frontend/src/components/Radio/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 39 | 40 | 80 | -------------------------------------------------------------------------------- /frontend/src/components/Select/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 49 | 50 | 86 | -------------------------------------------------------------------------------- /frontend/src/components/Switch/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 43 | 44 | 152 | -------------------------------------------------------------------------------- /frontend/src/components/Tabs/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 49 | 50 | 80 | -------------------------------------------------------------------------------- /frontend/src/components/Tag/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 59 | -------------------------------------------------------------------------------- /frontend/src/components/Tips/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 48 | 49 | 65 | -------------------------------------------------------------------------------- /frontend/src/components/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, App, Component } from 'vue' 2 | 3 | export { default as MainPage } from './MainPage.vue' 4 | export { default as TitleBar } from './TitleBar.vue' 5 | export { default as NavigationBar } from './NavigationBar.vue' 6 | 7 | const Components = import.meta.glob('./*/index.vue', { 8 | eager: true, 9 | import: 'default', 10 | }) 11 | 12 | export default { 13 | install: (app: App) => { 14 | for (const path in Components) { 15 | const name = path.split('/')[1] 16 | app.component(name, Components[path]) 17 | } 18 | }, 19 | } as Plugin 20 | -------------------------------------------------------------------------------- /frontend/src/constant/app.ts: -------------------------------------------------------------------------------- 1 | import { Color, ControllerCloseMode, PluginTrigger, ScheduledTasksType, View } from '@/enums/app' 2 | 3 | export const UserFilePath = 'data/user.yaml' 4 | 5 | export const ProfilesFilePath = 'data/profiles.yaml' 6 | 7 | export const SubscribesFilePath = 'data/subscribes.yaml' 8 | 9 | export const RulesetsFilePath = 'data/rulesets.yaml' 10 | 11 | export const PluginsFilePath = 'data/plugins.yaml' 12 | 13 | export const ScheduledTasksFilePath = 'data/scheduledtasks.yaml' 14 | 15 | export const PluginHubFilePath = 'data/.cache/plugin-list.json' 16 | 17 | export const DefaultFontFamily = 18 | 'system-ui, "Microsoft YaHei UI", "Source Han Sans CN", "Twemoji Mozilla", sans-serif' 19 | 20 | export const Colors = { 21 | [Color.Default]: { 22 | primary: 'rgb(0, 89, 214)', 23 | secondary: 'rgb(5, 62, 142)', 24 | }, 25 | [Color.Orange]: { 26 | primary: 'orange', 27 | secondary: '#ab7207', 28 | }, 29 | [Color.Pink]: { 30 | primary: 'pink', 31 | secondary: '#f1768b', 32 | }, 33 | [Color.Red]: { 34 | primary: 'red', 35 | secondary: '#9e0404', 36 | }, 37 | [Color.Skyblue]: { 38 | primary: 'skyblue', 39 | secondary: '#0ca4e2', 40 | }, 41 | [Color.Green]: { 42 | primary: 'green', 43 | secondary: '#025f02', 44 | }, 45 | [Color.Purple]: { 46 | primary: 'purple', 47 | secondary: '#6a0f9c', 48 | }, 49 | } 50 | 51 | export const ViewOptions = [ 52 | { label: 'common.grid', value: View.Grid }, 53 | { label: 'common.list', value: View.List }, 54 | ] 55 | 56 | export const ControllerCloseModeOptions = [ 57 | { label: 'home.controller.closeMode.all', value: ControllerCloseMode.All }, 58 | { label: 'home.controller.closeMode.button', value: ControllerCloseMode.Button }, 59 | ] 60 | 61 | // vue-draggable-plus config 62 | export const DraggableOptions = { 63 | animation: 150, 64 | } 65 | 66 | export const PluginsTriggerOptions = [ 67 | { label: 'plugin.on::manual', value: PluginTrigger.OnManual }, 68 | { label: 'plugin.on::startup', value: PluginTrigger.OnStartup }, 69 | { label: 'plugin.on::ready', value: PluginTrigger.OnReady }, 70 | { label: 'plugin.on::shutdown', value: PluginTrigger.OnShutdown }, 71 | { label: 'plugin.on::generate', value: PluginTrigger.OnGenerate }, 72 | { label: 'plugin.on::subscribe', value: PluginTrigger.OnSubscribe }, 73 | { label: 'plugin.on::before::core::start', value: PluginTrigger.OnBeforeCoreStart }, 74 | { label: 'plugin.on::core::started', value: PluginTrigger.OnCoreStarted }, 75 | { label: 'plugin.on::before::core::stop', value: PluginTrigger.OnBeforeCoreStop }, 76 | { label: 'plugin.on::core::stopped', value: PluginTrigger.OnCoreStopped }, 77 | ] 78 | 79 | export const ScheduledTaskOptions = [ 80 | { label: 'scheduledtask.update::subscription', value: ScheduledTasksType.UpdateSubscription }, 81 | { label: 'scheduledtask.update::ruleset', value: ScheduledTasksType.UpdateRuleset }, 82 | { label: 'scheduledtask.update::plugin', value: ScheduledTasksType.UpdatePlugin }, 83 | { label: 'scheduledtask.run::plugin', value: ScheduledTasksType.RunPlugin }, 84 | { label: 'scheduledtask.run::script', value: ScheduledTasksType.RunScript }, 85 | ] 86 | 87 | export const DefaultSubscribeScript = `const onSubscribe = async (proxies, subscription) => {\n return { proxies, subscription }\n}` 88 | 89 | export const DefaultTestURL = 'https://www.gstatic.com/generate_204' 90 | -------------------------------------------------------------------------------- /frontend/src/directives/index.ts: -------------------------------------------------------------------------------- 1 | import { vDraggable } from 'vue-draggable-plus' 2 | 3 | import menu from './menu' 4 | import tips from './tips' 5 | 6 | import type { Plugin, App } from 'vue' 7 | 8 | const directives: any = { 9 | menu, 10 | tips, 11 | draggable: vDraggable, 12 | } 13 | 14 | export default { 15 | install(app: App) { 16 | Object.keys(directives).forEach((key) => { 17 | app.directive(key, directives[key]) 18 | }) 19 | }, 20 | } as Plugin 21 | -------------------------------------------------------------------------------- /frontend/src/directives/menu.ts: -------------------------------------------------------------------------------- 1 | import { useAppStore } from '@/stores' 2 | import { sleep } from '@/utils' 3 | 4 | import type { Directive, DirectiveBinding } from 'vue' 5 | 6 | const updateMenus = (el: any, binding: DirectiveBinding) => { 7 | const appStore = useAppStore() 8 | 9 | el.oncontextmenu = async (e: MouseEvent) => { 10 | e.preventDefault() 11 | if (binding.value.length) { 12 | appStore.menuPosition = { x: e.clientX, y: e.clientY } 13 | appStore.menuList = binding.value 14 | if (appStore.menuShow) { 15 | appStore.menuShow = false 16 | await sleep(200) 17 | } 18 | appStore.menuShow = true 19 | } 20 | } 21 | } 22 | 23 | export default { 24 | mounted(el: any, binding: DirectiveBinding) { 25 | updateMenus(el, binding) 26 | }, 27 | updated(el: any, binding: DirectiveBinding) { 28 | updateMenus(el, binding) 29 | }, 30 | } as Directive 31 | -------------------------------------------------------------------------------- /frontend/src/directives/tips.ts: -------------------------------------------------------------------------------- 1 | import { type Directive, type DirectiveBinding } from 'vue' 2 | 3 | import { useAppStore } from '@/stores' 4 | import { debounce } from '@/utils' 5 | 6 | export default { 7 | mounted(el: HTMLElement, binding: DirectiveBinding) { 8 | const appStore = useAppStore() 9 | 10 | const delay = binding.modifiers.fast ? 200 : 500 11 | 12 | const show = debounce((x: number, y: number) => { 13 | if (el.dataset.showTips === 'true') { 14 | appStore.tipsPosition = { x, y } 15 | appStore.tipsMessage = binding.value 16 | appStore.tipsShow = true 17 | } 18 | }, delay) 19 | 20 | el.onmouseenter = (e: MouseEvent) => { 21 | el.dataset.showTips = 'true' 22 | show(e.clientX, e.clientY) 23 | } 24 | 25 | el.onmouseleave = () => { 26 | appStore.tipsShow = false 27 | el.dataset.showTips = 'false' 28 | } 29 | }, 30 | beforeUnmount(el: HTMLElement) { 31 | const appStore = useAppStore() 32 | appStore.tipsShow = false 33 | el.dataset.showTips = 'false' 34 | }, 35 | } as Directive 36 | -------------------------------------------------------------------------------- /frontend/src/enums/app.ts: -------------------------------------------------------------------------------- 1 | export enum WindowStartState { 2 | Normal = 0, 3 | Minimised = 2, 4 | } 5 | 6 | export enum WebviewGpuPolicy { 7 | Always = 0, 8 | OnDemand = 1, 9 | Never = 2, 10 | } 11 | 12 | export enum Theme { 13 | Auto = 'auto', 14 | Light = 'light', 15 | Dark = 'dark', 16 | } 17 | 18 | export enum Lang { 19 | EN = 'en', 20 | ZH = 'zh', 21 | RU = 'ru', 22 | FA = 'fa', 23 | } 24 | 25 | export enum View { 26 | Grid = 'grid', 27 | List = 'list', 28 | } 29 | 30 | export enum ControllerCloseMode { 31 | All = 'all', 32 | Button = 'button', 33 | } 34 | 35 | export enum Color { 36 | Default = 'default', 37 | Orange = 'orange', 38 | Pink = 'pink', 39 | Red = 'red', 40 | Skyblue = 'skyblue', 41 | Green = 'green', 42 | Purple = 'purple', 43 | } 44 | 45 | export enum Branch { 46 | Main = 'main', 47 | Alpha = 'alpha', 48 | } 49 | 50 | export enum ScheduledTasksType { 51 | UpdateSubscription = 'update::subscription', 52 | UpdateRuleset = 'update::ruleset', 53 | UpdatePlugin = 'update::plugin', 54 | RunPlugin = 'run::plugin', 55 | RunScript = 'run::script', 56 | } 57 | 58 | export enum PluginTrigger { 59 | OnManual = 'on::manual', 60 | OnSubscribe = 'on::subscribe', 61 | OnGenerate = 'on::generate', 62 | OnStartup = 'on::startup', 63 | OnShutdown = 'on::shutdown', 64 | OnReady = 'on::ready', 65 | OnCoreStarted = 'on::core::started', 66 | OnCoreStopped = 'on::core::stopped', 67 | OnBeforeCoreStart = 'on::before::core::start', 68 | OnBeforeCoreStop = 'on::before::core::stop', 69 | } 70 | 71 | export enum PluginTriggerEvent { 72 | OnInstall = 'onInstall', 73 | OnUninstall = 'onUninstall', 74 | OnManual = 'onRun', 75 | OnSubscribe = 'onSubscribe', 76 | OnGenerate = 'onGenerate', 77 | OnStartup = 'onStartup', 78 | OnShutdown = 'onShutdown', 79 | OnReady = 'onReady', 80 | OnTask = 'onTask', 81 | OnConfigure = 'onConfigure', 82 | OnCoreStarted = 'onCoreStarted', 83 | OnCoreStopped = 'onCoreStopped', 84 | OnBeforeCoreStart = 'onBeforeCoreStart', 85 | OnBeforeCoreStop = 'onBeforeCoreStop', 86 | } 87 | -------------------------------------------------------------------------------- /frontend/src/enums/kernel.ts: -------------------------------------------------------------------------------- 1 | export enum LogLevel { 2 | Trace = 'trace', 3 | Debug = 'debug', 4 | Info = 'info', 5 | Warn = 'warn', 6 | Error = 'error', 7 | Fatal = 'fatal', 8 | Panic = 'panic', 9 | } 10 | 11 | export enum ClashMode { 12 | Global = 'global', 13 | Rule = 'rule', 14 | Direct = 'direct', 15 | } 16 | 17 | export enum Inbound { 18 | Mixed = 'mixed', 19 | Socks = 'socks', 20 | Http = 'http', 21 | Tun = 'tun', 22 | } 23 | 24 | export enum Outbound { 25 | Direct = 'direct', 26 | Selector = 'selector', 27 | Urltest = 'urltest', 28 | } 29 | 30 | export enum TunStack { 31 | System = 'system', 32 | GVisor = 'gvisor', 33 | Mixed = 'mixed', 34 | } 35 | 36 | export enum RulesetType { 37 | Inline = 'inline', 38 | Local = 'local', 39 | Remote = 'remote', 40 | } 41 | 42 | export enum RulesetFormat { 43 | Source = 'source', 44 | Binary = 'binary', 45 | } 46 | 47 | export enum RuleType { 48 | Inbound = 'inbound', 49 | Network = 'network', 50 | Protocol = 'protocol', 51 | Domain = 'domain', 52 | DomainSuffix = 'domain_suffix', 53 | DomainKeyword = 'domain_keyword', 54 | DomainRegex = 'domain_regex', 55 | SourceIPCidr = 'source_ip_cidr', 56 | IPCidr = 'ip_cidr', 57 | IpIsPrivate = 'ip_is_private', 58 | SourcePort = 'source_port', 59 | SourcePortRange = 'source_port_range', 60 | Port = 'port', 61 | PortRange = 'port_range', 62 | ProcessName = 'process_name', 63 | ProcessPath = 'process_path', 64 | ProcessPathRegex = 'process_path_regex', 65 | ClashMode = 'clash_mode', 66 | RuleSet = 'rule_set', 67 | IpAcceptAny = 'ip_accept_any', 68 | // GUI 69 | Inline = 'inline', 70 | } 71 | 72 | export enum Strategy { 73 | Default = 'default', 74 | PreferIPv4 = 'prefer_ipv4', 75 | PreferIPv6 = 'prefer_ipv6', 76 | IPv4Only = 'ipv4_only', 77 | IPv6Only = 'ipv6_only', 78 | } 79 | 80 | export enum DnsServer { 81 | Local = 'local', 82 | Hosts = 'hosts', 83 | Tcp = 'tcp', 84 | Udp = 'udp', 85 | Tls = 'tls', 86 | Https = 'https', 87 | Quic = 'quic', 88 | H3 = 'h3', 89 | Dhcp = 'dhcp', 90 | FakeIP = 'fakeip', 91 | } 92 | 93 | export enum RuleAction { 94 | Route = 'route', 95 | RouteOptions = 'route-options', 96 | Reject = 'reject', 97 | HijackDNS = 'hijack-dns', 98 | Sniff = 'sniff', 99 | Resolve = 'resolve', 100 | Predefined = 'predefined', 101 | } 102 | 103 | export enum RuleActionReject { 104 | Default = 'default', 105 | Drop = 'drop', 106 | } 107 | 108 | export enum Sniffer { 109 | Http = 'http', 110 | Tls = 'tls', 111 | Quic = 'quic', 112 | Stun = 'stun', 113 | Dns = 'dns', 114 | Bittorrent = 'bittorrent', 115 | Dtls = 'dtls', 116 | Ssh = 'ssh', 117 | Rdp = 'rdp', 118 | Ntp = 'ntp', 119 | } 120 | -------------------------------------------------------------------------------- /frontend/src/globalMethods.ts: -------------------------------------------------------------------------------- 1 | import { stringify, parse } from 'yaml' 2 | 3 | import * as Bridge from '@/bridge' 4 | import * as Stores from '@/stores' 5 | import * as Utils from '@/utils' 6 | 7 | /** 8 | * Expose methods to be used by the plugin system 9 | */ 10 | window.Plugins = { 11 | ...Bridge, 12 | ...Utils, 13 | ...Stores, 14 | YAML: { 15 | parse, 16 | stringify, 17 | }, 18 | } 19 | 20 | window.AsyncFunction = Object.getPrototypeOf(async function () {}).constructor 21 | -------------------------------------------------------------------------------- /frontend/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useBool' 2 | -------------------------------------------------------------------------------- /frontend/src/hooks/useBool.ts: -------------------------------------------------------------------------------- 1 | import { ref, type Ref } from 'vue' 2 | 3 | export const useBool = (initialValue: boolean): [Ref, () => void] => { 4 | const value = ref(initialValue) 5 | 6 | const toggle = () => { 7 | value.value = !value.value 8 | } 9 | 10 | return [value, toggle] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/lang/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | 3 | import en from './locale/en' 4 | import fa from './locale/fa' 5 | import ru from './locale/ru' 6 | import zh from './locale/zh' 7 | 8 | const messages = { 9 | zh, 10 | en, 11 | ru, 12 | fa, 13 | } 14 | 15 | const i18n = createI18n({ 16 | legacy: false, 17 | locale: 'en', 18 | fallbackWarn: false, 19 | missingWarn: false, 20 | messages, 21 | }) 22 | 23 | export default i18n 24 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import { createApp } from 'vue' 3 | 4 | import './assets/main.less' 5 | import './globalMethods' 6 | 7 | import App from './App.vue' 8 | import components from './components' 9 | import directives from './directives' 10 | import i18n from './lang' 11 | import router from './router' 12 | 13 | const app = createApp(App) 14 | 15 | app.use(createPinia()) 16 | app.use(router) 17 | app.use(i18n) 18 | app.use(components) 19 | app.use(directives) 20 | 21 | app.mount('#app') 22 | -------------------------------------------------------------------------------- /frontend/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | 3 | import routes from './routes' 4 | 5 | const router = createRouter({ 6 | history: createWebHashHistory(import.meta.env.BASE_URL), 7 | routes, 8 | }) 9 | 10 | export default router 11 | -------------------------------------------------------------------------------- /frontend/src/router/router.d.ts: -------------------------------------------------------------------------------- 1 | import 'vue-router' 2 | 3 | import { type IconType } from '@/components/Icon/index.vue' 4 | 5 | declare module 'vue-router' { 6 | interface RouteMeta { 7 | name: string 8 | icon?: IconType 9 | hidden?: boolean 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteRecordRaw } from 'vue-router' 2 | 3 | import HomeView from '@/views/HomeView/index.vue' 4 | import PluginsView from '@/views/PluginsView/index.vue' 5 | import ProfilesView from '@/views/ProfilesView/index.vue' 6 | import RulesetsView from '@/views/RulesetsView/index.vue' 7 | import ScheduledTasksView from '@/views/ScheduledTasksView/index.vue' 8 | import SettingsView from '@/views/SettingsView/index.vue' 9 | import SubscribesView from '@/views/SubscribesView/index.vue' 10 | 11 | const routes: RouteRecordRaw[] = [ 12 | { 13 | path: '/', 14 | name: 'Overview', 15 | component: HomeView, 16 | meta: { 17 | name: 'router.overview', 18 | icon: 'overview', 19 | }, 20 | }, 21 | { 22 | path: '/profiles', 23 | name: 'Profiles', 24 | component: ProfilesView, 25 | meta: { 26 | name: 'router.profiles', 27 | icon: 'profiles', 28 | }, 29 | }, 30 | { 31 | path: '/subscriptions', 32 | name: 'Subscriptions', 33 | component: SubscribesView, 34 | meta: { 35 | name: 'router.subscriptions', 36 | icon: 'subscriptions', 37 | }, 38 | }, 39 | { 40 | path: '/rulesets', 41 | name: 'Rulesets', 42 | component: RulesetsView, 43 | meta: { 44 | name: 'router.rulesets', 45 | icon: 'rulesets', 46 | }, 47 | }, 48 | { 49 | path: '/plugins', 50 | name: 'Plugins', 51 | component: PluginsView, 52 | meta: { 53 | name: 'router.plugins', 54 | icon: 'plugins', 55 | }, 56 | }, 57 | { 58 | path: '/scheduledtasks', 59 | name: 'ScheduledTasks', 60 | component: ScheduledTasksView, 61 | meta: { 62 | name: 'router.scheduledtasks', 63 | icon: 'scheduledTasks', 64 | }, 65 | }, 66 | { 67 | path: '/settings', 68 | name: 'Settings', 69 | component: SettingsView, 70 | meta: { 71 | name: 'router.settings', 72 | icon: 'settings2', 73 | hidden: false, 74 | }, 75 | }, 76 | ] 77 | 78 | export default routes 79 | -------------------------------------------------------------------------------- /frontend/src/stores/env.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref, watch } from 'vue' 3 | 4 | import { GetEnv } from '@/bridge' 5 | import { useKernelApiStore } from '@/stores' 6 | import { updateTrayMenus, SetSystemProxy, GetSystemProxy } from '@/utils' 7 | 8 | export const useEnvStore = defineStore('env', () => { 9 | const env = ref({ 10 | appName: '', 11 | basePath: '', 12 | os: '', 13 | arch: '', 14 | x64Level: 0, 15 | }) 16 | 17 | const systemProxy = ref(false) 18 | 19 | const setupEnv = async () => { 20 | const _env = await GetEnv() 21 | env.value = _env 22 | } 23 | 24 | const updateSystemProxyStatus = async () => { 25 | const kernelApiStore = useKernelApiStore() 26 | const proxyServer = await GetSystemProxy() 27 | 28 | if (!proxyServer) { 29 | systemProxy.value = false 30 | } else { 31 | const { port, 'mixed-port': mixedPort, 'socks-port': socksPort } = kernelApiStore.config 32 | const proxyServerList = [ 33 | `http://127.0.0.1:${port}`, 34 | `http://127.0.0.1:${mixedPort}`, 35 | 36 | `socks5://127.0.0.1:${mixedPort}`, 37 | `socks5://127.0.0.1:${socksPort}`, 38 | 39 | `socks=127.0.0.1:${mixedPort}`, 40 | `socks=127.0.0.1:${socksPort}`, 41 | ] 42 | systemProxy.value = proxyServerList.includes(proxyServer) 43 | } 44 | 45 | return systemProxy.value 46 | } 47 | 48 | const setSystemProxy = async () => { 49 | const proxyPort = useKernelApiStore().getProxyPort() 50 | if (!proxyPort) throw 'home.overview.needPort' 51 | 52 | await SetSystemProxy(true, '127.0.0.1:' + proxyPort.port, proxyPort.proxyType) 53 | 54 | systemProxy.value = true 55 | } 56 | 57 | const clearSystemProxy = async () => { 58 | await SetSystemProxy(false, '') 59 | systemProxy.value = false 60 | } 61 | 62 | const switchSystemProxy = async (enable: boolean) => { 63 | if (enable) await setSystemProxy() 64 | else await clearSystemProxy() 65 | } 66 | 67 | watch(systemProxy, updateTrayMenus) 68 | 69 | return { 70 | env, 71 | setupEnv, 72 | systemProxy, 73 | setSystemProxy, 74 | clearSystemProxy, 75 | switchSystemProxy, 76 | updateSystemProxyStatus, 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /frontend/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from './appSettings' 2 | export * from './profiles' 3 | export * from './subscribes' 4 | export * from './rulesets' 5 | export * from './plugins' 6 | export * from './scheduledtasks' 7 | export * from './logs' 8 | export * from './kernelApi' 9 | export * from './app' 10 | export * from './env' 11 | -------------------------------------------------------------------------------- /frontend/src/stores/logs.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { computed, ref } from 'vue' 3 | 4 | import { message } from '@/utils' 5 | 6 | type TaskLogType = { 7 | name: string 8 | startTime: number 9 | endTime: number 10 | result: string[] 11 | } 12 | 13 | export const useLogsStore = defineStore('logs', () => { 14 | const kernelLogs = ref([]) 15 | const scheduledtasksLogs = ref([]) 16 | 17 | const recordKernelLog = (msg: string) => { 18 | msg.includes('FATAL') && message.error(msg) 19 | kernelLogs.value.unshift(msg) 20 | } 21 | 22 | const recordScheduledTasksLog = (log: TaskLogType) => scheduledtasksLogs.value.unshift(log) 23 | 24 | const isTasksLogEmpty = computed(() => scheduledtasksLogs.value.length === 0) 25 | 26 | const isEmpty = computed(() => kernelLogs.value.length === 0) 27 | 28 | const clearKernelLog = () => kernelLogs.value.splice(0) 29 | 30 | return { 31 | recordKernelLog, 32 | clearKernelLog, 33 | kernelLogs, 34 | isEmpty, 35 | scheduledtasksLogs, 36 | isTasksLogEmpty, 37 | recordScheduledTasksLog, 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /frontend/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | /** 3 | * The variable is initialized in `globalMethods.ts:10` 4 | */ 5 | Plugins: any 6 | /** 7 | * The variable is initialized in `globalMethods.ts:20` 8 | */ 9 | AsyncFunction: FunctionConstructor 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/types/kernel.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | export interface CoreApiConfig { 4 | port: number 5 | 'socks-port': number 6 | 'mixed-port': number 7 | 'interface-name': string 8 | 'allow-lan': boolean 9 | mode: string 10 | tun: { 11 | enable: boolean 12 | stack: string 13 | device: string 14 | } 15 | } 16 | 17 | export interface CoreApiProxy { 18 | alive: boolean 19 | all: string[] 20 | name: string 21 | now: string 22 | type: string 23 | udp: boolean 24 | history: { 25 | delay: number 26 | }[] 27 | } 28 | 29 | export interface CoreApiProxies { 30 | proxies: Record 31 | } 32 | 33 | export interface CoreApiConnections { 34 | connections: { 35 | id: string 36 | chains: string[] 37 | }[] 38 | } 39 | 40 | export interface CoreApiTrafficData { 41 | down: number 42 | up: number 43 | } 44 | 45 | export interface CoreApiMemoryData { 46 | inuse: number 47 | oslimit: number 48 | } 49 | 50 | export interface CoreApiLogsData { 51 | type: string 52 | payload: string 53 | } 54 | 55 | export interface CoreApiConnectionsData { 56 | memory: number 57 | uploadTotal: number 58 | downloadTotal: number 59 | connections: { 60 | chains: string[] 61 | download: number 62 | id: string 63 | metadata: { 64 | destinationIP: string 65 | destinationPort: string 66 | dnsMode: string 67 | host: string 68 | network: string 69 | processPath: string 70 | sourceIP: string 71 | sourcePort: string 72 | type: string 73 | } 74 | rule: string 75 | rulePayload: string 76 | start: string 77 | upload: number 78 | }[] 79 | } 80 | -------------------------------------------------------------------------------- /frontend/src/types/typescript.d.ts: -------------------------------------------------------------------------------- 1 | type Recordable = { [x: string]: T } 2 | -------------------------------------------------------------------------------- /frontend/src/utils/env.ts: -------------------------------------------------------------------------------- 1 | export const APP_TITLE = import.meta.env.VITE_APP_TITLE 2 | 3 | export const APP_VERSION = import.meta.env.VITE_APP_VERSION 4 | 5 | export const APP_VERSION_API = import.meta.env.VITE_APP_VERSION_API 6 | 7 | export const PROJECT_URL = import.meta.env.VITE_APP_PROJECT_URL 8 | 9 | export const TG_GROUP = import.meta.env.VITE_APP_TG_GROUP 10 | 11 | export const TG_CHANNEL = import.meta.env.VITE_APP_TG_CHANNEL 12 | 13 | export const isDev = import.meta.env.DEV 14 | -------------------------------------------------------------------------------- /frontend/src/utils/format.ts: -------------------------------------------------------------------------------- 1 | import i18n from '@/lang' 2 | 3 | export function formatBytes(bytes: number, decimals: number = 1): string { 4 | if (bytes === 0) return '0 B' 5 | 6 | const k = 1024 7 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 8 | 9 | const i = Math.max(0, Math.floor(Math.log(bytes) / Math.log(k))) 10 | const formattedValue = parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) 11 | 12 | return `${formattedValue} ${sizes[i]}` 13 | } 14 | 15 | export function formatRelativeTime(d: string | number) { 16 | const formatter = new Intl.RelativeTimeFormat(i18n.global.locale.value, { numeric: 'auto' }) 17 | const date = new Date(d) 18 | const now = Date.now() 19 | const diffMs = date.getTime() - now 20 | 21 | // now 22 | if (diffMs === 0) return formatter.format(0, 'second') 23 | 24 | const units: { unit: Intl.RelativeTimeFormatUnit; threshold: number }[] = [ 25 | { unit: 'year', threshold: 365 * 24 * 60 * 60 * 1000 }, 26 | { unit: 'month', threshold: 30 * 24 * 60 * 60 * 1000 }, 27 | { unit: 'day', threshold: 24 * 60 * 60 * 1000 }, 28 | { unit: 'hour', threshold: 60 * 60 * 1000 }, 29 | { unit: 'minute', threshold: 60 * 1000 }, 30 | { unit: 'second', threshold: 1000 }, 31 | ] 32 | 33 | for (const { unit, threshold } of units) { 34 | const amount = Math.round(diffMs / threshold) 35 | if (Math.abs(amount) > 0) return formatter.format(amount, unit) 36 | } 37 | 38 | return formatter.format(Math.round(diffMs / 1000), 'second') 39 | } 40 | 41 | export function formatDate(timestamp: number | string, format: string) { 42 | const date = new Date(timestamp) 43 | 44 | const map: Record = { 45 | YYYY: date.getFullYear(), 46 | MM: String(date.getMonth() + 1).padStart(2, '0'), 47 | DD: String(date.getDate()).padStart(2, '0'), 48 | HH: String(date.getHours()).padStart(2, '0'), 49 | mm: String(date.getMinutes()).padStart(2, '0'), 50 | ss: String(date.getSeconds()).padStart(2, '0'), 51 | } 52 | 53 | return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (matched) => map[matched]) 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env' 2 | export * from './format' 3 | export * from './generator' 4 | export * from './restorer' 5 | export * from './is' 6 | export * from './others' 7 | export * from './request' 8 | export * from './helper' 9 | export * from './websockets' 10 | export * from './tray' 11 | export * from './completion' 12 | export * from './profilesUpgrader' 13 | export * from './interaction' 14 | -------------------------------------------------------------------------------- /frontend/src/utils/is.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'yaml' 2 | 3 | export const isValidBase64 = (str: string) => { 4 | if (typeof str !== 'string') return false 5 | if (str === '' || str.trim() === '') { 6 | return false 7 | } 8 | try { 9 | return btoa(atob(str)) == str 10 | } catch { 11 | return false 12 | } 13 | } 14 | 15 | export const isValidSubYAML = (str: string) => { 16 | if (typeof str !== 'string') return false 17 | try { 18 | const { proxies } = parse(str) 19 | return !!proxies 20 | } catch { 21 | return false 22 | } 23 | } 24 | 25 | export const isValidSubJson = (str: string) => { 26 | if (typeof str !== 'string') return false 27 | try { 28 | const { outbounds } = JSON.parse(str) 29 | return !!outbounds 30 | } catch { 31 | return false 32 | } 33 | } 34 | 35 | export const isValidPaylodYAML = (str: string) => { 36 | try { 37 | const { payload } = parse(str) 38 | return !!payload 39 | } catch { 40 | return false 41 | } 42 | } 43 | 44 | export const isValidRulesJson = (str: string) => { 45 | try { 46 | const { rules } = JSON.parse(str) 47 | return !!rules 48 | } catch { 49 | return false 50 | } 51 | } 52 | 53 | export const isValidIPv4 = (ip: string) => 54 | /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(ip) 55 | 56 | export const isValidIPv6 = (ip: string) => 57 | /^([\da-fA-F]{1,4}:){6}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|::([\da−fA−F]1,4:)0,4((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|::([\da−fA−F]1,4:)0,4((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|^([\da-fA-F]{1,4}:):([\da-fA-F]{1,4}:){0,3}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)2:([\da−fA−F]1,4:)0,2((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)2:([\da−fA−F]1,4:)0,2((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|^([\da-fA-F]{1,4}:){3}:([\da-fA-F]{1,4}:){0,1}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)4:((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)4:((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}|:((:[\da−fA−F]1,4)1,6|:)|:((:[\da−fA−F]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\da−fA−F]1,4:)6:|([\da−fA−F]1,4:)6:/.test( 58 | ip, 59 | ) 60 | 61 | export const isValidJson = (str: string) => { 62 | try { 63 | return !!JSON.parse(str) 64 | } catch { 65 | return false 66 | } 67 | } 68 | 69 | export const isNumber = (v: any) => typeof v === 'number' 70 | -------------------------------------------------------------------------------- /frontend/src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'yaml' 2 | 3 | type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' 4 | 5 | enum ResponseType { 6 | JSON = 'JSON', 7 | TEXT = 'TEXT', 8 | YAML = 'YAML', 9 | } 10 | 11 | type RequestOptions = { 12 | base?: string 13 | bearer?: string 14 | timeout?: number 15 | responseType?: keyof typeof ResponseType 16 | beforeRequest?: () => void 17 | } 18 | 19 | export class Request { 20 | public base: string 21 | public bearer: string 22 | public timeout: number 23 | public responseType: string 24 | public beforeRequest: () => void 25 | 26 | constructor(options: RequestOptions = {}) { 27 | this.base = options.base || '' 28 | this.bearer = options.bearer || '' 29 | this.timeout = options.timeout || 10000 30 | this.responseType = options.responseType || ResponseType.JSON 31 | this.beforeRequest = options.beforeRequest || (() => 0) 32 | } 33 | 34 | private request = async ( 35 | url: string, 36 | options: { method: Method; body?: Record }, 37 | ) => { 38 | this.beforeRequest() 39 | 40 | const controller = new AbortController() 41 | 42 | const init: RequestInit = { 43 | method: options.method, 44 | signal: controller.signal, 45 | } 46 | 47 | if (this.base) { 48 | url = this.base + url 49 | } 50 | 51 | if (this.bearer) { 52 | if (!init.headers) init.headers = {} 53 | Object.assign(init.headers, { Authorization: `Bearer ${this.bearer}` }) 54 | } 55 | 56 | if (['GET'].includes(options.method)) { 57 | const query = new URLSearchParams(options.body || {}).toString() 58 | query && (url += '?' + query) 59 | } 60 | 61 | if (['POST', 'PUT', 'PATCH'].includes(options.method)) { 62 | init.body = JSON.stringify(options.body || {}) 63 | } 64 | 65 | const id = setTimeout(() => controller.abort(), this.timeout) 66 | 67 | const res = await fetch(url, init) 68 | 69 | clearTimeout(id) 70 | 71 | if (res.status === 204) { 72 | return null as T 73 | } 74 | 75 | if ([504, 401, 503].includes(res.status)) { 76 | const { message } = await res.json() 77 | throw message 78 | } 79 | 80 | if (this.responseType === ResponseType.TEXT) { 81 | const text = await res.text() 82 | return text as T 83 | } 84 | 85 | if (this.responseType === ResponseType.YAML) { 86 | const text = await res.text() 87 | return parse(text) as T 88 | } 89 | 90 | const json = await res.json() 91 | return json as T 92 | } 93 | 94 | public get = (url: string, body = {}) => this.request(url, { method: 'GET', body }) 95 | public post = (url: string, body = {}) => this.request(url, { method: 'POST', body }) 96 | public put = (url: string, body = {}) => this.request(url, { method: 'PUT', body }) 97 | public patch = (url: string, body = {}) => this.request(url, { method: 'PATCH', body }) 98 | public delete = (url: string) => this.request(url, { method: 'DELETE' }) 99 | } 100 | -------------------------------------------------------------------------------- /frontend/src/utils/websockets.ts: -------------------------------------------------------------------------------- 1 | type WebSocketsOptions = { 2 | base?: string 3 | bearer?: string 4 | beforeConnect?: () => void 5 | } 6 | 7 | type URLType = { name: string; url: string; cb: (data: any) => void; params?: Record } 8 | 9 | export class WebSockets { 10 | public base: string 11 | public bearer: string 12 | public beforeConnect: () => void 13 | 14 | constructor(options: WebSocketsOptions) { 15 | this.base = options.base || '' 16 | this.bearer = options.bearer || '' 17 | this.beforeConnect = options.beforeConnect || (() => 0) 18 | } 19 | 20 | public createWS(urls: URLType[]) { 21 | this.beforeConnect() 22 | 23 | const wsMap: Record void; open: () => void }> = {} 24 | 25 | urls.forEach(({ name, url, params = {}, cb }) => { 26 | Object.assign(params, { token: this.bearer }) 27 | 28 | const query = new URLSearchParams(params).toString() 29 | 30 | query && (url += '?' + query) 31 | 32 | const open = () => { 33 | if (!wsMap[name].ready) return 34 | const ws = new WebSocket(this.base + url) 35 | ws.onmessage = (e) => cb(JSON.parse(e.data)) 36 | ws.onerror = () => (wsMap[name].ready = true) 37 | ws.onclose = () => (wsMap[name].ready = true) 38 | wsMap[name].close = () => { 39 | ws.close() 40 | wsMap[name].ready = true 41 | } 42 | wsMap[name].ready = false 43 | } 44 | 45 | wsMap[name] = { ready: true, open, close: () => (wsMap[name].ready = false) } 46 | }) 47 | 48 | return { 49 | connect: () => Object.values(wsMap).forEach((ws) => ws.open()), 50 | disconnect: () => Object.values(wsMap).forEach((ws) => ws.close()), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 57 | 58 | 86 | -------------------------------------------------------------------------------- /frontend/src/views/HomeView/components/KernelLogs.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 41 | -------------------------------------------------------------------------------- /frontend/src/views/PluginsView/components/PluginChangelog.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 41 | 42 | 51 | -------------------------------------------------------------------------------- /frontend/src/views/PluginsView/components/PluginHub.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 67 | 68 | 100 | -------------------------------------------------------------------------------- /frontend/src/views/ProfilesView/components/DnsConfig.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 99 | -------------------------------------------------------------------------------- /frontend/src/views/ProfilesView/components/MixinAndScriptConfig.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 38 | -------------------------------------------------------------------------------- /frontend/src/views/ProfilesView/components/RouteConfig.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 88 | -------------------------------------------------------------------------------- /frontend/src/views/RulesetsView/components/RulesetView.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 69 | 70 | 81 | -------------------------------------------------------------------------------- /frontend/src/views/SettingsView/components/CoreSettings.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | -------------------------------------------------------------------------------- /frontend/src/views/SettingsView/components/components/BranchDetail.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 92 | 93 | 111 | -------------------------------------------------------------------------------- /frontend/src/views/SettingsView/components/components/SwitchBranch.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 47 | 48 | 64 | -------------------------------------------------------------------------------- /frontend/src/views/SettingsView/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 38 | -------------------------------------------------------------------------------- /frontend/src/views/SplashView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /frontend/src/views/SubscribesView/components/ProxiesEditor.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 76 | 77 | 88 | -------------------------------------------------------------------------------- /frontend/src/views/SubscribesView/components/SubscribeScript.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 60 | 61 | 70 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"], 10 | "@wails/*": ["./wailsjs/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*", 9 | "eslint.config.*" 10 | ], 11 | "compilerOptions": { 12 | "composite": true, 13 | "noEmit": true, 14 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 15 | "module": "ESNext", 16 | "moduleResolution": "Bundler", 17 | "types": ["node"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import vue from '@vitejs/plugin-vue' 4 | import Components from 'unplugin-vue-components/vite' 5 | import { defineConfig } from 'vite' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | base: './', 10 | plugins: [ 11 | vue(), 12 | Components({ 13 | types: [], 14 | dts: 'src/components/components.d.ts', 15 | globs: ['src/components/*/index.vue'], 16 | }), 17 | ], 18 | resolve: { 19 | alias: { 20 | '@': fileURLToPath(new URL('./src', import.meta.url)), 21 | '@wails': fileURLToPath(new URL('./wailsjs', import.meta.url)), 22 | }, 23 | }, 24 | build: { 25 | chunkSizeWarningLimit: 2048, // 2MB 26 | // __ROLLUP_MANUAL_CHUNKS__ 27 | }, 28 | }) 29 | -------------------------------------------------------------------------------- /frontend/wailsjs/go/bridge/App.d.ts: -------------------------------------------------------------------------------- 1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 2 | // This file is automatically generated. DO NOT EDIT 3 | import {bridge} from '../models'; 4 | 5 | export function AbsolutePath(arg1:string):Promise; 6 | 7 | export function AddScheduledTask(arg1:string,arg2:string):Promise; 8 | 9 | export function CloseMMDB(arg1:string,arg2:string):Promise; 10 | 11 | export function Copyfile(arg1:string,arg2:string):Promise; 12 | 13 | export function Download(arg1:string,arg2:string,arg3:Record,arg4:string,arg5:bridge.RequestOptions):Promise; 14 | 15 | export function Exec(arg1:string,arg2:Array,arg3:bridge.ExecOptions):Promise; 16 | 17 | export function ExecBackground(arg1:string,arg2:Array,arg3:string,arg4:string,arg5:bridge.ExecOptions):Promise; 18 | 19 | export function ExitApp():Promise; 20 | 21 | export function FileExists(arg1:string):Promise; 22 | 23 | export function GetEnv():Promise; 24 | 25 | export function GetInterfaces():Promise; 26 | 27 | export function IsStartup():Promise; 28 | 29 | export function KillProcess(arg1:number):Promise; 30 | 31 | export function ListServer():Promise; 32 | 33 | export function Makedir(arg1:string):Promise; 34 | 35 | export function Movefile(arg1:string,arg2:string):Promise; 36 | 37 | export function Notify(arg1:string,arg2:string,arg3:string):Promise; 38 | 39 | export function OpenMMDB(arg1:string,arg2:string):Promise; 40 | 41 | export function ProcessInfo(arg1:number):Promise; 42 | 43 | export function QueryMMDB(arg1:string,arg2:string,arg3:string):Promise; 44 | 45 | export function Readdir(arg1:string):Promise; 46 | 47 | export function Readfile(arg1:string,arg2:bridge.IOOptions):Promise; 48 | 49 | export function RemoveScheduledTask(arg1:number):Promise; 50 | 51 | export function Removefile(arg1:string):Promise; 52 | 53 | export function Requests(arg1:string,arg2:string,arg3:Record,arg4:string,arg5:bridge.RequestOptions):Promise; 54 | 55 | export function RestartApp():Promise; 56 | 57 | export function ShowMainWindow():Promise; 58 | 59 | export function StartServer(arg1:string,arg2:string,arg3:bridge.ServerOptions):Promise; 60 | 61 | export function StopServer(arg1:string):Promise; 62 | 63 | export function UnzipGZFile(arg1:string,arg2:string):Promise; 64 | 65 | export function UnzipTarGZFile(arg1:string,arg2:string):Promise; 66 | 67 | export function UnzipZIPFile(arg1:string,arg2:string):Promise; 68 | 69 | export function UpdateTray(arg1:bridge.TrayContent):Promise; 70 | 71 | export function UpdateTrayMenus(arg1:Array):Promise; 72 | 73 | export function Upload(arg1:string,arg2:string,arg3:Record,arg4:string,arg5:bridge.RequestOptions):Promise; 74 | 75 | export function ValidateCron(arg1:string):Promise; 76 | 77 | export function Writefile(arg1:string,arg2:string,arg3:bridge.IOOptions):Promise; 78 | -------------------------------------------------------------------------------- /frontend/wailsjs/runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wailsapp/runtime", 3 | "version": "2.0.0", 4 | "description": "Wails Javascript runtime library", 5 | "main": "runtime.js", 6 | "types": "runtime.d.ts", 7 | "scripts": { 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/wailsapp/wails.git" 12 | }, 13 | "keywords": [ 14 | "Wails", 15 | "Javascript", 16 | "Go" 17 | ], 18 | "author": "Lea Anthony ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/wailsapp/wails/issues" 22 | }, 23 | "homepage": "https://github.com/wailsapp/wails#readme" 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module guiforcores 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/energye/systray v1.0.2 9 | github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4 10 | github.com/klauspost/cpuid/v2 v2.2.10 11 | github.com/oschwald/geoip2-golang v1.11.0 12 | github.com/robfig/cron/v3 v3.0.1 13 | github.com/shirou/gopsutil v3.21.11+incompatible 14 | github.com/wailsapp/wails/v2 v2.10.1 15 | golang.org/x/sys v0.33.0 16 | golang.org/x/text v0.25.0 17 | gopkg.in/yaml.v3 v3.0.1 18 | ) 19 | 20 | require ( 21 | github.com/bep/debounce v1.2.1 // indirect 22 | github.com/go-ole/go-ole v1.3.0 // indirect 23 | github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect 24 | github.com/godbus/dbus/v5 v5.1.0 // indirect 25 | github.com/google/uuid v1.6.0 // indirect 26 | github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect 27 | github.com/kr/pretty v0.3.1 // indirect 28 | github.com/labstack/echo/v4 v4.13.3 // indirect 29 | github.com/labstack/gommon v0.4.2 // indirect 30 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect 31 | github.com/leaanthony/gosod v1.0.4 // indirect 32 | github.com/leaanthony/slicer v1.6.0 // indirect 33 | github.com/leaanthony/u v1.1.1 // indirect 34 | github.com/mattn/go-colorable v0.1.14 // indirect 35 | github.com/mattn/go-isatty v0.0.20 // indirect 36 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect 37 | github.com/oschwald/maxminddb-golang v1.13.1 // indirect 38 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 39 | github.com/pkg/errors v0.9.1 // indirect 40 | github.com/rivo/uniseg v0.4.7 // indirect 41 | github.com/rogpeppe/go-internal v1.14.1 // indirect 42 | github.com/samber/lo v1.50.0 // indirect 43 | github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect 44 | github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect 45 | github.com/tklauser/go-sysconf v0.3.15 // indirect 46 | github.com/tklauser/numcpus v0.10.0 // indirect 47 | github.com/tkrajina/go-reflector v0.5.8 // indirect 48 | github.com/valyala/bytebufferpool v1.0.0 // indirect 49 | github.com/valyala/fasttemplate v1.2.2 // indirect 50 | github.com/wailsapp/go-webview2 v1.0.21 // indirect 51 | github.com/wailsapp/mimetype v1.4.1 // indirect 52 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 53 | golang.org/x/crypto v0.38.0 // indirect 54 | golang.org/x/net v0.40.0 // indirect 55 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 56 | ) 57 | 58 | replace github.com/energye/systray => github.com/Ayideyia/systray v1.0.1 59 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "guiforcores/bridge" 7 | "time" 8 | 9 | "github.com/wailsapp/wails/v2" 10 | "github.com/wailsapp/wails/v2/pkg/logger" 11 | "github.com/wailsapp/wails/v2/pkg/options" 12 | "github.com/wailsapp/wails/v2/pkg/options/assetserver" 13 | "github.com/wailsapp/wails/v2/pkg/options/linux" 14 | "github.com/wailsapp/wails/v2/pkg/options/mac" 15 | "github.com/wailsapp/wails/v2/pkg/options/windows" 16 | "github.com/wailsapp/wails/v2/pkg/runtime" 17 | ) 18 | 19 | //go:embed all:frontend/dist 20 | var assets embed.FS 21 | 22 | //go:embed frontend/dist/favicon.ico 23 | var icon []byte 24 | 25 | func main() { 26 | app := bridge.CreateApp(assets) 27 | 28 | trayStart, _ := bridge.CreateTray(app, icon) 29 | 30 | // Create application with options 31 | err := wails.Run(&options.App{ 32 | MinWidth: 600, 33 | MinHeight: 400, 34 | DisableResize: false, 35 | Menu: app.AppMenu, 36 | Title: bridge.Env.AppName, 37 | Frameless: bridge.Env.OS == "windows", 38 | Width: bridge.Config.Width, 39 | Height: bridge.Config.Height, 40 | StartHidden: bridge.Config.StartHidden, 41 | WindowStartState: options.WindowStartState(bridge.Config.WindowStartState), 42 | BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 1}, 43 | Windows: &windows.Options{ 44 | WebviewIsTransparent: true, 45 | WindowIsTranslucent: true, 46 | BackdropType: windows.Acrylic, 47 | }, 48 | Mac: &mac.Options{ 49 | TitleBar: mac.TitleBarHiddenInset(), 50 | Appearance: mac.DefaultAppearance, 51 | WebviewIsTransparent: true, 52 | WindowIsTranslucent: true, 53 | About: &mac.AboutInfo{ 54 | Title: bridge.Env.AppName, 55 | Message: "© 2025 GUI.for.Cores", 56 | Icon: icon, 57 | }, 58 | }, 59 | Linux: &linux.Options{ 60 | Icon: icon, 61 | WindowIsTranslucent: false, 62 | ProgramName: bridge.Env.AppName, 63 | WebviewGpuPolicy: linux.WebviewGpuPolicy(bridge.Config.WebviewGpuPolicy), 64 | }, 65 | AssetServer: &assetserver.Options{ 66 | Assets: assets, 67 | Middleware: bridge.RollingRelease, 68 | }, 69 | SingleInstanceLock: &options.SingleInstanceLock{ 70 | UniqueId: func() string { 71 | if bridge.Config.MultipleInstance { 72 | return time.Now().String() 73 | } 74 | return bridge.Env.AppName 75 | }(), 76 | OnSecondInstanceLaunch: func(data options.SecondInstanceData) { 77 | runtime.Show(app.Ctx) 78 | runtime.EventsEmit(app.Ctx, "onLaunchApp", data.Args) 79 | }, 80 | }, 81 | OnStartup: func(ctx context.Context) { 82 | runtime.LogSetLogLevel(ctx, logger.INFO) 83 | app.Ctx = ctx 84 | bridge.InitScheduledTasks() 85 | trayStart() 86 | }, 87 | OnBeforeClose: func(ctx context.Context) (prevent bool) { 88 | runtime.EventsEmit(ctx, "onBeforeExitApp") 89 | return true 90 | }, 91 | Bind: []any{ 92 | app, 93 | }, 94 | Debug: options.Debug{ 95 | OpenInspectorOnStartup: true, 96 | }, 97 | }) 98 | 99 | if err != nil { 100 | println("Error:", err.Error()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /wails.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://wails.io/schemas/config.v2.json", 3 | "name": "GUI.for.SingBox", 4 | "outputfilename": "GUI.for.SingBox", 5 | "frontend:install": "pnpm install", 6 | "frontend:build": "pnpm run build", 7 | "frontend:dev:watcher": "pnpm run dev", 8 | "frontend:dev:serverUrl": "auto", 9 | "author": { 10 | "name": "GUI.for.Cores", 11 | "email": "GUI.for.Cores@github.com" 12 | }, 13 | "info": { 14 | "copyright": "Copyright", 15 | "comments": "https://github.com/GUI-for-Cores" 16 | } 17 | } 18 | --------------------------------------------------------------------------------