@@ -1427,6 +1427,10 @@ const MobileApp = () => {
14271427 const [ currentStep , setCurrentStep ] = React . useState ( 'upload' ) ; // upload, review, preview
14281428 const [ detectionFilter , setDetectionFilter ] = React . useState ( { } ) ;
14291429
1430+ // Multi-file state (parity with web)
1431+ const [ fileStates , setFileStates ] = React . useState ( [ ] ) ;
1432+ const [ currentFileIndex , setCurrentFileIndex ] = React . useState ( 0 ) ;
1433+
14301434 // Phase 2.4 modal states
14311435 const [ showPresets , setShowPresets ] = React . useState ( false ) ;
14321436 const [ showHistory , setShowHistory ] = React . useState ( false ) ;
@@ -1477,6 +1481,110 @@ const MobileApp = () => {
14771481 }
14781482 } , [ ] ) ;
14791483
1484+ // Multi-file: add files
1485+ const handleAddFiles = React . useCallback ( async ( event ) => {
1486+ const list = Array . from ( event . target . files || [ ] ) ;
1487+ if ( ! list . length ) return ;
1488+ setFileStates ( prev => {
1489+ const appended = list . map ( ( f ) => ( {
1490+ file : f ,
1491+ detections : [ ] ,
1492+ pages : 1 ,
1493+ selected : { } ,
1494+ actions : { } ,
1495+ analyzing : false ,
1496+ sanitizing : false ,
1497+ outputUri : null ,
1498+ previewUri : null ,
1499+ error : null ,
1500+ analyzed : false ,
1501+ sanitized : false
1502+ } ) ) ;
1503+ const next = [ ...prev , ...appended ] ;
1504+ if ( prev . length === 0 ) setCurrentFileIndex ( 0 ) ;
1505+ return next ;
1506+ } ) ;
1507+ // Reset input to allow re-selecting the same files later
1508+ event . target . value = '' ;
1509+ } , [ ] ) ;
1510+
1511+ // Analyze one file by index
1512+ const analyzeOne = React . useCallback ( async ( index ) => {
1513+ setFileStates ( prev => prev . map ( ( s , i ) => i === index ? { ...s , analyzing : true , error : null } : s ) ) ;
1514+ try {
1515+ if ( ! window . CleanSharePro || typeof window . CleanSharePro . processFile !== 'function' ) {
1516+ throw new Error ( 'Processing engine not ready' ) ;
1517+ }
1518+ const s = fileStates [ index ] ;
1519+ const res = await window . CleanSharePro . processFile ( s . file ) ;
1520+ if ( ! res || ! res . success ) throw new Error ( res ?. error || 'Analysis failed' ) ;
1521+ const detections = res . detections || [ ] ;
1522+ const selected = { } ; const actions = { } ;
1523+ detections . forEach ( d => { selected [ d . id ] = true ; actions [ d . id ] = { style : 'BOX' } ; } ) ;
1524+ setFileStates ( prev => prev . map ( ( fs , i ) => i === index ? { ...fs , detections, pages : res . pages || 1 , selected, actions, analyzed : true , analyzing : false } : fs ) ) ;
1525+ } catch ( err ) {
1526+ setFileStates ( prev => prev . map ( ( s , i ) => i === index ? { ...s , analyzing : false , error : err . message } : s ) ) ;
1527+ }
1528+ } , [ fileStates ] ) ;
1529+
1530+ // Sanitize one file by index
1531+ const sanitizeOne = React . useCallback ( async ( index ) => {
1532+ setFileStates ( prev => prev . map ( ( s , i ) => i === index ? { ...s , sanitizing : true , error : null } : s ) ) ;
1533+ try {
1534+ if ( ! window . CleanSharePro || typeof window . CleanSharePro . applyRedactions !== 'function' ) {
1535+ throw new Error ( 'Processing engine not ready' ) ;
1536+ }
1537+ const s = fileStates [ index ] ;
1538+ const actions = ( s . detections || [ ] ) . filter ( d => s . selected [ d . id ] ) . map ( d => ( { detectionId : d . id , style : ( s . actions [ d . id ] ?. style ) || 'BOX' } ) ) ;
1539+ const res = await window . CleanSharePro . applyRedactions ( s . file , actions , { detections : s . detections } ) ;
1540+ if ( ! res || ! res . success ) throw new Error ( res ?. error || 'Sanitization failed' ) ;
1541+ const mime = s . file . type === 'application/pdf' ? 'application/pdf' : ( s . file . type && s . file . type . startsWith ( 'image/' ) ? s . file . type : 'application/octet-stream' ) ;
1542+ const blob = new Blob ( [ res . data ] , { type : mime } ) ;
1543+ const url = URL . createObjectURL ( blob ) ;
1544+ setFileStates ( prev => prev . map ( ( fs , i ) => i === index ? { ...fs , outputUri : url , previewUri : url , sanitizing : false , sanitized : true } : fs ) ) ;
1545+ } catch ( err ) {
1546+ setFileStates ( prev => prev . map ( ( s , i ) => i === index ? { ...s , sanitizing : false , error : err . message } : s ) ) ;
1547+ }
1548+ } , [ fileStates ] ) ;
1549+
1550+ // Concurrency helper
1551+ const runWithConcurrency = React . useCallback ( async ( indexes , worker , limit = 3 ) => {
1552+ const queue = [ ...indexes ] ;
1553+ const runners = new Array ( Math . min ( limit , queue . length ) ) . fill ( 0 ) . map ( async ( ) => {
1554+ while ( queue . length ) {
1555+ const idx = queue . shift ( ) ;
1556+ try { await worker ( idx ) ; } catch { }
1557+ }
1558+ } ) ;
1559+ await Promise . all ( runners ) ;
1560+ } , [ ] ) ;
1561+
1562+ // Bulk actions
1563+ const analyzeAll = React . useCallback ( async ( ) => {
1564+ const toAnalyze = fileStates . map ( ( s , i ) => ( { s, i } ) ) . filter ( x => ! x . s . analyzed && ! x . s . analyzing ) ;
1565+ await runWithConcurrency ( toAnalyze . map ( x => x . i ) , analyzeOne , 3 ) ;
1566+ } , [ fileStates , analyzeOne , runWithConcurrency ] ) ;
1567+
1568+ const sanitizeAll = React . useCallback ( async ( ) => {
1569+ const toSanitize = fileStates . map ( ( s , i ) => ( { s, i } ) ) . filter ( x => x . s . analyzed && ! x . s . sanitized && ! x . s . sanitizing ) ;
1570+ await runWithConcurrency ( toSanitize . map ( x => x . i ) , sanitizeOne , 3 ) ;
1571+ } , [ fileStates , sanitizeOne , runWithConcurrency ] ) ;
1572+
1573+ const downloadAll = React . useCallback ( ( ) => {
1574+ fileStates . forEach ( ( s , i ) => {
1575+ if ( s . outputUri ) {
1576+ setTimeout ( ( ) => {
1577+ const a = document . createElement ( 'a' ) ;
1578+ a . href = s . outputUri ;
1579+ a . download = `sanitized_${ s . file . name } ` ;
1580+ document . body . appendChild ( a ) ;
1581+ a . click ( ) ;
1582+ document . body . removeChild ( a ) ;
1583+ } , i * 100 ) ;
1584+ }
1585+ } ) ;
1586+ } , [ fileStates ] ) ;
1587+
14801588 // Sanitization handler
14811589 const handleSanitize = React . useCallback ( async ( ) => {
14821590 if ( ! selectedFile || ! analysisResult ) {
@@ -1585,7 +1693,8 @@ const MobileApp = () => {
15851693 // Ctrl+O - File upload
15861694 if ( event . ctrlKey && event . key === 'o' ) {
15871695 event . preventDefault ( ) ;
1588- document . getElementById ( 'file-input' ) ?. click ( ) ;
1696+ const multi = document . getElementById ( 'multi-file-input' ) ;
1697+ if ( multi ) multi . click ( ) ; else document . getElementById ( 'file-input' ) ?. click ( ) ;
15891698 }
15901699
15911700 // Ctrl+Z - Undo
@@ -1670,6 +1779,14 @@ const MobileApp = () => {
16701779 borderBottom : '1px solid #e5e7eb'
16711780 }
16721781 } ,
1782+ React . createElement ( 'button' , {
1783+ onClick : ( ) => document . getElementById ( 'multi-file-input' ) ?. click ( ) ,
1784+ style : { padding : '8px 16px' , background : '#2563eb' , color : 'white' , border : 'none' , borderRadius : '6px' , fontSize : '14px' , cursor : 'pointer' , fontWeight : '600' }
1785+ } , '➕ Add Files' ) ,
1786+ React . createElement ( 'input' , { id : 'multi-file-input' , type : 'file' , multiple : true , accept : 'image/*,application/pdf' , style : { display : 'none' } , onChange : handleAddFiles } ) ,
1787+ React . createElement ( 'button' , { onClick : analyzeAll , style : { padding : '8px 16px' , background : '#0ea5e9' , color : 'white' , border : 'none' , borderRadius : '6px' , fontSize : '14px' , cursor : 'pointer' , fontWeight : '600' } } , '🔍 Analyze All' ) ,
1788+ React . createElement ( 'button' , { onClick : sanitizeAll , style : { padding : '8px 16px' , background : '#10b981' , color : 'white' , border : 'none' , borderRadius : '6px' , fontSize : '14px' , cursor : 'pointer' , fontWeight : '600' } } , '🧼 Sanitize All' ) ,
1789+ React . createElement ( 'button' , { onClick : downloadAll , style : { padding : '8px 16px' , background : '#6b7280' , color : 'white' , border : 'none' , borderRadius : '6px' , fontSize : '14px' , cursor : 'pointer' , fontWeight : '600' } } , '⬇️ Download All' ) ,
16731790 React . createElement ( 'button' , {
16741791 onClick : ( ) => setShowPresets ( true ) ,
16751792 style : {
@@ -1724,8 +1841,56 @@ const MobileApp = () => {
17241841 } , '⌨️ Help' )
17251842 ) ,
17261843
1844+ // Multi-file layout
1845+ fileStates . length > 0 ? React . createElement ( 'div' , { style : { padding : '20px' } } ,
1846+ React . createElement ( 'div' , { style : { display : 'grid' , gridTemplateColumns : '260px 1fr' , gap : '16px' } } ,
1847+ // File list
1848+ React . createElement ( 'div' , { style : { background : 'white' , borderRadius : '12px' , padding : '12px' , boxShadow : '0 2px 8px rgba(0,0,0,0.06)' , maxHeight : '70vh' , overflowY : 'auto' } } ,
1849+ fileStates . map ( ( s , i ) => React . createElement ( 'div' , {
1850+ key : i , onClick : ( ) => setCurrentFileIndex ( i ) ,
1851+ style : { padding : '10px' , border : i === currentFileIndex ? '2px solid #2563eb' : '1px solid #e5e7eb' , borderRadius : '8px' , marginBottom : '8px' , cursor : 'pointer' , background : i === currentFileIndex ? '#eff6ff' : 'white' }
1852+ } ,
1853+ React . createElement ( 'div' , { style : { fontWeight : 600 , fontSize : '14px' , color : '#1f2937' } } , s . file . name ) ,
1854+ React . createElement ( 'div' , { style : { fontSize : '12px' , color : '#6b7280' } } , s . error ? `❌ ${ s . error } ` : s . sanitized ? '✅ Sanitized' : s . analyzed ? `🔍 ${ s . detections . length } detections` : ( s . analyzing ? '🔄 Analyzing…' : '⏳ Pending' ) )
1855+ ) )
1856+ ) ,
1857+ // Detail pane
1858+ ( ( ) => {
1859+ const fs = fileStates [ currentFileIndex ] || { } ;
1860+ return React . createElement ( 'div' , { style : { background : 'white' , borderRadius : '12px' , padding : '16px' , boxShadow : '0 2px 8px rgba(0,0,0,0.06)' } } ,
1861+ // Actions
1862+ React . createElement ( 'div' , { style : { display : 'flex' , gap : '8px' , marginBottom : '12px' , flexWrap : 'wrap' } } ,
1863+ React . createElement ( 'button' , { onClick : ( ) => analyzeOne ( currentFileIndex ) , disabled : fs . analyzing , style : { padding : '8px 12px' , background : '#0ea5e9' , color : 'white' , border : 'none' , borderRadius : '6px' , cursor : fs . analyzing ? 'not-allowed' : 'pointer' } } , fs . analyzing ? 'Analyzing…' : 'Analyze' ) ,
1864+ React . createElement ( 'button' , { onClick : ( ) => sanitizeOne ( currentFileIndex ) , disabled : ! fs . analyzed || fs . sanitizing , style : { padding : '8px 12px' , background : '#10b981' , color : 'white' , border : 'none' , borderRadius : '6px' , cursor : ( ! fs . analyzed || fs . sanitizing ) ? 'not-allowed' : 'pointer' } } , fs . sanitizing ? 'Sanitizing…' : 'Sanitize' ) ,
1865+ React . createElement ( 'button' , { onClick : ( ) => { if ( fs . outputUri ) { const a = document . createElement ( 'a' ) ; a . href = fs . outputUri ; a . download = `sanitized_${ fs . file . name } ` ; document . body . appendChild ( a ) ; a . click ( ) ; document . body . removeChild ( a ) ; } } , disabled : ! fs . outputUri , style : { padding : '8px 12px' , background : '#6b7280' , color : 'white' , border : 'none' , borderRadius : '6px' , cursor : fs . outputUri ? 'pointer' : 'not-allowed' } } , 'Download' )
1866+ ) ,
1867+ // Detections
1868+ fs . analyzed ? React . createElement ( 'div' , { style : { marginBottom : '12px' } } ,
1869+ React . createElement ( 'h3' , { style : { margin : '0 0 8px 0' , fontSize : '16px' , color : '#1f2937' } } , `Detections (${ fs . detections . length } )` ) ,
1870+ fs . detections . length === 0 ? React . createElement ( 'div' , { style : { color : '#059669' } } , 'No sensitive information detected' ) :
1871+ React . createElement ( 'div' , { style : { display : 'grid' , gap : '6px' } } ,
1872+ fs . detections . map ( d => React . createElement ( 'label' , { key : d . id , style : { display : 'flex' , alignItems : 'center' , gap : '8px' , fontSize : '14px' } } ,
1873+ React . createElement ( 'input' , { type : 'checkbox' , checked : ! ! fs . selected [ d . id ] , onChange : ( e ) => setFileStates ( prev => prev . map ( ( s , ii ) => ii === currentFileIndex ? { ...s , selected : { ...s . selected , [ d . id ] : e . target . checked } } : s ) ) } ) ,
1874+ React . createElement ( 'span' , { style : { minWidth : '80px' , color : '#374151' } } , d . kind ) ,
1875+ React . createElement ( 'span' , { style : { color : '#6b7280' } } , d . preview || d . text || '' ) ,
1876+ React . createElement ( 'select' , { value : ( fs . actions [ d . id ] ?. style ) || 'BOX' , onChange : ( e ) => setFileStates ( prev => prev . map ( ( s , ii ) => ii === currentFileIndex ? { ...s , actions : { ...s . actions , [ d . id ] : { style : e . target . value } } } : s ) ) } ,
1877+ [ 'BOX' , 'BLUR' , 'PIXELATE' , 'LABEL' , 'MASK_LAST4' ] . map ( opt => React . createElement ( 'option' , { key : opt , value : opt } , opt ) )
1878+ )
1879+ ) )
1880+ )
1881+ ) : React . createElement ( 'div' , { style : { color : '#6b7280' , marginBottom : '12px' } } , 'Analyze to view detections' ) ,
1882+ // Preview
1883+ fs . previewUri ? React . createElement ( 'div' , { style : { marginTop : '8px' } } ,
1884+ fs . file && fs . file . type === 'application/pdf' ? React . createElement ( 'embed' , { src : fs . previewUri , type : 'application/pdf' , style : { width : '100%' , height : '400px' , border : '1px solid #e5e7eb' , borderRadius : '8px' } } )
1885+ : React . createElement ( 'img' , { src : fs . previewUri , alt : 'Preview' , style : { maxWidth : '100%' , maxHeight : '400px' , border : '1px solid #e5e7eb' , borderRadius : '8px' } } )
1886+ ) : null
1887+ ) ;
1888+ } ) ( )
1889+ )
1890+ ) : null ,
1891+
17271892 // Main Content - File Processing Interface
1728- React . createElement ( 'div' , {
1893+ ( fileStates . length === 0 ) && React . createElement ( 'div' , {
17291894 style : { padding : '20px' , maxWidth : '800px' , margin : '0 auto' }
17301895 } ,
17311896
0 commit comments