├── .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 |

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 | [](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 |
23 |
24 |
25 |
26 |
27 |
34 |
35 |
36 |
37 |
38 |
39 |
123 |
--------------------------------------------------------------------------------
/frontend/src/components/Card/index.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
27 |
{{ subtitle }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
76 |
--------------------------------------------------------------------------------
/frontend/src/components/CheckBox/index.vue:
--------------------------------------------------------------------------------
1 |
32 |
33 |
34 |
35 |
42 | {{ t(o.label) }}
43 |
44 |
45 |
46 |
47 |
88 |
--------------------------------------------------------------------------------
/frontend/src/components/CustomAction/index.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
52 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/frontend/src/components/Divider/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
28 |
--------------------------------------------------------------------------------
/frontend/src/components/Dropdown/index.vue:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
67 |
68 |
69 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
115 |
--------------------------------------------------------------------------------
/frontend/src/components/Empty/index.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 | {{ description }}
17 |
18 |
19 |
20 |
21 |
34 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/AddIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/ArrowDownIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/ArrowLeftIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/ArrowRightIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/Clear2Icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/ClearIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/CloseIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/CodeIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/CollapseIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/DeleteIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/DisabledIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/DragIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/EditIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/EmptyIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/ErrorIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/ExpandIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/FileIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/FilterIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/FolderIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/ForbiddenIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/GithubIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/GrantIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/LinkIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/LoadingIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/LogIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/Maximize2Icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/MaximizeIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/MessageErrorIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/MessageInfoIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/MessageSuccessIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/MessageWarnIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/MinimizeIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/MoreIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/OverviewIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/PauseIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/PinFillIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/PinIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/PlayIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/PluginsIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/PreviewIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/ProfilesIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/RefreshIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/ResetIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/RestartAppIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/RestartIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/RollbackIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/RulesetsIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/ScheduledTasksIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/SelectedIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/Settings2Icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/SettingsIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/SpeedTestIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/StopIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/SubscriptionsIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon/TelegramIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
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 |
29 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/frontend/src/components/InputList/index.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
56 |
57 |
58 |
83 |
--------------------------------------------------------------------------------
/frontend/src/components/InterfaceSelect/index.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/frontend/src/components/KeyValueEditor/index.vue:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 |
52 |
53 |
54 |
66 |
--------------------------------------------------------------------------------
/frontend/src/components/MainPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
24 |
--------------------------------------------------------------------------------
/frontend/src/components/Message/index.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
34 |
35 |
{{ t(content) }}
36 |
44 |
45 |
46 |
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 |
21 |
22 |
23 |
24 |
27 |
28 |
29 |
30 |
31 |
32 |
38 |
--------------------------------------------------------------------------------
/frontend/src/components/Pagination/index.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
47 |
48 |
49 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/frontend/src/components/Progress/index.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
41 |
42 |
43 |
44 |
65 |
--------------------------------------------------------------------------------
/frontend/src/components/Prompt/index.vue:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 |
39 |
{{ t(title) }}
40 |
50 |
51 |
52 |
55 |
56 |
57 |
58 |
59 |
60 |
76 |
--------------------------------------------------------------------------------
/frontend/src/components/Radio/index.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
35 | {{ t(o.label) }}
36 |
37 |
38 |
39 |
40 |
80 |
--------------------------------------------------------------------------------
/frontend/src/components/Select/index.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
40 |
47 |
48 |
49 |
50 |
86 |
--------------------------------------------------------------------------------
/frontend/src/components/Switch/index.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
152 |
--------------------------------------------------------------------------------
/frontend/src/components/Tabs/index.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
80 |
--------------------------------------------------------------------------------
/frontend/src/components/Tag/index.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
59 |
--------------------------------------------------------------------------------
/frontend/src/components/Tips/index.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
45 | {{ t(message) }}
46 |
47 |
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 |
23 |
24 |

25 |
{{ APP_TITLE }}
26 |
27 |
31 |
32 |
40 |
48 |
49 |
50 |
GitHub
51 |
Telegram Group
52 |
53 | Telegram Channel
54 |
55 |
56 |
57 |
58 |
86 |
--------------------------------------------------------------------------------
/frontend/src/views/HomeView/components/KernelLogs.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{ log }}
16 |
17 |
18 |
19 |
20 |
21 |
41 |
--------------------------------------------------------------------------------
/frontend/src/views/PluginsView/components/PluginChangelog.vue:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
51 |
--------------------------------------------------------------------------------
/frontend/src/views/PluginsView/components/PluginHub.vue:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
48 |
54 | {{ plugin.description }}
55 |
56 |
59 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
100 |
--------------------------------------------------------------------------------
/frontend/src/views/ProfilesView/components/DnsConfig.vue:
--------------------------------------------------------------------------------
1 |
48 |
49 |
50 |
51 |
52 |
53 | {{ t('kernel.dns.disable_cache') }}
54 |
55 |
56 |
57 | {{ t('kernel.dns.disable_expire') }}
58 |
59 |
60 |
61 | {{ t('kernel.dns.independent_cache') }}
62 |
63 |
64 |
65 | {{ t('kernel.dns.final') }}
66 |
67 |
68 |
69 | {{ t('kernel.dns.strategy') }}
70 |
71 |
72 |
73 | {{ t('kernel.dns.client_subnet') }}
74 |
75 |
76 |
77 |
78 |
86 |
87 |
88 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/frontend/src/views/ProfilesView/components/MixinAndScriptConfig.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 | {{ t('profile.mixinSettings.priority') }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/frontend/src/views/ProfilesView/components/RouteConfig.vue:
--------------------------------------------------------------------------------
1 |
40 |
41 |
42 |
43 |
44 |
45 | {{ t('kernel.route.find_process') }}
46 |
47 |
48 |
49 | {{ t('kernel.route.auto_detect_interface') }}
50 |
51 |
52 |
53 | {{ t('kernel.route.default_interface') }}
54 |
55 |
56 |
57 | {{ t('kernel.route.default_domain_resolver.server') }}
58 |
59 |
60 |
64 |
65 | {{ t('kernel.route.final') }}
66 |
67 |
68 |
69 |
70 |
75 |
76 |
77 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/frontend/src/views/RulesetsView/components/RulesetView.vue:
--------------------------------------------------------------------------------
1 |
55 |
56 |
57 |
58 |
59 |
60 |
63 |
66 |
67 |
68 |
69 |
70 |
81 |
--------------------------------------------------------------------------------
/frontend/src/views/SettingsView/components/CoreSettings.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/frontend/src/views/SettingsView/components/components/BranchDetail.vue:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
39 | {{ isAlpha ? 'Alpha' : t('settings.kernel.name') }}
40 |
48 |
49 |
57 |
58 |
59 |
60 | {{ t('settings.kernel.local') }}
61 | :
62 | {{ localVersionLoading ? 'Loading' : localVersion || t('kernel.notFound') }}
63 |
64 |
65 | {{ t('settings.kernel.remote') }}
66 | :
67 | {{ remoteVersionLoading ? 'Loading' : remoteVersion }}
68 |
69 |
78 |
87 |
88 |
89 | {{ versionDetail }}
90 |
91 |
92 |
93 |
111 |
--------------------------------------------------------------------------------
/frontend/src/views/SettingsView/components/components/SwitchBranch.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 | {{ t('settings.kernel.version') }}
28 |
29 |
35 | {{ t('settings.kernel.stable') }}
36 |
37 |
43 | {{ t('settings.kernel.alpha') }}
44 |
45 |
46 |
47 |
48 |
64 |
--------------------------------------------------------------------------------
/frontend/src/views/SettingsView/index.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/frontend/src/views/SplashView.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |

8 |
{{ APP_TITLE }}
9 |
{{ APP_VERSION }}
10 |
11 |
12 |
13 |
22 |
--------------------------------------------------------------------------------
/frontend/src/views/SubscribesView/components/ProxiesEditor.vue:
--------------------------------------------------------------------------------
1 |
62 |
63 |
64 |
65 |
66 |
67 |
70 |
73 |
74 |
75 |
76 |
77 |
88 |
--------------------------------------------------------------------------------
/frontend/src/views/SubscribesView/components/SubscribeScript.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
55 |
58 |
59 |
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 |
--------------------------------------------------------------------------------