updated tablet ui for plugin test screen

This commit is contained in:
tapframe 2026-01-08 16:15:15 +05:30
parent bb94a49662
commit 1fdcdd02bf
4 changed files with 163 additions and 134 deletions

View file

@ -389,6 +389,14 @@ const SettingsScreen: React.FC = () => {
renderControl={() => <ChevronRight />} renderControl={() => <ChevronRight />}
isTablet={isTablet} isTablet={isTablet}
/> />
<SettingItem
title={'Plugin Tester'}
description={'Run a plugin and inspect logs/streams'}
icon="terminal"
onPress={() => navigation.navigate('PluginTester')}
renderControl={() => <ChevronRight />}
isTablet={isTablet}
/>
<SettingItem <SettingItem
title={t('settings.items.reset_onboarding')} title={t('settings.items.reset_onboarding')}
icon="refresh-ccw" icon="refresh-ccw"

View file

@ -193,8 +193,9 @@ export const IndividualTester = ({ onSwitchTab }: IndividualTesterProps) => {
<View style={styles.largeScreenWrapper}> <View style={styles.largeScreenWrapper}>
<View style={styles.twoColumnContainer}> <View style={styles.twoColumnContainer}>
<View style={styles.leftColumn}> <View style={styles.leftColumn}>
<ScrollView style={styles.content} contentContainerStyle={{ paddingBottom: 10 }} keyboardShouldPersistTaps="handled"> <View style={{ flex: 1 }}>
<View style={styles.card}> <ScrollView style={styles.content} contentContainerStyle={{ paddingBottom: 10 }} keyboardShouldPersistTaps="handled">
<View style={styles.card}>
<View style={styles.cardTitleRow}> <View style={styles.cardTitleRow}>
<Text style={styles.cardTitle}>{t('plugin_tester.individual.load_from_url')}</Text> <Text style={styles.cardTitle}>{t('plugin_tester.individual.load_from_url')}</Text>
<Ionicons name="link-outline" size={18} color={currentTheme.colors.mediumEmphasis} /> <Ionicons name="link-outline" size={18} color={currentTheme.colors.mediumEmphasis} />
@ -219,9 +220,9 @@ export const IndividualTester = ({ onSwitchTab }: IndividualTesterProps) => {
<Ionicons name="download-outline" size={20} color={currentTheme.colors.white} /> <Ionicons name="download-outline" size={20} color={currentTheme.colors.white} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
<View style={[styles.card, { flex: 1, minHeight: 400 }]}> <View style={[styles.card, { flex: 1, minHeight: 400 }]}>
<View style={styles.cardTitleRow}> <View style={styles.cardTitleRow}>
<Text style={styles.cardTitle}>{t('plugin_tester.individual.plugin_code')}</Text> <Text style={styles.cardTitle}>{t('plugin_tester.individual.plugin_code')}</Text>
<View style={styles.cardActionsRow}> <View style={styles.cardActionsRow}>
@ -245,69 +246,72 @@ export const IndividualTester = ({ onSwitchTab }: IndividualTesterProps) => {
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
/> />
</View>
{/* Test parameters on large screen */}
<View style={styles.card}>
<View style={styles.cardTitleRow}>
<Text style={styles.cardTitle}>{t('plugin_tester.individual.test_parameters')}</Text>
<Ionicons name="options-outline" size={16} color={currentTheme.colors.mediumEmphasis} />
</View> </View>
</ScrollView>
<View style={styles.segment}> {/* Sticky footer on large screens (match mobile behavior) */}
<TouchableOpacity <View style={[styles.stickyFooter, { paddingBottom: Math.max(insets.bottom, 14) }]}>
style={[styles.segmentItem, mediaType === 'movie' && styles.segmentItemActive]} <View style={styles.footerCard}>
onPress={() => setMediaType('movie')} <View style={styles.footerTitleRow}>
> <Text style={styles.footerTitle}>{t('plugin_tester.individual.test_parameters')}</Text>
<Ionicons name="film-outline" size={18} color={mediaType === 'movie' ? currentTheme.colors.primary : currentTheme.colors.highEmphasis} /> <Ionicons name="options-outline" size={16} color={currentTheme.colors.mediumEmphasis} />
<Text style={[styles.segmentText, mediaType === 'movie' && styles.segmentTextActive]}>{t('plugin_tester.common.movie')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.segmentItem, mediaType === 'tv' && styles.segmentItemActive]}
onPress={() => setMediaType('tv')}
>
<Ionicons name="tv-outline" size={18} color={mediaType === 'tv' ? currentTheme.colors.primary : currentTheme.colors.highEmphasis} />
<Text style={[styles.segmentText, mediaType === 'tv' && styles.segmentTextActive]}>{t('plugin_tester.common.tv')}</Text>
</TouchableOpacity>
</View>
<View style={[styles.row, { marginTop: 10, alignItems: 'flex-start' }]}>
<View style={{ flex: 1 }}>
<Text style={styles.fieldLabel}>{t('plugin_tester.common.tmdb_id')}</Text>
<TextInput
style={styles.input}
value={tmdbId}
onChangeText={setTmdbId}
keyboardType="numeric"
/>
</View> </View>
{mediaType === 'tv' && ( <View style={styles.segment}>
<> <TouchableOpacity
<View style={{ width: 110 }}> style={[styles.segmentItem, mediaType === 'movie' && styles.segmentItemActive]}
<Text style={styles.fieldLabel}>{t('plugin_tester.common.season')}</Text> onPress={() => setMediaType('movie')}
<TextInput >
style={styles.input} <Ionicons name="film-outline" size={18} color={mediaType === 'movie' ? currentTheme.colors.primary : currentTheme.colors.highEmphasis} />
value={season} <Text style={[styles.segmentText, mediaType === 'movie' && styles.segmentTextActive]}>{t('plugin_tester.common.movie')}</Text>
onChangeText={setSeason} </TouchableOpacity>
keyboardType="numeric" <TouchableOpacity
/> style={[styles.segmentItem, mediaType === 'tv' && styles.segmentItemActive]}
</View> onPress={() => setMediaType('tv')}
<View style={{ width: 110 }}> >
<Text style={styles.fieldLabel}>{t('plugin_tester.common.episode')}</Text> <Ionicons name="tv-outline" size={18} color={mediaType === 'tv' ? currentTheme.colors.primary : currentTheme.colors.highEmphasis} />
<TextInput <Text style={[styles.segmentText, mediaType === 'tv' && styles.segmentTextActive]}>{t('plugin_tester.common.tv')}</Text>
style={styles.input} </TouchableOpacity>
value={episode} </View>
onChangeText={setEpisode}
keyboardType="numeric" <View style={[styles.row, { marginTop: 10, alignItems: 'flex-start' }]}>
/> <View style={{ flex: 1 }}>
</View> <Text style={styles.fieldLabel}>{t('plugin_tester.common.tmdb_id')}</Text>
</> <TextInput
)} style={styles.input}
value={tmdbId}
onChangeText={setTmdbId}
keyboardType="numeric"
/>
</View>
{mediaType === 'tv' && (
<>
<View style={{ width: 110 }}>
<Text style={styles.fieldLabel}>{t('plugin_tester.common.season')}</Text>
<TextInput
style={styles.input}
value={season}
onChangeText={setSeason}
keyboardType="numeric"
/>
</View>
<View style={{ width: 110 }}>
<Text style={styles.fieldLabel}>{t('plugin_tester.common.episode')}</Text>
<TextInput
style={styles.input}
value={episode}
onChangeText={setEpisode}
keyboardType="numeric"
/>
</View>
</>
)}
</View>
</View> </View>
<TouchableOpacity <TouchableOpacity
style={[styles.button, { marginTop: 12, opacity: isRunning ? 0.85 : 1 }]} style={[styles.button, { opacity: isRunning ? 0.85 : 1 }]}
onPress={runTest} onPress={runTest}
disabled={isRunning} disabled={isRunning}
> >
@ -319,7 +323,7 @@ export const IndividualTester = ({ onSwitchTab }: IndividualTesterProps) => {
<Text style={styles.buttonText}>{isRunning ? t('plugin_tester.common.running') : t('plugin_tester.common.run_test')}</Text> <Text style={styles.buttonText}>{isRunning ? t('plugin_tester.common.running') : t('plugin_tester.common.run_test')}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</ScrollView> </View>
</View> </View>
<View style={styles.rightColumn}> <View style={styles.rightColumn}>
@ -580,71 +584,79 @@ export const IndividualTester = ({ onSwitchTab }: IndividualTesterProps) => {
} as any); } as any);
}; };
const renderResultsTab = () => ( const renderResultsTab = () => {
<ScrollView style={styles.content} contentContainerStyle={{ paddingBottom: 40 }}> if (streams.length === 0) {
{streams.length === 0 ? ( return (
<View style={styles.emptyState}> <ScrollView style={styles.content}>
<Ionicons name="list-outline" size={48} color={currentTheme.colors.mediumGray} /> <View style={styles.emptyState}>
<Text style={styles.emptyText}>{t('plugin_tester.individual.no_streams')}</Text> <Ionicons name="list-outline" size={48} color={currentTheme.colors.mediumGray} />
</View> <Text style={styles.emptyText}>{t('plugin_tester.individual.no_streams')}</Text>
) : ( </View>
<View style={styles.listContainer}> </ScrollView>
<Text style={styles.sectionHeader}>{streams.length === 1 ? t('plugin_tester.individual.streams_found', { count: streams.length }) : t('plugin_tester.individual.streams_found_plural', { count: streams.length })}</Text> );
<Text style={styles.sectionSubHeader}>{t('plugin_tester.individual.tap_play_hint')}</Text> }
<FlatList
data={streams}
keyExtractor={(item, index) => item.url + index}
renderItem={({ item: stream }) => (
<TouchableOpacity
style={styles.resultItem}
onPress={() => playStream(stream)}
activeOpacity={0.7}
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<View style={styles.streamInfo}>
<Text style={styles.streamName}>{stream.name || stream.title || t('plugin_tester.individual.unnamed_stream')}</Text>
<Text style={styles.streamMeta}>{t('plugin_tester.individual.quality', { quality: stream.quality || 'Unknown' })}</Text>
{stream.description ? <Text style={styles.streamMeta}>{t('plugin_tester.individual.size', { size: stream.description })}</Text> : null}
<Text style={styles.streamMeta} numberOfLines={1}>{t('plugin_tester.individual.url_label', { url: stream.url })}</Text>
{stream.headers && Object.keys(stream.headers).length > 0 && (
<Text style={styles.streamMeta}>{t('plugin_tester.individual.headers_info', { count: Object.keys(stream.headers).length })}</Text>
)}
</View>
<TouchableOpacity
style={styles.playButton}
onPress={() => playStream(stream)}
>
<Ionicons name="play" size={16} color={currentTheme.colors.white} />
<Text style={styles.playButtonText}>{t('plugin_tester.common.play')}</Text>
</TouchableOpacity>
</View>
<Text return (
style={[ <FlatList
styles.logItem, style={styles.content}
{ contentContainerStyle={{ paddingBottom: 40 }}
marginTop: 10, data={streams}
marginBottom: 0, keyExtractor={(item, index) => item.url + index}
color: currentTheme.colors.highEmphasis, ListHeaderComponent={
}, <View style={{ paddingHorizontal: 16, paddingTop: 12, paddingBottom: 8 }}>
]} <Text style={styles.sectionHeader}>{streams.length === 1 ? t('plugin_tester.individual.streams_found', { count: streams.length }) : t('plugin_tester.individual.streams_found_plural', { count: streams.length })}</Text>
selectable <Text style={styles.sectionSubHeader}>{t('plugin_tester.individual.tap_play_hint')}</Text>
> </View>
{(() => { }
try { renderItem={({ item: stream }) => (
return JSON.stringify(stream, null, 2); <TouchableOpacity
} catch { style={[styles.resultItem, { marginHorizontal: 16, marginBottom: 8 }]}
return String(stream); onPress={() => playStream(stream)}
} activeOpacity={0.7}
})()} >
</Text> <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<View style={styles.streamInfo}>
<Text style={styles.streamName}>{stream.name || stream.title || t('plugin_tester.individual.unnamed_stream')}</Text>
<Text style={styles.streamMeta}>{t('plugin_tester.individual.quality', { quality: stream.quality || 'Unknown' })}</Text>
{stream.description ? <Text style={styles.streamMeta}>{t('plugin_tester.individual.size', { size: stream.description })}</Text> : null}
<Text style={styles.streamMeta} numberOfLines={1}>{t('plugin_tester.individual.url_label', { url: stream.url })}</Text>
{stream.headers && Object.keys(stream.headers).length > 0 && (
<Text style={styles.streamMeta}>{t('plugin_tester.individual.headers_info', { count: Object.keys(stream.headers).length })}</Text>
)}
</View>
<TouchableOpacity
style={styles.playButton}
onPress={() => playStream(stream)}
>
<Ionicons name="play" size={16} color={currentTheme.colors.white} />
<Text style={styles.playButtonText}>{t('plugin_tester.common.play')}</Text>
</TouchableOpacity> </TouchableOpacity>
)} </View>
/>
</View> <Text
)} style={[
</ScrollView> styles.logItem,
); {
marginTop: 10,
marginBottom: 0,
color: currentTheme.colors.highEmphasis,
},
]}
selectable
>
{(() => {
try {
return JSON.stringify(stream, null, 2);
} catch {
return String(stream);
}
})()}
</Text>
</TouchableOpacity>
)}
/>
);
};
const renderFocusedEditor = () => ( const renderFocusedEditor = () => (
<KeyboardAvoidingView <KeyboardAvoidingView

View file

@ -363,8 +363,9 @@ export const RepoTester = () => {
behavior={Platform.OS === 'ios' ? 'padding' : undefined} behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={Platform.OS === 'ios' ? 60 : 0} keyboardVerticalOffset={Platform.OS === 'ios' ? 60 : 0}
> >
<ScrollView style={styles.content} contentContainerStyle={{ paddingBottom: 20 }} keyboardShouldPersistTaps="handled"> <View style={isLargeScreen ? styles.largeScreenWrapper : { flex: 1 }}>
<View style={styles.card}> <ScrollView style={styles.content} contentContainerStyle={{ paddingBottom: 20 }} keyboardShouldPersistTaps="handled">
<View style={styles.card}>
<View style={styles.cardTitleRow}> <View style={styles.cardTitleRow}>
<Text style={styles.cardTitle}>{t('plugin_tester.repo.title')}</Text> <Text style={styles.cardTitle}>{t('plugin_tester.repo.title')}</Text>
<Ionicons name="git-branch-outline" size={18} color={currentTheme.colors.mediumEmphasis} /> <Ionicons name="git-branch-outline" size={18} color={currentTheme.colors.mediumEmphasis} />
@ -409,7 +410,7 @@ export const RepoTester = () => {
)} )}
</View> </View>
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardTitleRow}> <View style={styles.cardTitleRow}>
<Text style={styles.cardTitle}>{t('plugin_tester.repo.test_parameters')}</Text> <Text style={styles.cardTitle}>{t('plugin_tester.repo.test_parameters')}</Text>
<Ionicons name="options-outline" size={18} color={currentTheme.colors.mediumEmphasis} /> <Ionicons name="options-outline" size={18} color={currentTheme.colors.mediumEmphasis} />
@ -475,7 +476,7 @@ export const RepoTester = () => {
</Text> </Text>
</View> </View>
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardTitleRow}> <View style={styles.cardTitleRow}>
<Text style={styles.cardTitle}>{t('plugin_tester.repo.providers_title')}</Text> <Text style={styles.cardTitle}>{t('plugin_tester.repo.providers_title')}</Text>
<Ionicons name="list-outline" size={18} color={currentTheme.colors.mediumEmphasis} /> <Ionicons name="list-outline" size={18} color={currentTheme.colors.mediumEmphasis} />
@ -622,8 +623,9 @@ export const RepoTester = () => {
</View> </View>
); );
})} })}
</View> </View>
</ScrollView> </ScrollView>
</View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
); );
}; };

