1- import { useState , useEffect , useCallback } from 'react' ;
2- import { Trash2 , Plus , RefreshCw , Terminal , CheckCircle2 , Copy , Circle , X , AlertTriangle } from 'lucide-react' ;
1+ import { useState , useEffect , useCallback , useRef } from 'react' ;
2+ import { Trash2 , Plus , RefreshCw , Terminal , CheckCircle2 , Copy , Circle , X , AlertTriangle , Download , Upload } from 'lucide-react' ;
33import { toast } from 'sonner' ;
44import { sounds } from '@/lib/sound' ;
55import { decryptKeys , maskKey } from '@/utils/crypto' ;
@@ -11,7 +11,7 @@ import { Badge } from '@/components/ui/badge';
1111import { Progress } from '@/components/ui/progress' ;
1212import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from '@/components/ui/table' ;
1313import { Dialog , DialogContent , DialogDescription , DialogFooter , DialogHeader , DialogTitle , DialogTrigger } from '@/components/ui/dialog' ;
14- import { Input } from '@/components/ui/input ' ;
14+ import { Textarea } from '@/components/ui/textarea ' ;
1515import {
1616 AlertDialog ,
1717 AlertDialogAction ,
@@ -164,6 +164,7 @@ export default function KeyList() {
164164 const [ deleteIndex , setDeleteIndex ] = useState < number | null > ( null ) ;
165165 const [ newKey , setNewKey ] = useState ( '' ) ;
166166 const [ adding , setAdding ] = useState ( false ) ;
167+ const fileInputRef = useRef < HTMLInputElement > ( null ) ;
167168
168169 const loadData = useCallback ( async ( showRefreshing = false , autoRefresh = true , silent = false ) => {
169170 try {
@@ -233,17 +234,52 @@ export default function KeyList() {
233234 const handleAddKey = async ( ) => {
234235 if ( ! newKey . trim ( ) ) return ;
235236 setAdding ( true ) ;
236- const result = await addKey ( newKey . trim ( ) ) ;
237- if ( result . success ) {
237+ const lines = newKey . split ( '\n' ) . map ( l => l . trim ( ) ) . filter ( Boolean ) ;
238+ let successCount = 0 ;
239+ let lastError = '' ;
240+ for ( const key of lines ) {
241+ const result = await addKey ( key ) ;
242+ if ( result . success ) {
243+ successCount ++ ;
244+ } else {
245+ lastError = result . error || 'Failed to add key' ;
246+ }
247+ }
248+ if ( successCount > 0 ) {
238249 setNewKey ( '' ) ;
239250 setAddDialogOpen ( false ) ;
240251 await loadData ( true ) ;
241- } else {
242- alert ( result . error || 'Failed to add key' ) ;
252+ }
253+ if ( lastError && successCount < lines . length ) {
254+ alert ( `Added ${ successCount } /${ lines . length } keys. Error: ${ lastError } ` ) ;
243255 }
244256 setAdding ( false ) ;
245257 } ;
246258
259+ const handleFileSelect = ( e : React . ChangeEvent < HTMLInputElement > ) => {
260+ const file = e . target . files ?. [ 0 ] ;
261+ if ( ! file ) return ;
262+ const reader = new FileReader ( ) ;
263+ reader . onload = ( event ) => {
264+ const text = event . target ?. result as string ;
265+ setNewKey ( text ) ;
266+ } ;
267+ reader . readAsText ( file ) ;
268+ e . target . value = '' ;
269+ } ;
270+
271+ const handleExport = ( ) => {
272+ if ( keys . length === 0 ) return ;
273+ const text = keys . map ( k => k . key ) . join ( '\n' ) ;
274+ const blob = new Blob ( [ text ] , { type : 'text/plain' } ) ;
275+ const url = URL . createObjectURL ( blob ) ;
276+ const a = document . createElement ( 'a' ) ;
277+ a . href = url ;
278+ a . download = 'factory-keys.txt' ;
279+ a . click ( ) ;
280+ URL . revokeObjectURL ( url ) ;
281+ } ;
282+
247283 const handleRemoveKey = ( index : number ) => {
248284 setDeleteIndex ( index ) ;
249285 } ;
@@ -319,6 +355,9 @@ export default function KeyList() {
319355 < Button variant = "outline" size = "icon" onClick = { handleRefresh } disabled = { refreshing } className = "h-8 w-8" title = "Refresh" >
320356 < RefreshCw className = { `h-3.5 w-3.5 ${ refreshing ? 'animate-spin' : '' } ` } />
321357 </ Button >
358+ < Button variant = "outline" size = "icon" onClick = { handleExport } disabled = { keys . length === 0 } className = "h-8 w-8" title = "Export All Keys" >
359+ < Download className = "h-3.5 w-3.5" />
360+ </ Button >
322361 < Dialog open = { addDialogOpen } onOpenChange = { setAddDialogOpen } >
323362 < DialogTrigger asChild >
324363 < Button size = "sm" className = "h-8 text-xs px-3" >
@@ -328,19 +367,31 @@ export default function KeyList() {
328367 </ DialogTrigger >
329368 < DialogContent >
330369 < DialogHeader >
331- < DialogTitle > Inject New Key </ DialogTitle >
332- < DialogDescription > Enter your Factory API key below.</ DialogDescription >
370+ < DialogTitle > Inject Keys </ DialogTitle >
371+ < DialogDescription > Enter your Factory API keys below (one per line) .</ DialogDescription >
333372 </ DialogHeader >
334- < Input
335- placeholder = "fk-..."
373+ < Textarea
374+ placeholder = { "fk-xxx\nfk-yyy\nfk-zzz" }
336375 value = { newKey }
337376 onChange = { ( e ) => setNewKey ( e . target . value ) }
338- onKeyDown = { ( e ) => e . key === 'Enter' && handleAddKey ( ) }
377+ rows = { 5 }
378+ className = "font-mono text-sm"
379+ />
380+ < input
381+ type = "file"
382+ accept = ".txt"
383+ ref = { fileInputRef }
384+ onChange = { handleFileSelect }
385+ className = "hidden"
339386 />
340- < DialogFooter >
387+ < DialogFooter className = "flex-col sm:flex-row gap-2" >
388+ < Button variant = "outline" onClick = { ( ) => fileInputRef . current ?. click ( ) } className = "sm:mr-auto" >
389+ < Upload className = "h-3.5 w-3.5 mr-1.5" />
390+ IMPORT FROM FILE
391+ </ Button >
341392 < Button variant = "outline" onClick = { ( ) => setAddDialogOpen ( false ) } > CANCEL</ Button >
342393 < Button onClick = { handleAddKey } disabled = { adding || ! newKey . trim ( ) } >
343- { adding ? 'INJECTING...' : 'INJECT KEY ' }
394+ { adding ? 'INJECTING...' : 'INJECT KEYS ' }
344395 </ Button >
345396 </ DialogFooter >
346397 </ DialogContent >
0 commit comments