Frontend: Added a frontend API and serving the list of sound files available and
ci/woodpecker/push/ociImagePush Pipeline failed
Details
ci/woodpecker/push/ociImagePush Pipeline failed
Details
This commit is contained in:
parent
e88e46508d
commit
39f9ecd8d3
|
@ -20,12 +20,12 @@ nattyarch.local {
|
|||
header Accept text/html*
|
||||
}
|
||||
|
||||
@static {
|
||||
path /favicon.ico /favicon.png /favicon.svg /manifest.json /api-doc /sw.js /static-assets* /client-assets* /assets* /twemoji* /url
|
||||
@frontend {
|
||||
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 @static 127.0.0.1:4938
|
||||
reverse_proxy @frontend 127.0.0.1:4938
|
||||
|
||||
reverse_proxy 127.0.0.1:4937
|
||||
}
|
||||
|
|
|
@ -986,6 +986,7 @@ dependencies = [
|
|||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -40,6 +40,7 @@ tower-http = "0.4"
|
|||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
url = "2.3"
|
||||
walkdir = "2.3"
|
||||
|
||||
[dependencies]
|
||||
magnetar_core = { path = "./core" }
|
||||
|
|
|
@ -27,4 +27,6 @@ serde = { workspace = true, features = ["derive"] }
|
|||
toml = { 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 |
|
@ -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>
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": true,
|
||||
"singleQuote": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"plugins": ["vue"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.vue",
|
||||
"options": {
|
||||
"parser": "vue"
|
||||
}
|
||||
}
|
||||
]
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"plugins": ["vue"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.vue",
|
||||
"options": {
|
||||
"parser": "vue"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
"insert-text-at-cursor": "0.3.0",
|
||||
"json5": "2.2.3",
|
||||
"katex": "0.16.7",
|
||||
"magnetar-common": "workspace:*",
|
||||
"matter-js": "0.18.0",
|
||||
"mfm-js": "0.23.3",
|
||||
"photoswipe": "5.3.7",
|
||||
|
|
|
@ -7,6 +7,7 @@ export const host = _HOST || address.host;
|
|||
export const hostname = address.hostname;
|
||||
export const url = _REMOTE_URL || address.origin;
|
||||
export const apiUrl = `${url}/api`;
|
||||
export const feApiUrl = `${url}/fe-api`;
|
||||
export const wsUrl = `${url
|
||||
.replace("http://", "ws://")
|
||||
.replace("https://", "wss://")}/streaming`;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,40 +1,40 @@
|
|||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormRange
|
||||
v-model="masterVolume"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.05"
|
||||
:text-converter="(v) => `${Math.floor(v * 100)}%`"
|
||||
class="_formBlock"
|
||||
>
|
||||
<template #label>{{ i18n.ts.masterVolume }}</template>
|
||||
</FormRange>
|
||||
<div class="_formRoot">
|
||||
<FormRange
|
||||
v-model="masterVolume"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.05"
|
||||
:text-converter="(v) => `${Math.floor(v * 100)}%`"
|
||||
class="_formBlock"
|
||||
>
|
||||
<template #label>{{ i18n.ts.masterVolume }}</template>
|
||||
</FormRange>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.sounds }}</template>
|
||||
<div class="_formLinksGrid">
|
||||
<FormButton
|
||||
v-for="type in Object.keys(sounds)"
|
||||
:key="type"
|
||||
@click="edit(type)"
|
||||
>
|
||||
{{ i18n.t("_sfx." + type) }}
|
||||
<template #suffix>{{
|
||||
sounds[type].type || i18n.ts.none
|
||||
}}</template>
|
||||
<template #suffixIcon
|
||||
><i class="ph-caret-down ph-bold ph-lg"></i
|
||||
></template>
|
||||
</FormButton>
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.sounds }}</template>
|
||||
<div class="_formLinksGrid">
|
||||
<FormButton
|
||||
v-for="type in Object.keys(sounds)"
|
||||
:key="type"
|
||||
@click="edit(type)"
|
||||
>
|
||||
{{ i18n.t("_sfx." + type) }}
|
||||
<template #suffix>{{
|
||||
sounds[type].type || i18n.ts.none
|
||||
}}</template>
|
||||
<template #suffixIcon
|
||||
><i class="ph-caret-down ph-bold ph-lg"></i
|
||||
></template>
|
||||
</FormButton>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormButton danger class="_formBlock" @click="reset()"
|
||||
><i class="ph-arrow-clockwise ph-bold ph-lg"></i>
|
||||
{{ i18n.ts.default }}</FormButton
|
||||
>
|
||||
</div>
|
||||
<FormButton danger class="_formBlock" @click="reset()"
|
||||
><i class="ph-arrow-clockwise ph-bold ph-lg"></i>
|
||||
{{ i18n.ts.default }}</FormButton
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -47,79 +47,80 @@ import { ColdDeviceStorage } from "@/store";
|
|||
import { playFile } from "@/scripts/sound";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
import { feEndpoints } from "magnetar-common/built/fe-api";
|
||||
|
||||
const masterVolume = computed({
|
||||
get: () => {
|
||||
return ColdDeviceStorage.get("sound_masterVolume");
|
||||
},
|
||||
set: (value) => {
|
||||
ColdDeviceStorage.set("sound_masterVolume", value);
|
||||
},
|
||||
get: () => {
|
||||
return ColdDeviceStorage.get("sound_masterVolume");
|
||||
},
|
||||
set: (value) => {
|
||||
ColdDeviceStorage.set("sound_masterVolume", value);
|
||||
},
|
||||
});
|
||||
|
||||
const volumeIcon = computed(() =>
|
||||
masterVolume.value === 0
|
||||
? "ph-speaker-none ph-bold ph-lg"
|
||||
: "ph-speaker-high ph-bold ph-lg"
|
||||
masterVolume.value === 0
|
||||
? "ph-speaker-none ph-bold ph-lg"
|
||||
: "ph-speaker-high ph-bold ph-lg"
|
||||
);
|
||||
|
||||
const sounds = ref({
|
||||
note: ColdDeviceStorage.get("sound_note"),
|
||||
noteMy: ColdDeviceStorage.get("sound_noteMy"),
|
||||
notification: ColdDeviceStorage.get("sound_notification"),
|
||||
chat: ColdDeviceStorage.get("sound_chat"),
|
||||
chatBg: ColdDeviceStorage.get("sound_chatBg"),
|
||||
antenna: ColdDeviceStorage.get("sound_antenna"),
|
||||
channel: ColdDeviceStorage.get("sound_channel"),
|
||||
note: ColdDeviceStorage.get("sound_note"),
|
||||
noteMy: ColdDeviceStorage.get("sound_noteMy"),
|
||||
notification: ColdDeviceStorage.get("sound_notification"),
|
||||
chat: ColdDeviceStorage.get("sound_chat"),
|
||||
chatBg: ColdDeviceStorage.get("sound_chatBg"),
|
||||
antenna: ColdDeviceStorage.get("sound_antenna"),
|
||||
channel: ColdDeviceStorage.get("sound_channel"),
|
||||
});
|
||||
|
||||
const soundsTypes = await os.api("get-sounds");
|
||||
const soundsTypes = [null, ...(await os.feApi(feEndpoints.defaultSounds, {}))];
|
||||
|
||||
async function edit(type) {
|
||||
const { canceled, result } = await os.form(i18n.t("_sfx." + type), {
|
||||
type: {
|
||||
type: "enum",
|
||||
enum: soundsTypes.map((x) => ({
|
||||
value: x,
|
||||
label: x == null ? i18n.ts.none : x,
|
||||
})),
|
||||
label: i18n.ts.sound,
|
||||
default: sounds.value[type].type,
|
||||
},
|
||||
volume: {
|
||||
type: "range",
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
textConverter: (v) => `${Math.floor(v * 100)}%`,
|
||||
label: i18n.ts.volume,
|
||||
default: sounds.value[type].volume,
|
||||
},
|
||||
listen: {
|
||||
type: "button",
|
||||
content: i18n.ts.listen,
|
||||
action: (_, values) => {
|
||||
playFile(values.type, values.volume);
|
||||
},
|
||||
},
|
||||
});
|
||||
if (canceled) return;
|
||||
const { canceled, result } = await os.form(i18n.t("_sfx." + type), {
|
||||
type: {
|
||||
type: "enum",
|
||||
enum: soundsTypes.map((x) => ({
|
||||
value: x,
|
||||
label: x == null ? i18n.ts.none : x,
|
||||
})),
|
||||
label: i18n.ts.sound,
|
||||
default: sounds.value[type].type,
|
||||
},
|
||||
volume: {
|
||||
type: "range",
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
textConverter: (v) => `${Math.floor(v * 100)}%`,
|
||||
label: i18n.ts.volume,
|
||||
default: sounds.value[type].volume,
|
||||
},
|
||||
listen: {
|
||||
type: "button",
|
||||
content: i18n.ts.listen,
|
||||
action: (_, values) => {
|
||||
playFile(values.type, values.volume);
|
||||
},
|
||||
},
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const v = {
|
||||
type: result.type,
|
||||
volume: result.volume,
|
||||
};
|
||||
const v = {
|
||||
type: result.type,
|
||||
volume: result.volume,
|
||||
};
|
||||
|
||||
ColdDeviceStorage.set("sound_" + type, v);
|
||||
sounds.value[type] = v;
|
||||
ColdDeviceStorage.set("sound_" + type, v);
|
||||
sounds.value[type] = v;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
for (const sound of Object.keys(sounds.value)) {
|
||||
const v = ColdDeviceStorage.default["sound_" + sound];
|
||||
ColdDeviceStorage.set("sound_" + sound, v);
|
||||
sounds.value[sound] = v;
|
||||
}
|
||||
for (const sound of Object.keys(sounds.value)) {
|
||||
const v = ColdDeviceStorage.default["sound_" + sound];
|
||||
ColdDeviceStorage.set("sound_" + sound, v);
|
||||
sounds.value[sound] = v;
|
||||
}
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
@ -127,7 +128,7 @@ const headerActions = $computed(() => []);
|
|||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.sounds,
|
||||
icon: "ph-speaker-high ph-bold ph-lg",
|
||||
title: i18n.ts.sounds,
|
||||
icon: "ph-speaker-high ph-bold ph-lg",
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"semi": true,
|
||||
"bracketSameLine": true
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
};
|
|
@ -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",
|
||||
]
|
||||
}
|
|
@ -254,6 +254,9 @@ importers:
|
|||
katex:
|
||||
specifier: 0.16.7
|
||||
version: 0.16.7
|
||||
magnetar-common:
|
||||
specifier: workspace:*
|
||||
version: link:../magnetar-common
|
||||
matter-js:
|
||||
specifier: 0.18.0
|
||||
version: 0.18.0
|
||||
|
@ -366,6 +369,12 @@ importers:
|
|||
specifier: 4.1.0
|
||||
version: 4.1.0(vue@3.3.4)
|
||||
|
||||
magnetar-common:
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.1.6
|
||||
version: 5.1.6
|
||||
|
||||
sw:
|
||||
devDependencies:
|
||||
'@swc/cli':
|
||||
|
@ -7134,6 +7143,12 @@ packages:
|
|||
hasBin: 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:
|
||||
resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
|
@ -2,3 +2,4 @@ packages:
|
|||
- 'client'
|
||||
- 'sw'
|
||||
- 'calckey-js'
|
||||
- "magnetar-common"
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -10,7 +10,7 @@ use serde_json::Value;
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tera::{Context, Tera};
|
||||
use tracing::{error, info};
|
||||
use tracing::error;
|
||||
|
||||
pub fn new_frontend_render_router(frontend_renderer_config: FrontendConfig) -> Router {
|
||||
Router::new()
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
mod frontend_api;
|
||||
mod frontend_render;
|
||||
mod manifest;
|
||||
mod static_serve;
|
||||
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::static_serve::{static_serve, static_serve_svg, static_serve_sw};
|
||||
use crate::summary_proxy::generate_summary;
|
||||
|
@ -19,7 +21,6 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
use tera::Tera;
|
||||
use thiserror::Error;
|
||||
use tower::layer::layer_fn;
|
||||
use tower_http::services::ServeFile;
|
||||
use tower_http::trace::TraceLayer;
|
||||
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"),
|
||||
)
|
||||
.route("/manifest.json", get(handle_manifest).with_state(config))
|
||||
.nest_service(
|
||||
"/api-doc",
|
||||
ServeFile::new("fe_calckey/frontend/assets/redoc.html"),
|
||||
)
|
||||
.nest(
|
||||
"/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",
|
||||
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(
|
||||
"/streaming",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use axum::headers::{AccessControlMaxAge, HeaderMapExt};
|
||||
use axum::http::header::CONTENT_SECURITY_POLICY;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use axum::middleware::{from_fn, MapRequestLayer, Next};
|
||||
use axum::middleware::{from_fn, Next};
|
||||
use axum::Router;
|
||||
use hyper::Response;
|
||||
use std::time::Duration;
|
||||
|
|
Loading…
Reference in New Issue