mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
ui overhaul for plugnscreen
This commit is contained in:
parent
c9ea142dfb
commit
a30ace14c9
2 changed files with 773 additions and 167 deletions
|
|
@ -21,7 +21,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { localScraperService, ScraperInfo } from '../services/localScraperService';
|
||||
import { localScraperService, ScraperInfo, RepositoryInfo } from '../services/localScraperService';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
|
|
@ -189,28 +189,34 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
fontSize: 15,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: colors.elevation2,
|
||||
backgroundColor: 'transparent',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
marginRight: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.elevation3,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: 44,
|
||||
},
|
||||
primaryButton: {
|
||||
backgroundColor: colors.primary,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
secondaryButton: {
|
||||
backgroundColor: colors.elevation2,
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: colors.elevation3,
|
||||
},
|
||||
buttonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
color: colors.white,
|
||||
textAlign: 'center',
|
||||
},
|
||||
secondaryButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.mediumGray,
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
color: colors.white,
|
||||
textAlign: 'center',
|
||||
},
|
||||
clearButton: {
|
||||
|
|
@ -264,6 +270,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginTop: 8,
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 14,
|
||||
|
|
@ -440,35 +447,40 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16,
|
||||
gap: 8,
|
||||
gap: 12,
|
||||
},
|
||||
bulkActionButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: 44,
|
||||
borderWidth: 1,
|
||||
},
|
||||
bulkActionButtonEnabled: {
|
||||
backgroundColor: '#34C759',
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#34C759',
|
||||
},
|
||||
bulkActionButtonDisabled: {
|
||||
backgroundColor: colors.elevation2,
|
||||
borderWidth: 1,
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: colors.elevation3,
|
||||
},
|
||||
bulkActionButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
fontWeight: '500',
|
||||
},
|
||||
helpButton: {
|
||||
position: 'absolute',
|
||||
top: Platform.OS === 'ios' ? 44 : ANDROID_STATUSBAR_HEIGHT + 16,
|
||||
right: 16,
|
||||
backgroundColor: colors.elevation2,
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 20,
|
||||
padding: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.elevation3,
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
|
|
@ -498,16 +510,18 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
},
|
||||
modalButton: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 12,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: 16,
|
||||
minHeight: 48,
|
||||
},
|
||||
modalButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
fontWeight: '500',
|
||||
},
|
||||
quickSetupContainer: {
|
||||
backgroundColor: colors.elevation2,
|
||||
|
|
@ -531,15 +545,17 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
},
|
||||
quickSetupButton: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: 44,
|
||||
},
|
||||
quickSetupButtonText: {
|
||||
color: colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
},
|
||||
scraperCard: {
|
||||
backgroundColor: colors.elevation2,
|
||||
|
|
@ -581,6 +597,75 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
emptyStateIcon: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
// Repository management styles
|
||||
repositoriesList: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
repositoryItem: {
|
||||
backgroundColor: colors.elevation2,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.elevation3,
|
||||
},
|
||||
repositoryInfo: {
|
||||
flex: 1,
|
||||
marginBottom: 12,
|
||||
},
|
||||
repositoryName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
marginRight: 8,
|
||||
},
|
||||
repositoryDescription: {
|
||||
fontSize: 14,
|
||||
color: colors.mediumGray,
|
||||
marginBottom: 4,
|
||||
lineHeight: 18,
|
||||
},
|
||||
repositoryUrl: {
|
||||
fontSize: 12,
|
||||
color: colors.mediumGray,
|
||||
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
|
||||
marginBottom: 4,
|
||||
},
|
||||
repositoryMeta: {
|
||||
fontSize: 12,
|
||||
color: colors.mediumGray,
|
||||
},
|
||||
repositoryActions: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginTop: 8,
|
||||
},
|
||||
repositoryActionButton: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: 36,
|
||||
},
|
||||
repositoryActionButtonPrimary: {
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
repositoryActionButtonSecondary: {
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: colors.elevation3,
|
||||
},
|
||||
repositoryActionButtonDanger: {
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: '#ff3b30',
|
||||
},
|
||||
repositoryActionButtonText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
color: colors.white,
|
||||
},
|
||||
});
|
||||
|
||||
// Helper component for collapsible sections
|
||||
|
|
@ -660,6 +745,16 @@ const PluginsScreen: React.FC = () => {
|
|||
const [showboxCookie, setShowboxCookie] = useState<string>('');
|
||||
const [showboxRegion, setShowboxRegion] = useState<string>('');
|
||||
|
||||
// Multiple repositories state
|
||||
const [repositories, setRepositories] = useState<RepositoryInfo[]>([]);
|
||||
const [currentRepositoryId, setCurrentRepositoryId] = useState<string>('');
|
||||
const [showAddRepositoryModal, setShowAddRepositoryModal] = useState(false);
|
||||
const [newRepositoryUrl, setNewRepositoryUrl] = useState('');
|
||||
const [newRepositoryName, setNewRepositoryName] = useState('');
|
||||
const [newRepositoryDescription, setNewRepositoryDescription] = useState('');
|
||||
const [switchingRepository, setSwitchingRepository] = useState<string | null>(null);
|
||||
const [fetchingRepoName, setFetchingRepoName] = useState(false);
|
||||
|
||||
// New UX state
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedFilter, setSelectedFilter] = useState<'all' | 'movie' | 'tv'>('all');
|
||||
|
|
@ -742,9 +837,123 @@ const PluginsScreen: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleUrlChange = async (url: string) => {
|
||||
setNewRepositoryUrl(url);
|
||||
// Auto-populate repository name if it's empty and URL is valid
|
||||
if (!newRepositoryName.trim() && url.trim()) {
|
||||
setFetchingRepoName(true);
|
||||
try {
|
||||
// Try to fetch name from manifest first
|
||||
const manifestName = await localScraperService.fetchRepositoryNameFromManifest(url.trim());
|
||||
setNewRepositoryName(manifestName);
|
||||
} catch (error) {
|
||||
// Fallback to URL extraction if manifest fetch fails
|
||||
try {
|
||||
const extractedName = localScraperService.extractRepositoryName(url.trim());
|
||||
if (extractedName !== 'Unknown Repository') {
|
||||
setNewRepositoryName(extractedName);
|
||||
}
|
||||
} catch (extractError) {
|
||||
// Ignore errors, just don't auto-populate
|
||||
}
|
||||
} finally {
|
||||
setFetchingRepoName(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddRepository = async () => {
|
||||
if (!newRepositoryUrl.trim()) {
|
||||
Alert.alert('Error', 'Please enter a valid repository URL');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
const url = newRepositoryUrl.trim();
|
||||
if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) {
|
||||
Alert.alert(
|
||||
'Invalid URL Format',
|
||||
'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/branch/\n\nExample:\nhttps://raw.githubusercontent.com/tapframe/nuvio-providers/main/'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const repoId = await localScraperService.addRepository({
|
||||
name: newRepositoryName.trim(), // Let the service fetch from manifest if empty
|
||||
url,
|
||||
description: newRepositoryDescription.trim(),
|
||||
enabled: true
|
||||
});
|
||||
|
||||
await loadRepositories();
|
||||
|
||||
// Switch to the new repository and refresh it
|
||||
await localScraperService.setCurrentRepository(repoId);
|
||||
await loadRepositories();
|
||||
await loadScrapers();
|
||||
|
||||
setNewRepositoryUrl('');
|
||||
setNewRepositoryName('');
|
||||
setNewRepositoryDescription('');
|
||||
setFetchingRepoName(false);
|
||||
setShowAddRepositoryModal(false);
|
||||
Alert.alert('Success', 'Repository added and refreshed successfully');
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to add repository:', error);
|
||||
Alert.alert('Error', 'Failed to add repository');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitchRepository = async (repoId: string) => {
|
||||
try {
|
||||
setSwitchingRepository(repoId);
|
||||
await localScraperService.setCurrentRepository(repoId);
|
||||
await loadRepositories();
|
||||
await loadScrapers();
|
||||
Alert.alert('Success', 'Repository switched successfully');
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to switch repository:', error);
|
||||
Alert.alert('Error', 'Failed to switch repository');
|
||||
} finally {
|
||||
setSwitchingRepository(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveRepository = async (repoId: string) => {
|
||||
const repo = repositories.find(r => r.id === repoId);
|
||||
if (!repo) return;
|
||||
|
||||
Alert.alert(
|
||||
'Remove Repository',
|
||||
`Are you sure you want to remove "${repo.name}"? This will also remove all scrapers from this repository.`,
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Remove',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await localScraperService.removeRepository(repoId);
|
||||
await loadRepositories();
|
||||
await loadScrapers();
|
||||
Alert.alert('Success', 'Repository removed successfully');
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to remove repository:', error);
|
||||
Alert.alert('Error', error instanceof Error ? error.message : 'Failed to remove repository');
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadScrapers();
|
||||
checkRepository();
|
||||
loadRepositories();
|
||||
}, []);
|
||||
|
||||
const loadScrapers = async () => {
|
||||
|
|
@ -763,6 +972,27 @@ const PluginsScreen: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const loadRepositories = async () => {
|
||||
try {
|
||||
// First refresh repository names from manifests for existing repositories
|
||||
await localScraperService.refreshRepositoryNamesFromManifests();
|
||||
|
||||
const repos = await localScraperService.getRepositories();
|
||||
setRepositories(repos);
|
||||
setHasRepository(repos.length > 0);
|
||||
|
||||
const currentRepoId = localScraperService.getCurrentRepositoryId();
|
||||
setCurrentRepositoryId(currentRepoId);
|
||||
|
||||
const currentRepo = repos.find(r => r.id === currentRepoId);
|
||||
if (currentRepo) {
|
||||
setRepositoryUrl(currentRepo.url);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to load repositories:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkRepository = async () => {
|
||||
try {
|
||||
const repoUrl = await localScraperService.getRepositoryUrl();
|
||||
|
|
@ -1024,10 +1254,11 @@ const PluginsScreen: React.FC = () => {
|
|||
styles={styles}
|
||||
>
|
||||
<Text style={styles.sectionDescription}>
|
||||
Enter the URL of a Nuvio scraper repository to download and install scrapers.
|
||||
Manage multiple scraper repositories. Switch between repositories to access different sets of scrapers.
|
||||
</Text>
|
||||
|
||||
{hasRepository && repositoryUrl && (
|
||||
{/* Current Repository */}
|
||||
{currentRepositoryId && (
|
||||
<View style={styles.currentRepoContainer}>
|
||||
<Text style={styles.currentRepoLabel}>Current Repository:</Text>
|
||||
<Text style={styles.currentRepoUrl}>{localScraperService.getRepositoryName()}</Text>
|
||||
|
|
@ -1035,58 +1266,84 @@ const PluginsScreen: React.FC = () => {
|
|||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={[styles.textInput, !settings.enableLocalScrapers && styles.disabledInput]}
|
||||
value={repositoryUrl}
|
||||
onChangeText={setRepositoryUrl}
|
||||
placeholder="https://raw.githubusercontent.com/tapframe/nuvio-providers/main"
|
||||
placeholderTextColor={!settings.enableLocalScrapers ? colors.elevation3 : "#999"}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType="url"
|
||||
editable={settings.enableLocalScrapers}
|
||||
/>
|
||||
<Text style={[styles.urlHint, !settings.enableLocalScrapers && styles.disabledText]}>
|
||||
Use GitHub raw URL format. Default: https://raw.githubusercontent.com/tapframe/nuvio-providers/main
|
||||
{/* Repository List */}
|
||||
{repositories.length > 0 && (
|
||||
<View style={styles.repositoriesList}>
|
||||
<Text style={[styles.settingTitle, { marginBottom: 12 }]}>Available Repositories</Text>
|
||||
{repositories.map((repo) => (
|
||||
<View key={repo.id} style={styles.repositoryItem}>
|
||||
<View style={styles.repositoryInfo}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4 }}>
|
||||
<Text style={styles.repositoryName}>{repo.name}</Text>
|
||||
{repo.id === currentRepositoryId && (
|
||||
<View style={[styles.statusBadge, { backgroundColor: '#34C759' }]}>
|
||||
<Ionicons name="checkmark-circle" size={12} color="white" />
|
||||
<Text style={styles.statusBadgeText}>Active</Text>
|
||||
</View>
|
||||
)}
|
||||
{switchingRepository === repo.id && (
|
||||
<View style={[styles.statusBadge, { backgroundColor: colors.primary }]}>
|
||||
<ActivityIndicator size={12} color="white" />
|
||||
<Text style={styles.statusBadgeText}>Switching...</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{repo.description && (
|
||||
<Text style={styles.repositoryDescription}>{repo.description}</Text>
|
||||
)}
|
||||
<Text style={styles.repositoryUrl}>{repo.url}</Text>
|
||||
<Text style={styles.repositoryMeta}>
|
||||
{repo.scraperCount || 0} scrapers • Last updated: {repo.lastUpdated ? new Date(repo.lastUpdated).toLocaleDateString() : 'Never'}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.defaultRepoButton, !settings.enableLocalScrapers && styles.disabledButton]}
|
||||
onPress={handleUseDefaultRepo}
|
||||
disabled={!settings.enableLocalScrapers}
|
||||
>
|
||||
<Text style={[styles.defaultRepoButtonText, !settings.enableLocalScrapers && styles.disabledText]}>Use Default Repository</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.repositoryActions}>
|
||||
{repo.id !== currentRepositoryId && (
|
||||
<TouchableOpacity
|
||||
style={[styles.repositoryActionButton, styles.repositoryActionButtonPrimary]}
|
||||
onPress={() => handleSwitchRepository(repo.id)}
|
||||
disabled={switchingRepository === repo.id}
|
||||
>
|
||||
{switchingRepository === repo.id ? (
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
) : (
|
||||
<Text style={styles.repositoryActionButtonText}>Switch</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.repositoryActionButton, styles.repositoryActionButtonSecondary]}
|
||||
onPress={() => handleRefreshRepository()}
|
||||
disabled={isRefreshing || switchingRepository !== null}
|
||||
>
|
||||
{isRefreshing ? (
|
||||
<ActivityIndicator size="small" color={colors.mediumGray} />
|
||||
) : (
|
||||
<Text style={styles.repositoryActionButtonText}>Refresh</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{repositories.length > 1 && (
|
||||
<TouchableOpacity
|
||||
style={[styles.repositoryActionButton, styles.repositoryActionButtonDanger]}
|
||||
onPress={() => handleRemoveRepository(repo.id)}
|
||||
disabled={switchingRepository !== null}
|
||||
>
|
||||
<Text style={styles.repositoryActionButtonText}>Remove</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.primaryButton, !settings.enableLocalScrapers && styles.disabledButton]}
|
||||
onPress={handleSaveRepository}
|
||||
disabled={isLoading || !settings.enableLocalScrapers}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator size="small" color="#ffffff" />
|
||||
) : (
|
||||
<Text style={[styles.buttonText, !settings.enableLocalScrapers && styles.disabledText]}>Save Repository</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{hasRepository && (
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.secondaryButton, !settings.enableLocalScrapers && styles.disabledButton]}
|
||||
onPress={handleRefreshRepository}
|
||||
disabled={isRefreshing || !settings.enableLocalScrapers}
|
||||
>
|
||||
{isRefreshing ? (
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
) : (
|
||||
<Text style={[styles.secondaryButtonText, !settings.enableLocalScrapers && styles.disabledText]}>Refresh</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
{/* Add Repository Button */}
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.primaryButton, { marginTop: 16 }]}
|
||||
onPress={() => setShowAddRepositoryModal(true)}
|
||||
disabled={!settings.enableLocalScrapers || switchingRepository !== null}
|
||||
>
|
||||
<Text style={styles.buttonText}>Add New Repository</Text>
|
||||
</TouchableOpacity>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Available Scrapers */}
|
||||
|
|
@ -1112,9 +1369,9 @@ const PluginsScreen: React.FC = () => {
|
|||
{searchQuery.length > 0 && (
|
||||
<TouchableOpacity onPress={() => setSearchQuery('')}>
|
||||
<Ionicons name="close-circle" size={20} color={colors.mediumGray} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Filter Chips */}
|
||||
<View style={styles.filterContainer}>
|
||||
|
|
@ -1132,7 +1389,7 @@ const PluginsScreen: React.FC = () => {
|
|||
selectedFilter === filter && styles.filterChipTextSelected
|
||||
]}>
|
||||
{filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'}
|
||||
</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
|
@ -1145,14 +1402,14 @@ const PluginsScreen: React.FC = () => {
|
|||
onPress={() => handleBulkToggle(true)}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<Text style={styles.bulkActionButtonText}>Enable All</Text>
|
||||
<Text style={[styles.bulkActionButtonText, { color: '#34C759' }]}>Enable All</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.bulkActionButton, styles.bulkActionButtonDisabled]}
|
||||
onPress={() => handleBulkToggle(false)}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<Text style={styles.bulkActionButtonText}>Disable All</Text>
|
||||
<Text style={[styles.bulkActionButtonText, { color: colors.mediumGray }]}>Disable All</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -1169,7 +1426,7 @@ const PluginsScreen: React.FC = () => {
|
|||
/>
|
||||
<Text style={styles.emptyStateTitle}>
|
||||
{searchQuery ? 'No Scrapers Found' : 'No Scrapers Available'}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text style={styles.emptyStateDescription}>
|
||||
{searchQuery
|
||||
? `No scrapers match "${searchQuery}". Try a different search term.`
|
||||
|
|
@ -1178,42 +1435,42 @@ const PluginsScreen: React.FC = () => {
|
|||
</Text>
|
||||
{searchQuery && (
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
style={[styles.button, styles.secondaryButton]}
|
||||
onPress={() => setSearchQuery('')}
|
||||
>
|
||||
<Text style={styles.buttonText}>Clear Search</Text>
|
||||
<Text style={styles.secondaryButtonText}>Clear Search</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.scrapersContainer}>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.scrapersContainer}>
|
||||
{filteredScrapers.map((scraper) => (
|
||||
<View key={scraper.id} style={styles.scraperCard}>
|
||||
<View style={styles.scraperCardHeader}>
|
||||
{scraper.logo ? (
|
||||
<Image
|
||||
source={{ uri: scraper.logo }}
|
||||
{scraper.logo ? (
|
||||
<Image
|
||||
source={{ uri: scraper.logo }}
|
||||
style={styles.scraperLogo}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
) : (
|
||||
resizeMode="contain"
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.scraperLogo} />
|
||||
)}
|
||||
<View style={styles.scraperCardInfo}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4 }}>
|
||||
<Text style={styles.scraperName}>{scraper.name}</Text>
|
||||
<StatusBadge status={getScraperStatus(scraper)} colors={colors} />
|
||||
</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>
|
||||
<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.scraperCardMetaItem}>
|
||||
|
|
@ -1237,64 +1494,64 @@ const PluginsScreen: React.FC = () => {
|
|||
</View>
|
||||
|
||||
{/* ShowBox Settings */}
|
||||
{scraper.id === 'showboxog' && settings.enableLocalScrapers && (
|
||||
{scraper.id === 'showboxog' && settings.enableLocalScrapers && (
|
||||
<View style={{ marginTop: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.elevation3 }}>
|
||||
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox Cookie</Text>
|
||||
<TextInput
|
||||
style={[styles.textInput, { marginBottom: 12 }]}
|
||||
value={showboxCookie}
|
||||
onChangeText={setShowboxCookie}
|
||||
placeholder="Paste FebBox ui cookie value"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
multiline={true}
|
||||
numberOfLines={3}
|
||||
/>
|
||||
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>Region</Text>
|
||||
<View style={[styles.qualityChipsContainer, { marginBottom: 16 }]}>
|
||||
{regionOptions.map(opt => {
|
||||
const selected = showboxRegion === opt.value;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={opt.value}
|
||||
style={[styles.qualityChip, selected && styles.qualityChipSelected]}
|
||||
onPress={() => setShowboxRegion(opt.value)}
|
||||
>
|
||||
<Text style={[styles.qualityChipText, selected && styles.qualityChipTextSelected]}>
|
||||
{opt.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.primaryButton]}
|
||||
onPress={async () => {
|
||||
await localScraperService.setScraperSettings('showboxog', { cookie: showboxCookie, region: showboxRegion });
|
||||
Alert.alert('Saved', 'ShowBox settings updated');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.buttonText}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.secondaryButton]}
|
||||
onPress={async () => {
|
||||
setShowboxCookie('');
|
||||
setShowboxRegion('');
|
||||
await localScraperService.setScraperSettings('showboxog', {});
|
||||
}}
|
||||
>
|
||||
<Text style={styles.secondaryButtonText}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>ShowBox Cookie</Text>
|
||||
<TextInput
|
||||
style={[styles.textInput, { marginBottom: 12 }]}
|
||||
value={showboxCookie}
|
||||
onChangeText={setShowboxCookie}
|
||||
placeholder="Paste FebBox ui cookie value"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
multiline={true}
|
||||
numberOfLines={3}
|
||||
/>
|
||||
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>Region</Text>
|
||||
<View style={[styles.qualityChipsContainer, { marginBottom: 16 }]}>
|
||||
{regionOptions.map(opt => {
|
||||
const selected = showboxRegion === opt.value;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={opt.value}
|
||||
style={[styles.qualityChip, selected && styles.qualityChipSelected]}
|
||||
onPress={() => setShowboxRegion(opt.value)}
|
||||
>
|
||||
<Text style={[styles.qualityChipText, selected && styles.qualityChipTextSelected]}>
|
||||
{opt.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.primaryButton]}
|
||||
onPress={async () => {
|
||||
await localScraperService.setScraperSettings('showboxog', { cookie: showboxCookie, region: showboxRegion });
|
||||
Alert.alert('Saved', 'ShowBox settings updated');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.buttonText}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.secondaryButton]}
|
||||
onPress={async () => {
|
||||
setShowboxCookie('');
|
||||
setShowboxRegion('');
|
||||
await localScraperService.setScraperSettings('showboxog', {});
|
||||
}}
|
||||
>
|
||||
<Text style={styles.secondaryButtonText}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Additional Settings */}
|
||||
|
|
@ -1445,7 +1702,7 @@ const PluginsScreen: React.FC = () => {
|
|||
1. <Text style={{ fontWeight: '600' }}>Enable Local Scrapers</Text> - Turn on the main switch to allow plugins
|
||||
</Text>
|
||||
<Text style={styles.modalText}>
|
||||
2. <Text style={{ fontWeight: '600' }}>Set Repository URL</Text> - Enter a GitHub raw URL or use the default repository
|
||||
2. <Text style={{ fontWeight: '600' }}>Add Repository</Text> - Add a GitHub raw URL or use the default repository
|
||||
</Text>
|
||||
<Text style={styles.modalText}>
|
||||
3. <Text style={{ fontWeight: '600' }}>Refresh Repository</Text> - Download available scrapers from the repository
|
||||
|
|
@ -1462,6 +1719,85 @@ const PluginsScreen: React.FC = () => {
|
|||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* Add Repository Modal */}
|
||||
<Modal
|
||||
visible={showAddRepositoryModal}
|
||||
transparent={true}
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowAddRepositoryModal(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<Text style={styles.modalTitle}>Add New Repository</Text>
|
||||
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 8 }}>
|
||||
<Text style={styles.settingTitle}>Repository Name</Text>
|
||||
{fetchingRepoName && (
|
||||
<ActivityIndicator size="small" color={colors.primary} style={{ marginLeft: 8 }} />
|
||||
)}
|
||||
</View>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
value={newRepositoryName}
|
||||
onChangeText={setNewRepositoryName}
|
||||
placeholder="Enter repository name"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
autoCapitalize="words"
|
||||
/>
|
||||
|
||||
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>Repository URL</Text>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
value={newRepositoryUrl}
|
||||
onChangeText={handleUrlChange}
|
||||
placeholder="https://raw.githubusercontent.com/username/repo/branch/"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType="url"
|
||||
/>
|
||||
|
||||
<Text style={[styles.settingTitle, { marginBottom: 8 }]}>Description (Optional)</Text>
|
||||
<TextInput
|
||||
style={[styles.textInput, { height: 80 }]}
|
||||
value={newRepositoryDescription}
|
||||
onChangeText={setNewRepositoryDescription}
|
||||
placeholder="Enter repository description"
|
||||
placeholderTextColor={colors.mediumGray}
|
||||
multiline={true}
|
||||
numberOfLines={3}
|
||||
/>
|
||||
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.secondaryButton, { flex: 1 }]}
|
||||
onPress={() => {
|
||||
setShowAddRepositoryModal(false);
|
||||
setNewRepositoryUrl('');
|
||||
setNewRepositoryName('');
|
||||
setNewRepositoryDescription('');
|
||||
setFetchingRepoName(false);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.secondaryButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.primaryButton, { flex: 1 }]}
|
||||
onPress={handleAddRepository}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator size="small" color="#ffffff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Add Repository</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,18 @@ export interface ScraperInfo {
|
|||
// We support both `formats` and `supportedFormats` keys for manifest flexibility.
|
||||
formats?: string[];
|
||||
supportedFormats?: string[];
|
||||
repositoryId?: string; // Which repository this scraper came from
|
||||
}
|
||||
|
||||
export interface RepositoryInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
description?: string;
|
||||
isDefault?: boolean;
|
||||
enabled: boolean;
|
||||
lastUpdated?: number;
|
||||
scraperCount?: number;
|
||||
}
|
||||
|
||||
export interface LocalScraperResult {
|
||||
|
|
@ -55,12 +67,17 @@ class LocalScraperService {
|
|||
private static instance: LocalScraperService;
|
||||
private readonly STORAGE_KEY = 'local-scrapers';
|
||||
private readonly REPOSITORY_KEY = 'scraper-repository-url';
|
||||
private readonly REPOSITORIES_KEY = 'scraper-repositories';
|
||||
private readonly SCRAPER_SETTINGS_KEY = 'scraper-settings';
|
||||
private installedScrapers: Map<string, ScraperInfo> = new Map();
|
||||
private scraperCode: Map<string, string> = new Map();
|
||||
private repositories: Map<string, RepositoryInfo> = new Map();
|
||||
private currentRepositoryId: string = '';
|
||||
private repositoryUrl: string = '';
|
||||
private repositoryName: string = '';
|
||||
private initialized: boolean = false;
|
||||
private autoRefreshCompleted: boolean = false;
|
||||
private isRefreshing: boolean = false;
|
||||
private scraperSettingsCache: Record<string, any> | null = null;
|
||||
|
||||
private constructor() {
|
||||
|
|
@ -78,10 +95,43 @@ class LocalScraperService {
|
|||
if (this.initialized) return;
|
||||
|
||||
try {
|
||||
// Load repository URL
|
||||
const storedRepoUrl = await AsyncStorage.getItem(this.REPOSITORY_KEY);
|
||||
if (storedRepoUrl) {
|
||||
this.repositoryUrl = storedRepoUrl;
|
||||
// Load repositories
|
||||
const repositoriesData = await AsyncStorage.getItem(this.REPOSITORIES_KEY);
|
||||
if (repositoriesData) {
|
||||
const repos = JSON.parse(repositoriesData);
|
||||
this.repositories = new Map(Object.entries(repos));
|
||||
} else {
|
||||
// Migrate from old single repository format
|
||||
const storedRepoUrl = await AsyncStorage.getItem(this.REPOSITORY_KEY);
|
||||
if (storedRepoUrl) {
|
||||
const defaultRepo: RepositoryInfo = {
|
||||
id: 'default',
|
||||
name: this.extractRepositoryName(storedRepoUrl),
|
||||
url: storedRepoUrl,
|
||||
description: 'Default repository',
|
||||
isDefault: true,
|
||||
enabled: true,
|
||||
lastUpdated: Date.now()
|
||||
};
|
||||
this.repositories.set('default', defaultRepo);
|
||||
this.currentRepositoryId = 'default';
|
||||
await this.saveRepositories();
|
||||
}
|
||||
}
|
||||
|
||||
// Load current repository
|
||||
const currentRepoId = await AsyncStorage.getItem('current-repository-id');
|
||||
if (currentRepoId && this.repositories.has(currentRepoId)) {
|
||||
this.currentRepositoryId = currentRepoId;
|
||||
const currentRepo = this.repositories.get(currentRepoId)!;
|
||||
this.repositoryUrl = currentRepo.url;
|
||||
this.repositoryName = currentRepo.name;
|
||||
} else if (this.repositories.size > 0) {
|
||||
// Use first repository as default
|
||||
const firstRepo = Array.from(this.repositories.values())[0];
|
||||
this.currentRepositoryId = firstRepo.id;
|
||||
this.repositoryUrl = firstRepo.url;
|
||||
this.repositoryName = firstRepo.name;
|
||||
}
|
||||
|
||||
// Load installed scrapers
|
||||
|
|
@ -156,14 +206,16 @@ class LocalScraperService {
|
|||
// Load scraper code from cache
|
||||
await this.loadScraperCode();
|
||||
|
||||
// Auto-refresh repository on app startup if URL is configured
|
||||
if (this.repositoryUrl) {
|
||||
// Auto-refresh repository on app startup if URL is configured (only once)
|
||||
if (this.repositoryUrl && !this.autoRefreshCompleted) {
|
||||
try {
|
||||
logger.log('[LocalScraperService] Auto-refreshing repository on startup');
|
||||
await this.performRepositoryRefresh();
|
||||
this.autoRefreshCompleted = true;
|
||||
} catch (error) {
|
||||
logger.error('[LocalScraperService] Auto-refresh failed on startup:', error);
|
||||
// Don't fail initialization if auto-refresh fails
|
||||
this.autoRefreshCompleted = true; // Mark as completed even on error to prevent retries
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,6 +251,199 @@ class LocalScraperService {
|
|||
return this.repositoryName || 'Plugins';
|
||||
}
|
||||
|
||||
// Multiple repository management methods
|
||||
async getRepositories(): Promise<RepositoryInfo[]> {
|
||||
await this.ensureInitialized();
|
||||
return Array.from(this.repositories.values());
|
||||
}
|
||||
|
||||
async addRepository(repo: Omit<RepositoryInfo, 'id' | 'lastUpdated' | 'scraperCount'>): Promise<string> {
|
||||
await this.ensureInitialized();
|
||||
const id = `repo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Try to fetch the repository name from manifest if not provided
|
||||
let repositoryName = repo.name;
|
||||
if (!repositoryName || repositoryName.trim() === '') {
|
||||
try {
|
||||
repositoryName = await this.fetchRepositoryNameFromManifest(repo.url);
|
||||
} catch (error) {
|
||||
logger.warn('[LocalScraperService] Failed to fetch repository name from manifest, using fallback:', error);
|
||||
repositoryName = this.extractRepositoryName(repo.url);
|
||||
}
|
||||
}
|
||||
|
||||
const newRepo: RepositoryInfo = {
|
||||
...repo,
|
||||
name: repositoryName,
|
||||
id,
|
||||
lastUpdated: Date.now(),
|
||||
scraperCount: 0
|
||||
};
|
||||
this.repositories.set(id, newRepo);
|
||||
await this.saveRepositories();
|
||||
logger.log('[LocalScraperService] Added repository:', newRepo.name);
|
||||
return id;
|
||||
}
|
||||
|
||||
async updateRepository(id: string, updates: Partial<RepositoryInfo>): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
const repo = this.repositories.get(id);
|
||||
if (!repo) {
|
||||
throw new Error(`Repository with id ${id} not found`);
|
||||
}
|
||||
const updatedRepo = { ...repo, ...updates };
|
||||
this.repositories.set(id, updatedRepo);
|
||||
await this.saveRepositories();
|
||||
|
||||
// If this is the current repository, update current values
|
||||
if (id === this.currentRepositoryId) {
|
||||
this.repositoryUrl = updatedRepo.url;
|
||||
this.repositoryName = updatedRepo.name;
|
||||
}
|
||||
logger.log('[LocalScraperService] Updated repository:', updatedRepo.name);
|
||||
}
|
||||
|
||||
async removeRepository(id: string): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
if (!this.repositories.has(id)) {
|
||||
throw new Error(`Repository with id ${id} not found`);
|
||||
}
|
||||
|
||||
// Don't allow removing the last repository
|
||||
if (this.repositories.size <= 1) {
|
||||
throw new Error('Cannot remove the last repository');
|
||||
}
|
||||
|
||||
// If removing current repository, switch to another one
|
||||
if (id === this.currentRepositoryId) {
|
||||
const remainingRepos = Array.from(this.repositories.values()).filter(r => r.id !== id);
|
||||
if (remainingRepos.length > 0) {
|
||||
await this.setCurrentRepository(remainingRepos[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove scrapers from this repository
|
||||
const scrapersToRemove = Array.from(this.installedScrapers.values())
|
||||
.filter(s => s.repositoryId === id)
|
||||
.map(s => s.id);
|
||||
|
||||
for (const scraperId of scrapersToRemove) {
|
||||
this.installedScrapers.delete(scraperId);
|
||||
this.scraperCode.delete(scraperId);
|
||||
await AsyncStorage.removeItem(`scraper-code-${scraperId}`);
|
||||
}
|
||||
|
||||
this.repositories.delete(id);
|
||||
await this.saveRepositories();
|
||||
await this.saveInstalledScrapers();
|
||||
logger.log('[LocalScraperService] Removed repository:', id);
|
||||
}
|
||||
|
||||
async setCurrentRepository(id: string): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
const repo = this.repositories.get(id);
|
||||
if (!repo) {
|
||||
throw new Error(`Repository with id ${id} not found`);
|
||||
}
|
||||
|
||||
this.currentRepositoryId = id;
|
||||
this.repositoryUrl = repo.url;
|
||||
this.repositoryName = repo.name;
|
||||
|
||||
await AsyncStorage.setItem('current-repository-id', id);
|
||||
|
||||
// Refresh the repository to get its scrapers
|
||||
try {
|
||||
logger.log('[LocalScraperService] Refreshing repository after switch:', repo.name);
|
||||
await this.performRepositoryRefresh();
|
||||
} catch (error) {
|
||||
logger.error('[LocalScraperService] Failed to refresh repository after switch:', error);
|
||||
// Don't throw error, just log it - the switch should still succeed
|
||||
}
|
||||
|
||||
logger.log('[LocalScraperService] Switched to repository:', repo.name);
|
||||
}
|
||||
|
||||
getCurrentRepositoryId(): string {
|
||||
return this.currentRepositoryId;
|
||||
}
|
||||
|
||||
// Public method to extract repository name from URL
|
||||
extractRepositoryName(url: string): string {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const pathParts = urlObj.pathname.split('/').filter(part => part.length > 0);
|
||||
if (pathParts.length >= 2) {
|
||||
return `${pathParts[0]}/${pathParts[1]}`;
|
||||
}
|
||||
return urlObj.hostname || 'Unknown Repository';
|
||||
} catch {
|
||||
return 'Unknown Repository';
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch repository name from manifest.json
|
||||
async fetchRepositoryNameFromManifest(repositoryUrl: string): Promise<string> {
|
||||
try {
|
||||
logger.log('[LocalScraperService] Fetching repository name from manifest:', repositoryUrl);
|
||||
|
||||
// Construct manifest URL
|
||||
const baseManifestUrl = repositoryUrl.endsWith('/')
|
||||
? `${repositoryUrl}manifest.json`
|
||||
: `${repositoryUrl}/manifest.json`;
|
||||
const manifestUrl = `${baseManifestUrl}?t=${Date.now()}`;
|
||||
|
||||
const response = await axios.get(manifestUrl, {
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data && response.data.name) {
|
||||
logger.log('[LocalScraperService] Found repository name in manifest:', response.data.name);
|
||||
return response.data.name;
|
||||
} else {
|
||||
logger.warn('[LocalScraperService] No name found in manifest, using fallback');
|
||||
return this.extractRepositoryName(repositoryUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[LocalScraperService] Failed to fetch repository name from manifest:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Update repository name from manifest for existing repositories
|
||||
async refreshRepositoryNamesFromManifests(): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
for (const [id, repo] of this.repositories) {
|
||||
try {
|
||||
const manifestName = await this.fetchRepositoryNameFromManifest(repo.url);
|
||||
if (manifestName !== repo.name) {
|
||||
logger.log('[LocalScraperService] Updating repository name:', repo.name, '->', manifestName);
|
||||
repo.name = manifestName;
|
||||
|
||||
// If this is the current repository, update the current name
|
||||
if (id === this.currentRepositoryId) {
|
||||
this.repositoryName = manifestName;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[LocalScraperService] Failed to refresh name for repository:', repo.name, error);
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveRepositories();
|
||||
}
|
||||
|
||||
private async saveRepositories(): Promise<void> {
|
||||
const reposObject = Object.fromEntries(this.repositories);
|
||||
await AsyncStorage.setItem(this.REPOSITORIES_KEY, JSON.stringify(reposObject));
|
||||
}
|
||||
|
||||
|
||||
// Check if a scraper is compatible with the current platform
|
||||
private isPlatformCompatible(scraper: ScraperInfo): boolean {
|
||||
const currentPlatform = Platform.OS as 'ios' | 'android';
|
||||
|
|
@ -223,6 +468,7 @@ class LocalScraperService {
|
|||
async refreshRepository(): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
await this.performRepositoryRefresh();
|
||||
this.autoRefreshCompleted = true; // Mark as completed after manual refresh
|
||||
}
|
||||
|
||||
// Internal method to refresh repository without initialization check
|
||||
|
|
@ -231,6 +477,14 @@ class LocalScraperService {
|
|||
throw new Error('No repository URL configured');
|
||||
}
|
||||
|
||||
// Prevent multiple simultaneous refreshes
|
||||
if (this.isRefreshing) {
|
||||
logger.log('[LocalScraperService] Repository refresh already in progress, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
|
||||
try {
|
||||
logger.log('[LocalScraperService] Fetching repository manifest from:', this.repositoryUrl);
|
||||
|
||||
|
|
@ -285,8 +539,10 @@ class LocalScraperService {
|
|||
const isPlatformCompatible = this.isPlatformCompatible(scraperInfo);
|
||||
|
||||
if (isPlatformCompatible) {
|
||||
// Add repository ID to scraper info
|
||||
const scraperWithRepo = { ...scraperInfo, repositoryId: this.currentRepositoryId };
|
||||
// Download/update the scraper (downloadScraper handles force disabling based on manifest.enabled)
|
||||
await this.downloadScraper(scraperInfo);
|
||||
await this.downloadScraper(scraperWithRepo);
|
||||
} else {
|
||||
logger.log('[LocalScraperService] Skipping platform-incompatible scraper:', scraperInfo.name);
|
||||
// Remove if it was previously installed but is now platform-incompatible
|
||||
|
|
@ -300,11 +556,25 @@ class LocalScraperService {
|
|||
}
|
||||
|
||||
await this.saveInstalledScrapers();
|
||||
|
||||
// Update repository info
|
||||
const currentRepo = this.repositories.get(this.currentRepositoryId);
|
||||
if (currentRepo) {
|
||||
const scraperCount = Array.from(this.installedScrapers.values())
|
||||
.filter(s => s.repositoryId === this.currentRepositoryId).length;
|
||||
await this.updateRepository(this.currentRepositoryId, {
|
||||
lastUpdated: Date.now(),
|
||||
scraperCount
|
||||
});
|
||||
}
|
||||
|
||||
logger.log('[LocalScraperService] Repository refresh completed');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[LocalScraperService] Failed to refresh repository:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue