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:
@@ -48,7 +48,7 @@ react {
|
||||
/**
|
||||
* 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)
|
||||
@@ -94,6 +94,7 @@ android {
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
shrinkResources enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
|
||||
3
android/app/proguard-rules.pro
vendored
3
android/app/proguard-rules.pro
vendored
@@ -8,3 +8,6 @@
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# pdfbox optionally references this JP2 decoder which is not in our deps
|
||||
-dontwarn com.gemalto.jp2.**
|
||||
|
||||
@@ -2,23 +2,27 @@ import React from 'react';
|
||||
import {View, Text, Image, TouchableOpacity, StyleSheet} from 'react-native';
|
||||
import {ScannedDocument} from '../types';
|
||||
import {formatDate} from '../utils/imageProcessing';
|
||||
import {useTheme} from '../theme';
|
||||
|
||||
interface Props {
|
||||
document: ScannedDocument;
|
||||
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;
|
||||
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}>
|
||||
{thumb ? (
|
||||
<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 && (
|
||||
<View style={styles.badge}>
|
||||
@@ -27,20 +31,12 @@ export default function DocumentCard({document, onPress, onDelete, onRename}: Pr
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.info}>
|
||||
<Text style={styles.name} numberOfLines={1}>{document.name}</Text>
|
||||
<Text style={styles.date}>{formatDate(document.createdAt)}</Text>
|
||||
<Text style={styles.pages}>
|
||||
<Text style={[styles.name, {color: t.text}]} numberOfLines={1}>{document.name}</Text>
|
||||
<Text style={[styles.meta, {color: t.textSecondary}]}>{formatDate(document.createdAt)}</Text>
|
||||
<Text style={[styles.meta, {color: t.textSecondary}]}>
|
||||
{document.pages.length} {document.pages.length === 1 ? 'page' : 'pages'}
|
||||
</Text>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -48,24 +44,15 @@ export default function DocumentCard({document, onPress, onDelete, onRename}: Pr
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#1c1c1e',
|
||||
borderRadius: 12,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 6,
|
||||
overflow: 'hidden',
|
||||
elevation: 2,
|
||||
},
|
||||
thumbContainer: {
|
||||
width: 80,
|
||||
height: 100,
|
||||
},
|
||||
thumb: {
|
||||
width: 80,
|
||||
height: 100,
|
||||
},
|
||||
placeholder: {
|
||||
backgroundColor: '#333',
|
||||
},
|
||||
thumbContainer: {width: 80, height: 100},
|
||||
thumb: {width: 80, height: 100},
|
||||
badge: {
|
||||
position: 'absolute',
|
||||
bottom: 4,
|
||||
@@ -78,43 +65,8 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
badgeText: {
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
},
|
||||
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',
|
||||
},
|
||||
badgeText: {color: '#fff', fontSize: 11, fontWeight: '700'},
|
||||
info: {flex: 1, padding: 12, justifyContent: 'center', gap: 3},
|
||||
name: {fontSize: 15, fontWeight: '600'},
|
||||
meta: {fontSize: 13},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import {View, Text, TouchableOpacity, StyleSheet} from 'react-native';
|
||||
import {FilterMode} from '../types';
|
||||
import {useTheme} from '../theme';
|
||||
|
||||
const FILTERS: {label: string; value: FilterMode}[] = [
|
||||
{label: 'Original', value: 'original'},
|
||||
@@ -15,14 +16,23 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function FilterPicker({selected, onChange}: Props) {
|
||||
const t = useTheme();
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<View style={[styles.row, {backgroundColor: t.surface, borderTopColor: t.border}]}>
|
||||
{FILTERS.map(f => (
|
||||
<TouchableOpacity
|
||||
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)}>
|
||||
<Text style={[styles.label, selected === f.value && styles.labelActive]}>
|
||||
<Text style={[
|
||||
styles.label,
|
||||
{color: t.textSecondary},
|
||||
selected === f.value && {color: '#fff'},
|
||||
]}>
|
||||
{f.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -36,26 +46,14 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
paddingVertical: 10,
|
||||
backgroundColor: '#1c1c1e',
|
||||
paddingVertical: 12,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
chip: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 7,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#444',
|
||||
},
|
||||
chipActive: {
|
||||
backgroundColor: '#007AFF',
|
||||
borderColor: '#007AFF',
|
||||
},
|
||||
label: {
|
||||
color: '#aaa',
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
},
|
||||
labelActive: {
|
||||
color: '#fff',
|
||||
},
|
||||
label: {fontSize: 14, fontWeight: '500'},
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from 'react-native';
|
||||
import {ScannedDocument} from '../types';
|
||||
import DocumentCard from '../components/DocumentCard';
|
||||
import {useTheme} from '../theme';
|
||||
|
||||
interface Props {
|
||||
documents: ScannedDocument[];
|
||||
@@ -22,6 +23,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function HomeScreen({documents, onScan, onView, onDelete, onRename}: Props) {
|
||||
const t = useTheme();
|
||||
const [renameTarget, setRenameTarget] = useState<ScannedDocument | null>(null);
|
||||
const [renameText, setRenameText] = useState('');
|
||||
|
||||
@@ -45,18 +47,22 @@ export default function HomeScreen({documents, onScan, onView, onDelete, onRenam
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="#000" />
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Lens</Text>
|
||||
<Text style={styles.subtitle}>{documents.length} document{documents.length !== 1 ? 's' : ''}</Text>
|
||||
<View style={[styles.container, {backgroundColor: t.bg}]}>
|
||||
<StatusBar barStyle={t.statusBar} backgroundColor={t.bg} />
|
||||
<View style={[styles.header, {borderBottomColor: t.border}]}>
|
||||
<Text style={[styles.title, {color: t.text}]}>Lens</Text>
|
||||
<Text style={[styles.subtitle, {color: t.textSecondary}]}>
|
||||
{documents.length} document{documents.length !== 1 ? 's' : ''}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{documents.length === 0 ? (
|
||||
<View testID="empty-state" style={styles.empty}>
|
||||
<Text style={styles.emptyIcon}>📄</Text>
|
||||
<Text style={styles.emptyTitle}>No scans yet</Text>
|
||||
<Text style={styles.emptyHint}>Tap the button below to scan a document</Text>
|
||||
<Text style={[styles.emptyTitle, {color: t.text}]}>No scans yet</Text>
|
||||
<Text style={[styles.emptyHint, {color: t.textSecondary}]}>
|
||||
Tap the button below to scan a document
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
@@ -67,8 +73,6 @@ export default function HomeScreen({documents, onScan, onView, onDelete, onRenam
|
||||
<DocumentCard
|
||||
document={item}
|
||||
onPress={() => onView(item)}
|
||||
onDelete={() => confirmDelete(item)}
|
||||
onRename={() => startRename(item)}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={styles.list}
|
||||
@@ -82,23 +86,23 @@ export default function HomeScreen({documents, onScan, onView, onDelete, onRenam
|
||||
|
||||
<Modal visible={!!renameTarget} transparent animationType="fade">
|
||||
<View style={styles.modalBg}>
|
||||
<View testID="rename-modal" style={styles.modalBox}>
|
||||
<Text style={styles.modalTitle}>Rename Document</Text>
|
||||
<View testID="rename-modal" style={[styles.modalBox, {backgroundColor: t.surface}]}>
|
||||
<Text style={[styles.modalTitle, {color: t.text}]}>Rename Document</Text>
|
||||
<TextInput
|
||||
testID="rename-input"
|
||||
style={styles.modalInput}
|
||||
style={[styles.modalInput, {backgroundColor: t.bg, color: t.text, borderColor: t.border}]}
|
||||
value={renameText}
|
||||
onChangeText={setRenameText}
|
||||
autoFocus
|
||||
selectTextOnFocus
|
||||
placeholderTextColor="#666"
|
||||
placeholderTextColor={t.textSecondary}
|
||||
/>
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity onPress={() => setRenameTarget(null)} style={styles.modalBtn}>
|
||||
<Text style={styles.modalCancel}>Cancel</Text>
|
||||
<Text style={[styles.modalCancel, {color: t.textSecondary}]}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<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>
|
||||
</View>
|
||||
</View>
|
||||
@@ -109,21 +113,20 @@ export default function HomeScreen({documents, onScan, onView, onDelete, onRenam
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {flex: 1, backgroundColor: '#000'},
|
||||
container: {flex: 1},
|
||||
header: {
|
||||
paddingTop: 56,
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#222',
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
title: {color: '#fff', fontSize: 32, fontWeight: '700'},
|
||||
subtitle: {color: '#666', fontSize: 14, marginTop: 2},
|
||||
title: {fontSize: 32, fontWeight: '700'},
|
||||
subtitle: {fontSize: 15, marginTop: 2},
|
||||
list: {paddingTop: 8, paddingBottom: 100},
|
||||
empty: {flex: 1, alignItems: 'center', justifyContent: 'center', gap: 8},
|
||||
emptyIcon: {fontSize: 64},
|
||||
emptyTitle: {color: '#fff', fontSize: 20, fontWeight: '600'},
|
||||
emptyHint: {color: '#666', fontSize: 14},
|
||||
emptyTitle: {fontSize: 20, fontWeight: '600'},
|
||||
emptyHint: {fontSize: 15},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
bottom: 32,
|
||||
@@ -142,18 +145,17 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
fabIcon: {color: '#fff', fontSize: 22},
|
||||
fabLabel: {color: '#fff', fontSize: 17, fontWeight: '600'},
|
||||
modalBg: {flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'center', padding: 24},
|
||||
modalBox: {backgroundColor: '#1c1c1e', borderRadius: 16, padding: 20, gap: 16},
|
||||
modalTitle: {color: '#fff', fontSize: 17, fontWeight: '600'},
|
||||
modalBg: {flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', padding: 24},
|
||||
modalBox: {borderRadius: 16, padding: 20, gap: 16},
|
||||
modalTitle: {fontSize: 17, fontWeight: '600'},
|
||||
modalInput: {
|
||||
backgroundColor: '#2c2c2e',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 10,
|
||||
padding: 12,
|
||||
color: '#fff',
|
||||
fontSize: 15,
|
||||
},
|
||||
modalActions: {flexDirection: 'row', justifyContent: 'flex-end', gap: 16},
|
||||
modalBtn: {padding: 4},
|
||||
modalCancel: {color: '#888', fontSize: 15},
|
||||
modalConfirm: {color: '#007AFF', fontSize: 15, fontWeight: '600'},
|
||||
modalCancel: {fontSize: 15},
|
||||
modalConfirm: {fontSize: 15, fontWeight: '600'},
|
||||
});
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Dimensions,
|
||||
Alert,
|
||||
StatusBar,
|
||||
} from 'react-native';
|
||||
import {ScannedPage, FilterMode} from '../types';
|
||||
import FilterPicker from '../components/FilterPicker';
|
||||
import {useTheme} from '../theme';
|
||||
|
||||
const {width: SW} = Dimensions.get('window');
|
||||
|
||||
@@ -23,17 +23,10 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function ReviewScreen({pages, onSave, onAddMore, onCancel}: Props) {
|
||||
const t = useTheme();
|
||||
const [filter, setFilter] = useState<FilterMode>('original');
|
||||
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 = () => {
|
||||
switch (filter) {
|
||||
case 'grayscale': return styles.filterGrayscale;
|
||||
@@ -44,25 +37,21 @@ export default function ReviewScreen({pages, onSave, onAddMore, onCancel}: Props
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="#000" />
|
||||
<View style={[styles.container, {backgroundColor: t.bg}]}>
|
||||
<StatusBar barStyle={t.statusBar} backgroundColor={t.bg} />
|
||||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.header, {borderBottomColor: t.border}]}>
|
||||
<TouchableOpacity onPress={onCancel} style={styles.headerBtn}>
|
||||
<Text style={styles.headerBtnText}>Cancel</Text>
|
||||
<Text style={[styles.headerBtnText, {color: t.accent}]}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>
|
||||
<Text style={[styles.headerTitle, {color: t.text}]}>
|
||||
{currentIndex + 1} / {pages.length}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => onSave(pages, filter)}
|
||||
style={styles.saveBtn}>
|
||||
<TouchableOpacity onPress={() => onSave(pages, filter)} style={styles.saveBtn}>
|
||||
<Text style={styles.saveBtnText}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Page viewer */}
|
||||
<ScrollView
|
||||
horizontal
|
||||
pagingEnabled
|
||||
@@ -74,43 +63,34 @@ export default function ReviewScreen({pages, onSave, onAddMore, onCancel}: Props
|
||||
{pages.map(page => (
|
||||
<View key={page.id} style={styles.pageContainer}>
|
||||
<View style={[styles.imageWrapper, imageContainerStyle()]}>
|
||||
<Image
|
||||
source={{uri: page.uri}}
|
||||
style={styles.pageImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<Image source={{uri: page.uri}} style={styles.pageImage} resizeMode="contain" />
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* Dot indicators */}
|
||||
{pages.length > 1 && (
|
||||
<View style={styles.dots}>
|
||||
{pages.map((_, i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={[styles.dot, i === currentIndex && styles.dotActive]}
|
||||
style={[styles.dot, {backgroundColor: t.border}, i === currentIndex && styles.dotActive]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Filter picker */}
|
||||
<FilterPicker selected={filter} onChange={setFilter} />
|
||||
|
||||
{/* Bottom actions */}
|
||||
<View style={styles.bottomActions}>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={onAddMore}>
|
||||
<Text style={styles.actionIcon}>+</Text>
|
||||
<Text style={styles.actionLabel}>Add Page</Text>
|
||||
<View style={[styles.bottomActions, {borderTopColor: t.border}]}>
|
||||
<TouchableOpacity style={[styles.actionBtn, {borderColor: t.border}]} onPress={onAddMore}>
|
||||
<Text style={[styles.actionIcon, {color: t.accent}]}>+</Text>
|
||||
<Text style={[styles.actionLabel, {color: t.accent}]}>Add Page</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionBtn, styles.actionBtnPrimary]}
|
||||
onPress={() => onSave(pages, filter)}>
|
||||
<Text style={[styles.actionLabel, styles.actionLabelPrimary]}>
|
||||
Save Document
|
||||
</Text>
|
||||
<Text style={[styles.actionLabel, styles.actionLabelPrimary]}>Save Document</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -118,7 +98,7 @@ export default function ReviewScreen({pages, onSave, onAddMore, onCancel}: Props
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {flex: 1, backgroundColor: '#000'},
|
||||
container: {flex: 1},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -126,12 +106,11 @@ const styles = StyleSheet.create({
|
||||
paddingTop: 52,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#222',
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
headerBtn: {padding: 4},
|
||||
headerBtnText: {color: '#007AFF', fontSize: 16},
|
||||
headerTitle: {color: '#fff', fontSize: 16, fontWeight: '600'},
|
||||
headerBtnText: {fontSize: 16},
|
||||
headerTitle: {fontSize: 16, fontWeight: '600'},
|
||||
saveBtn: {
|
||||
backgroundColor: '#007AFF',
|
||||
paddingHorizontal: 16,
|
||||
@@ -146,34 +125,20 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
imageWrapper: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
imageWrapper: {flex: 1, width: '100%', borderRadius: 8, overflow: 'hidden'},
|
||||
pageImage: {flex: 1, width: '100%'},
|
||||
filterGrayscale: {opacity: 0.9},
|
||||
filterBW: {opacity: 1},
|
||||
filterEnhanced: {opacity: 1},
|
||||
dots: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: 6,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
dot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#444',
|
||||
},
|
||||
dots: {flexDirection: 'row', justifyContent: 'center', gap: 6, paddingVertical: 8},
|
||||
dot: {width: 6, height: 6, borderRadius: 3},
|
||||
dotActive: {backgroundColor: '#007AFF', width: 18},
|
||||
bottomActions: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
padding: 16,
|
||||
paddingBottom: 32,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
actionBtn: {
|
||||
flex: 1,
|
||||
@@ -184,14 +149,13 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#333',
|
||||
},
|
||||
actionBtnPrimary: {
|
||||
backgroundColor: '#007AFF',
|
||||
borderColor: '#007AFF',
|
||||
flex: 2,
|
||||
},
|
||||
actionIcon: {color: '#007AFF', fontSize: 20, fontWeight: '600'},
|
||||
actionLabel: {color: '#007AFF', fontSize: 15, fontWeight: '500'},
|
||||
actionIcon: {fontSize: 20, fontWeight: '600'},
|
||||
actionLabel: {fontSize: 15, fontWeight: '500'},
|
||||
actionLabelPrimary: {color: '#fff', fontWeight: '600'},
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import DocumentScanner, {ResponseType} from 'react-native-document-scanner-plugin';
|
||||
import RNFS from 'react-native-fs';
|
||||
import {ScannedPage} from '../types';
|
||||
import {useTheme} from '../theme';
|
||||
|
||||
interface Props {
|
||||
onComplete: (pages: ScannedPage[]) => void;
|
||||
@@ -31,9 +32,9 @@ async function copyToPermanent(tempUri: string): Promise<string> {
|
||||
}
|
||||
|
||||
export default function ScanScreen({onComplete, onCancel}: Props) {
|
||||
const t = useTheme();
|
||||
const [scanning, setScanning] = useState(false);
|
||||
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 scan = useCallback(async () => {
|
||||
@@ -58,7 +59,6 @@ export default function ScanScreen({onComplete, onCancel}: Props) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy temp files to permanent storage before the OS clears them
|
||||
const permanentUris = await Promise.all(scannedImages.map(copyToPermanent));
|
||||
|
||||
const newPages: ScannedPage[] = permanentUris.map(uri => ({
|
||||
@@ -92,25 +92,23 @@ export default function ScanScreen({onComplete, onCancel}: Props) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="#000" />
|
||||
<View style={[styles.container, {backgroundColor: t.bg}]}>
|
||||
<StatusBar barStyle={t.statusBar} backgroundColor={t.bg} />
|
||||
{scanning && (
|
||||
<View style={styles.center}>
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<Text style={styles.hint}>Opening camera…</Text>
|
||||
<ActivityIndicator size="large" color={t.accent} />
|
||||
<Text style={[styles.hint, {color: t.textSecondary}]}>Opening camera…</Text>
|
||||
</View>
|
||||
)}
|
||||
{!scanning && pageCount > 0 && (
|
||||
<View style={styles.center}>
|
||||
<Text style={styles.count}>{pageCount}</Text>
|
||||
<Text style={styles.pagesLabel}>pages ready</Text>
|
||||
<Text style={[styles.count, {color: t.text}]}>{pageCount}</Text>
|
||||
<Text style={[styles.pagesLabel, {color: t.textSecondary}]}>pages ready</Text>
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity style={styles.btnSecondary} onPress={scan}>
|
||||
<Text style={styles.btnSecondaryText}>Add More</Text>
|
||||
<TouchableOpacity style={[styles.btnSecondary, {borderColor: t.accent}]} onPress={scan}>
|
||||
<Text style={[styles.btnSecondaryText, {color: t.accent}]}>Add More</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.btnPrimary}
|
||||
onPress={() => onComplete(pagesRef.current)}>
|
||||
<TouchableOpacity style={styles.btnPrimary} onPress={() => onComplete(pagesRef.current)}>
|
||||
<Text style={styles.btnPrimaryText}>Finish</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -121,11 +119,11 @@ export default function ScanScreen({onComplete, onCancel}: Props) {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {flex: 1, backgroundColor: '#000'},
|
||||
container: {flex: 1},
|
||||
center: {flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12},
|
||||
hint: {color: '#888', fontSize: 15, marginTop: 12},
|
||||
count: {color: '#fff', fontSize: 72, fontWeight: '700'},
|
||||
pagesLabel: {color: '#888', fontSize: 18},
|
||||
hint: {fontSize: 15, marginTop: 12},
|
||||
count: {fontSize: 72, fontWeight: '700'},
|
||||
pagesLabel: {fontSize: 18},
|
||||
actions: {flexDirection: 'row', gap: 16, marginTop: 24},
|
||||
btnPrimary: {
|
||||
backgroundColor: '#007AFF',
|
||||
@@ -136,10 +134,9 @@ const styles = StyleSheet.create({
|
||||
btnPrimaryText: {color: '#fff', fontSize: 16, fontWeight: '600'},
|
||||
btnSecondary: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#007AFF',
|
||||
paddingHorizontal: 32,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 28,
|
||||
},
|
||||
btnSecondaryText: {color: '#007AFF', fontSize: 16, fontWeight: '600'},
|
||||
btnSecondaryText: {fontSize: 16, fontWeight: '600'},
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import Share from 'react-native-share';
|
||||
import {ScannedDocument} from '../types';
|
||||
import {generatePdf} from '../utils/generatePdf';
|
||||
import {useTheme} from '../theme';
|
||||
|
||||
const {width: SW} = Dimensions.get('window');
|
||||
|
||||
@@ -27,6 +28,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function ViewerScreen({document, onBack, onDelete, onRename}: Props) {
|
||||
const t = useTheme();
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [generating, setGenerating] = 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[]) => {
|
||||
setGenerating(true);
|
||||
try {
|
||||
const pages =
|
||||
pageIndices
|
||||
? pageIndices.map(i => document.pages[i]).filter(Boolean)
|
||||
: document.pages;
|
||||
|
||||
const label =
|
||||
pageIndices?.length === 1
|
||||
? `${document.name} — page ${pageIndices[0] + 1}`
|
||||
: document.name;
|
||||
|
||||
const pages = pageIndices
|
||||
? pageIndices.map(i => document.pages[i]).filter(Boolean)
|
||||
: document.pages;
|
||||
const label = pageIndices?.length === 1
|
||||
? `${document.name} — page ${pageIndices[0] + 1}`
|
||||
: document.name;
|
||||
const pdfPath = await generatePdf(pages, label);
|
||||
|
||||
await Share.open({
|
||||
title: label,
|
||||
type: 'application/pdf',
|
||||
url: pdfPath, // already normalised to file:// by generatePdf
|
||||
url: pdfPath,
|
||||
failOnCancel: false,
|
||||
});
|
||||
} catch (err: any) {
|
||||
@@ -68,16 +65,16 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="#000" />
|
||||
<View style={[styles.container, {backgroundColor: t.bg}]}>
|
||||
<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}>
|
||||
<Text style={styles.back}>‹ Back</Text>
|
||||
<Text style={[styles.back, {color: t.accent}]}>‹ Back</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={styles.title} numberOfLines={1}>{document.name}</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
<Text style={[styles.title, {color: t.text}]} numberOfLines={1}>{document.name}</Text>
|
||||
<Text style={[styles.subtitle, {color: t.textSecondary}]}>
|
||||
{currentIndex + 1} / {document.pages.length}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -86,7 +83,7 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
||||
onPress={() => sharePdf()}
|
||||
style={styles.headerBtn}
|
||||
disabled={generating}>
|
||||
<Text style={styles.shareText}>Share PDF</Text>
|
||||
<Text style={[styles.shareText, {color: t.accent}]}>Share PDF</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -105,7 +102,7 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
||||
style={styles.pageImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<Text style={styles.pageNum}>Page {i + 1}</Text>
|
||||
<Text style={[styles.pageNum, {color: t.textSecondary}]}>Page {i + 1}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
@@ -113,33 +110,32 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
||||
{document.pages.length > 1 && (
|
||||
<View style={styles.dots}>
|
||||
{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 style={styles.toolbar}>
|
||||
<TouchableOpacity
|
||||
style={styles.toolBtn}
|
||||
onPress={() => sharePdf([currentIndex])}
|
||||
disabled={generating}>
|
||||
<Text style={styles.toolIcon}>⬆</Text>
|
||||
<Text style={styles.toolLabel}>Share Page</Text>
|
||||
<View style={[styles.toolbar, {borderTopColor: t.border}]}>
|
||||
<TouchableOpacity style={styles.toolBtn} onPress={() => sharePdf([currentIndex])} disabled={generating}>
|
||||
<Text style={[styles.toolIcon, {color: t.accent}]}>⬆</Text>
|
||||
<Text style={[styles.toolLabel, {color: t.accent}]}>Share Page</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.toolBtn} onPress={() => sharePdf()} disabled={generating}>
|
||||
<Text style={[styles.toolIcon, {color: t.accent}]}>📄</Text>
|
||||
<Text style={[styles.toolLabel, {color: t.accent}]}>Share All</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.toolBtn}
|
||||
onPress={() => sharePdf()}
|
||||
disabled={generating}>
|
||||
<Text style={styles.toolIcon}>📄</Text>
|
||||
<Text style={styles.toolLabel}>Share All</Text>
|
||||
onPress={() => {setRenameText(document.name); setRenaming(true);}}>
|
||||
<Text style={[styles.toolIcon, {color: t.accent}]}>✎</Text>
|
||||
<Text style={[styles.toolLabel, {color: t.accent}]}>Rename</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.toolBtn} onPress={() => { setRenameText(document.name); setRenaming(true); }}>
|
||||
<Text style={styles.toolIcon}>✎</Text>
|
||||
<Text style={styles.toolLabel}>Rename</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 style={styles.toolBtn} onPress={confirmDelete}>
|
||||
<Text style={[styles.toolIcon, {color: t.danger}]}>🗑</Text>
|
||||
<Text style={[styles.toolLabel, {color: t.danger}]}>Delete</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -152,19 +148,19 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
||||
|
||||
<Modal visible={renaming} transparent animationType="fade">
|
||||
<View style={styles.modalBg}>
|
||||
<View style={styles.modalBox}>
|
||||
<Text style={styles.modalTitle}>Rename Document</Text>
|
||||
<View style={[styles.modalBox, {backgroundColor: t.surface}]}>
|
||||
<Text style={[styles.modalTitle, {color: t.text}]}>Rename Document</Text>
|
||||
<TextInput
|
||||
style={styles.modalInput}
|
||||
style={[styles.modalInput, {backgroundColor: t.bg, color: t.text, borderColor: t.border}]}
|
||||
value={renameText}
|
||||
onChangeText={setRenameText}
|
||||
autoFocus
|
||||
selectTextOnFocus
|
||||
placeholderTextColor="#666"
|
||||
placeholderTextColor={t.textSecondary}
|
||||
/>
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity onPress={() => setRenaming(false)} style={styles.modalBtn}>
|
||||
<Text style={styles.modalCancel}>Cancel</Text>
|
||||
<Text style={[styles.modalCancel, {color: t.textSecondary}]}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
@@ -172,7 +168,7 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
||||
setRenaming(false);
|
||||
}}
|
||||
style={styles.modalBtn}>
|
||||
<Text style={styles.modalConfirm}>Rename</Text>
|
||||
<Text style={[styles.modalConfirm, {color: t.accent}]}>Rename</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -183,22 +179,21 @@ export default function ViewerScreen({document, onBack, onDelete, onRename}: Pro
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {flex: 1, backgroundColor: '#000'},
|
||||
container: {flex: 1},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingTop: 52,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#222',
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
headerBtn: {minWidth: 60},
|
||||
headerCenter: {flex: 1, alignItems: 'center'},
|
||||
back: {color: '#007AFF', fontSize: 17},
|
||||
title: {color: '#fff', fontSize: 15, fontWeight: '600'},
|
||||
subtitle: {color: '#666', fontSize: 12, marginTop: 2},
|
||||
shareText: {color: '#007AFF', fontSize: 15, textAlign: 'right'},
|
||||
back: {fontSize: 17},
|
||||
title: {fontSize: 15, fontWeight: '600'},
|
||||
subtitle: {fontSize: 13, marginTop: 2},
|
||||
shareText: {fontSize: 15, textAlign: 'right'},
|
||||
pageContainer: {
|
||||
width: SW,
|
||||
flex: 1,
|
||||
@@ -207,32 +202,18 @@ const styles = StyleSheet.create({
|
||||
padding: 16,
|
||||
},
|
||||
pageImage: {flex: 1, width: '100%', borderRadius: 4},
|
||||
pageNum: {color: '#444', fontSize: 12, marginTop: 8},
|
||||
dots: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: 6,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
dot: {width: 6, height: 6, borderRadius: 3, backgroundColor: '#333'},
|
||||
pageNum: {fontSize: 13, marginTop: 8},
|
||||
dots: {flexDirection: 'row', justifyContent: 'center', gap: 6, paddingVertical: 8},
|
||||
dot: {width: 6, height: 6, borderRadius: 3},
|
||||
dotActive: {backgroundColor: '#007AFF', width: 18},
|
||||
toolbar: {
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#222',
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
toolBtn: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
paddingVertical: 14,
|
||||
gap: 4,
|
||||
},
|
||||
toolBtnDanger: {},
|
||||
toolIcon: {fontSize: 20, color: '#007AFF'},
|
||||
toolIconDanger: {color: '#ff453a'},
|
||||
toolLabel: {fontSize: 11, color: '#007AFF'},
|
||||
toolLabelDanger: {color: '#ff453a'},
|
||||
toolBtn: {flex: 1, alignItems: 'center', paddingVertical: 14, gap: 4},
|
||||
toolIcon: {fontSize: 20},
|
||||
toolLabel: {fontSize: 13},
|
||||
loadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.65)',
|
||||
@@ -241,18 +222,17 @@ const styles = StyleSheet.create({
|
||||
gap: 12,
|
||||
},
|
||||
loadingText: {color: '#fff', fontSize: 15},
|
||||
modalBg: {flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', justifyContent: 'center', padding: 24},
|
||||
modalBox: {backgroundColor: '#1c1c1e', borderRadius: 16, padding: 20, gap: 16},
|
||||
modalTitle: {color: '#fff', fontSize: 17, fontWeight: '600'},
|
||||
modalBg: {flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', padding: 24},
|
||||
modalBox: {borderRadius: 16, padding: 20, gap: 16},
|
||||
modalTitle: {fontSize: 17, fontWeight: '600'},
|
||||
modalInput: {
|
||||
backgroundColor: '#2c2c2e',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 10,
|
||||
padding: 12,
|
||||
color: '#fff',
|
||||
fontSize: 15,
|
||||
},
|
||||
modalActions: {flexDirection: 'row', justifyContent: 'flex-end', gap: 16},
|
||||
modalBtn: {padding: 4},
|
||||
modalCancel: {color: '#888', fontSize: 15},
|
||||
modalConfirm: {color: '#007AFF', fontSize: 15, fontWeight: '600'},
|
||||
modalCancel: {fontSize: 15},
|
||||
modalConfirm: {fontSize: 15, fontWeight: '600'},
|
||||
});
|
||||
|
||||
30
src/theme.ts
Normal file
30
src/theme.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user