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';