Frontend: Added a frontend API and serving the list of sound files available and
ci/woodpecker/push/ociImagePush Pipeline failed Details

This commit is contained in:
Natty 2023-07-22 23:36:46 +02:00
parent e88e46508d
commit 39f9ecd8d3
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
22 changed files with 1086 additions and 879 deletions

View File

@ -20,12 +20,12 @@ nattyarch.local {
header Accept text/html* header Accept text/html*
} }
@static { @frontend {
path /favicon.ico /favicon.png /favicon.svg /manifest.json /api-doc /sw.js /static-assets* /client-assets* /assets* /twemoji* /url path /favicon.ico /favicon.png /favicon.svg /manifest.json /api-doc /sw.js /static-assets* /client-assets* /assets* /twemoji* /url /fe-api*
} }
reverse_proxy @render_html 127.0.0.1:4938 reverse_proxy @render_html 127.0.0.1:4938
reverse_proxy @static 127.0.0.1:4938 reverse_proxy @frontend 127.0.0.1:4938
reverse_proxy 127.0.0.1:4937 reverse_proxy 127.0.0.1:4937
} }

1
Cargo.lock generated
View File

@ -986,6 +986,7 @@ dependencies = [
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"walkdir",
] ]
[[package]] [[package]]

View File

@ -40,6 +40,7 @@ tower-http = "0.4"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
url = "2.3" url = "2.3"
walkdir = "2.3"
[dependencies] [dependencies]
magnetar_core = { path = "./core" } magnetar_core = { path = "./core" }

View File

@ -27,4 +27,6 @@ serde = { workspace = true, features = ["derive"] }
toml = { workspace = true } toml = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
walkdir = { workspace = true }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Calckey API</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.50/bundles/redoc.standalone.js" integrity="sha256-WJbngBWN9vp6vkEuzeoSj5tE5saW9Hfj6/SinkzhL2s=" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -1,15 +1,15 @@
{ {
"tabWidth": 4, "tabWidth": 4,
"useTabs": true, "useTabs": false,
"singleQuote": false, "singleQuote": false,
"vueIndentScriptAndStyle": false, "vueIndentScriptAndStyle": false,
"plugins": ["vue"], "plugins": ["vue"],
"overrides": [ "overrides": [
{ {
"files": "*.vue", "files": "*.vue",
"options": { "options": {
"parser": "vue" "parser": "vue"
} }
} }
] ]
} }

View File

@ -53,6 +53,7 @@
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"json5": "2.2.3", "json5": "2.2.3",
"katex": "0.16.7", "katex": "0.16.7",
"magnetar-common": "workspace:*",
"matter-js": "0.18.0", "matter-js": "0.18.0",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"photoswipe": "5.3.7", "photoswipe": "5.3.7",

View File

