diff --git a/bun.lockb b/bun.lockb index 9e6de21..2b906a4 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index b5558b7..a4edce7 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@mui/icons-material": "^6.3.1", "@mui/material": "^6.3.1", "@mui/material-nextjs": "^6.3.1", - "@mui/x-charts": "^7.23.2", + "@mui/x-charts": "^7.23.6", "@tailwindcss/typography": "^0.5.16", "@toolpad/core": "^0.11.0", "@vercel/speed-insights": "^1.1.0", @@ -26,7 +26,7 @@ "axios-case-converter": "^1.1.1", "cookies-next": "^5.0.2", "feed": "^4.2.2", - "next": "15.1.3", + "next": "^15.1.4", "next-nprogress-bar": "^2.4.3", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -38,7 +38,7 @@ "remark-parse": "^11.0.0", "remark-rehype": "^11.1.1", "sitemap": "^8.0.0", - "solar-js-sdk": "0.0.8", + "solar-js-sdk": "^0.1.1", "unified": "^11.0.5", "zustand": "^5.0.3" }, @@ -49,7 +49,7 @@ "@types/react-dom": "^19.0.2", "postcss": "^8.4.49", "tailwindcss": "^3.4.17", - "eslint": "^9.17.0", + "eslint": "^9.18.0", "eslint-config-next": "15.1.3", "@eslint/eslintrc": "^3.2.0" }, diff --git a/packages/sn/package.json b/packages/sn/package.json index fd9585d..d845954 100644 --- a/packages/sn/package.json +++ b/packages/sn/package.json @@ -8,7 +8,7 @@ "name": "LittleSheep", "email": "littlesheep.code@hotmail.com" }, - "version": "0.0.8", + "version": "0.1.1", "tsup": { "entry": [ "src/index.ts" diff --git a/packages/sn/src/attachment.ts b/packages/sn/src/attachment.ts index 7c02b75..acad814 100644 --- a/packages/sn/src/attachment.ts +++ b/packages/sn/src/attachment.ts @@ -56,7 +56,6 @@ export type MultipartProgress = { export type MultipartInfo = { rid: string fileChunks: Record - isUploaded: boolean } export class UploadAttachmentTask { @@ -66,9 +65,9 @@ export class UploadAttachmentTask { private multipartInfo: MultipartInfo | null = null private multipartProgress: MultipartProgress = { value: null, current: 0, total: 0 } - loading: boolean = false - success: boolean = false - error: string | null = null + onProgress?: (progress: MultipartProgress) => void + onSuccess?: (success: boolean) => void + onError?: (error: string) => void constructor(content: File, pool: string) { if (!content || !pool) { @@ -78,8 +77,7 @@ export class UploadAttachmentTask { this.pool = pool } - public async submit(): Promise { - this.loading = true + public async submit(): Promise { const limit = 3 try { @@ -92,14 +90,20 @@ export class UploadAttachmentTask { const chunks = Object.keys(this.multipartInfo?.fileChunks || {}) this.multipartProgress.total = chunks.length + let result: SnAttachment | null = null + const uploadChunks = async (chunk: string): Promise => { try { - await this.uploadSingleMultipart(chunk) + const resp = await this.uploadOneChunk(chunk) this.multipartProgress.current++ console.log( `[Paperclip] Uploaded multipart ${this.multipartProgress.current}/${this.multipartProgress.total}`, ) this.multipartProgress.value = this.multipartProgress.current / this.multipartProgress.total + + if (this.onProgress) this.onProgress(this.multipartProgress) + + result = resp } catch (err) { console.log(`[Paperclip] Upload multipart ${chunk} failed, retrying in 3 seconds...`) await this.delay(3000) @@ -112,15 +116,13 @@ export class UploadAttachmentTask { await Promise.all(chunkSlice.map(uploadChunks)) } - if (this.multipartInfo?.isUploaded) { - console.log(`[Paperclip] Entire file has been uploaded in ${this.multipartProgress.total} chunk(s)`) - this.success = true - } - } catch (e) { - console.error(e) - this.error = e instanceof Error ? e.message : String(e) - } finally { - this.loading = false + console.log(`[Paperclip] Entire file has been uploaded in ${this.multipartProgress.total} chunk(s)`) + if (this.onSuccess) this.onSuccess(true) + + return result! + } catch (err: any) { + if (this.onError) this.onError(err.toString()) + throw err } } @@ -139,7 +141,7 @@ export class UploadAttachmentTask { const nameArray = this.content.name.split('.') nameArray.pop() - const resp = await sni.post('/cgi/uc/attachments/fragments', { + const resp = await sni.post('/cgi/uc/fragments', { pool: this.pool, size: this.content.size, name: this.content.name, @@ -153,20 +155,22 @@ export class UploadAttachmentTask { this.multipartInfo = data.meta } - private async uploadSingleMultipart(chunkId: string): Promise { - if (!this.multipartInfo) return + private async uploadOneChunk(chunkId: string): Promise { + if (!this.multipartInfo) return null const chunkIdx = this.multipartInfo.fileChunks[chunkId] const chunk = this.content.slice(chunkIdx * this.multipartSize, (chunkIdx + 1) * this.multipartSize) - const data = new FormData() - data.set('file', chunk) - - const resp = await sni.post(`/cgi/uc/attachments/fragments/${this.multipartInfo.rid}/${chunkId}`, data, { + const resp = await sni.post(`/cgi/uc/fragments/${this.multipartInfo.rid}/${chunkId}`, chunk, { + headers: { 'Content-Type': 'application/octet-stream' }, timeout: 3 * 60 * 1000, }) - this.multipartInfo = resp.data + if (resp.data['attachment']) { + return resp.data['attachment'] as SnAttachment + } + this.multipartInfo = resp.data['fragment'] + return null } private delay(ms: number): Promise { diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 5cb3168..4f71f28 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -38,7 +38,11 @@ export default function App({ Component, pageProps }: AppProps) { userStore.fetchUser() }, []) - const title = pageProps.title ? `${pageProps.title} | Solsynth LLC` : 'Solsynth LLC' + const title = pageProps.title + ? pageProps.startsWith('Solar Network Console') + ? pageProps.title + : `${pageProps.title} | Solsynth LLC` + : 'Solsynth LLC' return ( <> diff --git a/src/pages/attachments/new.tsx b/src/pages/attachments/new.tsx new file mode 100644 index 0000000..ce8410a --- /dev/null +++ b/src/pages/attachments/new.tsx @@ -0,0 +1,190 @@ +import { + styled, + Button, + Container, + Card, + Box, + List, + ListItem, + ListItemText, + IconButton, + FormControl, + InputLabel, + MenuItem, + Select, + LinearProgress, + Typography, + Alert, + Collapse, +} from '@mui/material' +import { MultipartProgress, SnAttachment, UploadAttachmentTask } from 'solar-js-sdk' +import { useState } from 'react' + +import ErrorIcon from '@mui/icons-material/Error' +import CloseIcon from '@mui/icons-material/Close' +import CloudUploadIcon from '@mui/icons-material/CloudUpload' +import PlayIcon from '@mui/icons-material/PlayArrow' + +const VisuallyHiddenInput = styled('input')({ + clip: 'rect(0 0 0 0)', + clipPath: 'inset(50%)', + height: 1, + overflow: 'hidden', + position: 'absolute', + bottom: 0, + left: 0, + whiteSpace: 'nowrap', + width: 1, +}) + +interface FileUploadTask { + file: File + attachment?: SnAttachment +} + +export default function AttachmentNew() { + const [files, setFiles] = useState([]) + + const [busy, setBusy] = useState(false) + const [error, setError] = useState(null) + + const [task, setTask] = useState() + const [taskProgress, setTaskProgress] = useState() + + const [pool, setPool] = useState('interactive') + + async function upload() { + if (files.length == 0) return + + setBusy(true) + + for (let idx = 0; idx < files.length; idx++) { + if (files[idx].attachment) continue + + try { + const task = new UploadAttachmentTask(files[idx].file, pool) + setTask(files[idx]) + task.onProgress = (progress) => setTaskProgress(progress) + task.onError = (err) => setError(err) + const attachment = await task.submit() + setFiles((files) => files.map((f, i) => (i == idx ? { ...f, attachment } : f))) + } catch (err: any) { + setError(err.toString()) + setBusy(false) + return + } + } + + setBusy(false) + } + + return ( + + + + + {files.length > 0 && ( + + )} + + + + } severity="error"> + {error} + + + + {taskProgress && ( + + + {task?.file.name ?? 'Waiting...'} + + + + )} + + {files.length > 0 && ( + + + + {files?.map((f, idx) => ( + setFiles((files) => files.filter((_, i) => i != idx))} + > + + + } + > + + + ))} + + + + )} + + + Attachment Pool + + + + + ) +}