light dark and font size and stripping (#7)

* light dark and font size

* striping

* remove delete and rename buttons from main screen

* add proguard rule
This commit is contained in:
2026-04-21 03:43:46 +05:30
committed by GitHub
parent 520a9029db
commit 46b421acf2
9 changed files with 204 additions and 277 deletions

View File

@@ -48,7 +48,7 @@ react {
/** /**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode. * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/ */
def enableProguardInReleaseBuilds = false def enableProguardInReleaseBuilds = true
/** /**
* The preferred build flavor of JavaScriptCore (JSC) * The preferred build flavor of JavaScriptCore (JSC)
@@ -94,6 +94,7 @@ android {
// see https://reactnative.dev/docs/signed-apk-android. // see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds minifyEnabled enableProguardInReleaseBuilds
shrinkResources enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
} }
} }

View File

@@ -8,3 +8,6 @@
# http://developer.android.com/guide/developing/tools/proguard.html # http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here: # Add any project specific keep options here:
# pdfbox optionally references this JP2 decoder which is not in our deps
-dontwarn com.gemalto.jp2.**

View File

@@ -2,23 +2,27 @@ import React from 'react';
import {View, Text, Image, TouchableOpacity, StyleSheet} from 'react-native'; import {View, Text, Image, TouchableOpacity, StyleSheet} from 'react-native';
import {ScannedDocument} from '../types'; import {ScannedDocument} from '../types';
import {formatDate} from '../utils/imageProcessing'; import {formatDate} from '../utils/imageProcessing';
import {useTheme} from '../theme';
interface Props { interface Props {
document: ScannedDocument; document: ScannedDocument;
onPress: () => void; onPress: () => void;
onDelete: () => void;
onRename: () => void;
} }
export default function DocumentCard({document, onPress, onDelete, onRename}: Props) { export default function DocumentCard({document, onPress}: Props) {
const t = useTheme();
const thumb = document.pages[0]?.uri; const thumb = document.pages[0]?.uri;
return ( return (
<TouchableOpacity testID={`document-card-${document.id}`} style={styles.card} onPress={onPress} activeOpacity={0.8}> <TouchableOpacity
testID={`document-card-${document.id}`}
style={[styles.card, {backgroundColor: t.surface, borderColor: t.border}]}
onPress={onPress}
activeOpacity={0.8}>
<View style={styles.thumbContainer}> <View style={styles.thumbContainer}>
{thumb ? ( {thumb ? (
<Image source={{uri: thumb}} style={styles.thumb} resizeMode="cover" /> <Image source={{uri: thumb}} style={styles.thumb} resizeMode="cover" />
) : ( ) : (
<View style={[styles.thumb, styles.placeholder]} /> <View style={[styles.thumb, {backgroundColor: t.border}]} />
)} )}
{document.pages.length > 1 && ( {document.pages.length > 1 && (
<View style={styles.badge}> <View style={styles.badge}>
@@ -27,20 +31,12 @@ export default function DocumentCard({document, onPress, onDelete, onRename}: Pr
)} )}
</View> </View>
<View style={styles.info}> <View style={styles.info}>
<Text style={styles.name} numberOfLines={1}>{document.name}</Text> <Text style={[styles.name, {color: t.text}]} numberOfLines={1}>{document.name}</Text>
<Text style={styles.date}>{formatDate(document.createdAt)}</Text> <Text style={[styles.meta, {color: t.textSecondary}]}>{formatDate(document.createdAt)}</Text>
<Text style={styles.pages}> <Text style={[styles.meta, {color: t.textSecondary}]}>
{document.pages.length} {document.pages.length === 1 ? 'page' : 'pages'} {document.pages.length} {document.pages.length === 1 ? 'page' : 'pages'}
</Text> </Text>
</View> </View>
<View style={styles.actions}>
<TouchableOpacity testID={`rename-card-${document.id}`} onPress={onRename} hitSlop={{top: 8, bottom: 8, left: 8, right: 8}}>
<Text style={styles.renameText}></Text>
</TouchableOpacity>
<TouchableOpacity testID={`delete-card-${document.id}`} onPress={onDelete} hitSlop={{top: 8, bottom: 8, left: 8, right: 8}}>
<Text style={styles.deleteText}></Text>
</TouchableOpacity>
</View>
</TouchableOpacity> </TouchableOpacity>
); );
} }
@@ -48,24 +44,15 @@ export default function DocumentCard({document, onPress, onDelete, onRename}: Pr
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
flexDirection: 'row', flexDirection: 'row',
backgroundColor: '#1c1c1e',
borderRadius: 12, borderRadius: 12,
borderWidth: StyleSheet.hairlineWidth,
marginHorizontal: 16, marginHorizontal: 16,
marginVertical: 6, marginVertical: 6,
overflow: 'hidden', overflow: 'hidden',
elevation: 2, elevation: 2,
}, },
thumbContainer: { thumbContainer: {width: 80, height: 100},
width: 80, thumb: {width: 80, height: 100},
height: 100,
},
thumb: {
width: 80,
height: 100,
},
placeholder: {
backgroundColor: '#333',
},
badge: { badge: {
position: 'absolute', position: 'absolute',
bottom: 4, bottom: 4,
@@ -78,43 +65,8 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
paddingHorizontal: 4, paddingHorizontal: 4,
}, },
badgeText: { badgeText: {color: '#fff', fontSize: 11, fontWeight: '700'},
color: '#fff', info: {flex: 1, padding: 12, justifyContent: 'center', gap: 3},
fontSize: 11, name: {fontSize: 15, fontWeight: '600'},
fontWeight: '700', meta: {fontSize: 13},
},
info: {
flex: 1,
padding: 12,
justifyContent: 'center',
},
name: {
color: '#fff',
fontSize: 15,
fontWeight: '600',
marginBottom: 4,
},
date: {
color: '#888',
fontSize: 12,
marginBottom: 2,
},
pages: {
color: '#666',
fontSize: 12,
},
actions: {
padding: 12,
justifyContent: 'center',
gap: 16,
},
renameText: {
color: '#007AFF',
fontSize: 18,
},
deleteText: {
color: '#ff453a',
fontSize: 16,
fontWeight: '600',
},
}); });

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import {View, Text, TouchableOpacity, StyleSheet} from 'react-native'; import {View, Text, TouchableOpacity, StyleSheet} from 'react-native';
import {FilterMode} from '../types'; import {FilterMode} from '../types';
import {useTheme} from '../theme';
const FILTERS: {label: string; value: FilterMode}[] = [ const FILTERS: {label: string; value: FilterMode}[] = [
{label: 'Original', value: 'original'}, {label: 'Original', value: 'original'},
@@ -15,14 +16,23 @@ interface Props {
} }
export default function FilterPicker({selected, onChange}: Props) { export default function FilterPicker({selected, onChange}: Props) {
const t = useTheme();
return ( return (
<View style={styles.row}> <View style={[styles.row, {backgroundColor: t.surface, borderTopColor: t.border}]}>
{FILTERS.map(f => ( {FILTERS.map(f => (
<TouchableOpacity <TouchableOpacity
key={f.value} key={f.value}
style={[styles.chip, selected === f.value && styles.chipActive]} style={[
styles.chip,
{borderColor: t.border},
selected === f.value && {backgroundColor: t.accent, borderColor: t.accent},
]}
onPress={() => onChange(f.value)}> onPress={() => onChange(f.value)}>
<Text style={[styles.label, selected === f.value && styles.labelActive]}> <Text style={[
styles.label,
{color: t.textSecondary},
selected === f.value && {color: '#fff'},
]}>
{f.label} {f.label}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@@ -36,26 +46,14 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
gap: 8, gap: 8,
paddingVertical: 10, paddingVertical: 12,
backgroundColor: '#1c1c1e', borderTopWidth: StyleSheet.hairlineWidth,
}, },
chip: { chip: {
paddingHorizontal: 14, paddingHorizontal: 16,
paddingVertical: 6, paddingVertical: 7,
borderRadius: 20, borderRadius: 20,
borderWidth: 1, borderWidth: 1,
borderColor: '#444',
},
chipActive: {
backgroundColor: '#007AFF',
borderColor: '#007AFF',
},
label: {
color: '#aaa',
fontSize: 13,
fontWeight: '500',
},
labelActive: {
color: '#fff',
}, },
label: {fontSize: 14, fontWeight: '500'},
}); });

View File

@@ -12,6 +12,7 @@ import {
} from 'react-native'; } from 'react-native';
import {ScannedDocument} from '../types'; import {ScannedDocument} from '../types';
import DocumentCard from '../components/DocumentCard'; import DocumentCard from '../components/DocumentCard';
import {useTheme} from '../theme';
interface Props { interface Props {
documents: ScannedDocument[]; documents: ScannedDocument[];
@@ -22,6 +23,7 @@ interface Props {
} }
export default function HomeScreen({documents, onScan, onView, onDelete, onRename}: Props) { export default function HomeScreen({documents, onScan, onView, onDelete, onRename}: Props) {
const t = useTheme();
const [renameTarget, setRenameTarget] = useState<ScannedDocument | null>(null); const [renameTarget, setRenameTarget] = useState<ScannedDocument | null>(null);
const [renameText, setRenameText] = useState(''); const [renameText, setRenameText] = useState('');
@@ -45,18 +47,22 @@ export default function HomeScreen({documents, onScan, onView, onDelete, onRenam
}; };
return ( return (
<View style={styles.container}> <View style={[styles.container, {backgroundColor: t.bg}]}>
<StatusBar barStyle="light-content" backgroundColor="#000" /> <StatusBar barStyle={t.statusBar} backgroundColor={t.bg} />
<View style={styles.header}> <View style={[styles.header, {borderBottomColor: t.border}]}>
<Text style={styles.title}>Lens</Text> <Text style={[styles.title, {color: t.text}]}>Lens</Text>
<Text style={styles.subtitle}>{documents.length} document{documents.length !== 1 ? 's' : ''}</Text> <Text style={[styles.subtitle, {color: t.textSecondary}]}>
{documents.length} document{documents.length !== 1 ? 's' : ''}
</Text>
</View> </View>
{documents.length === 0 ? ( {documents.length === 0 ? (
<View testID="empty-state" style={styles.empty}> <View testID="empty-state" style={styles.empty}>
<Text style={styles.emptyIcon}>📄</Text> <Text style={styles.emptyIcon}>📄</Text>
<Text style={styles.emptyTitle}>No scans yet</Text> <Text style={[styles.emptyTitle, {color: t.text}]}>No scans yet</Text>
<Text style={styles.emptyHint}>Tap the button below to scan a document</Text> <Text style={[styles.emptyHint, {color: t.textSecondary}]}>
Tap the button below to scan a document
</Text>
</View> </View>
) : ( ) : (
<FlatList <FlatList
@@ -67,8 +73,6 @@ export default function HomeScreen({documents, onScan, onView, onDelete, onRenam
<DocumentCard <DocumentCard
document={item} document={item}
onPress={() => onView(item)} onPress={() => onView(item)}
onDelete={() => confirmDelete(item)}
onRename={() => startRename(item)}
/> />
)} )}
contentContainerStyle={styles.list} contentContainerStyle={styles.list}
@@ -82,23 +86,23 @@ export default function HomeScreen({documents, onScan, onView, onDelete, onRenam
<Modal visible={!!renameTarget} transparent animationType="fade"> <Modal visible={!!renameTarget} transparent animationType="fade">
<View style={styles.modalBg}> <View style={styles.modalBg}>
<View testID="rename-modal" style={styles.modalBox}> <View testID="rename-modal" style={[styles.modalBox, {backgroundColor: t.surface}]}>
<Text style={styles.modalTitle}>Rename Document</Text> <Text style={[styles.modalTitle, {color: t.text}]}>Rename Document</Text>
<TextInput <TextInput
testID="rename-input" testID="rename-input"
style={styles.modalInput} style={[styles.modalInput, {backgroundColor: t.bg, color: t.text, borderColor: t.border}]}
value={renameText} value={renameText}
onChangeText={setRenameText} onChangeText={setRenameText}
autoFocus autoFocus
selectTextOnFocus selectTextOnFocus
placeholderTextColor="#666" placeholderTextColor={t.textSecondary}
/> />
<View style={styles.modalActions}> <View style={styles.modalActions}>
<TouchableOpacity onPress={() => setRenameTarget(null)} style={styles.modalBtn}> <TouchableOpacity onPress={() => setRenameTarget(null)} style={styles.modalBtn}>
<Text style={styles.modalCancel}>Cancel</Text> <Text style={[styles.modalCancel, {color: t.textSecondary}]}>Cancel</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity testID="rename-confirm" onPress={submitRename} style={styles.modalBtn}> <TouchableOpacity testID="rename-confirm" onPress={submitRename} style={styles.modalBtn}>
<Text style={styles.modalConfirm}>Rename</Text> <Text style={[styles.modalConfirm, {color: t.accent}]}>Rename</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
@@ -109,21 +113,20 @@ export default function HomeScreen({documents, onScan, onView, onDelete, onRenam
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: {flex: 1, backgroundColor: '#000'}, container: {flex: 1},
header: { header: {
paddingTop: 56, paddingTop: 56,
paddingHorizontal: 20, paddingHorizontal: 20,
paddingBottom: 16, paddingBottom: 16,
borderBottomWidth: 1, borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#222',
}, },
title: {color: '#fff', fontSize: 32, fontWeight: '700'}, title: {fontSize: 32, fontWeight: '700'},
subtitle: {color: '#666', fontSize: 14, marginTop: 2}, subtitle: {fontSize: 15, marginTop: 2},
list: {paddingTop: 8, paddingBottom: 100}, list: {paddingTop: 8, paddingBottom: 100},
empty: {flex: 1, alignItems: 'center', justifyContent: 'center', gap: 8}, empty: {flex: 1, alignItems: 'center', justifyContent: 'center', gap: 8},
emptyIcon: {fontSize: 64}, emptyIcon: {fontSize: 64},
emptyTitle: {color: '#fff', fontSize: 20, fontWeight: '600'}, emptyTitle: {fontSize: 20, fontWeight: '600'},
emptyHint: {color: '#666', fontSize: 14}, emptyHint: {fontSize: 15},
fab: { fab: {
position: 'absolute', position: 'absolute',
bottom: 32, bottom: 32,
@@ -142,18 +145,17 @@ const styles = StyleSheet.create({
}, },
fabIcon: {color: '#fff', fontSize: 22}, fabIcon: {color: '#fff', fontSize: 22},
fabLabel: {color: '#fff', fontSize: 17, fontWeight: '600'}, fabLabel: {color: '#fff', fontSize: 17, fontWeight: '600'},
modalBg: {flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'center', padding: 24}, modalBg: {flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', padding: 24},
modalBox: {backgroundColor: '#1c1c1e', borderRadius: 16, padding: 20, gap: 16}, modalBox: {borderRadius: 16, padding: 20, gap: 16},
modalTitle: {color: '#fff', fontSize: 17, fontWeight: '600'}, modalTitle: {fontSize: 17, fontWeight: '600'},
modalInput: { modalInput: {
backgroundColor: '#2c2c2e', borderWidth: StyleSheet.hairlineWidth,
borderRadius: 10, borderRadius: 10,
padding: 12, padding: 12,
color: '#fff',
fontSize: 15, fontSize: 15,
}, },
modalActions: {flexDirection: 'row', justifyContent: 'flex-end', gap: 16}, modalActions: {flexDirection: 'row', justifyContent: 'flex-end', gap: 16},
modalBtn: {padding: 4}, modalBtn: {padding: 4},
modalCancel: {color: '#888', fontSize: 15}, modalCancel: {fontSize: 15},
modalConfirm: {color: '#007AFF', fontSize: 15, fontWeight: '600'}, modalConfirm: {fontSize: 15, fontWeight: '600'},
}); });

View File

@@ -7,11 +7,11 @@ import {
TouchableOpacity, TouchableOpacity,
StyleSheet, StyleSheet,
Dimensions, Dimensions,
Alert,
StatusBar, StatusBar,
} from 'react-native'; } from 'react-native';
import {ScannedPage, FilterMode} from '../types'; import {ScannedPage, FilterMode} from '../types';
import FilterPicker from '../components/FilterPicker'; import FilterPicker from '../components/FilterPicker';
import {useTheme} from '../theme';
const {width: SW} = Dimensions.get('window'); const {width: SW} = Dimensions.get('window');
@@ -23,17 +23,10 @@ interface Props {
} }
export default function ReviewScreen({pages, onSave, onAddMore, onCancel}: Props) { export default function ReviewScreen({pages, onSave, onAddMore, onCancel}: Props) {
const t = useTheme();
const [filter, setFilter] = useState<FilterMode>('original'); const [filter, setFilter] = useState<FilterMode>('original');
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const filterStyle = () => {
switch (filter) {
case 'grayscale': return {opacity: 0.85, tintColor: undefined};
case 'blackwhite': return {opacity: 1};
default: return {};
}
};
const imageContainerStyle = () => { const imageContainerStyle = () => {
switch (filter) { switch (filter) {
case 'grayscale': return styles.filterGrayscale; case 'grayscale': return styles.filterGrayscale;
@@ -44,25 +37,21 @@ export default function ReviewScreen({pages, onSave, onAddMore, onCancel}: Props
}; };
return ( return (
<View style={styles.container}> <View style={[styles.container, {backgroundColor: t.bg}]}>
<StatusBar barStyle="light-content" backgroundColor="#000" /> <StatusBar barStyle={t.statusBar} backgroundColor={t.bg} />
{/* Header */} <View style={[styles.header, {borderBottomColor: t.border}]}>
<View style={styles.header}>
<TouchableOpacity onPress={onCancel} style={styles.headerBtn}> <TouchableOpacity onPress={onCancel} style={styles.headerBtn}>
<Text style={styles.headerBtnText}>Cancel</Text> <Text style={[styles.headerBtnText, {color: t.accent}]}>Cancel</Text>
</TouchableOpacity> </TouchableOpacity>
<Text style={styles.headerTitle}> <Text style={[styles.headerTitle, {color: t.text}]}>
{currentIndex + 1} / {pages.length} {currentIndex + 1} / {pages.length}
</Text> </Text>
<TouchableOpacity <TouchableOpacity onPress={() => onSave(pages, filter)} style={styles.saveBtn}>
onPress={() => onSave(pages, filter)}
style={styles.saveBtn}>
<Text style={styles.saveBtnText}>Save</Text> <Text style={styles.saveBtnText}>Save</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* Page viewer */}
<ScrollView <ScrollView
horizontal horizontal
pagingEnabled pagingEnabled
@@ -74,43 +63,34 @@ export default function ReviewScreen({pages, onSave, onAddMore, onCancel}: Props
{pages.map(page => ( {pages.map(page => (
<View key={page.id} style={styles.pageContainer}> <View key={page.id} style={styles.pageContainer}>
<View style={[styles.imageWrapper, imageContainerStyle()]}> <View style={[styles.imageWrapper, imageContainerStyle()]}>
<Image <Image source={{uri: page.uri}} style={styles.pageImage} resizeMode="contain" />
source={{uri: page.uri}}
style={styles.pageImage}
resizeMode="contain"
/>
</View> </View>
</View> </View>
))} ))}
</ScrollView> </ScrollView>
{/* Dot indicators */}
{pages.length > 1 && ( {pages.length > 1 && (
<View style={styles.dots}> <View style={styles.dots}>
{pages.map((_, i) => ( {pages.map((_, i) => (
<View <View
key={i} key={i}
style={[styles.dot, i === currentIndex && styles.dotActive]} style={[styles.dot, {backgroundColor: t.border}, i === currentIndex && styles.dotActive]}
/> />
))} ))}
</View> </View>
)} )}
{/* Filter picker */}
<FilterPicker selected={filter} onChange={setFilter} /> <FilterPicker selected={filter} onChange={setFilter} />
{/* Bottom actions */} <View style={[styles.bottomActions, {borderTopColor: t.border}]}>
<View style={styles.bottomActions}> <TouchableOpacity style={[styles.actionBtn, {borderColor: t.border}]} onPress={onAddMore}>
<TouchableOpacity style={styles.actionBtn} onPress={onAddMore}> <Text style={[styles.actionIcon, {color: t.accent}]}>+</Text>
<Text style={styles.actionIcon}>+</Text> <Text style={[styles.actionLabel, {color: t.accent}]}>Add Page</Text>
<Text style={styles.actionLabel}>Add Page</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.actionBtn, styles.actionBtnPrimary]} style={[styles.actionBtn, styles.actionBtnPrimary]}
onPress={() => onSave(pages, filter)}> onPress={() => onSave(pages, filter)}>
<Text style={[styles.actionLabel, styles.actionLabelPrimary]}> <Text style={[styles.actionLabel, styles.actionLabelPrimary]}>Save Document</Text>
Save Document
</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
@@ -118,7 +98,7 @@ export default function ReviewScreen({pages, onSave, onAddMore, onCancel}: Props
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: {flex: 1, backgroundColor: '#000'}, container: {flex: 1},
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@@ -126,12 +106,11 @@ const styles = StyleSheet.create({
paddingTop: 52, paddingTop: 52,
paddingHorizontal: 16, paddingHorizontal: 16,
paddingBottom: 12, paddingBottom: 12,
borderBottomWidth: 1, borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#222',
}, },
headerBtn: {padding: 4}, headerBtn: {padding: 4},
headerBtnText: {color: '#007AFF', fontSize: 16}, headerBtnText: {fontSize: 16},
headerTitle: {color: '#fff', fontSize: 16, fontWeight: '600'}, headerTitle: {fontSize: 16, fontWeight: '600'},
saveBtn: { saveBtn: {
backgroundColor: '#007AFF', backgroundColor: '#007AFF',
paddingHorizontal: 16, paddingHorizontal: 16,
@@ -146,34 +125,20 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
padding: 16, padding: 16,
}, },
imageWrapper: { imageWrapper: {flex: 1, width: '100%', borderRadius: 8, overflow: 'hidden'},
flex: 1,
width: '100%',
borderRadius: 8,
overflow: 'hidden',
},
pageImage: {flex: 1, width: '100%'}, pageImage: {flex: 1, width: '100%'},
filterGrayscale: {opacity: 0.9}, filterGrayscale: {opacity: 0.9},
filterBW: {opacity: 1}, filterBW: {opacity: 1},
filterEnhanced: {opacity: 1}, filterEnhanced: {opacity: 1},
dots: { dots: {flexDirection: 'row', justifyContent: 'center', gap: 6, paddingVertical: 8},
flexDirection: 'row', dot: {width: 6, height: 6, borderRadius: 3},
justifyContent: 'center',
gap: 6,
paddingVertical: 8,
},
dot: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: '#444',
},
dotActive: {backgroundColor: '#007AFF', width: 18}, dotActive: {backgroundColor: '#007AFF', width: 18},
bottomActions: { bottomActions: {
flexDirection: 'row', flexDirection: 'row',
gap: 12, gap: 12,
padding: 16, padding: 16,
paddingBottom: 32, paddingBottom: 32,
borderTopWidth: StyleSheet.hairlineWidth,
}, },
actionBtn: { actionBtn: {
flex: 1, flex: 1,
@@ -184,14 +149,13 @@ const styles = StyleSheet.create({
paddingVertical: 14, paddingVertical: 14,
borderRadius: 14, borderRadius: 14,
borderWidth: 1, borderWidth: 1,
borderColor: '#333',
}, },
actionBtnPrimary: { actionBtnPrimary: {
backgroundColor: '#007AFF', backgroundColor: '#007AFF',
borderColor: '#007AFF', borderColor: '#007AFF',
flex: 2, flex: 2,
}, },
actionIcon: {color: '#007AFF', fontSize: 20, fontWeight: '600'}, actionIcon: {fontSize: 20, fontWeight: '600'},
actionLabel: {color: '#007AFF', fontSize: 15, fontWeight: '500'}, actionLabel: {fontSize: 15, fontWeight: '500'},
actionLabelPrimary: {color: '#fff', fontWeight: '600'}, actionLabelPrimary: {color: '#fff', fontWeight: '600'},
}); });

