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,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();
|
||||
}
|
||||
Reference in New Issue
Block a user