diff --git a/src/cli/index.tsx b/src/cli/index.tsx index fe60568..a72db2a 100644 --- a/src/cli/index.tsx +++ b/src/cli/index.tsx @@ -6,6 +6,7 @@ import type { UploadOptions, StatusOptions } from "./types.ts"; import { render } from "ink"; import { App } from "../components/App.tsx"; import { StatusApp } from "../components/StatusApp.tsx"; +import { MediaDetector } from "../lib/processing/media-detector.ts"; export function setupCLI() { const program = new Command(); @@ -101,7 +102,30 @@ export function setupCLI() { process.exit(1); } - render(); + let confirmMediaType: "gif" | "video" | null = null; + if (options.image && !options.test) { + const filesToCheck = options.isBulk + ? (options.images ?? []) + : [options.image]; + + for (const file of filesToCheck) { + const info = await MediaDetector.detectFromFile(file).catch( + () => null, + ); + if (info?.type === "gif" || info?.type === "video") { + confirmMediaType = info.type; + break; + } + } + } + + render( + , + ); }); program diff --git a/src/components/App.tsx b/src/components/App.tsx index 625ca72..0e7e660 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,16 +1,45 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Box, Text, useApp } from "ink"; import Spinner from "ink-spinner"; -import { Header, UploadProgress, ConnectionStatus } from "./index.ts"; +import { + Header, + UploadProgress, + ConnectionStatus, + ConfirmAnimatedUpload, +} from "./index.ts"; import { useUpload } from "../hooks/useUpload.ts"; import type { UploadOptions } from "../cli/types.ts"; export interface AppProps { options: UploadOptions; verbose: boolean; + /** Set when the upload contains a GIF/video and requires user confirmation */ + confirmMediaType?: "gif" | "video" | null; } -export const App: React.FC = ({ options, verbose }) => { +export const App: React.FC = ({ + options, + verbose, + confirmMediaType, +}) => { + const [confirmed, setConfirmed] = useState(!confirmMediaType); + + if (!confirmed && confirmMediaType) { + return ( + setConfirmed(true)} + /> + ); + } + + return ; +}; + +const UploadFlow: React.FC<{ options: UploadOptions; verbose: boolean }> = ({ + options, + verbose, +}) => { const { exit } = useApp(); const { status, diff --git a/src/components/ConfirmAnimatedUpload.tsx b/src/components/ConfirmAnimatedUpload.tsx new file mode 100644 index 0000000..aa3b5a2 --- /dev/null +++ b/src/components/ConfirmAnimatedUpload.tsx @@ -0,0 +1,63 @@ +import React, { useState } from "react"; +import { Box, Text, useApp, useInput } from "ink"; +import { Header } from "./Header.tsx"; + +export interface ConfirmAnimatedUploadProps { + mediaType: "gif" | "video"; + onConfirm: () => void; +} + +const CONFIRM_PHRASE = "YES"; + +export const ConfirmAnimatedUpload: React.FC = ({ + mediaType, + onConfirm, +}) => { + const { exit } = useApp(); + const [input, setInput] = useState(""); + + useInput((char, key) => { + if (key.escape || (key.ctrl && char === "c")) { + exit(); + process.exit(1); + } + + if (key.return) { + if (input === CONFIRM_PHRASE) { + onConfirm(); + } + return; + } + + if (key.backspace || key.delete) { + setInput((prev) => prev.slice(0, -1)); + return; + } + + if (char) { + setInput((prev) => prev + char); + } + }); + + return ( + +
+ + + + WARNING: Uploading {mediaType} to your BeamBox device is EXPERIMENTAL. + + + There are known cases where this can permanently brick your device. + + Proceed only if you understand and accept this risk. + + + + + Type "{CONFIRM_PHRASE}" to continue (Esc to cancel): {input} + + + + ); +}; diff --git a/src/components/index.ts b/src/components/index.ts index ed1d89e..f15c4e2 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,4 +1,5 @@ export { Header } from './Header.tsx'; +export { ConfirmAnimatedUpload } from './ConfirmAnimatedUpload.tsx'; export { UploadProgress } from './UploadProgress.tsx'; export { Status } from './Status.tsx'; export { ConnectionStatus, type ConnectionStep } from './ConnectionStatus.tsx';