Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "SimpleMediaBrowser",
"owner": "kallb123",
"slug": "SimpleMediaBrowser",
"version": "1.9.0",
"version": "1.10.0",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "simplemediabrowser",
Expand All @@ -20,7 +20,7 @@
},
"predictiveBackGestureEnabled": false,
"package": "net.nawt.simplemediabrowser",
"versionCode": 42
"versionCode": 43
},
"web": {
"output": "static",
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

123 changes: 122 additions & 1 deletion src/app/(drawer)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import { ThemedTextInput } from '@/components/ThemedTextInput';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import React from 'react';

Check warning on line 5 in src/app/(drawer)/settings.tsx

View workflow job for this annotation

GitHub Actions / Pre-build checks

'/home/runner/work/SimpleMediaBrowser/SimpleMediaBrowser/node_modules/react/index.js' imported multiple times
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';

Check warning on line 6 in src/app/(drawer)/settings.tsx

View workflow job for this annotation

GitHub Actions / Pre-build checks

'/home/runner/work/SimpleMediaBrowser/SimpleMediaBrowser/node_modules/react/index.js' imported multiple times
import { router } from 'expo-router';
import { useNavigation } from '@react-navigation/native';
import { useDispatch, useSelector } from 'react-redux';
Expand All @@ -18,6 +18,8 @@
import Constants from 'expo-constants';
import { useEditMode } from '@/contexts/EditModeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { exportJson, importJson, exportToFilesystem } from '@/scripts/ImportExportService';
import type { JsonExportOptions } from '@/scripts/ImportExportService';

const DIVIDER_COLOR = 'rgba(128,128,128,0.35)';
const DESTRUCTIVE_COLOR = '#E55';
Expand Down Expand Up @@ -84,6 +86,10 @@
const [scanning, setScanning] = useState(false);
const [scanComplete, setScanComplete] = useState(false);
const [troubleshootingExpanded, setTroubleshootingExpanded] = useState(false);
const [importExportExpanded, setImportExportExpanded] = useState(false);
const [exportIncludeSettings, setExportIncludeSettings] = useState(true);
const [exportIncludeOverrides, setExportIncludeOverrides] = useState(true);
const [exportIncludeMatches, setExportIncludeMatches] = useState(true);

const dispatch = useDispatch();
const settingsPassword = useSelector(selectPassword);
Expand Down Expand Up @@ -236,9 +242,9 @@
} catch (e) {
logger.error('Settings', 'Exception while syncing local settings state', e as Error);
}
}, [settingsPassword, settingsTmdbApiKey, settingsTvdbApiKey, settingsTvdbPin, settingsDataSource, settingsMediaStructure, settingsViewOrientation, settingsViewScale, settingsDefaultPage, settingsEnablePosterFetching, settingsEnableThumbnailGeneration, settingsRescanOnStartup, settingsFetchEpisodeNames, settingsFetchEpisodeThumbnails, settingsAppColorScheme]);

Check warning on line 245 in src/app/(drawer)/settings.tsx

View workflow job for this annotation

GitHub Actions / Pre-build checks

React Hook useEffect has missing dependencies: 'appColorSchemeOptions', 'dataSourceOptions', 'defaultPageOptions', 'uiTypeOptions', and 'viewTypeOptions'. Either include them or remove the dependency array

const save = () => {

Check warning on line 247 in src/app/(drawer)/settings.tsx

View workflow job for this annotation

GitHub Actions / Pre-build checks

The 'save' function makes the dependencies of useEffect Hook (at line 336) change on every render. To fix this, wrap the definition of 'save' in its own useCallback() Hook
try {
logger.log('Settings', 'Save pressed – evaluating changes');
let changeCount = 0;
Expand Down Expand Up @@ -500,7 +506,61 @@
],
);
}, [dispatch]);


const handleExportJson = useCallback(async () => {
try {
const options: JsonExportOptions = {
includeSettings: exportIncludeSettings,
includeOverrides: exportIncludeOverrides,
includeMatches: exportIncludeMatches,
};
if (!options.includeSettings && !options.includeOverrides && !options.includeMatches) {
Alert.alert('Nothing to export', 'Please select at least one section to include in the export.');
return;
}
await exportJson(options);
Alert.alert('Export complete', 'Settings and metadata have been saved to the selected folder.');
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
if (!msg.includes('denied') && !msg.includes('selected')) {
Alert.alert('Export failed', msg);
}
logger.warn('Settings', 'JSON export failed', e);
}
}, [exportIncludeSettings, exportIncludeOverrides, exportIncludeMatches]);

