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.
|
* 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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
# 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.**
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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'},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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'},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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'},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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'},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
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