diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ced1dd5..c5a9dd3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,15 +5,48 @@ on: - "main" jobs: + build-browser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 8 + run_install: false + + - name: NodeJS + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: ./browser/pnpm-lock.yaml + + - name: Install dependencies + working-directory: ./browser + run: pnpm i + + - name: Check TypeScript + working-directory: ./browser + run: pnpm ts-check + + - name: Build + working-directory: ./browser + run: pnpm build + test: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 + - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.12.7" + cache: "pip" - name: Install dependencies run: | diff --git a/browser/src/store/scanning_store.ts b/browser/src/store/scanning_store.ts index 7bc92f5..89c6db0 100644 --- a/browser/src/store/scanning_store.ts +++ b/browser/src/store/scanning_store.ts @@ -2,7 +2,13 @@ import { val, derive, combine, Val, ReadonlyVal } from "value-enhancer"; import { fetchJson, fetchJsonEvents, EventFetcher } from "../utils"; type Event = { - readonly kind: "scanning" | "completed" | "interrupted" | "heartbeat"; + readonly kind: ( + "scanning" | + "completed" | + "interrupting" | + "interrupted" | + "heartbeat" + ); } | { readonly kind: "scanCompleted"; readonly count: number; @@ -31,6 +37,8 @@ export type ScanningStore$ = { readonly handlingFile: ReadonlyVal; readonly completedFiles: ReadonlyVal; readonly error: ReadonlyVal; + readonly isInterrupting: ReadonlyVal; + readonly isInterrupted: ReadonlyVal; readonly isScanning: ReadonlyVal; }; @@ -39,7 +47,6 @@ export enum ScanningPhase { Scanning, HandingFiles, Completed, - Error, } export type HandingFile = { @@ -63,6 +70,8 @@ export class ScanningStore { readonly #handlingFile$: Val = val(null); readonly #completedFiles$: Val = val([]); readonly #error$: Val = val(null); + readonly #isInterrupting$: Val = val(false); + readonly #isInterrupted$: Val = val(false); public constructor() { this.#fetcher = fetchJsonEvents("/api/scanning"); @@ -72,18 +81,26 @@ export class ScanningStore { handlingFile: derive(this.#handlingFile$), completedFiles: derive(this.#completedFiles$), error: derive(this.#error$), - isScanning: derive(this.#phase$, phase => { - switch (phase) { - case ScanningPhase.Ready: - case ScanningPhase.Completed: - case ScanningPhase.Error: { + isInterrupting: derive(this.#isInterrupting$), + isInterrupted: derive(this.#isInterrupted$), + isScanning: combine( + [this.#phase$, this.#error$, this.#isInterrupted$], + ([phase, error, isInterrupted]) => { + switch (phase) { + case ScanningPhase.Ready: + case ScanningPhase.Completed: { + return false; + } + } + if (error) { return false; } - default: { - return true; + if (isInterrupted) { + return false; } - } - }), + return true; + }, + ), }; } @@ -91,6 +108,10 @@ export class ScanningStore { await fetchJson("/api/scanning", { method: "POST" }); } + public async interrupt(): Promise { + await fetchJson("/api/scanning", { method: "DELETE" }); + } + public close(): void { this.#fetcher.close(); } @@ -108,6 +129,7 @@ export class ScanningStore { this.#handlingFile$.set(null); this.#completedFiles$.set([]); this.#error$.set(null); + this.#isInterrupted$.set(false); break; } case "scanCompleted": { @@ -152,10 +174,18 @@ export class ScanningStore { break; } case "failure": { - this.#phase$.set(ScanningPhase.Error); this.#error$.set(event.error); break; } + case "interrupting": { + this.#isInterrupting$.set(true); + break; + } + case "interrupted": { + this.#isInterrupting$.set(false); + this.#isInterrupted$.set(true); + break; + } } } } diff --git a/browser/src/views/ScannerPage.module.less b/browser/src/views/ScannerPage.module.less index 840c34b..da69595 100644 --- a/browser/src/views/ScannerPage.module.less +++ b/browser/src/views/ScannerPage.module.less @@ -5,6 +5,7 @@ .scan-button { width: 128px; + margin-right: 24px; } .steps-bar { diff --git a/browser/src/views/ScannerPage.tsx b/browser/src/views/ScannerPage.tsx index 76c7752..f0733dc 100644 --- a/browser/src/views/ScannerPage.tsx +++ b/browser/src/views/ScannerPage.tsx @@ -2,7 +2,7 @@ import React from "react"; import styles from "./ScannerPage.module.less"; import { Skeleton, Result, Steps, List, Button, Divider, Progress, Typography } from "antd"; -import { ScanOutlined, ProfileTwoTone, SyncOutlined, FilePdfTwoTone } from "@ant-design/icons"; +import { ScanOutlined, ProfileTwoTone, SyncOutlined, FilePdfTwoTone, PauseOutlined } from "@ant-design/icons"; import { val } from "value-enhancer"; import { useVal } from "use-value-enhancer"; import { ScannerStore, ScanningStore, ScanningPhase } from "../store"; @@ -49,10 +49,15 @@ type ScannerProps = { const Scanner: React.FC = ({ store }) => { const scanningStore = store.scanningStore; const isScanning = useVal(scanningStore.$.isScanning); + const isInterrupting = useVal(scanningStore.$.isInterrupting); const onClickScan = React.useCallback( () => scanningStore.scan(), [scanningStore], ); + const onClickInterrupt = React.useCallback( + () => scanningStore.interrupt(), + [scanningStore], + ); return <> 扫描 @@ -71,6 +76,18 @@ const Scanner: React.FC = ({ store }) => { onClick={onClickScan} > 扫 描 + {isScanning && ( + + )} ; } @@ -91,6 +108,7 @@ const ScanningPanel: React.FC = ({ store }) => { const completedFiles = useVal(store.$.completedFiles); const handlingFile = useVal(store.$.handlingFile); const error = useVal(store.$.error); + const isInterrupted = useVal(store.$.isInterrupted); let currentIndex: number; let status: "wait" | "process" | "finish" | "error" = "process"; @@ -112,11 +130,6 @@ const ScanningPanel: React.FC = ({ store }) => { status = "finish"; break; } - case ScanningPhase.Error: { - currentIndex = 2; - status = "error"; - break; - } } if (phase === ScanningPhase.Scanning) { records.push({ @@ -157,7 +170,7 @@ const ScanningPanel: React.FC = ({ store }) => { items={[ { title: "扫描" }, { title: "处理文件" }, - { title: phase === ScanningPhase.Error ? "错误" : "完成" }, + { title: "完成" }, ]} /> = ({ store }) => { /> {error && ( = ({ store }) => { subTitle={error} /> )} + {isInterrupted && ( + + )} ; }; type ProgressBarProps = { readonly name: string; + readonly error: boolean; readonly pdfPage?: { readonly index: number; readonly total: number; }; }; -const ProgressBar: React.FC = ({ name, pdfPage }) => { +const ProgressBar: React.FC = ({ name, error, pdfPage }) => { if (!pdfPage) { return null; } const { index, total } = pdfPage; const percent = Math.floor(Math.min(index / total, 1.0) * 100); - const status = percent === 100 ? "success" : "active"; + const status = error ? "exception" : (percent === 100 ? "success" : "active"); return (