@ -7,6 +7,7 @@ export const host = _HOST || address.host;
export const hostname = address.hostname; export const hostname = address.hostname;
export const url = _REMOTE_URL || address.origin; export const url = _REMOTE_URL || address.origin;
export const apiUrl = `${url}/api`; export const apiUrl = `${url}/api`;
export const feApiUrl = `${url}/fe-api`;
export const wsUrl = `${url export const wsUrl = `${url
.replace("http://", "ws://") .replace("http://", "ws://")
.replace("https://", "wss://")}/streaming`; .replace("https://", "wss://")}/streaming`;

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,40 @@
<template> <template>
<div class="_formRoot"> <div class="_formRoot">
<FormRange <FormRange
v-model="masterVolume" v-model="masterVolume"
:min="0" :min="0"
:max="1" :max="1"
:step="0.05" :step="0.05"
:text-converter="(v) => `${Math.floor(v * 100)}%`" :text-converter="(v) => `${Math.floor(v * 100)}%`"
class="_formBlock" class="_formBlock"
> >
<template #label>{{ i18n.ts.masterVolume }}</template> <template #label>{{ i18n.ts.masterVolume }}</template>
</FormRange> </FormRange>
<FormSection> <FormSection>
<template #label>{{ i18n.ts.sounds }}</template> <template #label>{{ i18n.ts.sounds }}</template>
<div class="_formLinksGrid"> <div class="_formLinksGrid">
<FormButton <FormButton
v-for="type in Object.keys(sounds)" v-for="type in Object.keys(sounds)"
:key="type" :key="type"
@click="edit(type)" @click="edit(type)"
> >
{{ i18n.t("_sfx." + type) }} {{ i18n.t("_sfx." + type) }}
<template #suffix>{{ <template #suffix>{{
sounds[type].type || i18n.ts.none sounds[type].type || i18n.ts.none
}}</template> }}</template>
<template #suffixIcon <template #suffixIcon
><i class="ph-caret-down ph-bold ph-lg"></i ><i class="ph-caret-down ph-bold ph-lg"></i
></template> ></template>
</FormButton> </FormButton>
</div> </div>
</FormSection> </FormSection>
<FormButton danger class="_formBlock" @click="reset()" <FormButton danger class="_formBlock" @click="reset()"
><i class="ph-arrow-clockwise ph-bold ph-lg"></i> ><i class="ph-arrow-clockwise ph-bold ph-lg"></i>
{{ i18n.ts.default }}</FormButton {{ i18n.ts.default }}</FormButton
> >
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -47,79 +47,80 @@ import { ColdDeviceStorage } from "@/store";
import { playFile } from "@/scripts/sound"; import { playFile } from "@/scripts/sound";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
import { feEndpoints } from "magnetar-common/built/fe-api";
const masterVolume = computed({ const masterVolume = computed({
get: () => { get: () => {
return ColdDeviceStorage.get("sound_masterVolume"); return ColdDeviceStorage.get("sound_masterVolume");
}, },
set: (value) => { set: (value) => {
ColdDeviceStorage.set("sound_masterVolume", value); ColdDeviceStorage.set("sound_masterVolume", value);
}, },
}); });
const volumeIcon = computed(() => const volumeIcon = computed(() =>
masterVolume.value === 0 masterVolume.value === 0
? "ph-speaker-none ph-bold ph-lg" ? "ph-speaker-none ph-bold ph-lg"
: "ph-speaker-high ph-bold ph-lg" : "ph-speaker-high ph-bold ph-lg"
); );
const sounds = ref({ const sounds = ref({
note: ColdDeviceStorage.get("sound_note"), note: ColdDeviceStorage.get("sound_note"),
noteMy: ColdDeviceStorage.get("sound_noteMy"), noteMy: ColdDeviceStorage.get("sound_noteMy"),
notification: ColdDeviceStorage.get("sound_notification"), notification: ColdDeviceStorage.get("sound_notification"),
chat: ColdDeviceStorage.get("sound_chat"), chat: ColdDeviceStorage.get("sound_chat"),
chatBg: ColdDeviceStorage.get("sound_chatBg"), chatBg: ColdDeviceStorage.get("sound_chatBg"),
antenna: ColdDeviceStorage.get("sound_antenna"), antenna: ColdDeviceStorage.get("sound_antenna"),
channel: ColdDeviceStorage.get("sound_channel"), channel: ColdDeviceStorage.get("sound_channel"),
}); });
const soundsTypes = await os.api("get-sounds"); const soundsTypes = [null, ...(await os.feApi(feEndpoints.defaultSounds, {}))];
async function edit(type) { async function edit(type) {
const { canceled, result } = await os.form(i18n.t("_sfx." + type), { const { canceled, result } = await os.form(i18n.t("_sfx." + type), {
type: { type: {
type: "enum", type: "enum",
enum: soundsTypes.map((x) => ({ enum: soundsTypes.map((x) => ({
value: x, value: x,
label: x == null ? i18n.ts.none : x, label: x == null ? i18n.ts.none : x,
})), })),
label: i18n.ts.sound, label: i18n.ts.sound,
default: sounds.value[type].type, default: sounds.value[type].type,
}, },
volume: { volume: {
type: "range", type: "range",
min: 0, min: 0,
max: 1, max: 1,
step: 0.05, step: 0.05,
textConverter: (v) => `${Math.floor(v * 100)}%`, textConverter: (v) => `${Math.floor(v * 100)}%`,
label: i18n.ts.volume, label: i18n.ts.volume,
default: sounds.value[type].volume, default: sounds.value[type].volume,
}, },
listen: { listen: {
type: "button", type: "button",
content: i18n.ts.listen, content: i18n.ts.listen,
action: (_, values) => { action: (_, values) => {
playFile(values.type, values.volume); playFile(values.type, values.volume);
}, },
}, },
}); });
if (canceled) return; if (canceled) return;
const v = { const v = {
type: result.type, type: result.type,
volume: result.volume, volume: result.volume,
}; };
ColdDeviceStorage.set("sound_" + type, v); ColdDeviceStorage.set("sound_" + type, v);
sounds.value[type] = v; sounds.value[type] = v;
} }
function reset() { function reset() {
for (const sound of Object.keys(sounds.value)) { for (const sound of Object.keys(sounds.value)) {
const v = ColdDeviceStorage.default["sound_" + sound]; const v = ColdDeviceStorage.default["sound_" + sound];
ColdDeviceStorage.set("sound_" + sound, v); ColdDeviceStorage.set("sound_" + sound, v);
sounds.value[sound] = v; sounds.value[sound] = v;
} }
} }
const headerActions = $computed(() => []); const headerActions = $computed(() => []);
@ -127,7 +128,7 @@ const headerActions = $computed(() => []);
const headerTabs = $computed(() => []); const headerTabs = $computed(() => []);
definePageMetadata({ definePageMetadata({
title: i18n.ts.sounds, title: i18n.ts.sounds,
icon: "ph-speaker-high ph-bold ph-lg", icon: "ph-speaker-high ph-bold ph-lg",
}); });
</script> </script>

