Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 946c1beabf |
@@ -13,5 +13,8 @@
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# local notes
|
||||||
|
TODO.md
|
||||||
|
|
||||||
# protobuf generated
|
# 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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
<title>Тест (Спектр)</title>
|
<title>Спектр</title>
|
||||||
<script type="module" crossorigin src="/assets/index.cb23cfd6.js"></script>
|
<script type="module" crossorigin src="/assets/index.02c817da.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/index.448f6f10.css">
|
<link rel="stylesheet" href="/assets/index.41fde905.css">
|
||||||
</head>
|
</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 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 active" data-tab="receiver">Режим приемника</button>
|
||||||
<button class="tab-button" data-tab="detector">Режим детектора</button>
|
<button class="tab-button" data-tab="detector">Режим детектора</button>
|
||||||
<button class="tab-button" data-tab="info">Информация</button>
|
<button class="tab-button" data-tab="info">Информация</button>
|
||||||
|
|||||||
@@ -4,12 +4,33 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" href="./src/style.css" />
|
<link rel="stylesheet" href="./src/style.css" />
|
||||||
<title>Тест (Спектр)</title>
|
<title>Спектр</title>
|
||||||
</head>
|
</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 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 active" data-tab="receiver">Режим приемника</button>
|
||||||
<button class="tab-button" data-tab="detector">Режим детектора</button>
|
<button class="tab-button" data-tab="detector">Режим детектора</button>
|
||||||
<button class="tab-button" data-tab="info">Информация</button>
|
<button class="tab-button" data-tab="info">Информация</button>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 176 KiB |
@@ -4,7 +4,73 @@ import {
|
|||||||
Quit,
|
Quit,
|
||||||
} from "../wailsjs/go/main/App";
|
} from "../wailsjs/go/main/App";
|
||||||
|
|
||||||
|
// импорт через ES-модуль, а не строкой-путём — иначе vite не подставит
|
||||||
|
// правильный хэшированный путь в собранном билде (сломается в проде)
|
||||||
|
import logoDarkUrl from "./assets/images/logo-dark.jpg";
|
||||||
|
|
||||||
(function() {
|
(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 tabButtons = document.querySelectorAll('.tab-button');
|
||||||
const tabContents = document.querySelectorAll('.tab-content');
|
const tabContents = document.querySelectorAll('.tab-content');
|
||||||
|
|||||||
@@ -10,138 +10,233 @@ html, body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #f0f0f0;
|
|
||||||
font-family: Arial, sans-serif;
|
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;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Панель вкладок */
|
/* ===================== ТЕМА (тёмная/светлая) ===================== */
|
||||||
.tabs {
|
body {
|
||||||
display: flex;
|
background: radial-gradient(circle at 50% 18%, #1a1d24 0%, #0d0f13 70%);
|
||||||
background: #e9e9e9;
|
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 {
|
.tab-button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
|
color: #9aa0aa;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
.tab-button:hover { background: rgba(255,255,255,0.05); }
|
||||||
.tab-button:hover {
|
|
||||||
background: #dcdcdc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-button.active {
|
.tab-button.active {
|
||||||
background: white;
|
color: #eee;
|
||||||
border-bottom: 3px solid #3498db;
|
border-bottom: 2px solid #b8362a;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
body[data-theme="light"] .tab-button { color: #7a7f89; }
|
||||||
|
body[data-theme="light"] .tab-button.active { color: #1b1d22; }
|
||||||
|
|
||||||
/* Содержимое вкладок */
|
/* Содержимое вкладок */
|
||||||
.tab-content {
|
.tab-content { display: none; padding: 4px 22px 24px; }
|
||||||
display: none;
|
.tab-content.active { display: block; }
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Вертикально расположенные кнопки */
|
/* Вертикально расположенные кнопки */
|
||||||
.buttons-vertical {
|
.buttons-vertical {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button {
|
.action-button {
|
||||||
padding: 12px;
|
padding: 11px;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid #bbb;
|
border: 1px solid #2a2d34;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background-color: #f8f8f8;
|
background-color: #15171c;
|
||||||
|
color: #e8e8e8;
|
||||||
transition: background-color 0.2s, border-color 0.2s;
|
transition: background-color 0.2s, border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button:hover {
|
.action-button:hover {
|
||||||
background-color: #e2e6ea;
|
background-color: #1d2027;
|
||||||
border-color: #999;
|
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 {
|
.close-button {
|
||||||
background-color: #e74c3c;
|
background-color: #b8362a;
|
||||||
color: white;
|
color: white;
|
||||||
|
border-color: #8f2a20;
|
||||||
|
}
|
||||||
|
.close-button:hover { background-color: #c73f31; }
|
||||||
|
body[data-theme="light"] .close-button {
|
||||||
|
background-color: #e74c3c;
|
||||||
border-color: #c0392b;
|
border-color: #c0392b;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
body[data-theme="light"] .close-button:hover { background-color: #d8432f; }
|
||||||
.close-button:hover {
|
|
||||||
background-color: #c0392b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Панель частоты */
|
/* Панель частоты */
|
||||||
#frequency-panel {
|
#frequency-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 14px;
|
padding: 9px 13px;
|
||||||
background-color: #f9f9f9;
|
background-color: #15171c;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #2a2d34;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 9px;
|
||||||
}
|
|
||||||
|
|
||||||
#frequency-label {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
#frequency-value {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
}
|
||||||
|
#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 {
|
#status-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 9px;
|
||||||
padding: 10px 14px;
|
padding: 9px 13px;
|
||||||
background-color: #f9f9f9;
|
background-color: #15171c;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #2a2d34;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
body[data-theme="light"] #status-panel { background-color: #ffffff; border-color: #ccd1d9; }
|
||||||
|
|
||||||
.indicator {
|
.indicator {
|
||||||
width: 16px;
|
width: 14px;
|
||||||
height: 16px;
|
height: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
.indicator-red { background-color: #e74c3c; }
|
||||||
|
.indicator-green { background-color: #2ecc71; }
|
||||||
|
|
||||||
.indicator-red {
|
#status-text { font-size: 14px; color: #ddd; }
|
||||||
background-color: #e74c3c;
|
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 {
|
||||||
.indicator-green {
|
animation: introPop 0.45s cubic-bezier(.2,.9,.3,1) forwards;
|
||||||
background-color: #2ecc71;
|
|
||||||
}
|
}
|
||||||
|
@keyframes introPop { to { transform: scale(1); } }
|
||||||
|
|
||||||
#status-text {
|
.door {
|
||||||
font-size: 16px;
|
position: absolute;
|
||||||
color: #333;
|
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;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0,0,0,0.4);
|
background: rgba(0,0,0,0.5);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -163,12 +258,17 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: white;
|
background: #1b1e24;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
width: 300px;
|
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;
|
position: relative;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
body[data-theme="light"] .modal-content {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-modal {
|
.close-modal {
|
||||||
@@ -177,12 +277,11 @@ html, body {
|
|||||||
right: 15px;
|
right: 15px;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #777;
|
color: #999;
|
||||||
}
|
|
||||||
|
|
||||||
.close-modal:hover {
|
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
.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 {
|
.modal-content h3 {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
@@ -201,25 +300,29 @@ html, body {
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
background: #3498db;
|
background: #b8362a;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
.freq-btn:hover { background: #c73f31; }
|
||||||
.freq-btn:hover {
|
|
||||||
background: #2980b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
#freq-input {
|
#freq-input {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #444;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
background: #15171c;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
body[data-theme="light"] #freq-input {
|
||||||
|
border-color: #ccc;
|
||||||
|
background: #fff;
|
||||||
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
#btn-set-freq {
|
#btn-set-freq {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||