View File

@@ -11,6 +11,7 @@ import {
import DocumentScanner, {ResponseType} from 'react-native-document-scanner-plugin'; import DocumentScanner, {ResponseType} from 'react-native-document-scanner-plugin';
import RNFS from 'react-native-fs'; import RNFS from 'react-native-fs';
import {ScannedPage} from '../types'; import {ScannedPage} from '../types';
import {useTheme} from '../theme';
interface Props { interface Props {
onComplete: (pages: ScannedPage[]) => void; onComplete: (pages: ScannedPage[]) => void;
@@ -31,9 +32,9 @@ async function copyToPermanent(tempUri: string): Promise<string> {
} }
export default function ScanScreen({onComplete, onCancel}: Props) { export default function ScanScreen({onComplete, onCancel}: Props) {
const t = useTheme();
const [scanning, setScanning] = useState(false); const [scanning, setScanning] = useState(false);
const [pageCount, setPageCount] = useState(0); const [pageCount, setPageCount] = useState(0);
// Use a ref so the Alert closure always reads the latest pages without stale capture
const pagesRef = useRef<ScannedPage[]>([]); const pagesRef = useRef<ScannedPage[]>([]);
const scan = useCallback(async () => { const scan = useCallback(async () => {
@@ -58,7 +59,6 @@ export default function ScanScreen({onComplete, onCancel}: Props) {
return; return;
} }
// Copy temp files to permanent storage before the OS clears them
const permanentUris = await Promise.all(scannedImages.map(copyToPermanent)); const permanentUris = await Promise.all(scannedImages.map(copyToPermanent));
const newPages: ScannedPage[] = permanentUris.map(uri => ({ const newPages: ScannedPage[] = permanentUris.map(uri => ({
@@ -92,25 +92,23 @@ export default function ScanScreen({onComplete, onCancel}: Props) {
}, []); }, []);
return ( return (
<View style={styles.container}> <View style={[styles.container, {backgroundColor: t.bg}]}>
<StatusBar barStyle="light-content" backgroundColor="#000" /> <StatusBar barStyle={t.statusBar} backgroundColor={t.bg} />
{scanning && ( {scanning && (
<View style={styles.center}> <View style={styles.center}>
<ActivityIndicator size="large" color="#007AFF" /> <ActivityIndicator size="large" color={t.accent} />
<Text style={styles.hint}>Opening camera</Text> <Text style={[styles.hint, {color: t.textSecondary}]}>Opening camera</Text>
</View> </View>
)} )}
{!scanning && pageCount > 0 && ( {!scanning && pageCount > 0 && (
<View style={styles.center}> <View style={styles.center}>
<Text style={styles.count}>{pageCount}</Text> <Text style={[styles.count, {color: t.text}]}>{pageCount}</Text>
<Text style={styles.pagesLabel}>pages ready</Text> <Text style={[styles.pagesLabel, {color: t.textSecondary}]}>pages ready</Text>
<View style={styles.actions}> <View style={styles.actions}>
<TouchableOpacity style={styles.btnSecondary} onPress={scan}> <TouchableOpacity style={[styles.btnSecondary, {borderColor: t.accent}]} onPress={scan}>
<Text style={styles.btnSecondaryText}>Add More</Text> <Text style={[styles.btnSecondaryText, {color: t.accent}]}>Add More</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity style={styles.btnPrimary} onPress={() => onComplete(pagesRef.current)}>
style={styles.btnPrimary}
onPress={() => onComplete(pagesRef.current)}>
<Text style={styles.btnPrimaryText}>Finish</Text> <Text style={styles.btnPrimaryText}>Finish</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -121,11 +119,11 @@ export default function ScanScreen({onComplete, onCancel}: Props) {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: {flex: 1, backgroundColor: '#000'}, container: {flex: 1},
center: {flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12}, center: {flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12},
hint: {color: '#888', fontSize: 15, marginTop: 12}, hint: {fontSize: 15, marginTop: 12},
count: {color: '#fff', fontSize: 72, fontWeight: '700'}, count: {fontSize: 72, fontWeight: '700'},
pagesLabel: {color: '#888', fontSize: 18}, pagesLabel: {fontSize: 18},
actions: {flexDirection: 'row', gap: 16, marginTop: 24}, actions: {flexDirection: 'row', gap: 16, marginTop: 24},
btnPrimary: { btnPrimary: {
backgroundColor: '#007AFF', backgroundColor: '#007AFF',
@@ -136,10 +134,9 @@ const styles = StyleSheet.create({
btnPrimaryText: {color: '#fff', fontSize: 16, fontWeight: '600'}, btnPrimaryText: {color: '#fff', fontSize: 16, fontWeight: '600'},
btnSecondary: { btnSecondary: {
borderWidth: 1, borderWidth: 1,
borderColor: '#007AFF',
paddingHorizontal: 32, paddingHorizontal: 32,
paddingVertical: 14, paddingVertical: 14,
borderRadius: 28, borderRadius: 28,
}, },
btnSecondaryText: {color: '#007AFF', fontSize: 16, fontWeight: '600'}, btnSecondaryText: {fontSize: 16, fontWeight: '600'},
}); });

View File

@@ -16,6 +16,7 @@ import {
import Share from 'react-native-share'; import Share from 'react-native-share';
import {ScannedDocument} from '../types'; import {ScannedDocument} from '../types';
import {generatePdf} from '../utils/generatePdf'; import {generatePdf} from '../utils/generatePdf';
import {useTheme} from '../theme';
const {width: SW} = Dimensions.get('window'); const {width: SW} = Dimensions.get('window');
@@ -27,6 +28,7 @@ interface Props {
} }
export default function ViewerScreen({document, onBack, onDelete, onRename}: Props) { export default function ViewerScreen({document, onBack, onDelete, onRename}: Props) {
const t = useTheme();
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [generating, setGenerating] = useState(false); const [generating, setGenerating] = useState(false);
const [renaming, setRenaming] = useState(false); const [renaming, setRenaming] = useState(false);
@@ -35,22 +37,17 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
const sharePdf = async (pageIndices?: number[]) => { const sharePdf = async (pageIndices?: number[]) => {
setGenerating(true); setGenerating(true);
try { try {
const pages = const pages = pageIndices
pageIndices ? pageIndices.map(i => document.pages[i]).filter(Boolean)
? pageIndices.map(i => document.pages[i]).filter(Boolean) : document.pages;
: document.pages; const label = pageIndices?.length === 1
? `${document.name} — page ${pageIndices[0] + 1}`
const label = : document.name;
pageIndices?.length === 1
? `${document.name} — page ${pageIndices[0] + 1}`
: document.name;
const pdfPath = await generatePdf(pages, label); const pdfPath = await generatePdf(pages, label);
await Share.open({ await Share.open({
title: label, title: label,
type: 'application/pdf', type: 'application/pdf',
url: pdfPath, // already normalised to file:// by generatePdf url: pdfPath,
failOnCancel: false, failOnCancel: false,
}); });
} catch (err: any) { } catch (err: any) {
@@ -68,16 +65,16 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
}; };
return ( return (
<View style={styles.container}> <View style={[styles.container, {backgroundColor: t.bg}]}>
<StatusBar barStyle="light-content" backgroundColor="#000" /> <StatusBar barStyle={t.statusBar} backgroundColor={t.bg} />
<View style={styles.header}> <View style={[styles.header, {borderBottomColor: t.border}]}>
<TouchableOpacity testID="viewer-back" onPress={onBack} style={styles.headerBtn}> <TouchableOpacity testID="viewer-back" onPress={onBack} style={styles.headerBtn}>
<Text style={styles.back}> Back</Text> <Text style={[styles.back, {color: t.accent}]}> Back</Text>
</TouchableOpacity> </TouchableOpacity>
<View style={styles.headerCenter}> <View style={styles.headerCenter}>
<Text style={styles.title} numberOfLines={1}>{document.name}</Text> <Text style={[styles.title, {color: t.text}]} numberOfLines={1}>{document.name}</Text>
<Text style={styles.subtitle}> <Text style={[styles.subtitle, {color: t.textSecondary}]}>
{currentIndex + 1} / {document.pages.length} {currentIndex + 1} / {document.pages.length}
</Text> </Text>
</View> </View>
@@ -86,7 +83,7 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
onPress={() => sharePdf()} onPress={() => sharePdf()}
style={styles.headerBtn} style={styles.headerBtn}
disabled={generating}> disabled={generating}>
<Text style={styles.shareText}>Share PDF</Text> <Text style={[styles.shareText, {color: t.accent}]}>Share PDF</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -105,7 +102,7 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
style={styles.pageImage} style={styles.pageImage}
resizeMode="contain" resizeMode="contain"
/> />
<Text style={styles.pageNum}>Page {i + 1}</Text> <Text style={[styles.pageNum, {color: t.textSecondary}]}>Page {i + 1}</Text>
</View> </View>
))} ))}
</ScrollView> </ScrollView>
@@ -113,33 +110,32 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
{document.pages.length > 1 && ( {document.pages.length > 1 && (
<View style={styles.dots}> <View style={styles.dots}>
{document.pages.map((_, i) => ( {document.pages.map((_, i) => (
<View key={i} style={[styles.dot, i === currentIndex && styles.dotActive]} /> <View
key={i}
style={[styles.dot, {backgroundColor: t.border}, i === currentIndex && styles.dotActive]}
/>
))} ))}
</View> </View>
)} )}
<View style={styles.toolbar}> <View style={[styles.toolbar, {borderTopColor: t.border}]}>
<TouchableOpacity <TouchableOpacity style={styles.toolBtn} onPress={() => sharePdf([currentIndex])} disabled={generating}>
style={styles.toolBtn} <Text style={[styles.toolIcon, {color: t.accent}]}></Text>
onPress={() => sharePdf([currentIndex])} <Text style={[styles.toolLabel, {color: t.accent}]}>Share Page</Text>
disabled={generating}> </TouchableOpacity>
<Text style={styles.toolIcon}></Text> <TouchableOpacity style={styles.toolBtn} onPress={() => sharePdf()} disabled={generating}>
<Text style={styles.toolLabel}>Share Page</Text> <Text style={[styles.toolIcon, {color: t.accent}]}>📄</Text>
<Text style={[styles.toolLabel, {color: t.accent}]}>Share All</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={styles.toolBtn} style={styles.toolBtn}
onPress={() => sharePdf()} onPress={() => {setRenameText(document.name); setRenaming(true);}}>
disabled={generating}> <Text style={[styles.toolIcon, {color: t.accent}]}></Text>
<Text style={styles.toolIcon}>📄</Text> <Text style={[styles.toolLabel, {color: t.accent}]}>Rename</Text>
<Text style={styles.toolLabel}>Share All</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.toolBtn} onPress={() => { setRenameText(document.name); setRenaming(true); }}> <TouchableOpacity style={styles.toolBtn} onPress={confirmDelete}>
<Text style={styles.toolIcon}></Text> <Text style={[styles.toolIcon, {color: t.danger}]}>🗑</Text>
<Text style={styles.toolLabel}>Rename</Text> <Text style={[styles.toolLabel, {color: t.danger}]}>Delete</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.toolBtn, styles.toolBtnDanger]} onPress={confirmDelete}>
<Text style={[styles.toolIcon, styles.toolIconDanger]}>🗑</Text>
<Text style={[styles.toolLabel, styles.toolLabelDanger]}>Delete</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@@ -152,19 +148,19 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
<Modal visible={renaming} transparent animationType="fade"> <Modal visible={renaming} transparent animationType="fade">
<View style={styles.modalBg}> <View style={styles.modalBg}>
<View style={styles.modalBox}> <View style={[styles.modalBox, {backgroundColor: t.surface}]}>
<Text style={styles.modalTitle}>Rename Document</Text> <Text style={[styles.modalTitle, {color: t.text}]}>Rename Document</Text>
<TextInput <TextInput
style={styles.modalInput} style={[styles.modalInput, {backgroundColor: t.bg, color: t.text, borderColor: t.border}]}
value={renameText} value={renameText}
onChangeText={setRenameText} onChangeText={setRenameText}
autoFocus autoFocus
selectTextOnFocus selectTextOnFocus
placeholderTextColor="#666" placeholderTextColor={t.textSecondary}
/> />
<View style={styles.modalActions}> <View style={styles.modalActions}>
<TouchableOpacity onPress={() => setRenaming(false)} style={styles.modalBtn}> <TouchableOpacity onPress={() => setRenaming(false)} style={styles.modalBtn}>
<Text style={styles.modalCancel}>Cancel</Text> <Text style={[styles.modalCancel, {color: t.textSecondary}]}>Cancel</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
@@ -172,7 +168,7 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
setRenaming(false); setRenaming(false);
}} }}
style={styles.modalBtn}> style={styles.modalBtn}>
<Text style={styles.modalConfirm}>Rename</Text> <Text style={[styles.modalConfirm, {color: t.accent}]}>Rename</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
@@ -183,22 +179,21 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: {flex: 1, backgroundColor: '#000'}, container: {flex: 1},
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingTop: 52, paddingTop: 52,
paddingHorizontal: 16, paddingHorizontal: 16,
paddingBottom: 12, paddingBottom: 12,
borderBottomWidth: 1, borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#222',
}, },
headerBtn: {minWidth: 60}, headerBtn: {minWidth: 60},
headerCenter: {flex: 1, alignItems: 'center'}, headerCenter: {flex: 1, alignItems: 'center'},
back: {color: '#007AFF', fontSize: 17}, back: {fontSize: 17},
title: {color: '#fff', fontSize: 15, fontWeight: '600'}, title: {fontSize: 15, fontWeight: '600'},
subtitle: {color: '#666', fontSize: 12, marginTop: 2}, subtitle: {fontSize: 13, marginTop: 2},
shareText: {color: '#007AFF', fontSize: 15, textAlign: 'right'}, shareText: {fontSize: 15, textAlign: 'right'},
pageContainer: { pageContainer: {
width: SW, width: SW,
flex: 1, flex: 1,
@@ -207,32 +202,18 @@ const styles = StyleSheet.create({
padding: 16, padding: 16,
}, },
pageImage: {flex: 1, width: '100%', borderRadius: 4}, pageImage: {flex: 1, width: '100%', borderRadius: 4},
pageNum: {color: '#444', fontSize: 12, marginTop: 8}, pageNum: {fontSize: 13, marginTop: 8},
dots: { dots: {flexDirection: 'row', justifyContent: 'center', gap: 6, paddingVertical: 8},
flexDirection: 'row', dot: {width: 6, height: 6, borderRadius: 3},
justifyContent: 'center',
gap: 6,
paddingVertical: 8,
},
dot: {width: 6, height: 6, borderRadius: 3, backgroundColor: '#333'},
dotActive: {backgroundColor: '#007AFF', width: 18}, dotActive: {backgroundColor: '#007AFF', width: 18},
toolbar: { toolbar: {
flexDirection: 'row', flexDirection: 'row',
borderTopWidth: 1, borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#222',
paddingBottom: 32, paddingBottom: 32,
}, },
toolBtn: { toolBtn: {flex: 1, alignItems: 'center', paddingVertical: 14, gap: 4},
flex: 1, toolIcon: {fontSize: 20},
alignItems: 'center', toolLabel: {fontSize: 13},
paddingVertical: 14,
gap: 4,
},
toolBtnDanger: {},
toolIcon: {fontSize: 20, color: '#007AFF'},
toolIconDanger: {color: '#ff453a'},
toolLabel: {fontSize: 11, color: '#007AFF'},
toolLabelDanger: {color: '#ff453a'},
loadingOverlay: { loadingOverlay: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.65)', backgroundColor: 'rgba(0,0,0,0.65)',
@@ -241,18 +222,17 @@ const styles = StyleSheet.create({
gap: 12, gap: 12,
}, },
loadingText: {color: '#fff', fontSize: 15}, loadingText: {color: '#fff', fontSize: 15},
modalBg: {flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'center', padding: 24}, modalBg: {flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', padding: 24},
modalBox: {backgroundColor: '#1c1c1e', borderRadius: 16, padding: 20, gap: 16}, modalBox: {borderRadius: 16, padding: 20, gap: 16},
modalTitle: {color: '#fff', fontSize: 17, fontWeight: '600'}, modalTitle: {fontSize: 17, fontWeight: '600'},
modalInput: { modalInput: {
backgroundColor: '#2c2c2e', borderWidth: StyleSheet.hairlineWidth,
borderRadius: 10, borderRadius: 10,
padding: 12, padding: 12,
color: '#fff',
fontSize: 15, fontSize: 15,
}, },
modalActions: {flexDirection: 'row', justifyContent: 'flex-end', gap: 16}, modalActions: {flexDirection: 'row', justifyContent: 'flex-end', gap: 16},
modalBtn: {padding: 4}, modalBtn: {padding: 4},
modalCancel: {color: '#888', fontSize: 15}, modalCancel: {fontSize: 15},
modalConfirm: {color: '#007AFF', fontSize: 15, fontWeight: '600'}, modalConfirm: {fontSize: 15, fontWeight: '600'},
}); });

30
src/theme.ts Normal file
View File

@@ -0,0 +1,30 @@
import {useColorScheme} from 'react-native';
const dark = {
bg: '#000000',
surface: '#1c1c1e',
text: '#ffffff',
textSecondary: '#8e8e93',
border: '#2c2c2e',
accent: '#007AFF',
danger: '#ff453a',
statusBar: 'light-content' as const,
};
const light = {
bg: '#f2f2f7',
surface: '#ffffff',
text: '#000000',
textSecondary: '#636366',
border: '#d1d1d6',
accent: '#007AFF',
danger: '#ff3b30',
statusBar: 'dark-content' as const,
};
export type Theme = typeof dark;
export function useTheme(): Theme {
const scheme = useColorScheme();
return scheme === 'dark' ? dark : light;
}