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

View File

@@ -7,27 +7,45 @@ export enum Oauth2RequestType {
BIND = 2, 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}`, { return fetcher<ModelOauth2LoginResponse>(FetcherMethod.GET, `/api/v1/oauth2/${provider}`, {
"type": rType, sType: rType,
}) })
} }
export const bindOauth2 = async (
export const bindOauth2 = async (provider: string, state: string, code: string): Promise<ModelOauth2LoginResponse> => { provider: string,
return fetcher<ModelOauth2LoginResponse>(FetcherMethod.POST, `/api/v1/oauth2/${provider}/bind`, { state: string,
"state": state, code: string,
"code": code, ): Promise<ModelOauth2LoginResponse> => {
}) return fetcher<ModelOauth2LoginResponse>(
FetcherMethod.POST,
`/api/v1/oauth2/${provider}/bind`,
{
state: state,
code: code,
},
)
} }
export const unbindOauth2 = async (provider: string): Promise<ModelOauth2LoginResponse> => { 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 (
export const oauth2callback = async (provider: string, state: string, code: string): Promise<void> => { provider: string,
state: string,
code: string,
): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, `/api/v1/oauth2/${provider}/callback`, { 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 { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
import { Button } from "./ui/button"
import { TableCell, TableRow } from "./ui/table" import { TableCell, TableRow } from "./ui/table"
import { Filepath } from "./xui/filepath" import { Filepath } from "./xui/filepath"
import { IconButton } from "./xui/icon-button" import { IconButton } from "./xui/icon-button"
@@ -102,7 +103,7 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
header: () => <span>{t("Actions")}</span>, header: () => <span>{t("Actions")}</span>,
id: "download", id: "download",
cell: ({ row }) => { cell: ({ row }) => {
return ( return row.original.type == 0 ? (
<IconButton <IconButton
variant="ghost" variant="ghost"
icon="download" icon="download"
@@ -111,6 +112,8 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
downloadFile(row.original.name) 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(() => { useEffect(() => {
const ws = new WebSocket(wsUrl) const ws = new WebSocket(wsUrl)
wsRef.current = ws wsRef.current = ws
@@ -209,27 +176,27 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
description: t("Results.UnExpectedError"), description: t("Results.UnExpectedError"),
}) })
} }
ws.onmessage = async (e) => { ws.onmessage = async (e: MessageEvent<ArrayBufferLike>) => {
try { 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) { if (firstChunk.current) {
const identifier = new Uint8Array(buf, 0, 4)
if (arraysEqual(identifier, FMIdentifier.file)) { if (arraysEqual(identifier, FMIdentifier.file)) {
worker.postMessage({ worker.postMessage({
operation: 1, operation: 1,
arrayBuffer: buf, arrayBuffer: e.data,
fileName: currentBasename.current, fileName: currentBasename.current,
}) })
firstChunk.current = false firstChunk.current = false
} else if (arraysEqual(identifier, FMIdentifier.fileName)) { } else if (arraysEqual(identifier, FMIdentifier.fileName)) {
const { path, fmList } = await fm.parseFMList(buf) const { path, fmList } = await fm.parseFMList(e.data)
setPath(path) setPath(path)
setFMEntries(fmList) 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)) { } else if (arraysEqual(identifier, FMIdentifier.complete)) {
// Upload completed // Upload completed
if (uOpen) setuOpen(false) if (uOpen) setuOpen(false)
@@ -241,7 +208,7 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
await waitForHandleReady() await waitForHandleReady()
worker.postMessage({ worker.postMessage({
operation: 2, operation: 2,
arrayBuffer: buf, arrayBuffer: e.data,
fileName: currentBasename.current, fileName: currentBasename.current,
}) })
} }
@@ -250,12 +217,61 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
toast("FM" + " " + t("Error"), { toast("FM" + " " + t("Error"), {
description: t("Results.UnExpectedError"), description: t("Results.UnExpectedError"),
}) })
if (dOpen) setdOpen(false) setdOpen(false)
if (uOpen) setuOpen(false) setuOpen(false)
} }
} }
}, [wsUrl]) }, [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 listFile = () => {
const prefix = new Int8Array([FMOpcode.List]) const prefix = new Int8Array([FMOpcode.List])
const pathMsg = new TextEncoder().encode(currentPath) const pathMsg = new TextEncoder().encode(currentPath)
@@ -317,7 +333,7 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
toast("FM" + " " + t("Error"), { toast("FM" + " " + t("Error"), {
description: error.message, 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 { useAuth } from "@/hooks/useAuth"
import useSettings from "@/hooks/useSetting" import useSettings from "@/hooks/useSetting"
import { copyToClipboard } from "@/lib/utils" import { copyToClipboard } from "@/lib/utils"
import { ModelProfile, ModelConfig } from "@/types" import { ModelConfig, ModelProfile } from "@/types"
import i18next from "i18next" import i18next from "i18next"
import { Check, Clipboard } from "lucide-react" import { Check, Clipboard } from "lucide-react"
import { forwardRef, useState } from "react" import { forwardRef, useState } from "react"

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ import { GithubComNezhahqNezhaModelSettingResponseModelConfig } from "@/types"
import useSWR from "swr" import useSWR from "swr"
export default function useSetting() { 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") throw new Error("accessHandle is undefined")
} }
const dataChunk = arrayBuffer accessHandle.write(arrayBuffer, { at: receivedLength })
accessHandle.write(dataChunk, { at: receivedLength }) receivedLength += arrayBuffer.byteLength
receivedLength += dataChunk.byteLength
if (receivedLength === expectedLength) { if (receivedLength === expectedLength) {
accessHandle.flush() 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 { Button } from "@/components/ui/button"
import { import {
Form, Form,
@@ -59,7 +59,7 @@ function Login() {
window.location.href = redirectUrl.redirect! window.location.href = redirectUrl.redirect!
} catch (error: any) { } catch (error: any) {
toast.error(error.message) toast.error(error.message)
} }
} }
const { t } = useTranslation() const { t } = useTranslation()
@@ -103,9 +103,9 @@ function Login() {
</form> </form>
</Form> </Form>
<div className="mt-4"> <div className="mt-4">
{settingData?.config?.oauth2_providers?.map((p: string) => {settingData?.config?.oauth2_providers?.map((p: string) => (
<Button onClick={() => loginWith(p)}>{p}</Button> <Button onClick={() => loginWith(p)}>{p}</Button>
)} ))}
</div> </div>
</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 { getProfile } from "@/api/user"
import { ProfileCard } from "@/components/profile" import { ProfileCard } from "@/components/profile"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
@@ -31,7 +31,8 @@ export default function ProfilePage() {
getProfile().then((profile) => { getProfile().then((profile) => {
setProfile(profile) setProfile(profile)
}) })
}).finally(() => { })
.finally(() => {
window.history.replaceState({}, document.title, window.location.pathname) window.history.replaceState({}, document.title, window.location.pathname)
}) })
} }
@@ -116,12 +117,20 @@ export default function ProfilePage() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="text-lg font-semibold"> <CardContent className="text-lg font-semibold">
{settingData?.config?.oauth2_providers?.map((provider) => <div> {settingData?.config?.oauth2_providers?.map((provider) => (
{provider}: {profile.oauth2_bind?.[provider.toLowerCase()]} {profile.oauth2_bind?.[provider.toLowerCase()] ? <div>
<Button size="sm" onClick={() => unbindO2(provider)}>Unbind</Button> {provider}: {profile.oauth2_bind?.[provider.toLowerCase()]}{" "}
: {profile.oauth2_bind?.[provider.toLowerCase()] ? (
<Button size="sm" onClick={() => bindO2(provider)}>Bind</Button>} <Button size="sm" onClick={() => unbindO2(provider)}>
</div>)} Unbind
</Button>
) : (
<Button size="sm" onClick={() => bindO2(provider)}>
Bind
</Button>
)}
</div>
))}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

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

View File

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