View File

@ -0,0 +1,7 @@
{
"tabWidth": 4,
"useTabs": false,
"singleQuote": false,
"semi": true,
"bracketSameLine": true
}

View File

@ -0,0 +1,29 @@
{
"name": "magnetar-common",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "magnetar-common",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"typescript": "^5.1.6"
}
},
"node_modules/typescript": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

View File

@ -0,0 +1,14 @@
{
"name": "magnetar-common",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"build": "tsc"
},
"author": "Natty",
"license": "MIT",
"description": "A library with common utilities for Magnetar application development",
"devDependencies": {
"typescript": "^5.1.6"
}
}

View File

@ -0,0 +1,28 @@
type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
export interface FrontendApiEndpoint<M extends Method, P extends string, T, R> {
method: M;
path: P;
request?: T;
response?: R;
}
function endpointDef<T, R>(
method: Method,
path: string
): FrontendApiEndpoint<Method, string, T, R> {
return {
method,
path,
};
}
export const feEndpoints = {
defaultSounds: endpointDef<{}, string[]>("GET", "default-sounds"),
} as const;
type Endpoints = typeof feEndpoints;
export type FrontendApiEndpoints = {
[N in keyof Endpoints as Endpoints[N]["path"] & string]: Endpoints[N];
};

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "esnext",
"target": "es2020",
"moduleResolution": "node",
"sourceMap": true,
"strict": true,
"declaration": true,
"declarationMap": true,
"outDir": "./built/",
"removeComments": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
"esModuleInterop": true,
},
"exclude": [
"node_modules",
"built",
]
}

View File

@ -254,6 +254,9 @@ importers:
katex: katex:
specifier: 0.16.7 specifier: 0.16.7
version: 0.16.7 version: 0.16.7
magnetar-common:
specifier: workspace:*
version: link:../magnetar-common
matter-js: matter-js:
specifier: 0.18.0 specifier: 0.18.0
version: 0.18.0 version: 0.18.0
@ -366,6 +369,12 @@ importers:
specifier: 4.1.0 specifier: 4.1.0
version: 4.1.0(vue@3.3.4) version: 4.1.0(vue@3.3.4)
magnetar-common:
devDependencies:
typescript:
specifier: ^5.1.6
version: 5.1.6
sw: sw:
devDependencies: devDependencies:
'@swc/cli': '@swc/cli':
@ -7134,6 +7143,12 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/typescript@5.1.6:
resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
engines: {node: '>=14.17'}
hasBin: true
dev: true
/unc-path-regex@0.1.2: /unc-path-regex@0.1.2:
resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}

View File

@ -2,3 +2,4 @@ packages:
- 'client' - 'client'
- 'sw' - 'sw'
- 'calckey-js' - 'calckey-js'
- "magnetar-common"

View File

