first commit
This commit is contained in:
27
app.go
Normal file
27
app.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App struct
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp creates a new App application struct
|
||||||
|
func NewApp() *App {
|
||||||
|
return &App{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startup is called when the app starts. The context is saved
|
||||||
|
// so we can call the runtime methods
|
||||||
|
func (a *App) startup(ctx context.Context) {
|
||||||
|
a.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greet returns a greeting for the given name
|
||||||
|
func (a *App) Greet(name string) string {
|
||||||
|
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||||
|
}
|
||||||
5
frontend/.vscode/extensions.json
vendored
Normal file
5
frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"svelte.svelte-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
63
frontend/README.md
Normal file
63
frontend/README.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Svelte + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Svelte in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VS Code](https://code.visualstudio.com/)
|
||||||
|
|
||||||
|
+ [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
|
||||||
|
|
||||||
|
## Need an official Svelte framework?
|
||||||
|
|
||||||
|
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its
|
||||||
|
serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less,
|
||||||
|
and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
|
||||||
|
|
||||||
|
## Technical considerations
|
||||||
|
|
||||||
|
**Why use this over SvelteKit?**
|
||||||
|
|
||||||
|
- It brings its own routing solution which might not be preferable for some users.
|
||||||
|
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
|
||||||
|
`vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example.
|
||||||
|
|
||||||
|
This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer
|
||||||
|
experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite`
|
||||||
|
templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
|
||||||
|
|
||||||
|
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been
|
||||||
|
structured similarly to SvelteKit so that it is easy to migrate.
|
||||||
|
|
||||||
|
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
|
||||||
|
|
||||||
|
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash
|
||||||
|
references keeps the default TypeScript setting of accepting type information from the entire workspace, while also
|
||||||
|
adding `svelte` and `vite/client` type information.
|
||||||
|
|
||||||
|
**Why include `.vscode/extensions.json`?**
|
||||||
|
|
||||||
|
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to
|
||||||
|
install the recommended extension upon opening the project.
|
||||||
|
|
||||||
|
**Why enable `checkJs` in the JS template?**
|
||||||
|
|
||||||
|
It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate.
|
||||||
|
This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of
|
||||||
|
JavaScript, it is trivial to change the configuration.
|
||||||
|
|
||||||
|
**Why is HMR not preserving my local component state?**
|
||||||
|
|
||||||
|
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr`
|
||||||
|
and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the
|
||||||
|
details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
|
||||||
|
|
||||||
|
If you have state that's important to retain within a component, consider creating an external store which would not be
|
||||||
|
replaced by HMR.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// store.js
|
||||||
|
// An extremely simple external store
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
export default writable(0)
|
||||||
|
```
|
||||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>FlexDXCluster2</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="./src/main.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
38
frontend/jsconfig.json
Normal file
38
frontend/jsconfig.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
/**
|
||||||
|
* svelte-preprocess cannot figure out whether you have
|
||||||
|
* a value or a type, so tell TypeScript to enforce using
|
||||||
|
* `import type` instead of `import` for Types.
|
||||||
|
*/
|
||||||
|
"importsNotUsedAsValues": "error",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
/**
|
||||||
|
* To have warnings / errors of the Svelte compiler at the
|
||||||
|
* correct position, enable source maps by default.
|
||||||
|
*/
|
||||||
|
"sourceMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
/**
|
||||||
|
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||||
|
* Disable this if you'd like to use dynamic types.
|
||||||
|
*/
|
||||||
|
"checkJs": true
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Use global.d.ts instead of compilerOptions.types
|
||||||
|
* to avoid limiting type declarations.
|
||||||
|
*/
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.js",
|
||||||
|
"src/**/*.svelte"
|
||||||
|
]
|
||||||
|
}
|
||||||
781
frontend/package-lock.json
generated
Normal file
781
frontend/package-lock.json
generated
Normal file
@@ -0,0 +1,781 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||||
|
"svelte": "^3.49.0",
|
||||||
|
"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/@sveltejs/vite-plugin-svelte": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-6QupI/jemMfK+yI2pMtJcu5iO2gtgTfcBdGwMZZt+lgbFELhszbDl6Qjh000HgAV8+XUA+8EY8DusOFk8WhOIg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"kleur": "^4.1.5",
|
||||||
|
"magic-string": "^0.26.7",
|
||||||
|
"svelte-hmr": "^0.15.1",
|
||||||
|
"vitefu": "^0.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.18.0 || >= 16"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^3.44.0",
|
||||||
|
"vite": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/deepmerge": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-core-module": {
|
||||||
|
"version": "2.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
|
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/kleur": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/magic-string": {
|
||||||
|
"version": "0.26.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz",
|
||||||
|
"integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sourcemap-codec": "^1.4.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"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.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||||
|
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||||
|
"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.11",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve": {
|
||||||
|
"version": "1.22.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
|
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"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/sourcemap-codec": {
|
||||||
|
"version": "1.4.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||||
|
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
|
||||||
|
"deprecated": "Please use @jridgewell/sourcemap-codec instead",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"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/svelte": {
|
||||||
|
"version": "3.59.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz",
|
||||||
|
"integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svelte-hmr": {
|
||||||
|
"version": "0.15.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
||||||
|
"integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20 || ^14.13.1 || >= 16"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^3.19.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vitefu": {
|
||||||
|
"version": "0.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
|
||||||
|
"integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
frontend/package.json
Normal file
16
frontend/package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||||
|
"svelte": "^3.49.0",
|
||||||
|
"vite": "^3.0.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/package.json.md5
Normal file
1
frontend/package.json.md5
Normal file
@@ -0,0 +1 @@
|
|||||||
|
d9dc84f0d17ed164f36dd584057aae68
|
||||||
79
frontend/src/App.svelte
Normal file
79
frontend/src/App.svelte
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<script>
|
||||||
|
import logo from './assets/images/logo-universal.png'
|
||||||
|
import {Greet} from '../wailsjs/go/main/App.js'
|
||||||
|
|
||||||
|
let resultText = "Please enter your name below 👇"
|
||||||
|
let name
|
||||||
|
|
||||||
|
function greet() {
|
||||||
|
Greet(name).then(result => resultText = result)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<img alt="Wails logo" id="logo" src="{logo}">
|
||||||
|
<div class="result" id="result">{resultText}</div>
|
||||||
|
<div class="input-box" id="input">
|
||||||
|
<input autocomplete="off" bind:value={name} class="input" id="name" type="text"/>
|
||||||
|
<button class="btn" on:click={greet}>Greet</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
93
frontend/src/assets/fonts/OFL.txt
Normal file
93
frontend/src/assets/fonts/OFL.txt
Normal file
@@ -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.
|
||||||
BIN
frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
BIN
frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/images/logo-universal.png
Normal file
BIN
frontend/src/assets/images/logo-universal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
8
frontend/src/main.js
Normal file
8
frontend/src/main.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import './style.css'
|
||||||
|
import App from './App.svelte'
|
||||||
|
|
||||||
|
const app = new App({
|
||||||
|
target: document.getElementById('app')
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app
|
||||||
26
frontend/src/style.css
Normal file
26
frontend/src/style.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
html {
|
||||||
|
background-color: rgba(27, 38, 54, 1);
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
color: white;
|
||||||
|
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||||
|
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Nunito";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local(""),
|
||||||
|
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
2
frontend/src/vite-env.d.ts
vendored
Normal file
2
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="svelte" />
|
||||||
|
/// <reference types="vite/client" />
|
||||||
7
frontend/vite.config.js
Normal file
7
frontend/vite.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import {defineConfig} from 'vite'
|
||||||
|
import {svelte} from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [svelte()]
|
||||||
|
})
|
||||||
4
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
4
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function Greet(arg1:string):Promise<string>;
|
||||||
7
frontend/wailsjs/go/main/App.js
Normal file
7
frontend/wailsjs/go/main/App.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function Greet(arg1) {
|
||||||
|
return window['go']['main']['App']['Greet'](arg1);
|
||||||
|
}
|
||||||
24
frontend/wailsjs/runtime/package.json
Normal file
24
frontend/wailsjs/runtime/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
249
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
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
|
||||||
242
frontend/wailsjs/runtime/runtime.js
Normal file
242
frontend/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
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);
|
||||||
|
}
|
||||||
132
internal/bands/bands.go
Normal file
132
internal/bands/bands.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package bands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BandDefinition struct {
|
||||||
|
Name string
|
||||||
|
MinFreqMHz float64
|
||||||
|
MaxFreqMHz float64
|
||||||
|
UseUSB bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var AmateurBands = []BandDefinition{
|
||||||
|
{Name: "160M", MinFreqMHz: 1.800, MaxFreqMHz: 2.000, UseUSB: false},
|
||||||
|
{Name: "80M", MinFreqMHz: 3.500, MaxFreqMHz: 3.800, UseUSB: false},
|
||||||
|
{Name: "60M", MinFreqMHz: 5.330, MaxFreqMHz: 5.405, UseUSB: false},
|
||||||
|
{Name: "40M", MinFreqMHz: 7.000, MaxFreqMHz: 7.300, UseUSB: false},
|
||||||
|
{Name: "30M", MinFreqMHz: 10.100, MaxFreqMHz: 10.150, UseUSB: true},
|
||||||
|
{Name: "20M", MinFreqMHz: 14.000, MaxFreqMHz: 14.350, UseUSB: true},
|
||||||
|
{Name: "17M", MinFreqMHz: 18.068, MaxFreqMHz: 18.168, UseUSB: true},
|
||||||
|
{Name: "15M", MinFreqMHz: 21.000, MaxFreqMHz: 21.450, UseUSB: true},
|
||||||
|
{Name: "12M", MinFreqMHz: 24.890, MaxFreqMHz: 24.990, UseUSB: true},
|
||||||
|
{Name: "10M", MinFreqMHz: 28.000, MaxFreqMHz: 29.700, UseUSB: true},
|
||||||
|
{Name: "6M", MinFreqMHz: 50.000, MaxFreqMHz: 54.000, UseUSB: true},
|
||||||
|
{Name: "QO-100", MinFreqMHz: 10489.500, MaxFreqMHz: 10490.000, UseUSB: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
func FrequencyToBand(freqMHz float64) string {
|
||||||
|
for _, band := range AmateurBands {
|
||||||
|
if freqMHz >= band.MinFreqMHz && freqMHz < band.MaxFreqMHz {
|
||||||
|
return band.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "N/A"
|
||||||
|
}
|
||||||
|
|
||||||
|
// FrequencyKHzToBand convertit depuis kHz (format interne du projet)
|
||||||
|
func FrequencyKHzToBand(freqKHz float64) string {
|
||||||
|
return FrequencyToBand(freqKHz / 1000.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FrequencyStringToBand(freqStr string) string {
|
||||||
|
freqMHz, err := strconv.ParseFloat(freqStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "N/A"
|
||||||
|
}
|
||||||
|
return FrequencyToBand(freqMHz)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBandDefinition(bandName string) *BandDefinition {
|
||||||
|
for _, band := range AmateurBands {
|
||||||
|
if band.Name == bandName {
|
||||||
|
return &band
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsUSBBand(bandName string) bool {
|
||||||
|
band := GetBandDefinition(bandName)
|
||||||
|
if band == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return band.UseUSB
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsLSBBand(bandName string) bool {
|
||||||
|
band := GetBandDefinition(bandName)
|
||||||
|
if band == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !band.UseUSB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizeSSBMode(mode string, band string) string {
|
||||||
|
if mode != "SSB" {
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
if IsUSBBand(band) {
|
||||||
|
return "USB"
|
||||||
|
}
|
||||||
|
return "LSB"
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizeSSBModeByFrequency(mode string, freqMHz float64) string {
|
||||||
|
if mode != "SSB" {
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
band := FrequencyToBand(freqMHz)
|
||||||
|
return NormalizeSSBMode(mode, band)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllBandNames() []string {
|
||||||
|
names := make([]string, len(AmateurBands))
|
||||||
|
for i, band := range AmateurBands {
|
||||||
|
names[i] = band.Name
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsBandValid(bandName string) bool {
|
||||||
|
return GetBandDefinition(bandName) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBandFrequencyRange(bandName string) string {
|
||||||
|
band := GetBandDefinition(bandName)
|
||||||
|
if band == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.3f - %.3f MHz", band.MinFreqMHz, band.MaxFreqMHz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreqMHzString formate une fréquence kHz en string MHz pour FlexRadio
|
||||||
|
func FreqMHzString(freqKHz float64) string {
|
||||||
|
return fmt.Sprintf("%.6f", freqKHz/1000.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBandFromFrequencyString gère kHz et MHz automatiquement
|
||||||
|
func GetBandFromFrequencyString(freqStr string) string {
|
||||||
|
freqStr = strings.TrimSpace(freqStr)
|
||||||
|
freqMHz, err := strconv.ParseFloat(freqStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "N/A"
|
||||||
|
}
|
||||||
|
if freqMHz > 1000 {
|
||||||
|
freqMHz = freqMHz / 1000.0
|
||||||
|
}
|
||||||
|
return FrequencyToBand(freqMHz)
|
||||||
|
}
|
||||||
332
internal/cluster/client.go
Normal file
332
internal/cluster/client.go
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/user/flexdxcluster2/internal/spot"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MaxReconnectAttempts = 10
|
||||||
|
BaseReconnectDelay = 1 * time.Second
|
||||||
|
MaxReconnectDelay = 60 * time.Second
|
||||||
|
ConnectionTimeout = 10 * time.Second
|
||||||
|
ReadTimeout = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config reprend ClusterConfig de l'ancien code
|
||||||
|
type Config struct {
|
||||||
|
Name string
|
||||||
|
Server string
|
||||||
|
Port string
|
||||||
|
Login string
|
||||||
|
Password string
|
||||||
|
Skimmer bool
|
||||||
|
FT8 bool
|
||||||
|
FT4 bool
|
||||||
|
Beacon bool
|
||||||
|
Command string
|
||||||
|
LoginPrompt string
|
||||||
|
Enabled bool
|
||||||
|
Master bool
|
||||||
|
Type string // dxspider, cc_cluster, ar_cluster — vide = auto
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client gère la connexion TCP à un cluster DX
|
||||||
|
type Client struct {
|
||||||
|
cfg Config
|
||||||
|
conn net.Conn
|
||||||
|
reader *bufio.Reader
|
||||||
|
writer *bufio.Writer
|
||||||
|
mu sync.Mutex
|
||||||
|
loggedIn bool
|
||||||
|
clusterType string
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
reconnects int
|
||||||
|
|
||||||
|
// SpotChan reçoit les spots parsés — consommé par le SpotProcessor
|
||||||
|
SpotChan chan *spot.Spot
|
||||||
|
// ConsoleChan reçoit toutes les lignes brutes — pour l'onglet Console
|
||||||
|
ConsoleChan chan string
|
||||||
|
// CmdChan reçoit les commandes à envoyer au cluster depuis l'UI
|
||||||
|
CmdChan chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg Config) *Client {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
return &Client{
|
||||||
|
cfg: cfg,
|
||||||
|
clusterType: cfg.Type,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
SpotChan: make(chan *spot.Spot, 200),
|
||||||
|
ConsoleChan: make(chan string, 200),
|
||||||
|
CmdChan: make(chan string, 100),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Name() string { return c.cfg.Name }
|
||||||
|
func (c *Client) IsMaster() bool { return c.cfg.Master }
|
||||||
|
func (c *Client) ClusterType() string { return c.clusterType }
|
||||||
|
|
||||||
|
// Start démarre le client avec reconnexion automatique
|
||||||
|
func (c *Client) Start() {
|
||||||
|
// Goroutine commandes UI → cluster
|
||||||
|
go c.commandLoop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.connect(); err != nil {
|
||||||
|
c.reconnects++
|
||||||
|
if c.reconnects >= MaxReconnectAttempts {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delay := c.backoff()
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(delay):
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.readLoop()
|
||||||
|
c.loggedIn = false
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Close() {
|
||||||
|
c.cancel()
|
||||||
|
if c.conn != nil {
|
||||||
|
c.write([]byte("bye\r\n"))
|
||||||
|
c.conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReloadFilters renvoie les commandes de filtres au cluster
|
||||||
|
func (c *Client) ReloadFilters() {
|
||||||
|
if c.loggedIn {
|
||||||
|
c.setFilters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCommand envoie une commande arbitraire au cluster depuis l'UI
|
||||||
|
func (c *Client) SendCommand(cmd string) {
|
||||||
|
select {
|
||||||
|
case c.CmdChan <- cmd:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) connect() error {
|
||||||
|
addr := c.cfg.Server + ":" + c.cfg.Port
|
||||||
|
conn, err := net.DialTimeout("tcp", addr, ConnectionTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect %s: %w", addr, err)
|
||||||
|
}
|
||||||
|
c.conn = conn
|
||||||
|
c.reader = bufio.NewReader(conn)
|
||||||
|
c.writer = bufio.NewWriter(conn)
|
||||||
|
c.loggedIn = false
|
||||||
|
c.reconnects = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) commandLoop() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
case cmd := <-c.CmdChan:
|
||||||
|
c.write([]byte(cmd + "\r\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) readLoop() {
|
||||||
|
defer c.conn.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.loggedIn {
|
||||||
|
c.conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||||
|
msg, err := c.reader.ReadBytes(':')
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.conn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
line := string(msg)
|
||||||
|
c.detectType(line)
|
||||||
|
c.sendConsole(line)
|
||||||
|
|
||||||
|
if strings.Contains(line, c.cfg.LoginPrompt) || strings.Contains(line, "login:") {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
c.write([]byte(c.cfg.Login + "\n\r"))
|
||||||
|
c.loggedIn = true
|
||||||
|
go func() {
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
c.setFilters()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn.SetReadDeadline(time.Now().Add(ReadTimeout))
|
||||||
|
msg, err := c.reader.ReadBytes('\n')
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.conn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
line := string(msg)
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.clusterType == "" {
|
||||||
|
c.detectType(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(line, "password") {
|
||||||
|
c.write([]byte(c.cfg.Password + "\r\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(line, "Hello") || strings.Contains(line, "Welcome") {
|
||||||
|
if c.cfg.Command != "" {
|
||||||
|
c.write([]byte(c.cfg.Command + "\n\r"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tenter de parser comme spot DX
|
||||||
|
isDX := strings.Contains(line, "DX de ") || spot.ShortSpotDetectRe.MatchString(line)
|
||||||
|
if isDX && !c.shouldSkip(line) {
|
||||||
|
if parsed := spot.ParseLine(line, c.cfg.Name); parsed != nil {
|
||||||
|
select {
|
||||||
|
case c.SpotChan <- parsed:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Console — marquer les messages "To ALL"
|
||||||
|
consoleMsg := line
|
||||||
|
if strings.HasPrefix(strings.TrimSpace(line), "To ALL de ") {
|
||||||
|
consoleMsg = "TO_ALL:" + strings.TrimSpace(line)
|
||||||
|
}
|
||||||
|
c.sendConsole(consoleMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldSkip applique le filtre applicatif selon la config du cluster
|
||||||
|
func (c *Client) shouldSkip(line string) bool {
|
||||||
|
upper := strings.ToUpper(line)
|
||||||
|
if strings.Contains(upper, "FT8") && !c.cfg.FT8 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.Contains(upper, "FT4") && !c.cfg.FT4 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (strings.Contains(upper, "CW SKIMMER") || strings.Contains(upper, "SKIMMER")) && !c.cfg.Skimmer {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.Contains(upper, "BEACON") && !c.cfg.Beacon {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) detectType(line string) {
|
||||||
|
if c.cfg.Type != "" {
|
||||||
|
c.clusterType = c.cfg.Type
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(line)
|
||||||
|
switch {
|
||||||
|
case strings.Contains(lower, "dxspider"):
|
||||||
|
c.clusterType = "dxspider"
|
||||||
|
case strings.Contains(lower, "cc cluster") || strings.Contains(lower, "cc-cluster"):
|
||||||
|
c.clusterType = "cc_cluster"
|
||||||
|
case strings.Contains(lower, "ar-cluster") || strings.Contains(lower, "arcluster"):
|
||||||
|
c.clusterType = "ar_cluster"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) setFilters() {
|
||||||
|
switch c.clusterType {
|
||||||
|
case "dxspider":
|
||||||
|
c.setFiltersDXSpider()
|
||||||
|
case "ar_cluster":
|
||||||
|
c.setFiltersAR()
|
||||||
|
default:
|
||||||
|
c.setFiltersCC()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) setFiltersCC() {
|
||||||
|
if c.cfg.FT8 { c.write([]byte("set/ft8\r\n")) } else { c.write([]byte("set/noft8\r\n")) }
|
||||||
|
if c.cfg.FT4 { c.write([]byte("set/ft4\r\n")) } else { c.write([]byte("set/noft4\r\n")) }
|
||||||
|
if c.cfg.Skimmer { c.write([]byte("set/skimmer\r\n")) } else { c.write([]byte("set/noskimmer\r\n")) }
|
||||||
|
if c.cfg.Beacon { c.write([]byte("set/beacon\r\n")) } else { c.write([]byte("set/nobeacon\r\n")) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) setFiltersDXSpider() {
|
||||||
|
if c.cfg.Skimmer && c.cfg.FT8 && c.cfg.FT4 {
|
||||||
|
c.write([]byte("SET/SKIMMER CW FT8 FT4\r\n"))
|
||||||
|
} else if c.cfg.Skimmer && c.cfg.FT8 {
|
||||||
|
c.write([]byte("SET/SKIMMER CW FT8\r\n"))
|
||||||
|
} else if c.cfg.Skimmer {
|
||||||
|
c.write([]byte("SET/SKIMMER CW\r\n"))
|
||||||
|
} else {
|
||||||
|
c.write([]byte("UNSET/SKIMMER\r\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) setFiltersAR() {
|
||||||
|
if c.cfg.FT8 { c.write([]byte("set/ft8\r\n")) } else { c.write([]byte("set/noft8\r\n")) }
|
||||||
|
if c.cfg.FT4 { c.write([]byte("set/ft4\r\n")) } else { c.write([]byte("set/noft4\r\n")) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) write(data []byte) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
if c.conn == nil || c.writer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.writer.Write(data)
|
||||||
|
c.writer.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) sendConsole(line string) {
|
||||||
|
select {
|
||||||
|
case c.ConsoleChan <- line:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) backoff() time.Duration {
|
||||||
|
d := time.Duration(float64(BaseReconnectDelay) * math.Pow(2, float64(c.reconnects)))
|
||||||
|
if d > MaxReconnectDelay {
|
||||||
|
return MaxReconnectDelay
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
103
internal/db/db.go
Normal file
103
internal/db/db.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB est le wrapper principal autour de la connexion SQLite
|
||||||
|
type DB struct {
|
||||||
|
conn *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open ouvre (ou crée) la base SQLite et applique les migrations
|
||||||
|
func Open(path string) (*DB, error) {
|
||||||
|
conn, err := sql.Open("sqlite3", path+"?_journal_mode=WAL&_busy_timeout=5000")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open db: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.SetMaxOpenConns(1)
|
||||||
|
conn.SetMaxIdleConns(1)
|
||||||
|
|
||||||
|
db := &DB{conn: conn}
|
||||||
|
if err := db.migrate(); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("migrate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Close() error {
|
||||||
|
return db.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Conn() *sql.DB {
|
||||||
|
return db.conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate crée les tables si elles n'existent pas
|
||||||
|
func (db *DB) migrate() error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
_, err := db.conn.ExecContext(ctx, `
|
||||||
|
CREATE TABLE IF NOT EXISTS spots (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
dx TEXT NOT NULL,
|
||||||
|
spotter TEXT NOT NULL,
|
||||||
|
frequency_khz REAL NOT NULL,
|
||||||
|
frequency_mhz TEXT NOT NULL,
|
||||||
|
band TEXT NOT NULL,
|
||||||
|
mode TEXT NOT NULL,
|
||||||
|
comment TEXT DEFAULT '',
|
||||||
|
original_comment TEXT DEFAULT '',
|
||||||
|
time TEXT DEFAULT '',
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
dxcc TEXT DEFAULT '',
|
||||||
|
country_name TEXT DEFAULT '',
|
||||||
|
new_dxcc INTEGER DEFAULT 0,
|
||||||
|
new_band INTEGER DEFAULT 0,
|
||||||
|
new_mode INTEGER DEFAULT 0,
|
||||||
|
new_slot INTEGER DEFAULT 0,
|
||||||
|
callsign_worked INTEGER DEFAULT 0,
|
||||||
|
in_watchlist INTEGER DEFAULT 0,
|
||||||
|
pota_ref TEXT DEFAULT '',
|
||||||
|
sota_ref TEXT DEFAULT '',
|
||||||
|
park_name TEXT DEFAULT '',
|
||||||
|
summit_name TEXT DEFAULT '',
|
||||||
|
flex_spot_number INTEGER DEFAULT 0,
|
||||||
|
command_number INTEGER DEFAULT 0,
|
||||||
|
color TEXT DEFAULT '#ffeaeaea',
|
||||||
|
background_color TEXT DEFAULT '#ff000000',
|
||||||
|
priority TEXT DEFAULT '5',
|
||||||
|
life_time TEXT DEFAULT '900',
|
||||||
|
cluster_name TEXT DEFAULT '',
|
||||||
|
source INTEGER DEFAULT 0
|
||||||
|
)`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create spots table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index pour les recherches fréquentes
|
||||||
|
_, err = db.conn.ExecContext(ctx, `
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_spots_dx_band ON spots(dx, band);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_spots_timestamp ON spots(timestamp);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_spots_flex_number ON spots(flex_spot_number);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_spots_command_number ON spots(command_number);
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create indexes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAll supprime tous les spots (appelé au démarrage comme l'ancien code)
|
||||||
|
func (db *DB) DeleteAll(ctx context.Context) error {
|
||||||
|
_, err := db.conn.ExecContext(ctx, `DELETE FROM spots`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
214
internal/db/spots_store.go
Normal file
214
internal/db/spots_store.go
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/user/flexdxcluster2/internal/spot"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpotsStore struct {
|
||||||
|
db *DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSpotsStore(db *DB) *SpotsStore {
|
||||||
|
return &SpotsStore{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create insère un nouveau spot et retourne son ID
|
||||||
|
func (s *SpotsStore) Create(ctx context.Context, sp spot.Spot) (int64, error) {
|
||||||
|
res, err := s.db.conn.ExecContext(ctx, `
|
||||||
|
INSERT INTO spots (
|
||||||
|
dx, spotter, frequency_khz, frequency_mhz, band, mode,
|
||||||
|
comment, original_comment, time, timestamp,
|
||||||
|
dxcc, country_name,
|
||||||
|
new_dxcc, new_band, new_mode, new_slot, callsign_worked, in_watchlist,
|
||||||
|
pota_ref, sota_ref, park_name, summit_name,
|
||||||
|
flex_spot_number, command_number,
|
||||||
|
color, background_color, priority, life_time,
|
||||||
|
cluster_name, source
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||||
|
sp.DX, sp.Spotter, sp.FrequencyKHz, sp.FrequencyMHz, sp.Band, sp.Mode,
|
||||||
|
sp.Comment, sp.OriginalComment, sp.Time, sp.Timestamp,
|
||||||
|
sp.DXCC, sp.CountryName,
|
||||||
|
boolToInt(sp.NewDXCC), boolToInt(sp.NewBand), boolToInt(sp.NewMode),
|
||||||
|
boolToInt(sp.NewSlot), boolToInt(sp.CallsignWorked), boolToInt(sp.InWatchlist),
|
||||||
|
sp.POTARef, sp.SOTARef, sp.ParkName, sp.SummitName,
|
||||||
|
sp.FlexSpotNumber, sp.CommandNumber,
|
||||||
|
sp.Color, sp.BackgroundColor, sp.Priority, sp.LifeTime,
|
||||||
|
sp.ClusterName, int(sp.Source),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("create spot: %w", err)
|
||||||
|
}
|
||||||
|
return res.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll retourne tous les spots, optionnellement filtrés par bande
|
||||||
|
func (s *SpotsStore) GetAll(ctx context.Context, band string) ([]spot.Spot, error) {
|
||||||
|
var rows *sql.Rows
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if band == "" || band == "0" || band == "ALL" {
|
||||||
|
rows, err = s.db.conn.QueryContext(ctx,
|
||||||
|
`SELECT * FROM spots ORDER BY timestamp DESC`)
|
||||||
|
} else {
|
||||||
|
rows, err = s.db.conn.QueryContext(ctx,
|
||||||
|
`SELECT * FROM spots WHERE band = ? ORDER BY timestamp DESC`, band)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
return scanSpots(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByDXAndBand cherche un spot existant pour le même DX sur la même bande
|
||||||
|
func (s *SpotsStore) FindByDXAndBand(ctx context.Context, dx, band string) (*spot.Spot, error) {
|
||||||
|
row := s.db.conn.QueryRowContext(ctx,
|
||||||
|
`SELECT * FROM spots WHERE dx = ? AND band = ? ORDER BY timestamp DESC LIMIT 1`,
|
||||||
|
dx, band)
|
||||||
|
sp, err := scanSpot(row)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return sp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByCommandNumber cherche un spot par son numéro de commande Flex
|
||||||
|
func (s *SpotsStore) FindByCommandNumber(ctx context.Context, cmdNum int) (*spot.Spot, error) {
|
||||||
|
row := s.db.conn.QueryRowContext(ctx,
|
||||||
|
`SELECT * FROM spots WHERE command_number = ? LIMIT 1`, cmdNum)
|
||||||
|
sp, err := scanSpot(row)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return sp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByFlexSpotNumber cherche un spot par son numéro de spot Flex
|
||||||
|
func (s *SpotsStore) FindByFlexSpotNumber(ctx context.Context, flexNum int) (*spot.Spot, error) {
|
||||||
|
row := s.db.conn.QueryRowContext(ctx,
|
||||||
|
`SELECT * FROM spots WHERE flex_spot_number = ? LIMIT 1`, flexNum)
|
||||||
|
sp, err := scanSpot(row)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return sp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFlexSpotNumber met à jour le numéro de spot Flex après confirmation du Flex
|
||||||
|
func (s *SpotsStore) UpdateFlexSpotNumber(ctx context.Context, cmdNum, flexNum int) error {
|
||||||
|
_, err := s.db.conn.ExecContext(ctx,
|
||||||
|
`UPDATE spots SET flex_spot_number = ? WHERE command_number = ?`,
|
||||||
|
flexNum, cmdNum)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByFlexSpotNumber supprime un spot par son numéro Flex
|
||||||
|
func (s *SpotsStore) DeleteByFlexSpotNumber(ctx context.Context, flexNum int) error {
|
||||||
|
_, err := s.db.conn.ExecContext(ctx,
|
||||||
|
`DELETE FROM spots WHERE flex_spot_number = ?`, flexNum)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByID supprime un spot par son ID
|
||||||
|
func (s *SpotsStore) DeleteByID(ctx context.Context, id int64) error {
|
||||||
|
_, err := s.db.conn.ExecContext(ctx,
|
||||||
|
`DELETE FROM spots WHERE id = ?`, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteExpired supprime les spots expirés selon leur lifetime
|
||||||
|
func (s *SpotsStore) DeleteExpired(ctx context.Context, lifetimeSeconds int64) ([]spot.Spot, error) {
|
||||||
|
cutoff := time.Now().Unix() - lifetimeSeconds
|
||||||
|
rows, err := s.db.conn.QueryContext(ctx,
|
||||||
|
`SELECT * FROM spots WHERE timestamp < ?`, cutoff)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
expired, err := scanSpots(rows)
|
||||||
|
rows.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.db.conn.ExecContext(ctx,
|
||||||
|
`DELETE FROM spots WHERE timestamp < ?`, cutoff)
|
||||||
|
return expired, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
func boolToInt(b bool) int {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanSpots(rows *sql.Rows) ([]spot.Spot, error) {
|
||||||
|
var spots []spot.Spot
|
||||||
|
for rows.Next() {
|
||||||
|
sp, err := scanSpotFromRows(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
spots = append(spots, *sp)
|
||||||
|
}
|
||||||
|
return spots, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanSpot(row *sql.Row) (*spot.Spot, error) {
|
||||||
|
var sp spot.Spot
|
||||||
|
var newDXCC, newBand, newMode, newSlot, worked, inWatchlist, source int
|
||||||
|
err := row.Scan(
|
||||||
|
&sp.ID, &sp.DX, &sp.Spotter, &sp.FrequencyKHz, &sp.FrequencyMHz,
|
||||||
|
&sp.Band, &sp.Mode, &sp.Comment, &sp.OriginalComment, &sp.Time, &sp.Timestamp,
|
||||||
|
&sp.DXCC, &sp.CountryName,
|
||||||
|
&newDXCC, &newBand, &newMode, &newSlot, &worked, &inWatchlist,
|
||||||
|
&sp.POTARef, &sp.SOTARef, &sp.ParkName, &sp.SummitName,
|
||||||
|
&sp.FlexSpotNumber, &sp.CommandNumber,
|
||||||
|
&sp.Color, &sp.BackgroundColor, &sp.Priority, &sp.LifeTime,
|
||||||
|
&sp.ClusterName, &source,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sp.NewDXCC = newDXCC == 1
|
||||||
|
sp.NewBand = newBand == 1
|
||||||
|
sp.NewMode = newMode == 1
|
||||||
|
sp.NewSlot = newSlot == 1
|
||||||
|
sp.CallsignWorked = worked == 1
|
||||||
|
sp.InWatchlist = inWatchlist == 1
|
||||||
|
sp.Source = spot.SpotSource(source)
|
||||||
|
return &sp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanSpotFromRows(rows *sql.Rows) (*spot.Spot, error) {
|
||||||
|
var sp spot.Spot
|
||||||
|
var newDXCC, newBand, newMode, newSlot, worked, inWatchlist, source int
|
||||||
|
err := rows.Scan(
|
||||||
|
&sp.ID, &sp.DX, &sp.Spotter, &sp.FrequencyKHz, &sp.FrequencyMHz,
|
||||||
|
&sp.Band, &sp.Mode, &sp.Comment, &sp.OriginalComment, &sp.Time, &sp.Timestamp,
|
||||||
|
&sp.DXCC, &sp.CountryName,
|
||||||
|
&newDXCC, &newBand, &newMode, &newSlot, &worked, &inWatchlist,
|
||||||
|
&sp.POTARef, &sp.SOTARef, &sp.ParkName, &sp.SummitName,
|
||||||
|
&sp.FlexSpotNumber, &sp.CommandNumber,
|
||||||
|
&sp.Color, &sp.BackgroundColor, &sp.Priority, &sp.LifeTime,
|
||||||
|
&sp.ClusterName, &source,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sp.NewDXCC = newDXCC == 1
|
||||||
|
sp.NewBand = newBand == 1
|
||||||
|
sp.NewMode = newMode == 1
|
||||||
|
sp.NewSlot = newSlot == 1
|
||||||
|
sp.CallsignWorked = worked == 1
|
||||||
|
sp.InWatchlist = inWatchlist == 1
|
||||||
|
sp.Source = spot.SpotSource(source)
|
||||||
|
return &sp, nil
|
||||||
|
}
|
||||||
229
internal/modes/modes.go
Normal file
229
internal/modes/modes.go
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
package modes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/user/flexdxcluster2/internal/bands"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModeRange struct {
|
||||||
|
MinFreqMHz float64
|
||||||
|
MaxFreqMHz float64
|
||||||
|
Mode string
|
||||||
|
}
|
||||||
|
|
||||||
|
var BandModeRanges = map[string][]ModeRange{
|
||||||
|
"160M": {
|
||||||
|
{1.800, 1.838, "CW"},
|
||||||
|
{1.838, 1.843, "FT8"},
|
||||||
|
{1.843, 2.000, "LSB"},
|
||||||
|
},
|
||||||
|
"80M": {
|
||||||
|
{3.500, 3.560, "CW"},
|
||||||
|
{3.560, 3.575, "FT8"},
|
||||||
|
{3.575, 3.578, "FT4"},
|
||||||
|
{3.578, 3.590, "RTTY"},
|
||||||
|
{3.590, 3.800, "LSB"},
|
||||||
|
},
|
||||||
|
"60M": {
|
||||||
|
{5.330, 5.357, "CW"},
|
||||||
|
{5.357, 5.359, "FT8"},
|
||||||
|
{5.359, 5.405, "USB"},
|
||||||
|
},
|
||||||
|
"40M": {
|
||||||
|
{7.000, 7.040, "CW"},
|
||||||
|
{7.040, 7.047, "RTTY"},
|
||||||
|
{7.047, 7.050, "FT4"},
|
||||||
|
{7.050, 7.100, "FT8"},
|
||||||
|
{7.100, 7.300, "LSB"},
|
||||||
|
},
|
||||||
|
"30M": {
|
||||||
|
{10.100, 10.130, "CW"},
|
||||||
|
{10.130, 10.142, "FT8"},
|
||||||
|
{10.142, 10.150, "FT4"},
|
||||||
|
},
|
||||||
|
"20M": {
|
||||||
|
{14.000, 14.070, "CW"},
|
||||||
|
{14.070, 14.078, "FT8"},
|
||||||
|
{14.078, 14.083, "FT4"},
|
||||||
|
{14.083, 14.100, "FT8"},
|
||||||
|
{14.100, 14.112, "RTTY"},
|
||||||
|
{14.112, 14.350, "USB"},
|
||||||
|
},
|
||||||
|
"17M": {
|
||||||
|
{18.068, 18.090, "CW"},
|
||||||
|
{18.090, 18.104, "FT8"},
|
||||||
|
{18.104, 18.106, "FT4"},
|
||||||
|
{18.106, 18.110, "FT8"},
|
||||||
|
{18.110, 18.168, "USB"},
|
||||||
|
},
|
||||||
|
"15M": {
|
||||||
|
{21.000, 21.070, "CW"},
|
||||||
|
{21.070, 21.100, "FT8"},
|
||||||
|
{21.100, 21.130, "RTTY"},
|
||||||
|
{21.130, 21.143, "FT4"},
|
||||||
|
{21.143, 21.450, "USB"},
|
||||||
|
},
|
||||||
|
"12M": {
|
||||||
|
{24.890, 24.910, "CW"},
|
||||||
|
{24.910, 24.918, "FT8"},
|
||||||
|
{24.918, 24.930, "FT4"},
|
||||||
|
{24.930, 24.990, "USB"},
|
||||||
|
},
|
||||||
|
"10M": {
|
||||||
|
{28.000, 28.070, "CW"},
|
||||||
|
{28.070, 28.110, "FT8"},
|
||||||
|
{28.110, 28.179, "RTTY"},
|
||||||
|
{28.179, 28.190, "FT4"},
|
||||||
|
{28.190, 29.000, "USB"},
|
||||||
|
{29.000, 29.700, "FM"},
|
||||||
|
},
|
||||||
|
"6M": {
|
||||||
|
{50.000, 50.100, "CW"},
|
||||||
|
{50.100, 50.313, "USB"},
|
||||||
|
{50.313, 50.318, "FT8"},
|
||||||
|
{50.318, 50.323, "FT4"},
|
||||||
|
{50.323, 51.000, "USB"},
|
||||||
|
{51.000, 54.000, "FM"},
|
||||||
|
},
|
||||||
|
"QO-100": {
|
||||||
|
{10489.500, 10489.540, "CW"},
|
||||||
|
{10489.540, 10489.650, "FT8"},
|
||||||
|
{10489.650, 10489.990, "USB"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func GuessMode(freqMHz float64) string {
|
||||||
|
band := bands.FrequencyToBand(freqMHz)
|
||||||
|
if band == "N/A" {
|
||||||
|
if freqMHz < 10.0 {
|
||||||
|
return "LSB"
|
||||||
|
}
|
||||||
|
return "USB"
|
||||||
|
}
|
||||||
|
return GuessModeForBand(freqMHz, band)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GuessModeForBand(freqMHz float64, band string) string {
|
||||||
|
ranges, exists := BandModeRanges[band]
|
||||||
|
if !exists {
|
||||||
|
if bands.IsUSBBand(band) {
|
||||||
|
return "USB"
|
||||||
|
}
|
||||||
|
return "LSB"
|
||||||
|
}
|
||||||
|
for _, r := range ranges {
|
||||||
|
if freqMHz >= r.MinFreqMHz && freqMHz < r.MaxFreqMHz {
|
||||||
|
return r.Mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bands.IsUSBBand(band) {
|
||||||
|
return "USB"
|
||||||
|
}
|
||||||
|
return "LSB"
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractModeFromComment(comment string) string {
|
||||||
|
if comment == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
commentUpper := strings.ToUpper(comment)
|
||||||
|
|
||||||
|
if strings.Contains(commentUpper, "FT8") ||
|
||||||
|
(strings.Contains(commentUpper, "DB") && strings.Contains(commentUpper, "HZ")) {
|
||||||
|
return "FT8"
|
||||||
|
}
|
||||||
|
if strings.Contains(commentUpper, "FT4") {
|
||||||
|
return "FT4"
|
||||||
|
}
|
||||||
|
if strings.Contains(commentUpper, "WPM") || strings.Contains(commentUpper, " CW ") ||
|
||||||
|
strings.HasSuffix(commentUpper, "CW") || strings.HasPrefix(commentUpper, "CW ") {
|
||||||
|
return "CW"
|
||||||
|
}
|
||||||
|
|
||||||
|
digitalModes := []string{"RTTY", "PSK31", "PSK63", "PSK", "MFSK", "OLIVIA", "JT65", "JT9"}
|
||||||
|
for _, mode := range digitalModes {
|
||||||
|
if strings.Contains(commentUpper, mode) {
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
voiceModes := []string{"USB", "LSB", "SSB", "FM", "AM"}
|
||||||
|
for _, mode := range voiceModes {
|
||||||
|
if strings.Contains(commentUpper, " "+mode+" ") ||
|
||||||
|
strings.HasPrefix(commentUpper, mode+" ") ||
|
||||||
|
strings.HasSuffix(commentUpper, " "+mode) ||
|
||||||
|
commentUpper == mode {
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetermineMode — priorité : mode explicite > commentaire > fréquence
|
||||||
|
func DetermineMode(explicitMode string, comment string, freqMHz float64) string {
|
||||||
|
if explicitMode != "" {
|
||||||
|
explicitMode = strings.ToUpper(explicitMode)
|
||||||
|
if explicitMode == "SSB" {
|
||||||
|
return bands.NormalizeSSBModeByFrequency(explicitMode, freqMHz)
|
||||||
|
}
|
||||||
|
return explicitMode
|
||||||
|
}
|
||||||
|
|
||||||
|
modeFromComment := ExtractModeFromComment(comment)
|
||||||
|
if modeFromComment != "" {
|
||||||
|
if modeFromComment == "SSB" {
|
||||||
|
return bands.NormalizeSSBModeByFrequency(modeFromComment, freqMHz)
|
||||||
|
}
|
||||||
|
return modeFromComment
|
||||||
|
}
|
||||||
|
|
||||||
|
return GuessMode(freqMHz)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsCWMode(mode string) bool { return strings.ToUpper(mode) == "CW" }
|
||||||
|
func IsSSBMode(mode string) bool {
|
||||||
|
m := strings.ToUpper(mode)
|
||||||
|
return m == "SSB" || m == "USB" || m == "LSB"
|
||||||
|
}
|
||||||
|
func IsDigitalMode(mode string) bool {
|
||||||
|
m := strings.ToUpper(mode)
|
||||||
|
for _, dm := range []string{"FT8", "FT4", "RTTY", "PSK31", "PSK63", "PSK", "MFSK", "OLIVIA", "JT65", "JT9"} {
|
||||||
|
if m == dm {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func IsPhoneMode(mode string) bool {
|
||||||
|
m := strings.ToUpper(mode)
|
||||||
|
for _, pm := range []string{"SSB", "USB", "LSB", "FM", "AM"} {
|
||||||
|
if m == pm {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseModeFromRawSpot(rawSpot string) string {
|
||||||
|
re := regexp.MustCompile(`\b(CW|SSB|USB|LSB|FM|AM|FT8|FT4|RTTY|PSK\d*)\b`)
|
||||||
|
return re.FindString(strings.ToUpper(rawSpot))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetModeColor(mode string) string {
|
||||||
|
switch strings.ToUpper(mode) {
|
||||||
|
case "CW":
|
||||||
|
return "#10b981"
|
||||||
|
case "FT8", "FT4":
|
||||||
|
return "#8b5cf6"
|
||||||
|
case "RTTY":
|
||||||
|
return "#f59e0b"
|
||||||
|
case "FM":
|
||||||
|
return "#ec4899"
|
||||||
|
}
|
||||||
|
if IsSSBMode(mode) {
|
||||||
|
return "#3b82f6"
|
||||||
|
}
|
||||||
|
return "#6b7280"
|
||||||
|
}
|
||||||
20
internal/solar/solar.go
Normal file
20
internal/solar/solar.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package solar
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
type Data struct {
|
||||||
|
SolarFlux string `xml:"solarflux" json:"solarFlux"`
|
||||||
|
Sunspots string `xml:"sunspots" json:"sunspots"`
|
||||||
|
AIndex string `xml:"aindex" json:"aIndex"`
|
||||||
|
KIndex string `xml:"kindex" json:"kIndex"`
|
||||||
|
SolarWind string `xml:"solarwind" json:"solarWind"`
|
||||||
|
HeliumLine string `xml:"heliumline" json:"heliumLine"`
|
||||||
|
ProtonFlux string `xml:"protonflux" json:"protonFlux"`
|
||||||
|
GeomagField string `xml:"geomagfield" json:"geomagField"`
|
||||||
|
Updated string `xml:"updated" json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type XML struct {
|
||||||
|
XMLName xml.Name `xml:"solar"`
|
||||||
|
Data Data `xml:"solardata"`
|
||||||
|
}
|
||||||
89
internal/spot/parser.go
Normal file
89
internal/spot/parser.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package spot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Regex pour les deux formats de spots cluster
|
||||||
|
var (
|
||||||
|
// Format standard : DX de SPOTTER: FREQ DX MODE COMMENT TIME
|
||||||
|
SpotRe = regexp.MustCompile(`(?i)DX\sde\s([\w\d\/]+?)(?:-[#\d-]+)?\s*:\s*(\d+\.\d+)\s+([\w\d\/]+)\s+(?:(CW|SSB|FT8|FT4|RTTY|USB|LSB|FM)\s+)?(.+?)\s+(\d{4}Z)`)
|
||||||
|
|
||||||
|
// Format court : FREQ DX DATE TIME COMMENT <SPOTTER>
|
||||||
|
SpotReShort = regexp.MustCompile(`^(\d+\.\d+)\s+([\w\d\/]+)\s+\d{2}-\w{3}-\d{4}\s+(\d{4}Z)\s+(.+?)\s*<([\w\d\/]+)>\s*$`)
|
||||||
|
|
||||||
|
// Détection rapide du format court
|
||||||
|
ShortSpotDetectRe = regexp.MustCompile(`^\d+\.\d+\s+[\w\d\/]+\s+\d{2}-\w{3}-\d{4}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseResult contient le spot parsé et une éventuelle erreur
|
||||||
|
type ParseResult struct {
|
||||||
|
Spot *Spot
|
||||||
|
Err error
|
||||||
|
Skipped bool // true si la ligne n'est pas un spot (pas une erreur)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseLine tente de parser une ligne brute du cluster en Spot
|
||||||
|
// Retourne nil si la ligne n'est pas un spot DX
|
||||||
|
func ParseLine(line string, clusterName string) *Spot {
|
||||||
|
// Détecter si c'est un spot
|
||||||
|
isSpot := strings.Contains(line, "DX de ") || ShortSpotDetectRe.MatchString(line)
|
||||||
|
if !isSpot {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
match := SpotRe.FindStringSubmatch(line)
|
||||||
|
if len(match) > 0 {
|
||||||
|
return parseStandardFormat(match, clusterName)
|
||||||
|
}
|
||||||
|
|
||||||
|
match = SpotReShort.FindStringSubmatch(line)
|
||||||
|
if len(match) > 0 {
|
||||||
|
return parseShortFormat(match, clusterName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseStandardFormat(match []string, clusterName string) *Spot {
|
||||||
|
freqKHz := parseFreq(match[2])
|
||||||
|
return &Spot{
|
||||||
|
Spotter: match[1],
|
||||||
|
FrequencyKHz: freqKHz,
|
||||||
|
DX: match[3],
|
||||||
|
Mode: match[4],
|
||||||
|
Comment: strings.TrimSpace(match[5]),
|
||||||
|
Time: match[6],
|
||||||
|
ClusterName: clusterName,
|
||||||
|
Source: SourceCluster,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseShortFormat(match []string, clusterName string) *Spot {
|
||||||
|
freqKHz := parseFreq(match[1])
|
||||||
|
return &Spot{
|
||||||
|
FrequencyKHz: freqKHz,
|
||||||
|
DX: match[2],
|
||||||
|
Time: match[3],
|
||||||
|
Comment: strings.TrimSpace(match[4]),
|
||||||
|
Spotter: match[5],
|
||||||
|
ClusterName: clusterName,
|
||||||
|
Source: SourceCluster,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFreq parse une fréquence string en kHz float64
|
||||||
|
// Gère kHz (>1000) et MHz (<1000) automatiquement
|
||||||
|
func parseFreq(s string) float64 {
|
||||||
|
f, err := strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// Si < 1000 c'est en MHz, on convertit en kHz
|
||||||
|
if f < 1000 {
|
||||||
|
return f * 1000.0
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
97
internal/spot/spot.go
Normal file
97
internal/spot/spot.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package spot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SpotSource identifie l'origine d'un spot
|
||||||
|
type SpotSource int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SourceCluster SpotSource = iota // Spot reçu depuis un cluster DX telnet
|
||||||
|
SourceManual // Spot ajouté manuellement depuis l'UI
|
||||||
|
)
|
||||||
|
|
||||||
|
// Spot est la struct universelle — remplace TelnetSpot + FlexSpot
|
||||||
|
// Plus de conversion entre les deux, tout passe par cette struct unique
|
||||||
|
type Spot struct {
|
||||||
|
// --- Persistance ---
|
||||||
|
ID int64
|
||||||
|
|
||||||
|
// --- Identité ---
|
||||||
|
DX string
|
||||||
|
Spotter string
|
||||||
|
|
||||||
|
// --- Fréquence ---
|
||||||
|
// FrequencyKHz est la source de vérité interne
|
||||||
|
// FrequencyMHz est le format string attendu par FlexRadio (ex: "14.195000")
|
||||||
|
FrequencyKHz float64
|
||||||
|
FrequencyMHz string
|
||||||
|
Band string
|
||||||
|
|
||||||
|
// --- Mode ---
|
||||||
|
Mode string
|
||||||
|
|
||||||
|
// --- Métadonnées cluster ---
|
||||||
|
Comment string
|
||||||
|
Time string // Format "1234Z"
|
||||||
|
Timestamp int64 // Unix timestamp
|
||||||
|
ReceivedAt time.Time
|
||||||
|
ClusterName string
|
||||||
|
Source SpotSource
|
||||||
|
|
||||||
|
// --- DXCC ---
|
||||||
|
DXCC string
|
||||||
|
CountryName string
|
||||||
|
|
||||||
|
// --- Flags Log4OM (calculés depuis la DB Log4OM) ---
|
||||||
|
NewDXCC bool
|
||||||
|
NewBand bool
|
||||||
|
NewMode bool
|
||||||
|
NewSlot bool
|
||||||
|
CallsignWorked bool
|
||||||
|
|
||||||
|
// --- Watchlist ---
|
||||||
|
InWatchlist bool
|
||||||
|
WatchlistNotify bool
|
||||||
|
|
||||||
|
// --- POTA / SOTA ---
|
||||||
|
POTARef string
|
||||||
|
SOTARef string
|
||||||
|
ParkName string
|
||||||
|
SummitName string
|
||||||
|
|
||||||
|
// --- FlexRadio panadapter ---
|
||||||
|
FlexSpotNumber int
|
||||||
|
CommandNumber int
|
||||||
|
Color string
|
||||||
|
BackgroundColor string
|
||||||
|
Priority string
|
||||||
|
LifeTime string
|
||||||
|
OriginalComment string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreqMHzString retourne la fréquence formatée pour FlexRadio
|
||||||
|
// ex: 14195.0 kHz → "14.195000"
|
||||||
|
func (s *Spot) FreqMHzString() string {
|
||||||
|
if s.FrequencyMHz != "" {
|
||||||
|
return s.FrequencyMHz
|
||||||
|
}
|
||||||
|
if s.FrequencyKHz > 1000 {
|
||||||
|
return formatMHz(s.FrequencyKHz / 1000.0)
|
||||||
|
}
|
||||||
|
return formatMHz(s.FrequencyKHz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreqKHz retourne la fréquence en kHz depuis le champ disponible
|
||||||
|
func (s *Spot) FreqKHz() float64 {
|
||||||
|
if s.FrequencyKHz > 0 {
|
||||||
|
return s.FrequencyKHz
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMHz(mhz float64) string {
|
||||||
|
return fmt.Sprintf("%.6f", mhz)
|
||||||
|
}
|
||||||
57
internal/stats/stats.go
Normal file
57
internal/stats/stats.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package stats
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type SpotStats struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
Received int64
|
||||||
|
Processed int64
|
||||||
|
Rejected int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var Global = &SpotStats{}
|
||||||
|
|
||||||
|
func (s *SpotStats) IncrReceived() {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.Received++
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpotStats) IncrProcessed() {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.Processed++
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpotStats) IncrRejected() {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.Rejected++
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpotStats) Get() (received, processed, rejected int64) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.Received, s.Processed, s.Rejected
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpotStats) SuccessRate() float64 {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
if s.Received == 0 {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
return float64(s.Processed) / float64(s.Received) * 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot retourne un map JSON-friendly pour le frontend
|
||||||
|
func (s *SpotStats) Snapshot() map[string]interface{} {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return map[string]interface{}{
|
||||||
|
"received": s.Received,
|
||||||
|
"processed": s.Processed,
|
||||||
|
"rejected": s.Rejected,
|
||||||
|
"successRate": s.SuccessRate(),
|
||||||
|
}
|
||||||
|
}
|
||||||
260
internal/watchlist/watchlist.go
Normal file
260
internal/watchlist/watchlist.go
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
package watchlist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
Callsign string `json:"callsign"`
|
||||||
|
LastSeen time.Time `json:"lastSeen"`
|
||||||
|
LastSeenStr string `json:"lastSeenStr"`
|
||||||
|
AddedAt time.Time `json:"addedAt"`
|
||||||
|
SpotCount int `json:"spotCount"`
|
||||||
|
IsContest bool `json:"isContest"`
|
||||||
|
Notify bool `json:"notify"`
|
||||||
|
|
||||||
|
// ActiveSpotIDs n'est pas sérialisé — reconstruit en mémoire
|
||||||
|
activeSpotIDs map[int64]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Watchlist struct {
|
||||||
|
entries map[string]*Entry
|
||||||
|
filePath string
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(filePath string) *Watchlist {
|
||||||
|
w := &Watchlist{
|
||||||
|
entries: make(map[string]*Entry),
|
||||||
|
filePath: filePath,
|
||||||
|
}
|
||||||
|
w.load()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) load() {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(w.filePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []Entry
|
||||||
|
if err := json.Unmarshal(data, &entries); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range entries {
|
||||||
|
e := &entries[i]
|
||||||
|
e.activeSpotIDs = make(map[int64]bool)
|
||||||
|
if e.LastSeen.IsZero() {
|
||||||
|
e.LastSeenStr = "Never"
|
||||||
|
} else {
|
||||||
|
e.LastSeenStr = FormatLastSeen(e.LastSeen)
|
||||||
|
}
|
||||||
|
w.entries[e.Callsign] = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) save() error {
|
||||||
|
entries := make([]Entry, 0, len(w.entries))
|
||||||
|
for _, e := range w.entries {
|
||||||
|
entries = append(entries, *e)
|
||||||
|
}
|
||||||
|
data, err := json.MarshalIndent(entries, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(w.filePath, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) Add(callsign string) error {
|
||||||
|
return w.add(callsign, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) AddContest(callsign string) error {
|
||||||
|
return w.add(callsign, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) add(callsign string, isContest bool) error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
callsign = strings.ToUpper(strings.TrimSpace(callsign))
|
||||||
|
if callsign == "" {
|
||||||
|
return fmt.Errorf("callsign cannot be empty")
|
||||||
|
}
|
||||||
|
if _, exists := w.entries[callsign]; exists {
|
||||||
|
return fmt.Errorf("callsign already in watchlist")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.entries[callsign] = &Entry{
|
||||||
|
Callsign: callsign,
|
||||||
|
AddedAt: time.Now(),
|
||||||
|
LastSeenStr: "Never",
|
||||||
|
activeSpotIDs: make(map[int64]bool),
|
||||||
|
IsContest: isContest,
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) Remove(callsign string) error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
callsign = strings.ToUpper(strings.TrimSpace(callsign))
|
||||||
|
if _, exists := w.entries[callsign]; !exists {
|
||||||
|
return fmt.Errorf("callsign not in watchlist")
|
||||||
|
}
|
||||||
|
delete(w.entries, callsign)
|
||||||
|
return w.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) SetNotify(callsign string, notify bool) error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
callsign = strings.ToUpper(strings.TrimSpace(callsign))
|
||||||
|
e, exists := w.entries[callsign]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("callsign not found")
|
||||||
|
}
|
||||||
|
e.Notify = notify
|
||||||
|
return w.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) Matches(callsign string) bool {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
_, ok := w.entries[strings.ToUpper(callsign)]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) GetEntry(callsign string) *Entry {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
e, ok := w.entries[strings.ToUpper(callsign)]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
copy := *e
|
||||||
|
return ©
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) GetAll() []Entry {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
entries := make([]Entry, 0, len(w.entries))
|
||||||
|
for _, e := range w.entries {
|
||||||
|
cp := *e
|
||||||
|
if !cp.LastSeen.IsZero() {
|
||||||
|
cp.LastSeenStr = FormatLastSeen(cp.LastSeen)
|
||||||
|
}
|
||||||
|
entries = append(entries, cp)
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) GetAllCallsigns() []string {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
callsigns := make([]string, 0, len(w.entries))
|
||||||
|
for cs := range w.entries {
|
||||||
|
callsigns = append(callsigns, cs)
|
||||||
|
}
|
||||||
|
return callsigns
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) MarkSeen(callsign string) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
e, ok := w.entries[strings.ToUpper(callsign)]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.LastSeen = time.Now()
|
||||||
|
e.LastSeenStr = FormatLastSeen(e.LastSeen)
|
||||||
|
e.SpotCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) AddActiveSpot(callsign string, spotID int64) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
e, ok := w.entries[strings.ToUpper(callsign)]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e.activeSpotIDs == nil {
|
||||||
|
e.activeSpotIDs = make(map[int64]bool)
|
||||||
|
}
|
||||||
|
e.activeSpotIDs[spotID] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) RemoveActiveSpot(spotID int64) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
for _, e := range w.entries {
|
||||||
|
delete(e.activeSpotIDs, spotID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watchlist) GetAllWithActiveStatus() []map[string]interface{} {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
result := make([]map[string]interface{}, 0, len(w.entries))
|
||||||
|
for _, e := range w.entries {
|
||||||
|
lastSeenStr := "Never"
|
||||||
|
if !e.LastSeen.IsZero() {
|
||||||
|
lastSeenStr = FormatLastSeen(e.LastSeen)
|
||||||
|
}
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"callsign": e.Callsign,
|
||||||
|
"lastSeen": e.LastSeen,
|
||||||
|
"lastSeenStr": lastSeenStr,
|
||||||
|
"addedAt": e.AddedAt,
|
||||||
|
"spotCount": e.SpotCount,
|
||||||
|
"notify": e.Notify,
|
||||||
|
"isContest": e.IsContest,
|
||||||
|
"hasActiveSpots": len(e.activeSpotIDs) > 0,
|
||||||
|
"activeCount": len(e.activeSpotIDs),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatLastSeen(t time.Time) string {
|
||||||
|
if t.IsZero() {
|
||||||
|
return "Never"
|
||||||
|
}
|
||||||
|
d := time.Since(t)
|
||||||
|
switch {
|
||||||
|
case d < time.Minute:
|
||||||
|
return "Just now"
|
||||||
|
case d < time.Hour:
|
||||||
|
m := int(d.Minutes())
|
||||||
|
if m == 1 {
|
||||||
|
return "1 minute ago"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d minutes ago", m)
|
||||||
|
case d < 24*time.Hour:
|
||||||
|
h := int(d.Hours())
|
||||||
|
if h == 1 {
|
||||||
|
return "1 hour ago"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d hours ago", h)
|
||||||
|
default:
|
||||||
|
days := int(d.Hours() / 24)
|
||||||
|
if days == 1 {
|
||||||
|
return "1 day ago"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d days ago", days)
|
||||||
|
}
|
||||||
|
}
|
||||||
36
main.go
Normal file
36
main.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:frontend/dist
|
||||||
|
var assets embed.FS
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create an instance of the app structure
|
||||||
|
app := NewApp()
|
||||||
|
|
||||||
|
// Create application with options
|
||||||
|
err := wails.Run(&options.App{
|
||||||
|
Title: "FlexDXCluster2",
|
||||||
|
Width: 1024,
|
||||||
|
Height: 768,
|
||||||
|
AssetServer: &assetserver.Options{
|
||||||
|
Assets: assets,
|
||||||
|
},
|
||||||
|
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||||
|
OnStartup: app.startup,
|
||||||
|
Bind: []interface{}{
|
||||||
|
app,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
println("Error:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user