View file

@ -1,7 +1,9 @@
import { StyleSheet, Platform, useWindowDimensions } from 'react-native'; import { StyleSheet, Platform, useWindowDimensions } from 'react-native';
// Breakpoint for larger screens (tablets, iPads) // Breakpoint for the two-column "large screen" layout.
export const LARGE_SCREEN_BREAKPOINT = 768; // 768px wide tablets in portrait are usually too narrow for side-by-side columns,
// so we enable the large layout only on wider screens (e.g., tablet landscape).
export const LARGE_SCREEN_BREAKPOINT = 900;
export const useIsLargeScreen = () => { export const useIsLargeScreen = () => {
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
@ -16,13 +18,16 @@ export const getPluginTesterStyles = (theme: any, isLargeScreen: boolean = false
// Large screen wrapper for centering content // Large screen wrapper for centering content
largeScreenWrapper: { largeScreenWrapper: {
flex: 1, flex: 1,
maxWidth: isLargeScreen ? 900 : undefined, // Allow tablet/desktop to use more horizontal space while still
// keeping content comfortably contained.
maxWidth: isLargeScreen ? 1200 : undefined,
alignSelf: isLargeScreen ? 'center' : undefined, alignSelf: isLargeScreen ? 'center' : undefined,
width: isLargeScreen ? '100%' : undefined, width: isLargeScreen ? '100%' : undefined,
paddingHorizontal: isLargeScreen ? 24 : 0, paddingHorizontal: isLargeScreen ? 24 : 0,
}, },
// Two-column layout for large screens // Two-column layout for large screens
twoColumnContainer: { twoColumnContainer: {
flex: isLargeScreen ? 1 : undefined,
flexDirection: isLargeScreen ? 'row' : 'column', flexDirection: isLargeScreen ? 'row' : 'column',
gap: isLargeScreen ? 16 : 0, gap: isLargeScreen ? 16 : 0,
}, },
@ -94,7 +99,9 @@ export const getPluginTesterStyles = (theme: any, isLargeScreen: boolean = false
}, },
content: { content: {
flex: 1, flex: 1,
paddingHorizontal: 16, // On large screens the wrapper already adds horizontal padding.
// Avoid "double padding" that makes columns feel cramped.
paddingHorizontal: isLargeScreen ? 0 : 16,
paddingTop: 12, paddingTop: 12,
}, },
card: { card: {