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*
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -986,6 +986,7 @@ dependencies = [
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -28,3 +28,5 @@ 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 |
|
@ -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,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"useTabs": true,
|
"useTabs": false,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"vueIndentScriptAndStyle": false,
|
"vueIndentScriptAndStyle": false,
|
||||||
"plugins": ["vue"],
|
"plugins": ["vue"],
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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`;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
|
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
|
||||||
|
|
||||||
import { Component, markRaw, Ref, ref, defineAsyncComponent } from "vue";
|
import { Component, defineAsyncComponent, markRaw, ref, Ref } from "vue";
|
||||||
import { EventEmitter } from "eventemitter3";
|
import { EventEmitter } from "eventemitter3";
|
||||||
import insertTextAtCursor from "insert-text-at-cursor";
|
import insertTextAtCursor from "insert-text-at-cursor";
|
||||||
import * as Misskey from "calckey-js";
|
import * as Misskey from "calckey-js";
|
||||||
import { apiUrl, url } from "@/config";
|
import { apiUrl, feApiUrl, url } from "@/config";
|
||||||
import MkPostFormDialog from "@/components/MkPostFormDialog.vue";
|
import MkPostFormDialog from "@/components/MkPostFormDialog.vue";
|
||||||
import MkWaitingDialog from "@/components/MkWaitingDialog.vue";
|
import MkWaitingDialog from "@/components/MkWaitingDialog.vue";
|
||||||
import MkToast from "@/components/MkToast.vue";
|
import MkToast from "@/components/MkToast.vue";
|
||||||
|
@ -12,6 +12,10 @@ import MkDialog from "@/components/MkDialog.vue";
|
||||||
import { MenuItem } from "@/types/menu";
|
import { MenuItem } from "@/types/menu";
|
||||||
import { $i } from "@/account";
|
import { $i } from "@/account";
|
||||||
import { i18n } from "./i18n";
|
import { i18n } from "./i18n";
|
||||||
|
import {
|
||||||
|
FrontendApiEndpoint,
|
||||||
|
FrontendApiEndpoints,
|
||||||
|
} from "magnetar-common/built/fe-api";
|
||||||
|
|
||||||
export const pendingApiRequestsCount = ref(0);
|
export const pendingApiRequestsCount = ref(0);
|
||||||
|
|
||||||
|
@ -19,10 +23,70 @@ const apiClient = new Misskey.api.APIClient({
|
||||||
origin: url,
|
origin: url,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export async function feApi<T extends keyof FrontendApiEndpoints & string>(
|
||||||
|
endpointDef: FrontendApiEndpoint<
|
||||||
|
FrontendApiEndpoints[T]["method"],
|
||||||
|
FrontendApiEndpoints[T]["path"],
|
||||||
|
FrontendApiEndpoints[T]["request"],
|
||||||
|
FrontendApiEndpoints[T]["response"]
|
||||||
|
>,
|
||||||
|
data: FrontendApiEndpoints[T]["request"],
|
||||||
|
token?: string | null | undefined
|
||||||
|
): Promise<FrontendApiEndpoints[T]["response"]> {
|
||||||
|
type Response = FrontendApiEndpoints[T]["response"];
|
||||||
|
|
||||||
|
pendingApiRequestsCount.value++;
|
||||||
|
|
||||||
|
const authorizationToken = token ?? $i?.token ?? undefined;
|
||||||
|
const authorization = authorizationToken
|
||||||
|
? `Bearer ${authorizationToken}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const endpoint = endpointDef.path;
|
||||||
|
|
||||||
|
let url = `${feApiUrl}/${endpoint}`;
|
||||||
|
|
||||||
|
if (endpointDef.method === "GET") {
|
||||||
|
const query = new URLSearchParams(data as any).toString();
|
||||||
|
if (query) {
|
||||||
|
url += `?${query}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise: Promise<Response> = new Promise((resolve, reject) => {
|
||||||
|
fetch(url, {
|
||||||
|
method: endpointDef.method,
|
||||||
|
body:
|
||||||
|
endpointDef.method !== "GET" ? JSON.stringify(data) : undefined,
|
||||||
|
credentials: "omit",
|
||||||
|
cache: "no-cache",
|
||||||
|
headers: authorization ? { authorization } : {},
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
const body = res.status === 204 ? null : await res.json();
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
resolve(body as Response);
|
||||||
|
} else if (res.status === 204) {
|
||||||
|
resolve(null as any as Response);
|
||||||
|
} else {
|
||||||
|
reject(body.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.finally(() => {
|
||||||
|
pendingApiRequestsCount.value--;
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
export const api = ((
|
export const api = ((
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data: Record<string, any> = {},
|
data: Record<string, any> = {},
|
||||||
token?: string | null | undefined,
|
token?: string | null | undefined
|
||||||
) => {
|
) => {
|
||||||
pendingApiRequestsCount.value++;
|
pendingApiRequestsCount.value++;
|
||||||
|
|
||||||
|
@ -36,13 +100,16 @@ export const api = ((
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const promise = new Promise((resolve, reject) => {
|
const promise = new Promise((resolve, reject) => {
|
||||||
fetch(endpoint.indexOf("://") > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
|
fetch(
|
||||||
|
endpoint.indexOf("://") > -1 ? endpoint : `${apiUrl}/${endpoint}`,
|
||||||
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
credentials: "omit",
|
credentials: "omit",
|
||||||
cache: "no-cache",
|
cache: "no-cache",
|
||||||
headers: authorization ? { authorization } : {},
|
headers: authorization ? { authorization } : {},
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const body = res.status === 204 ? null : await res.json();
|
const body = res.status === 204 ? null : await res.json();
|
||||||
|
|
||||||
|
@ -65,7 +132,7 @@ export const api = ((
|
||||||
export const apiGet = ((
|
export const apiGet = ((
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data: Record<string, any> = {},
|
data: Record<string, any> = {},
|
||||||
token?: string | null | undefined,
|
token?: string | null | undefined
|
||||||
) => {
|
) => {
|
||||||
pendingApiRequestsCount.value++;
|
pendingApiRequestsCount.value++;
|
||||||
|
|
||||||
|
@ -110,7 +177,7 @@ export const apiGet = ((
|
||||||
export const apiWithDialog = ((
|
export const apiWithDialog = ((
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data: Record<string, any> = {},
|
data: Record<string, any> = {},
|
||||||
token?: string | null | undefined,
|
token?: string | null | undefined
|
||||||
) => {
|
) => {
|
||||||
const promise = api(endpoint, data, token);
|
const promise = api(endpoint, data, token);
|
||||||
promiseDialog(promise, null, (err) => {
|
promiseDialog(promise, null, (err) => {
|
||||||
|
@ -127,7 +194,7 @@ export function promiseDialog<T extends Promise<any>>(
|
||||||
promise: T,
|
promise: T,
|
||||||
onSuccess?: ((res: any) => void) | null,
|
onSuccess?: ((res: any) => void) | null,
|
||||||
onFailure?: ((err: Error) => void) | null,
|
onFailure?: ((err: Error) => void) | null,
|
||||||
text?: string,
|
text?: string
|
||||||
): T {
|
): T {
|
||||||
const showing = ref(true);
|
const showing = ref(true);
|
||||||
const success = ref(false);
|
const success = ref(false);
|
||||||
|
@ -165,7 +232,7 @@ export function promiseDialog<T extends Promise<any>>(
|
||||||
text: text,
|
text: text,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
|
@ -186,7 +253,7 @@ const zIndexes = {
|
||||||
high: 3000000,
|
high: 3000000,
|
||||||
};
|
};
|
||||||
export function claimZIndex(
|
export function claimZIndex(
|
||||||
priority: "low" | "middle" | "high" = "low",
|
priority: "low" | "middle" | "high" = "low"
|
||||||
): number {
|
): number {
|
||||||
zIndexes[priority] += 100;
|
zIndexes[priority] += 100;
|
||||||
return zIndexes[priority];
|
return zIndexes[priority];
|
||||||
|
@ -201,7 +268,7 @@ export async function popup(
|
||||||
component: Component,
|
component: Component,
|
||||||
props: Record<string, any>,
|
props: Record<string, any>,
|
||||||
events = {},
|
events = {},
|
||||||
disposeEvent?: string,
|
disposeEvent?: string
|
||||||
) {
|
) {
|
||||||
markRaw(component);
|
markRaw(component);
|
||||||
|
|
||||||
|
@ -242,7 +309,7 @@ export function pageWindow(path: string) {
|
||||||
initialPath: path,
|
initialPath: path,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,7 +324,7 @@ export function modalPageWindow(path: string) {
|
||||||
initialPath: path,
|
initialPath: path,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,7 +335,7 @@ export function toast(message: string) {
|
||||||
message,
|
message,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,7 +356,7 @@ export function alert(props: {
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -313,7 +380,7 @@ export function confirm(props: {
|
||||||
resolve(result ? result : { canceled: true });
|
resolve(result ? result : { canceled: true });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -340,7 +407,7 @@ export function yesno(props: {
|
||||||
resolve(result ? result : { canceled: true });
|
resolve(result ? result : { canceled: true });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -381,7 +448,7 @@ export function inputText(props: {
|
||||||
resolve(result ? result : { canceled: true });
|
resolve(result ? result : { canceled: true });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -419,7 +486,7 @@ export function inputParagraph(props: {
|
||||||
resolve(result ? result : { canceled: true });
|
resolve(result ? result : { canceled: true });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -459,7 +526,7 @@ export function inputNumber(props: {
|
||||||
resolve(result ? result : { canceled: true });
|
resolve(result ? result : { canceled: true });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -496,11 +563,11 @@ export function inputDate(props: {
|
||||||
result: new Date(result.result),
|
result: new Date(result.result),
|
||||||
canceled: false,
|
canceled: false,
|
||||||
}
|
}
|
||||||
: { canceled: true },
|
: { canceled: true }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -526,7 +593,7 @@ export function select<C = any>(
|
||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
),
|
)
|
||||||
): Promise<
|
): Promise<
|
||||||
| { canceled: true; result: undefined }
|
| { canceled: true; result: undefined }
|
||||||
| {
|
| {
|
||||||
|
@ -551,7 +618,7 @@ export function select<C = any>(
|
||||||
resolve(result ? result : { canceled: true });
|
resolve(result ? result : { canceled: true });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -571,7 +638,7 @@ export function success(): Promise<void> {
|
||||||
{
|
{
|
||||||
done: () => resolve(),
|
done: () => resolve(),
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -588,7 +655,7 @@ export function waiting(): Promise<void> {
|
||||||
{
|
{
|
||||||
done: () => resolve(),
|
done: () => resolve(),
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -607,7 +674,7 @@ export function form(title, form) {
|
||||||
resolve(result);
|
resolve(result);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -626,7 +693,7 @@ export async function selectUser() {
|
||||||
resolve(user);
|
resolve(user);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -645,7 +712,7 @@ export async function selectInstance(): Promise<Misskey.entities.Instance> {
|
||||||
resolve(instance);
|
resolve(instance);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -669,7 +736,7 @@ export async function selectDriveFile(multiple: boolean) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -693,7 +760,7 @@ export async function selectDriveFolder(multiple: boolean) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -715,7 +782,7 @@ export async function pickEmoji(src: HTMLElement | null, opts) {
|
||||||
resolve(emoji);
|
resolve(emoji);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -724,7 +791,7 @@ export async function cropImage(
|
||||||
image: Misskey.entities.DriveFile,
|
image: Misskey.entities.DriveFile,
|
||||||
options: {
|
options: {
|
||||||
aspectRatio: number;
|
aspectRatio: number;
|
||||||
},
|
}
|
||||||
): Promise<Misskey.entities.DriveFile> {
|
): Promise<Misskey.entities.DriveFile> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
popup(
|
popup(
|
||||||
|
@ -742,7 +809,7 @@ export async function cropImage(
|
||||||
resolve(x);
|
resolve(x);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -757,7 +824,7 @@ let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
|
||||||
export async function openEmojiPicker(
|
export async function openEmojiPicker(
|
||||||
src?: HTMLElement,
|
src?: HTMLElement,
|
||||||
opts,
|
opts,
|
||||||
initialTextarea: typeof activeTextarea,
|
initialTextarea: typeof activeTextarea
|
||||||
) {
|
) {
|
||||||
if (openingEmojiPicker) return;
|
if (openingEmojiPicker) return;
|
||||||
|
|
||||||
|
@ -773,13 +840,14 @@ export async function openEmojiPicker(
|
||||||
const observer = new MutationObserver((records) => {
|
const observer = new MutationObserver((records) => {
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
for (const node of Array.from(record.addedNodes).filter(
|
for (const node of Array.from(record.addedNodes).filter(
|
||||||
(node) => node instanceof HTMLElement,
|
(node) => node instanceof HTMLElement
|
||||||
) as HTMLElement[]) {
|
) as HTMLElement[]) {
|
||||||
const textareas = node.querySelectorAll("textarea, input");
|
const textareas = node.querySelectorAll("textarea, input");
|
||||||
for (const textarea of Array.from(textareas).filter(
|
for (const textarea of Array.from(textareas).filter(
|
||||||
(textarea) => textarea.dataset.preventEmojiInsert == null,
|
(textarea) => textarea.dataset.preventEmojiInsert == null
|
||||||
)) {
|
)) {
|
||||||
if (document.activeElement === textarea) activeTextarea = textarea;
|
if (document.activeElement === textarea)
|
||||||
|
activeTextarea = textarea;
|
||||||
textarea.addEventListener("focus", () => {
|
textarea.addEventListener("focus", () => {
|
||||||
activeTextarea = textarea;
|
activeTextarea = textarea;
|
||||||
});
|
});
|
||||||
|
@ -817,7 +885,7 @@ export async function openEmojiPicker(
|
||||||
openingEmojiPicker = null;
|
openingEmojiPicker = null;
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -829,7 +897,7 @@ export function popupMenu(
|
||||||
width?: number;
|
width?: number;
|
||||||
viaKeyboard?: boolean;
|
viaKeyboard?: boolean;
|
||||||
noReturnFocus?: boolean;
|
noReturnFocus?: boolean;
|
||||||
},
|
}
|
||||||
) {
|
) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let dispose;
|
let dispose;
|
||||||
|
@ -852,7 +920,7 @@ export function popupMenu(
|
||||||
resolve();
|
resolve();
|
||||||
dispose();
|
dispose();
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
dispose = res.dispose;
|
dispose = res.dispose;
|
||||||
});
|
});
|
||||||
|
@ -861,7 +929,7 @@ export function popupMenu(
|
||||||
|
|
||||||
export function contextMenu(
|
export function contextMenu(
|
||||||
items: MenuItem[] | Ref<MenuItem[]>,
|
items: MenuItem[] | Ref<MenuItem[]>,
|
||||||
ev: MouseEvent,
|
ev: MouseEvent
|
||||||
) {
|
) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -881,7 +949,7 @@ export function contextMenu(
|
||||||
resolve();
|
resolve();
|
||||||
dispose();
|
dispose();
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
dispose = res.dispose;
|
dispose = res.dispose;
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,6 +47,7 @@ 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: () => {
|
||||||
|
@ -73,7 +74,7 @@ const sounds = ref({
|
||||||
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), {
|
||||||
|
|
|
@ -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:
|
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'}
|
||||||
|
|
|
@ -2,3 +2,4 @@ packages:
|
||||||
- 'client'
|
- 'client'
|
||||||
- 'sw'
|
- 'sw'
|
||||||
- 'calckey-js'
|
- '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::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()
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue