optimize fm (#82)

* optimize fm

* chore: auto-fix linting and formatting issues

* fix: type

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: uubulb <uubulb@users.noreply.github.com>
Co-authored-by: naiba <hi@nai.ba>
Co-authored-by: naiba <naiba@users.noreply.github.com>
This commit is contained in:
UUBulb
2024-12-29 22:19:50 +08:00
committed by GitHub
parent 97a5deb648
commit 01add8b160
12 changed files with 164 additions and 120 deletions

View File

@@ -25,7 +25,7 @@ let lastestRefreshTokenAt = 0
export async function fetcher<T>(method: FetcherMethod, path: string, data?: any): Promise<T> {
let response
if (method === FetcherMethod.GET || method === FetcherMethod.DELETE) {
if (method === FetcherMethod.GET || method === FetcherMethod.DELETE) {
response = await fetch(buildUrl(path, data), {
method: "GET",
})

View File

@@ -7,27 +7,45 @@ export enum Oauth2RequestType {
BIND = 2,
}
export const getOauth2RedirectURL = async (provider: string, rType: Oauth2RequestType): Promise<ModelOauth2LoginResponse> => {
export const getOauth2RedirectURL = async (
provider: string,
rType: Oauth2RequestType,
): Promise<ModelOauth2LoginResponse> => {
const sType = "type"
return fetcher<ModelOauth2LoginResponse>(FetcherMethod.GET, `/api/v1/oauth2/${provider}`, {
"type": rType,
sType: rType,
})
}
export const bindOauth2 = async (provider: string, state: string, code: string): Promise<ModelOauth2LoginResponse> => {
return fetcher<ModelOauth2LoginResponse>(FetcherMethod.POST, `/api/v1/oauth2/${provider}/bind`, {
"state": state,
"code": code,
})
export const bindOauth2 = async (
provider: string,
state: string,
code: string,
): Promise<ModelOauth2LoginResponse> => {
return fetcher<ModelOauth2LoginResponse>(
FetcherMethod.POST,
`/api/v1/oauth2/${provider}/bind`,
{
state: state,
code: code,
},
)
}
export const unbindOauth2 = async (provider: string): Promise<ModelOauth2LoginResponse> => {
return fetcher<ModelOauth2LoginResponse>(FetcherMethod.POST, `/api/v1/oauth2/${provider}/unbind`)
return fetcher<ModelOauth2LoginResponse>(
FetcherMethod.POST,
`/api/v1/oauth2/${provider}/unbind`,
)
}
export const oauth2callback = async (provider: string, state: string, code: string): Promise<void> => {
export const oauth2callback = async (
provider: string,
state: string,
code: string,
): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, `/api/v1/oauth2/${provider}/callback`, {
state, code
state,
code,
})
}

View File

@@ -41,6 +41,7 @@ import { HTMLAttributes, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { Button } from "./ui/button"
import { TableCell, TableRow } from "./ui/table"
import { Filepath } from "./xui/filepath"
import { IconButton } from "./xui/icon-button"
@@ -102,7 +103,7 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
header: () => <span>{t("Actions")}</span>,
id: "download",
cell: ({ row }) => {
return (
return row.original.type == 0 ? (
<IconButton
variant="ghost"
icon="download"
@@ -111,6 +112,8 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
downloadFile(row.original.name)
}}
/>
) : (
<Button size="icon" variant="ghost" className="pointer-events-none" />
)
},
},
@@ -157,42 +160,6 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
}
}
worker.onmessage = async (event: MessageEvent<FMWorkerData>) => {
switch (event.data.type) {
case FMWorkerOpcode.Error: {
console.error("Error from worker", event.data.error)
break
}
case FMWorkerOpcode.Progress: {
handleReady.current = true
break
}
case FMWorkerOpcode.Result: {
handleReady.current = false
if (event.data.blob && event.data.fileName) {
const url = URL.createObjectURL(event.data.blob)
const anchor = document.createElement("a")
anchor.href = url
anchor.download = event.data.fileName
anchor.click()
URL.revokeObjectURL(url)
}
firstChunk.current = true
if (dOpen) setdOpen(false)
break
}
}
}
const [currentPath, setPath] = useState("")
useEffect(() => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
listFile()
}
}, [wsRef.current, currentPath])
useEffect(() => {
const ws = new WebSocket(wsUrl)
wsRef.current = ws
@@ -209,27 +176,27 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
description: t("Results.UnExpectedError"),
})
}
ws.onmessage = async (e) => {
ws.onmessage = async (e: MessageEvent<ArrayBufferLike>) => {
try {
const buf: ArrayBufferLike = e.data
const identifier = new Uint8Array(e.data, 0, 4)
if (arraysEqual(identifier, FMIdentifier.error)) {
const errBytes = e.data.slice(4)
const errMsg = new TextDecoder("utf-8").decode(errBytes)
throw new Error(errMsg)
}
if (firstChunk.current) {
const identifier = new Uint8Array(buf, 0, 4)
if (arraysEqual(identifier, FMIdentifier.file)) {
worker.postMessage({
operation: 1,
arrayBuffer: buf,
arrayBuffer: e.data,
fileName: currentBasename.current,
})
firstChunk.current = false
} else if (arraysEqual(identifier, FMIdentifier.fileName)) {
const { path, fmList } = await fm.parseFMList(buf)
const { path, fmList } = await fm.parseFMList(e.data)
setPath(path)
setFMEntries(fmList)
} else if (arraysEqual(identifier, FMIdentifier.error)) {
const errBytes = buf.slice(4)
const errMsg = new TextDecoder("utf-8").decode(errBytes)
throw new Error(errMsg)
} else if (arraysEqual(identifier, FMIdentifier.complete)) {
// Upload completed
if (uOpen) setuOpen(false)
@@ -241,7 +208,7 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
await waitForHandleReady()
worker.postMessage({
operation: 2,
arrayBuffer: buf,
arrayBuffer: e.data,
fileName: currentBasename.current,
})
}
@@ -250,12 +217,61 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
toast("FM" + " " + t("Error"), {
description: t("Results.UnExpectedError"),
})
if (dOpen) setdOpen(false)
if (uOpen) setuOpen(false)
setdOpen(false)
setuOpen(false)
}
}
}, [wsUrl])
useEffect(() => {
worker.onmessage = async (event: MessageEvent<FMWorkerData>) => {
switch (event.data.type) {
case FMWorkerOpcode.Error: {
console.error("Error from worker", event.data.error)
break
}
case FMWorkerOpcode.Progress: {
handleReady.current = true
break
}
case FMWorkerOpcode.Result: {
handleReady.current = false
if (event.data.blob && event.data.fileName) {
const url = URL.createObjectURL(event.data.blob)
const anchor = document.createElement("a")
anchor.href = url
anchor.download = event.data.fileName
anchor.click()
URL.revokeObjectURL(url)
}
firstChunk.current = true
if (dOpen) setdOpen(false)
break
}
}
}
const handleBeforeUnload = () => {
worker.postMessage({
operation: 3,
})
}
window.addEventListener("beforeunload", handleBeforeUnload)
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
}
}, [worker, dOpen])
const [currentPath, setPath] = useState("")
useEffect(() => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
listFile()
}
}, [wsRef.current, currentPath])
const listFile = () => {
const prefix = new Int8Array([FMOpcode.List])
const pathMsg = new TextEncoder().encode(currentPath)
@@ -317,7 +333,7 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
toast("FM" + " " + t("Error"), {
description: error.message,
})
console.log("copy error: ", error)
console.error("copy error: ", error)
}
}}
>

