mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
fixes
This commit is contained in:
parent
2b3069a988
commit
680a1b1ea6
7 changed files with 139 additions and 64 deletions
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from 'react-native';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import Animated, { FadeIn } from 'react-native-reanimated';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||
import { StreamingContent, catalogService } from '../../services/catalogService';
|
||||
|
|
@ -214,6 +214,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
const progressPercent = (progress.currentTime / progress.duration) * 100;
|
||||
// Skip fully watched movies
|
||||
if (type === 'movie' && progressPercent >= 85) continue;
|
||||
// Skip movies with no actual progress (ensure > 0%)
|
||||
if (type === 'movie' && (!isFinite(progressPercent) || progressPercent <= 0)) continue;
|
||||
const contentKey = `${type}:${id}`;
|
||||
if (!contentGroups[contentKey]) contentGroups[contentKey] = { type, id, episodes: [] };
|
||||
contentGroups[contentKey].episodes.push({ key, episodeId, progress, progressPercent });
|
||||
|
|
@ -447,7 +449,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
refreshTimerRef.current = setTimeout(() => {
|
||||
// Trigger a background refresh
|
||||
loadContinueWatching(true);
|
||||
}, 2000); // Increased debounce time significantly to reduce churn
|
||||
}, 800); // Shorter debounce for snappier UI without battery impact
|
||||
};
|
||||
|
||||
// Try to set up a custom event listener or use a timer as fallback
|
||||
|
|
@ -484,6 +486,14 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
loadContinueWatching();
|
||||
}, [loadContinueWatching]);
|
||||
|
||||
// Refresh on screen focus (lightweight, no polling)
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
loadContinueWatching(true);
|
||||
return () => {};
|
||||
}, [loadContinueWatching])
|
||||
);
|
||||
|
||||
// Expose the refresh function via the ref
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
refresh: async () => {
|
||||
|
|
@ -619,7 +629,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
<View style={styles.contentDetails}>
|
||||
<View style={styles.titleRow}>
|
||||
{(() => {
|
||||
const isUpNext = item.progress === 0;
|
||||
const isUpNext = item.type === 'series' && item.progress === 0;
|
||||
return (
|
||||
<View style={styles.titleRow}>
|
||||
<Text
|
||||
|
|
|
|||
|
|
@ -1407,11 +1407,18 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
(error?.error?.localizedDescription &&
|
||||
error.error.localizedDescription.includes('server is not correctly configured'));
|
||||
|
||||
// Expand audio decoder error detection to include DTS/TrueHD/Atmos families
|
||||
const isHeavyCodecDecoderError =
|
||||
(error?.error?.errorString && /(dts|true\s?hd|truehd|atmos)/i.test(String(error.error.errorString))) ||
|
||||
(error?.error?.errorException && /(dts|true\s?hd|truehd|atmos)/i.test(String(error.error.errorException)));
|
||||
|
||||
// Format error details for user display
|
||||
let errorMessage = 'An unknown error occurred';
|
||||
if (error) {
|
||||
if (isDolbyCodecError) {
|
||||
errorMessage = 'Audio codec compatibility issue detected. The video contains Dolby Digital Plus audio which is not supported on this device. Please try selecting a different audio track or use an alternative video source.';
|
||||
} else if (isHeavyCodecDecoderError) {
|
||||
errorMessage = 'Audio codec issue (DTS/TrueHD/Atmos). Switching to a stereo/standard audio track may help.';
|
||||
} else if (isServerConfigError) {
|
||||
errorMessage = 'Stream server configuration issue. This may be a temporary problem with the video source.';
|
||||
} else if (typeof error === 'string') {
|
||||
|
|
@ -2627,7 +2634,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
ignoreSilentSwitch="ignore"
|
||||
mixWithOthers="inherit"
|
||||
progressUpdateInterval={1000}
|
||||
maxBitRate={2000000}
|
||||
// Remove artificial bit rate cap to allow high-bitrate streams (e.g., Blu-ray remux) to play
|
||||
// maxBitRate intentionally omitted
|
||||
disableFocus={true}
|
||||
// iOS AVPlayer startup tuning
|
||||
automaticallyWaitsToMinimizeStalling={true as any}
|
||||
|
|
@ -2636,11 +2644,14 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
preventsDisplaySleepDuringVideoPlayback={true as any}
|
||||
// ExoPlayer HLS optimization
|
||||
bufferConfig={{
|
||||
minBufferMs: 15000,
|
||||
maxBufferMs: 50000,
|
||||
// Larger buffers for high-bitrate remuxes to reduce rebuffering/crashes
|
||||
minBufferMs: 60000,
|
||||
maxBufferMs: 180000,
|
||||
bufferForPlaybackMs: 2500,
|
||||
bufferForPlaybackAfterRebufferMs: 5000,
|
||||
bufferForPlaybackAfterRebufferMs: 8000,
|
||||
} as any}
|
||||
// Use SurfaceView on Android to lower memory pressure with 4K/high-bitrate content
|
||||
useTextureView={Platform.OS === 'android' ? false : (undefined as any)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ interface UseMetadataReturn {
|
|||
loadingStreams: boolean;
|
||||
episodeStreams: GroupedStreams;
|
||||
loadingEpisodeStreams: boolean;
|
||||
addonResponseOrder: string[];
|
||||
preloadedStreams: GroupedStreams;
|
||||
preloadedEpisodeStreams: { [episodeId: string]: GroupedStreams };
|
||||
selectedEpisode: string | null;
|
||||
|
|
@ -134,6 +135,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
const [availableStreams, setAvailableStreams] = useState<{ [sourceType: string]: Stream }>({});
|
||||
const [scraperStatuses, setScraperStatuses] = useState<ScraperStatus[]>([]);
|
||||
const [activeFetchingScrapers, setActiveFetchingScrapers] = useState<string[]>([]);
|
||||
// Track response order for addons to preserve actual response order
|
||||
const [addonResponseOrder, setAddonResponseOrder] = useState<string[]>([]);
|
||||
// Prevent re-initializing season selection repeatedly for the same series
|
||||
const initializedSeasonRef = useRef(false);
|
||||
|
||||
|
|
@ -287,6 +290,14 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
};
|
||||
};
|
||||
|
||||
// Track response order for addons
|
||||
setAddonResponseOrder(prevOrder => {
|
||||
if (!prevOrder.includes(addonId)) {
|
||||
return [...prevOrder, addonId];
|
||||
}
|
||||
return prevOrder;
|
||||
});
|
||||
|
||||
if (isEpisode) {
|
||||
setEpisodeStreams(updateState);
|
||||
setLoadingEpisodeStreams(false);
|
||||
|
|
@ -824,6 +835,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
// Reset scraper tracking
|
||||
setScraperStatuses([]);
|
||||
setActiveFetchingScrapers([]);
|
||||
setAddonResponseOrder([]); // Reset response order
|
||||
|
||||
// Get TMDB ID for external sources and determine the correct ID for Stremio addons
|
||||
if (__DEV__) console.log('🔍 [loadStreams] Getting TMDB ID for:', id);
|
||||
|
|
@ -990,6 +1002,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
// Reset scraper tracking for episodes
|
||||
setScraperStatuses([]);
|
||||
setActiveFetchingScrapers([]);
|
||||
setAddonResponseOrder([]); // Reset response order
|
||||
|
||||
// Initialize scraper tracking for episodes
|
||||
try {
|
||||
|
|
@ -1358,6 +1371,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
loadingStreams,
|
||||
episodeStreams,
|
||||
loadingEpisodeStreams,
|
||||
addonResponseOrder,
|
||||
preloadedStreams,
|
||||
preloadedEpisodeStreams,
|
||||
selectedEpisode,
|
||||
|
|
|
|||
|
|
@ -705,26 +705,36 @@ const StatusBadge: React.FC<{
|
|||
const getStatusConfig = () => {
|
||||
switch (status) {
|
||||
case 'enabled':
|
||||
return { color: '#34C759', text: 'Active', icon: 'checkmark-circle' };
|
||||
return { color: '#34C759', text: 'Active' };
|
||||
case 'disabled':
|
||||
return { color: colors.mediumGray, text: 'Disabled', icon: 'close-circle' };
|
||||
return { color: colors.mediumGray, text: 'Disabled' };
|
||||
case 'available':
|
||||
return { color: colors.primary, text: 'Available', icon: 'download' };
|
||||
return { color: colors.primary, text: 'Available' };
|
||||
case 'platform-disabled':
|
||||
return { color: '#FF9500', text: 'Platform Disabled', icon: 'phone-portrait' };
|
||||
return { color: '#FF9500', text: 'Platform Disabled' };
|
||||
case 'error':
|
||||
return { color: '#FF3B30', text: 'Error', icon: 'warning' };
|
||||
return { color: '#FF3B30', text: 'Error' };
|
||||
default:
|
||||
return { color: colors.mediumGray, text: 'Unknown', icon: 'help-circle' };
|
||||
return { color: colors.mediumGray, text: 'Unknown' };
|
||||
}
|
||||
};
|
||||
|
||||
const config = getStatusConfig();
|
||||
|
||||
return (
|
||||
<View style={[{ backgroundColor: config.color, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 12, gap: 4 }]}>
|
||||
<Ionicons name={config.icon as any} size={12} color="white" />
|
||||
<Text style={{ color: 'white', fontSize: 11, fontWeight: '600' }}>{config.text}</Text>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 9999,
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: config.color,
|
||||
gap: 6,
|
||||
}}>
|
||||
<View style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: config.color }} />
|
||||
<Text style={{ color: config.color, fontSize: 11, fontWeight: '600' }}>{config.text}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
@ -874,7 +884,7 @@ const PluginsScreen: React.FC = () => {
|
|||
setShowAddRepositoryModal(false);
|
||||
Alert.alert('Success', 'Repository added and refreshed successfully');
|
||||
} catch (error) {
|
||||
logger.error('[ScraperSettings] Failed to add repository:', error);
|
||||
logger.error('[PluginsScreen] Failed to add repository:', error);
|
||||
Alert.alert('Error', 'Failed to add repository');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
|
@ -1447,7 +1457,7 @@ const PluginsScreen: React.FC = () => {
|
|||
<View style={styles.scraperLogo} />
|
||||
)}
|
||||
<View style={styles.scraperCardInfo}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4 }}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4, gap: 8 }}>
|
||||
<Text style={styles.scraperName}>{scraper.name}</Text>
|
||||
<StatusBadge status={getScraperStatus(scraper)} colors={colors} />
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -455,6 +455,7 @@ export const StreamsScreen = () => {
|
|||
imdbId,
|
||||
scraperStatuses,
|
||||
activeFetchingScrapers,
|
||||
addonResponseOrder,
|
||||
} = useMetadata({ id, type });
|
||||
|
||||
// Get backdrop from metadata assets
|
||||
|
|
@ -1378,13 +1379,20 @@ export const StreamsScreen = () => {
|
|||
return addonId === selectedProvider;
|
||||
})
|
||||
.sort(([addonIdA], [addonIdB]) => {
|
||||
// Sort by Stremio addon installation order
|
||||
const indexA = installedAddons.findIndex(addon => addon.id === addonIdA);
|
||||
const indexB = installedAddons.findIndex(addon => addon.id === addonIdB);
|
||||
|
||||
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
|
||||
// Sort by response order (actual order addons responded)
|
||||
const indexA = addonResponseOrder.indexOf(addonIdA);
|
||||
const indexB = addonResponseOrder.indexOf(addonIdB);
|
||||
|
||||
// If both are in response order, sort by response order
|
||||
if (indexA !== -1 && indexB !== -1) {
|
||||
return indexA - indexB;
|
||||
}
|
||||
|
||||
// If only one is in response order, prioritize it
|
||||
if (indexA !== -1) return -1;
|
||||
if (indexB !== -1) return 1;
|
||||
|
||||
// If neither is in response order, maintain original order
|
||||
return 0;
|
||||
});
|
||||
|
||||
|
|
@ -1408,17 +1416,16 @@ export const StreamsScreen = () => {
|
|||
pluginOriginalCount += providerStreams.length;
|
||||
}
|
||||
|
||||
// Apply quality filtering and sorting to streams
|
||||
// Apply quality filtering only; keep original addon stream order
|
||||
const filteredStreams = filterStreamsByQuality(providerStreams);
|
||||
const sortedStreams = sortStreams(filteredStreams);
|
||||
|
||||
if (isInstalledAddon) {
|
||||
addonStreams.push(...sortedStreams);
|
||||
addonStreams.push(...filteredStreams);
|
||||
if (!addonNames.includes(addonName)) {
|
||||
addonNames.push(addonName);
|
||||
}
|
||||
} else {
|
||||
pluginStreams.push(...sortedStreams);
|
||||
pluginStreams.push(...filteredStreams);
|
||||
if (!pluginNames.includes(addonName)) {
|
||||
pluginNames.push(addonName);
|
||||
}
|
||||
|
|
@ -1427,14 +1434,10 @@ export const StreamsScreen = () => {
|
|||
|
||||
const sections = [];
|
||||
if (addonStreams.length > 0) {
|
||||
// Apply final sorting to the combined addon streams for quality-first mode
|
||||
const finalSortedAddonStreams = settings.streamSortMode === 'quality-then-scraper' ?
|
||||
sortStreams(addonStreams) : addonStreams;
|
||||
|
||||
sections.push({
|
||||
title: addonNames.join(', '),
|
||||
addonId: 'grouped-addons',
|
||||
data: finalSortedAddonStreams
|
||||
data: addonStreams
|
||||
});
|
||||
} else if (addonOriginalCount > 0 && addonStreams.length === 0) {
|
||||
// Show empty section with message for addons that had streams but all were filtered
|
||||
|
|
@ -1447,14 +1450,10 @@ export const StreamsScreen = () => {
|
|||
}
|
||||
|
||||
if (pluginStreams.length > 0) {
|
||||
// Apply final sorting to the combined plugin streams for quality-first mode
|
||||
const finalSortedPluginStreams = settings.streamSortMode === 'quality-then-scraper' ?
|
||||
sortStreams(pluginStreams) : pluginStreams;
|
||||
|
||||
sections.push({
|
||||
title: localScraperService.getRepositoryName(),
|
||||
addonId: 'grouped-plugins',
|
||||
data: finalSortedPluginStreams
|
||||
data: pluginStreams
|
||||
});
|
||||
} else if (pluginOriginalCount > 0 && pluginStreams.length === 0) {
|
||||
// Show empty section with message for plugins that had streams but all were filtered
|
||||
|
|
@ -1473,21 +1472,20 @@ export const StreamsScreen = () => {
|
|||
// Count original streams before filtering
|
||||
const originalCount = providerStreams.length;
|
||||
|
||||
// Apply quality filtering and sorting to streams
|
||||
// Apply quality filtering only; keep original addon stream order
|
||||
const filteredStreams = filterStreamsByQuality(providerStreams);
|
||||
const sortedStreams = sortStreams(filteredStreams);
|
||||
|
||||
const isEmptyDueToQualityFilter = originalCount > 0 && sortedStreams.length === 0;
|
||||
const isEmptyDueToQualityFilter = originalCount > 0 && filteredStreams.length === 0;
|
||||
|
||||
return {
|
||||
title: addonName,
|
||||
addonId,
|
||||
data: isEmptyDueToQualityFilter ? [{ isEmptyPlaceholder: true } as any] : sortedStreams,
|
||||
data: isEmptyDueToQualityFilter ? [{ isEmptyPlaceholder: true } as any] : filteredStreams,
|
||||
isEmptyDueToQualityFilter
|
||||
};
|
||||
});
|
||||
}
|
||||
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality, sortStreams]);
|
||||
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode, filterStreamsByQuality, sortStreams, addonResponseOrder]);
|
||||
|
||||
const episodeImage = useMemo(() => {
|
||||
if (episodeThumbnail) {
|
||||
|
|
|
|||
|
|
@ -113,19 +113,28 @@ class SyncService {
|
|||
await storageService.addWatchProgressTombstone(id, type, episodeId || undefined, remoteUpdated);
|
||||
} catch {}
|
||||
} else {
|
||||
// Preserve the most recent timestamp between local and remote to maintain proper continue watching order
|
||||
const remoteTimestamp = row.last_updated_ms || Date.now();
|
||||
const existingProgress = await storageService.getWatchProgress(id, type, (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined);
|
||||
const localTimestamp = existingProgress?.lastUpdated || 0;
|
||||
|
||||
// Use the newer timestamp to maintain proper continue watching order across devices
|
||||
const finalTimestamp = Math.max(remoteTimestamp, localTimestamp);
|
||||
|
||||
await storageService.setWatchProgress(
|
||||
id,
|
||||
type,
|
||||
{
|
||||
currentTime: row.current_time_seconds || 0,
|
||||
duration: row.duration_seconds || 0,
|
||||
lastUpdated: row.last_updated_ms || Date.now(),
|
||||
lastUpdated: finalTimestamp,
|
||||
traktSynced: row.trakt_synced ?? undefined,
|
||||
traktLastSynced: row.trakt_last_synced_ms ?? undefined,
|
||||
traktProgress: row.trakt_progress_percent ?? undefined,
|
||||
},
|
||||
// Ensure we pass through the full remote episode_id as-is; empty string becomes undefined
|
||||
(row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined
|
||||
(row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined,
|
||||
{ preserveTimestamp: true, forceNotify: true, forceWrite: true }
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
|
|
@ -372,19 +381,32 @@ class SyncService {
|
|||
if (wp && Array.isArray(wp)) {
|
||||
const remoteActiveKeys = new Set<string>();
|
||||
for (const row of wp as any[]) {
|
||||
// Preserve the most recent timestamp between local and remote to maintain proper continue watching order
|
||||
const remoteTimestamp = row.last_updated_ms || Date.now();
|
||||
const existingProgress = await storageService.getWatchProgress(
|
||||
row.media_id,
|
||||
row.media_type,
|
||||
(row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined
|
||||
);
|
||||
const localTimestamp = existingProgress?.lastUpdated || 0;
|
||||
|
||||
// Use the newer timestamp to maintain proper continue watching order across devices
|
||||
const finalTimestamp = Math.max(remoteTimestamp, localTimestamp);
|
||||
|
||||
await storageService.setWatchProgress(
|
||||
row.media_id,
|
||||
row.media_type,
|
||||
{
|
||||
currentTime: row.current_time_seconds,
|
||||
duration: row.duration_seconds,
|
||||
lastUpdated: row.last_updated_ms,
|
||||
lastUpdated: finalTimestamp,
|
||||
traktSynced: row.trakt_synced ?? undefined,
|
||||
traktLastSynced: row.trakt_last_synced_ms ?? undefined,
|
||||
traktProgress: row.trakt_progress_percent ?? undefined,
|
||||
},
|
||||
// Ensure full episode_id is preserved; treat empty as undefined
|
||||
(row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined
|
||||
(row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined,
|
||||
{ preserveTimestamp: true, forceNotify: true, forceWrite: true }
|
||||
);
|
||||
remoteActiveKeys.add(`${row.media_type}|${row.media_id}|${row.episode_id || ''}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,10 +223,11 @@ class StorageService {
|
|||
}
|
||||
|
||||
public async setWatchProgress(
|
||||
id: string,
|
||||
type: string,
|
||||
id: string,
|
||||
type: string,
|
||||
progress: WatchProgress,
|
||||
episodeId?: string
|
||||
episodeId?: string,
|
||||
options?: { preserveTimestamp?: boolean; forceNotify?: boolean; forceWrite?: boolean }
|
||||
): Promise<void> {
|
||||
try {
|
||||
const key = await this.getWatchProgressKeyScoped(id, type, episodeId);
|
||||
|
|
@ -243,23 +244,32 @@ class StorageService {
|
|||
}
|
||||
} catch {}
|
||||
|
||||
// Check if progress has actually changed significantly
|
||||
const existingProgress = await this.getWatchProgress(id, type, episodeId);
|
||||
if (existingProgress) {
|
||||
const timeDiff = Math.abs(progress.currentTime - existingProgress.currentTime);
|
||||
const durationDiff = Math.abs(progress.duration - existingProgress.duration);
|
||||
|
||||
// Only update if there's a significant change (>5 seconds or duration change)
|
||||
if (timeDiff < 5 && durationDiff < 1) {
|
||||
return; // Skip update for minor changes
|
||||
// Check if progress has actually changed significantly, unless forceWrite is requested
|
||||
if (!options?.forceWrite) {
|
||||
const existingProgress = await this.getWatchProgress(id, type, episodeId);
|
||||
if (existingProgress) {
|
||||
const timeDiff = Math.abs(progress.currentTime - existingProgress.currentTime);
|
||||
const durationDiff = Math.abs(progress.duration - existingProgress.duration);
|
||||
|
||||
// Only update if there's a significant change (>5 seconds or duration change)
|
||||
if (timeDiff < 5 && durationDiff < 1) {
|
||||
return; // Skip update for minor changes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updated = { ...progress, lastUpdated: Date.now() };
|
||||
await AsyncStorage.setItem(key, JSON.stringify(updated));
|
||||
|
||||
// Use debounced notification to reduce spam
|
||||
this.debouncedNotifySubscribers();
|
||||
|
||||
const timestamp = (options?.preserveTimestamp && typeof progress.lastUpdated === 'number')
|
||||
? progress.lastUpdated
|
||||
: Date.now();
|
||||
const updated = { ...progress, lastUpdated: timestamp };
|
||||
await AsyncStorage.setItem(key, JSON.stringify(updated));
|
||||
|
||||
// Notify subscribers; allow forcing immediate notification
|
||||
if (options?.forceNotify) {
|
||||
this.notifyWatchProgressSubscribers();
|
||||
} else {
|
||||
this.debouncedNotifySubscribers();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error setting watch progress:', error);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue