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.
*/
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"
}
}

View File

@@ -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.**

View File

@@ -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},
});

View File

@@ -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'},
});

View File

@@ -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'},
});

View File

@@ -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'},
});

View File

@@ -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'},
});

View File

@@ -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
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;
}