diff --git a/frontend/src/components/navigator.tsx b/frontend/src/components/navigator.tsx index de2682c..73abf0d 100644 --- a/frontend/src/components/navigator.tsx +++ b/frontend/src/components/navigator.tsx @@ -4,6 +4,7 @@ import {useNavigate, useOutlet} from "react-router"; import {useEffect, useState} from "react"; import {MdOutlinePerson, MdSearch, MdSettings} from "react-icons/md"; import { useTranslation } from "react-i18next"; +import UploadingSideBar from "./uploading_side_bar.tsx"; export default function Navigator() { const outlet = useOutlet() @@ -20,6 +21,7 @@ export default function Navigator() {
+ { app.isAdmin() && +
+ +
+

Cancel Task

+

Are you sure you want to cancel this task?

+
+
+ +
+ +
+
+
+ +} \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index c472407..1fef2ae 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -110,4 +110,18 @@ body { .animate-appearance-in { animation: appearance-in 250ms ease-out normal both; +} + +.move-up-animation { + animation: moveUpAndDown 2s infinite; + position: relative; +} + +@keyframes moveUpAndDown { + 0% { + top: 0; + } + 100% { + top: -100%; + } } \ No newline at end of file diff --git a/frontend/src/network/uploading.ts b/frontend/src/network/uploading.ts index 1eb5bad..1db264a 100644 --- a/frontend/src/network/uploading.ts +++ b/frontend/src/network/uploading.ts @@ -1,9 +1,191 @@ import {Response} from "./models.ts"; +import {network} from "./network.ts"; -class UploadingManager { - async addTask(file: File, resourceID: number, storageID: number, description: string): Promise> { - // TODO: implement this - throw new Error("Not implemented"); +enum UploadingStatus { + PENDING = "pending", + UPLOADING = "uploading", + DONE = "done", + ERROR = "error", +} + +class Listenable { + listeners: (() => void)[] = []; + + addListener(listener: () => void) { + this.listeners.push(listener); + } + + removeListener(listener: () => void) { + this.listeners = this.listeners.filter(l => l !== listener); + } + + notifyListeners() { + this.listeners.forEach(listener => listener()); + } +} + +export class UploadingTask extends Listenable { + id: number; + file: File; + blocks: boolean[]; + blockSize: number; + + status: UploadingStatus = UploadingStatus.PENDING; + errorMessage: string | null = null; + uploadingBlocks: number[] = []; + finishedBlocksCount: number = 0; + + onFinished: (() => void); + + get filename() { + return this.file.name; + } + + get progress() { + if (this.blocks.length === 0) { + return 0; + } + return this.finishedBlocksCount / this.blocks.length; + } + + constructor(id: number, file: File, blocksCount: number, blockSize: number, onFinished: () => void) { + super(); + this.id = id; + this.file = file; + this.blocks = new Array(blocksCount).fill(false); + this.blockSize = blockSize; + this.onFinished = onFinished; + } + + async upload(id: number) { + let index = 0; + while (index < this.blocks.length) { + if (this.blocks[index] || this.uploadingBlocks.includes(index)) { + index++; + continue; + } + if (this.status !== UploadingStatus.UPLOADING) { + return; + } + console.log(`${id}: uploading block ${index}`); + this.uploadingBlocks.push(index); + const start = index * this.blockSize; + const end = Math.min(start + this.blockSize, this.file.size); + const block = this.file.slice(start, end); + const data = await block.arrayBuffer(); + let retries = 3; + while (true) { + const res = await network.uploadFileBlock(this.id, index, data); + if (!res.success) { + retries--; + if (retries === 0) { + this.status = UploadingStatus.ERROR; + this.errorMessage = res.message; + this.notifyListeners(); + return; + } + } else { + break; + } + } + console.log(`${id}: uploaded block ${index}`); + this.blocks[index] = true; + this.finishedBlocksCount++; + this.uploadingBlocks = this.uploadingBlocks.filter(i => i !== index); + index++; + this.notifyListeners(); + } + } + + async start() { + this.status = UploadingStatus.UPLOADING; + this.notifyListeners(); + await Promise.all([ + this.upload(0), + this.upload(1), + this.upload(2), + this.upload(3), + ]) + if (this.status !== UploadingStatus.UPLOADING) { + return; + } + const res = await network.finishFileUpload(this.id); + if (res.success) { + this.status = UploadingStatus.DONE; + this.notifyListeners(); + this.onFinished(); + } else { + this.status = UploadingStatus.ERROR; + this.errorMessage = res.message; + this.notifyListeners(); + } + } + + cancel() { + this.status = UploadingStatus.ERROR; + this.errorMessage = "Cancelled"; + this.notifyListeners(); + network.cancelFileUpload(this.id); + } +} + +class UploadingManager extends Listenable { + tasks: UploadingTask[] = []; + + onTaskStatusChanged = () => { + if (this.tasks.length === 0) { + return; + } + if (this.tasks[0].status === UploadingStatus.PENDING) { + this.tasks[0].start(); + } else if (this.tasks[0].status === UploadingStatus.DONE) { + this.tasks[0].removeListener(this.onTaskStatusChanged); + this.tasks.shift(); + this.onTaskStatusChanged(); + } else if (this.tasks[0].status === UploadingStatus.ERROR && this.tasks[0].errorMessage === "Cancelled") { + this.tasks[0].removeListener(this.onTaskStatusChanged); + this.tasks.shift(); + this.onTaskStatusChanged(); + } + this.notifyListeners(); + } + + async addTask(file: File, resourceID: number, storageID: number, description: string, onFinished: () => void): Promise> { + const res = await network.initFileUpload( + file.name, + description, + file.size, + resourceID, + storageID + ) + if (!res.success) { + return { + success: false, + message: res.message, + }; + } + const task = new UploadingTask(res.data!.id, file, res.data!.blocksCount, res.data!.blockSize, onFinished); + task.addListener(this.onTaskStatusChanged); + this.tasks.push(task); + this.onTaskStatusChanged(); + return { + success: true, + message: "ok", + } + } + + getTasks() { + return this.tasks + } + + removeTask(task: UploadingTask) { + task.cancel(); + task.removeListener(this.onTaskStatusChanged); + this.tasks = this.tasks.filter(t => t !== task); + } + + hasTasks() { + return this.tasks.length > 0; } } diff --git a/frontend/src/pages/resource_details_page.tsx b/frontend/src/pages/resource_details_page.tsx index e2180d1..da26a48 100644 --- a/frontend/src/pages/resource_details_page.tsx +++ b/frontend/src/pages/resource_details_page.tsx @@ -22,6 +22,8 @@ export default function ResourcePage() { const [resource, setResource] = useState(null) + const [page, setPage] = useState(0) + const reload = useCallback(async () => { if (!isNaN(id)) { setResource(null) @@ -86,8 +88,10 @@ export default function ResourcePage() { }

-