View File

@@ -8,7 +8,7 @@ import {
import { useAuth } from "@/hooks/useAuth"
import useSettings from "@/hooks/useSetting"
import { copyToClipboard } from "@/lib/utils"
import { ModelProfile, ModelConfig } from "@/types"
import { ModelConfig, ModelProfile } from "@/types"
import i18next from "i18next"
import { Check, Clipboard } from "lucide-react"
import { forwardRef, useState } from "react"

View File

@@ -52,7 +52,7 @@ export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => {
setPath("/")
}}
>
{"/"}
/
</p>
</BreadcrumbItem>
<BreadcrumbSeparator />

View File

@@ -1,3 +1,4 @@
import { oauth2callback } from "@/api/oauth2"
import { getProfile, login as loginRequest } from "@/api/user"
import { AuthContextProps } from "@/types"
import { createContext, useContext, useEffect, useMemo } from "react"
@@ -5,13 +6,12 @@ import { useNavigate } from "react-router-dom"
import { toast } from "sonner"
import { useMainStore } from "./useMainStore"
import { oauth2callback } from "@/api/oauth2"
const AuthContext = createContext<AuthContextProps>({
profile: undefined,
login: () => { },
loginOauth2: () => { },
logout: () => { },
login: () => {},
loginOauth2: () => {},
logout: () => {},
})
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
@@ -19,7 +19,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const setProfile = useMainStore((store) => store.setProfile)
useEffect(() => {
; (async () => {
;(async () => {
try {
const user = await getProfile()
user.role = user.role || 0

View File

@@ -3,6 +3,8 @@ import { GithubComNezhahqNezhaModelSettingResponseModelConfig } from "@/types"
import useSWR from "swr"
export default function useSetting() {
return useSWR<GithubComNezhahqNezhaModelSettingResponseModelConfig>("/api/v1/setting", swrFetcher)
return useSWR<GithubComNezhahqNezhaModelSettingResponseModelConfig>(
"/api/v1/setting",
swrFetcher,
)
}

View File

@@ -38,9 +38,8 @@ onmessage = async function (event) {
throw new Error("accessHandle is undefined")
}
const dataChunk = arrayBuffer
accessHandle.write(dataChunk, { at: receivedLength })
receivedLength += dataChunk.byteLength
accessHandle.write(arrayBuffer, { at: receivedLength })
receivedLength += arrayBuffer.byteLength
if (receivedLength === expectedLength) {
accessHandle.flush()

View File

@@ -1,4 +1,4 @@
import { getOauth2RedirectURL, Oauth2RequestType } from "@/api/oauth2"
import { Oauth2RequestType, getOauth2RedirectURL } from "@/api/oauth2"
import { Button } from "@/components/ui/button"
import {
Form,
@@ -59,7 +59,7 @@ function Login() {
window.location.href = redirectUrl.redirect!
} catch (error: any) {
toast.error(error.message)
}
}
}
const { t } = useTranslation()
@@ -103,9 +103,9 @@ function Login() {
</form>
</Form>
<div className="mt-4">
{settingData?.config?.oauth2_providers?.map((p: string) =>
{settingData?.config?.oauth2_providers?.map((p: string) => (
<Button onClick={() => loginWith(p)}>{p}</Button>
)}
))}
</div>
</div>
)

View File

@@ -1,4 +1,4 @@
import { bindOauth2, getOauth2RedirectURL, Oauth2RequestType, unbindOauth2 } from "@/api/oauth2"
import { Oauth2RequestType, bindOauth2, getOauth2RedirectURL, unbindOauth2 } from "@/api/oauth2"
import { getProfile } from "@/api/user"
import { ProfileCard } from "@/components/profile"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
@@ -31,7 +31,8 @@ export default function ProfilePage() {
getProfile().then((profile) => {
setProfile(profile)
})
}).finally(() => {
})
.finally(() => {
window.history.replaceState({}, document.title, window.location.pathname)
})
}
@@ -116,12 +117,20 @@ export default function ProfilePage() {
</CardTitle>
</CardHeader>
<CardContent className="text-lg font-semibold">
{settingData?.config?.oauth2_providers?.map((provider) => <div>
{provider}: {profile.oauth2_bind?.[provider.toLowerCase()]} {profile.oauth2_bind?.[provider.toLowerCase()] ?
<Button size="sm" onClick={() => unbindO2(provider)}>Unbind</Button>
:
<Button size="sm" onClick={() => bindO2(provider)}>Bind</Button>}
</div>)}
{settingData?.config?.oauth2_providers?.map((provider) => (
<div>
{provider}: {profile.oauth2_bind?.[provider.toLowerCase()]}{" "}
{profile.oauth2_bind?.[provider.toLowerCase()] ? (
<Button size="sm" onClick={() => unbindO2(provider)}>
Unbind
</Button>
) : (
<Button size="sm" onClick={() => bindO2(provider)}>
Bind
</Button>
)}
</div>
))}
</CardContent>
</Card>
</div>

View File

@@ -67,21 +67,21 @@ export default function SettingsPage() {
resolver: zodResolver(settingFormSchema),
defaultValues: config
? {
...config,
language: config?.config?.language,
site_name: config.config?.site_name || "",
user_template:
config.config?.user_template ||
Object.keys(config.frontend_templates?.filter((t) => !t.is_admin) || {})[0] ||
"user-dist",
}
...config,
language: config?.config?.language,
site_name: config.config?.site_name || "",
user_template:
config.config?.user_template ||
Object.keys(config.frontend_templates?.filter((t) => !t.is_admin) || {})[0] ||
"user-dist",
}
: {
ip_change_notification_group_id: 0,
cover: 1,
site_name: "",
language: "",
user_template: "user-dist",
},
ip_change_notification_group_id: 0,
cover: 1,
site_name: "",
language: "",
user_template: "user-dist",
},
resetOptions: {
keepDefaultValues: false,
},
@@ -229,15 +229,15 @@ export default function SettingsPage() {
{!config?.frontend_templates?.find(
(t) => t.path === field.value,
)?.is_official && (
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-md p-2">
<div className="font-medium text-lg mb-1">
{t("CommunityThemeWarning")}
</div>
<div className="text-yellow-700 dark:text-yellow-200">
{t("CommunityThemeDescription")}
</div>
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-md p-2">
<div className="font-medium text-lg mb-1">
{t("CommunityThemeWarning")}
</div>
)}
<div className="text-yellow-700 dark:text-yellow-200">
{t("CommunityThemeDescription")}
</div>
</div>
)}
</FormItem>
)}
/>

View File

@@ -1,14 +1,14 @@
/* eslint-disable */
/* tslint:disable */
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
/* eslint-disable */
/* tslint:disable */
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
export interface GithubComNezhahqNezhaModelCommonResponseAny {
data?: any
error?: string