Skip to content

Commit e3dab64

Browse files
committed
update
1 parent 8e57f77 commit e3dab64

1 file changed

Lines changed: 167 additions & 2 deletions

File tree

apps/mobile/web/mobile-fixed.js

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)