From 39f9ecd8d39459a533a141a1bbbf106f9bda7d0b Mon Sep 17 00:00:00 2001 From: Natty Date: Sat, 22 Jul 2023 23:36:46 +0200 Subject: [PATCH] Frontend: Added a frontend API and serving the list of sound files available and --- .dev/Caddyfile | 6 +- Cargo.lock | 1 + Cargo.toml | 1 + fe_calckey/Cargo.toml | 4 +- fe_calckey/frontend/assets/api-doc.png | Bin 10463 -> 0 bytes fe_calckey/frontend/assets/redoc.html | 23 - fe_calckey/frontend/client/.prettierrc | 24 +- fe_calckey/frontend/client/package.json | 1 + fe_calckey/frontend/client/src/config.ts | 1 + fe_calckey/frontend/client/src/os.ts | 1548 +++++++++-------- .../client/src/pages/settings/sounds.vue | 185 +- .../frontend/magnetar-common/.prettierrc | 7 + .../magnetar-common/package-lock.json | 29 + .../frontend/magnetar-common/package.json | 14 + .../frontend/magnetar-common/src/fe-api.ts | 28 + .../frontend/magnetar-common/tsconfig.json | 22 + fe_calckey/frontend/pnpm-lock.yaml | 15 + fe_calckey/frontend/pnpm-workspace.yaml | 1 + fe_calckey/src/frontend_api.rs | 41 + fe_calckey/src/frontend_render.rs | 2 +- fe_calckey/src/main.rs | 10 +- fe_calckey/src/static_serve.rs | 2 +- 22 files changed, 1086 insertions(+), 879 deletions(-) delete mode 100644 fe_calckey/frontend/assets/api-doc.png delete mode 100644 fe_calckey/frontend/assets/redoc.html create mode 100644 fe_calckey/frontend/magnetar-common/.prettierrc create mode 100644 fe_calckey/frontend/magnetar-common/package-lock.json create mode 100644 fe_calckey/frontend/magnetar-common/package.json create mode 100644 fe_calckey/frontend/magnetar-common/src/fe-api.ts create mode 100644 fe_calckey/frontend/magnetar-common/tsconfig.json create mode 100644 fe_calckey/src/frontend_api.rs diff --git a/.dev/Caddyfile b/.dev/Caddyfile index 8505b3a..8018ced 100644 --- a/.dev/Caddyfile +++ b/.dev/Caddyfile @@ -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 } diff --git a/Cargo.lock b/Cargo.lock index 3e20f30..d60fcdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -986,6 +986,7 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "walkdir", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c52c94d..a5b5ce5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/fe_calckey/Cargo.toml b/fe_calckey/Cargo.toml index f082f0e..90a01f5 100644 --- a/fe_calckey/Cargo.toml +++ b/fe_calckey/Cargo.toml @@ -27,4 +27,6 @@ serde = { workspace = true, features = ["derive"] } toml = { workspace = true } serde_json = { workspace = true } -chrono = { workspace = true } \ No newline at end of file +chrono = { workspace = true } + +walkdir = { workspace = true } \ No newline at end of file diff --git a/fe_calckey/frontend/assets/api-doc.png b/fe_calckey/frontend/assets/api-doc.png deleted file mode 100644 index 95fe6977fd4a0d84c2855da974dc4afb463eca62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10463 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 Wdd6-FrRasEh@dR5CRZ(E4*5TOnh06| diff --git a/fe_calckey/frontend/assets/redoc.html b/fe_calckey/frontend/assets/redoc.html deleted file mode 100644 index 6f48c17..0000000 --- a/fe_calckey/frontend/assets/redoc.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - Calckey API - - - - - - - - - - - - diff --git a/fe_calckey/frontend/client/.prettierrc b/fe_calckey/frontend/client/.prettierrc index 77a5016..47c6381 100644 --- a/fe_calckey/frontend/client/.prettierrc +++ b/fe_calckey/frontend/client/.prettierrc @@ -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" + } + } + ] } diff --git a/fe_calckey/frontend/client/package.json b/fe_calckey/frontend/client/package.json index 879d305..9aa1116 100644 --- a/fe_calckey/frontend/client/package.json +++ b/fe_calckey/frontend/client/package.json @@ -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", diff --git a/fe_calckey/frontend/client/src/config.ts b/fe_calckey/frontend/client/src/config.ts index 8a18ba9..52fc761 100644 --- a/fe_calckey/frontend/client/src/config.ts +++ b/fe_calckey/frontend/client/src/config.ts @@ -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`; diff --git a/fe_calckey/frontend/client/src/os.ts b/fe_calckey/frontend/client/src/os.ts index 32bb8f7..58145e5 100644 --- a/fe_calckey/frontend/client/src/os.ts +++ b/fe_calckey/frontend/client/src/os.ts @@ -1,10 +1,10 @@ // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する -import { Component, markRaw, Ref, ref, defineAsyncComponent } from "vue"; +import { Component, defineAsyncComponent, markRaw, ref, Ref } from "vue"; import { EventEmitter } from "eventemitter3"; import insertTextAtCursor from "insert-text-at-cursor"; import * as Misskey from "calckey-js"; -import { apiUrl, url } from "@/config"; +import { apiUrl, feApiUrl, url } from "@/config"; import MkPostFormDialog from "@/components/MkPostFormDialog.vue"; import MkWaitingDialog from "@/components/MkWaitingDialog.vue"; import MkToast from "@/components/MkToast.vue"; @@ -12,899 +12,967 @@ import MkDialog from "@/components/MkDialog.vue"; import { MenuItem } from "@/types/menu"; import { $i } from "@/account"; import { i18n } from "./i18n"; +import { + FrontendApiEndpoint, + FrontendApiEndpoints, +} from "magnetar-common/built/fe-api"; export const pendingApiRequestsCount = ref(0); const apiClient = new Misskey.api.APIClient({ - origin: url, + origin: url, }); +export async function feApi( + endpointDef: FrontendApiEndpoint< + FrontendApiEndpoints[T]["method"], + FrontendApiEndpoints[T]["path"], + FrontendApiEndpoints[T]["request"], + FrontendApiEndpoints[T]["response"] + >, + data: FrontendApiEndpoints[T]["request"], + token?: string | null | undefined +): Promise { + 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 = 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 = (( - endpoint: string, - data: Record = {}, - token?: string | null | undefined, + endpoint: string, + data: Record = {}, + token?: string | null | undefined ) => { - pendingApiRequestsCount.value++; + pendingApiRequestsCount.value++; - const onFinally = () => { - pendingApiRequestsCount.value--; - }; + const onFinally = () => { + pendingApiRequestsCount.value--; + }; - const authorizationToken = token ?? $i?.token ?? undefined; - const authorization = authorizationToken - ? `Bearer ${authorizationToken}` - : undefined; + const authorizationToken = token ?? $i?.token ?? undefined; + const authorization = authorizationToken + ? `Bearer ${authorizationToken}` + : undefined; - const promise = new Promise((resolve, reject) => { - fetch(endpoint.indexOf("://") > -1 ? endpoint : `${apiUrl}/${endpoint}`, { - method: "POST", - body: JSON.stringify(data), - credentials: "omit", - cache: "no-cache", - headers: authorization ? { authorization } : {}, - }) - .then(async (res) => { - const body = res.status === 204 ? null : await res.json(); + const promise = new Promise((resolve, reject) => { + fetch( + endpoint.indexOf("://") > -1 ? endpoint : `${apiUrl}/${endpoint}`, + { + method: "POST", + body: JSON.stringify(data), + 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); - } else if (res.status === 204) { - resolve(); - } else { - reject(body.error); - } - }) - .catch(reject); - }); + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(); + } else { + reject(body.error); + } + }) + .catch(reject); + }); - promise.then(onFinally, onFinally); + promise.then(onFinally, onFinally); - return promise; + return promise; }) as typeof apiClient.request; export const apiGet = (( - endpoint: string, - data: Record = {}, - token?: string | null | undefined, + endpoint: string, + data: Record = {}, + token?: string | null | undefined ) => { - pendingApiRequestsCount.value++; + pendingApiRequestsCount.value++; - const onFinally = () => { - pendingApiRequestsCount.value--; - }; + const onFinally = () => { + pendingApiRequestsCount.value--; + }; - const query = new URLSearchParams(data); + const query = new URLSearchParams(data); - const authorizationToken = token ?? $i?.token ?? undefined; - const authorization = authorizationToken - ? `Bearer ${authorizationToken}` - : undefined; + const authorizationToken = token ?? $i?.token ?? undefined; + const authorization = authorizationToken + ? `Bearer ${authorizationToken}` + : undefined; - const promise = new Promise((resolve, reject) => { - // Send request - fetch(`${apiUrl}/${endpoint}?${query}`, { - method: "GET", - credentials: "omit", - cache: "default", - headers: authorization ? { authorization } : {}, - }) - .then(async (res) => { - const body = res.status === 204 ? null : await res.json(); + const promise = new Promise((resolve, reject) => { + // Send request + fetch(`${apiUrl}/${endpoint}?${query}`, { + method: "GET", + credentials: "omit", + cache: "default", + headers: authorization ? { authorization } : {}, + }) + .then(async (res) => { + const body = res.status === 204 ? null : await res.json(); - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(); - } else { - reject(body.error); - } - }) - .catch(reject); - }); + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(); + } else { + reject(body.error); + } + }) + .catch(reject); + }); - promise.then(onFinally, onFinally); + promise.then(onFinally, onFinally); - return promise; + return promise; }) as typeof apiClient.request; export const apiWithDialog = (( - endpoint: string, - data: Record = {}, - token?: string | null | undefined, + endpoint: string, + data: Record = {}, + token?: string | null | undefined ) => { - const promise = api(endpoint, data, token); - promiseDialog(promise, null, (err) => { - alert({ - type: "error", - text: err.message + "\n" + (err as any).id, - }); - }); + const promise = api(endpoint, data, token); + promiseDialog(promise, null, (err) => { + alert({ + type: "error", + text: err.message + "\n" + (err as any).id, + }); + }); - return promise; + return promise; }) as typeof api; export function promiseDialog>( - promise: T, - onSuccess?: ((res: any) => void) | null, - onFailure?: ((err: Error) => void) | null, - text?: string, + promise: T, + onSuccess?: ((res: any) => void) | null, + onFailure?: ((err: Error) => void) | null, + text?: string ): T { - const showing = ref(true); - const success = ref(false); + const showing = ref(true); + const success = ref(false); - promise - .then((res) => { - if (onSuccess) { - showing.value = false; - onSuccess(res); - } else { - success.value = true; - window.setTimeout(() => { - showing.value = false; - }, 1000); - } - }) - .catch((err) => { - showing.value = false; - if (onFailure) { - onFailure(err); - } else { - alert({ - type: "error", - text: err, - }); - } - }); + promise + .then((res) => { + if (onSuccess) { + showing.value = false; + onSuccess(res); + } else { + success.value = true; + window.setTimeout(() => { + showing.value = false; + }, 1000); + } + }) + .catch((err) => { + showing.value = false; + if (onFailure) { + onFailure(err); + } else { + alert({ + type: "error", + text: err, + }); + } + }); - // NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない) - popup( - MkWaitingDialog, - { - success: success, - showing: showing, - text: text, - }, - {}, - "closed", - ); + // NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない) + popup( + MkWaitingDialog, + { + success: success, + showing: showing, + text: text, + }, + {}, + "closed" + ); - return promise; + return promise; } let popupIdCount = 0; export const popups = ref([]) as Ref< - { - id: any; - component: any; - props: Record; - }[] + { + id: any; + component: any; + props: Record; + }[] >; const zIndexes = { - low: 1000000, - middle: 2000000, - high: 3000000, + low: 1000000, + middle: 2000000, + high: 3000000, }; export function claimZIndex( - priority: "low" | "middle" | "high" = "low", + priority: "low" | "middle" | "high" = "low" ): number { - zIndexes[priority] += 100; - return zIndexes[priority]; + zIndexes[priority] += 100; + return zIndexes[priority]; } let uniqueId = 0; export function getUniqueId(): string { - return uniqueId++ + ""; + return uniqueId++ + ""; } export async function popup( - component: Component, - props: Record, - events = {}, - disposeEvent?: string, + component: Component, + props: Record, + events = {}, + disposeEvent?: string ) { - markRaw(component); + markRaw(component); - const id = ++popupIdCount; - const dispose = () => { - // このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ? - window.setTimeout(() => { - popups.value = popups.value.filter((popup) => popup.id !== id); - }, 0); - }; - const state = { - component, - props, - events: disposeEvent - ? { - ...events, - [disposeEvent]: dispose, - } - : events, - id, - }; + const id = ++popupIdCount; + const dispose = () => { + // このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ? + window.setTimeout(() => { + popups.value = popups.value.filter((popup) => popup.id !== id); + }, 0); + }; + const state = { + component, + props, + events: disposeEvent + ? { + ...events, + [disposeEvent]: dispose, + } + : events, + id, + }; - popups.value.push(state); + popups.value.push(state); - return { - dispose, - }; + return { + dispose, + }; } export function pageWindow(path: string) { - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkPageWindow.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { - initialPath: path, - }, - {}, - "closed", - ); + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkPageWindow.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { + initialPath: path, + }, + {}, + "closed" + ); } export function modalPageWindow(path: string) { - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkModalPageWindow.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { - initialPath: path, - }, - {}, - "closed", - ); + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkModalPageWindow.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { + initialPath: path, + }, + {}, + "closed" + ); } export function toast(message: string) { - popup( - MkToast, - { - message, - }, - {}, - "closed", - ); + popup( + MkToast, + { + message, + }, + {}, + "closed" + ); } export function alert(props: { - type?: "error" | "info" | "success" | "warning" | "waiting" | "question"; - title?: string | null; - text?: string | null; + type?: "error" | "info" | "success" | "warning" | "waiting" | "question"; + title?: string | null; + text?: string | null; }): Promise { - return new Promise((resolve, reject) => { - if (props.text == null && props.type === "error") { - props.text = i18n.ts.somethingHappened; - } - popup( - MkDialog, - props, - { - done: (result) => { - resolve(); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + if (props.text == null && props.type === "error") { + props.text = i18n.ts.somethingHappened; + } + popup( + MkDialog, + props, + { + done: (result) => { + resolve(); + }, + }, + "closed" + ); + }); } export function confirm(props: { - type: "error" | "info" | "success" | "warning" | "waiting" | "question"; - title?: string | null; - text?: string | null; - okText?: string; - cancelText?: string; + type: "error" | "info" | "success" | "warning" | "waiting" | "question"; + title?: string | null; + text?: string | null; + okText?: string; + cancelText?: string; }): Promise<{ canceled: boolean }> { - return new Promise((resolve, reject) => { - popup( - MkDialog, - { - ...props, - showCancelButton: true, - }, - { - done: (result) => { - resolve(result ? result : { canceled: true }); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + MkDialog, + { + ...props, + showCancelButton: true, + }, + { + done: (result) => { + resolve(result ? result : { canceled: true }); + }, + }, + "closed" + ); + }); } export function yesno(props: { - type: "error" | "info" | "success" | "warning" | "waiting" | "question"; - title?: string | null; - text?: string | null; + type: "error" | "info" | "success" | "warning" | "waiting" | "question"; + title?: string | null; + text?: string | null; }): Promise<{ canceled: boolean }> { - return new Promise((resolve, reject) => { - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkDialog.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { - ...props, - showCancelButton: true, - isYesNo: true, - }, - { - done: (result) => { - resolve(result ? result : { canceled: true }); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkDialog.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { + ...props, + showCancelButton: true, + isYesNo: true, + }, + { + done: (result) => { + resolve(result ? result : { canceled: true }); + }, + }, + "closed" + ); + }); } export function inputText(props: { - type?: "text" | "email" | "password" | "url" | "search"; - title?: string | null; - text?: string | null; - placeholder?: string | null; - autocomplete?: string; - default?: string | null; - minLength?: number; - maxLength?: number; + type?: "text" | "email" | "password" | "url" | "search"; + title?: string | null; + text?: string | null; + placeholder?: string | null; + autocomplete?: string; + default?: string | null; + minLength?: number; + maxLength?: number; }): Promise< - | { canceled: true; result: undefined } - | { - canceled: false; - result: string; - } + | { canceled: true; result: undefined } + | { + canceled: false; + result: string; + } > { - return new Promise((resolve, reject) => { - popup( - MkDialog, - { - title: props.title, - text: props.text, - input: { - type: props.type, - placeholder: props.placeholder, - autocomplete: props.autocomplete, - default: props.default, - minLength: props.minLength, - maxLength: props.maxLength, - }, - }, - { - done: (result) => { - resolve(result ? result : { canceled: true }); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + MkDialog, + { + title: props.title, + text: props.text, + input: { + type: props.type, + placeholder: props.placeholder, + autocomplete: props.autocomplete, + default: props.default, + minLength: props.minLength, + maxLength: props.maxLength, + }, + }, + { + done: (result) => { + resolve(result ? result : { canceled: true }); + }, + }, + "closed" + ); + }); } export function inputParagraph(props: { - title?: string | null; - text?: string | null; - placeholder?: string | null; - default?: string | null; + title?: string | null; + text?: string | null; + placeholder?: string | null; + default?: string | null; }): Promise< - | { canceled: true; result: undefined } - | { - canceled: false; - result: string; - } + | { canceled: true; result: undefined } + | { + canceled: false; + result: string; + } > { - return new Promise((resolve, reject) => { - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkDialog.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { - title: props.title, - text: props.text, - input: { - type: "paragraph", - placeholder: props.placeholder, - default: props.default, - }, - }, - { - done: (result) => { - resolve(result ? result : { canceled: true }); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkDialog.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { + title: props.title, + text: props.text, + input: { + type: "paragraph", + placeholder: props.placeholder, + default: props.default, + }, + }, + { + done: (result) => { + resolve(result ? result : { canceled: true }); + }, + }, + "closed" + ); + }); } export function inputNumber(props: { - title?: string | null; - text?: string | null; - placeholder?: string | null; - default?: number | null; - autocomplete?: string; + title?: string | null; + text?: string | null; + placeholder?: string | null; + default?: number | null; + autocomplete?: string; }): Promise< - | { canceled: true; result: undefined } - | { - canceled: false; - result: number; - } + | { canceled: true; result: undefined } + | { + canceled: false; + result: number; + } > { - return new Promise((resolve, reject) => { - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkDialog.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { - title: props.title, - text: props.text, - input: { - type: "number", - placeholder: props.placeholder, - autocomplete: props.autocomplete, - default: props.default, - }, - }, - { - done: (result) => { - resolve(result ? result : { canceled: true }); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkDialog.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { + title: props.title, + text: props.text, + input: { + type: "number", + placeholder: props.placeholder, + autocomplete: props.autocomplete, + default: props.default, + }, + }, + { + done: (result) => { + resolve(result ? result : { canceled: true }); + }, + }, + "closed" + ); + }); } export function inputDate(props: { - title?: string | null; - text?: string | null; - placeholder?: string | null; - default?: Date | null; + title?: string | null; + text?: string | null; + placeholder?: string | null; + default?: Date | null; }): Promise< - | { canceled: true; result: undefined } - | { - canceled: false; - result: Date; - } + | { canceled: true; result: undefined } + | { + canceled: false; + result: Date; + } > { - return new Promise((resolve, reject) => { - popup( - MkDialog, - { - title: props.title, - text: props.text, - input: { - type: "date", - placeholder: props.placeholder, - default: props.default, - }, - }, - { - done: (result) => { - resolve( - result - ? { - result: new Date(result.result), - canceled: false, - } - : { canceled: true }, - ); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + MkDialog, + { + title: props.title, + text: props.text, + input: { + type: "date", + placeholder: props.placeholder, + default: props.default, + }, + }, + { + done: (result) => { + resolve( + result + ? { + result: new Date(result.result), + canceled: false, + } + : { canceled: true } + ); + }, + }, + "closed" + ); + }); } export function select( - props: { - title?: string | null; - text?: string | null; - default?: string | null; - } & ( - | { - items: { - value: C; - text: string; - }[]; - } - | { - groupedItems: { - label: string; - items: { - value: C; - text: string; - }[]; - }[]; - } - ), + props: { + title?: string | null; + text?: string | null; + default?: string | null; + } & ( + | { + items: { + value: C; + text: string; + }[]; + } + | { + groupedItems: { + label: string; + items: { + value: C; + text: string; + }[]; + }[]; + } + ) ): Promise< - | { canceled: true; result: undefined } - | { - canceled: false; - result: C; - } + | { canceled: true; result: undefined } + | { + canceled: false; + result: C; + } > { - return new Promise((resolve, reject) => { - popup( - MkDialog, - { - title: props.title, - text: props.text, - select: { - items: props.items, - groupedItems: props.groupedItems, - default: props.default, - }, - }, - { - done: (result) => { - resolve(result ? result : { canceled: true }); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + MkDialog, + { + title: props.title, + text: props.text, + select: { + items: props.items, + groupedItems: props.groupedItems, + default: props.default, + }, + }, + { + done: (result) => { + resolve(result ? result : { canceled: true }); + }, + }, + "closed" + ); + }); } export function success(): Promise { - return new Promise((resolve, reject) => { - const showing = ref(true); - window.setTimeout(() => { - showing.value = false; - }, 1000); - popup( - MkWaitingDialog, - { - success: true, - showing: showing, - }, - { - done: () => resolve(), - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + const showing = ref(true); + window.setTimeout(() => { + showing.value = false; + }, 1000); + popup( + MkWaitingDialog, + { + success: true, + showing: showing, + }, + { + done: () => resolve(), + }, + "closed" + ); + }); } export function waiting(): Promise { - return new Promise((resolve, reject) => { - const showing = ref(true); - popup( - MkWaitingDialog, - { - success: false, - showing: showing, - }, - { - done: () => resolve(), - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + const showing = ref(true); + popup( + MkWaitingDialog, + { + success: false, + showing: showing, + }, + { + done: () => resolve(), + }, + "closed" + ); + }); } export function form(title, form) { - return new Promise((resolve, reject) => { - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkFormDialog.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { title, form }, - { - done: (result) => { - resolve(result); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkFormDialog.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { title, form }, + { + done: (result) => { + resolve(result); + }, + }, + "closed" + ); + }); } export async function selectUser() { - return new Promise((resolve, reject) => { - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkUserSelectDialog.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - {}, - { - ok: (user) => { - resolve(user); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkUserSelectDialog.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + {}, + { + ok: (user) => { + resolve(user); + }, + }, + "closed" + ); + }); } export async function selectInstance(): Promise { - return new Promise((resolve, reject) => { - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkInstanceSelectDialog.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - {}, - { - ok: (instance) => { - resolve(instance); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkInstanceSelectDialog.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + {}, + { + ok: (instance) => { + resolve(instance); + }, + }, + "closed" + ); + }); } export async function selectDriveFile(multiple: boolean) { - return new Promise((resolve, reject) => { - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkDriveSelectDialog.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { - type: "file", - multiple, - }, - { - done: (files) => { - if (files) { - resolve(multiple ? files : files[0]); - } - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkDriveSelectDialog.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { + type: "file", + multiple, + }, + { + done: (files) => { + if (files) { + resolve(multiple ? files : files[0]); + } + }, + }, + "closed" + ); + }); } export async function selectDriveFolder(multiple: boolean) { - return new Promise((resolve, reject) => { - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkDriveSelectDialog.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { - type: "folder", - multiple, - }, - { - done: (folders) => { - if (folders) { - resolve(multiple ? folders : folders[0]); - } - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkDriveSelectDialog.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { + type: "folder", + multiple, + }, + { + done: (folders) => { + if (folders) { + resolve(multiple ? folders : folders[0]); + } + }, + }, + "closed" + ); + }); } export async function pickEmoji(src: HTMLElement | null, opts) { - return new Promise((resolve, reject) => { - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkEmojiPickerDialog.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { - src, - ...opts, - }, - { - done: (emoji) => { - resolve(emoji); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkEmojiPickerDialog.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { + src, + ...opts, + }, + { + done: (emoji) => { + resolve(emoji); + }, + }, + "closed" + ); + }); } export async function cropImage( - image: Misskey.entities.DriveFile, - options: { - aspectRatio: number; - }, + image: Misskey.entities.DriveFile, + options: { + aspectRatio: number; + } ): Promise { - return new Promise((resolve, reject) => { - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkCropperDialog.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { - file: image, - aspectRatio: options.aspectRatio, - }, - { - ok: (x) => { - resolve(x); - }, - }, - "closed", - ); - }); + return new Promise((resolve, reject) => { + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkCropperDialog.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { + file: image, + aspectRatio: options.aspectRatio, + }, + { + ok: (x) => { + resolve(x); + }, + }, + "closed" + ); + }); } type AwaitType = T extends Promise - ? U - : T extends (...args: any[]) => Promise - ? V - : T; + ? U + : T extends (...args: any[]) => Promise + ? V + : T; let openingEmojiPicker: AwaitType> | null = null; let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null; export async function openEmojiPicker( - src?: HTMLElement, - opts, - initialTextarea: typeof activeTextarea, + src?: HTMLElement, + opts, + initialTextarea: typeof activeTextarea ) { - if (openingEmojiPicker) return; + if (openingEmojiPicker) return; - activeTextarea = initialTextarea; + activeTextarea = initialTextarea; - const textareas = document.querySelectorAll("textarea, input"); - for (const textarea of Array.from(textareas)) { - textarea.addEventListener("focus", () => { - activeTextarea = textarea; - }); - } + const textareas = document.querySelectorAll("textarea, input"); + for (const textarea of Array.from(textareas)) { + textarea.addEventListener("focus", () => { + activeTextarea = textarea; + }); + } - const observer = new MutationObserver((records) => { - for (const record of records) { - for (const node of Array.from(record.addedNodes).filter( - (node) => node instanceof HTMLElement, - ) as HTMLElement[]) { - const textareas = node.querySelectorAll("textarea, input"); - for (const textarea of Array.from(textareas).filter( - (textarea) => textarea.dataset.preventEmojiInsert == null, - )) { - if (document.activeElement === textarea) activeTextarea = textarea; - textarea.addEventListener("focus", () => { - activeTextarea = textarea; - }); - } - } - } - }); + const observer = new MutationObserver((records) => { + for (const record of records) { + for (const node of Array.from(record.addedNodes).filter( + (node) => node instanceof HTMLElement + ) as HTMLElement[]) { + const textareas = node.querySelectorAll("textarea, input"); + for (const textarea of Array.from(textareas).filter( + (textarea) => textarea.dataset.preventEmojiInsert == null + )) { + if (document.activeElement === textarea) + activeTextarea = textarea; + textarea.addEventListener("focus", () => { + activeTextarea = textarea; + }); + } + } + } + }); - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: false, - characterData: false, - }); + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: false, + characterData: false, + }); - openingEmojiPicker = await popup( - defineAsyncComponent({ - loader: () => import("@/components/MkEmojiPickerDialog.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { - src, - ...opts, - }, - { - chosen: (emoji) => { - insertTextAtCursor(activeTextarea, emoji); - }, - done: (emoji) => { - insertTextAtCursor(activeTextarea, emoji); - }, - closed: () => { - openingEmojiPicker!.dispose(); - openingEmojiPicker = null; - observer.disconnect(); - }, - }, - ); + openingEmojiPicker = await popup( + defineAsyncComponent({ + loader: () => import("@/components/MkEmojiPickerDialog.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { + src, + ...opts, + }, + { + chosen: (emoji) => { + insertTextAtCursor(activeTextarea, emoji); + }, + done: (emoji) => { + insertTextAtCursor(activeTextarea, emoji); + }, + closed: () => { + openingEmojiPicker!.dispose(); + openingEmojiPicker = null; + observer.disconnect(); + }, + } + ); } export function popupMenu( - items: MenuItem[] | Ref, - src?: HTMLElement, - options?: { - align?: string; - width?: number; - viaKeyboard?: boolean; - noReturnFocus?: boolean; - }, + items: MenuItem[] | Ref, + src?: HTMLElement, + options?: { + align?: string; + width?: number; + viaKeyboard?: boolean; + noReturnFocus?: boolean; + } ) { - return new Promise((resolve, reject) => { - let dispose; - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkPopupMenu.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { - items, - src, - width: options?.width, - align: options?.align, - viaKeyboard: options?.viaKeyboard, - noReturnFocus: options?.noReturnFocus, - }, - { - closed: () => { - resolve(); - dispose(); - }, - }, - ).then((res) => { - dispose = res.dispose; - }); - }); + return new Promise((resolve, reject) => { + let dispose; + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkPopupMenu.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { + items, + src, + width: options?.width, + align: options?.align, + viaKeyboard: options?.viaKeyboard, + noReturnFocus: options?.noReturnFocus, + }, + { + closed: () => { + resolve(); + dispose(); + }, + } + ).then((res) => { + dispose = res.dispose; + }); + }); } export function contextMenu( - items: MenuItem[] | Ref, - ev: MouseEvent, + items: MenuItem[] | Ref, + ev: MouseEvent ) { - ev.preventDefault(); - return new Promise((resolve, reject) => { - let dispose; - popup( - defineAsyncComponent({ - loader: () => import("@/components/MkContextMenu.vue"), - loadingComponent: MkWaitingDialog, - delay: 1000, - }), - { - items, - ev, - }, - { - closed: () => { - resolve(); - dispose(); - }, - }, - ).then((res) => { - dispose = res.dispose; - }); - }); + ev.preventDefault(); + return new Promise((resolve, reject) => { + let dispose; + popup( + defineAsyncComponent({ + loader: () => import("@/components/MkContextMenu.vue"), + loadingComponent: MkWaitingDialog, + delay: 1000, + }), + { + items, + ev, + }, + { + closed: () => { + resolve(); + dispose(); + }, + } + ).then((res) => { + dispose = res.dispose; + }); + }); } export function post(props: Record = {}) { - return new Promise((resolve, reject) => { - // NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない - // NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、 - // Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、 - // 複数のpost formを開いたときに場合によってはエラーになる - // もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが - let dispose; - popup(MkPostFormDialog, props, { - closed: () => { - resolve(); - dispose(); - }, - }).then((res) => { - dispose = res.dispose; - }); - }); + return new Promise((resolve, reject) => { + // NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない + // NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、 + // Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、 + // 複数のpost formを開いたときに場合によってはエラーになる + // もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが + let dispose; + popup(MkPostFormDialog, props, { + closed: () => { + resolve(); + dispose(); + }, + }).then((res) => { + dispose = res.dispose; + }); + }); } export const deckGlobalEvents = new EventEmitter(); diff --git a/fe_calckey/frontend/client/src/pages/settings/sounds.vue b/fe_calckey/frontend/client/src/pages/settings/sounds.vue index d01fc36..bc7d864 100644 --- a/fe_calckey/frontend/client/src/pages/settings/sounds.vue +++ b/fe_calckey/frontend/client/src/pages/settings/sounds.vue @@ -1,40 +1,40 @@ diff --git a/fe_calckey/frontend/magnetar-common/.prettierrc b/fe_calckey/frontend/magnetar-common/.prettierrc new file mode 100644 index 0000000..0601cc1 --- /dev/null +++ b/fe_calckey/frontend/magnetar-common/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 4, + "useTabs": false, + "singleQuote": false, + "semi": true, + "bracketSameLine": true +} diff --git a/fe_calckey/frontend/magnetar-common/package-lock.json b/fe_calckey/frontend/magnetar-common/package-lock.json new file mode 100644 index 0000000..b52515e --- /dev/null +++ b/fe_calckey/frontend/magnetar-common/package-lock.json @@ -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" + } + } + } +} diff --git a/fe_calckey/frontend/magnetar-common/package.json b/fe_calckey/frontend/magnetar-common/package.json new file mode 100644 index 0000000..9546eca --- /dev/null +++ b/fe_calckey/frontend/magnetar-common/package.json @@ -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" + } +} diff --git a/fe_calckey/frontend/magnetar-common/src/fe-api.ts b/fe_calckey/frontend/magnetar-common/src/fe-api.ts new file mode 100644 index 0000000..a7a8e84 --- /dev/null +++ b/fe_calckey/frontend/magnetar-common/src/fe-api.ts @@ -0,0 +1,28 @@ +type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + +export interface FrontendApiEndpoint { + method: M; + path: P; + request?: T; + response?: R; +} + +function endpointDef( + method: Method, + path: string +): FrontendApiEndpoint { + 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]; +}; diff --git a/fe_calckey/frontend/magnetar-common/tsconfig.json b/fe_calckey/frontend/magnetar-common/tsconfig.json new file mode 100644 index 0000000..47f57ab --- /dev/null +++ b/fe_calckey/frontend/magnetar-common/tsconfig.json @@ -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", + ] +} \ No newline at end of file diff --git a/fe_calckey/frontend/pnpm-lock.yaml b/fe_calckey/frontend/pnpm-lock.yaml index b46abae..d9c2c28 100644 --- a/fe_calckey/frontend/pnpm-lock.yaml +++ b/fe_calckey/frontend/pnpm-lock.yaml @@ -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'} diff --git a/fe_calckey/frontend/pnpm-workspace.yaml b/fe_calckey/frontend/pnpm-workspace.yaml index aa05066..0bc81b7 100644 --- a/fe_calckey/frontend/pnpm-workspace.yaml +++ b/fe_calckey/frontend/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - 'client' - 'sw' - 'calckey-js' + - "magnetar-common" diff --git a/fe_calckey/src/frontend_api.rs b/fe_calckey/src/frontend_api.rs new file mode 100644 index 0000000..f05fdaf --- /dev/null +++ b/fe_calckey/src/frontend_api.rs @@ -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) -> Json> { + 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) +} diff --git a/fe_calckey/src/frontend_render.rs b/fe_calckey/src/frontend_render.rs index f97c7db..fbaf06b 100644 --- a/fe_calckey/src/frontend_render.rs +++ b/fe_calckey/src/frontend_render.rs @@ -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() diff --git a/fe_calckey/src/main.rs b/fe_calckey/src/main.rs index fc0127c..40b4a7c 100644 --- a/fe_calckey/src/main.rs +++ b/fe_calckey/src/main.rs @@ -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 Result