@ -0,0 +1,41 @@
use axum::extract::State;
use axum::routing::get;
use axum::{Json, Router};
use std::path::Path;
use walkdir::WalkDir;
pub fn create_frontend_api(assets_dir: &str) -> Router {
Router::new().route(
"/default-sounds",
get(default_sounds).with_state(assets_dir.to_owned()),
)
}
async fn default_sounds(State(ref assets_dir): State<String>) -> Json<Vec<String>> {
let sounds_dir = &Path::new(assets_dir).join("sounds");
let mut sounds = Vec::new();
for entry in WalkDir::new(sounds_dir) {
let Ok(entry) = entry else {
continue;
};
if !entry.file_type().is_file() {
continue;
}
let Ok(Some(entry)) = entry
.path()
.strip_prefix(sounds_dir)
.map(|p| p.to_str())
.map(|p| p.and_then(|pp| pp.strip_suffix(".mp3"))) else {
continue;
};
sounds.push(entry.to_owned());
}
sounds.sort();
Json(sounds)
}

View File

@ -10,7 +10,7 @@ use serde_json::Value;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tera::{Context, Tera}; use tera::{Context, Tera};
use tracing::{error, info}; use tracing::error;
pub fn new_frontend_render_router(frontend_renderer_config: FrontendConfig) -> Router { pub fn new_frontend_render_router(frontend_renderer_config: FrontendConfig) -> Router {
Router::new() Router::new()

View File

@ -1,9 +1,11 @@
mod frontend_api;
mod frontend_render; mod frontend_render;
mod manifest; mod manifest;
mod static_serve; mod static_serve;
mod summary_proxy; mod summary_proxy;
use crate::frontend_render::{new_frontend_render_router, render_frontend, FrontendConfig}; use crate::frontend_api::create_frontend_api;
use crate::frontend_render::{new_frontend_render_router, FrontendConfig};
use crate::manifest::handle_manifest; use crate::manifest::handle_manifest;
use crate::static_serve::{static_serve, static_serve_svg, static_serve_sw}; use crate::static_serve::{static_serve, static_serve_svg, static_serve_sw};
use crate::summary_proxy::generate_summary; use crate::summary_proxy::generate_summary;
@ -19,7 +21,6 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tera::Tera; use tera::Tera;
use thiserror::Error; use thiserror::Error;
use tower::layer::layer_fn;
use tower_http::services::ServeFile; use tower_http::services::ServeFile;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::log::info; use tracing::log::info;
@ -52,10 +53,6 @@ fn new_calckey_fe_router(config: &'static MagnetarConfig) -> Result<Router, Rout
ServeFile::new("fe_calckey/frontend/assets/favicon.svg"), ServeFile::new("fe_calckey/frontend/assets/favicon.svg"),
) )
.route("/manifest.json", get(handle_manifest).with_state(config)) .route("/manifest.json", get(handle_manifest).with_state(config))
.nest_service(
"/api-doc",
ServeFile::new("fe_calckey/frontend/assets/redoc.html"),
)
.nest( .nest(
"/sw.js", "/sw.js",
static_serve_sw("fe_calckey/frontend/built/_sw_dist_/sw.js"), static_serve_sw("fe_calckey/frontend/built/_sw_dist_/sw.js"),
@ -73,6 +70,7 @@ fn new_calckey_fe_router(config: &'static MagnetarConfig) -> Result<Router, Rout
"/twemoji", "/twemoji",
static_serve_svg("fe_calckey/frontend/assets-be/twemoji"), static_serve_svg("fe_calckey/frontend/assets-be/twemoji"),
) )
.nest("/fe-api", create_frontend_api("fe_calckey/frontend/assets"))
.route("/url", get(generate_summary)) .route("/url", get(generate_summary))
.route( .route(
"/streaming", "/streaming",

View File

@ -1,7 +1,7 @@
use axum::headers::{AccessControlMaxAge, HeaderMapExt}; use axum::headers::{AccessControlMaxAge, HeaderMapExt};
use axum::http::header::CONTENT_SECURITY_POLICY; use axum::http::header::CONTENT_SECURITY_POLICY;
use axum::http::{Request, StatusCode}; use axum::http::{Request, StatusCode};
use axum::middleware::{from_fn, MapRequestLayer, Next}; use axum::middleware::{from_fn, Next};
use axum::Router; use axum::Router;
use hyper::Response; use hyper::Response;
use std::time::Duration; use std::time::Duration;