mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 06:51:56 +00:00
revamped alert UI
This commit is contained in:
parent
d876b7618c
commit
59cb902658
3 changed files with 328 additions and 308 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -85,4 +85,5 @@ node_modules
|
||||||
expofs.md
|
expofs.md
|
||||||
ios/sentry.properties
|
ios/sentry.properties
|
||||||
android/sentry.properties
|
android/sentry.properties
|
||||||
Stremio addons refer
|
Stremio addons refer
|
||||||
|
trakt-docs
|
||||||
|
|
@ -15,7 +15,7 @@ import Animated, {
|
||||||
withTiming,
|
withTiming,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { Portal, Dialog, Button } from 'react-native-paper';
|
import { Portal } from 'react-native-paper';
|
||||||
|
|
||||||
interface CustomAlertProps {
|
interface CustomAlertProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
|
@ -40,8 +40,8 @@ export const CustomAlert = ({
|
||||||
}: CustomAlertProps) => {
|
}: CustomAlertProps) => {
|
||||||
const opacity = useSharedValue(0);
|
const opacity = useSharedValue(0);
|
||||||
const scale = useSharedValue(0.95);
|
const scale = useSharedValue(0.95);
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
// Using hardcoded dark theme values to match SeriesContent modal
|
||||||
const themeColors = currentTheme.colors;
|
const themeColors = currentTheme.colors;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -68,10 +68,11 @@ export const CustomAlert = ({
|
||||||
const handleActionPress = useCallback((action: { label: string; onPress: () => void; style?: object }) => {
|
const handleActionPress = useCallback((action: { label: string; onPress: () => void; style?: object }) => {
|
||||||
try {
|
try {
|
||||||
action.onPress();
|
action.onPress();
|
||||||
|
// Don't auto-close here if the action handles it, or check if we should
|
||||||
|
// Standard behavior is to close
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[CustomAlert] Error in action handler:', error);
|
console.warn('[CustomAlert] Error in action handler:', error);
|
||||||
// Still close the alert even if action fails
|
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
@ -91,7 +92,7 @@ export const CustomAlert = ({
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.overlay,
|
styles.overlay,
|
||||||
{ backgroundColor: 'rgba(0,0,0,0.6)' },
|
{ backgroundColor: 'rgba(0, 0, 0, 0.85)' },
|
||||||
overlayStyle
|
overlayStyle
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|
@ -100,23 +101,22 @@ export const CustomAlert = ({
|
||||||
<Animated.View style={[
|
<Animated.View style={[
|
||||||
styles.alertContainer,
|
styles.alertContainer,
|
||||||
alertStyle,
|
alertStyle,
|
||||||
{
|
|
||||||
backgroundColor: themeColors.darkBackground,
|
|
||||||
borderColor: themeColors.primary,
|
|
||||||
}
|
|
||||||
]}>
|
]}>
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<Text style={[styles.title, { color: themeColors.highEmphasis }]}>
|
<Text style={styles.title}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Message */}
|
{/* Message */}
|
||||||
<Text style={[styles.message, { color: themeColors.mediumEmphasis }]}>
|
<Text style={styles.message}>
|
||||||
{message}
|
{message}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<View style={styles.actionsRow}>
|
<View style={[
|
||||||
|
styles.actionsRow,
|
||||||
|
actions.length === 1 && { justifyContent: 'center' }
|
||||||
|
]}>
|
||||||
{actions.map((action, idx) => {
|
{actions.map((action, idx) => {
|
||||||
const isPrimary = idx === actions.length - 1;
|
const isPrimary = idx === actions.length - 1;
|
||||||
return (
|
return (
|
||||||
|
|
@ -125,9 +125,10 @@ export const CustomAlert = ({
|
||||||
style={[
|
style={[
|
||||||
styles.actionButton,
|
styles.actionButton,
|
||||||
isPrimary
|
isPrimary
|
||||||
? { ...styles.primaryButton, backgroundColor: themeColors.primary }
|
? { backgroundColor: themeColors.primary }
|
||||||
: styles.secondaryButton,
|
: styles.secondaryButton,
|
||||||
action.style
|
action.style,
|
||||||
|
actions.length === 1 && { minWidth: 120, maxWidth: '100%' }
|
||||||
]}
|
]}
|
||||||
onPress={() => handleActionPress(action)}
|
onPress={() => handleActionPress(action)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -135,8 +136,8 @@ export const CustomAlert = ({
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.actionText,
|
styles.actionText,
|
||||||
isPrimary
|
isPrimary
|
||||||
? { color: themeColors.white }
|
? { color: '#FFFFFF' }
|
||||||
: { color: themeColors.primary }
|
: { color: '#FFFFFF' }
|
||||||
]}>
|
]}>
|
||||||
{action.label}
|
{action.label}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -157,6 +158,7 @@ const styles = StyleSheet.create({
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
},
|
},
|
||||||
overlayPressable: {
|
overlayPressable: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
|
|
@ -165,29 +167,32 @@ const styles = StyleSheet.create({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 24,
|
paddingHorizontal: 20,
|
||||||
|
width: '100%',
|
||||||
},
|
},
|
||||||
alertContainer: {
|
alertContainer: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: 340,
|
maxWidth: 400,
|
||||||
borderRadius: 24,
|
backgroundColor: '#1E1E1E', // Solid opaque dark background
|
||||||
padding: 28,
|
borderRadius: 16,
|
||||||
|
padding: 24,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#007AFF', // iOS blue - will be overridden by theme
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
overflow: 'hidden', // Ensure background fills entire card
|
overflow: 'hidden',
|
||||||
...Platform.select({
|
...Platform.select({
|
||||||
ios: {
|
ios: {
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 8 },
|
shadowOffset: { width: 0, height: 10 },
|
||||||
shadowOpacity: 0.3,
|
shadowOpacity: 0.51,
|
||||||
shadowRadius: 24,
|
shadowRadius: 13.16,
|
||||||
},
|
},
|
||||||
android: {
|
android: {
|
||||||
elevation: 12,
|
elevation: 20,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
|
color: '#FFFFFF',
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
|
|
@ -195,6 +200,7 @@ const styles = StyleSheet.create({
|
||||||
letterSpacing: 0.2,
|
letterSpacing: 0.2,
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
|
color: '#AAAAAA',
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
|
@ -209,17 +215,16 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
actionButton: {
|
actionButton: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingVertical: 11,
|
paddingVertical: 12,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
flex: 1, // Distribute space
|
||||||
primaryButton: {
|
maxWidth: 200, // But limit width
|
||||||
// Background color set dynamically via theme
|
|
||||||
},
|
},
|
||||||
secondaryButton: {
|
secondaryButton: {
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
},
|
},
|
||||||
actionText: {
|
actionText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|
|
||||||
|
|
@ -78,17 +78,17 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: colors.white,
|
color: colors.white,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
sectionHeader: {
|
sectionHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
sectionDescription: {
|
sectionDescription: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: colors.mediumGray,
|
color: colors.mediumGray,
|
||||||
|
|
@ -283,59 +283,59 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
},
|
},
|
||||||
infoText: {
|
infoText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: colors.mediumEmphasis,
|
color: colors.mediumEmphasis,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 32,
|
paddingVertical: 32,
|
||||||
},
|
},
|
||||||
emptyStateTitle: {
|
emptyStateTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: colors.white,
|
color: colors.white,
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
emptyStateDescription: {
|
emptyStateDescription: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: colors.mediumGray,
|
color: colors.mediumGray,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
scrapersList: {
|
scrapersList: {
|
||||||
gap: 12,
|
gap: 12,
|
||||||
},
|
},
|
||||||
scrapersContainer: {
|
scrapersContainer: {
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
inputContainer: {
|
inputContainer: {
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
lastSection: {
|
lastSection: {
|
||||||
borderBottomWidth: 0,
|
borderBottomWidth: 0,
|
||||||
},
|
},
|
||||||
disabledSection: {
|
disabledSection: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
disabledText: {
|
disabledText: {
|
||||||
color: colors.elevation3,
|
color: colors.elevation3,
|
||||||
},
|
},
|
||||||
disabledContainer: {
|
disabledContainer: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
disabledInput: {
|
disabledInput: {
|
||||||
backgroundColor: colors.elevation1,
|
backgroundColor: colors.elevation1,
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
disabledButton: {
|
disabledButton: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
disabledImage: {
|
disabledImage: {
|
||||||
opacity: 0.3,
|
opacity: 0.3,
|
||||||
},
|
},
|
||||||
availableIndicator: {
|
availableIndicator: {
|
||||||
|
|
@ -484,46 +484,60 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
},
|
},
|
||||||
modalOverlay: {
|
modalOverlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
backgroundColor: 'rgba(0, 0, 0, 0.85)',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
padding: 20,
|
||||||
},
|
},
|
||||||
modalContent: {
|
modalContent: {
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: '#1E1E1E', // Match CustomAlert
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
padding: 20,
|
padding: 24,
|
||||||
margin: 20,
|
width: '100%',
|
||||||
maxHeight: '70%',
|
maxWidth: 400,
|
||||||
width: screenWidth - 40,
|
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.elevation3,
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
alignSelf: 'center',
|
||||||
|
...Platform.select({
|
||||||
|
ios: {
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 10 },
|
||||||
|
shadowOpacity: 0.51,
|
||||||
|
shadowRadius: 13.16,
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
elevation: 20,
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
modalTitle: {
|
modalTitle: {
|
||||||
fontSize: 18,
|
fontSize: 20,
|
||||||
fontWeight: '600',
|
fontWeight: '700',
|
||||||
color: colors.white,
|
color: '#FFFFFF',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
modalText: {
|
modalText: {
|
||||||
fontSize: 16,
|
fontSize: 15,
|
||||||
color: colors.mediumGray,
|
color: '#AAAAAA',
|
||||||
lineHeight: 24,
|
lineHeight: 22,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
modalButton: {
|
modalButton: {
|
||||||
backgroundColor: colors.primary,
|
backgroundColor: colors.primary,
|
||||||
paddingVertical: 14,
|
paddingVertical: 12,
|
||||||
paddingHorizontal: 24,
|
paddingHorizontal: 20,
|
||||||
borderRadius: 8,
|
borderRadius: 12,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
minHeight: 48,
|
minHeight: 48,
|
||||||
},
|
},
|
||||||
modalButtonText: {
|
modalButtonText: {
|
||||||
color: colors.white,
|
color: '#FFFFFF',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '500',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
// Compact modal styles
|
// Compact modal styles
|
||||||
modalHeader: {
|
modalHeader: {
|
||||||
|
|
@ -761,10 +775,10 @@ const CollapsibleSection: React.FC<{
|
||||||
<View style={styles.collapsibleSection}>
|
<View style={styles.collapsibleSection}>
|
||||||
<TouchableOpacity style={styles.collapsibleHeader} onPress={onToggle}>
|
<TouchableOpacity style={styles.collapsibleHeader} onPress={onToggle}>
|
||||||
<Text style={styles.collapsibleTitle}>{title}</Text>
|
<Text style={styles.collapsibleTitle}>{title}</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={isExpanded ? "chevron-up" : "chevron-down"}
|
name={isExpanded ? "chevron-up" : "chevron-down"}
|
||||||
size={20}
|
size={20}
|
||||||
color={colors.mediumGray}
|
color={colors.mediumGray}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{isExpanded && <View style={styles.collapsibleContent}>{children}</View>}
|
{isExpanded && <View style={styles.collapsibleContent}>{children}</View>}
|
||||||
|
|
@ -803,7 +817,7 @@ const StatusBadge: React.FC<{
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = getStatusConfig();
|
const config = getStatusConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{
|
<View style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
@ -828,7 +842,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const colors = currentTheme.colors;
|
const colors = currentTheme.colors;
|
||||||
const styles = createStyles(colors);
|
const styles = createStyles(colors);
|
||||||
|
|
||||||
// CustomAlert state
|
// CustomAlert state
|
||||||
const [alertVisible, setAlertVisible] = useState(false);
|
const [alertVisible, setAlertVisible] = useState(false);
|
||||||
const [alertTitle, setAlertTitle] = useState('');
|
const [alertTitle, setAlertTitle] = useState('');
|
||||||
|
|
@ -842,7 +856,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
) => {
|
) => {
|
||||||
setAlertTitle(title);
|
setAlertTitle(title);
|
||||||
setAlertMessage(message);
|
setAlertMessage(message);
|
||||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]);
|
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
|
||||||
setAlertVisible(true);
|
setAlertVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -856,14 +870,14 @@ const PluginsScreen: React.FC = () => {
|
||||||
const [showboxSavedToken, setShowboxSavedToken] = useState<string>('');
|
const [showboxSavedToken, setShowboxSavedToken] = useState<string>('');
|
||||||
const [showboxScraperId, setShowboxScraperId] = useState<string | null>(null);
|
const [showboxScraperId, setShowboxScraperId] = useState<string | null>(null);
|
||||||
const [showboxTokenVisible, setShowboxTokenVisible] = useState<boolean>(false);
|
const [showboxTokenVisible, setShowboxTokenVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
// Multiple repositories state
|
// Multiple repositories state
|
||||||
const [repositories, setRepositories] = useState<RepositoryInfo[]>([]);
|
const [repositories, setRepositories] = useState<RepositoryInfo[]>([]);
|
||||||
const [currentRepositoryId, setCurrentRepositoryId] = useState<string>('');
|
const [currentRepositoryId, setCurrentRepositoryId] = useState<string>('');
|
||||||
const [showAddRepositoryModal, setShowAddRepositoryModal] = useState(false);
|
const [showAddRepositoryModal, setShowAddRepositoryModal] = useState(false);
|
||||||
const [newRepositoryUrl, setNewRepositoryUrl] = useState('');
|
const [newRepositoryUrl, setNewRepositoryUrl] = useState('');
|
||||||
const [switchingRepository, setSwitchingRepository] = useState<string | null>(null);
|
const [switchingRepository, setSwitchingRepository] = useState<string | null>(null);
|
||||||
|
|
||||||
// New UX state
|
// New UX state
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedFilter, setSelectedFilter] = useState<'all' | 'movie' | 'tv'>('all');
|
const [selectedFilter, setSelectedFilter] = useState<'all' | 'movie' | 'tv'>('all');
|
||||||
|
|
@ -897,7 +911,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
// Filter by search query
|
// Filter by search query
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) {
|
||||||
const query = searchQuery.toLowerCase();
|
const query = searchQuery.toLowerCase();
|
||||||
filtered = filtered.filter(scraper =>
|
filtered = filtered.filter(scraper =>
|
||||||
scraper.name.toLowerCase().includes(query) ||
|
scraper.name.toLowerCase().includes(query) ||
|
||||||
scraper.description.toLowerCase().includes(query) ||
|
scraper.description.toLowerCase().includes(query) ||
|
||||||
scraper.id.toLowerCase().includes(query)
|
scraper.id.toLowerCase().includes(query)
|
||||||
|
|
@ -906,7 +920,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
|
|
||||||
// Filter by type
|
// Filter by type
|
||||||
if (selectedFilter !== 'all') {
|
if (selectedFilter !== 'all') {
|
||||||
filtered = filtered.filter(scraper =>
|
filtered = filtered.filter(scraper =>
|
||||||
scraper.supportedTypes?.includes(selectedFilter as 'movie' | 'tv')
|
scraper.supportedTypes?.includes(selectedFilter as 'movie' | 'tv')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -933,7 +947,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
const handleBulkToggle = async (enabled: boolean) => {
|
const handleBulkToggle = async (enabled: boolean) => {
|
||||||
try {
|
try {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
const promises = filteredScrapers.map(scraper =>
|
const promises = filteredScrapers.map(scraper =>
|
||||||
pluginService.setScraperEnabled(scraper.id, enabled)
|
pluginService.setScraperEnabled(scraper.id, enabled)
|
||||||
);
|
);
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
@ -994,14 +1008,14 @@ const PluginsScreen: React.FC = () => {
|
||||||
description: '',
|
description: '',
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
|
|
||||||
await loadRepositories();
|
await loadRepositories();
|
||||||
|
|
||||||
// Switch to the new repository and refresh it
|
// Switch to the new repository and refresh it
|
||||||
await pluginService.setCurrentRepository(repoId);
|
await pluginService.setCurrentRepository(repoId);
|
||||||
await loadRepositories();
|
await loadRepositories();
|
||||||
await loadScrapers();
|
await loadScrapers();
|
||||||
|
|
||||||
setNewRepositoryUrl('');
|
setNewRepositoryUrl('');
|
||||||
setShowAddRepositoryModal(false);
|
setShowAddRepositoryModal(false);
|
||||||
openAlert('Success', 'Repository added and refreshed successfully');
|
openAlert('Success', 'Repository added and refreshed successfully');
|
||||||
|
|
@ -1034,9 +1048,9 @@ const PluginsScreen: React.FC = () => {
|
||||||
|
|
||||||
// Special handling for the last repository
|
// Special handling for the last repository
|
||||||
const isLastRepository = repositories.length === 1;
|
const isLastRepository = repositories.length === 1;
|
||||||
|
|
||||||
const alertTitle = isLastRepository ? 'Remove Last Repository' : 'Remove Repository';
|
const alertTitle = isLastRepository ? 'Remove Last Repository' : 'Remove Repository';
|
||||||
const alertMessage = isLastRepository
|
const alertMessage = isLastRepository
|
||||||
? `Are you sure you want to remove "${repo.name}"? This is your only repository, so you'll have no scrapers available until you add a new repository.`
|
? `Are you sure you want to remove "${repo.name}"? This is your only repository, so you'll have no scrapers available until you add a new repository.`
|
||||||
: `Are you sure you want to remove "${repo.name}"? This will also remove all scrapers from this repository.`;
|
: `Are you sure you want to remove "${repo.name}"? This will also remove all scrapers from this repository.`;
|
||||||
|
|
||||||
|
|
@ -1044,7 +1058,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
alertTitle,
|
alertTitle,
|
||||||
alertMessage,
|
alertMessage,
|
||||||
[
|
[
|
||||||
{ label: 'Cancel', onPress: () => {} },
|
{ label: 'Cancel', onPress: () => { } },
|
||||||
{
|
{
|
||||||
label: 'Remove',
|
label: 'Remove',
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
|
|
@ -1052,7 +1066,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
await pluginService.removeRepository(repoId);
|
await pluginService.removeRepository(repoId);
|
||||||
await loadRepositories();
|
await loadRepositories();
|
||||||
await loadScrapers();
|
await loadScrapers();
|
||||||
const successMessage = isLastRepository
|
const successMessage = isLastRepository
|
||||||
? 'Repository removed successfully. You can add a new repository using the "Add Repository" button.'
|
? 'Repository removed successfully. You can add a new repository using the "Add Repository" button.'
|
||||||
: 'Repository removed successfully';
|
: 'Repository removed successfully';
|
||||||
openAlert('Success', successMessage);
|
openAlert('Success', successMessage);
|
||||||
|
|
@ -1105,14 +1119,14 @@ const PluginsScreen: React.FC = () => {
|
||||||
try {
|
try {
|
||||||
// First refresh repository names from manifests for existing repositories
|
// First refresh repository names from manifests for existing repositories
|
||||||
await pluginService.refreshRepositoryNamesFromManifests();
|
await pluginService.refreshRepositoryNamesFromManifests();
|
||||||
|
|
||||||
const repos = await pluginService.getRepositories();
|
const repos = await pluginService.getRepositories();
|
||||||
setRepositories(repos);
|
setRepositories(repos);
|
||||||
setHasRepository(repos.length > 0);
|
setHasRepository(repos.length > 0);
|
||||||
|
|
||||||
const currentRepoId = pluginService.getCurrentRepositoryId();
|
const currentRepoId = pluginService.getCurrentRepositoryId();
|
||||||
setCurrentRepositoryId(currentRepoId);
|
setCurrentRepositoryId(currentRepoId);
|
||||||
|
|
||||||
const currentRepo = repos.find(r => r.id === currentRepoId);
|
const currentRepo = repos.find(r => r.id === currentRepoId);
|
||||||
if (currentRepo) {
|
if (currentRepo) {
|
||||||
setRepositoryUrl(currentRepo.url);
|
setRepositoryUrl(currentRepo.url);
|
||||||
|
|
@ -1144,7 +1158,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
const url = repositoryUrl.trim();
|
const url = repositoryUrl.trim();
|
||||||
if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) {
|
if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) {
|
||||||
openAlert(
|
openAlert(
|
||||||
'Invalid URL Format',
|
'Invalid URL Format',
|
||||||
'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch\n\nExample:\nhttps://raw.githubusercontent.com/tapframe/nuvio-providers/refs/heads/master'
|
'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch\n\nExample:\nhttps://raw.githubusercontent.com/tapframe/nuvio-providers/refs/heads/master'
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
@ -1199,7 +1213,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
// If enabling a scraper, ensure it's installed first
|
// If enabling a scraper, ensure it's installed first
|
||||||
const installedScrapers = await pluginService.getInstalledScrapers();
|
const installedScrapers = await pluginService.getInstalledScrapers();
|
||||||
const isInstalled = installedScrapers.some(scraper => scraper.id === scraperId);
|
const isInstalled = installedScrapers.some(scraper => scraper.id === scraperId);
|
||||||
|
|
||||||
if (!isInstalled) {
|
if (!isInstalled) {
|
||||||
// Need to install the scraper first
|
// Need to install the scraper first
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
|
@ -1207,7 +1221,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await pluginService.setScraperEnabled(scraperId, enabled);
|
await pluginService.setScraperEnabled(scraperId, enabled);
|
||||||
await loadScrapers();
|
await loadScrapers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1222,7 +1236,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
'Clear All Scrapers',
|
'Clear All Scrapers',
|
||||||
'Are you sure you want to remove all installed scrapers? This action cannot be undone.',
|
'Are you sure you want to remove all installed scrapers? This action cannot be undone.',
|
||||||
[
|
[
|
||||||
{ label: 'Cancel', onPress: () => {} },
|
{ label: 'Cancel', onPress: () => { } },
|
||||||
{
|
{
|
||||||
label: 'Clear',
|
label: 'Clear',
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
|
|
@ -1245,7 +1259,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
'Clear Repository Cache',
|
'Clear Repository Cache',
|
||||||
'This will remove the saved repository URL and clear all cached scraper data. You will need to re-enter your repository URL.',
|
'This will remove the saved repository URL and clear all cached scraper data. You will need to re-enter your repository URL.',
|
||||||
[
|
[
|
||||||
{ label: 'Cancel', onPress: () => {} },
|
{ label: 'Cancel', onPress: () => { } },
|
||||||
{
|
{
|
||||||
label: 'Clear Cache',
|
label: 'Clear Cache',
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
|
|
@ -1274,19 +1288,19 @@ const PluginsScreen: React.FC = () => {
|
||||||
|
|
||||||
const handleToggleLocalScrapers = async (enabled: boolean) => {
|
const handleToggleLocalScrapers = async (enabled: boolean) => {
|
||||||
await updateSetting('enableLocalScrapers', enabled);
|
await updateSetting('enableLocalScrapers', enabled);
|
||||||
|
|
||||||
// If enabling plugins, refresh repository and reload plugins
|
// If enabling plugins, refresh repository and reload plugins
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
try {
|
try {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
logger.log('[PluginsScreen] Enabling plugins - refreshing repository...');
|
logger.log('[PluginsScreen] Enabling plugins - refreshing repository...');
|
||||||
|
|
||||||
// Refresh repository to ensure plugins are available
|
// Refresh repository to ensure plugins are available
|
||||||
await pluginService.refreshRepository();
|
await pluginService.refreshRepository();
|
||||||
|
|
||||||
// Reload plugins to get the latest state
|
// Reload plugins to get the latest state
|
||||||
await loadScrapers();
|
await loadScrapers();
|
||||||
|
|
||||||
logger.log('[PluginsScreen] Plugins enabled and repository refreshed');
|
logger.log('[PluginsScreen] Plugins enabled and repository refreshed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[PluginsScreen] Failed to refresh repository when enabling plugins:', error);
|
logger.error('[PluginsScreen] Failed to refresh repository when enabling plugins:', error);
|
||||||
|
|
@ -1304,7 +1318,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
const handleToggleQualityExclusion = async (quality: string) => {
|
const handleToggleQualityExclusion = async (quality: string) => {
|
||||||
const currentExcluded = settings.excludedQualities || [];
|
const currentExcluded = settings.excludedQualities || [];
|
||||||
const isExcluded = currentExcluded.includes(quality);
|
const isExcluded = currentExcluded.includes(quality);
|
||||||
|
|
||||||
let newExcluded: string[];
|
let newExcluded: string[];
|
||||||
if (isExcluded) {
|
if (isExcluded) {
|
||||||
// Remove from excluded list
|
// Remove from excluded list
|
||||||
|
|
@ -1313,14 +1327,14 @@ const PluginsScreen: React.FC = () => {
|
||||||
// Add to excluded list
|
// Add to excluded list
|
||||||
newExcluded = [...currentExcluded, quality];
|
newExcluded = [...currentExcluded, quality];
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateSetting('excludedQualities', newExcluded);
|
await updateSetting('excludedQualities', newExcluded);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleLanguageExclusion = async (language: string) => {
|
const handleToggleLanguageExclusion = async (language: string) => {
|
||||||
const currentExcluded = settings.excludedLanguages || [];
|
const currentExcluded = settings.excludedLanguages || [];
|
||||||
const isExcluded = currentExcluded.includes(language);
|
const isExcluded = currentExcluded.includes(language);
|
||||||
|
|
||||||
let newExcluded: string[];
|
let newExcluded: string[];
|
||||||
if (isExcluded) {
|
if (isExcluded) {
|
||||||
// Remove from excluded list
|
// Remove from excluded list
|
||||||
|
|
@ -1329,13 +1343,13 @@ const PluginsScreen: React.FC = () => {
|
||||||
// Add to excluded list
|
// Add to excluded list
|
||||||
newExcluded = [...currentExcluded, language];
|
newExcluded = [...currentExcluded, language];
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateSetting('excludedLanguages', newExcluded);
|
await updateSetting('excludedLanguages', newExcluded);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Define available quality options
|
// Define available quality options
|
||||||
const qualityOptions = ['Auto', 'Adaptive', '2160p', '4K', '1080p', '720p', '360p', 'DV', 'HDR', 'REMUX', '480p', 'CAM', 'TS'];
|
const qualityOptions = ['Auto', 'Adaptive', '2160p', '4K', '1080p', '720p', '360p', 'DV', 'HDR', 'REMUX', '480p', 'CAM', 'TS'];
|
||||||
|
|
||||||
// Define available language options
|
// Define available language options
|
||||||
const languageOptions = ['Original', 'English', 'Spanish', 'Latin', 'French', 'German', 'Italian', 'Portuguese', 'Russian', 'Japanese', 'Korean', 'Chinese', 'Arabic', 'Hindi', 'Turkish', 'Dutch', 'Polish'];
|
const languageOptions = ['Original', 'English', 'Spanish', 'Latin', 'French', 'German', 'Italian', 'Portuguese', 'Russian', 'Japanese', 'Korean', 'Chinese', 'Arabic', 'Hindi', 'Turkish', 'Dutch', 'Polish'];
|
||||||
|
|
||||||
|
|
@ -1344,7 +1358,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|
@ -1354,7 +1368,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
<Ionicons name="arrow-back" size={24} color={colors.primary} />
|
<Ionicons name="arrow-back" size={24} color={colors.primary} />
|
||||||
<Text style={styles.backText}>Settings</Text>
|
<Text style={styles.backText}>Settings</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.headerActions}>
|
<View style={styles.headerActions}>
|
||||||
{/* Help Button */}
|
{/* Help Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|
@ -1365,7 +1379,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.headerTitle}>Plugins</Text>
|
<Text style={styles.headerTitle}>Plugins</Text>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
|
@ -1429,7 +1443,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
<Text style={styles.sectionDescription}>
|
<Text style={styles.sectionDescription}>
|
||||||
Manage multiple scraper repositories. Switch between repositories to access different sets of scrapers.
|
Manage multiple scraper repositories. Switch between repositories to access different sets of scrapers.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Current Repository */}
|
{/* Current Repository */}
|
||||||
{currentRepositoryId && (
|
{currentRepositoryId && (
|
||||||
<View style={styles.currentRepoContainer}>
|
<View style={styles.currentRepoContainer}>
|
||||||
|
|
@ -1438,7 +1452,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
<Text style={[styles.currentRepoUrl, { fontSize: 12, opacity: 0.7, marginTop: 4 }]}>{repositoryUrl}</Text>
|
<Text style={[styles.currentRepoUrl, { fontSize: 12, opacity: 0.7, marginTop: 4 }]}>{repositoryUrl}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Repository List */}
|
{/* Repository List */}
|
||||||
{repositories.length > 0 && (
|
{repositories.length > 0 && (
|
||||||
<View style={styles.repositoriesList}>
|
<View style={styles.repositoriesList}>
|
||||||
|
|
@ -1467,8 +1481,8 @@ const PluginsScreen: React.FC = () => {
|
||||||
<Text style={styles.repositoryUrl}>{repo.url}</Text>
|
<Text style={styles.repositoryUrl}>{repo.url}</Text>
|
||||||
<Text style={styles.repositoryMeta}>
|
<Text style={styles.repositoryMeta}>
|
||||||
{repo.scraperCount || 0} scrapers • Last updated: {repo.lastUpdated ? new Date(repo.lastUpdated).toLocaleDateString() : 'Never'}
|
{repo.scraperCount || 0} scrapers • Last updated: {repo.lastUpdated ? new Date(repo.lastUpdated).toLocaleDateString() : 'Never'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.repositoryActions}>
|
<View style={styles.repositoryActions}>
|
||||||
{repo.id !== currentRepositoryId && (
|
{repo.id !== currentRepositoryId && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|
@ -1502,7 +1516,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
<Text style={styles.repositoryActionButtonText}>Remove</Text>
|
<Text style={styles.repositoryActionButtonText}>Remove</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1541,9 +1555,9 @@ const PluginsScreen: React.FC = () => {
|
||||||
{searchQuery.length > 0 && (
|
{searchQuery.length > 0 && (
|
||||||
<TouchableOpacity onPress={() => setSearchQuery('')}>
|
<TouchableOpacity onPress={() => setSearchQuery('')}>
|
||||||
<Ionicons name="close-circle" size={20} color={colors.mediumGray} />
|
<Ionicons name="close-circle" size={20} color={colors.mediumGray} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Filter Chips */}
|
{/* Filter Chips */}
|
||||||
<View style={styles.filterContainer}>
|
<View style={styles.filterContainer}>
|
||||||
|
|
@ -1561,7 +1575,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
selectedFilter === filter && styles.filterChipTextSelected
|
selectedFilter === filter && styles.filterChipTextSelected
|
||||||
]}>
|
]}>
|
||||||
{filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'}
|
{filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -1590,17 +1604,17 @@ const PluginsScreen: React.FC = () => {
|
||||||
|
|
||||||
{filteredScrapers.length === 0 ? (
|
{filteredScrapers.length === 0 ? (
|
||||||
<View style={styles.emptyStateContainer}>
|
<View style={styles.emptyStateContainer}>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={searchQuery ? "search" : "download-outline"}
|
name={searchQuery ? "search" : "download-outline"}
|
||||||
size={48}
|
size={48}
|
||||||
color={colors.mediumGray}
|
color={colors.mediumGray}
|
||||||
style={styles.emptyStateIcon}
|
style={styles.emptyStateIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.emptyStateTitle}>
|
<Text style={styles.emptyStateTitle}>
|
||||||
{searchQuery ? 'No Scrapers Found' : 'No Scrapers Available'}
|
{searchQuery ? 'No Scrapers Found' : 'No Scrapers Available'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.emptyStateDescription}>
|
<Text style={styles.emptyStateDescription}>
|
||||||
{searchQuery
|
{searchQuery
|
||||||
? `No scrapers match "${searchQuery}". Try a different search term.`
|
? `No scrapers match "${searchQuery}". Try a different search term.`
|
||||||
: 'Configure a repository above to view available scrapers.'
|
: 'Configure a repository above to view available scrapers.'
|
||||||
}
|
}
|
||||||
|
|
@ -1613,45 +1627,45 @@ const PluginsScreen: React.FC = () => {
|
||||||
<Text style={styles.secondaryButtonText}>Clear Search</Text>
|
<Text style={styles.secondaryButtonText}>Clear Search</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.scrapersContainer}>
|
<View style={styles.scrapersContainer}>
|
||||||
{filteredScrapers.map((scraper) => (
|
{filteredScrapers.map((scraper) => (
|
||||||
<View key={scraper.id} style={styles.scraperCard}>
|
<View key={scraper.id} style={styles.scraperCard}>
|
||||||
<View style={styles.scraperCardHeader}>
|
<View style={styles.scraperCardHeader}>
|
||||||
{scraper.logo ? (
|
{scraper.logo ? (
|
||||||
(scraper.logo.toLowerCase().endsWith('.svg') || scraper.logo.toLowerCase().includes('.svg?')) ? (
|
(scraper.logo.toLowerCase().endsWith('.svg') || scraper.logo.toLowerCase().includes('.svg?')) ? (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: scraper.logo }}
|
source={{ uri: scraper.logo }}
|
||||||
style={styles.scraperLogo}
|
style={styles.scraperLogo}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri: scraper.logo }}
|
source={{ uri: scraper.logo }}
|
||||||
style={styles.scraperLogo}
|
style={styles.scraperLogo}
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.scraperLogo} />
|
<View style={styles.scraperLogo} />
|
||||||
)}
|
)}
|
||||||
<View style={styles.scraperCardInfo}>
|
<View style={styles.scraperCardInfo}>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4, gap: 8 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4, gap: 8 }}>
|
||||||
<Text style={styles.scraperName}>{scraper.name}</Text>
|
<Text style={styles.scraperName}>{scraper.name}</Text>
|
||||||
<StatusBadge status={getScraperStatus(scraper)} colors={colors} />
|
<StatusBadge status={getScraperStatus(scraper)} colors={colors} />
|
||||||
</View>
|
|
||||||
<Text style={styles.scraperDescription}>{scraper.description}</Text>
|
|
||||||
</View>
|
|
||||||
<Switch
|
|
||||||
value={scraper.enabled && settings.enableLocalScrapers}
|
|
||||||
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
|
|
||||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
|
||||||
thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
|
||||||
disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
|
<Text style={styles.scraperDescription}>{scraper.description}</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={scraper.enabled && settings.enableLocalScrapers}
|
||||||
|
onValueChange={(enabled) => handleToggleScraper(scraper.id, enabled)}
|
||||||
|
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||||
|
thumbColor={scraper.enabled && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
||||||
|
disabled={!settings.enableLocalScrapers || scraper.manifestEnabled === false || (scraper.disabledPlatforms && scraper.disabledPlatforms.includes(Platform.OS as 'ios' | 'android'))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.scraperCardMeta}>
|
<View style={styles.scraperCardMeta}>
|
||||||
<View style={styles.scraperCardMetaItem}>
|
<View style={styles.scraperCardMetaItem}>
|
||||||
<Ionicons name="information-circle" size={12} color={colors.mediumGray} />
|
<Ionicons name="information-circle" size={12} color={colors.mediumGray} />
|
||||||
|
|
@ -1682,62 +1696,62 @@ const PluginsScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* ShowBox Settings - only visible when ShowBox scraper is available */}
|
{/* ShowBox Settings - only visible when ShowBox scraper is available */}
|
||||||
{showboxScraperId && scraper.id === showboxScraperId && settings.enableLocalScrapers && (
|
{showboxScraperId && scraper.id === showboxScraperId && settings.enableLocalScrapers && (
|
||||||
<View style={{ marginTop: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.elevation3 }}>
|
<View style={{ marginTop: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.elevation3 }}>
|
||||||
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox UI Token</Text>
|
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox UI Token</Text>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.textInput, { flex: 1, marginBottom: 0 }]}
|
style={[styles.textInput, { flex: 1, marginBottom: 0 }]}
|
||||||
value={showboxUiToken}
|
value={showboxUiToken}
|
||||||
onChangeText={setShowboxUiToken}
|
onChangeText={setShowboxUiToken}
|
||||||
placeholder="Paste your ShowBox UI token"
|
placeholder="Paste your ShowBox UI token"
|
||||||
placeholderTextColor={colors.mediumGray}
|
placeholderTextColor={colors.mediumGray}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
secureTextEntry={showboxSavedToken.length > 0 && !showboxTokenVisible}
|
secureTextEntry={showboxSavedToken.length > 0 && !showboxTokenVisible}
|
||||||
multiline={false}
|
multiline={false}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
/>
|
/>
|
||||||
{showboxSavedToken.length > 0 && (
|
{showboxSavedToken.length > 0 && (
|
||||||
<TouchableOpacity onPress={() => setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}>
|
<TouchableOpacity onPress={() => setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}>
|
||||||
<Ionicons name={showboxTokenVisible ? 'eye-off' : 'eye'} size={18} color={colors.primary} />
|
<Ionicons name={showboxTokenVisible ? 'eye-off' : 'eye'} size={18} color={colors.primary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.buttonRow}>
|
<View style={styles.buttonRow}>
|
||||||
{showboxUiToken !== showboxSavedToken && (
|
{showboxUiToken !== showboxSavedToken && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.button, styles.primaryButton]}
|
style={[styles.button, styles.primaryButton]}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
if (showboxScraperId) {
|
if (showboxScraperId) {
|
||||||
await pluginService.setScraperSettings(showboxScraperId, { uiToken: showboxUiToken });
|
await pluginService.setScraperSettings(showboxScraperId, { uiToken: showboxUiToken });
|
||||||
}
|
}
|
||||||
setShowboxSavedToken(showboxUiToken);
|
setShowboxSavedToken(showboxUiToken);
|
||||||
openAlert('Saved', 'ShowBox settings updated');
|
openAlert('Saved', 'ShowBox settings updated');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>Save</Text>
|
<Text style={styles.buttonText}>Save</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.button, styles.secondaryButton]}
|
style={[styles.button, styles.secondaryButton]}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
setShowboxUiToken('');
|
setShowboxUiToken('');
|
||||||
setShowboxSavedToken('');
|
setShowboxSavedToken('');
|
||||||
if (showboxScraperId) {
|
if (showboxScraperId) {
|
||||||
await pluginService.setScraperSettings(showboxScraperId, {});
|
await pluginService.setScraperSettings(showboxScraperId, {});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.secondaryButtonText}>Clear</Text>
|
<Text style={styles.secondaryButtonText}>Clear</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* Additional Settings */}
|
{/* Additional Settings */}
|
||||||
|
|
@ -1763,7 +1777,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
disabled={!settings.enableLocalScrapers}
|
disabled={!settings.enableLocalScrapers}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.settingRow}>
|
<View style={styles.settingRow}>
|
||||||
<View style={styles.settingInfo}>
|
<View style={styles.settingInfo}>
|
||||||
<Text style={styles.settingTitle}>Group Plugin Streams</Text>
|
<Text style={styles.settingTitle}>Group Plugin Streams</Text>
|
||||||
|
|
@ -1772,20 +1786,20 @@ const PluginsScreen: React.FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.streamDisplayMode === 'grouped'}
|
value={settings.streamDisplayMode === 'grouped'}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
updateSetting('streamDisplayMode', value ? 'grouped' : 'separate');
|
updateSetting('streamDisplayMode', value ? 'grouped' : 'separate');
|
||||||
// Auto-disable quality sorting when grouping is disabled
|
// Auto-disable quality sorting when grouping is disabled
|
||||||
if (!value && settings.streamSortMode === 'quality-then-scraper') {
|
if (!value && settings.streamSortMode === 'quality-then-scraper') {
|
||||||
updateSetting('streamSortMode', 'scraper-then-quality');
|
updateSetting('streamSortMode', 'scraper-then-quality');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||||
thumbColor={settings.streamDisplayMode === 'grouped' ? colors.white : '#f4f3f4'}
|
thumbColor={settings.streamDisplayMode === 'grouped' ? colors.white : '#f4f3f4'}
|
||||||
disabled={!settings.enableLocalScrapers}
|
disabled={!settings.enableLocalScrapers}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.settingRow}>
|
<View style={styles.settingRow}>
|
||||||
<View style={styles.settingInfo}>
|
<View style={styles.settingInfo}>
|
||||||
<Text style={styles.settingTitle}>Sort by Quality First</Text>
|
<Text style={styles.settingTitle}>Sort by Quality First</Text>
|
||||||
|
|
@ -1794,14 +1808,14 @@ const PluginsScreen: React.FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.streamSortMode === 'quality-then-scraper'}
|
value={settings.streamSortMode === 'quality-then-scraper'}
|
||||||
onValueChange={(value) => updateSetting('streamSortMode', value ? 'quality-then-scraper' : 'scraper-then-quality')}
|
onValueChange={(value) => updateSetting('streamSortMode', value ? 'quality-then-scraper' : 'scraper-then-quality')}
|
||||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||||
thumbColor={settings.streamSortMode === 'quality-then-scraper' ? colors.white : '#f4f3f4'}
|
thumbColor={settings.streamSortMode === 'quality-then-scraper' ? colors.white : '#f4f3f4'}
|
||||||
disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'}
|
disabled={!settings.enableLocalScrapers || settings.streamDisplayMode !== 'grouped'}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.settingRow}>
|
<View style={styles.settingRow}>
|
||||||
<View style={styles.settingInfo}>
|
<View style={styles.settingInfo}>
|
||||||
<Text style={styles.settingTitle}>Show Scraper Logos</Text>
|
<Text style={styles.settingTitle}>Show Scraper Logos</Text>
|
||||||
|
|
@ -1810,12 +1824,12 @@ const PluginsScreen: React.FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Switch
|
<Switch
|
||||||
value={settings.showScraperLogos && settings.enableLocalScrapers}
|
value={settings.showScraperLogos && settings.enableLocalScrapers}
|
||||||
onValueChange={(value) => updateSetting('showScraperLogos', value)}
|
onValueChange={(value) => updateSetting('showScraperLogos', value)}
|
||||||
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
trackColor={{ false: colors.elevation3, true: colors.primary }}
|
||||||
thumbColor={settings.showScraperLogos && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
thumbColor={settings.showScraperLogos && settings.enableLocalScrapers ? colors.white : '#f4f3f4'}
|
||||||
disabled={!settings.enableLocalScrapers}
|
disabled={!settings.enableLocalScrapers}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
|
|
@ -1830,7 +1844,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
<Text style={styles.sectionDescription}>
|
<Text style={styles.sectionDescription}>
|
||||||
Exclude specific video qualities from search results. Tap on a quality to exclude it from plugin results.
|
Exclude specific video qualities from search results. Tap on a quality to exclude it from plugin results.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={styles.qualityChipsContainer}>
|
<View style={styles.qualityChipsContainer}>
|
||||||
{qualityOptions.map((quality) => {
|
{qualityOptions.map((quality) => {
|
||||||
const isExcluded = (settings.excludedQualities || []).includes(quality);
|
const isExcluded = (settings.excludedQualities || []).includes(quality);
|
||||||
|
|
@ -1856,7 +1870,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{(settings.excludedQualities || []).length > 0 && (
|
{(settings.excludedQualities || []).length > 0 && (
|
||||||
<Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}>
|
<Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}>
|
||||||
Excluded qualities: {(settings.excludedQualities || []).join(', ')}
|
Excluded qualities: {(settings.excludedQualities || []).join(', ')}
|
||||||
|
|
@ -1875,11 +1889,11 @@ const PluginsScreen: React.FC = () => {
|
||||||
<Text style={styles.sectionDescription}>
|
<Text style={styles.sectionDescription}>
|
||||||
Exclude specific languages from search results. Tap on a language to exclude it from plugin results.
|
Exclude specific languages from search results. Tap on a language to exclude it from plugin results.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text style={[styles.infoText, { marginTop: 8, fontSize: 13, color: colors.mediumEmphasis }]}>
|
<Text style={[styles.infoText, { marginTop: 8, fontSize: 13, color: colors.mediumEmphasis }]}>
|
||||||
<Text style={{ fontWeight: '600' }}>Note:</Text> This filter only applies to providers that include language information in their stream names. It does not affect other providers.
|
<Text style={{ fontWeight: '600' }}>Note:</Text> This filter only applies to providers that include language information in their stream names. It does not affect other providers.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={styles.qualityChipsContainer}>
|
<View style={styles.qualityChipsContainer}>
|
||||||
{languageOptions.map((language) => {
|
{languageOptions.map((language) => {
|
||||||
const isExcluded = (settings.excludedLanguages || []).includes(language);
|
const isExcluded = (settings.excludedLanguages || []).includes(language);
|
||||||
|
|
@ -1905,7 +1919,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{(settings.excludedLanguages || []).length > 0 && (
|
{(settings.excludedLanguages || []).length > 0 && (
|
||||||
<Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}>
|
<Text style={[styles.infoText, { marginTop: 12 }, !settings.enableLocalScrapers && styles.disabledText]}>
|
||||||
Excluded languages: {(settings.excludedLanguages || []).join(', ')}
|
Excluded languages: {(settings.excludedLanguages || []).join(', ')}
|
||||||
|
|
@ -1988,36 +2002,36 @@ const PluginsScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
{/* Format Hint */}
|
{/* Format Hint */}
|
||||||
<Text style={styles.formatHint}>
|
<Text style={styles.formatHint}>
|
||||||
Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch
|
Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<View style={styles.compactActions}>
|
<View style={styles.compactActions}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.compactButton, styles.cancelButton]}
|
style={[styles.compactButton, styles.cancelButton]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setShowAddRepositoryModal(false);
|
setShowAddRepositoryModal(false);
|
||||||
setNewRepositoryUrl('');
|
setNewRepositoryUrl('');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]}
|
style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]}
|
||||||
onPress={handleAddRepository}
|
onPress={handleAddRepository}
|
||||||
disabled={!newRepositoryUrl.trim() || isLoading}
|
disabled={!newRepositoryUrl.trim() || isLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ActivityIndicator size="small" color={colors.white} />
|
<ActivityIndicator size="small" color={colors.white} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.addButtonText}>Add</Text>
|
<Text style={styles.addButtonText}>Add</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue