fix(mobile): watch-folder button visibility + Karoo file picker crash
- useFocusEffect re-reads auto_import_path from DB every time the Import tab is focused, so the scan button appears immediately after saving in Settings (was broken by conditional hook call which violated Rules of Hooks and never re-fired on setting changes) - Nested inner try/catch isolates DocumentPicker.getDocumentAsync so ActivityNotFoundException (Karoo has no DocumentsUI) shows a friendly message instead of crashing the tab - Settings watch-directory placeholder updated to /sdcard/FitFiles
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import { useFocusEffect } from 'expo-router';
|
||||
import { useSQLiteContext } from 'expo-sqlite';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { AppState, PermissionsAndroid, Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { insertActivity, isSourcePathImported, getSetting, useSetting } from '@/db/queries';
|
||||
import { insertActivity, isSourcePathImported, getSetting } from '@/db/queries';
|
||||
import { PyodideWebView } from '@/extraction/PyodideWebView';
|
||||
import { extractFile } from '@/extraction/extractActivity';
|
||||
import { useTheme } from '@/ThemeContext';
|
||||
@@ -22,8 +23,19 @@ export default function ImportScreen() {
|
||||
const db = useSQLiteContext();
|
||||
const theme = useTheme();
|
||||
const [state, setState] = useState<ImportState>({ status: 'idle' });
|
||||
const [watchPath, setWatchPath] = useState('');
|
||||
const isImporting = useRef(false);
|
||||
const watchPath = Platform.OS === 'android' ? (useSetting('auto_import_path') ?? '') : '';
|
||||
|
||||
// Reload watch path every time the Import tab comes into focus so changes
|
||||
// saved in Settings are picked up without remounting the tab.
|
||||
useFocusEffect(useCallback(() => {
|
||||
if (Platform.OS !== 'android') return;
|
||||
const row = db.getFirstSync<{ value: string }>(
|
||||
'SELECT value FROM settings WHERE key = ?',
|
||||
['auto_import_path'],
|
||||
);
|
||||
setWatchPath(row?.value ?? '');
|
||||
}, [db]));
|
||||
|
||||
// Auto-scan watch folder on mount and when app comes to foreground.
|
||||
useEffect(() => {
|
||||
@@ -77,11 +89,26 @@ export default function ImportScreen() {
|
||||
if (isImporting.current) return;
|
||||
setState({ status: 'loading', msg: 'Picking files…', current: 0, total: 0 });
|
||||
try {
|
||||
const result = await DocumentPicker.getDocumentAsync({
|
||||
let result: DocumentPicker.DocumentPickerResult;
|
||||
try {
|
||||
result = await DocumentPicker.getDocumentAsync({
|
||||
type: ['*/*'],
|
||||
copyToCacheDirectory: true,
|
||||
multiple: true,
|
||||
});
|
||||
} catch (pickerErr: unknown) {
|
||||
// Some Android devices (e.g. Karoo) have no system file picker app.
|
||||
const raw = pickerErr instanceof Error ? pickerErr.message : String(pickerErr);
|
||||
const noApp = raw.includes('ActivityNotFoundException') || raw.includes('No Activity found');
|
||||
setState({
|
||||
status: 'error',
|
||||
message: noApp
|
||||
? 'No file picker available on this device. Set a Watch directory in Settings to import from a folder.'
|
||||
: raw,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.canceled || !result.assets?.length) {
|
||||
setState({ status: 'idle' });
|
||||
return;
|
||||
@@ -93,15 +120,8 @@ export default function ImportScreen() {
|
||||
isImporting.current = false;
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const raw = e instanceof Error ? e.message : String(e);
|
||||
// Karoo and other stripped-down Android devices have no DocumentsUI app
|
||||
const noPickerAvailable = raw.includes('ActivityNotFoundException') || raw.includes('No Activity found');
|
||||
setState({
|
||||
status: 'error',
|
||||
message: noPickerAvailable
|
||||
? 'No file picker available on this device. Set a Watch directory in Settings to import from a folder automatically.'
|
||||
: raw,
|
||||
});
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
setState({ status: 'error', message: msg });
|
||||
isImporting.current = false;
|
||||
}
|
||||
}
|
||||
@@ -240,7 +260,7 @@ export default function ImportScreen() {
|
||||
<Text style={styles.watchLabel}>Watch folder</Text>
|
||||
<Text style={styles.watchPath} numberOfLines={2}>{watchPath}</Text>
|
||||
<Pressable
|
||||
style={[styles.button, styles.buttonWatch, state.status === 'loading' && styles.buttonDisabled]}
|
||||
style={[styles.scanButton, state.status === 'loading' && styles.buttonDisabled]}
|
||||
onPress={state.status !== 'loading' ? manualScan : undefined}
|
||||
>
|
||||
<Text style={styles.buttonText}>
|
||||
@@ -317,7 +337,7 @@ export default function ImportScreen() {
|
||||
<Text style={styles.noticeText}>
|
||||
FIT/GPX/TCX extraction runs entirely on your device.{'\n'}
|
||||
A Bincio instance must be reachable on first run to download the extraction engine (~35 MB, then cached).{'\n\n'}
|
||||
On Karoo or Android: set a Watch directory in Settings to auto-import new FIT files when the app opens.
|
||||
On Karoo: set Watch directory to <Text style={styles.noticeCode}>/sdcard/FitFiles</Text> in Settings to auto-import rides.
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
@@ -433,15 +453,18 @@ const styles = StyleSheet.create({
|
||||
code: { color: '#60a5fa', fontFamily: 'monospace' },
|
||||
watchBox: {
|
||||
backgroundColor: '#18181b', borderRadius: 10, borderWidth: 1,
|
||||
borderColor: '#27272a', padding: 14, marginBottom: 16, gap: 8,
|
||||
borderColor: '#27272a', padding: 14, marginBottom: 16, gap: 10,
|
||||
},
|
||||
watchLabel: { color: '#71717a', fontSize: 11, fontWeight: '600', letterSpacing: 0.5 },
|
||||
watchPath: { color: '#a1a1aa', fontSize: 13, fontFamily: 'monospace' },
|
||||
scanButton: {
|
||||
backgroundColor: '#16a34a', borderRadius: 10,
|
||||
paddingVertical: 14, alignItems: 'center',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#2563eb', borderRadius: 10,
|
||||
paddingVertical: 14, alignItems: 'center', marginBottom: 16,
|
||||
},
|
||||
buttonWatch: { backgroundColor: '#16a34a', marginBottom: 0 },
|
||||
buttonDisabled: { opacity: 0.5 },
|
||||
buttonText: { color: '#fff', fontWeight: '600', fontSize: 16 },
|
||||
statusBox: {
|
||||
@@ -472,4 +495,5 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 8, padding: 12, borderWidth: 1, borderColor: '#27272a',
|
||||
},
|
||||
noticeText: { color: '#71717a', fontSize: 12, lineHeight: 18 },
|
||||
noticeCode: { fontFamily: 'monospace', color: '#a1a1aa' },
|
||||
});
|
||||
|
||||
@@ -170,7 +170,7 @@ export default function SettingsScreen() {
|
||||
<Section title="Auto-import (Android)">
|
||||
<Field
|
||||
label="Watch directory"
|
||||
placeholder="/sdcard/Karoo/Rides"
|
||||
placeholder="/sdcard/FitFiles"
|
||||
value={autoPath}
|
||||
onChangeText={setAutoPath}
|
||||
autoCapitalize="none"
|
||||
|
||||
Reference in New Issue
Block a user