const handleImportJson = useCallback(async () => {
try {
const { applied } = await importJson();
Alert.alert('Import complete', `Applied: ${applied.join(', ')}.`);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
if (!msg.includes('No file') && !msg.includes('cancel')) {
Alert.alert('Import failed', msg);
}
logger.warn('Settings', 'JSON import failed', e);
}
}, []);

const handleExportToFilesystem = useCallback(async () => {
try {
const { showsExported, moviesExported, failed, skipped } = await exportToFilesystem(safeMediaSources);
const parts: string[] = [];
if (showsExported > 0) parts.push(`${showsExported} show(s)`);
if (moviesExported > 0) parts.push(`${moviesExported} movie(s)`);
const summary = parts.length > 0 ? parts.join(' and ') : 'nothing';
const extra: string[] = [];
if (skipped > 0) extra.push(`${skipped} skipped (no data or no subfolder)`);
if (failed > 0) extra.push(`${failed} failed`);
const detail = extra.length > 0 ? `\n\n${extra.join(', ')}.` : '';
Alert.alert('Export complete', `Exported metadata alongside ${summary}.${detail}`);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
Alert.alert('Export failed', msg);
logger.warn('Settings', 'Filesystem export failed', e);
}
}, [safeMediaSources]);

return (
<SettingsErrorBoundary>
<ScrollView style={containerStyle} contentContainerStyle={[styles.content, { paddingBottom: 20 + insets.bottom }]} keyboardShouldPersistTaps="handled">
Expand Down Expand Up @@ -871,6 +931,50 @@
</View>
</View>

{/* Import / Export */}
<View style={styles.section}>
<TouchableOpacity
style={styles.troubleshootingHeader}
onPress={() => setImportExportExpanded(prev => !prev)}
accessibilityRole="button"
accessibilityState={{ expanded: importExportExpanded }}
>
<ThemedText type="subtitle" style={styles.sectionTitle}>Import / Export</ThemedText>
<ThemedText style={styles.troubleshootingChevron}>{importExportExpanded ? '▲' : '▼'}</ThemedText>
</TouchableOpacity>
{importExportExpanded && (
<View style={styles.troubleshootingContent}>
{/* JSON export options */}
<ThemedText style={styles.importExportLabel}>Export to JSON file: select what to include:</ThemedText>
<View style={styles.row}>
<Switch value={exportIncludeSettings} onValueChange={setExportIncludeSettings} />
<ThemedText style={styles.rowLabel}>Settings (API keys, view options)</ThemedText>
</View>
<View style={styles.row}>
<Switch value={exportIncludeOverrides} onValueChange={setExportIncludeOverrides} />
<ThemedText style={styles.rowLabel}>Overrides (custom titles, sort titles)</ThemedText>
</View>
<View style={styles.row}>
<Switch value={exportIncludeMatches} onValueChange={setExportIncludeMatches} />
<ThemedText style={styles.rowLabel}>Matches (TMDB/TVDB IDs, episode names)</ThemedText>
</View>
<TouchableOpacity style={styles.importExportButton} onPress={handleExportJson}>
<ThemedText style={styles.importExportButtonText}>📤 Export app state to JSON</ThemedText>
<ThemedText style={styles.troubleshootingButtonDesc}>Saves selected data to a JSON file in a folder you choose.</ThemedText>
</TouchableOpacity>
<TouchableOpacity style={styles.importExportButton} onPress={handleImportJson}>
<ThemedText style={styles.importExportButtonText}>📥 Import app state from JSON</ThemedText>
<ThemedText style={styles.troubleshootingButtonDesc}>Restores settings, overrides and/or matches from a previously exported JSON file.</ThemedText>
</TouchableOpacity>
{/* Filesystem export */}
<TouchableOpacity style={styles.importExportButton} onPress={handleExportToFilesystem}>
<ThemedText style={styles.importExportButtonText}>💾 Export metadata to media folders</ThemedText>
<ThemedText style={styles.troubleshootingButtonDesc}>Writes smb.json, poster.jpg and episode thumbnails alongside your media files. These are picked up automatically during the next library scan.</ThemedText>
</TouchableOpacity>
</View>
)}
</View>

{/* Troubleshooting */}
<View style={styles.section}>
<TouchableOpacity
Expand Down Expand Up @@ -1086,4 +1190,21 @@
opacity: 0.55,
fontStyle: 'italic',
},
importExportButton: {
gap: 2,
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 8,
borderWidth: StyleSheet.hairlineWidth,
borderColor: DIVIDER_COLOR,
},
importExportButtonText: {
fontSize: 15,
fontWeight: '500',
},
importExportLabel: {
fontSize: 13,
opacity: 0.7,
fontStyle: 'italic',
},
});
Loading
Loading