GUI
@@ -13,5 +13,8 @@
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# local notes
|
||||
TODO.md
|
||||
|
||||
# protobuf generated
|
||||
internal/grpc/pb/*.pb.go
|
||||
internal/grpc/pb/*.pb.goa
|
||||
@@ -0,0 +1,19 @@
|
||||
# README
|
||||
|
||||
## About
|
||||
|
||||
This is the official Wails Vanilla template.
|
||||
|
||||
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
||||
here: https://wails.io/docs/reference/project-config
|
||||
|
||||
## Live Development
|
||||
|
||||
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
|
||||
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
|
||||
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
|
||||
to this in your browser, and you can call your Go code from devtools.
|
||||
|
||||
## Building
|
||||
|
||||
To build a redistributable, production mode package, use `wails build`.
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 176 KiB |
@@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>Спектр</title>
|
||||
<script type="module" crossorigin src="/assets/index.02c817da.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.41fde905.css">
|
||||
</head>
|
||||
<body data-theme="dark">
|
||||
|
||||
<!-- Переключатель темы -->
|
||||
<button id="theme-toggle" title="Сменить тему">☀</button>
|
||||
|
||||
<!-- Интро при запуске: закрывает всё окно приложения, потом "двери" разъезжаются -->
|
||||
<div id="intro-overlay">
|
||||
<div class="door door-left"></div>
|
||||
<div class="door door-right"></div>
|
||||
</div>
|
||||
|
||||
<div id="app">
|
||||
|
||||
<!-- Бренд-блок -->
|
||||
<div id="brand">
|
||||
<div class="logo-icon">
|
||||
<img class="logo-dark" src="/assets/logo-dark.ff9a8bc7.jpg" alt="ТЭЧ ДМ" />
|
||||
<img class="logo-light" src="/assets/logo-light.d5e577a8.png" alt="ТЭЧ ДМ" />
|
||||
</div>
|
||||
<h1>СПЕКТР</h1>
|
||||
<div class="tag">видеоприёмник · панель управления</div>
|
||||
</div>
|
||||
|
||||
<!-- Заголовки вкладок (пока скрыты — активен только режим приёмника) -->
|
||||
<div class="tabs" hidden>
|
||||
<button class="tab-button active" data-tab="receiver">Режим приемника</button>
|
||||
<button class="tab-button" data-tab="detector">Режим детектора</button>
|
||||
<button class="tab-button" data-tab="info">Информация</button>
|
||||
</div>
|
||||
|
||||
<!-- Содержимое вкладок -->
|
||||
<div class="tab-content active" id="tab-receiver">
|
||||
<div class="buttons-vertical">
|
||||
<button class="action-button" id="btn-check">Проверка подключения</button>
|
||||
<button class="action-button" id="btn-freq">Установка частоты</button>
|
||||
<button class="action-button close-button" id="btn-quit">Закрыть приложение</button>
|
||||
</div>
|
||||
|
||||
<!-- Панель частоты (новая, выше статуса) -->
|
||||
<div id="frequency-panel">
|
||||
<span id="frequency-label">Текущая частота:</span>
|
||||
<span id="frequency-value">---</span>
|
||||
</div>
|
||||
|
||||
<!-- Панель состояния соединения -->
|
||||
<div id="status-panel">
|
||||
<span id="status-indicator" class="indicator indicator-red"></span>
|
||||
<span id="status-text">Нет соединения</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-detector">
|
||||
<p>Здесь будет проводиться сканирование доступных частот!</p>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-info">
|
||||
<p>О программе: Версия 0.01, сделано на Wails в тэч дм!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для установки частоты -->
|
||||
<div id="freq-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<span class="close-modal" id="close-modal">×</span>
|
||||
<h3>Укажите желаемую частоту</h3>
|
||||
<div class="freq-input-group">
|
||||
<button id="btn-minus" class="freq-btn">-</button>
|
||||
<input type="text" id="freq-input" value="145" />
|
||||
<button id="btn-plus" class="freq-btn">+</button>
|
||||
</div>
|
||||
<button id="btn-set-freq" class="action-button">Установить</button>
|
||||
<p id="modal-error" class="error-msg hidden"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="./src/style.css" />
|
||||
<title>Спектр</title>
|
||||
</head>
|
||||
<body data-theme="dark">
|
||||
|
||||
<!-- Переключатель темы -->
|
||||
<button id="theme-toggle" title="Сменить тему">☀</button>
|
||||
|
||||
<!-- Интро при запуске: закрывает всё окно приложения, потом "двери" разъезжаются -->
|
||||
<div id="intro-overlay">
|
||||
<div class="door door-left"></div>
|
||||
<div class="door door-right"></div>
|
||||
</div>
|
||||
|
||||
<div id="app">
|
||||
|
||||
<!-- Бренд-блок -->
|
||||
<div id="brand">
|
||||
<div class="logo-icon">
|
||||
<img class="logo-dark" src="/src/assets/images/logo-dark.jpg" alt="ТЭЧ ДМ" />
|
||||
<img class="logo-light" src="/src/assets/images/logo-light.png" alt="ТЭЧ ДМ" />
|
||||
</div>
|
||||
<h1>СПЕКТР</h1>
|
||||
<div class="tag">видеоприёмник · панель управления</div>
|
||||
</div>
|
||||
|
||||
<!-- Заголовки вкладок (пока скрыты — активен только режим приёмника) -->
|
||||
<div class="tabs" hidden>
|
||||
<button class="tab-button active" data-tab="receiver">Режим приемника</button>
|
||||
<button class="tab-button" data-tab="detector">Режим детектора</button>
|
||||
<button class="tab-button" data-tab="info">Информация</button>
|
||||
</div>
|
||||
|
||||
<!-- Содержимое вкладок -->
|
||||
<div class="tab-content active" id="tab-receiver">
|
||||
<div class="buttons-vertical">
|
||||
<button class="action-button" id="btn-check">Проверка подключения</button>
|
||||
<button class="action-button" id="btn-freq">Установка частоты</button>
|
||||
<button class="action-button close-button" id="btn-quit">Закрыть приложение</button>
|
||||
</div>
|
||||
|
||||
<!-- Панель частоты (новая, выше статуса) -->
|
||||
<div id="frequency-panel">
|
||||
<span id="frequency-label">Текущая частота:</span>
|
||||
<span id="frequency-value">---</span>
|
||||
</div>
|
||||
|
||||
<!-- Панель состояния соединения -->
|
||||
<div id="status-panel">
|
||||
<span id="status-indicator" class="indicator indicator-red"></span>
|
||||
<span id="status-text">Нет соединения</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-detector">
|
||||
<p>Здесь будет проводиться сканирование доступных частот!</p>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-info">
|
||||
<p>О программе: Версия 0.01, сделано на Wails в тэч дм!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для установки частоты -->
|
||||
<div id="freq-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<span class="close-modal" id="close-modal">×</span>
|
||||
<h3>Укажите желаемую частоту</h3>
|
||||
<div class="freq-input-group">
|
||||
<button id="btn-minus" class="freq-btn">-</button>
|
||||
<input type="text" id="freq-input" value="145" />
|
||||
<button id="btn-plus" class="freq-btn">+</button>
|
||||
</div>
|
||||
<button id="btn-set-freq" class="action-button">Установить</button>
|
||||
<p id="modal-error" class="error-msg hidden"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,664 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
|
||||
"integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz",
|
||||
"integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz",
|
||||
"integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.15.18",
|
||||
"@esbuild/linux-loong64": "0.15.18",
|
||||
"esbuild-android-64": "0.15.18",
|
||||
"esbuild-android-arm64": "0.15.18",
|
||||
"esbuild-darwin-64": "0.15.18",
|
||||
"esbuild-darwin-arm64": "0.15.18",
|
||||
"esbuild-freebsd-64": "0.15.18",
|
||||
"esbuild-freebsd-arm64": "0.15.18",
|
||||
"esbuild-linux-32": "0.15.18",
|
||||
"esbuild-linux-64": "0.15.18",
|
||||
"esbuild-linux-arm": "0.15.18",
|
||||
"esbuild-linux-arm64": "0.15.18",
|
||||
"esbuild-linux-mips64le": "0.15.18",
|
||||
"esbuild-linux-ppc64le": "0.15.18",
|
||||
"esbuild-linux-riscv64": "0.15.18",
|
||||
"esbuild-linux-s390x": "0.15.18",
|
||||
"esbuild-netbsd-64": "0.15.18",
|
||||
"esbuild-openbsd-64": "0.15.18",
|
||||
"esbuild-sunos-64": "0.15.18",
|
||||
"esbuild-windows-32": "0.15.18",
|
||||
"esbuild-windows-64": "0.15.18",
|
||||
"esbuild-windows-arm64": "0.15.18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz",
|
||||
"integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz",
|
||||
"integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz",
|
||||
"integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-32": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz",
|
||||
"integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
|
||||
"integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz",
|
||||
"integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-mips64le": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz",
|
||||
"integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-ppc64le": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz",
|
||||
"integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-riscv64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz",
|
||||
"integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-s390x": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz",
|
||||
"integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-netbsd-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz",
|
||||
"integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-openbsd-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz",
|
||||
"integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-sunos-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz",
|
||||
"integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-32": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz",
|
||||
"integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
|
||||
"integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.2",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
|
||||
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.12",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"is-core-module": "^2.16.1",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "2.80.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz",
|
||||
"integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "3.2.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz",
|
||||
"integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.15.9",
|
||||
"postcss": "^8.4.18",
|
||||
"resolve": "^1.22.1",
|
||||
"rollup": "^2.79.1"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 14",
|
||||
"less": "*",
|
||||
"sass": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "node ./node_modules/vite/bin/vite.js",
|
||||
"build": "node ./node_modules/vite/bin/vite.js build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#logo {
|
||||
display: block;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
margin: auto;
|
||||
padding: 10% 0 0;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-origin: content-box;
|
||||
}
|
||||
|
||||
.result {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 1.5rem auto;
|
||||
}
|
||||
|
||||
.input-box .btn {
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
margin: 0 0 0 20px;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-box .btn:hover {
|
||||
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.input-box .input {
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 10px;
|
||||
background-color: rgba(240, 240, 240, 1);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.input-box .input:hover {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.input-box .input:focus {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 136 KiB |
@@ -0,0 +1,189 @@
|
||||
import {
|
||||
CheckConnection,
|
||||
SetFrequency,
|
||||
Quit,
|
||||
} from "../wailsjs/go/main/App";
|
||||
|
||||
// импорт через ES-модуль, а не строкой-путём — иначе vite не подставит
|
||||
// правильный хэшированный путь в собранном билде (сломается в проде)
|
||||
import logoDarkUrl from "./assets/images/logo-dark.jpg";
|
||||
|
||||
(function() {
|
||||
// === Тема (тёмная/светлая) ===
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
themeToggle.addEventListener('click', () => {
|
||||
const cur = document.body.getAttribute('data-theme');
|
||||
const next = cur === 'light' ? 'dark' : 'light';
|
||||
document.body.setAttribute('data-theme', next);
|
||||
themeToggle.textContent = next === 'light' ? '☾' : '☀';
|
||||
});
|
||||
|
||||
// === Интро при запуске (лого "двери" разъезжаются) ===
|
||||
const NATURAL_W = 640, NATURAL_H = 480;
|
||||
const AXIS_X = 312; // ось раздела — середина носа черепа / верхнего зубца шестерни
|
||||
const LOGO_DARK_URL = logoDarkUrl;
|
||||
|
||||
const introOverlay = document.getElementById('intro-overlay');
|
||||
const doorLeft = document.querySelector('#intro-overlay .door-left');
|
||||
const doorRight = document.querySelector('#intro-overlay .door-right');
|
||||
|
||||
function layoutDoors() {
|
||||
const W = window.innerWidth;
|
||||
const H = window.innerHeight;
|
||||
const scale = Math.max(W / NATURAL_W, H / NATURAL_H); // object-fit: cover на весь размер окна
|
||||
const renderW = NATURAL_W * scale;
|
||||
const renderH = NATURAL_H * scale;
|
||||
const offsetX = (W - renderW) / 2;
|
||||
const offsetY = (H - renderH) / 2;
|
||||
const axisDisplayX = offsetX + AXIS_X * scale;
|
||||
|
||||
const leftW = Math.max(0, Math.min(W, axisDisplayX));
|
||||
const rightW = W - leftW;
|
||||
|
||||
doorLeft.style.width = leftW + 'px';
|
||||
doorLeft.style.backgroundImage = `url(${LOGO_DARK_URL})`;
|
||||
doorLeft.style.backgroundSize = renderW + 'px ' + renderH + 'px';
|
||||
doorLeft.style.backgroundPosition = offsetX + 'px ' + offsetY + 'px';
|
||||
|
||||
doorRight.style.left = leftW + 'px';
|
||||
doorRight.style.width = rightW + 'px';
|
||||
doorRight.style.backgroundImage = `url(${LOGO_DARK_URL})`;
|
||||
doorRight.style.backgroundSize = renderW + 'px ' + renderH + 'px';
|
||||
doorRight.style.backgroundPosition = (offsetX - leftW) + 'px ' + offsetY + 'px';
|
||||
}
|
||||
|
||||
const OPEN_AT = 2100; // момент начала разъезда дверей от старта интро, мс
|
||||
const APPEAR_START = 100;
|
||||
const APPEAR_DUR = 450;
|
||||
|
||||
function playIntro() {
|
||||
introOverlay.style.display = '';
|
||||
introOverlay.classList.remove('appear', 'open');
|
||||
void introOverlay.offsetWidth; // reflow reset
|
||||
layoutDoors();
|
||||
|
||||
setTimeout(() => introOverlay.classList.add('appear'), APPEAR_START);
|
||||
setTimeout(() => introOverlay.classList.add('open'), OPEN_AT);
|
||||
// прячем оверлей из потока/кликов после того как двери разъехались и погасли
|
||||
setTimeout(() => { introOverlay.style.display = 'none'; }, OPEN_AT + 1170);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', layoutDoors);
|
||||
playIntro(); // main.js подключён как type="module" — DOM уже готов на этот момент
|
||||
|
||||
// === Переключение вкладок ===
|
||||
const tabButtons = document.querySelectorAll('.tab-button');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||
tabContents.forEach(content => content.classList.remove('active'));
|
||||
button.classList.add('active');
|
||||
const tabId = button.getAttribute('data-tab');
|
||||
document.getElementById(`tab-${tabId}`).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// === Элементы интерфейса ===
|
||||
const btnCheck = document.getElementById('btn-check');
|
||||
const btnFreq = document.getElementById('btn-freq');
|
||||
const btnQuit = document.getElementById('btn-quit');
|
||||
const statusIndicator = document.getElementById('status-indicator');
|
||||
const statusText = document.getElementById('status-text');
|
||||
const freqLabel = document.getElementById('frequency-label');
|
||||
const freqValue = document.getElementById('frequency-value');
|
||||
|
||||
// Модальное окно
|
||||
const modal = document.getElementById('freq-modal');
|
||||
const closeModal = document.getElementById('close-modal');
|
||||
const freqInput = document.getElementById('freq-input');
|
||||
const btnMinus = document.getElementById('btn-minus');
|
||||
const btnPlus = document.getElementById('btn-plus');
|
||||
const btnSetFreq = document.getElementById('btn-set-freq');
|
||||
const modalError = document.getElementById('modal-error');
|
||||
|
||||
// === Проверка подключения ===
|
||||
btnCheck.addEventListener('click', () => {
|
||||
CheckConnection()
|
||||
.then((response) => {
|
||||
// Обновление индикатора соединения
|
||||
statusIndicator.classList.remove('indicator-red');
|
||||
statusIndicator.classList.add('indicator-green');
|
||||
statusText.textContent = `Соединение установлено! ${response.status}`;
|
||||
|
||||
// Обновление панели частоты (текущая)
|
||||
freqLabel.textContent = 'Текущая частота:';
|
||||
freqValue.textContent = response.frequency;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Ошибка CheckConnection:', err);
|
||||
statusText.textContent = 'Ошибка: не удалось выполнить проверку';
|
||||
});
|
||||
});
|
||||
|
||||
// === Установка частоты (открытие модального окна) ===
|
||||
btnFreq.addEventListener('click', () => {
|
||||
modal.classList.remove('hidden');
|
||||
// Показываем текущую частоту из панели (или 145)
|
||||
const current = freqValue.textContent !== '---' ? freqValue.textContent : '145';
|
||||
freqInput.value = current;
|
||||
modalError.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Закрытие модального окна
|
||||
closeModal.addEventListener('click', () => {
|
||||
modal.classList.add('hidden');
|
||||
});
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Кнопки +/- для изменения значения на 1
|
||||
btnMinus.addEventListener('click', () => {
|
||||
let val = parseInt(freqInput.value, 10) || 1;
|
||||
val = Math.max(1, val - 1); // минимум 1
|
||||
freqInput.value = val.toString();
|
||||
});
|
||||
btnPlus.addEventListener('click', () => {
|
||||
let val = parseInt(freqInput.value, 10) || 1;
|
||||
val += 1;
|
||||
freqInput.value = val.toString();
|
||||
});
|
||||
|
||||
// Кнопка "Установить"
|
||||
btnSetFreq.addEventListener('click', () => {
|
||||
const inputVal = freqInput.value.trim();
|
||||
// Проверка: только цифры
|
||||
if (!/^\d+$/.test(inputVal)) {
|
||||
modalError.textContent = 'Вводимое значение должно быть целым положительным числом';
|
||||
modalError.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
const num = parseInt(inputVal, 10);
|
||||
if (num <= 0) {
|
||||
modalError.textContent = 'Вводимое значение должно быть целым положительным числом';
|
||||
modalError.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
SetFrequency(inputVal)
|
||||
.then((newFreq) => {
|
||||
// Обновляем панель частоты
|
||||
freqLabel.textContent = 'Установленная частота:';
|
||||
freqValue.textContent = newFreq;
|
||||
modal.classList.add('hidden');
|
||||
})
|
||||
.catch((err) => {
|
||||
modalError.textContent = err;
|
||||
modalError.classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
// === Закрыть приложение ===
|
||||
btnQuit.addEventListener('click', () => {
|
||||
Quit();
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,343 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===================== ТЕМА (тёмная/светлая) ===================== */
|
||||
body {
|
||||
background: radial-gradient(circle at 50% 18%, #1a1d24 0%, #0d0f13 70%);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
body[data-theme="light"] {
|
||||
background: radial-gradient(circle at 50% 18%, #ffffff 0%, #dde1e7 70%);
|
||||
}
|
||||
|
||||
#theme-toggle {
|
||||
position: fixed;
|
||||
top: 16px; right: 18px;
|
||||
z-index: 500;
|
||||
width: 34px; height: 34px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #333a46;
|
||||
background: #222a37;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
body[data-theme="light"] #theme-toggle {
|
||||
border-color: #c7cbd1;
|
||||
background: #ffffff;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* ===================== КАРТОЧКА ПРИЛОЖЕНИЯ ===================== */
|
||||
#app {
|
||||
width: 360px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
background: #1b1e24;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
body[data-theme="light"] #app {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* ---- бренд-блок ---- */
|
||||
#brand {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: 26px 24px 16px;
|
||||
gap: 6px;
|
||||
}
|
||||
.logo-icon {
|
||||
position: relative;
|
||||
width: 64px; height: 48px; /* пропорции 640:480 исходного лого */
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.logo-icon img {
|
||||
position: absolute; inset: 0;
|
||||
width: 100%; height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.logo-icon .logo-dark { mix-blend-mode: screen; display: block; }
|
||||
.logo-icon .logo-light { mix-blend-mode: multiply; display: none; }
|
||||
body[data-theme="light"] .logo-icon .logo-dark { display: none; }
|
||||
body[data-theme="light"] .logo-icon .logo-light { display: block; }
|
||||
|
||||
#brand h1 {
|
||||
color: #eeeeee;
|
||||
font-size: 21px;
|
||||
letter-spacing: 3.5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
#brand .tag {
|
||||
color: #7a7f89;
|
||||
font-size: 11px;
|
||||
letter-spacing: 1.2px;
|
||||
}
|
||||
body[data-theme="light"] #brand h1 { color: #1b1d22; }
|
||||
body[data-theme="light"] #brand .tag { color: #888d97; }
|
||||
|
||||
/* Панель вкладок (пока скрыта атрибутом hidden в HTML) */
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: transparent;
|
||||
}
|
||||
.tabs[hidden] {
|
||||
display: none; /* без этого .tabs{display:flex} выше перебивает атрибут hidden */
|
||||
}
|
||||
.tab-button {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #9aa0aa;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.tab-button:hover { background: rgba(255,255,255,0.05); }
|
||||
.tab-button.active {
|
||||
color: #eee;
|
||||
border-bottom: 2px solid #b8362a;
|
||||
font-weight: bold;
|
||||
}
|
||||
body[data-theme="light"] .tab-button { color: #7a7f89; }
|
||||
body[data-theme="light"] .tab-button.active { color: #1b1d22; }
|
||||
|
||||
/* Содержимое вкладок */
|
||||
.tab-content { display: none; padding: 4px 22px 24px; }
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
/* Вертикально расположенные кнопки */
|
||||
.buttons-vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 11px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #2a2d34;
|
||||
border-radius: 8px;
|
||||
background-color: #15171c;
|
||||
color: #e8e8e8;
|
||||
transition: background-color 0.2s, border-color 0.2s;
|
||||
}
|
||||
.action-button:hover {
|
||||
background-color: #1d2027;
|
||||
border-color: #3a3e46;
|
||||
}
|
||||
body[data-theme="light"] .action-button {
|
||||
background-color: #ffffff;
|
||||
border-color: #ccd1d9;
|
||||
color: #22242a;
|
||||
}
|
||||
body[data-theme="light"] .action-button:hover {
|
||||
background-color: #f0f2f5;
|
||||
border-color: #aeb4bd;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background-color: #b8362a;
|
||||
color: white;
|
||||
border-color: #8f2a20;
|
||||
}
|
||||
.close-button:hover { background-color: #c73f31; }
|
||||
body[data-theme="light"] .close-button {
|
||||
background-color: #e74c3c;
|
||||
border-color: #c0392b;
|
||||
color: #fff;
|
||||
}
|
||||
body[data-theme="light"] .close-button:hover { background-color: #d8432f; }
|
||||
|
||||
/* Панель частоты */
|
||||
#frequency-panel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 9px 13px;
|
||||
background-color: #15171c;
|
||||
border: 1px solid #2a2d34;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
#frequency-label { font-weight: bold; color: #9aa0aa; font-size: 13px; }
|
||||
#frequency-value { font-size: 17px; color: #f2f2f2; }
|
||||
body[data-theme="light"] #frequency-panel { background-color: #ffffff; border-color: #ccd1d9; }
|
||||
body[data-theme="light"] #frequency-label { color: #7a7f89; }
|
||||
body[data-theme="light"] #frequency-value { color: #1b1d22; }
|
||||
|
||||
/* Панель статуса */
|
||||
#status-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 9px 13px;
|
||||
background-color: #15171c;
|
||||
border: 1px solid #2a2d34;
|
||||
border-radius: 8px;
|
||||
}
|
||||
body[data-theme="light"] #status-panel { background-color: #ffffff; border-color: #ccd1d9; }
|
||||
|
||||
.indicator {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
.indicator-red { background-color: #e74c3c; }
|
||||
.indicator-green { background-color: #2ecc71; }
|
||||
|
||||
#status-text { font-size: 14px; color: #ddd; }
|
||||
body[data-theme="light"] #status-text { color: #333; }
|
||||
|
||||
/* ===================== ИНТРО (двери) ===================== */
|
||||
#intro-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: #000; /* подстраховка: перекрывает всё, даже пока JS ещё не выставил размеры дверей */
|
||||
transform: scale(1.04);
|
||||
}
|
||||
#intro-overlay.appear {
|
||||
animation: introPop 0.45s cubic-bezier(.2,.9,.3,1) forwards;
|
||||
}
|
||||
@keyframes introPop { to { transform: scale(1); } }
|
||||
|
||||
.door {
|
||||
position: absolute;
|
||||
top: 0; bottom: 0;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
background-repeat: no-repeat;
|
||||
transition: transform 1.17s cubic-bezier(.65,0,.35,1);
|
||||
}
|
||||
.door-left { left: 0; }
|
||||
.door-right { left: 0; } /* left/width выставляются из JS под реальный размер окна */
|
||||
|
||||
#intro-overlay.open .door-left { transform: translateX(-100%); }
|
||||
#intro-overlay.open .door-right { transform: translateX(100%); }
|
||||
#intro-overlay.open {
|
||||
transition: opacity 0.45s ease 0.65s;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Модальное окно */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #1b1e24;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 300px;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.4);
|
||||
position: relative;
|
||||
color: #eee;
|
||||
}
|
||||
body[data-theme="light"] .modal-content {
|
||||
background: #ffffff;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
}
|
||||
.close-modal:hover { color: #eee; }
|
||||
body[data-theme="light"] .close-modal { color: #777; }
|
||||
body[data-theme="light"] .close-modal:hover { color: #333; }
|
||||
|
||||
.modal-content h3 {
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.freq-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.freq-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
background: #b8362a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.freq-btn:hover { background: #c73f31; }
|
||||
|
||||
#freq-input {
|
||||
width: 100px;
|
||||
padding: 8px;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
background: #15171c;
|
||||
color: #eee;
|
||||
}
|
||||
body[data-theme="light"] #freq-input {
|
||||
border-color: #ccc;
|
||||
background: #fff;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
#btn-set-freq {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
color: #e74c3c;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error-msg.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {main} from '../models';
|
||||
|
||||
export function CheckConnection():Promise<main.DeviceResponse>;
|
||||
|
||||
export function Quit():Promise<void>;
|
||||
|
||||
export function SetFrequency(arg1:string):Promise<string>;
|
||||
@@ -0,0 +1,15 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function CheckConnection() {
|
||||
return window['go']['main']['App']['CheckConnection']();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
return window['go']['main']['App']['Quit']();
|
||||
}
|
||||
|
||||
export function SetFrequency(arg1) {
|
||||
return window['go']['main']['App']['SetFrequency'](arg1);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export namespace main {
|
||||
|
||||
export class DeviceResponse {
|
||||
status: string;
|
||||
frequency: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new DeviceResponse(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.status = source["status"];
|
||||
this.frequency = source["frequency"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
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#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all 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;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [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;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [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;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [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;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
|
||||
// Notification types
|
||||
export interface NotificationOptions {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string; // macOS and Linux only
|
||||
body?: string;
|
||||
categoryId?: string;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
id?: string;
|
||||
title?: string;
|
||||
destructive?: boolean; // macOS-specific
|
||||
}
|
||||
|
||||
export interface NotificationCategory {
|
||||
id?: string;
|
||||
actions?: NotificationAction[];
|
||||
hasReplyField?: boolean;
|
||||
replyPlaceholder?: string;
|
||||
replyButtonTitle?: string;
|
||||
}
|
||||
|
||||
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||
// Initializes the notification service for the application.
|
||||
// This must be called before sending any notifications.
|
||||
export function InitializeNotifications(): Promise<void>;
|
||||
|
||||
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||
// Cleans up notification resources and releases any held connections.
|
||||
export function CleanupNotifications(): Promise<void>;
|
||||
|
||||
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||
// Checks if notifications are available on the current platform.
|
||||
export function IsNotificationAvailable(): Promise<boolean>;
|
||||
|
||||
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||
// Requests notification authorization from the user (macOS only).
|
||||
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||
// Checks the current notification authorization status (macOS only).
|
||||
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||
// Sends a basic notification with the given options.
|
||||
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||
// Sends a notification with action buttons. Requires a registered category.
|
||||
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||
|
||||
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||
// Removes a previously registered notification category.
|
||||
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||
|
||||
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||
// Removes all pending notifications from the notification center.
|
||||
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||
|
||||
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||
// Removes a specific pending notification by its identifier.
|
||||
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||
// Removes all delivered notifications from the notification center.
|
||||
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||
|
||||
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||
// Removes a specific delivered notification by its identifier.
|
||||
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||
// Removes a notification by its identifier (cross-platform convenience function).
|
||||
export function RemoveNotification(identifier: string): Promise<void>;
|
||||
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
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) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return 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 WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
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 WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
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 WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
|
||||
export function InitializeNotifications() {
|
||||
return window.runtime.InitializeNotifications();
|
||||
}
|
||||
|
||||
export function CleanupNotifications() {
|
||||
return window.runtime.CleanupNotifications();
|
||||
}
|
||||
|
||||
export function IsNotificationAvailable() {
|
||||
return window.runtime.IsNotificationAvailable();
|
||||
}
|
||||
|
||||
export function RequestNotificationAuthorization() {
|
||||
return window.runtime.RequestNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function CheckNotificationAuthorization() {
|
||||
return window.runtime.CheckNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function SendNotification(options) {
|
||||
return window.runtime.SendNotification(options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options) {
|
||||
return window.runtime.SendNotificationWithActions(options);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category) {
|
||||
return window.runtime.RegisterNotificationCategory(category);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryId) {
|
||||
return window.runtime.RemoveNotificationCategory(categoryId);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications() {
|
||||
return window.runtime.RemoveAllPendingNotifications();
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier) {
|
||||
return window.runtime.RemovePendingNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications() {
|
||||
return window.runtime.RemoveAllDeliveredNotifications();
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier) {
|
||||
return window.runtime.RemoveDeliveredNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier) {
|
||||
return window.runtime.RemoveNotification(identifier);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}html,body{width:100%;height:100%;display:flex;justify-content:center;align-items:center;background-color:#f0f0f0;font-family:Arial,sans-serif}#app{width:400px;background:white;border-radius:8px;box-shadow:0 2px 10px #0000001a;overflow:hidden}.tabs{display:flex;background:#e9e9e9}.tab-button{flex:1;padding:12px;background:transparent;border:none;cursor:pointer;font-size:16px;transition:background .2s}.tab-button:hover{background:#dcdcdc}.tab-button.active{background:white;border-bottom:3px solid #3498db;font-weight:700}.tab-content{display:none;padding:20px}.tab-content.active{display:block}.buttons-vertical{display:flex;flex-direction:column;gap:12px;margin-bottom:20px}.action-button{padding:12px;font-size:16px;cursor:pointer;border:1px solid #bbb;border-radius:6px;background-color:#f8f8f8;transition:background-color .2s,border-color .2s}.action-button:hover{background-color:#e2e6ea;border-color:#999}.close-button{background-color:#e74c3c;color:#fff;border-color:#c0392b}.close-button:hover{background-color:#c0392b}#frequency-panel{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;background-color:#f9f9f9;border:1px solid #ddd;border-radius:6px;margin-bottom:10px}#frequency-label{font-weight:700;color:#555}#frequency-value{font-size:18px;color:#2c3e50}#status-panel{display:flex;align-items:center;gap:10px;padding:10px 14px;background-color:#f9f9f9;border:1px solid #ddd;border-radius:6px}.indicator{width:16px;height:16px;border-radius:50%;display:inline-block}.indicator-red{background-color:#e74c3c}.indicator-green{background-color:#2ecc71}#status-text{font-size:16px;color:#333}.modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.4);display:flex;justify-content:center;align-items:center;z-index:100}.modal.hidden{display:none}.modal-content{background:white;padding:20px;border-radius:8px;width:300px;box-shadow:0 5px 15px #0000004d;position:relative}.close-modal{position:absolute;top:10px;right:15px;font-size:22px;cursor:pointer;color:#777}.close-modal:hover{color:#333}.modal-content h3{margin-bottom:15px;text-align:center}.freq-input-group{display:flex;align-items:center;justify-content:center;gap:10px;margin-bottom:15px}.freq-btn{width:40px;height:40px;font-size:20px;background:#3498db;color:#fff;border:none;border-radius:4px;cursor:pointer;transition:background .2s}.freq-btn:hover{background:#2980b9}#freq-input{width:100px;padding:8px;font-size:18px;text-align:center;border:1px solid #ccc;border-radius:4px}#btn-set-freq{display:block;width:100%;margin-top:10px}.error-msg{color:#e74c3c;margin-top:8px;text-align:center;font-size:14px}.error-msg.hidden{display:none}
|
||||
@@ -1 +0,0 @@
|
||||
(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const t of document.querySelectorAll('link[rel="modulepreload"]'))a(t);new MutationObserver(t=>{for(const n of t)if(n.type==="childList")for(const s of n.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&a(s)}).observe(document,{childList:!0,subtree:!0});function m(t){const n={};return t.integrity&&(n.integrity=t.integrity),t.referrerpolicy&&(n.referrerPolicy=t.referrerpolicy),t.crossorigin==="use-credentials"?n.credentials="include":t.crossorigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function a(t){if(t.ep)return;t.ep=!0;const n=m(t);fetch(t.href,n)}})();function h(){return window.go.main.App.CheckConnection()}function b(){return window.go.main.App.Quit()}function p(d){return window.go.main.App.SetFrequency(d)}(function(){const d=document.querySelectorAll(".tab-button"),i=document.querySelectorAll(".tab-content");d.forEach(e=>{e.addEventListener("click",()=>{d.forEach(c=>c.classList.remove("active")),i.forEach(c=>c.classList.remove("active")),e.classList.add("active");const y=e.getAttribute("data-tab");document.getElementById(`tab-${y}`).classList.add("active")})});const m=document.getElementById("btn-check"),a=document.getElementById("btn-freq"),t=document.getElementById("btn-quit"),n=document.getElementById("status-indicator"),s=document.getElementById("status-text"),f=document.getElementById("frequency-label"),u=document.getElementById("frequency-value"),l=document.getElementById("freq-modal"),g=document.getElementById("close-modal"),r=document.getElementById("freq-input"),v=document.getElementById("btn-minus"),E=document.getElementById("btn-plus"),L=document.getElementById("btn-set-freq"),o=document.getElementById("modal-error");m.addEventListener("click",()=>{h().then(e=>{n.classList.remove("indicator-red"),n.classList.add("indicator-green"),s.textContent=`\u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E! ${e.status}`,f.textContent="\u0422\u0435\u043A\u0443\u0449\u0430\u044F \u0447\u0430\u0441\u0442\u043E\u0442\u0430:",u.textContent=e.frequency}).catch(e=>{console.error("\u041E\u0448\u0438\u0431\u043A\u0430 CheckConnection:",e),s.textContent="\u041E\u0448\u0438\u0431\u043A\u0430: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443"})}),a.addEventListener("click",()=>{l.classList.remove("hidden");const e=u.textContent!=="---"?u.textContent:"145";r.value=e,o.classList.add("hidden")}),g.addEventListener("click",()=>{l.classList.add("hidden")}),window.addEventListener("click",e=>{e.target===l&&l.classList.add("hidden")}),v.addEventListener("click",()=>{let e=parseInt(r.value,10)||1;e=Math.max(1,e-1),r.value=e.toString()}),E.addEventListener("click",()=>{let e=parseInt(r.value,10)||1;e+=1,r.value=e.toString()}),L.addEventListener("click",()=>{const e=r.value.trim();if(!/^\d+$/.test(e)){o.textContent="\u0412\u0432\u043E\u0434\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0446\u0435\u043B\u044B\u043C \u043F\u043E\u043B\u043E\u0436\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C",o.classList.remove("hidden");return}if(parseInt(e,10)<=0){o.textContent="\u0412\u0432\u043E\u0434\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0446\u0435\u043B\u044B\u043C \u043F\u043E\u043B\u043E\u0436\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C",o.classList.remove("hidden");return}p(e).then(c=>{f.textContent="\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u0430\u044F \u0447\u0430\u0441\u0442\u043E\u0442\u0430:",u.textContent=c,l.classList.add("hidden")}).catch(c=>{o.textContent=c,o.classList.remove("hidden")})}),t.addEventListener("click",()=>{b()})})();
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 176 KiB |
@@ -4,14 +4,35 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>Тест (Спектр)</title>
|
||||
<script type="module" crossorigin src="/assets/index.cb23cfd6.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.448f6f10.css">
|
||||
<title>Спектр</title>
|
||||
<script type="module" crossorigin src="/assets/index.02c817da.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.41fde905.css">
|
||||
</head>
|
||||
<body>
|
||||
<body data-theme="dark">
|
||||
|
||||
<!-- Переключатель темы -->
|
||||
<button id="theme-toggle" title="Сменить тему">☀</button>
|
||||
|
||||
<!-- Интро при запуске: закрывает всё окно приложения, потом "двери" разъезжаются -->
|
||||
<div id="intro-overlay">
|
||||
<div class="door door-left"></div>
|
||||
<div class="door door-right"></div>
|
||||
</div>
|
||||
|
||||
<div id="app">
|
||||
<!-- Заголовки вкладок -->
|
||||
<div class="tabs">
|
||||
|
||||
<!-- Бренд-блок -->
|
||||
<div id="brand">
|
||||
<div class="logo-icon">
|
||||
<img class="logo-dark" src="/assets/logo-dark.ff9a8bc7.jpg" alt="ТЭЧ ДМ" />
|
||||
<img class="logo-light" src="/assets/logo-light.d5e577a8.png" alt="ТЭЧ ДМ" />
|
||||
</div>
|
||||
<h1>СПЕКТР</h1>
|
||||
<div class="tag">видеоприёмник · панель управления</div>
|
||||
</div>
|
||||
|
||||
<!-- Заголовки вкладок (пока скрыты — активен только режим приёмника) -->
|
||||
<div class="tabs" hidden>
|
||||
<button class="tab-button active" data-tab="receiver">Режим приемника</button>
|
||||
<button class="tab-button" data-tab="detector">Режим детектора</button>
|
||||
<button class="tab-button" data-tab="info">Информация</button>
|
||||
@@ -64,4 +85,4 @@
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -4,12 +4,33 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="./src/style.css" />
|
||||
<title>Тест (Спектр)</title>
|
||||
<title>Спектр</title>
|
||||
</head>
|
||||
<body>
|
||||
<body data-theme="dark">
|
||||
|
||||
<!-- Переключатель темы -->
|
||||
<button id="theme-toggle" title="Сменить тему">☀</button>
|
||||
|
||||
<!-- Интро при запуске: закрывает всё окно приложения, потом "двери" разъезжаются -->
|
||||
<div id="intro-overlay">
|
||||
<div class="door door-left"></div>
|
||||
<div class="door door-right"></div>
|
||||
</div>
|
||||
|
||||
<div id="app">
|
||||
<!-- Заголовки вкладок -->
|
||||
<div class="tabs">
|
||||
|
||||
<!-- Бренд-блок -->
|
||||
<div id="brand">
|
||||
<div class="logo-icon">
|
||||
<img class="logo-dark" src="/src/assets/images/logo-dark.jpg" alt="ТЭЧ ДМ" />
|
||||
<img class="logo-light" src="/src/assets/images/logo-light.png" alt="ТЭЧ ДМ" />
|
||||
</div>
|
||||
<h1>СПЕКТР</h1>
|
||||
<div class="tag">видеоприёмник · панель управления</div>
|
||||
</div>
|
||||
|
||||
<!-- Заголовки вкладок (пока скрыты — активен только режим приёмника) -->
|
||||
<div class="tabs" hidden>
|
||||
<button class="tab-button active" data-tab="receiver">Режим приемника</button>
|
||||
<button class="tab-button" data-tab="detector">Режим детектора</button>
|
||||
<button class="tab-button" data-tab="info">Информация</button>
|
||||
@@ -62,4 +83,4 @@
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 176 KiB |
@@ -4,7 +4,73 @@ import {
|
||||
Quit,
|
||||
} from "../wailsjs/go/main/App";
|
||||
|
||||
// импорт через ES-модуль, а не строкой-путём — иначе vite не подставит
|
||||
// правильный хэшированный путь в собранном билде (сломается в проде)
|
||||
import logoDarkUrl from "./assets/images/logo-dark.jpg";
|
||||
|
||||
(function() {
|
||||
// === Тема (тёмная/светлая) ===
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
themeToggle.addEventListener('click', () => {
|
||||
const cur = document.body.getAttribute('data-theme');
|
||||
const next = cur === 'light' ? 'dark' : 'light';
|
||||
document.body.setAttribute('data-theme', next);
|
||||
themeToggle.textContent = next === 'light' ? '☾' : '☀';
|
||||
});
|
||||
|
||||
// === Интро при запуске (лого "двери" разъезжаются) ===
|
||||
const NATURAL_W = 640, NATURAL_H = 480;
|
||||
const AXIS_X = 312; // ось раздела — середина носа черепа / верхнего зубца шестерни
|
||||
const LOGO_DARK_URL = logoDarkUrl;
|
||||
|
||||
const introOverlay = document.getElementById('intro-overlay');
|
||||
const doorLeft = document.querySelector('#intro-overlay .door-left');
|
||||
const doorRight = document.querySelector('#intro-overlay .door-right');
|
||||
|
||||
function layoutDoors() {
|
||||
const W = window.innerWidth;
|
||||
const H = window.innerHeight;
|
||||
const scale = Math.max(W / NATURAL_W, H / NATURAL_H); // object-fit: cover на весь размер окна
|
||||
const renderW = NATURAL_W * scale;
|
||||
const renderH = NATURAL_H * scale;
|
||||
const offsetX = (W - renderW) / 2;
|
||||
const offsetY = (H - renderH) / 2;
|
||||
const axisDisplayX = offsetX + AXIS_X * scale;
|
||||
|
||||
const leftW = Math.max(0, Math.min(W, axisDisplayX));
|
||||
const rightW = W - leftW;
|
||||
|
||||
doorLeft.style.width = leftW + 'px';
|
||||
doorLeft.style.backgroundImage = `url(${LOGO_DARK_URL})`;
|
||||
doorLeft.style.backgroundSize = renderW + 'px ' + renderH + 'px';
|
||||
doorLeft.style.backgroundPosition = offsetX + 'px ' + offsetY + 'px';
|
||||
|
||||
doorRight.style.left = leftW + 'px';
|
||||
doorRight.style.width = rightW + 'px';
|
||||
doorRight.style.backgroundImage = `url(${LOGO_DARK_URL})`;
|
||||
doorRight.style.backgroundSize = renderW + 'px ' + renderH + 'px';
|
||||
doorRight.style.backgroundPosition = (offsetX - leftW) + 'px ' + offsetY + 'px';
|
||||
}
|
||||
|
||||
const OPEN_AT = 2100; // момент начала разъезда дверей от старта интро, мс
|
||||
const APPEAR_START = 100;
|
||||
const APPEAR_DUR = 450;
|
||||
|
||||
function playIntro() {
|
||||
introOverlay.style.display = '';
|
||||
introOverlay.classList.remove('appear', 'open');
|
||||
void introOverlay.offsetWidth; // reflow reset
|
||||
layoutDoors();
|
||||
|
||||
setTimeout(() => introOverlay.classList.add('appear'), APPEAR_START);
|
||||
setTimeout(() => introOverlay.classList.add('open'), OPEN_AT);
|
||||
// прячем оверлей из потока/кликов после того как двери разъехались и погасли
|
||||
setTimeout(() => { introOverlay.style.display = 'none'; }, OPEN_AT + 1170);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', layoutDoors);
|
||||
playIntro(); // main.js подключён как type="module" — DOM уже готов на этот момент
|
||||
|
||||
// === Переключение вкладок ===
|
||||
const tabButtons = document.querySelectorAll('.tab-button');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
|
||||
@@ -10,138 +10,233 @@ html, body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f0f0f0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 400px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Панель вкладок */
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: #e9e9e9;
|
||||
/* ===================== ТЕМА (тёмная/светлая) ===================== */
|
||||
body {
|
||||
background: radial-gradient(circle at 50% 18%, #1a1d24 0%, #0d0f13 70%);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
body[data-theme="light"] {
|
||||
background: radial-gradient(circle at 50% 18%, #ffffff 0%, #dde1e7 70%);
|
||||
}
|
||||
|
||||
#theme-toggle {
|
||||
position: fixed;
|
||||
top: 16px; right: 18px;
|
||||
z-index: 500;
|
||||
width: 34px; height: 34px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #333a46;
|
||||
background: #222a37;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
body[data-theme="light"] #theme-toggle {
|
||||
border-color: #c7cbd1;
|
||||
background: #ffffff;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* ===================== КАРТОЧКА ПРИЛОЖЕНИЯ ===================== */
|
||||
#app {
|
||||
width: 360px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
background: #1b1e24;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
body[data-theme="light"] #app {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* ---- бренд-блок ---- */
|
||||
#brand {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: 26px 24px 16px;
|
||||
gap: 6px;
|
||||
}
|
||||
.logo-icon {
|
||||
position: relative;
|
||||
width: 64px; height: 48px; /* пропорции 640:480 исходного лого */
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.logo-icon img {
|
||||
position: absolute; inset: 0;
|
||||
width: 100%; height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.logo-icon .logo-dark { mix-blend-mode: screen; display: block; }
|
||||
.logo-icon .logo-light { mix-blend-mode: multiply; display: none; }
|
||||
body[data-theme="light"] .logo-icon .logo-dark { display: none; }
|
||||
body[data-theme="light"] .logo-icon .logo-light { display: block; }
|
||||
|
||||
#brand h1 {
|
||||
color: #eeeeee;
|
||||
font-size: 21px;
|
||||
letter-spacing: 3.5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
#brand .tag {
|
||||
color: #7a7f89;
|
||||
font-size: 11px;
|
||||
letter-spacing: 1.2px;
|
||||
}
|
||||
body[data-theme="light"] #brand h1 { color: #1b1d22; }
|
||||
body[data-theme="light"] #brand .tag { color: #888d97; }
|
||||
|
||||
/* Панель вкладок (пока скрыта атрибутом hidden в HTML) */
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: transparent;
|
||||
}
|
||||
.tabs[hidden] {
|
||||
display: none; /* без этого .tabs{display:flex} выше перебивает атрибут hidden */
|
||||
}
|
||||
.tab-button {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
color: #9aa0aa;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
background: #dcdcdc;
|
||||
}
|
||||
|
||||
.tab-button:hover { background: rgba(255,255,255,0.05); }
|
||||
.tab-button.active {
|
||||
background: white;
|
||||
border-bottom: 3px solid #3498db;
|
||||
color: #eee;
|
||||
border-bottom: 2px solid #b8362a;
|
||||
font-weight: bold;
|
||||
}
|
||||
body[data-theme="light"] .tab-button { color: #7a7f89; }
|
||||
body[data-theme="light"] .tab-button.active { color: #1b1d22; }
|
||||
|
||||
/* Содержимое вкладок */
|
||||
.tab-content {
|
||||
display: none;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
.tab-content { display: none; padding: 4px 22px 24px; }
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
/* Вертикально расположенные кнопки */
|
||||
.buttons-vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
padding: 11px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 6px;
|
||||
background-color: #f8f8f8;
|
||||
border: 1px solid #2a2d34;
|
||||
border-radius: 8px;
|
||||
background-color: #15171c;
|
||||
color: #e8e8e8;
|
||||
transition: background-color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background-color: #e2e6ea;
|
||||
border-color: #999;
|
||||
background-color: #1d2027;
|
||||
border-color: #3a3e46;
|
||||
}
|
||||
body[data-theme="light"] .action-button {
|
||||
background-color: #ffffff;
|
||||
border-color: #ccd1d9;
|
||||
color: #22242a;
|
||||
}
|
||||
body[data-theme="light"] .action-button:hover {
|
||||
background-color: #f0f2f5;
|
||||
border-color: #aeb4bd;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background-color: #e74c3c;
|
||||
background-color: #b8362a;
|
||||
color: white;
|
||||
border-color: #8f2a20;
|
||||
}
|
||||
.close-button:hover { background-color: #c73f31; }
|
||||
body[data-theme="light"] .close-button {
|
||||
background-color: #e74c3c;
|
||||
border-color: #c0392b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: #c0392b;
|
||||
}
|
||||
body[data-theme="light"] .close-button:hover { background-color: #d8432f; }
|
||||
|
||||
/* Панель частоты */
|
||||
#frequency-panel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#frequency-label {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
#frequency-value {
|
||||
font-size: 18px;
|
||||
color: #2c3e50;
|
||||
padding: 9px 13px;
|
||||
background-color: #15171c;
|
||||
border: 1px solid #2a2d34;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
#frequency-label { font-weight: bold; color: #9aa0aa; font-size: 13px; }
|
||||
#frequency-value { font-size: 17px; color: #f2f2f2; }
|
||||
body[data-theme="light"] #frequency-panel { background-color: #ffffff; border-color: #ccd1d9; }
|
||||
body[data-theme="light"] #frequency-label { color: #7a7f89; }
|
||||
body[data-theme="light"] #frequency-value { color: #1b1d22; }
|
||||
|
||||
/* Панель статуса */
|
||||
#status-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
gap: 9px;
|
||||
padding: 9px 13px;
|
||||
background-color: #15171c;
|
||||
border: 1px solid #2a2d34;
|
||||
border-radius: 8px;
|
||||
}
|
||||
body[data-theme="light"] #status-panel { background-color: #ffffff; border-color: #ccd1d9; }
|
||||
|
||||
.indicator {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
.indicator-red { background-color: #e74c3c; }
|
||||
.indicator-green { background-color: #2ecc71; }
|
||||
|
||||
.indicator-red {
|
||||
background-color: #e74c3c;
|
||||
#status-text { font-size: 14px; color: #ddd; }
|
||||
body[data-theme="light"] #status-text { color: #333; }
|
||||
|
||||
/* ===================== ИНТРО (двери) ===================== */
|
||||
#intro-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: #000; /* подстраховка: перекрывает всё, даже пока JS ещё не выставил размеры дверей */
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
.indicator-green {
|
||||
background-color: #2ecc71;
|
||||
#intro-overlay.appear {
|
||||
animation: introPop 0.45s cubic-bezier(.2,.9,.3,1) forwards;
|
||||
}
|
||||
@keyframes introPop { to { transform: scale(1); } }
|
||||
|
||||
#status-text {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
.door {
|
||||
position: absolute;
|
||||
top: 0; bottom: 0;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
background-repeat: no-repeat;
|
||||
transition: transform 1.17s cubic-bezier(.65,0,.35,1);
|
||||
}
|
||||
.door-left { left: 0; }
|
||||
.door-right { left: 0; } /* left/width выставляются из JS под реальный размер окна */
|
||||
|
||||
#intro-overlay.open .door-left { transform: translateX(-100%); }
|
||||
#intro-overlay.open .door-right { transform: translateX(100%); }
|
||||
#intro-overlay.open {
|
||||
transition: opacity 0.45s ease 0.65s;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Модальное окно */
|
||||
@@ -151,7 +246,7 @@ html, body {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.4);
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -163,12 +258,17 @@ html, body {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
background: #1b1e24;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 300px;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.4);
|
||||
position: relative;
|
||||
color: #eee;
|
||||
}
|
||||
body[data-theme="light"] .modal-content {
|
||||
background: #ffffff;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
@@ -177,12 +277,11 @@ html, body {
|
||||
right: 15px;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.close-modal:hover {
|
||||
color: #333;
|
||||
color: #999;
|
||||
}
|
||||
.close-modal:hover { color: #eee; }
|
||||
body[data-theme="light"] .close-modal { color: #777; }
|
||||
body[data-theme="light"] .close-modal:hover { color: #333; }
|
||||
|
||||
.modal-content h3 {
|
||||
margin-bottom: 15px;
|
||||
@@ -201,25 +300,29 @@ html, body {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
background: #3498db;
|
||||
background: #b8362a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.freq-btn:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
.freq-btn:hover { background: #c73f31; }
|
||||
|
||||
#freq-input {
|
||||
width: 100px;
|
||||
padding: 8px;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
background: #15171c;
|
||||
color: #eee;
|
||||
}
|
||||
body[data-theme="light"] #freq-input {
|
||||
border-color: #ccc;
|
||||
background: #fff;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
#btn-set-freq {
|
||||
@@ -237,4 +340,4 @@ html, body {
|
||||
|
||||
.error-msg.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v4.25.3
|
||||
// source: receiver.proto
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type ClientMessage struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
|
||||
// Types that are valid to be assigned to Payload:
|
||||
//
|
||||
// *ClientMessage_Av
|
||||
// *ClientMessage_SetFrequency
|
||||
// *ClientMessage_GetFrequency
|
||||
Payload isClientMessage_Payload `protobuf_oneof:"payload"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ClientMessage) Reset() {
|
||||
*x = ClientMessage{}
|
||||
mi := &file_receiver_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ClientMessage) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ClientMessage) ProtoMessage() {}
|
||||
|
||||
func (x *ClientMessage) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_receiver_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ClientMessage.ProtoReflect.Descriptor instead.
|
||||
func (*ClientMessage) Descriptor() ([]byte, []int) {
|
||||
return file_receiver_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *ClientMessage) GetRequestId() string {
|
||||
if x != nil {
|
||||
return x.RequestId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ClientMessage) GetPayload() isClientMessage_Payload {
|
||||
if x != nil {
|
||||
return x.Payload
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ClientMessage) GetAv() *AvCommand {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ClientMessage_Av); ok {
|
||||
return x.Av
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ClientMessage) GetSetFrequency() *SetFrequencyCommand {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ClientMessage_SetFrequency); ok {
|
||||
return x.SetFrequency
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ClientMessage) GetGetFrequency() *GetFrequencyCommand {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ClientMessage_GetFrequency); ok {
|
||||
return x.GetFrequency
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isClientMessage_Payload interface {
|
||||
isClientMessage_Payload()
|
||||
}
|
||||
|
||||
type ClientMessage_Av struct {
|
||||
Av *AvCommand `protobuf:"bytes,10,opt,name=av,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ClientMessage_SetFrequency struct {
|
||||
SetFrequency *SetFrequencyCommand `protobuf:"bytes,11,opt,name=set_frequency,json=setFrequency,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ClientMessage_GetFrequency struct {
|
||||
GetFrequency *GetFrequencyCommand `protobuf:"bytes,12,opt,name=get_frequency,json=getFrequency,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*ClientMessage_Av) isClientMessage_Payload() {}
|
||||
|
||||
func (*ClientMessage_SetFrequency) isClientMessage_Payload() {}
|
||||
|
||||
func (*ClientMessage_GetFrequency) isClientMessage_Payload() {}
|
||||
|
||||
type ServerMessage struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
|
||||
// Types that are valid to be assigned to Payload:
|
||||
//
|
||||
// *ServerMessage_Av
|
||||
// *ServerMessage_SetFrequency
|
||||
// *ServerMessage_GetFrequency
|
||||
// *ServerMessage_Error
|
||||
Payload isServerMessage_Payload `protobuf_oneof:"payload"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ServerMessage) Reset() {
|
||||
*x = ServerMessage{}
|
||||
mi := &file_receiver_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ServerMessage) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ServerMessage) ProtoMessage() {}
|
||||
|
||||
func (x *ServerMessage) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_receiver_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ServerMessage.ProtoReflect.Descriptor instead.
|
||||
func (*ServerMessage) Descriptor() ([]byte, []int) {
|
||||
return file_receiver_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *ServerMessage) GetRequestId() string {
|
||||
if x != nil {
|
||||
return x.RequestId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ServerMessage) GetPayload() isServerMessage_Payload {
|
||||
if x != nil {
|
||||
return x.Payload
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ServerMessage) GetAv() *AvResponse {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ServerMessage_Av); ok {
|
||||
return x.Av
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ServerMessage) GetSetFrequency() *SetFrequencyResponse {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ServerMessage_SetFrequency); ok {
|
||||
return x.SetFrequency
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ServerMessage) GetGetFrequency() *GetFrequencyResponse {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ServerMessage_GetFrequency); ok {
|
||||
return x.GetFrequency
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ServerMessage) GetError() *Error {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*ServerMessage_Error); ok {
|
||||
return x.Error
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isServerMessage_Payload interface {
|
||||
isServerMessage_Payload()
|
||||
}
|
||||
|
||||
type ServerMessage_Av struct {
|
||||
Av *AvResponse `protobuf:"bytes,10,opt,name=av,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ServerMessage_SetFrequency struct {
|
||||
SetFrequency *SetFrequencyResponse `protobuf:"bytes,11,opt,name=set_frequency,json=setFrequency,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ServerMessage_GetFrequency struct {
|
||||
GetFrequency *GetFrequencyResponse `protobuf:"bytes,12,opt,name=get_frequency,json=getFrequency,proto3,oneof"`
|
||||
}
|
||||
|
||||
type ServerMessage_Error struct {
|
||||
Error *Error `protobuf:"bytes,50,opt,name=error,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*ServerMessage_Av) isServerMessage_Payload() {}
|
||||
|
||||
func (*ServerMessage_SetFrequency) isServerMessage_Payload() {}
|
||||
|
||||
func (*ServerMessage_GetFrequency) isServerMessage_Payload() {}
|
||||
|
||||
func (*ServerMessage_Error) isServerMessage_Payload() {}
|
||||
|
||||
type AvCommand struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AvCommand) Reset() {
|
||||
*x = AvCommand{}
|
||||
mi := &file_receiver_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AvCommand) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AvCommand) ProtoMessage() {}
|
||||
|
||||
func (x *AvCommand) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_receiver_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AvCommand.ProtoReflect.Descriptor instead.
|
||||
func (*AvCommand) Descriptor() ([]byte, []int) {
|
||||
return file_receiver_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
type SetFrequencyCommand struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Frequency uint32 `protobuf:"varint,1,opt,name=frequency,proto3" json:"frequency,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SetFrequencyCommand) Reset() {
|
||||
*x = SetFrequencyCommand{}
|
||||
mi := &file_receiver_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SetFrequencyCommand) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SetFrequencyCommand) ProtoMessage() {}
|
||||
|
||||
func (x *SetFrequencyCommand) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_receiver_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SetFrequencyCommand.ProtoReflect.Descriptor instead.
|
||||
func (*SetFrequencyCommand) Descriptor() ([]byte, []int) {
|
||||
return file_receiver_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *SetFrequencyCommand) GetFrequency() uint32 {
|
||||
if x != nil {
|
||||
return x.Frequency
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetFrequencyCommand struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetFrequencyCommand) Reset() {
|
||||
*x = GetFrequencyCommand{}
|
||||
mi := &file_receiver_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetFrequencyCommand) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetFrequencyCommand) ProtoMessage() {}
|
||||
|
||||
func (x *GetFrequencyCommand) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_receiver_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetFrequencyCommand.ProtoReflect.Descriptor instead.
|
||||
func (*GetFrequencyCommand) Descriptor() ([]byte, []int) {
|
||||
return file_receiver_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
type AvResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
|
||||
Hw string `protobuf:"bytes,2,opt,name=hw,proto3" json:"hw,omitempty"`
|
||||
Sw string `protobuf:"bytes,3,opt,name=sw,proto3" json:"sw,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AvResponse) Reset() {
|
||||
*x = AvResponse{}
|
||||
mi := &file_receiver_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AvResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AvResponse) ProtoMessage() {}
|
||||
|
||||
func (x *AvResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_receiver_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AvResponse.ProtoReflect.Descriptor instead.
|
||||
func (*AvResponse) Descriptor() ([]byte, []int) {
|
||||
return file_receiver_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *AvResponse) GetOk() bool {
|
||||
if x != nil {
|
||||
return x.Ok
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *AvResponse) GetHw() string {
|
||||
if x != nil {
|
||||
return x.Hw
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AvResponse) GetSw() string {
|
||||
if x != nil {
|
||||
return x.Sw
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SetFrequencyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SetFrequencyResponse) Reset() {
|
||||
*x = SetFrequencyResponse{}
|
||||
mi := &file_receiver_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SetFrequencyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SetFrequencyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SetFrequencyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_receiver_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SetFrequencyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SetFrequencyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_receiver_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *SetFrequencyResponse) GetOk() bool {
|
||||
if x != nil {
|
||||
return x.Ok
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type GetFrequencyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Frequency uint32 `protobuf:"varint,1,opt,name=frequency,proto3" json:"frequency,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetFrequencyResponse) Reset() {
|
||||
*x = GetFrequencyResponse{}
|
||||
mi := &file_receiver_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetFrequencyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetFrequencyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetFrequencyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_receiver_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetFrequencyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetFrequencyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_receiver_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *GetFrequencyResponse) GetFrequency() uint32 {
|
||||
if x != nil {
|
||||
return x.Frequency
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Error) Reset() {
|
||||
*x = Error{}
|
||||
mi := &file_receiver_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Error) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Error) ProtoMessage() {}
|
||||
|
||||
func (x *Error) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_receiver_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Error.ProtoReflect.Descriptor instead.
|
||||
func (*Error) Descriptor() ([]byte, []int) {
|
||||
return file_receiver_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *Error) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_receiver_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_receiver_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x0ereceiver.proto\x12\vreceiver.v1\"\xf5\x01\n" +
|
||||
"\rClientMessage\x12\x1d\n" +
|
||||
"\n" +
|
||||
"request_id\x18\x01 \x01(\tR\trequestId\x12(\n" +
|
||||
"\x02av\x18\n" +
|
||||
" \x01(\v2\x16.receiver.v1.AvCommandH\x00R\x02av\x12G\n" +
|
||||
"\rset_frequency\x18\v \x01(\v2 .receiver.v1.SetFrequencyCommandH\x00R\fsetFrequency\x12G\n" +
|
||||
"\rget_frequency\x18\f \x01(\v2 .receiver.v1.GetFrequencyCommandH\x00R\fgetFrequencyB\t\n" +
|
||||
"\apayload\"\xa4\x02\n" +
|
||||
"\rServerMessage\x12\x1d\n" +
|
||||
"\n" +
|
||||
"request_id\x18\x01 \x01(\tR\trequestId\x12)\n" +
|
||||
"\x02av\x18\n" +
|
||||
" \x01(\v2\x17.receiver.v1.AvResponseH\x00R\x02av\x12H\n" +
|
||||
"\rset_frequency\x18\v \x01(\v2!.receiver.v1.SetFrequencyResponseH\x00R\fsetFrequency\x12H\n" +
|
||||
"\rget_frequency\x18\f \x01(\v2!.receiver.v1.GetFrequencyResponseH\x00R\fgetFrequency\x12*\n" +
|
||||
"\x05error\x182 \x01(\v2\x12.receiver.v1.ErrorH\x00R\x05errorB\t\n" +
|
||||
"\apayload\"\v\n" +
|
||||
"\tAvCommand\"3\n" +
|
||||
"\x13SetFrequencyCommand\x12\x1c\n" +
|
||||
"\tfrequency\x18\x01 \x01(\rR\tfrequency\"\x15\n" +
|
||||
"\x13GetFrequencyCommand\"<\n" +
|
||||
"\n" +
|
||||
"AvResponse\x12\x0e\n" +
|
||||
"\x02ok\x18\x01 \x01(\bR\x02ok\x12\x0e\n" +
|
||||
"\x02hw\x18\x02 \x01(\tR\x02hw\x12\x0e\n" +
|
||||
"\x02sw\x18\x03 \x01(\tR\x02sw\"&\n" +
|
||||
"\x14SetFrequencyResponse\x12\x0e\n" +
|
||||
"\x02ok\x18\x01 \x01(\bR\x02ok\"4\n" +
|
||||
"\x14GetFrequencyResponse\x12\x1c\n" +
|
||||
"\tfrequency\x18\x01 \x01(\rR\tfrequency\"!\n" +
|
||||
"\x05Error\x12\x18\n" +
|
||||
"\amessage\x18\x01 \x01(\tR\amessage2X\n" +
|
||||
"\x0fReceiverService\x12E\n" +
|
||||
"\aConnect\x12\x1a.receiver.v1.ClientMessage\x1a\x1a.receiver.v1.ServerMessage(\x010\x01B\x1fZ\x1d../spectr/internal/grpc/pb;pbb\x06proto3"
|
||||
|
||||
var (
|
||||
file_receiver_proto_rawDescOnce sync.Once
|
||||
file_receiver_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_receiver_proto_rawDescGZIP() []byte {
|
||||
file_receiver_proto_rawDescOnce.Do(func() {
|
||||
file_receiver_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_receiver_proto_rawDesc), len(file_receiver_proto_rawDesc)))
|
||||
})
|
||||
return file_receiver_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_receiver_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_receiver_proto_goTypes = []any{
|
||||
(*ClientMessage)(nil), // 0: receiver.v1.ClientMessage
|
||||
(*ServerMessage)(nil), // 1: receiver.v1.ServerMessage
|
||||
(*AvCommand)(nil), // 2: receiver.v1.AvCommand
|
||||
(*SetFrequencyCommand)(nil), // 3: receiver.v1.SetFrequencyCommand
|
||||
(*GetFrequencyCommand)(nil), // 4: receiver.v1.GetFrequencyCommand
|
||||
(*AvResponse)(nil), // 5: receiver.v1.AvResponse
|
||||
(*SetFrequencyResponse)(nil), // 6: receiver.v1.SetFrequencyResponse
|
||||
(*GetFrequencyResponse)(nil), // 7: receiver.v1.GetFrequencyResponse
|
||||
(*Error)(nil), // 8: receiver.v1.Error
|
||||
}
|
||||
var file_receiver_proto_depIdxs = []int32{
|
||||
2, // 0: receiver.v1.ClientMessage.av:type_name -> receiver.v1.AvCommand
|
||||
3, // 1: receiver.v1.ClientMessage.set_frequency:type_name -> receiver.v1.SetFrequencyCommand
|
||||
4, // 2: receiver.v1.ClientMessage.get_frequency:type_name -> receiver.v1.GetFrequencyCommand
|
||||
5, // 3: receiver.v1.ServerMessage.av:type_name -> receiver.v1.AvResponse
|
||||
6, // 4: receiver.v1.ServerMessage.set_frequency:type_name -> receiver.v1.SetFrequencyResponse
|
||||
7, // 5: receiver.v1.ServerMessage.get_frequency:type_name -> receiver.v1.GetFrequencyResponse
|
||||
8, // 6: receiver.v1.ServerMessage.error:type_name -> receiver.v1.Error
|
||||
0, // 7: receiver.v1.ReceiverService.Connect:input_type -> receiver.v1.ClientMessage
|
||||
1, // 8: receiver.v1.ReceiverService.Connect:output_type -> receiver.v1.ServerMessage
|
||||
8, // [8:9] is the sub-list for method output_type
|
||||
7, // [7:8] is the sub-list for method input_type
|
||||
7, // [7:7] is the sub-list for extension type_name
|
||||
7, // [7:7] is the sub-list for extension extendee
|
||||
0, // [0:7] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_receiver_proto_init() }
|
||||
func file_receiver_proto_init() {
|
||||
if File_receiver_proto != nil {
|
||||
return
|
||||
}
|
||||
file_receiver_proto_msgTypes[0].OneofWrappers = []any{
|
||||
(*ClientMessage_Av)(nil),
|
||||
(*ClientMessage_SetFrequency)(nil),
|
||||
(*ClientMessage_GetFrequency)(nil),
|
||||
}
|
||||
file_receiver_proto_msgTypes[1].OneofWrappers = []any{
|
||||
(*ServerMessage_Av)(nil),
|
||||
(*ServerMessage_SetFrequency)(nil),
|
||||
(*ServerMessage_GetFrequency)(nil),
|
||||
(*ServerMessage_Error)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_receiver_proto_rawDesc), len(file_receiver_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_receiver_proto_goTypes,
|
||||
DependencyIndexes: file_receiver_proto_depIdxs,
|
||||
MessageInfos: file_receiver_proto_msgTypes,
|
||||
}.Build()
|
||||
File_receiver_proto = out.File
|
||||
file_receiver_proto_goTypes = nil
|
||||
file_receiver_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v4.25.3
|
||||
// source: receiver.proto
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
ReceiverService_Connect_FullMethodName = "/receiver.v1.ReceiverService/Connect"
|
||||
)
|
||||
|
||||
// ReceiverServiceClient is the client API for ReceiverService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ReceiverServiceClient interface {
|
||||
Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ClientMessage, ServerMessage], error)
|
||||
}
|
||||
|
||||
type receiverServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewReceiverServiceClient(cc grpc.ClientConnInterface) ReceiverServiceClient {
|
||||
return &receiverServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *receiverServiceClient) Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ClientMessage, ServerMessage], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &ReceiverService_ServiceDesc.Streams[0], ReceiverService_Connect_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[ClientMessage, ServerMessage]{ClientStream: stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type ReceiverService_ConnectClient = grpc.BidiStreamingClient[ClientMessage, ServerMessage]
|
||||
|
||||
// ReceiverServiceServer is the server API for ReceiverService service.
|
||||
// All implementations must embed UnimplementedReceiverServiceServer
|
||||
// for forward compatibility.
|
||||
type ReceiverServiceServer interface {
|
||||
Connect(grpc.BidiStreamingServer[ClientMessage, ServerMessage]) error
|
||||
mustEmbedUnimplementedReceiverServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedReceiverServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedReceiverServiceServer struct{}
|
||||
|
||||
func (UnimplementedReceiverServiceServer) Connect(grpc.BidiStreamingServer[ClientMessage, ServerMessage]) error {
|
||||
return status.Error(codes.Unimplemented, "method Connect not implemented")
|
||||
}
|
||||
func (UnimplementedReceiverServiceServer) mustEmbedUnimplementedReceiverServiceServer() {}
|
||||
func (UnimplementedReceiverServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeReceiverServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ReceiverServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeReceiverServiceServer interface {
|
||||
mustEmbedUnimplementedReceiverServiceServer()
|
||||
}
|
||||
|
||||
func RegisterReceiverServiceServer(s grpc.ServiceRegistrar, srv ReceiverServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedReceiverServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&ReceiverService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _ReceiverService_Connect_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(ReceiverServiceServer).Connect(&grpc.GenericServerStream[ClientMessage, ServerMessage]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type ReceiverService_ConnectServer = grpc.BidiStreamingServer[ClientMessage, ServerMessage]
|
||||
|
||||
// ReceiverService_ServiceDesc is the grpc.ServiceDesc for ReceiverService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var ReceiverService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "receiver.v1.ReceiverService",
|
||||
HandlerType: (*ReceiverServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "Connect",
|
||||
Handler: _ReceiverService_Connect_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "receiver.proto",
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Package localuart находит и открывает USB-UART адаптер напрямую, без
|
||||
// демона — для standalone-версии приложения (internal/localctl).
|
||||
package localuart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"go.bug.st/serial/enumerator"
|
||||
)
|
||||
|
||||
// knownVID — VID распространённых чипов USB-UART мостов. Порты с таким VID
|
||||
// считаются вероятным переходником "UART to USB" и предлагаются первыми.
|
||||
var knownVID = map[string]bool{
|
||||
"0403": true, // FTDI
|
||||
"1A86": true, // WCH CH340 / CH341
|
||||
"10C4": true, // Silicon Labs CP210x
|
||||
"067B": true, // Prolific PL2303
|
||||
}
|
||||
|
||||
// Candidate — один найденный USB-serial порт.
|
||||
type Candidate struct {
|
||||
Name string
|
||||
VID string
|
||||
PID string
|
||||
}
|
||||
|
||||
// ListCandidates возвращает USB-serial порты, обнаруженные в системе;
|
||||
// порты с известным VID переходника идут первыми.
|
||||
func ListCandidates() ([]Candidate, error) {
|
||||
ports, err := enumerator.GetDetailedPortsList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var known, other []Candidate
|
||||
for _, p := range ports {
|
||||
if !p.IsUSB {
|
||||
continue
|
||||
}
|
||||
c := Candidate{
|
||||
Name: p.Name,
|
||||
VID: strings.ToUpper(p.VID),
|
||||
PID: strings.ToUpper(p.PID),
|
||||
}
|
||||
if knownVID[c.VID] {
|
||||
known = append(known, c)
|
||||
} else {
|
||||
other = append(other, c)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(known, func(i, j int) bool { return known[i].Name < known[j].Name })
|
||||
sort.Slice(other, func(i, j int) bool { return other[i].Name < other[j].Name })
|
||||
|
||||
return append(known, other...), nil
|
||||
}
|
||||
|
||||
// PickPort возвращает наиболее вероятный порт UART-to-USB переходника.
|
||||
func PickPort() (string, error) {
|
||||
candidates, err := ListCandidates()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("не удалось получить список портов: %w", err)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return "", fmt.Errorf("USB-UART адаптер не найден")
|
||||
}
|
||||
return candidates[0].Name, nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package localuart
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.bug.st/serial"
|
||||
)
|
||||
|
||||
const (
|
||||
bufferSize = 4096
|
||||
readTimeout = 50 * time.Millisecond
|
||||
idle = 200 * time.Millisecond
|
||||
silenceTimeout = 2 * time.Second // если вообще нет ответа — не виснуть
|
||||
)
|
||||
|
||||
// Port — открытый напрямую USB-UART порт (без демона).
|
||||
type Port struct {
|
||||
name string
|
||||
port serial.Port
|
||||
}
|
||||
|
||||
// Open открывает порт по имени на заданной скорости.
|
||||
func Open(name string, baud int) (*Port, error) {
|
||||
mode := &serial.Mode{BaudRate: baud}
|
||||
p, err := serial.Open(name, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.SetReadTimeout(readTimeout); err != nil {
|
||||
_ = p.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &Port{name: name, port: p}, nil
|
||||
}
|
||||
|
||||
// Name возвращает имя порта (например /dev/ttyUSB0 или COM3).
|
||||
func (p *Port) Name() string { return p.name }
|
||||
|
||||
// Write отправляет команду устройству (как в internal/uart.UART).
|
||||
func (p *Port) Write(cmd string) error {
|
||||
_, err := p.port.Write([]byte(strings.TrimSpace(cmd) + "\r"))
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadResponse читает ответ до паузы в потоке данных (idle), либо
|
||||
// возвращает ошибку, если устройство вообще не ответило за silenceTimeout —
|
||||
// это важно для standalone-режима: кнопка проверки не должна виснуть
|
||||
// навсегда, если выбранный порт "не тот".
|
||||
func (p *Port) ReadResponse() (string, error) {
|
||||
buf := make([]byte, bufferSize)
|
||||
var out bytes.Buffer
|
||||
|
||||
received := false
|
||||
lastData := time.Now()
|
||||
started := time.Now()
|
||||
for {
|
||||
n, err := p.port.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
received = true
|
||||
lastData = time.Now()
|
||||
out.Write(buf[:n])
|
||||
}
|
||||
|
||||
if received && time.Since(lastData) >= idle {
|
||||
return strings.TrimSpace(out.String()), nil
|
||||
}
|
||||
if !received && time.Since(started) >= silenceTimeout {
|
||||
return "", io.ErrNoProgress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exec отправляет команду и возвращает ответ устройства.
|
||||
func (p *Port) Exec(cmd string) (string, error) {
|
||||
if err := p.Write(cmd); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return p.ReadResponse()
|
||||
}
|
||||
|
||||
// Close закрывает порт.
|
||||
func (p *Port) Close() error {
|
||||
return p.port.Close()
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Package protocol содержит кодирование/декодирование команд устройства
|
||||
// (av/af/ar), вынесенное из internal/service, чтобы им мог пользоваться и
|
||||
// демон (через internal/service), и standalone-приложение (internal/localctl)
|
||||
// без зависимости друг от друга.
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
versionRe = regexp.MustCompile(`(?i)HW[:=]\s*([^\s]+).*?(?:FW|SW)[:=]\s*([^\s]+)`)
|
||||
freqRe = regexp.MustCompile(`(?i)\bf\d{7,9}\b`)
|
||||
)
|
||||
|
||||
// ParseVersion извлекает HW/FW из сырого ответа на команду "av".
|
||||
func ParseVersion(line string) (hw, sw string) {
|
||||
match := versionRe.FindStringSubmatch(line)
|
||||
if len(match) == 3 {
|
||||
return match[1], match[2]
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// EncodeFrequencyCommand формирует команду "af" для частоты в МГц.
|
||||
func EncodeFrequencyCommand(frequency uint32) string {
|
||||
return fmt.Sprintf("af%07d", frequency*100)
|
||||
}
|
||||
|
||||
// DecodeFrequencyResponse разбирает ответ на команду "ar" в частоту в МГц.
|
||||
func DecodeFrequencyResponse(line string) (uint32, error) {
|
||||
match := freqRe.FindString(line)
|
||||
if match == "" {
|
||||
return 0, fmt.Errorf("invalid frequency response %q", line)
|
||||
}
|
||||
|
||||
value, err := strconv.ParseUint(match[1:], 10, 32)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid frequency response %q: %w", line, err)
|
||||
}
|
||||
|
||||
return uint32(value / 100), nil
|
||||
}
|
||||