internal/gui: Wails app with Classic React variant + theme toggle
- app.go: App struct with stub bindings (RunCheck/StartEngine/
StopEngine/GetStatus/Version) — emits check:result, check:done,
engine:status, stats:update events. Real backend lands in Phase 1.
- run.go: wails.Run() with frameless 480x640 fixed window, Classic
dark bg matching theme.
- embed.go: //go:embed all:frontend/dist for the Vite build output.
- frontend/: Vite + React project derived from `wails init -t react`.
Removed default template assets and wired Classic variant from
docs/design/v2/.
- components/Classic.jsx: variant 1 with custom title bar
(drag region, sun/moon theme toggle, min/close hooked to
Wails WindowMinimise/Quit).
- components/shared.jsx: useDrover hook adapted to call Wails
bindings and listen on backend events instead of mock SCENARIOS.
Added IconSun + IconMoon for the title-bar toggle.
- App.jsx: owns mode state, wraps setMode in
document.startViewTransition so the title-bar toggle gives a
circle-reveal sweep from the cursor.
- style.css: clean reset (overflow hidden, no scrollbars, brand
background) — replaces the wails-react-template defaults.
- wailsjs/go/gui/App.js: hand-written bindings since our App
struct lives in package gui rather than the standard top-level
main; `wails generate module` would have written package main
bindings here.
- build/: standard wails artifacts (icon, manifest); will be
consumed by `wails build` once we wire it through CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,172 @@
|
|||||||
|
// Package gui hosts the Wails app: the App struct (whose exported methods
|
||||||
|
// become the JS API for the frontend) and the Run() helper invoked from
|
||||||
|
// cmd/drover/main.go when the user double-clicks the binary.
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App is the Wails-bound struct. Every exported method is callable from JS
|
||||||
|
// via the auto-generated wailsjs/go/main/App.* bindings.
|
||||||
|
//
|
||||||
|
// Right now everything except the proxy form is a deterministic stub —
|
||||||
|
// the real WinDivert + SOCKS5 engine arrives in Phase 1. The stubs are
|
||||||
|
// sufficient for the UI to feel alive: Check fakes a 7-step diagnostic,
|
||||||
|
// Start/Stop toggles a phase, GetStats emits realistic-looking numbers.
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
version string
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
running bool
|
||||||
|
startedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp returns a fresh App stamped with the binary's build version
|
||||||
|
// (so the GUI can display it in the title bar).
|
||||||
|
func NewApp(version string) *App { return &App{version: version} }
|
||||||
|
|
||||||
|
// Version returns the build version (e.g. "0.2.0", "test-local", or
|
||||||
|
// "dev"). Frontend reads it on mount to populate the custom title bar.
|
||||||
|
func (a *App) Version() string { return a.version }
|
||||||
|
|
||||||
|
// Startup is called by Wails right after the window is created and the
|
||||||
|
// JS runtime is ready. We grab the context for runtime.EventsEmit calls
|
||||||
|
// from any subsequent method.
|
||||||
|
func (a *App) Startup(ctx context.Context) {
|
||||||
|
a.ctx = ctx
|
||||||
|
go a.statsLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is the proxy/auth payload the frontend sends back from the form.
|
||||||
|
type Config struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Auth bool `json:"auth"`
|
||||||
|
Login string `json:"login"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckResult is one row in the diagnostic table; the frontend listens
|
||||||
|
// for them on the "check:result" event.
|
||||||
|
type CheckResult struct {
|
||||||
|
ID string `json:"id"` // tcp / greet / auth / connect / udp / stun / api
|
||||||
|
Status string `json:"status"` // running | passed | failed | skipped
|
||||||
|
Metric string `json:"metric"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Hint string `json:"hint,omitempty"`
|
||||||
|
Duration int64 `json:"duration_ms,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCheck plays out a fake 7-step diagnostic. Each result is pushed to
|
||||||
|
// the JS side as a "check:result" event; when the run finishes we emit
|
||||||
|
// "check:done" with the overall summary.
|
||||||
|
//
|
||||||
|
// Phase 1+: replace this with internal/checker.Run() and stream its
|
||||||
|
// real results.
|
||||||
|
func (a *App) RunCheck(cfg Config) {
|
||||||
|
go func() {
|
||||||
|
ids := []string{"tcp", "greet"}
|
||||||
|
if cfg.Auth {
|
||||||
|
ids = append(ids, "auth")
|
||||||
|
}
|
||||||
|
ids = append(ids, "connect", "udp", "stun", "api")
|
||||||
|
|
||||||
|
fakeMetric := map[string]string{
|
||||||
|
"tcp": "14 ms", "greet": "SOCKS5/0x05", "auth": "user/pass · ok",
|
||||||
|
"connect": "gateway.discord.gg", "udp": "relay 1.2.3.4:54321",
|
||||||
|
"stun": "38 ms RTT", "api": "204 OK · 89 ms",
|
||||||
|
}
|
||||||
|
|
||||||
|
passedCount := 0
|
||||||
|
for _, id := range ids {
|
||||||
|
runtime.EventsEmit(a.ctx, "check:result", CheckResult{ID: id, Status: "running"})
|
||||||
|
time.Sleep(350 * time.Millisecond)
|
||||||
|
|
||||||
|
// Stub: everything passes. Real checker will fail UDP if proxy
|
||||||
|
// rejects ASSOCIATE, etc.
|
||||||
|
runtime.EventsEmit(a.ctx, "check:result", CheckResult{
|
||||||
|
ID: id,
|
||||||
|
Status: "passed",
|
||||||
|
Metric: fakeMetric[id],
|
||||||
|
Duration: 350,
|
||||||
|
})
|
||||||
|
passedCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.EventsEmit(a.ctx, "check:done", map[string]int{
|
||||||
|
"total": len(ids),
|
||||||
|
"passed": passedCount,
|
||||||
|
"failed": len(ids) - passedCount,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartEngine flips the proxy on. In the stub we just toggle the flag and
|
||||||
|
// note the start time so GetStats can produce a believable uptime.
|
||||||
|
func (a *App) StartEngine() error {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
a.running = true
|
||||||
|
a.startedAt = time.Now()
|
||||||
|
runtime.EventsEmit(a.ctx, "engine:status", map[string]any{"running": true})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopEngine turns the proxy off.
|
||||||
|
func (a *App) StopEngine() error {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
a.running = false
|
||||||
|
runtime.EventsEmit(a.ctx, "engine:status", map[string]any{"running": false})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatus is read by the frontend on first paint to know whether to
|
||||||
|
// show "Idle" or "Active".
|
||||||
|
func (a *App) GetStatus() map[string]any {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
return map[string]any{
|
||||||
|
"running": a.running,
|
||||||
|
"uptimeS": int(time.Since(a.startedAt).Seconds()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// statsLoop emits a stats event every second when the engine is running.
|
||||||
|
// Numbers are random but stable enough to look real.
|
||||||
|
func (a *App) statsLoop() {
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
tick := time.NewTicker(time.Second)
|
||||||
|
defer tick.Stop()
|
||||||
|
for range tick.C {
|
||||||
|
a.mu.Lock()
|
||||||
|
if !a.running || a.ctx == nil {
|
||||||
|
a.mu.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uptime := int(time.Since(a.startedAt).Seconds())
|
||||||
|
a.mu.Unlock()
|
||||||
|
|
||||||
|
runtime.EventsEmit(a.ctx, "stats:update", map[string]any{
|
||||||
|
"up": r.Intn(50_000) + 5_000, // bytes/sec out
|
||||||
|
"down": r.Intn(500_000) + 50_000, // bytes/sec in
|
||||||
|
"tcp": r.Intn(8) + 1,
|
||||||
|
"udp": r.Intn(5) + 1,
|
||||||
|
"uptimeS": uptime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greet remains as a smoke check that the bindings pipeline survived
|
||||||
|
// the transition. Frontend can call it from a debug button if needed.
|
||||||
|
func (a *App) Greet(name string) string {
|
||||||
|
return fmt.Sprintf("Hello %s — Drover-Go GUI is alive.", name)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Build Directory
|
||||||
|
|
||||||
|
The build directory is used to house all the build files and assets for your application.
|
||||||
|
|
||||||
|
The structure is:
|
||||||
|
|
||||||
|
* bin - Output directory
|
||||||
|
* darwin - macOS specific files
|
||||||
|
* windows - Windows specific files
|
||||||
|
|
||||||
|
## Mac
|
||||||
|
|
||||||
|
The `darwin` directory holds files specific to Mac builds.
|
||||||
|
These may be customised and used as part of the build. To return these files to the default state, simply delete them
|
||||||
|
and
|
||||||
|
build with `wails build`.
|
||||||
|
|
||||||
|
The directory contains the following files:
|
||||||
|
|
||||||
|
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
|
||||||
|
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
The `windows` directory contains the manifest and rc files used when building with `wails build`.
|
||||||
|
These may be customised for your application. To return these files to the default state, simply delete them and
|
||||||
|
build with `wails build`.
|
||||||
|
|
||||||
|
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
|
||||||
|
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
|
||||||
|
will be created using the `appicon.png` file in the build directory.
|
||||||
|
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
|
||||||
|
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
|
||||||
|
as well as the application itself (right click the exe -> properties -> details)
|
||||||
|
- `wails.exe.manifest` - The main application manifest file.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
@@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{{.Info.ProductName}}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{{.OutputFilename}}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.wails.{{.Name}}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{{.Info.Comments}}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>iconfile</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>{{.Info.Copyright}}</string>
|
||||||
|
{{if .Info.FileAssociations}}
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Ext}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>{{.IconName}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
{{if .Info.Protocols}}
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.wails.{{.Scheme}}</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Scheme}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{{.Info.ProductName}}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{{.OutputFilename}}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.wails.{{.Name}}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{{.Info.Comments}}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>iconfile</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>{{.Info.Copyright}}</string>
|
||||||
|
{{if .Info.FileAssociations}}
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Ext}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>{{.IconName}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
{{if .Info.Protocols}}
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.wails.{{.Scheme}}</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Scheme}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"fixed": {
|
||||||
|
"file_version": "{{.Info.ProductVersion}}"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"0000": {
|
||||||
|
"ProductVersion": "{{.Info.ProductVersion}}",
|
||||||
|
"CompanyName": "{{.Info.CompanyName}}",
|
||||||
|
"FileDescription": "{{.Info.ProductName}}",
|
||||||
|
"LegalCopyright": "{{.Info.Copyright}}",
|
||||||
|
"ProductName": "{{.Info.ProductName}}",
|
||||||
|
"Comments": "{{.Info.Comments}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
Unicode true
|
||||||
|
|
||||||
|
####
|
||||||
|
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||||
|
## mentioned underneath.
|
||||||
|
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
|
||||||
|
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
|
||||||
|
## from outside of Wails for debugging and development of the installer.
|
||||||
|
##
|
||||||
|
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||||
|
## > wails build --target windows/amd64 --nsis
|
||||||
|
## Then you can call makensis on this file with specifying the path to your binary:
|
||||||
|
## For a AMD64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a ARM64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a installer with both architectures:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||||
|
####
|
||||||
|
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
|
||||||
|
####
|
||||||
|
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||||
|
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||||
|
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||||
|
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||||
|
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||||
|
###
|
||||||
|
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||||
|
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
####
|
||||||
|
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||||
|
####
|
||||||
|
## Include the wails tools
|
||||||
|
####
|
||||||
|
!include "wails_tools.nsh"
|
||||||
|
|
||||||
|
# The version information for this two must consist of 4 parts
|
||||||
|
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
|
||||||
|
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||||
|
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||||
|
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||||
|
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||||
|
|
||||||
|
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||||
|
ManifestDPIAware true
|
||||||
|
|
||||||
|
!include "MUI.nsh"
|
||||||
|
|
||||||
|
!define MUI_ICON "..\icon.ico"
|
||||||
|
!define MUI_UNICON "..\icon.ico"
|
||||||
|
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||||
|
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||||
|
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||||
|
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||||
|
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||||
|
|
||||||
|
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||||
|
#!uninstfinalize 'signtool --file "%1"'
|
||||||
|
#!finalize 'signtool --file "%1"'
|
||||||
|
|
||||||
|
Name "${INFO_PRODUCTNAME}"
|
||||||
|
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||||
|
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||||
|
ShowInstDetails show # This will always show the installation details.
|
||||||
|
|
||||||
|
Function .onInit
|
||||||
|
!insertmacro wails.checkArchitecture
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
!insertmacro wails.webview2runtime
|
||||||
|
|
||||||
|
SetOutPath $INSTDIR
|
||||||
|
|
||||||
|
!insertmacro wails.files
|
||||||
|
|
||||||
|
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
|
||||||
|
!insertmacro wails.associateFiles
|
||||||
|
!insertmacro wails.associateCustomProtocols
|
||||||
|
|
||||||
|
!insertmacro wails.writeUninstaller
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "uninstall"
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||||
|
|
||||||
|
RMDir /r $INSTDIR
|
||||||
|
|
||||||
|
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
|
||||||
|
!insertmacro wails.unassociateFiles
|
||||||
|
!insertmacro wails.unassociateCustomProtocols
|
||||||
|
|
||||||
|
!insertmacro wails.deleteUninstaller
|
||||||
|
SectionEnd
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
# DO NOT EDIT - Generated automatically by `wails build`
|
||||||
|
|
||||||
|
!include "x64.nsh"
|
||||||
|
!include "WinVer.nsh"
|
||||||
|
!include "FileFunc.nsh"
|
||||||
|
|
||||||
|
!ifndef INFO_PROJECTNAME
|
||||||
|
!define INFO_PROJECTNAME "{{.Name}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COMPANYNAME
|
||||||
|
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTNAME
|
||||||
|
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTVERSION
|
||||||
|
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COPYRIGHT
|
||||||
|
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
|
||||||
|
!endif
|
||||||
|
!ifndef PRODUCT_EXECUTABLE
|
||||||
|
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||||
|
!endif
|
||||||
|
!ifndef UNINST_KEY_NAME
|
||||||
|
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
!endif
|
||||||
|
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||||
|
|
||||||
|
!ifndef REQUEST_EXECUTION_LEVEL
|
||||||
|
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_AMD64_BINARY
|
||||||
|
!define SUPPORTS_AMD64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_ARM64_BINARY
|
||||||
|
!define SUPPORTS_ARM64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "amd64_arm64"
|
||||||
|
!else
|
||||||
|
!define ARCH "amd64"
|
||||||
|
!endif
|
||||||
|
!else
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "arm64"
|
||||||
|
!else
|
||||||
|
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||||
|
!endif
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!macro wails.checkArchitecture
|
||||||
|
!ifndef WAILS_WIN10_REQUIRED
|
||||||
|
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||||
|
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
${If} ${AtLeastWin10}
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
IfSilent silentArch notSilentArch
|
||||||
|
silentArch:
|
||||||
|
SetErrorLevel 65
|
||||||
|
Abort
|
||||||
|
notSilentArch:
|
||||||
|
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||||
|
Quit
|
||||||
|
${else}
|
||||||
|
IfSilent silentWin notSilentWin
|
||||||
|
silentWin:
|
||||||
|
SetErrorLevel 64
|
||||||
|
Abort
|
||||||
|
notSilentWin:
|
||||||
|
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||||
|
Quit
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.files
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.writeUninstaller
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||||
|
|
||||||
|
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||||
|
IntFmt $0 "0x%08X" $0
|
||||||
|
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.deleteUninstaller
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.setShellContext
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||||
|
SetShellVarContext all
|
||||||
|
${else}
|
||||||
|
SetShellVarContext current
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Install webview2 by launching the bootstrapper
|
||||||
|
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||||
|
!macro wails.webview2runtime
|
||||||
|
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||||
|
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
# If the admin key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||||
|
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||||
|
SetDetailsPrint listonly
|
||||||
|
|
||||||
|
InitPluginsDir
|
||||||
|
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||||
|
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||||
|
File "tmp\MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||||
|
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||||
|
|
||||||
|
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.associateFiles
|
||||||
|
; Create file associations
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||||
|
|
||||||
|
File "..\{{.IconName}}.ico"
|
||||||
|
{{end}}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateFiles
|
||||||
|
; Delete app associations
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
|
||||||
|
|
||||||
|
Delete "$INSTDIR\{{.IconName}}.ico"
|
||||||
|
{{end}}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||||
|
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||||
|
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.associateCustomProtocols
|
||||||
|
; Create custom protocols associations
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateCustomProtocols
|
||||||
|
; Delete app custom protocol associations
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
|
||||||
|
{{end}}
|
||||||
|
!macroend
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
<asmv3:application>
|
||||||
|
<asmv3:windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||||
|
</asmv3:windowsSettings>
|
||||||
|
</asmv3:application>
|
||||||
|
</assembly>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
// Assets embeds the built React frontend (frontend/dist/) into the
|
||||||
|
// Go binary so a single drover.exe ships with no external files.
|
||||||
|
// Build the frontend before `go build`:
|
||||||
|
//
|
||||||
|
// cd internal/gui/frontend && npm install && npm run build
|
||||||
|
//
|
||||||
|
// Then `go build ./cmd/drover` will pick up the fresh dist/ via this
|
||||||
|
// directive.
|
||||||
|
//
|
||||||
|
//go:embed all:frontend/dist
|
||||||
|
var Assets embed.FS
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestEmbed verifies that frontend/dist actually got embedded — easy to
|
||||||
|
// silently miss this and end up with a Wails window that 404s on every
|
||||||
|
// asset.
|
||||||
|
func TestEmbed(t *testing.T) {
|
||||||
|
sub, err := fs.Sub(Assets, "frontend/dist")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("fs.Sub: %v", err)
|
||||||
|
}
|
||||||
|
var files []string
|
||||||
|
fs.WalkDir(sub, ".", func(p string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
files = append(files, p)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if len(files) == 0 {
|
||||||
|
t.Fatal("frontend/dist embed is empty — did you forget `npm run build`?")
|
||||||
|
}
|
||||||
|
if !sliceContains(files, "index.html") {
|
||||||
|
t.Fatalf("no index.html in embed; got %v", files)
|
||||||
|
}
|
||||||
|
hasJS := false
|
||||||
|
for _, f := range files {
|
||||||
|
if strings.HasSuffix(f, ".js") {
|
||||||
|
hasJS = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasJS {
|
||||||
|
t.Fatalf("no .js bundle in embed; got %v", files)
|
||||||
|
}
|
||||||
|
t.Logf("embed contains %d files (looks healthy):", len(files))
|
||||||
|
for _, f := range files {
|
||||||
|
t.Logf(" %s", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceContains(xs []string, x string) bool {
|
||||||
|
for _, v := range xs {
|
||||||
|
if v == x {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>drover-gui</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="./src/main.jsx" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Generated
+1426
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.17",
|
||||||
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@vitejs/plugin-react": "^2.0.1",
|
||||||
|
"vite": "^3.0.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import ClassicWindow from './components/Classic.jsx'
|
||||||
|
|
||||||
|
// Wails sizes the host window itself (internal/gui/run.go). Classic renders
|
||||||
|
// 100% of that surface; we own the mode state here so the title-bar toggle
|
||||||
|
// in Classic can flip between dark and light without re-mounting.
|
||||||
|
//
|
||||||
|
// onToggleMode receives the click event so we can plant a circle-reveal
|
||||||
|
// origin at the cursor position. The View Transitions API (Chromium 111+,
|
||||||
|
// Edge / WebView2 included) snapshots the old DOM, swaps to the new one
|
||||||
|
// after setMode commits, and animates between them. Fallback path just
|
||||||
|
// flips the mode synchronously when the API is missing.
|
||||||
|
export default function App() {
|
||||||
|
const [mode, setMode] = React.useState('dark')
|
||||||
|
|
||||||
|
function onToggleMode(e) {
|
||||||
|
const x = e?.clientX ?? window.innerWidth - 24
|
||||||
|
const y = e?.clientY ?? 16
|
||||||
|
document.documentElement.style.setProperty('--reveal-x', x + 'px')
|
||||||
|
document.documentElement.style.setProperty('--reveal-y', y + 'px')
|
||||||
|
|
||||||
|
const flip = () => setMode(m => (m === 'dark' ? 'light' : 'dark'))
|
||||||
|
if (document.startViewTransition) {
|
||||||
|
document.startViewTransition(flip)
|
||||||
|
} else {
|
||||||
|
flip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ClassicWindow mode={mode} onToggleMode={onToggleMode} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
@@ -0,0 +1,469 @@
|
|||||||
|
// Classic.jsx — Variant 1: Classic devtool.
|
||||||
|
// Information-dense. Mono metrics. Plain rectangles, hairline borders.
|
||||||
|
// Sober palette: neutral grays + one teal accent for primary action / success.
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import {
|
||||||
|
useDrover,
|
||||||
|
BrandMark, IconGear, IconMin, IconClose, IconChevron, IconCopy,
|
||||||
|
IconArrowUp, IconArrowDown, IconSun, IconMoon, StatusDot,
|
||||||
|
fmtBytes, fmtUptime, fmtTime,
|
||||||
|
} from './shared.jsx'
|
||||||
|
import { WindowMinimise, Quit } from '../../wailsjs/runtime/runtime'
|
||||||
|
import { Version as GoVersion } from '../../wailsjs/go/gui/App'
|
||||||
|
|
||||||
|
const ClassicTheme = {
|
||||||
|
// dark
|
||||||
|
d: {
|
||||||
|
bg: '#1c1d20',
|
||||||
|
chrome: '#15161a',
|
||||||
|
panel: '#22242a',
|
||||||
|
panelAlt: '#1a1c20',
|
||||||
|
border: '#34373d',
|
||||||
|
borderSoft:'#2a2c32',
|
||||||
|
text: '#dde0e6',
|
||||||
|
dim: '#8a8f99',
|
||||||
|
dimmer: '#5b6068',
|
||||||
|
accent: '#3ea99f', // teal
|
||||||
|
accentDim: '#2a7d76',
|
||||||
|
danger: '#d96565',
|
||||||
|
warn: '#d9a155',
|
||||||
|
pass: '#5cba8b',
|
||||||
|
skip: '#7c8088',
|
||||||
|
inputBg: '#15161a',
|
||||||
|
btnBg: '#2c2f36',
|
||||||
|
btnBgH: '#373b43',
|
||||||
|
primaryBg: '#3ea99f',
|
||||||
|
primaryFg: '#0c1a18',
|
||||||
|
},
|
||||||
|
l: {
|
||||||
|
bg: '#f3f4f6',
|
||||||
|
chrome: '#e8eaef',
|
||||||
|
panel: '#ffffff',
|
||||||
|
panelAlt: '#f8f9fb',
|
||||||
|
border: '#d8dbe1',
|
||||||
|
borderSoft:'#e6e8ec',
|
||||||
|
text: '#1c1f24',
|
||||||
|
dim: '#5c6168',
|
||||||
|
dimmer: '#8a8f97',
|
||||||
|
accent: '#2a7d76',
|
||||||
|
accentDim: '#bdded9',
|
||||||
|
danger: '#c0463f',
|
||||||
|
warn: '#a8731e',
|
||||||
|
pass: '#2f8c5a',
|
||||||
|
skip: '#7c8088',
|
||||||
|
inputBg: '#ffffff',
|
||||||
|
btnBg: '#ffffff',
|
||||||
|
btnBgH: '#f1f2f5',
|
||||||
|
primaryBg: '#2a7d76',
|
||||||
|
primaryFg: '#ffffff',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ClassicWindow({ mode = 'dark', initial, onToggleMode }) {
|
||||||
|
const t = ClassicTheme[mode === 'dark' ? 'd' : 'l'];
|
||||||
|
const D = useDrover(initial);
|
||||||
|
const [version, setVersion] = React.useState('');
|
||||||
|
React.useEffect(() => { GoVersion().then(setVersion).catch(() => {}); }, []);
|
||||||
|
const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip };
|
||||||
|
const fontMono = '"JetBrains Mono","SF Mono",ui-monospace,Menlo,Consolas,monospace';
|
||||||
|
const fontUI = '"Inter","Segoe UI",system-ui,sans-serif';
|
||||||
|
const isActive = D.phase === 'active';
|
||||||
|
const allChecked = D.phase === 'checked' || D.phase === 'active';
|
||||||
|
const failed = D.lastSummary?.failed ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
width: '100vw', height: '100vh', background: t.bg, color: t.text, display: 'flex', flexDirection: 'column',
|
||||||
|
fontFamily: fontUI, fontSize: 13, lineHeight: 1.4, overflow: 'hidden',
|
||||||
|
border: mode === 'dark' ? '1px solid #000' : '1px solid #c0c3c9',
|
||||||
|
}}>
|
||||||
|
{/* ─── title bar ─── */}
|
||||||
|
<ClassicTitleBar t={t} version={version} mode={mode} onToggleMode={onToggleMode} />
|
||||||
|
|
||||||
|
{/* ─── content ─── */}
|
||||||
|
<div style={{ flex: 1, overflow: 'auto', padding: '14px 16px 0' }}>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<SectionLabel t={t}>SOCKS5 Proxy</SectionLabel>
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
|
||||||
|
<Field t={t} label="Host" style={{ flex: 1 }}>
|
||||||
|
<input value={D.form.host} onChange={e => D.update({ host: e.target.value })}
|
||||||
|
placeholder="95.165.72.59 или example.com"
|
||||||
|
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
|
||||||
|
style={inputStyle(t, fontMono)} />
|
||||||
|
</Field>
|
||||||
|
<Field t={t} label="Port" style={{ width: 92 }}>
|
||||||
|
<input value={D.form.port} onChange={e => D.update({ port: e.target.value.replace(/\D/g,'') })}
|
||||||
|
placeholder="12334" inputMode="numeric"
|
||||||
|
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
|
||||||
|
style={inputStyle(t, fontMono)} />
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Checkbox t={t} checked={D.form.auth}
|
||||||
|
onChange={(v) => { D.update({ auth: v }); if (v) setTimeout(() => document.getElementById('cls-login')?.focus(), 30); }}>
|
||||||
|
Authentication
|
||||||
|
</Checkbox>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginTop: 8, marginBottom: 12, opacity: D.form.auth ? 1 : 0.45 }}>
|
||||||
|
<Field t={t} label="Login" style={{ flex: 1 }}>
|
||||||
|
<input id="cls-login" disabled={!D.form.auth} value={D.form.login}
|
||||||
|
onChange={e => D.update({ login: e.target.value })} placeholder="user"
|
||||||
|
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
|
||||||
|
style={inputStyle(t, fontMono, !D.form.auth)} />
|
||||||
|
</Field>
|
||||||
|
<Field t={t} label="Password" style={{ flex: 1 }}>
|
||||||
|
<input disabled={!D.form.auth} type="password" value={D.form.password}
|
||||||
|
onChange={e => D.update({ password: e.target.value })} placeholder="••••••"
|
||||||
|
onKeyDown={e => e.key === 'Enter' && D.runCheck()}
|
||||||
|
style={inputStyle(t, fontMono, !D.form.auth)} />
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PrimaryBtn t={t} onClick={D.runCheck} disabled={D.phase === 'checking' || isActive}>
|
||||||
|
{D.phase === 'checking' ? 'Checking…' : 'Check connection'}
|
||||||
|
</PrimaryBtn>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div style={{ height: 18 }} />
|
||||||
|
<SectionLabel t={t}>Status</SectionLabel>
|
||||||
|
<ClassicStatus t={t} D={D} palette={palette} fontMono={fontMono} />
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div style={{ height: 14 }} />
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<ClassicStartBtn t={t} D={D} fontMono={fontMono} />
|
||||||
|
<ClassicStopBtn t={t} D={D} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isActive && <ClassicLiveStats t={t} stats={D.stats} fontMono={fontMono} />}
|
||||||
|
|
||||||
|
<div style={{ height: 14 }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logs collapsible */}
|
||||||
|
<ClassicLogs t={t} D={D} fontMono={fontMono} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── pieces ─────────────────────────────────────────────────────────────────
|
||||||
|
function ClassicTitleBar({ t, version, mode, onToggleMode }) {
|
||||||
|
// Cell height = full title-bar height so the hover background fills
|
||||||
|
// edge-to-edge (no thin sliver of chrome above the red close highlight).
|
||||||
|
// alignSelf:'stretch' on the wrapper keeps it pinned to the top/bottom
|
||||||
|
// of the flex row even though parent uses alignItems:'center' for text.
|
||||||
|
const cellStyle = {
|
||||||
|
width: 38, height: '100%', display:'flex', alignItems:'center', justifyContent:'center',
|
||||||
|
color: t.dim, cursor:'pointer',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
height: 32, background: t.chrome, borderBottom: `1px solid ${t.borderSoft}`,
|
||||||
|
display:'flex', alignItems:'stretch',
|
||||||
|
// CSS Wails recognises for the OS title-bar drag region. The
|
||||||
|
// close/min cells below override it with --wails-draggable: no-drag
|
||||||
|
// so clicks land on the buttons, not the drag handler.
|
||||||
|
['--wails-draggable']: 'drag',
|
||||||
|
userSelect:'none',
|
||||||
|
}}>
|
||||||
|
<div style={{ display:'flex', alignItems:'center', gap:8, padding:'0 12px', flex:1 }}>
|
||||||
|
<BrandMark size={14} color={t.accent}/>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 600, letterSpacing: 0.1 }}>Drover-Go</span>
|
||||||
|
{version && <span style={{ fontSize: 11, color: t.dimmer, fontFamily:'ui-monospace,monospace' }}>v{version}</span>}
|
||||||
|
</div>
|
||||||
|
<div style={{ display:'flex', alignItems:'stretch', ['--wails-draggable']: 'no-drag' }}>
|
||||||
|
<div style={cellStyle}
|
||||||
|
title={mode === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
|
||||||
|
onClick={(e) => onToggleMode && onToggleMode(e)}>
|
||||||
|
{mode === 'dark' ? <IconSun color={t.dim}/> : <IconMoon color={t.dim}/>}
|
||||||
|
</div>
|
||||||
|
<div style={cellStyle} title="Minimize" onClick={() => WindowMinimise()}>
|
||||||
|
<IconMin color={t.dim}/>
|
||||||
|
</div>
|
||||||
|
<div style={cellStyle} title="Close"
|
||||||
|
onClick={() => Quit()}
|
||||||
|
onMouseEnter={e => e.currentTarget.style.background = '#c0463f'}
|
||||||
|
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
|
||||||
|
<IconClose color={t.dim}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionLabel({ children, t }) {
|
||||||
|
return <div style={{
|
||||||
|
fontSize: 10.5, fontWeight: 600, letterSpacing: 1.2, textTransform: 'uppercase',
|
||||||
|
color: t.dim, marginBottom: 8,
|
||||||
|
}}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Field({ children, label, t, style }) {
|
||||||
|
return (
|
||||||
|
<label style={{ display:'flex', flexDirection:'column', gap: 4, ...style }}>
|
||||||
|
<span style={{ fontSize: 10.5, color: t.dim, fontWeight: 500 }}>{label}</span>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputStyle(t, fontMono, disabled) {
|
||||||
|
return {
|
||||||
|
background: t.inputBg, color: disabled ? t.dimmer : t.text,
|
||||||
|
border: `1px solid ${t.border}`, borderRadius: 3, padding: '7px 9px',
|
||||||
|
fontFamily: fontMono, fontSize: 12, outline: 'none', width: '100%', boxSizing: 'border-box',
|
||||||
|
transition: 'border-color .12s, box-shadow .12s',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function Checkbox({ checked, onChange, children, t }) {
|
||||||
|
return (
|
||||||
|
<label style={{ display:'inline-flex', alignItems:'center', gap: 7, cursor:'pointer', userSelect:'none', fontSize: 12 }}>
|
||||||
|
<span style={{
|
||||||
|
width: 14, height: 14, borderRadius: 2, border: `1px solid ${checked ? t.accent : t.border}`,
|
||||||
|
background: checked ? t.accent : 'transparent', display:'flex', alignItems:'center', justifyContent:'center',
|
||||||
|
transition: 'background .12s, border-color .12s',
|
||||||
|
}}>
|
||||||
|
{checked && <svg width="9" height="9" viewBox="0 0 9 9"><path d="M1.5 4.5l2 2 4-4" stroke={t.primaryFg} strokeWidth="1.6" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>}
|
||||||
|
</span>
|
||||||
|
<input type="checkbox" checked={checked} onChange={e => onChange(e.target.checked)} style={{ display:'none' }}/>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PrimaryBtn({ t, onClick, disabled, children, style }) {
|
||||||
|
return (
|
||||||
|
<button onClick={onClick} disabled={disabled}
|
||||||
|
style={{
|
||||||
|
width:'100%', padding:'9px 12px', border:'none', borderRadius: 3,
|
||||||
|
background: disabled ? t.btnBg : t.primaryBg, color: disabled ? t.dimmer : t.primaryFg,
|
||||||
|
fontWeight: 600, fontSize: 12.5, letterSpacing: 0.1, cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
boxShadow: disabled ? 'none' : `inset 0 -1px 0 rgba(0,0,0,.18)`,
|
||||||
|
transition: 'background .12s', ...style,
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── status panel ──────────────────────────────────────────────────────────
|
||||||
|
function ClassicStatus({ t, D, palette, fontMono }) {
|
||||||
|
const idle = D.phase === 'idle';
|
||||||
|
if (idle) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
background: t.panel, border: `1px solid ${t.borderSoft}`, borderRadius: 4,
|
||||||
|
padding: '14px 14px', display:'flex', alignItems:'center', gap: 10,
|
||||||
|
}}>
|
||||||
|
<span style={{ width: 8, height: 8, borderRadius: 4, background: t.dimmer }}/>
|
||||||
|
<span style={{ color: t.dim, fontSize: 12.5 }}>Ready to check</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ background: t.panel, border: `1px solid ${t.borderSoft}`, borderRadius: 4, overflow:'hidden' }}>
|
||||||
|
{/* header */}
|
||||||
|
<div style={{
|
||||||
|
padding: '8px 12px', display:'flex', alignItems:'center', gap: 8,
|
||||||
|
borderBottom: `1px solid ${t.borderSoft}`, background: t.panelAlt, fontSize: 12,
|
||||||
|
}}>
|
||||||
|
{D.phase === 'checking'
|
||||||
|
? <>
|
||||||
|
<StatusDot state="running" palette={palette} size={12}/>
|
||||||
|
<span>Running diagnostics…</span>
|
||||||
|
<span style={{ marginLeft:'auto', color: t.dim, fontFamily: fontMono, fontSize: 11 }}>
|
||||||
|
{Object.keys(D.results).length}/{D.tests.length}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
: D.lastSummary?.failed === 0
|
||||||
|
? <span style={{ color: t.pass, fontWeight: 600 }}>All checks passed. Ready to start.</span>
|
||||||
|
: <span style={{ color: t.warn, fontWeight: 600 }}>{D.lastSummary?.failed} of {D.tests.length} checks failed. Some features won't work.</span>}
|
||||||
|
</div>
|
||||||
|
{/* tests */}
|
||||||
|
<div>
|
||||||
|
{D.tests.map((test, i) => {
|
||||||
|
const r = D.results[test.id];
|
||||||
|
const state = r?.result || (D.running === test.id ? 'running' : 'pending');
|
||||||
|
const isLast = i === D.tests.length - 1;
|
||||||
|
return (
|
||||||
|
<div key={test.id} style={{
|
||||||
|
borderBottom: !isLast ? `1px solid ${t.borderSoft}` : 'none',
|
||||||
|
padding: '6px 12px',
|
||||||
|
}}>
|
||||||
|
<div style={{ display:'flex', alignItems:'center', gap: 9, height: 22 }}>
|
||||||
|
<StatusDot state={state} palette={palette} size={12}/>
|
||||||
|
<span style={{ fontSize: 12, color: state === 'pending' ? t.dim : t.text }} title={test.desc}>
|
||||||
|
{test.label}
|
||||||
|
</span>
|
||||||
|
<span style={{ marginLeft:'auto', fontFamily: fontMono, fontSize: 11,
|
||||||
|
color: state === 'failed' ? t.danger : state === 'skipped' ? t.skip : t.dim }}>
|
||||||
|
{r?.metric || (state === 'running' ? '...' : '')}
|
||||||
|
</span>
|
||||||
|
{r?.result === 'failed' && (
|
||||||
|
<button onClick={() => D.toggleExpand(test.id)} style={iconBtnStyle(t)} title="Подробнее">
|
||||||
|
<IconChevron color={t.dim} dir={r.expanded ? 'up' : 'down'}/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{r?.result === 'failed' && r.expanded && (
|
||||||
|
<div className="drv-fadein" style={{
|
||||||
|
margin: '4px 0 6px 21px', padding: '8px 10px', borderRadius: 3,
|
||||||
|
background: mode_mix(t.danger, t.panel, 0.9), border: `1px solid ${mode_mix(t.danger, t.panel, 0.78)}`,
|
||||||
|
fontSize: 11.5, color: t.text,
|
||||||
|
}}>
|
||||||
|
<div style={{ color: t.danger, fontWeight: 600, marginBottom: 2 }}>{r.error}</div>
|
||||||
|
<div style={{ color: t.dim }}>{r.hint}</div>
|
||||||
|
<div style={{ display:'flex', gap: 6, marginTop: 6 }}>
|
||||||
|
<button onClick={() => navigator.clipboard?.writeText(`[${test.label}] ${r.error} — ${r.metric}`)}
|
||||||
|
style={smallBtn(t, fontMono)}>
|
||||||
|
<IconCopy color={t.dim}/> copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconBtnStyle(t) {
|
||||||
|
return {
|
||||||
|
width: 20, height: 20, padding: 0, border:'none', background:'transparent',
|
||||||
|
cursor:'pointer', display:'inline-flex', alignItems:'center', justifyContent:'center',
|
||||||
|
borderRadius: 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function smallBtn(t, fontMono) {
|
||||||
|
return {
|
||||||
|
display:'inline-flex', alignItems:'center', gap: 4, padding: '3px 7px',
|
||||||
|
background: t.btnBg, border: `1px solid ${t.border}`, color: t.dim,
|
||||||
|
borderRadius: 3, fontFamily: fontMono, fontSize: 10.5, cursor:'pointer',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// crude color mix for dark/light. expects hex (#rrggbb), bg can be hex too. amount=share-of-bg.
|
||||||
|
function mode_mix(fg, bg, amt) {
|
||||||
|
const a = hexToRgb(fg), b = hexToRgb(bg);
|
||||||
|
return `rgb(${Math.round(a.r*(1-amt)+b.r*amt)},${Math.round(a.g*(1-amt)+b.g*amt)},${Math.round(a.b*(1-amt)+b.b*amt)})`;
|
||||||
|
}
|
||||||
|
function hexToRgb(h) {
|
||||||
|
const v = h.replace('#','');
|
||||||
|
return { r: parseInt(v.slice(0,2),16), g: parseInt(v.slice(2,4),16), b: parseInt(v.slice(4,6),16) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── start/stop ────────────────────────────────────────────────────────────
|
||||||
|
function ClassicStartBtn({ t, D, fontMono }) {
|
||||||
|
const phase = D.phase;
|
||||||
|
const summary = D.lastSummary;
|
||||||
|
const allFailed = summary && summary.failed === D.tests.length;
|
||||||
|
const checkedOk = phase === 'checked' && !allFailed;
|
||||||
|
const active = phase === 'active';
|
||||||
|
const warning = active && (summary?.failed || 0) > 0;
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
flex:1, padding:'9px 12px', borderRadius: 3, display:'flex', alignItems:'center', justifyContent:'center', gap: 8,
|
||||||
|
background: warning ? mode_mix(t.warn, t.panel, 0.85) : mode_mix(t.pass, t.panel, 0.85),
|
||||||
|
border: `1px solid ${warning ? t.warn : t.pass}`,
|
||||||
|
color: warning ? t.warn : t.pass, fontWeight: 600, fontSize: 12.5, fontFamily: fontMono,
|
||||||
|
}}>
|
||||||
|
<span className="drv-pulsedot" style={{
|
||||||
|
width: 8, height: 8, borderRadius: 4, background: warning ? t.warn : t.pass,
|
||||||
|
}}/>
|
||||||
|
Active{warning ? ' · UDP fallback' : ''}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<PrimaryBtn t={t} disabled={!checkedOk} onClick={D.startProxy} style={{ flex: 1 }}>
|
||||||
|
Start proxying
|
||||||
|
</PrimaryBtn>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClassicStopBtn({ t, D }) {
|
||||||
|
const enabled = D.phase === 'active';
|
||||||
|
return (
|
||||||
|
<button onClick={D.stopProxy} disabled={!enabled}
|
||||||
|
style={{
|
||||||
|
flex:1, padding:'9px 12px', borderRadius: 3, fontWeight: 600, fontSize: 12.5,
|
||||||
|
background: t.btnBg, color: enabled ? t.text : t.dimmer,
|
||||||
|
border: `1px solid ${t.border}`, cursor: enabled ? 'pointer':'not-allowed',
|
||||||
|
}}>
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClassicLiveStats({ t, stats, fontMono }) {
|
||||||
|
const cell = (icon, val) => (
|
||||||
|
<div style={{ display:'flex', alignItems:'center', gap: 4, color: t.dim, fontFamily: fontMono, fontSize: 11 }}>
|
||||||
|
{icon}<span>{val}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
marginTop: 8, padding: '6px 10px', borderRadius: 3,
|
||||||
|
background: t.panel, border: `1px solid ${t.borderSoft}`,
|
||||||
|
display:'flex', justifyContent:'space-between', alignItems:'center',
|
||||||
|
}}>
|
||||||
|
{cell(<IconArrowUp color={t.dim}/>, fmtBytes(stats.up))}
|
||||||
|
{cell(<IconArrowDown color={t.dim}/>, fmtBytes(stats.down))}
|
||||||
|
{cell(<span style={{fontSize:9, color:t.dimmer}}>TCP</span>, stats.tcp)}
|
||||||
|
{cell(<span style={{fontSize:9, color:t.dimmer}}>UDP</span>, stats.udp)}
|
||||||
|
{cell(<span style={{fontSize:9, color:t.dimmer}}>↑t</span>, fmtUptime(stats.uptimeS))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── logs ──────────────────────────────────────────────────────────────────
|
||||||
|
function ClassicLogs({ t, D, fontMono }) {
|
||||||
|
return (
|
||||||
|
<div style={{ borderTop: `1px solid ${t.borderSoft}`, background: t.chrome, flexShrink: 0 }}>
|
||||||
|
<button onClick={() => D.setLogsOpen(!D.logsOpen)} style={{
|
||||||
|
width:'100%', padding: '8px 14px', display:'flex', alignItems:'center', gap:8,
|
||||||
|
background:'transparent', border:'none', color: t.dim, cursor:'pointer',
|
||||||
|
fontSize: 11, fontFamily: fontMono, letterSpacing: 0.3,
|
||||||
|
}}>
|
||||||
|
<IconChevron color={t.dim} dir={D.logsOpen ? 'down' : 'right'}/>
|
||||||
|
<span style={{ textTransform:'uppercase' }}>Logs</span>
|
||||||
|
<span style={{ marginLeft: 'auto', color: t.dimmer }}>{D.logs.length} lines</span>
|
||||||
|
</button>
|
||||||
|
{D.logsOpen && (
|
||||||
|
<div style={{ borderTop: `1px solid ${t.borderSoft}` }}>
|
||||||
|
<div style={{ display:'flex', gap: 6, padding: '6px 12px', borderBottom: `1px solid ${t.borderSoft}` }}>
|
||||||
|
<button style={smallBtn(t, fontMono)}
|
||||||
|
onClick={() => navigator.clipboard?.writeText(D.logs.map(l => `[${l.level}] ${l.msg}`).join('\n'))}>copy all</button>
|
||||||
|
<button style={smallBtn(t, fontMono)} onClick={D.clearLogs}>clear</button>
|
||||||
|
<button style={smallBtn(t, fontMono)}>open log file</button>
|
||||||
|
</div>
|
||||||
|
<div className="drv-log" style={{
|
||||||
|
maxHeight: 130, overflowY: 'auto', padding: '6px 12px',
|
||||||
|
fontFamily: fontMono, fontSize: 10.5, lineHeight: 1.55, color: t.dim,
|
||||||
|
background: t.panelAlt,
|
||||||
|
}} ref={el => el && (el.scrollTop = el.scrollHeight)}>
|
||||||
|
{D.logs.map((l, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<span style={{ color: t.dimmer }}>{fmtTime(l.t)}</span>
|
||||||
|
{' '}
|
||||||
|
<span style={{ color: l.level==='ERROR'?t.danger:l.level==='WARN'?t.warn:t.pass, fontWeight: 600 }}>[{l.level}]</span>
|
||||||
|
{' '}
|
||||||
|
<span>{l.msg}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default export so App.jsx can `import ClassicWindow from './components/Classic'`.
|
||||||
|
export default ClassicWindow;
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
// shared.jsx — state machine + shared icons/utilities for all Drover-Go variants.
|
||||||
|
//
|
||||||
|
// Original prototype loaded everything via window globals (babel script-tag
|
||||||
|
// build). For Wails + Vite we use real ESM imports/exports — additions:
|
||||||
|
// - `import * as React from 'react'` so `React.useState/useMemo/useEffect`
|
||||||
|
// keep working unchanged.
|
||||||
|
// - `export` on everything the variant components need.
|
||||||
|
// - `useDrover` no longer simulates with `SCENARIOS`; it calls the Wails
|
||||||
|
// bindings on `window.go.main.App` and listens for the events the Go
|
||||||
|
// side emits (`check:result`, `check:done`, `stats:update`, ...).
|
||||||
|
//
|
||||||
|
// The state surface (form/phase/results/stats/logs) is unchanged, so the
|
||||||
|
// UI components don't need to be rewritten — only their imports.
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import { RunCheck, StartEngine, StopEngine, GetStatus } from '../../wailsjs/go/gui/App'
|
||||||
|
import { EventsOn, EventsOff } from '../../wailsjs/runtime/runtime'
|
||||||
|
|
||||||
|
// ─── Test catalog ──────────────────────────────────────────────────────────
|
||||||
|
export const ALL_TESTS = [
|
||||||
|
{ id: 'tcp', label: 'TCP reachability', desc: 'TCP-соединение до прокси установлено' },
|
||||||
|
{ id: 'greet', label: 'SOCKS5 greeting', desc: 'Прокси отвечает по протоколу SOCKS5' },
|
||||||
|
{ id: 'auth', label: 'SOCKS5 authentication', desc: 'Аутентификация по логину/паролю принята', authOnly: true },
|
||||||
|
{ id: 'connect', label: 'TCP CONNECT to Discord', desc: 'Прокси проксирует TCP к gateway' },
|
||||||
|
{ id: 'udp', label: 'UDP ASSOCIATE', desc: 'Прокси выдал UDP-релей' },
|
||||||
|
{ id: 'stun', label: 'UDP round-trip via STUN', desc: 'UDP-пакеты ходят туда-обратно' },
|
||||||
|
{ id: 'api', label: 'Discord API reachable', desc: 'HTTPS до discord.com через прокси' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Pre-baked scenarios so the prototype feels alive. Each entry per test:
|
||||||
|
// { result: 'passed'|'failed'|'skipped', metric: '12 ms' | 'ok' | …, error?: 'short msg', hint?: 'what to try' }
|
||||||
|
const SCENARIOS = {
|
||||||
|
// Default happy path (no auth)
|
||||||
|
happy: {
|
||||||
|
tcp: { result: 'passed', metric: '14 ms' },
|
||||||
|
greet: { result: 'passed', metric: 'SOCKS5/0x05' },
|
||||||
|
connect: { result: 'passed', metric: 'gateway.discord.gg' },
|
||||||
|
udp: { result: 'passed', metric: 'relay 95.165.72.59:54321' },
|
||||||
|
stun: { result: 'passed', metric: '38 ms RTT' },
|
||||||
|
api: { result: 'passed', metric: '204 OK · 89 ms' },
|
||||||
|
},
|
||||||
|
// With auth
|
||||||
|
happyAuth: {
|
||||||
|
tcp: { result: 'passed', metric: '14 ms' },
|
||||||
|
greet: { result: 'passed', metric: 'SOCKS5/0x05' },
|
||||||
|
auth: { result: 'passed', metric: 'user/pass · ok' },
|
||||||
|
connect: { result: 'passed', metric: 'gateway.discord.gg' },
|
||||||
|
udp: { result: 'passed', metric: 'relay 95.165.72.59:54321' },
|
||||||
|
stun: { result: 'passed', metric: '38 ms RTT' },
|
||||||
|
api: { result: 'passed', metric: '204 OK · 89 ms' },
|
||||||
|
},
|
||||||
|
// UDP fails — common Discord scenario
|
||||||
|
udpFail: {
|
||||||
|
tcp: { result: 'passed', metric: '17 ms' },
|
||||||
|
greet: { result: 'passed', metric: 'SOCKS5/0x05' },
|
||||||
|
connect: { result: 'passed', metric: 'gateway.discord.gg' },
|
||||||
|
udp: { result: 'failed', metric: 'X\'07 cmd not supported',
|
||||||
|
error: 'Прокси не поддерживает UDP ASSOCIATE.',
|
||||||
|
hint: 'Голос и демонстрация экрана работать не будут. Текст и API — будут. Попробуйте другой SOCKS5-сервер с поддержкой UDP.' },
|
||||||
|
stun: { result: 'skipped', metric: 'требует UDP ASSOCIATE' },
|
||||||
|
api: { result: 'passed', metric: '204 OK · 92 ms' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getTests(authEnabled) {
|
||||||
|
return ALL_TESTS.filter(t => !t.authOnly || authEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Drover state hook ─────────────────────────────────────────────────────
|
||||||
|
// Owns: form values, diagnostic phase, per-test results, drover-active state,
|
||||||
|
// live stats counter, log buffer.
|
||||||
|
// phase: 'idle' | 'checking' | 'checked' | 'active'
|
||||||
|
export function useDrover(initial = {}) {
|
||||||
|
const [form, setForm] = React.useState({
|
||||||
|
host: '95.165.72.59',
|
||||||
|
port: '12334',
|
||||||
|
auth: false,
|
||||||
|
login: '',
|
||||||
|
password: '',
|
||||||
|
...initial,
|
||||||
|
});
|
||||||
|
const [phase, setPhase] = React.useState('idle');
|
||||||
|
const [results, setResults] = React.useState({}); // testId -> {result, metric, error, hint, expanded}
|
||||||
|
const [running, setRunning] = React.useState(null); // currently-running test id
|
||||||
|
const [scenario, setScenario] = React.useState('happy'); // kept for compat with prototype, unused with real backend
|
||||||
|
const [stats, setStats] = React.useState({ up: 0, down: 0, tcp: 0, udp: 0, uptimeS: 0 });
|
||||||
|
const [logs, setLogs] = React.useState(() => seedLogs());
|
||||||
|
const [logsOpen, setLogsOpen] = React.useState(false);
|
||||||
|
const tests = getTests(form.auth);
|
||||||
|
const lastSummary = React.useMemo(() => {
|
||||||
|
if (phase !== 'checked' && phase !== 'active') return null;
|
||||||
|
const ids = tests.map(t => t.id);
|
||||||
|
const failed = ids.filter(id => results[id]?.result === 'failed').length;
|
||||||
|
return { total: ids.length, failed };
|
||||||
|
}, [phase, results, tests]);
|
||||||
|
|
||||||
|
// ── actions ────────────────────────────────────────────────────────────
|
||||||
|
function update(patch) { setForm(f => ({ ...f, ...patch })); }
|
||||||
|
|
||||||
|
function pushLog(level, msg) {
|
||||||
|
setLogs(l => [...l.slice(-499), { t: Date.now(), level, msg }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to backend events once. The Go side emits:
|
||||||
|
// check:result → one test result (id, status, metric, error, hint)
|
||||||
|
// check:done → diagnostic finished, summary {total, passed, failed}
|
||||||
|
// engine:status → {running: bool}
|
||||||
|
// stats:update → {up, down, tcp, udp, uptimeS}
|
||||||
|
React.useEffect(() => {
|
||||||
|
const offResult = EventsOn('check:result', (r) => {
|
||||||
|
if (r.status === 'running') {
|
||||||
|
setRunning(r.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Convert backend "status" field to the frontend's "result" field used
|
||||||
|
// by the Classic/Fluent/etc components.
|
||||||
|
setResults(prev => ({
|
||||||
|
...prev,
|
||||||
|
[r.id]: {
|
||||||
|
result: r.status,
|
||||||
|
metric: r.metric,
|
||||||
|
error: r.error,
|
||||||
|
hint: r.hint,
|
||||||
|
expanded: r.status === 'failed',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
pushLog(r.status === 'failed' ? 'ERROR' : r.status === 'skipped' ? 'WARN' : 'INFO',
|
||||||
|
`${r.id}: ${r.status}${r.metric ? ' · ' + r.metric : ''}`);
|
||||||
|
});
|
||||||
|
const offDone = EventsOn('check:done', (s) => {
|
||||||
|
setRunning(null);
|
||||||
|
setPhase('checked');
|
||||||
|
pushLog('INFO', `check finished — ${s.passed}/${s.total} passed`);
|
||||||
|
});
|
||||||
|
const offStatus = EventsOn('engine:status', (s) => {
|
||||||
|
setPhase(s.running ? 'active' : 'checked');
|
||||||
|
pushLog('INFO', s.running ? 'engine: started' : 'engine: stopped');
|
||||||
|
});
|
||||||
|
const offStats = EventsOn('stats:update', (s) => setStats(s));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
offResult();
|
||||||
|
offDone();
|
||||||
|
offStatus();
|
||||||
|
offStats();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function runCheck() {
|
||||||
|
if (phase === 'checking') return;
|
||||||
|
setPhase('checking');
|
||||||
|
setResults({});
|
||||||
|
setRunning(null);
|
||||||
|
pushLog('INFO', `connect ${form.host}:${form.port}${form.auth ? ' (auth)' : ''}`);
|
||||||
|
await RunCheck({
|
||||||
|
host: form.host,
|
||||||
|
port: parseInt(form.port, 10) || 0,
|
||||||
|
auth: form.auth,
|
||||||
|
login: form.login,
|
||||||
|
password: form.password,
|
||||||
|
});
|
||||||
|
// The rest is event-driven (check:result, check:done) — see useEffect above.
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startProxy() {
|
||||||
|
if (phase !== 'checked') return;
|
||||||
|
if (lastSummary?.failed === tests.length) return;
|
||||||
|
await StartEngine();
|
||||||
|
// engine:status event will flip phase to 'active'.
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopProxy() {
|
||||||
|
if (phase !== 'active') return;
|
||||||
|
await StopEngine();
|
||||||
|
setStats({ up: 0, down: 0, tcp: 0, udp: 0, uptimeS: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reflect initial backend state (in case the engine was already running
|
||||||
|
// when the GUI was opened — e.g. via service mode).
|
||||||
|
React.useEffect(() => {
|
||||||
|
GetStatus().then((s) => {
|
||||||
|
if (s?.running) setPhase('active');
|
||||||
|
}).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Toggle a test's expanded explanation
|
||||||
|
function toggleExpand(id) {
|
||||||
|
setResults(r => ({ ...r, [id]: { ...r[id], expanded: !r[id]?.expanded } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
form, update,
|
||||||
|
phase, setPhase,
|
||||||
|
tests, results, running,
|
||||||
|
scenario, setScenario,
|
||||||
|
stats,
|
||||||
|
logs, logsOpen, setLogsOpen, pushLog, clearLogs: () => setLogs([]),
|
||||||
|
lastSummary,
|
||||||
|
runCheck, startProxy, stopProxy,
|
||||||
|
toggleExpand,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||||||
|
|
||||||
|
// Sun + moon icons for the theme-toggle button in the title bar. Style
|
||||||
|
// matches the rest (1.2 stroke, 14px square viewBox).
|
||||||
|
export function IconSun({ size=14, color='currentColor' }) {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 16 16" fill="none">
|
||||||
|
<circle cx="8" cy="8" r="3" stroke={color} strokeWidth="1.2"/>
|
||||||
|
<path d="M8 1.5v1.5M8 13v1.5M14.5 8H13M3 8H1.5M12.6 3.4l-1 1M4.4 11.6l-1 1M12.6 12.6l-1-1M4.4 4.4l-1-1"
|
||||||
|
stroke={color} strokeWidth="1.2" strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function IconMoon({ size=14, color='currentColor' }) {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M13.5 9.5A5.5 5.5 0 1 1 6.5 2.5a4 4 0 0 0 7 7z"
|
||||||
|
stroke={color} strokeWidth="1.2" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function seedLogs() {
|
||||||
|
const t = Date.now();
|
||||||
|
return [
|
||||||
|
{ t: t-9200, level: 'INFO', msg: 'drover-go v0.4.2 starting' },
|
||||||
|
{ t: t-9100, level: 'INFO', msg: 'config: ~/.drover/config.toml' },
|
||||||
|
{ t: t-9000, level: 'INFO', msg: 'no active session' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fmtBytes(n) {
|
||||||
|
if (n < 1024) return n.toFixed(0) + ' B/s';
|
||||||
|
if (n < 1024*1024) return (n/1024).toFixed(1) + ' KB/s';
|
||||||
|
return (n/1024/1024).toFixed(2) + ' MB/s';
|
||||||
|
}
|
||||||
|
export function fmtUptime(s) {
|
||||||
|
const h = Math.floor(s/3600), m = Math.floor((s%3600)/60), ss = s%60;
|
||||||
|
if (h) return `${h}h ${m}m`;
|
||||||
|
if (m) return `${m}m ${ss}s`;
|
||||||
|
return `${ss}s`;
|
||||||
|
}
|
||||||
|
export function fmtTime(t) {
|
||||||
|
const d = new Date(t);
|
||||||
|
return d.toLocaleTimeString('en-GB', { hour12: false }) + '.' + String(d.getMilliseconds()).padStart(3,'0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Shared icons (small, original) ────────────────────────────────────────
|
||||||
|
// Drover-Go mark: a downward chevron through a ring — "tunneled traffic".
|
||||||
|
export function BrandMark({ size = 16, color = 'currentColor', strokeWidth = 1.6 }) {
|
||||||
|
const s = size;
|
||||||
|
return (
|
||||||
|
<svg width={s} height={s} viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="9" stroke={color} strokeWidth={strokeWidth}/>
|
||||||
|
<path d="M7 9 L12 14 L17 9" stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M12 14 L12 19" stroke={color} strokeWidth={strokeWidth} strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconGear({ size=14, color='currentColor' }) {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 16 16" fill="none">
|
||||||
|
<circle cx="8" cy="8" r="2.2" stroke={color} strokeWidth="1.2"/>
|
||||||
|
<path d="M8 1.5v2M8 12.5v2M14.5 8h-2M3.5 8h-2M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4M12.6 12.6l-1.4-1.4M4.8 4.8L3.4 3.4"
|
||||||
|
stroke={color} strokeWidth="1.2" strokeLinecap="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function IconMin({ size=14, color='currentColor' }) {
|
||||||
|
return <svg width={size} height={size} viewBox="0 0 16 16"><path d="M3 8h10" stroke={color} strokeWidth="1.2" strokeLinecap="round"/></svg>;
|
||||||
|
}
|
||||||
|
export function IconClose({ size=14, color='currentColor' }) {
|
||||||
|
return <svg width={size} height={size} viewBox="0 0 16 16"><path d="M4 4l8 8M12 4l-8 8" stroke={color} strokeWidth="1.2" strokeLinecap="round"/></svg>;
|
||||||
|
}
|
||||||
|
export function IconChevron({ size=12, color='currentColor', dir='down' }) {
|
||||||
|
const r = { down: 0, up: 180, left: 90, right: -90 }[dir];
|
||||||
|
return <svg width={size} height={size} viewBox="0 0 12 12" style={{ transform: `rotate(${r}deg)` }}>
|
||||||
|
<path d="M3 4.5 L6 7.5 L9 4.5" stroke={color} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
export function IconCopy({ size=12, color='currentColor' }) {
|
||||||
|
return <svg width={size} height={size} viewBox="0 0 12 12" fill="none">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1" stroke={color} strokeWidth="1.2"/>
|
||||||
|
<path d="M2 8.5V2.5C2 1.95 2.45 1.5 3 1.5h6" stroke={color} strokeWidth="1.2"/>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
export function IconArrowUp({ size=10, color='currentColor' }) {
|
||||||
|
return <svg width={size} height={size} viewBox="0 0 10 10" fill="none">
|
||||||
|
<path d="M5 8.5V1.5M5 1.5L2 4.5M5 1.5L8 4.5" stroke={color} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
export function IconArrowDown({ size=10, color='currentColor' }) {
|
||||||
|
return <svg width={size} height={size} viewBox="0 0 10 10" fill="none">
|
||||||
|
<path d="M5 1.5V8.5M5 8.5L2 5.5M5 8.5L8 5.5" stroke={color} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Test row state icons (per visual variant supplies its own colors) ─────
|
||||||
|
export function StatusDot({ state, palette, size = 12 }) {
|
||||||
|
// state: 'pending' | 'running' | 'passed' | 'failed' | 'skipped'
|
||||||
|
const c = palette[state] || palette.pending;
|
||||||
|
if (state === 'running') {
|
||||||
|
return (
|
||||||
|
<span style={{ display:'inline-block', width:size, height:size, position:'relative' }}>
|
||||||
|
<svg width={size} height={size} viewBox="0 0 16 16" style={{ animation: 'drv-spin 0.8s linear infinite' }}>
|
||||||
|
<circle cx="8" cy="8" r="6" stroke={c} strokeOpacity="0.25" strokeWidth="2" fill="none"/>
|
||||||
|
<path d="M8 2 a6 6 0 0 1 6 6" stroke={c} strokeWidth="2" strokeLinecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (state === 'passed') {
|
||||||
|
return <svg width={size} height={size} viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="7" fill={c}/>
|
||||||
|
<path d="M5 8.2l2 2 4-4.4" stroke="white" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
if (state === 'failed') {
|
||||||
|
return <svg width={size} height={size} viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="7" fill={c}/>
|
||||||
|
<path d="M5.5 5.5l5 5M10.5 5.5l-5 5" stroke="white" strokeWidth="1.6" strokeLinecap="round"/>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
if (state === 'skipped') {
|
||||||
|
return <svg width={size} height={size} viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="7" fill="none" stroke={c} strokeWidth="1.4" strokeDasharray="2 2"/>
|
||||||
|
<path d="M5 8h6" stroke={c} strokeWidth="1.4" strokeLinecap="round"/>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
// pending
|
||||||
|
return <svg width={size} height={size} viewBox="0 0 16 16">
|
||||||
|
<circle cx="8" cy="8" r="3" fill="none" stroke={c} strokeWidth="1.4"/>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS for the spinner — injected once.
|
||||||
|
if (typeof document !== 'undefined' && !document.getElementById('drv-shared-css')) {
|
||||||
|
const s = document.createElement('style');
|
||||||
|
s.id = 'drv-shared-css';
|
||||||
|
s.textContent = `
|
||||||
|
@keyframes drv-spin { to { transform: rotate(360deg); } }
|
||||||
|
@keyframes drv-pulse { 0%,100% { opacity:1; transform:scale(1);} 50% { opacity:.55; transform:scale(0.7);} }
|
||||||
|
@keyframes drv-blink { 0%,100% { opacity:1;} 50% { opacity:.35;} }
|
||||||
|
@keyframes drv-fadein { from { opacity:0; transform:translateY(-2px);} to { opacity:1; transform:none;} }
|
||||||
|
.drv-fadein { animation: drv-fadein .18s ease-out; }
|
||||||
|
.drv-pulsedot { animation: drv-pulse 1.4s ease-in-out infinite; }
|
||||||
|
.drv-shimmer::after {
|
||||||
|
content:''; position:absolute; inset:0; background: linear-gradient(90deg,transparent,rgba(255,255,255,.25),transparent);
|
||||||
|
transform:translateX(-100%); animation: drv-shim 1.6s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes drv-shim { to { transform: translateX(100%); } }
|
||||||
|
/* Hide scrollbars for log panes inside artboards */
|
||||||
|
.drv-log::-webkit-scrollbar { width:6px; }
|
||||||
|
.drv-log::-webkit-scrollbar-thumb { background: rgba(127,127,127,.35); border-radius: 3px; }
|
||||||
|
`;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose globals
|
||||||
|
Object.assign(window, {
|
||||||
|
useDrover, getTests, ALL_TESTS, SCENARIOS,
|
||||||
|
fmtBytes, fmtUptime, fmtTime,
|
||||||
|
BrandMark, StatusDot,
|
||||||
|
IconGear, IconMin, IconClose, IconChevron, IconCopy, IconArrowUp, IconArrowDown,
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {createRoot} from 'react-dom/client'
|
||||||
|
import './style.css'
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
const container = document.getElementById('root')
|
||||||
|
|
||||||
|
const root = createRoot(container)
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App/>
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/* Reset everything Wails react-template ships by default — Classic component
|
||||||
|
* draws the entire surface, including the title bar. */
|
||||||
|
|
||||||
|
html, body, #app, #root {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #1c1d20;
|
||||||
|
color: #dde0e6;
|
||||||
|
font-family:
|
||||||
|
"Inter", "Segoe UI Variable", "Segoe UI", system-ui, -apple-system,
|
||||||
|
BlinkMacSystemFont, sans-serif;
|
||||||
|
font-size: 13.5px;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Theme switch: circle reveal from the cursor ──────────────────────── */
|
||||||
|
/* The title-bar sun/moon button calls document.startViewTransition() before
|
||||||
|
* flipping the mode state; the API snapshots the old DOM, runs the React
|
||||||
|
* update, and gives us pseudo-elements `::view-transition-old(root)` and
|
||||||
|
* `::view-transition-new(root)` to animate between. We override the default
|
||||||
|
* cross-fade with a circular clip-path expanding from --reveal-x/y, which
|
||||||
|
* is set to the click coordinates by App.jsx right before the transition. */
|
||||||
|
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: none;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The new state expands from a tiny circle at the cursor into the whole
|
||||||
|
* window. The old state stays put underneath. 0.45s feels lively without
|
||||||
|
* dragging — long-form circle reveals (>700ms) start to feel laggy. */
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: theme-reveal 0.45s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root) {
|
||||||
|
animation: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes theme-reveal {
|
||||||
|
from { clip-path: circle(0% at var(--reveal-x, 50%) var(--reveal-y, 50%)); }
|
||||||
|
to { clip-path: circle(150% at var(--reveal-x, 50%) var(--reveal-y, 50%)); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import {defineConfig} from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()]
|
||||||
|
})
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Manually-written JS bindings for the App struct in package
|
||||||
|
// git.okcu.io/root/drover-go/internal/gui.
|
||||||
|
//
|
||||||
|
// Wails Bind() exposes the app's methods at runtime under
|
||||||
|
// window.go.<package>.App.<Method>, where <package> is the Go package
|
||||||
|
// where App is defined (here: "gui"). These wrappers give us a stable
|
||||||
|
// import path from the React side and are the equivalent of what
|
||||||
|
// `wails generate module` would have produced if we used the standard
|
||||||
|
// flat layout.
|
||||||
|
//
|
||||||
|
// Whenever a new App method is added in internal/gui/app.go, mirror it here.
|
||||||
|
|
||||||
|
export function RunCheck(cfg) { return window['go']['gui']['App']['RunCheck'](cfg) }
|
||||||
|
export function StartEngine() { return window['go']['gui']['App']['StartEngine']() }
|
||||||
|
export function StopEngine() { return window['go']['gui']['App']['StopEngine']() }
|
||||||
|
export function GetStatus() { return window['go']['gui']['App']['GetStatus']() }
|
||||||
|
export function Version() { return window['go']['gui']['App']['Version']() }
|
||||||
|
export function Greet(name) { return window['go']['gui']['App']['Greet'](name) }
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@wailsapp/runtime",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "Wails Javascript runtime library",
|
||||||
|
"main": "runtime.js",
|
||||||
|
"types": "runtime.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/wailsapp/wails.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Wails",
|
||||||
|
"Javascript",
|
||||||
|
"Go"
|
||||||
|
],
|
||||||
|
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/wailsapp/wails/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||||
|
}
|
||||||
+211
@@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The electron alternative for Go
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Position {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Size {
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Screen {
|
||||||
|
isCurrent: boolean;
|
||||||
|
isPrimary: boolean;
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment information such as platform, buildtype, ...
|
||||||
|
export interface EnvironmentInfo {
|
||||||
|
buildType: string;
|
||||||
|
platform: string;
|
||||||
|
arch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||||
|
// emits the given event. Optional data may be passed with the event.
|
||||||
|
// This will trigger any event listeners.
|
||||||
|
export function EventsEmit(eventName: string, ...data: any): void;
|
||||||
|
|
||||||
|
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||||
|
export function EventsOn(eventName: string, callback: (...data: any) => void): void;
|
||||||
|
|
||||||
|
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||||
|
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||||
|
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): void;
|
||||||
|
|
||||||
|
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||||
|
// sets up a listener for the given event name, but will only trigger once.
|
||||||
|
export function EventsOnce(eventName: string, callback: (...data: any) => void): void;
|
||||||
|
|
||||||
|
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsff)
|
||||||
|
// unregisters the listener for the given event name.
|
||||||
|
export function EventsOff(eventName: string): void;
|
||||||
|
|
||||||
|
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||||
|
// unregisters all event listeners.
|
||||||
|
export function EventsOffAll(): void;
|
||||||
|
|
||||||
|
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||||
|
// logs the given message as a raw message
|
||||||
|
export function LogPrint(message: string): void;
|
||||||
|
|
||||||
|
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||||
|
// logs the given message at the `trace` log level.
|
||||||
|
export function LogTrace(message: string): void;
|
||||||
|
|
||||||
|
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||||
|
// logs the given message at the `debug` log level.
|
||||||
|
export function LogDebug(message: string): void;
|
||||||
|
|
||||||
|
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||||
|
// logs the given message at the `error` log level.
|
||||||
|
export function LogError(message: string): void;
|
||||||
|
|
||||||
|
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||||
|
// logs the given message at the `fatal` log level.
|
||||||
|
// The application will quit after calling this method.
|
||||||
|
export function LogFatal(message: string): void;
|
||||||
|
|
||||||
|
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||||
|
// logs the given message at the `info` log level.
|
||||||
|
export function LogInfo(message: string): void;
|
||||||
|
|
||||||
|
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||||
|
// logs the given message at the `warning` log level.
|
||||||
|
export function LogWarning(message: string): void;
|
||||||
|
|
||||||
|
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||||
|
// Forces a reload by the main application as well as connected browsers.
|
||||||
|
export function WindowReload(): void;
|
||||||
|
|
||||||
|
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||||
|
// Reloads the application frontend.
|
||||||
|
export function WindowReloadApp(): void;
|
||||||
|
|
||||||
|
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||||
|
// Sets the window AlwaysOnTop or not on top.
|
||||||
|
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||||
|
|
||||||
|
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window theme to system default (dark/light).
|
||||||
|
export function WindowSetSystemDefaultTheme(): void;
|
||||||
|
|
||||||
|
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window to light theme.
|
||||||
|
export function WindowSetLightTheme(): void;
|
||||||
|
|
||||||
|
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window to dark theme.
|
||||||
|
export function WindowSetDarkTheme(): void;
|
||||||
|
|
||||||
|
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||||
|
// Centers the window on the monitor the window is currently on.
|
||||||
|
export function WindowCenter(): void;
|
||||||
|
|
||||||
|
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||||
|
// Sets the text in the window title bar.
|
||||||
|
export function WindowSetTitle(title: string): void;
|
||||||
|
|
||||||
|
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||||
|
// Makes the window full screen.
|
||||||
|
export function WindowFullscreen(): void;
|
||||||
|
|
||||||
|
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||||
|
// Restores the previous window dimensions and position prior to full screen.
|
||||||
|
export function WindowUnfullscreen(): void;
|
||||||
|
|
||||||
|
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||||
|
// Sets the width and height of the window.
|
||||||
|
export function WindowSetSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||||
|
// Gets the width and height of the window.
|
||||||
|
export function WindowGetSize(): Promise<Size>;
|
||||||
|
|
||||||
|
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||||
|
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||||
|
// Setting a size of 0,0 will disable this constraint.
|
||||||
|
export function WindowSetMaxSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||||
|
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||||
|
// Setting a size of 0,0 will disable this constraint.
|
||||||
|
export function WindowSetMinSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||||
|
// Sets the window position relative to the monitor the window is currently on.
|
||||||
|
export function WindowSetPosition(x: number, y: number): void;
|
||||||
|
|
||||||
|
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||||
|
// Gets the window position relative to the monitor the window is currently on.
|
||||||
|
export function WindowGetPosition(): Promise<Position>;
|
||||||
|
|
||||||
|
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||||
|
// Hides the window.
|
||||||
|
export function WindowHide(): void;
|
||||||
|
|
||||||
|
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||||
|
// Shows the window, if it is currently hidden.
|
||||||
|
export function WindowShow(): void;
|
||||||
|
|
||||||
|
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||||
|
// Maximises the window to fill the screen.
|
||||||
|
export function WindowMaximise(): void;
|
||||||
|
|
||||||
|
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||||
|
// Toggles between Maximised and UnMaximised.
|
||||||
|
export function WindowToggleMaximise(): void;
|
||||||
|
|
||||||
|
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||||
|
// Restores the window to the dimensions and position prior to maximising.
|
||||||
|
export function WindowUnmaximise(): void;
|
||||||
|
|
||||||
|
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||||
|
// Minimises the window.
|
||||||
|
export function WindowMinimise(): void;
|
||||||
|
|
||||||
|
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||||
|
// Restores the window to the dimensions and position prior to minimising.
|
||||||
|
export function WindowUnminimise(): void;
|
||||||
|
|
||||||
|
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||||
|
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||||
|
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||||
|
|
||||||
|
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||||
|
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||||
|
export function ScreenGetAll(): Promise<Screen[]>;
|
||||||
|
|
||||||
|
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||||
|
// Opens the given URL in the system browser.
|
||||||
|
export function BrowserOpenURL(url: string): void;
|
||||||
|
|
||||||
|
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||||
|
// Returns information about the environment
|
||||||
|
export function Environment(): Promise<EnvironmentInfo>;
|
||||||
|
|
||||||
|
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||||
|
// Quits the application.
|
||||||
|
export function Quit(): void;
|
||||||
|
|
||||||
|
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||||
|
// Hides the application.
|
||||||
|
export function Hide(): void;
|
||||||
|
|
||||||
|
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||||
|
// Shows the application.
|
||||||
|
export function Show(): void;
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The electron alternative for Go
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function LogPrint(message) {
|
||||||
|
window.runtime.LogPrint(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogTrace(message) {
|
||||||
|
window.runtime.LogTrace(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogDebug(message) {
|
||||||
|
window.runtime.LogDebug(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogInfo(message) {
|
||||||
|
window.runtime.LogInfo(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogWarning(message) {
|
||||||
|
window.runtime.LogWarning(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogError(message) {
|
||||||
|
window.runtime.LogError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogFatal(message) {
|
||||||
|
window.runtime.LogFatal(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||||
|
window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOn(eventName, callback) {
|
||||||
|
EventsOnMultiple(eventName, callback, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOff(eventName) {
|
||||||
|
return window.runtime.EventsOff(eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOffAll() {
|
||||||
|
return window.runtime.EventsOffAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOnce(eventName, callback) {
|
||||||
|
EventsOnMultiple(eventName, callback, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsEmit(eventName) {
|
||||||
|
let args = [eventName].slice.call(arguments);
|
||||||
|
return window.runtime.EventsEmit.apply(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowReload() {
|
||||||
|
window.runtime.WindowReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowReloadApp() {
|
||||||
|
window.runtime.WindowReloadApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetAlwaysOnTop(b) {
|
||||||
|
window.runtime.WindowSetAlwaysOnTop(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetSystemDefaultTheme() {
|
||||||
|
window.runtime.WindowSetSystemDefaultTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetLightTheme() {
|
||||||
|
window.runtime.WindowSetLightTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetDarkTheme() {
|
||||||
|
window.runtime.WindowSetDarkTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowCenter() {
|
||||||
|
window.runtime.WindowCenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetTitle(title) {
|
||||||
|
window.runtime.WindowSetTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowFullscreen() {
|
||||||
|
window.runtime.WindowFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnfullscreen() {
|
||||||
|
window.runtime.WindowUnfullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowGetSize() {
|
||||||
|
return window.runtime.WindowGetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetSize(width, height) {
|
||||||
|
window.runtime.WindowSetSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetMaxSize(width, height) {
|
||||||
|
window.runtime.WindowSetMaxSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetMinSize(width, height) {
|
||||||
|
window.runtime.WindowSetMinSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetPosition(x, y) {
|
||||||
|
window.runtime.WindowSetPosition(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowGetPosition() {
|
||||||
|
return window.runtime.WindowGetPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowHide() {
|
||||||
|
window.runtime.WindowHide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowShow() {
|
||||||
|
window.runtime.WindowShow();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMaximise() {
|
||||||
|
window.runtime.WindowMaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowToggleMaximise() {
|
||||||
|
window.runtime.WindowToggleMaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnmaximise() {
|
||||||
|
window.runtime.WindowUnmaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMinimise() {
|
||||||
|
window.runtime.WindowMinimise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnminimise() {
|
||||||
|
window.runtime.WindowUnminimise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||||
|
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScreenGetAll() {
|
||||||
|
return window.runtime.ScreenGetAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BrowserOpenURL(url) {
|
||||||
|
window.runtime.BrowserOpenURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Environment() {
|
||||||
|
return window.runtime.Environment();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Quit() {
|
||||||
|
window.runtime.Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Hide() {
|
||||||
|
window.runtime.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Show() {
|
||||||
|
window.runtime.Show();
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wailsapp/wails/v2"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run launches the Wails GUI. It blocks until the window is closed.
|
||||||
|
//
|
||||||
|
// Window size matches the React design (480×640) but is resizable so
|
||||||
|
// users on smaller displays can shrink it. Title shows the version.
|
||||||
|
func Run(version string) error {
|
||||||
|
app := NewApp(version)
|
||||||
|
|
||||||
|
// Frameless = no native Windows chrome; the React Classic component
|
||||||
|
// renders its own title bar (brand mark, version, theme toggle,
|
||||||
|
// min/close icons) so we deliberately suppress the OS chrome to
|
||||||
|
// avoid stacking two title bars.
|
||||||
|
// The Classic React component renders a fixed 480×640 surface, so we
|
||||||
|
// pin the host window to exactly the same. Allowing resize would
|
||||||
|
// expose the bare Wails background colour around the React canvas
|
||||||
|
// (the "blue strip on the side" issue from early testing).
|
||||||
|
const w, h = 480, 640
|
||||||
|
return wails.Run(&options.App{
|
||||||
|
Title: "Drover-Go " + version,
|
||||||
|
Width: w,
|
||||||
|
Height: h,
|
||||||
|
MinWidth: w,
|
||||||
|
MinHeight: h,
|
||||||
|
MaxWidth: w,
|
||||||
|
MaxHeight: h,
|
||||||
|
DisableResize: true,
|
||||||
|
Frameless: true,
|
||||||
|
AssetServer: &assetserver.Options{
|
||||||
|
Assets: Assets,
|
||||||
|
},
|
||||||
|
BackgroundColour: &options.RGBA{R: 28, G: 29, B: 32, A: 1}, // matches Classic dark bg
|
||||||
|
OnStartup: app.Startup,
|
||||||
|
Windows: &windows.Options{
|
||||||
|
WebviewIsTransparent: false,
|
||||||
|
WindowIsTranslucent: false,
|
||||||
|
DisableFramelessWindowDecorations: false,
|
||||||
|
},
|
||||||
|
Bind: []interface{}{
|
||||||
|
app,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user