Add Store MSIX packaging and dark/light theme for 2.9.0.
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,6 +14,11 @@ Navis-pending.exe
|
|||||||
*.bak
|
*.bak
|
||||||
Navis.exe.bak
|
Navis.exe.bak
|
||||||
|
|
||||||
|
# MSIX staging / local packages
|
||||||
|
packaging/msix/_staging/
|
||||||
|
packaging/msix/Assets/
|
||||||
|
*.msix
|
||||||
|
|
||||||
# Android
|
# Android
|
||||||
android/.gradle/
|
android/.gradle/
|
||||||
android/local.properties
|
android/local.properties
|
||||||
|
|||||||
@@ -204,6 +204,19 @@ build.bat
|
|||||||
go build -ldflags="-H windowsgui -s -w" -o Navis.exe ./cmd/vpnapp
|
go build -ldflags="-H windowsgui -s -w" -o Navis.exe ./cmd/vpnapp
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Microsoft Store (MSIX)
|
||||||
|
|
||||||
|
Navis — Win32 + WebView2 VPN/прокси-клиент (системный прокси, локальные cores). **PWA в Store не подходит** как замена: без Desktop Bridge нельзя дать те же возможности. Для Store используется **MSIX full-trust** поверх того же `Navis.exe`.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
build.bat
|
||||||
|
.\scripts\pack-msix.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Нужен Windows SDK (`MakeAppx.exe`). Перед публикацией подставьте Identity Name / Publisher из Partner Center (см. `packaging/msix/AppxManifest.xml` и [packaging/msix/README.md](packaging/msix/README.md)).
|
||||||
|
|
||||||
|
Сборка из Store **не** самообновляется через in-app updater (детект MSIX package identity) — только обновления Microsoft Store.
|
||||||
|
|
||||||
## Сборка macOS (кросс с Windows / Linux)
|
## Сборка macOS (кросс с Windows / Linux)
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
@@ -257,6 +270,10 @@ https://evilfox.win/
|
|||||||
|
|
||||||
Некоторые AV (Bkav, Microsoft Wacapew/Wacatac, Ikarus Trojan.WinGo.Agent, Google, Trapmine) часто помечают **любые** неподписанные Go-бинарники с сетью и сменой системного прокси.
|
Некоторые AV (Bkav, Microsoft Wacapew/Wacatac, Ikarus Trojan.WinGo.Agent, Google, Trapmine) часто помечают **любые** неподписанные Go-бинарники с сетью и сменой системного прокси.
|
||||||
|
|
||||||
|
В 2.9.0:
|
||||||
|
- светлая / тёмная тема с переключателем (preference в localStorage, по умолчанию — системная);
|
||||||
|
- подготовка к Microsoft Store: MSIX-манифест, `scripts/pack-msix.ps1`, отключение in-app updater в MSIX-сборке.
|
||||||
|
|
||||||
В 2.8.0:
|
В 2.8.0:
|
||||||
- релизы разложены по папкам `windows/` · `macos/` · `android/`;
|
- релизы разложены по папкам `windows/` · `macos/` · `android/`;
|
||||||
- обновления строго по платформе (Windows не получит Mac-сборку и наоборот).
|
- обновления строго по платформе (Windows не получит Mac-сборку и наоборот).
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ android {
|
|||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
// versionCode = major*1_000_000 + minor*10_000 + patch*100 + build
|
// versionCode = major*1_000_000 + minor*10_000 + patch*100 + build
|
||||||
versionCode = 2_080_001
|
versionCode = 2_090_001
|
||||||
versionName = "2.8.0+1"
|
versionName = "2.9.0+1"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
<assemblyIdentity
|
<assemblyIdentity
|
||||||
version="1.7.1.0"
|
version="2.9.0.1"
|
||||||
processorArchitecture="*"
|
processorArchitecture="*"
|
||||||
name="EvilFox.Navis"
|
name="EvilFox.Navis"
|
||||||
type="win32"/>
|
type="win32"/>
|
||||||
|
|||||||
+3
-3
@@ -34,11 +34,11 @@ if errorlevel 1 exit /b 1
|
|||||||
go build -o "tools\packmac\packmac.exe" .\tools\packmac
|
go build -o "tools\packmac\packmac.exe" .\tools\packmac
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
tools\packmac\packmac.exe -bin "dist\navis-release\macos\arm64\Navis" -out "dist\navis-release\macos\arm64" -version 2.8.0 -build 2.8.0.1 -arch arm64
|
tools\packmac\packmac.exe -bin "dist\navis-release\macos\arm64\Navis" -out "dist\navis-release\macos\arm64" -version 2.9.0 -build 2.9.0.1 -arch arm64
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
tools\packmac\packmac.exe -bin "dist\navis-release\macos\amd64\Navis" -out "dist\navis-release\macos\amd64" -version 2.8.0 -build 2.8.0.1 -arch amd64
|
tools\packmac\packmac.exe -bin "dist\navis-release\macos\amd64\Navis" -out "dist\navis-release\macos\amd64" -version 2.9.0 -build 2.9.0.1 -arch amd64
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
tools\packmac\packmac.exe -bin "dist\navis-release\macos\universal\Navis" -out "dist\navis-release\macos\universal" -version 2.8.0 -build 2.8.0.1 -arch universal
|
tools\packmac\packmac.exe -bin "dist\navis-release\macos\universal\Navis" -out "dist\navis-release\macos\universal" -version 2.9.0 -build 2.9.0.1 -arch universal
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
echo Built Mac GUI + CLI:
|
echo Built Mac GUI + CLI:
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ type uiState struct {
|
|||||||
Pings []netcheck.Result `json:"pings"`
|
Pings []netcheck.Result `json:"pings"`
|
||||||
Subscription string `json:"subscription_url"`
|
Subscription string `json:"subscription_url"`
|
||||||
Hy2 core.Hy2Options `json:"hy2"`
|
Hy2 core.Hy2Options `json:"hy2"`
|
||||||
|
StorePackaged bool `json:"store_packaged,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -209,6 +210,7 @@ func (a *app) getState() (uiState, error) {
|
|||||||
Pings: append([]netcheck.Result(nil), a.pings...),
|
Pings: append([]netcheck.Result(nil), a.pings...),
|
||||||
Subscription: cfg.SubscriptionURL,
|
Subscription: cfg.SubscriptionURL,
|
||||||
Hy2: a.mgr.ActiveHy2Options(),
|
Hy2: a.mgr.ActiveHy2Options(),
|
||||||
|
StorePackaged: update.IsStorePackaged(),
|
||||||
}
|
}
|
||||||
if out.Protocol == "" {
|
if out.Protocol == "" {
|
||||||
if p, err := cfg.ActiveProfile(); err == nil {
|
if p, err := cfg.ActiveProfile(); err == nil {
|
||||||
@@ -428,6 +430,9 @@ func (a *app) checkUpdate() (update.Status, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *app) applyUpdate() (string, error) {
|
func (a *app) applyUpdate() (string, error) {
|
||||||
|
if update.IsStorePackaged() {
|
||||||
|
return "", fmt.Errorf("обновления устанавливаются через Microsoft Store")
|
||||||
|
}
|
||||||
// Explicit user action only — never called from autoCheckUpdate.
|
// Explicit user action only — never called from autoCheckUpdate.
|
||||||
st, err := a.checkUpdate()
|
st, err := a.checkUpdate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -455,6 +460,17 @@ func (a *app) autoCheckUpdate() {
|
|||||||
// Only refresh update status for the UI banner — never auto-install.
|
// Only refresh update status for the UI banner — never auto-install.
|
||||||
// (Previously auto-apply caused an infinite update loop when the feed version
|
// (Previously auto-apply caused an infinite update loop when the feed version
|
||||||
// stayed ahead of the shipped CurrentVersion in the downloaded binary.)
|
// stayed ahead of the shipped CurrentVersion in the downloaded binary.)
|
||||||
|
if update.IsStorePackaged() {
|
||||||
|
a.mu.Lock()
|
||||||
|
a.updateStatus = update.Status{
|
||||||
|
Current: update.DisplayVersion(),
|
||||||
|
ManifestURL: update.DefaultManifestURL,
|
||||||
|
StoreManaged: true,
|
||||||
|
Notes: "Обновления через Microsoft Store",
|
||||||
|
}
|
||||||
|
a.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
_, _ = a.checkUpdate()
|
_, _ = a.checkUpdate()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type UIState struct {
|
|||||||
Pings []netcheck.Result `json:"pings"`
|
Pings []netcheck.Result `json:"pings"`
|
||||||
Subscription string `json:"subscription_url"`
|
Subscription string `json:"subscription_url"`
|
||||||
Hy2 core.Hy2Options `json:"hy2"`
|
Hy2 core.Hy2Options `json:"hy2"`
|
||||||
|
StorePackaged bool `json:"store_packaged,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PingBestResult struct {
|
type PingBestResult struct {
|
||||||
@@ -130,6 +131,7 @@ func (a *App) GetState() (UIState, error) {
|
|||||||
Pings: append([]netcheck.Result(nil), a.Pings...),
|
Pings: append([]netcheck.Result(nil), a.Pings...),
|
||||||
Subscription: cfg.SubscriptionURL,
|
Subscription: cfg.SubscriptionURL,
|
||||||
Hy2: a.Mgr.ActiveHy2Options(),
|
Hy2: a.Mgr.ActiveHy2Options(),
|
||||||
|
StorePackaged: update.IsStorePackaged(),
|
||||||
}
|
}
|
||||||
if out.Protocol == "" {
|
if out.Protocol == "" {
|
||||||
if p, err := cfg.ActiveProfile(); err == nil {
|
if p, err := cfg.ActiveProfile(); err == nil {
|
||||||
@@ -332,6 +334,9 @@ func (a *App) CheckUpdate() (update.Status, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) ApplyUpdate() (string, error) {
|
func (a *App) ApplyUpdate() (string, error) {
|
||||||
|
if update.IsStorePackaged() {
|
||||||
|
return "", fmt.Errorf("обновления устанавливаются через Microsoft Store")
|
||||||
|
}
|
||||||
// Explicit user action only — never called from AutoCheckUpdate.
|
// Explicit user action only — never called from AutoCheckUpdate.
|
||||||
st, err := a.CheckUpdate()
|
st, err := a.CheckUpdate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -361,6 +366,17 @@ func (a *App) ApplyUpdate() (string, error) {
|
|||||||
func (a *App) AutoCheckUpdate() {
|
func (a *App) AutoCheckUpdate() {
|
||||||
// Only check and populate status for the banner — never auto-apply.
|
// Only check and populate status for the banner — never auto-apply.
|
||||||
// Auto-apply previously caused endless download→relaunch loops on Mac/Windows.
|
// Auto-apply previously caused endless download→relaunch loops on Mac/Windows.
|
||||||
|
if update.IsStorePackaged() {
|
||||||
|
a.mu.Lock()
|
||||||
|
a.UpdateStatus = update.Status{
|
||||||
|
Current: update.DisplayVersion(),
|
||||||
|
ManifestURL: update.DefaultManifestURL,
|
||||||
|
StoreManaged: true,
|
||||||
|
Notes: "Обновления через Microsoft Store",
|
||||||
|
}
|
||||||
|
a.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
_, _ = a.CheckUpdate()
|
_, _ = a.CheckUpdate()
|
||||||
}
|
}
|
||||||
|
|||||||
+216
-41
@@ -4,12 +4,28 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Navis</title>
|
<title>Navis</title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var k = "navis.theme";
|
||||||
|
var t = localStorage.getItem(k);
|
||||||
|
if (t !== "light" && t !== "dark") {
|
||||||
|
t = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute("data-theme", t);
|
||||||
|
} catch (_) {
|
||||||
|
document.documentElement.setAttribute("data-theme", "light");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@500;600;700;800&family=Figtree:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@500;600;700;800&family=Figtree:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
--ink: #0b1c18;
|
--ink: #0b1c18;
|
||||||
|
--text: var(--ink);
|
||||||
--muted: #5a736b;
|
--muted: #5a736b;
|
||||||
--line: rgba(11, 28, 24, 0.10);
|
--line: rgba(11, 28, 24, 0.10);
|
||||||
--accent: #0d8a66;
|
--accent: #0d8a66;
|
||||||
@@ -17,11 +33,96 @@
|
|||||||
--accent-soft: #d8f3e9;
|
--accent-soft: #d8f3e9;
|
||||||
--danger: #c0362c;
|
--danger: #c0362c;
|
||||||
--ok: #0d8a66;
|
--ok: #0d8a66;
|
||||||
|
--ms-good: #0a7a3e;
|
||||||
|
--ms-mid: #b8860b;
|
||||||
--surface: rgba(255, 255, 255, 0.82);
|
--surface: rgba(255, 255, 255, 0.82);
|
||||||
--surface-2: rgba(255, 255, 255, 0.62);
|
--surface-2: rgba(255, 255, 255, 0.62);
|
||||||
|
--input-bg: rgba(255, 255, 255, 0.94);
|
||||||
|
--row-bg: rgba(255, 255, 255, 0.6);
|
||||||
|
--hero-bg: linear-gradient(160deg, rgba(255,255,255,.92), rgba(232,247,240,.88));
|
||||||
|
--hero-bg-on: linear-gradient(160deg, rgba(216,243,233,.95), rgba(255,255,255,.9));
|
||||||
|
--chip-bg: rgba(255, 255, 255, 0.75);
|
||||||
|
--server-list-bg: rgba(255, 255, 255, 0.4);
|
||||||
|
--server-row-bg: rgba(255, 255, 255, 0.72);
|
||||||
|
--server-row-hover: #fff;
|
||||||
|
--server-row-active: linear-gradient(135deg, rgba(216,243,233,.95), #fff);
|
||||||
|
--meta-bg: rgba(255, 255, 255, 0.35);
|
||||||
|
--meta-ok-bg: rgba(216, 243, 233, 0.65);
|
||||||
|
--meta-err-bg: rgba(255, 236, 234, 0.75);
|
||||||
|
--shop-bg: linear-gradient(135deg, rgba(13,138,102,.09), rgba(255,255,255,.55));
|
||||||
|
--shop-border: rgba(13, 138, 102, 0.16);
|
||||||
|
--modal-bg: #f5fbf8;
|
||||||
|
--modal-scrim: rgba(8, 28, 24, 0.42);
|
||||||
|
--update-bg: linear-gradient(135deg, rgba(13,138,102,.14), rgba(255,255,255,.75));
|
||||||
|
--update-border: rgba(13, 138, 102, 0.28);
|
||||||
|
--icon-btn-bg: linear-gradient(180deg, #d2efe4, #bfe6d6);
|
||||||
|
--icon-btn-border: rgba(8, 90, 68, 0.22);
|
||||||
|
--icon-btn-hover: linear-gradient(180deg, #c5e9db, #aedfcb);
|
||||||
|
--mini-bg: linear-gradient(180deg, #d2efe4, #bfe6d6);
|
||||||
|
--mini-accent-bg: linear-gradient(180deg, #2f6f5f, #25584b);
|
||||||
|
--mini-accent-fg: #f2fff9;
|
||||||
|
--switch-off: #c7d5cf;
|
||||||
|
--switch-knob: #fff;
|
||||||
|
--dot-off: #9aaba4;
|
||||||
--shadow: 0 18px 50px rgba(8, 40, 32, 0.12);
|
--shadow: 0 18px 50px rgba(8, 40, 32, 0.12);
|
||||||
--radius: 22px;
|
--radius: 22px;
|
||||||
--ease: cubic-bezier(.22,.8,.24,1);
|
--ease: cubic-bezier(.22,.8,.24,1);
|
||||||
|
--page-bg:
|
||||||
|
radial-gradient(780px 420px at 8% -10%, #9fe0c8 0%, transparent 55%),
|
||||||
|
radial-gradient(640px 380px at 100% 0%, #b8d4e8 0%, transparent 48%),
|
||||||
|
radial-gradient(500px 320px at 50% 110%, #cfe8dc 0%, transparent 45%),
|
||||||
|
linear-gradient(165deg, #e7f6f0 0%, #f4faf7 48%, #eef4f8 100%);
|
||||||
|
--theme-toggle-bg: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--ink: #e4f2eb;
|
||||||
|
--text: var(--ink);
|
||||||
|
--muted: #8fa89c;
|
||||||
|
--line: rgba(180, 220, 200, 0.12);
|
||||||
|
--accent: #2ec995;
|
||||||
|
--accent-deep: #1aa877;
|
||||||
|
--accent-soft: rgba(46, 201, 149, 0.16);
|
||||||
|
--danger: #e07068;
|
||||||
|
--ok: #2ec995;
|
||||||
|
--ms-good: #3dd68c;
|
||||||
|
--ms-mid: #d4a017;
|
||||||
|
--surface: rgba(18, 32, 28, 0.88);
|
||||||
|
--surface-2: rgba(24, 40, 35, 0.72);
|
||||||
|
--input-bg: rgba(12, 24, 20, 0.92);
|
||||||
|
--row-bg: rgba(22, 36, 31, 0.75);
|
||||||
|
--hero-bg: linear-gradient(160deg, rgba(22,38,33,.95), rgba(16,28,24,.9));
|
||||||
|
--hero-bg-on: linear-gradient(160deg, rgba(18,48,40,.96), rgba(22,38,33,.92));
|
||||||
|
--chip-bg: rgba(28, 44, 38, 0.85);
|
||||||
|
--server-list-bg: rgba(10, 20, 17, 0.55);
|
||||||
|
--server-row-bg: rgba(22, 36, 31, 0.85);
|
||||||
|
--server-row-hover: rgba(30, 48, 41, 0.95);
|
||||||
|
--server-row-active: linear-gradient(135deg, rgba(18,52,42,.95), rgba(24,40,35,.95));
|
||||||
|
--meta-bg: rgba(14, 26, 22, 0.55);
|
||||||
|
--meta-ok-bg: rgba(18, 52, 42, 0.65);
|
||||||
|
--meta-err-bg: rgba(56, 28, 26, 0.7);
|
||||||
|
--shop-bg: linear-gradient(135deg, rgba(46,201,149,.1), rgba(18,32,28,.7));
|
||||||
|
--shop-border: rgba(46, 201, 149, 0.2);
|
||||||
|
--modal-bg: #15251f;
|
||||||
|
--modal-scrim: rgba(4, 12, 10, 0.62);
|
||||||
|
--update-bg: linear-gradient(135deg, rgba(46,201,149,.16), rgba(18,32,28,.85));
|
||||||
|
--update-border: rgba(46, 201, 149, 0.28);
|
||||||
|
--icon-btn-bg: linear-gradient(180deg, #1e3d34, #17332b);
|
||||||
|
--icon-btn-border: rgba(46, 201, 149, 0.28);
|
||||||
|
--icon-btn-hover: linear-gradient(180deg, #255045, #1c3c33);
|
||||||
|
--mini-bg: linear-gradient(180deg, #1e3d34, #17332b);
|
||||||
|
--mini-accent-bg: linear-gradient(180deg, #1aa877, #0d8a66);
|
||||||
|
--mini-accent-fg: #f2fff9;
|
||||||
|
--switch-off: #3a4f47;
|
||||||
|
--switch-knob: #e8f5ef;
|
||||||
|
--dot-off: #5a736b;
|
||||||
|
--shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
|
||||||
|
--page-bg:
|
||||||
|
radial-gradient(720px 400px at 6% -8%, rgba(26, 90, 70, 0.45) 0%, transparent 55%),
|
||||||
|
radial-gradient(560px 340px at 100% 0%, rgba(28, 56, 72, 0.4) 0%, transparent 48%),
|
||||||
|
radial-gradient(480px 300px at 50% 110%, rgba(18, 64, 48, 0.35) 0%, transparent 45%),
|
||||||
|
linear-gradient(165deg, #0c1613 0%, #121f1a 48%, #101a1c 100%);
|
||||||
|
--theme-toggle-bg: rgba(28, 44, 38, 0.85);
|
||||||
}
|
}
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
html, body {
|
html, body {
|
||||||
@@ -29,11 +130,7 @@
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
font-family: Figtree, sans-serif;
|
font-family: Figtree, sans-serif;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
background:
|
background: var(--page-bg);
|
||||||
radial-gradient(780px 420px at 8% -10%, #9fe0c8 0%, transparent 55%),
|
|
||||||
radial-gradient(640px 380px at 100% 0%, #b8d4e8 0%, transparent 48%),
|
|
||||||
radial-gradient(500px 320px at 50% 110%, #cfe8dc 0%, transparent 45%),
|
|
||||||
linear-gradient(165deg, #e7f6f0 0%, #f4faf7 48%, #eef4f8 100%);
|
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -60,8 +157,8 @@
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid rgba(13, 138, 102, 0.28);
|
border: 1px solid var(--update-border);
|
||||||
background: linear-gradient(135deg, rgba(13,138,102,.14), rgba(255,255,255,.75));
|
background: var(--update-bg);
|
||||||
animation: rise .4s var(--ease) both;
|
animation: rise .4s var(--ease) both;
|
||||||
}
|
}
|
||||||
.update-banner.show { display: block; }
|
.update-banner.show { display: block; }
|
||||||
@@ -136,14 +233,35 @@
|
|||||||
font-size: .84rem;
|
font-size: .84rem;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
.theme-toggle {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--theme-toggle-bg);
|
||||||
|
color: var(--ink);
|
||||||
|
cursor: pointer;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
transition: background .15s, border-color .15s, transform .12s;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.theme-toggle:hover {
|
||||||
|
border-color: rgba(13, 138, 102, 0.35);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.theme-toggle:active { transform: none; }
|
||||||
|
.theme-toggle svg { width: 20px; height: 20px; display: none; }
|
||||||
|
html[data-theme="light"] .theme-toggle .icon-moon { display: block; }
|
||||||
|
html[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background:
|
background: var(--hero-bg);
|
||||||
linear-gradient(160deg, rgba(255,255,255,.92), rgba(232,247,240,.88));
|
|
||||||
padding: 16px 16px 14px;
|
padding: 16px 16px 14px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
transition: border-color .25s, box-shadow .25s, background .25s;
|
transition: border-color .25s, box-shadow .25s, background .25s;
|
||||||
@@ -151,8 +269,7 @@
|
|||||||
.hero.on {
|
.hero.on {
|
||||||
border-color: rgba(13,138,102,.35);
|
border-color: rgba(13,138,102,.35);
|
||||||
box-shadow: 0 14px 36px rgba(13, 138, 102, 0.14);
|
box-shadow: 0 14px 36px rgba(13, 138, 102, 0.14);
|
||||||
background:
|
background: var(--hero-bg-on);
|
||||||
linear-gradient(160deg, rgba(216,243,233,.95), rgba(255,255,255,.9));
|
|
||||||
}
|
}
|
||||||
.hero::after {
|
.hero::after {
|
||||||
content: "";
|
content: "";
|
||||||
@@ -184,7 +301,7 @@
|
|||||||
}
|
}
|
||||||
.dot {
|
.dot {
|
||||||
width: 11px; height: 11px; border-radius: 50%;
|
width: 11px; height: 11px; border-radius: 50%;
|
||||||
background: #9aaba4;
|
background: var(--dot-off);
|
||||||
transition: background .2s;
|
transition: background .2s;
|
||||||
}
|
}
|
||||||
.dot.on {
|
.dot.on {
|
||||||
@@ -202,7 +319,7 @@
|
|||||||
letter-spacing: .03em;
|
letter-spacing: .03em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
background: rgba(255,255,255,.75);
|
background: var(--chip-bg);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
@@ -265,7 +382,7 @@
|
|||||||
select, input[type="text"], textarea {
|
select, input[type="text"], textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: rgba(255,255,255,.94);
|
background: var(--input-bg);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 11px 13px;
|
padding: 11px 13px;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
@@ -290,8 +407,8 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
border: 1px solid rgba(8, 90, 68, 0.22);
|
border: 1px solid var(--icon-btn-border);
|
||||||
background: linear-gradient(180deg, #d2efe4, #bfe6d6);
|
background: var(--icon-btn-bg);
|
||||||
color: var(--accent-deep);
|
color: var(--accent-deep);
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -300,9 +417,9 @@
|
|||||||
box-shadow: 0 4px 12px rgba(8, 90, 68, 0.08);
|
box-shadow: 0 4px 12px rgba(8, 90, 68, 0.08);
|
||||||
}
|
}
|
||||||
.icon-btn:hover {
|
.icon-btn:hover {
|
||||||
background: linear-gradient(180deg, #c5e9db, #aedfcb);
|
background: var(--icon-btn-hover);
|
||||||
border-color: rgba(8, 90, 68, 0.38);
|
border-color: rgba(8, 90, 68, 0.38);
|
||||||
color: #05553f;
|
color: var(--accent-deep);
|
||||||
box-shadow: 0 6px 14px rgba(8, 90, 68, 0.14);
|
box-shadow: 0 6px 14px rgba(8, 90, 68, 0.14);
|
||||||
}
|
}
|
||||||
.icon-btn.danger {
|
.icon-btn.danger {
|
||||||
@@ -374,7 +491,7 @@
|
|||||||
padding: 11px 13px;
|
padding: 11px 13px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: rgba(255,255,255,.6);
|
background: var(--row-bg);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
@@ -382,7 +499,7 @@
|
|||||||
.switch { position: relative; width: 44px; height: 26px; flex: 0 0 auto; }
|
.switch { position: relative; width: 44px; height: 26px; flex: 0 0 auto; }
|
||||||
.switch input { opacity: 0; width: 0; height: 0; }
|
.switch input { opacity: 0; width: 0; height: 0; }
|
||||||
.slider {
|
.slider {
|
||||||
position: absolute; inset: 0; background: #c7d5cf;
|
position: absolute; inset: 0; background: var(--switch-off);
|
||||||
border-radius: 999px; cursor: pointer; transition: background .15s;
|
border-radius: 999px; cursor: pointer; transition: background .15s;
|
||||||
}
|
}
|
||||||
.slider::before {
|
.slider::before {
|
||||||
@@ -390,7 +507,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 20px; height: 20px;
|
width: 20px; height: 20px;
|
||||||
left: 3px; top: 3px;
|
left: 3px; top: 3px;
|
||||||
background: #fff;
|
background: var(--switch-knob);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: transform .15s;
|
transition: transform .15s;
|
||||||
box-shadow: 0 2px 6px rgba(0,0,0,.12);
|
box-shadow: 0 2px 6px rgba(0,0,0,.12);
|
||||||
@@ -471,8 +588,8 @@
|
|||||||
}
|
}
|
||||||
.server-actions .mini {
|
.server-actions .mini {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: 1px solid rgba(8, 90, 68, 0.22);
|
border: 1px solid var(--icon-btn-border);
|
||||||
background: linear-gradient(180deg, #d2efe4, #bfe6d6);
|
background: var(--mini-bg);
|
||||||
color: var(--accent-deep);
|
color: var(--accent-deep);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
@@ -484,8 +601,8 @@
|
|||||||
}
|
}
|
||||||
.server-actions .mini:hover { filter: brightness(.97); }
|
.server-actions .mini:hover { filter: brightness(.97); }
|
||||||
.server-actions .mini.accent {
|
.server-actions .mini.accent {
|
||||||
background: linear-gradient(180deg, #2f6f5f, #25584b);
|
background: var(--mini-accent-bg);
|
||||||
color: #f2fff9;
|
color: var(--mini-accent-fg);
|
||||||
border-color: rgba(12, 48, 40, 0.35);
|
border-color: rgba(12, 48, 40, 0.35);
|
||||||
}
|
}
|
||||||
.auto-best {
|
.auto-best {
|
||||||
@@ -506,7 +623,7 @@
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: rgba(255,255,255,.4);
|
background: var(--server-list-bg);
|
||||||
}
|
}
|
||||||
.server-row {
|
.server-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -516,7 +633,7 @@
|
|||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
background: rgba(255,255,255,.72);
|
background: var(--server-row-bg);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background .12s, border-color .12s, box-shadow .12s;
|
transition: background .12s, border-color .12s, box-shadow .12s;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -526,11 +643,11 @@
|
|||||||
}
|
}
|
||||||
.server-row:hover {
|
.server-row:hover {
|
||||||
border-color: rgba(13,138,102,.25);
|
border-color: rgba(13,138,102,.25);
|
||||||
background: #fff;
|
background: var(--server-row-hover);
|
||||||
}
|
}
|
||||||
.server-row.active {
|
.server-row.active {
|
||||||
border-color: rgba(13,138,102,.4);
|
border-color: rgba(13,138,102,.4);
|
||||||
background: linear-gradient(135deg, rgba(216,243,233,.95), #fff);
|
background: var(--server-row-active);
|
||||||
box-shadow: 0 4px 14px rgba(13,138,102,.1);
|
box-shadow: 0 4px 14px rgba(13,138,102,.1);
|
||||||
}
|
}
|
||||||
.server-row .left { min-width: 0; }
|
.server-row .left { min-width: 0; }
|
||||||
@@ -573,8 +690,8 @@
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
.server-row .ms.ok { color: var(--ok); }
|
.server-row .ms.ok { color: var(--ok); }
|
||||||
.server-row .ms.good { color: #0a7a3e; }
|
.server-row .ms.good { color: var(--ms-good); }
|
||||||
.server-row .ms.mid { color: #b8860b; }
|
.server-row .ms.mid { color: var(--ms-mid); }
|
||||||
.server-row .ms.bad { color: var(--danger); }
|
.server-row .ms.bad { color: var(--danger); }
|
||||||
.server-empty {
|
.server-empty {
|
||||||
padding: 14px 10px;
|
padding: 14px 10px;
|
||||||
@@ -600,18 +717,18 @@
|
|||||||
min-height: 2.4em;
|
min-height: 2.4em;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: rgba(255,255,255,.35);
|
background: var(--meta-bg);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
transition: color .15s, border-color .15s, background .15s;
|
transition: color .15s, border-color .15s, background .15s;
|
||||||
}
|
}
|
||||||
.meta.ok {
|
.meta.ok {
|
||||||
color: var(--ok);
|
color: var(--ok);
|
||||||
background: rgba(216,243,233,.65);
|
background: var(--meta-ok-bg);
|
||||||
border-color: rgba(13,138,102,.18);
|
border-color: rgba(13,138,102,.18);
|
||||||
}
|
}
|
||||||
.meta.err {
|
.meta.err {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
background: rgba(255,236,234,.75);
|
background: var(--meta-err-bg);
|
||||||
border-color: rgba(192,54,44,.16);
|
border-color: rgba(192,54,44,.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,7 +744,7 @@
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: rgba(255,255,255,.6);
|
background: var(--row-bg);
|
||||||
font-size: .82rem;
|
font-size: .82rem;
|
||||||
}
|
}
|
||||||
.ping-item .ms { font-weight: 700; }
|
.ping-item .ms { font-weight: 700; }
|
||||||
@@ -638,8 +755,8 @@
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 13px 14px 12px;
|
padding: 13px 14px 12px;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
border: 1px solid rgba(13, 138, 102, 0.16);
|
border: 1px solid var(--shop-border);
|
||||||
background: linear-gradient(135deg, rgba(13,138,102,.09), rgba(255,255,255,.55));
|
background: var(--shop-bg);
|
||||||
}
|
}
|
||||||
.shop h3 {
|
.shop h3 {
|
||||||
font-family: Sora, sans-serif;
|
font-family: Sora, sans-serif;
|
||||||
@@ -680,7 +797,7 @@
|
|||||||
|
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
position: fixed; inset: 0;
|
position: fixed; inset: 0;
|
||||||
background: rgba(8, 28, 24, .42);
|
background: var(--modal-scrim);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
display: none;
|
display: none;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -690,7 +807,7 @@
|
|||||||
.modal-backdrop.open { display: grid; }
|
.modal-backdrop.open { display: grid; }
|
||||||
.modal {
|
.modal {
|
||||||
width: min(400px, 100%);
|
width: min(400px, 100%);
|
||||||
background: #f5fbf8;
|
background: var(--modal-bg);
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
@@ -737,10 +854,19 @@
|
|||||||
<div class="brand-wrap">
|
<div class="brand-wrap">
|
||||||
<div class="brand-row">
|
<div class="brand-row">
|
||||||
<h1 class="brand">Navis</h1>
|
<h1 class="brand">Navis</h1>
|
||||||
<span class="badge-ver">2.7.1</span>
|
<span class="badge-ver" id="badgeVer">2.9.0</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
|
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="theme-toggle" id="themeToggle" title="Тема" aria-label="Переключить тему">
|
||||||
|
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path d="M20.5 14.2A8.2 8.2 0 0 1 9.8 3.5 8.5 8.5 0 1 0 20.5 14.2z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="4.2" stroke="currentColor" stroke-width="1.8"/>
|
||||||
|
<path d="M12 2.8v2.2M12 19v2.2M2.8 12h2.2M19 12h2.2M5.1 5.1l1.6 1.6M17.3 17.3l1.6 1.6M17.3 6.7l1.6-1.6M5.1 18.9l1.6-1.6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="hero" id="hero">
|
<section class="hero" id="hero">
|
||||||
@@ -867,6 +993,7 @@
|
|||||||
<button class="action secondary" id="updCheckBtn" type="button">Проверить обновление</button>
|
<button class="action secondary" id="updCheckBtn" type="button">Проверить обновление</button>
|
||||||
<button class="action secondary" id="coreBtn" type="button">Установить cores</button>
|
<button class="action secondary" id="coreBtn" type="button">Установить cores</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="meta" id="storeUpdateNote" hidden style="margin-top:8px;min-height:0">Обновления этой сборки — через Microsoft Store.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<p class="meta" id="meta">Загрузка…</p>
|
<p class="meta" id="meta">Загрузка…</p>
|
||||||
@@ -952,7 +1079,41 @@
|
|||||||
const updateBtn = $("updateBtn");
|
const updateBtn = $("updateBtn");
|
||||||
const skipUpdateBtn = $("skipUpdateBtn");
|
const skipUpdateBtn = $("skipUpdateBtn");
|
||||||
const verLabel = $("verLabel");
|
const verLabel = $("verLabel");
|
||||||
|
const badgeVer = $("badgeVer");
|
||||||
|
const storeUpdateNote = $("storeUpdateNote");
|
||||||
|
const themeToggle = $("themeToggle");
|
||||||
|
const THEME_KEY = "navis.theme";
|
||||||
const SKIP_UPDATE_KEY = "navis.skipUpdateVersion";
|
const SKIP_UPDATE_KEY = "navis.skipUpdateVersion";
|
||||||
|
let storePackaged = false;
|
||||||
|
|
||||||
|
function readTheme() {
|
||||||
|
try {
|
||||||
|
const t = localStorage.getItem(THEME_KEY);
|
||||||
|
if (t === "light" || t === "dark") return t;
|
||||||
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
} catch (_) {
|
||||||
|
return "light";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function applyTheme(t) {
|
||||||
|
const theme = t === "dark" ? "dark" : "light";
|
||||||
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
|
try { localStorage.setItem(THEME_KEY, theme); } catch (_) {}
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.title = theme === "dark" ? "Светлая тема" : "Тёмная тема";
|
||||||
|
themeToggle.setAttribute("aria-label", themeToggle.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyTheme(document.documentElement.getAttribute("data-theme") || readTheme());
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener("click", () => {
|
||||||
|
const cur = document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
|
||||||
|
applyTheme(cur === "dark" ? "light" : "dark");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function skippedVersion() {
|
function skippedVersion() {
|
||||||
try { return localStorage.getItem(SKIP_UPDATE_KEY) || ""; } catch (_) { return ""; }
|
try { return localStorage.getItem(SKIP_UPDATE_KEY) || ""; } catch (_) { return ""; }
|
||||||
}
|
}
|
||||||
@@ -1191,7 +1352,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderUpdate(u, version) {
|
function renderUpdate(u, version) {
|
||||||
verLabel.textContent = "Navis v" + (version || "?");
|
const label = "Navis v" + (version || "?");
|
||||||
|
verLabel.textContent = label;
|
||||||
|
if (badgeVer && version) {
|
||||||
|
const short = String(version).split("+")[0];
|
||||||
|
badgeVer.textContent = short || version;
|
||||||
|
}
|
||||||
|
if (storePackaged || (u && u.store_managed)) {
|
||||||
|
updateBanner.classList.remove("show");
|
||||||
|
if (updCheckBtn) updCheckBtn.hidden = true;
|
||||||
|
if (storeUpdateNote) storeUpdateNote.hidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (updCheckBtn) updCheckBtn.hidden = false;
|
||||||
|
if (storeUpdateNote) storeUpdateNote.hidden = true;
|
||||||
if (!u) {
|
if (!u) {
|
||||||
updateBanner.classList.remove("show");
|
updateBanner.classList.remove("show");
|
||||||
return;
|
return;
|
||||||
@@ -1214,6 +1388,7 @@
|
|||||||
|
|
||||||
function paint(state, opts) {
|
function paint(state, opts) {
|
||||||
const syncForm = opts && opts.syncForm;
|
const syncForm = opts && opts.syncForm;
|
||||||
|
storePackaged = !!(state && state.store_packaged);
|
||||||
connected = !!state.connected;
|
connected = !!state.connected;
|
||||||
dot.classList.toggle("on", connected);
|
dot.classList.toggle("on", connected);
|
||||||
hero.classList.toggle("on", connected);
|
hero.classList.toggle("on", connected);
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package update
|
||||||
|
|
||||||
|
// IsStorePackaged is false outside Windows (no Microsoft Store / MSIX package identity).
|
||||||
|
func IsStorePackaged() bool { return false }
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
appModelErrorNoPackage = 15700 // APPMODEL_ERROR_NO_PACKAGE
|
||||||
|
errorInsufficientBuffer = 122 // ERROR_INSUFFICIENT_BUFFER
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
storeOnce sync.Once
|
||||||
|
storePackaged bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsStorePackaged reports whether this process has an MSIX / Store package identity.
|
||||||
|
// Store builds must not self-update (Store owns updates).
|
||||||
|
func IsStorePackaged() bool {
|
||||||
|
storeOnce.Do(func() {
|
||||||
|
storePackaged = detectStorePackage()
|
||||||
|
})
|
||||||
|
return storePackaged
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectStorePackage() bool {
|
||||||
|
mod := windows.NewLazySystemDLL("kernel32.dll")
|
||||||
|
proc := mod.NewProc("GetCurrentPackageFullName")
|
||||||
|
if err := proc.Find(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var n uint32
|
||||||
|
r, _, _ := proc.Call(uintptr(unsafe.Pointer(&n)), 0)
|
||||||
|
switch r {
|
||||||
|
case appModelErrorNoPackage:
|
||||||
|
return false
|
||||||
|
case errorInsufficientBuffer:
|
||||||
|
return true
|
||||||
|
case 0:
|
||||||
|
return n > 0
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
|
|
||||||
// CurrentVersion is the product/semver used for update eligibility (feed "version").
|
// CurrentVersion is the product/semver used for update eligibility (feed "version").
|
||||||
// Keep major.minor.patch only — no build suffix here.
|
// Keep major.minor.patch only — no build suffix here.
|
||||||
const CurrentVersion = "2.8.0"
|
const CurrentVersion = "2.9.0"
|
||||||
|
|
||||||
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
|
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
|
||||||
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
|
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
|
||||||
@@ -75,6 +75,7 @@ type Status struct {
|
|||||||
Checking bool `json:"checking,omitempty"`
|
Checking bool `json:"checking,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
ManifestURL string `json:"manifest_url"`
|
ManifestURL string `json:"manifest_url"`
|
||||||
|
StoreManaged bool `json:"store_managed,omitempty"` // MSIX / Microsoft Store — no in-app updates
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlatformKey is "GOOS-GOARCH", e.g. windows-amd64, darwin-arm64.
|
// PlatformKey is "GOOS-GOARCH", e.g. windows-amd64, darwin-arm64.
|
||||||
@@ -192,6 +193,12 @@ func Check(ctx context.Context, manifestURL string) (Status, error) {
|
|||||||
}
|
}
|
||||||
key := PlatformKey()
|
key := PlatformKey()
|
||||||
st := Status{Current: DisplayVersion(), ManifestURL: manifestURL, Platform: key}
|
st := Status{Current: DisplayVersion(), ManifestURL: manifestURL, Platform: key}
|
||||||
|
if IsStorePackaged() {
|
||||||
|
st.StoreManaged = true
|
||||||
|
st.Available = false
|
||||||
|
st.Notes = "Обновления через Microsoft Store"
|
||||||
|
return st, nil
|
||||||
|
}
|
||||||
|
|
||||||
m, err := fetchManifest(ctx, manifestURL)
|
m, err := fetchManifest(ctx, manifestURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ const finishUpdateFlag = "--navis-finish-update"
|
|||||||
|
|
||||||
// Apply downloads the new build and replaces the running Navis.exe after this process exits.
|
// Apply downloads the new build and replaces the running Navis.exe after this process exits.
|
||||||
func Apply(ctx context.Context, manifestURL string) (string, error) {
|
func Apply(ctx context.Context, manifestURL string) (string, error) {
|
||||||
|
if IsStorePackaged() {
|
||||||
|
return "", fmt.Errorf("обновления устанавливаются через Microsoft Store")
|
||||||
|
}
|
||||||
latest, _, _, exe, tmp, err := prepareDownload(ctx, manifestURL)
|
latest, _, _, exe, tmp, err := prepareDownload(ctx, manifestURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Navis MSIX package manifest (Microsoft Store / sideload).
|
||||||
|
|
||||||
|
WHY MSIX (not PWA):
|
||||||
|
Navis is a Win32 + WebView2 desktop VPN/proxy client (system proxy, local cores,
|
||||||
|
Naive/Hy2/AWG/Xray). A PWA cannot replace those capabilities. Package the existing
|
||||||
|
GUI binary with Desktop Bridge / full-trust MSIX for Store distribution.
|
||||||
|
|
||||||
|
IDENTITY PLACEHOLDERS — replace before Store submission:
|
||||||
|
Identity/@Name → Partner Center "Package/Identity/Name" (e.g. EvilFox.Navis)
|
||||||
|
Identity/@Publisher → exact Publisher CN from your Store certificate
|
||||||
|
(e.g. CN=EvilFox LLC or CN=A1B2C3D4-…)
|
||||||
|
Identity/@Version → Major.Minor.Build.Revision (all numeric; Revision often 0)
|
||||||
|
DisplayName / PublisherDisplayName → Store listing names
|
||||||
|
|
||||||
|
CAPABILITIES (VPN / proxy client):
|
||||||
|
internetClient — outbound HTTPS / VPN tunnels
|
||||||
|
internetClientServer — local listen (SOCKS/HTTP if needed)
|
||||||
|
privateNetworkClientServer — LAN / private routes
|
||||||
|
runFullTrust — classic Win32 + WebView2 (restricted capability;
|
||||||
|
requires Microsoft Store approval for full-trust apps)
|
||||||
|
|
||||||
|
Store note: in-app updater is disabled when running as an MSIX package
|
||||||
|
(GetCurrentPackageFullName). Updates ship only via the Store.
|
||||||
|
-->
|
||||||
|
<Package
|
||||||
|
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||||
|
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||||
|
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||||
|
IgnorableNamespaces="uap rescap">
|
||||||
|
|
||||||
|
<Identity
|
||||||
|
Name="EvilFox.Navis"
|
||||||
|
Publisher="CN=EvilFox"
|
||||||
|
Version="2.9.0.1"
|
||||||
|
ProcessorArchitecture="x64" />
|
||||||
|
|
||||||
|
<Properties>
|
||||||
|
<DisplayName>Navis</DisplayName>
|
||||||
|
<PublisherDisplayName>EvilFox</PublisherDisplayName>
|
||||||
|
<Description>Navis — VPN/proxy client (Naive, Hysteria 2, AmneziaWG, Xray)</Description>
|
||||||
|
<Logo>Assets\StoreLogo.png</Logo>
|
||||||
|
</Properties>
|
||||||
|
|
||||||
|
<Resources>
|
||||||
|
<Resource Language="en-us" />
|
||||||
|
<Resource Language="ru-ru" />
|
||||||
|
</Resources>
|
||||||
|
|
||||||
|
<Dependencies>
|
||||||
|
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
|
||||||
|
</Dependencies>
|
||||||
|
|
||||||
|
<Capabilities>
|
||||||
|
<Capability Name="internetClient" />
|
||||||
|
<Capability Name="internetClientServer" />
|
||||||
|
<Capability Name="privateNetworkClientServer" />
|
||||||
|
<rescap:Capability Name="runFullTrust" />
|
||||||
|
</Capabilities>
|
||||||
|
|
||||||
|
<Applications>
|
||||||
|
<Application
|
||||||
|
Id="Navis"
|
||||||
|
Executable="Navis.exe"
|
||||||
|
EntryPoint="Windows.FullTrustApplication">
|
||||||
|
<uap:VisualElements
|
||||||
|
DisplayName="Navis"
|
||||||
|
Description="Navis VPN client"
|
||||||
|
BackgroundColor="transparent"
|
||||||
|
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||||
|
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||||
|
<uap:DefaultTile
|
||||||
|
Wide310x150Logo="Assets\Wide310x150Logo.png"
|
||||||
|
Square71x71Logo="Assets\Square71x71Logo.png"
|
||||||
|
Square310x310Logo="Assets\Square310x310Logo.png"
|
||||||
|
ShortName="Navis" />
|
||||||
|
<uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="#0c1613" />
|
||||||
|
</uap:VisualElements>
|
||||||
|
</Application>
|
||||||
|
</Applications>
|
||||||
|
</Package>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# MSIX packaging for Navis (Microsoft Store)
|
||||||
|
|
||||||
|
Navis ships as a **full-trust Win32 + WebView2** app. Prefer **MSIX** over a PWA:
|
||||||
|
system proxy and protocol cores cannot be delivered as a Store PWA alone.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Build Windows binaries: `build.bat` → `dist\navis-release\windows\Navis.exe`
|
||||||
|
2. [Windows SDK](https://developer.microsoft.com/windows/downloads/windows-sdk/) with `MakeAppx.exe` (and `SignTool.exe` for sideload signing)
|
||||||
|
3. Optional: a code-signing certificate whose **Subject CN** matches `Identity/@Publisher` in `AppxManifest.xml`
|
||||||
|
|
||||||
|
## Pack
|
||||||
|
|
||||||
|
From the repo root (PowerShell):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\pack-msix.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\pack-msix.ps1 -ExePath "dist\navis-release\windows\Navis.exe" -Version "2.9.0.1"
|
||||||
|
.\scripts\pack-msix.ps1 -Publisher "CN=Your Store Publisher" -Name "EvilFox.Navis"
|
||||||
|
.\scripts\pack-msix.ps1 -CertPath ".\certs\navis-msix.pfx" -CertPassword "..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `dist\navis-release\windows\Navis_<version>.msix`
|
||||||
|
|
||||||
|
## Store submission
|
||||||
|
|
||||||
|
1. Create the app in [Partner Center](https://partner.microsoft.com/dashboard).
|
||||||
|
2. Copy **Package/Identity Name** and **Publisher** into `AppxManifest.xml` (or pass `-Name` / `-Publisher` to the script).
|
||||||
|
3. Upload the `.msix` (Partner Center can re-sign with the Store cert).
|
||||||
|
4. Declare **runFullTrust** and network capabilities; answer Store questions about VPN/proxy use.
|
||||||
|
5. Privacy policy URL required (document system-proxy changes and outbound tunnel traffic).
|
||||||
|
6. **Do not enable in-app self-update** for Store builds — Navis detects MSIX package identity and hides the updater (Store updates only).
|
||||||
|
|
||||||
|
## Sideload test
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Add-AppxPackage -Path .\dist\navis-release\windows\Navis_2.9.0.1.msix
|
||||||
|
```
|
||||||
|
|
||||||
|
Unsigned packages need a trusted cert or developer mode / sideloading enabled.
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Pack Navis.exe into an MSIX for Microsoft Store / sideload.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Builds Store assets, stages AppxManifest + Navis.exe, runs MakeAppx.
|
||||||
|
Prefers MSIX over PWA: Navis needs Win32/WebView2 VPN and system-proxy access.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\scripts\pack-msix.ps1
|
||||||
|
.\scripts\pack-msix.ps1 -Version 2.9.0.1 -Publisher "CN=EvilFox"
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$ExePath = "",
|
||||||
|
[string]$OutDir = "",
|
||||||
|
[string]$Version = "2.9.0.1",
|
||||||
|
[string]$Name = "EvilFox.Navis",
|
||||||
|
[string]$Publisher = "CN=EvilFox",
|
||||||
|
[string]$DisplayName = "Navis",
|
||||||
|
[string]$PublisherDisplayName = "EvilFox",
|
||||||
|
[string]$CertPath = "",
|
||||||
|
[string]$CertPassword = "",
|
||||||
|
[switch]$SkipAssets
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||||
|
Set-Location $Root
|
||||||
|
|
||||||
|
if (-not $ExePath) {
|
||||||
|
$ExePath = Join-Path $Root "dist\navis-release\windows\Navis.exe"
|
||||||
|
}
|
||||||
|
if (-not $OutDir) {
|
||||||
|
$OutDir = Join-Path $Root "dist\navis-release\windows"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $ExePath)) {
|
||||||
|
throw "Navis.exe not found at '$ExePath'. Run build.bat first."
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-SdkTool([string]$name) {
|
||||||
|
$candidates = @()
|
||||||
|
$kits = Join-Path ${env:ProgramFiles(x86)} "Windows Kits\10\bin"
|
||||||
|
if (Test-Path $kits) {
|
||||||
|
Get-ChildItem -Path $kits -Directory -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object Name -Descending |
|
||||||
|
ForEach-Object {
|
||||||
|
$candidates += (Join-Path $_.FullName "x64\$name")
|
||||||
|
$candidates += (Join-Path $_.FullName "x86\$name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($p in $candidates) {
|
||||||
|
if (Test-Path -LiteralPath $p) { return $p }
|
||||||
|
}
|
||||||
|
$cmd = Get-Command $name -ErrorAction SilentlyContinue
|
||||||
|
if ($cmd) { return $cmd.Source }
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$MakeAppx = Find-SdkTool "makeappx.exe"
|
||||||
|
|
||||||
|
if (-not $SkipAssets) {
|
||||||
|
Write-Host "Generating Store assets..."
|
||||||
|
go run ./tools/mkstoreassets
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
|
}
|
||||||
|
|
||||||
|
$Staging = Join-Path $Root "packaging\msix\_staging"
|
||||||
|
if (Test-Path $Staging) { Remove-Item -Recurse -Force $Staging }
|
||||||
|
New-Item -ItemType Directory -Path $Staging | Out-Null
|
||||||
|
New-Item -ItemType Directory -Path (Join-Path $Staging "Assets") | Out-Null
|
||||||
|
|
||||||
|
$AssetsSrc = Join-Path $Root "packaging\msix\Assets"
|
||||||
|
Copy-Item -Path (Join-Path $AssetsSrc "*") -Destination (Join-Path $Staging "Assets") -Force
|
||||||
|
Copy-Item -LiteralPath $ExePath -Destination (Join-Path $Staging "Navis.exe") -Force
|
||||||
|
|
||||||
|
$ManifestSrc = Join-Path $Root "packaging\msix\AppxManifest.xml"
|
||||||
|
$ManifestDst = Join-Path $Staging "AppxManifest.xml"
|
||||||
|
$manifestText = Get-Content -LiteralPath $ManifestSrc -Raw -Encoding UTF8
|
||||||
|
# Prefer exact placeholder swaps so we never touch <?xml version=...?>.
|
||||||
|
$manifestText = $manifestText.Replace('Name="EvilFox.Navis"', "Name=`"$Name`"")
|
||||||
|
$manifestText = $manifestText.Replace('Publisher="CN=EvilFox"', "Publisher=`"$Publisher`"")
|
||||||
|
$manifestText = $manifestText.Replace('Version="2.9.0.1"', "Version=`"$Version`"")
|
||||||
|
$manifestText = $manifestText.Replace('<DisplayName>Navis</DisplayName>', "<DisplayName>$DisplayName</DisplayName>")
|
||||||
|
$manifestText = $manifestText.Replace('<PublisherDisplayName>EvilFox</PublisherDisplayName>', "<PublisherDisplayName>$PublisherDisplayName</PublisherDisplayName>")
|
||||||
|
$manifestText = $manifestText.Replace('DisplayName="Navis"', "DisplayName=`"$DisplayName`"")
|
||||||
|
[System.IO.File]::WriteAllText($ManifestDst, $manifestText, (New-Object System.Text.UTF8Encoding $false))
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
||||||
|
$MsixPath = Join-Path $OutDir ("Navis_" + $Version + ".msix")
|
||||||
|
if (Test-Path $MsixPath) { Remove-Item -Force $MsixPath }
|
||||||
|
|
||||||
|
if (-not $MakeAppx) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "MakeAppx.exe not found — install the Windows 10/11 SDK to produce .msix." -ForegroundColor Yellow
|
||||||
|
Write-Host "Staging is ready: $Staging"
|
||||||
|
Write-Host "Then run:"
|
||||||
|
Write-Host " makeappx pack /d `"$Staging`" /p `"$MsixPath`" /o"
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Packing $MsixPath ..."
|
||||||
|
& $MakeAppx pack /d $Staging /p $MsixPath /o
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
|
|
||||||
|
if ($CertPath) {
|
||||||
|
$SignTool = Find-SdkTool "signtool.exe"
|
||||||
|
if (-not $SignTool) { throw "signtool.exe not found (needed for -CertPath)." }
|
||||||
|
Write-Host "Signing with $CertPath ..."
|
||||||
|
if ($CertPassword) {
|
||||||
|
& $SignTool sign /fd SHA256 /a /f $CertPath /p $CertPassword $MsixPath
|
||||||
|
} else {
|
||||||
|
& $SignTool sign /fd SHA256 /a /f $CertPath $MsixPath
|
||||||
|
}
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Done:"
|
||||||
|
Write-Host " $MsixPath"
|
||||||
|
Write-Host " Staging: $Staging"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Store: upload MSIX in Partner Center (identity must match reservation)."
|
||||||
|
Write-Host "Sideload: Add-AppxPackage -Path `"$MsixPath`""
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// Generates Store / MSIX PNG assets from assets/navis-icon.png.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
in := filepath.Join("assets", "navis-icon.png")
|
||||||
|
outDir := filepath.Join("packaging", "msix", "Assets")
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
in = os.Args[1]
|
||||||
|
}
|
||||||
|
if len(os.Args) > 2 {
|
||||||
|
outDir = os.Args[2]
|
||||||
|
}
|
||||||
|
f, err := os.Open(in)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
src, err := png.Decode(f)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(outDir, 0o755); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
square := map[string]int{
|
||||||
|
"StoreLogo.png": 50,
|
||||||
|
"Square44x44Logo.png": 44,
|
||||||
|
"Square71x71Logo.png": 71,
|
||||||
|
"Square150x150Logo.png": 150,
|
||||||
|
"Square310x310Logo.png": 310,
|
||||||
|
}
|
||||||
|
for name, size := range square {
|
||||||
|
if err := writePNG(filepath.Join(outDir, name), scaleContain(src, size, size, color.RGBA{12, 22, 19, 255})); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wide tile + splash: brand mark centered on deep forest panel.
|
||||||
|
if err := writePNG(filepath.Join(outDir, "Wide310x150Logo.png"), scaleContain(src, 310, 150, color.RGBA{12, 22, 19, 255})); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
if err := writePNG(filepath.Join(outDir, "SplashScreen.png"), scaleContain(src, 620, 300, color.RGBA{12, 22, 19, 255})); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println("Wrote Store assets to", outDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scaleContain(src image.Image, w, h int, bg color.Color) image.Image {
|
||||||
|
dst := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||||
|
draw.Draw(dst, dst.Bounds(), &image.Uniform{C: bg}, image.Point{}, draw.Src)
|
||||||
|
sb := src.Bounds()
|
||||||
|
sw, sh := sb.Dx(), sb.Dy()
|
||||||
|
scale := float64(w) / float64(sw)
|
||||||
|
if float64(h)/float64(sh) < scale {
|
||||||
|
scale = float64(h) / float64(sh)
|
||||||
|
}
|
||||||
|
// Keep icon smaller than full tile for padding.
|
||||||
|
scale *= 0.72
|
||||||
|
nw, nh := int(float64(sw)*scale), int(float64(sh)*scale)
|
||||||
|
if nw < 1 {
|
||||||
|
nw = 1
|
||||||
|
}
|
||||||
|
if nh < 1 {
|
||||||
|
nh = 1
|
||||||
|
}
|
||||||
|
x, y := (w-nw)/2, (h-nh)/2
|
||||||
|
resized := image.NewRGBA(image.Rect(0, 0, nw, nh))
|
||||||
|
draw.CatmullRom.Scale(resized, resized.Bounds(), src, sb, draw.Over, nil)
|
||||||
|
draw.Draw(dst, image.Rect(x, y, x+nw, y+nh), resized, image.Point{}, draw.Over)
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePNG(path string, img image.Image) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return png.Encode(f, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fatal(err error) {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"FixedFileInfo": {
|
"FixedFileInfo": {
|
||||||
"FileVersion": { "Major": 2, "Minor": 8, "Patch": 0, "Build": 1 },
|
"FileVersion": { "Major": 2, "Minor": 9, "Patch": 0, "Build": 1 },
|
||||||
"ProductVersion": { "Major": 2, "Minor": 8, "Patch": 0, "Build": 1 },
|
"ProductVersion": { "Major": 2, "Minor": 9, "Patch": 0, "Build": 1 },
|
||||||
"FileFlagsMask": "3f",
|
"FileFlagsMask": "3f",
|
||||||
"FileFlags": "00",
|
"FileFlags": "00",
|
||||||
"FileOS": "40004",
|
"FileOS": "40004",
|
||||||
@@ -11,12 +11,12 @@
|
|||||||
"StringFileInfo": {
|
"StringFileInfo": {
|
||||||
"CompanyName": "EvilFox",
|
"CompanyName": "EvilFox",
|
||||||
"FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
"FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
||||||
"FileVersion": "2.8.0.1",
|
"FileVersion": "2.9.0.1",
|
||||||
"InternalName": "Navis",
|
"InternalName": "Navis",
|
||||||
"LegalCopyright": "Copyright (c) EvilFox",
|
"LegalCopyright": "Copyright (c) EvilFox",
|
||||||
"OriginalFilename": "Navis.exe",
|
"OriginalFilename": "Navis.exe",
|
||||||
"ProductName": "Navis",
|
"ProductName": "Navis",
|
||||||
"ProductVersion": "2.8.0.1",
|
"ProductVersion": "2.9.0.1",
|
||||||
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
||||||
},
|
},
|
||||||
"VarFileInfo": {
|
"VarFileInfo": {
|
||||||
|
|||||||
Reference in New Issue
Block a user