diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx
index 0f28967..b64cf98 100644
--- a/src/components/metadata/HeroSection.tsx
+++ b/src/components/metadata/HeroSection.tsx
@@ -179,17 +179,7 @@ const ActionButtons = React.memo(({
{Platform.OS === 'ios' ? (
) : (
- Constants.executionEnvironment === ExecutionEnvironment.StoreClient ? (
-
- ) : (
-
- )
+
)}
) : (
- Constants.executionEnvironment === ExecutionEnvironment.StoreClient ? (
-
- ) : (
-
- )
+
)}
{
const isMounted = useRef(true);
const controlsTimeout = useRef(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);
}
};
diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx
index ec7f654..0674c23 100644
--- a/src/components/player/VideoPlayer.tsx
+++ b/src/components/player/VideoPlayer.tsx
@@ -152,6 +152,9 @@ const VideoPlayer: React.FC = () => {
const isMounted = useRef(true);
const controlsTimeout = useRef(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);
}
};
diff --git a/src/screens/AddonsScreen.tsx b/src/screens/AddonsScreen.tsx
index 39804a0..3d7f725 100644
--- a/src/screens/AddonsScreen.tsx
+++ b/src/screens/AddonsScreen.tsx
@@ -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([]);
- const [communityLoading, setCommunityLoading] = useState(true);
- const [communityError, setCommunityError] = useState(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('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 (
-
- {logo ? (
-
- ) : (
-
-
-
- )}
-
- {manifest.name}
- {description}
-
- v{manifest.version || 'N/A'}
- •
- {categoryText}
-
-
-
- {isConfigurable && (
- handleConfigureAddon(manifest, transportUrl)}
- >
-
-
- )}
- handleAddAddon(transportUrl)}
- disabled={installing}
- >
- {installing ? (
-
- ) : (
-
- )}
-
-
-
- );
- };
-
const StatsCard = ({ value, label }: { value: number; label: string }) => (
{value}
@@ -1186,95 +1031,6 @@ const AddonsScreen = () => {
)}
-
- {/* Separator */}
-
-
- {/* Community Addons Section */}
-
- COMMUNITY ADDONS
-
- {communityLoading ? (
-
-
-
- ) : communityError ? (
-
-
- {communityError}
-
- ) : communityAddons.length === 0 ? (
-
-
- No community addons available
-
- ) : (
- communityAddons.map((item, index) => (
-
-
-
- {item.manifest.logo ? (
-
- ) : (
-
-
-
- )}
-
- {item.manifest.name}
-
- v{item.manifest.version || 'N/A'}
- •
-
- {item.manifest.types && item.manifest.types.length > 0
- ? item.manifest.types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(' • ')
- : 'General'}
-
-
-
-
- {item.manifest.behaviorHints?.configurable && (
- handleConfigureAddon(item.manifest, item.transportUrl)}
- >
-
-
- )}
- handleAddAddon(item.transportUrl)}
- disabled={installing}
- >
- {installing ? (
-
- ) : (
-
- )}
-
-
-
-
-
- {item.manifest.description
- ? (item.manifest.description.length > 100
- ? item.manifest.description.substring(0, 100) + '...'
- : item.manifest.description)
- : 'No description provided.'}
-
-
-
- ))
- )}
-
-
)}
diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx
index 1c2f807..dad0e32 100644
--- a/src/screens/SettingsScreen.tsx
+++ b/src/screens/SettingsScreen.tsx
@@ -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 = ({ children, title, delay = 0 }) => {
+const SettingsCard: React.FC = ({ children, title }) => {
const { currentTheme } = useTheme();
return (
-
{title && (
@@ -64,7 +61,7 @@ const SettingsCard: React.FC = ({ children, title, delay = 0
]}>
{children}
-
+
);
};
@@ -297,7 +294,7 @@ const SettingsScreen: React.FC = () => {
contentContainerStyle={styles.scrollContent}
>
{/* Account Section */}
-
+
{
{/* Content & Discovery */}
-
+
{
{/* Appearance & Interface */}
-
+
{
{/* Integrations */}
-
+
{
{/* Playback & Experience */}
-
+
{
{/* About & Support */}
-
+
{
{/* Developer Options - Only show in development */}
{__DEV__ && (
-
+
{
{/* Cache Management - Only show if MDBList is connected */}
{mdblistKeySet && (
-
+