remove unecessary blur on adnroid
This commit is contained in:
parent
9a446f5a45
commit
da4e4031bf
5 changed files with 32 additions and 290 deletions
|
|
@ -179,17 +179,7 @@ const ActionButtons = React.memo(({
|
|||
{Platform.OS === 'ios' ? (
|
||||
<ExpoBlurView intensity={80} style={styles.blurBackground} tint="dark" />
|
||||
) : (
|
||||
Constants.executionEnvironment === ExecutionEnvironment.StoreClient ? (
|
||||
<View style={styles.androidFallbackBlur} />
|
||||
) : (
|
||||
<CommunityBlurView
|
||||
style={styles.blurBackground}
|
||||
blurType="dark"
|
||||
blurAmount={8}
|
||||
overlayColor="rgba(255,255,255,0.1)"
|
||||
reducedTransparencyFallbackColor="rgba(255,255,255,0.15)"
|
||||
/>
|
||||
)
|
||||
<View style={styles.androidFallbackBlur} />
|
||||
)}
|
||||
<MaterialIcons
|
||||
name={inLibrary ? 'bookmark' : 'bookmark-border'}
|
||||
|
|
@ -210,17 +200,7 @@ const ActionButtons = React.memo(({
|
|||
{Platform.OS === 'ios' ? (
|
||||
<ExpoBlurView intensity={80} style={styles.blurBackgroundRound} tint="dark" />
|
||||
) : (
|
||||
Constants.executionEnvironment === ExecutionEnvironment.StoreClient ? (
|
||||
<View style={styles.androidFallbackBlurRound} />
|
||||
) : (
|
||||
<CommunityBlurView
|
||||
style={styles.blurBackgroundRound}
|
||||
blurType="dark"
|
||||
blurAmount={8}
|
||||
overlayColor="rgba(255,255,255,0.1)"
|
||||
reducedTransparencyFallbackColor="rgba(255,255,255,0.15)"
|
||||
/>
|
||||
)
|
||||
<View style={styles.androidFallbackBlurRound} />
|
||||
)}
|
||||
<MaterialIcons
|
||||
name="assessment"
|
||||
|
|
|
|||
|
|
@ -157,6 +157,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const isMounted = useRef(true);
|
||||
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
||||
// Offset in seconds to avoid seeking to the exact end, which fires onEnd and resets.
|
||||
const END_EPSILON = 0.3;
|
||||
|
||||
const hideControls = () => {
|
||||
Animated.timing(fadeAnim, {
|
||||
|
|
@ -355,7 +357,9 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
};
|
||||
}, [id, type, currentTime, duration]);
|
||||
|
||||
const seekToTime = (timeInSeconds: number) => {
|
||||
const seekToTime = (rawSeconds: number) => {
|
||||
// Clamp to just before the end of the media.
|
||||
const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds));
|
||||
if (videoRef.current && duration > 0 && !isSeeking.current) {
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[AndroidVideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`);
|
||||
|
|
@ -415,8 +419,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
|
||||
const processProgressTouch = (locationX: number, isDragging = false) => {
|
||||
progressBarRef.current?.measure((x, y, width, height, pageX, pageY) => {
|
||||
const percentage = Math.max(0, Math.min(locationX / width, 1));
|
||||
const seekTime = percentage * duration;
|
||||
const percentage = Math.max(0, Math.min(locationX / width, 0.999));
|
||||
const seekTime = Math.min(percentage * duration, duration - END_EPSILON);
|
||||
progressAnim.setValue(percentage);
|
||||
if (isDragging) {
|
||||
pendingSeekValue.current = seekTime;
|
||||
|
|
@ -519,7 +523,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
|
||||
const skip = (seconds: number) => {
|
||||
if (videoRef.current) {
|
||||
const newTime = Math.max(0, Math.min(currentTime + seconds, duration));
|
||||
const newTime = Math.max(0, Math.min(currentTime + seconds, duration - END_EPSILON));
|
||||
seekToTime(newTime);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -152,6 +152,9 @@ const VideoPlayer: React.FC = () => {
|
|||
const isMounted = useRef(true);
|
||||
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
||||
// Small offset (in seconds) used to avoid seeking to the *exact* end of the
|
||||
// file which triggers the `onEnd` callback and causes playback to restart.
|
||||
const END_EPSILON = 0.3;
|
||||
|
||||
const hideControls = () => {
|
||||
Animated.timing(fadeAnim, {
|
||||
|
|
@ -371,7 +374,9 @@ const VideoPlayer: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const seekToTime = (timeInSeconds: number) => {
|
||||
const seekToTime = (rawSeconds: number) => {
|
||||
// Clamp to just before the end to avoid triggering onEnd.
|
||||
const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds));
|
||||
if (vlcRef.current && duration > 0 && !isSeeking.current) {
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[VideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`);
|
||||
|
|
@ -443,8 +448,8 @@ const VideoPlayer: React.FC = () => {
|
|||
|
||||
const processProgressTouch = (locationX: number, isDragging = false) => {
|
||||
progressBarRef.current?.measure((x, y, width, height, pageX, pageY) => {
|
||||
const percentage = Math.max(0, Math.min(locationX / width, 1));
|
||||
const seekTime = percentage * duration;
|
||||
const percentage = Math.max(0, Math.min(locationX / width, 0.999));
|
||||
const seekTime = Math.min(percentage * duration, duration - END_EPSILON);
|
||||
progressAnim.setValue(percentage);
|
||||
if (isDragging) {
|
||||
pendingSeekValue.current = seekTime;
|
||||
|
|
@ -530,7 +535,7 @@ const VideoPlayer: React.FC = () => {
|
|||
|
||||
const skip = (seconds: number) => {
|
||||
if (vlcRef.current) {
|
||||
const newTime = Math.max(0, Math.min(currentTime + seconds, duration));
|
||||
const newTime = Math.max(0, Math.min(currentTime + seconds, duration - END_EPSILON));
|
||||
seekToTime(newTime);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -462,70 +462,6 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
padding: 6,
|
||||
marginRight: 8,
|
||||
},
|
||||
communityAddonsList: {
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
communityAddonItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 8,
|
||||
padding: 15,
|
||||
marginBottom: 10,
|
||||
},
|
||||
communityAddonIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 6,
|
||||
marginRight: 15,
|
||||
},
|
||||
communityAddonIconPlaceholder: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 6,
|
||||
marginRight: 15,
|
||||
backgroundColor: colors.darkGray,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
communityAddonDetails: {
|
||||
flex: 1,
|
||||
marginRight: 10,
|
||||
},
|
||||
communityAddonName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.white,
|
||||
marginBottom: 3,
|
||||
},
|
||||
communityAddonDesc: {
|
||||
fontSize: 13,
|
||||
color: colors.lightGray,
|
||||
marginBottom: 5,
|
||||
opacity: 0.9,
|
||||
},
|
||||
communityAddonMetaContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
opacity: 0.8,
|
||||
},
|
||||
communityAddonVersion: {
|
||||
fontSize: 12,
|
||||
color: colors.lightGray,
|
||||
},
|
||||
communityAddonDot: {
|
||||
fontSize: 12,
|
||||
color: colors.lightGray,
|
||||
marginHorizontal: 5,
|
||||
},
|
||||
communityAddonCategory: {
|
||||
fontSize: 12,
|
||||
color: colors.lightGray,
|
||||
flexShrink: 1,
|
||||
},
|
||||
separator: {
|
||||
height: 10,
|
||||
},
|
||||
sectionSeparator: {
|
||||
height: 1,
|
||||
backgroundColor: colors.border,
|
||||
|
|
@ -560,7 +496,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
androidBlurContainer: {
|
||||
position: 'absolute',
|
||||
|
|
@ -618,14 +554,8 @@ const AddonsScreen = () => {
|
|||
const colors = currentTheme.colors;
|
||||
const styles = createStyles(colors);
|
||||
|
||||
// State for community addons
|
||||
const [communityAddons, setCommunityAddons] = useState<CommunityAddon[]>([]);
|
||||
const [communityLoading, setCommunityLoading] = useState(true);
|
||||
const [communityError, setCommunityError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadAddons();
|
||||
loadCommunityAddons();
|
||||
}, []);
|
||||
|
||||
const loadAddons = async () => {
|
||||
|
|
@ -662,34 +592,10 @@ const AddonsScreen = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Function to load community addons
|
||||
const loadCommunityAddons = async () => {
|
||||
setCommunityLoading(true);
|
||||
setCommunityError(null);
|
||||
try {
|
||||
const response = await axios.get<CommunityAddon[]>('https://stremio-addons.com/catalog.json');
|
||||
// Filter out addons without a manifest or transportUrl (basic validation)
|
||||
let validAddons = response.data.filter(addon => addon.manifest && addon.transportUrl);
|
||||
|
||||
// Filter out Cinemeta if it's already in the community list to avoid duplication
|
||||
validAddons = validAddons.filter(addon => addon.manifest.id !== 'com.linvo.cinemeta');
|
||||
|
||||
// Add Cinemeta to the beginning of the list
|
||||
setCommunityAddons([cinemetaAddon, ...validAddons]);
|
||||
} catch (error) {
|
||||
logger.error('Failed to load community addons:', error);
|
||||
setCommunityError('Failed to load community addons. Please try again later.');
|
||||
// Still show Cinemeta if the community list fails to load
|
||||
setCommunityAddons([cinemetaAddon]);
|
||||
} finally {
|
||||
setCommunityLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAddon = async (url?: string) => {
|
||||
const urlToInstall = url || addonUrl;
|
||||
if (!urlToInstall) {
|
||||
Alert.alert('Error', 'Please enter an addon URL or select a community addon');
|
||||
Alert.alert('Error', 'Please enter an addon URL');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -728,7 +634,6 @@ const AddonsScreen = () => {
|
|||
|
||||
const refreshAddons = async () => {
|
||||
loadAddons();
|
||||
loadCommunityAddons();
|
||||
};
|
||||
|
||||
const moveAddonUp = (addon: ExtendedManifest) => {
|
||||
|
|
@ -991,66 +896,6 @@ const AddonsScreen = () => {
|
|||
);
|
||||
};
|
||||
|
||||
// Function to render community addon items
|
||||
const renderCommunityAddonItem = ({ item }: { item: CommunityAddon }) => {
|
||||
const { manifest, transportUrl } = item;
|
||||
const types = manifest.types || [];
|
||||
const description = manifest.description || 'No description provided.';
|
||||
// @ts-ignore - logo might exist
|
||||
const logo = manifest.logo || null;
|
||||
const categoryText = types.length > 0
|
||||
? types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
|
||||
: 'General';
|
||||
// Check if addon is configurable
|
||||
const isConfigurable = manifest.behaviorHints?.configurable === true;
|
||||
|
||||
return (
|
||||
<View style={styles.communityAddonItem}>
|
||||
{logo ? (
|
||||
<ExpoImage
|
||||
source={{ uri: logo }}
|
||||
style={styles.communityAddonIcon}
|
||||
contentFit="contain"
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.communityAddonIconPlaceholder}>
|
||||
<MaterialIcons name="extension" size={22} color={colors.darkGray} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.communityAddonDetails}>
|
||||
<Text style={styles.communityAddonName}>{manifest.name}</Text>
|
||||
<Text style={styles.communityAddonDesc} numberOfLines={2}>{description}</Text>
|
||||
<View style={styles.communityAddonMetaContainer}>
|
||||
<Text style={styles.communityAddonVersion}>v{manifest.version || 'N/A'}</Text>
|
||||
<Text style={styles.communityAddonDot}>•</Text>
|
||||
<Text style={styles.communityAddonCategory}>{categoryText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.addonActionButtons}>
|
||||
{isConfigurable && (
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(manifest, transportUrl)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
||||
onPress={() => handleAddAddon(transportUrl)}
|
||||
disabled={installing}
|
||||
>
|
||||
{installing ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
) : (
|
||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const StatsCard = ({ value, label }: { value: number; label: string }) => (
|
||||
<View style={styles.statsCard}>
|
||||
<Text style={styles.statsValue}>{value}</Text>
|
||||
|
|
@ -1186,95 +1031,6 @@ const AddonsScreen = () => {
|
|||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Separator */}
|
||||
<View style={styles.sectionSeparator} />
|
||||
|
||||
{/* Community Addons Section */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>COMMUNITY ADDONS</Text>
|
||||
<View style={styles.addonList}>
|
||||
{communityLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
</View>
|
||||
) : communityError ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons name="error-outline" size={32} color={colors.error} />
|
||||
<Text style={styles.emptyText}>{communityError}</Text>
|
||||
</View>
|
||||
) : communityAddons.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons name="extension-off" size={32} color={colors.mediumGray} />
|
||||
<Text style={styles.emptyText}>No community addons available</Text>
|
||||
</View>
|
||||
) : (
|
||||
communityAddons.map((item, index) => (
|
||||
<View
|
||||
key={item.transportUrl}
|
||||
style={{ marginBottom: index === communityAddons.length - 1 ? 32 : 16 }}
|
||||
>
|
||||
<View style={styles.addonItem}>
|
||||
<View style={styles.addonHeader}>
|
||||
{item.manifest.logo ? (
|
||||
<ExpoImage
|
||||
source={{ uri: item.manifest.logo }}
|
||||
style={styles.addonIcon}
|
||||
contentFit="contain"
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.addonIconPlaceholder}>
|
||||
<MaterialIcons name="extension" size={22} color={colors.mediumGray} />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.addonTitleContainer}>
|
||||
<Text style={styles.addonName}>{item.manifest.name}</Text>
|
||||
<View style={styles.addonMetaContainer}>
|
||||
<Text style={styles.addonVersion}>v{item.manifest.version || 'N/A'}</Text>
|
||||
<Text style={styles.addonDot}>•</Text>
|
||||
<Text style={styles.addonCategory}>
|
||||
{item.manifest.types && item.manifest.types.length > 0
|
||||
? item.manifest.types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
|
||||
: 'General'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.addonActions}>
|
||||
{item.manifest.behaviorHints?.configurable && (
|
||||
<TouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(item.manifest, item.transportUrl)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
||||
onPress={() => handleAddAddon(item.transportUrl)}
|
||||
disabled={installing}
|
||||
>
|
||||
{installing ? (
|
||||
<ActivityIndicator size="small" color={colors.white} />
|
||||
) : (
|
||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.addonDescription}>
|
||||
{item.manifest.description
|
||||
? (item.manifest.description.length > 100
|
||||
? item.manifest.description.substring(0, 100) + '...'
|
||||
: item.manifest.description)
|
||||
: 'No description provided.'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import { useTheme } from '../contexts/ThemeContext';
|
|||
import { catalogService } from '../services/catalogService';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import * as Sentry from '@sentry/react-native';
|
||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -39,15 +38,13 @@ const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
|||
interface SettingsCardProps {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
const SettingsCard: React.FC<SettingsCardProps> = ({ children, title, delay = 0 }) => {
|
||||
const SettingsCard: React.FC<SettingsCardProps> = ({ children, title }) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeInDown.delay(delay).duration(400)}
|
||||
<View
|
||||
style={[styles.cardContainer]}
|
||||
>
|
||||
{title && (
|
||||
|
|
@ -64,7 +61,7 @@ const SettingsCard: React.FC<SettingsCardProps> = ({ children, title, delay = 0
|
|||
]}>
|
||||
{children}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -297,7 +294,7 @@ const SettingsScreen: React.FC = () => {
|
|||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
{/* Account Section */}
|
||||
<SettingsCard title="ACCOUNT" delay={0}>
|
||||
<SettingsCard title="ACCOUNT">
|
||||
<SettingItem
|
||||
title="Trakt"
|
||||
description={isAuthenticated ? `@${userProfile?.username || 'User'}` : "Sign in to sync"}
|
||||
|
|
@ -318,7 +315,7 @@ const SettingsScreen: React.FC = () => {
|
|||
</SettingsCard>
|
||||
|
||||
{/* Content & Discovery */}
|
||||
<SettingsCard title="CONTENT & DISCOVERY" delay={100}>
|
||||
<SettingsCard title="CONTENT & DISCOVERY">
|
||||
<SettingItem
|
||||
title="Addons"
|
||||
description={`${addonCount} installed`}
|
||||
|
|
@ -344,7 +341,7 @@ const SettingsScreen: React.FC = () => {
|
|||
</SettingsCard>
|
||||
|
||||
{/* Appearance & Interface */}
|
||||
<SettingsCard title="APPEARANCE" delay={200}>
|
||||
<SettingsCard title="APPEARANCE">
|
||||
<SettingItem
|
||||
title="Theme"
|
||||
description={currentTheme.name}
|
||||
|
|
@ -367,7 +364,7 @@ const SettingsScreen: React.FC = () => {
|
|||
</SettingsCard>
|
||||
|
||||
{/* Integrations */}
|
||||
<SettingsCard title="INTEGRATIONS" delay={300}>
|
||||
<SettingsCard title="INTEGRATIONS">
|
||||
<SettingItem
|
||||
title="MDBList"
|
||||
description={mdblistKeySet ? "Connected" : "Enable to add ratings & reviews"}
|
||||
|
|
@ -393,7 +390,7 @@ const SettingsScreen: React.FC = () => {
|
|||
</SettingsCard>
|
||||
|
||||
{/* Playback & Experience */}
|
||||
<SettingsCard title="PLAYBACK" delay={400}>
|
||||
<SettingsCard title="PLAYBACK">
|
||||
<SettingItem
|
||||
title="Video Player"
|
||||
description={Platform.OS === 'ios'
|
||||
|
|
@ -422,7 +419,7 @@ const SettingsScreen: React.FC = () => {
|
|||
</SettingsCard>
|
||||
|
||||
{/* About & Support */}
|
||||
<SettingsCard title="ABOUT" delay={500}>
|
||||
<SettingsCard title="ABOUT">
|
||||
<SettingItem
|
||||
title="Privacy Policy"
|
||||
icon="lock"
|
||||
|
|
@ -445,7 +442,7 @@ const SettingsScreen: React.FC = () => {
|
|||
|
||||
{/* Developer Options - Only show in development */}
|
||||
{__DEV__ && (
|
||||
<SettingsCard title="DEVELOPER" delay={600}>
|
||||
<SettingsCard title="DEVELOPER">
|
||||
<SettingItem
|
||||
title="Test Onboarding"
|
||||
icon="play-circle-outline"
|
||||
|
|
@ -496,7 +493,7 @@ const SettingsScreen: React.FC = () => {
|
|||
|
||||
{/* Cache Management - Only show if MDBList is connected */}
|
||||
{mdblistKeySet && (
|
||||
<SettingsCard title="CACHE MANAGEMENT" delay={600}>
|
||||
<SettingsCard title="CACHE MANAGEMENT">
|
||||
<SettingItem
|
||||
title="Clear MDBList Cache"
|
||||
icon="cached"
|
||||
|
|
|
|||
Loading…
Reference in a new issue