mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Added option to sync Trakt watchlist into local library of Nuvio
This commit is contained in:
parent
dbb5337204
commit
cc9fa995b0
2 changed files with 511 additions and 11 deletions
|
|
@ -42,6 +42,7 @@ import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner';
|
|||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useScrollToTop } from '../contexts/ScrollToTopContext';
|
||||
import { TMDBService } from '../services/tmdbService';
|
||||
|
||||
interface LibraryItem extends StreamingContent {
|
||||
progress?: number;
|
||||
|
|
@ -75,6 +76,7 @@ interface TraktFolder {
|
|||
}
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
const TRAKT_LIBRARY_SYNC_MODE_KEY = 'trakt_library_sync_mode';
|
||||
|
||||
function getGridLayout(screenWidth: number): { numColumns: number; itemWidth: number } {
|
||||
const horizontalPadding = 26;
|
||||
|
|
@ -89,8 +91,6 @@ function getGridLayout(screenWidth: number): { numColumns: number; itemWidth: nu
|
|||
return { numColumns, itemWidth };
|
||||
}
|
||||
|
||||
import { TMDBService } from '../services/tmdbService';
|
||||
|
||||
const TraktItem = React.memo(({
|
||||
item,
|
||||
width,
|
||||
|
|
@ -126,10 +126,6 @@ const TraktItem = React.memo(({
|
|||
tmdbId = await tmdbService.findTMDBIdByIMDB(item.imdbId);
|
||||
}
|
||||
|
||||
if (!tmdbId && item.traktId) {
|
||||
|
||||
}
|
||||
|
||||
if (tmdbId) {
|
||||
let posterPath: string | null = null;
|
||||
|
||||
|
|
@ -270,6 +266,9 @@ const LibraryScreen = () => {
|
|||
const { currentTheme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
const flashListRef = useRef<any>(null);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [traktSyncMode, setTraktSyncMode] = useState<'off' | 'manual' | 'automatic'>('off');
|
||||
const hasAutoSyncedThisSession = useRef(false);
|
||||
|
||||
const scrollToTop = useCallback(() => {
|
||||
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||
|
|
@ -315,6 +314,29 @@ const LibraryScreen = () => {
|
|||
loadAllCollections: loadSimklCollections
|
||||
} = useSimklContext();
|
||||
|
||||
// Load Trakt sync mode preferences
|
||||
useEffect(() => {
|
||||
const loadSyncMode = async () => {
|
||||
try {
|
||||
const mode = await mmkvStorage.getItem(TRAKT_LIBRARY_SYNC_MODE_KEY);
|
||||
if (mode === 'manual' || mode === 'automatic') {
|
||||
setTraktSyncMode(mode);
|
||||
} else {
|
||||
setTraktSyncMode('off');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[LibraryScreen] Failed to load sync mode:', error);
|
||||
setTraktSyncMode('off');
|
||||
}
|
||||
};
|
||||
|
||||
loadSyncMode();
|
||||
|
||||
// Reload when screen is focused (to pick up changes from settings)
|
||||
const unsubscribe = navigation.addListener('focus', loadSyncMode);
|
||||
return unsubscribe;
|
||||
}, [navigation]);
|
||||
|
||||
useEffect(() => {
|
||||
const applyStatusBarConfig = () => {
|
||||
StatusBar.setBarStyle('light-content');
|
||||
|
|
@ -422,6 +444,248 @@ const LibraryScreen = () => {
|
|||
};
|
||||
}, [navigation]);
|
||||
|
||||
// Refs to always have access to latest context values (avoids stale closure)
|
||||
const watchlistMoviesRef = useRef(watchlistMovies);
|
||||
const watchlistShowsRef = useRef(watchlistShows);
|
||||
|
||||
useEffect(() => {
|
||||
watchlistMoviesRef.current = watchlistMovies;
|
||||
watchlistShowsRef.current = watchlistShows;
|
||||
}, [watchlistMovies, watchlistShows]);
|
||||
|
||||
// Sync Trakt watchlist to local library
|
||||
const syncTraktWatchlistToLibrary = useCallback(async () => {
|
||||
if (!traktAuthenticated) {
|
||||
showError('Sync Failed', 'Please connect to Trakt first');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSyncing(true);
|
||||
logger.log('[LibraryScreen] Starting Trakt watchlist sync...');
|
||||
|
||||
try {
|
||||
// Load Trakt data fresh before syncing
|
||||
logger.log('[LibraryScreen] Loading Trakt collections...');
|
||||
await loadAllCollections();
|
||||
|
||||
// Wait for React to process state updates
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Access FRESH values from refs (updated by useEffect)
|
||||
const currentMovies = watchlistMoviesRef.current;
|
||||
const currentShows = watchlistShowsRef.current;
|
||||
|
||||
logger.log(`[LibraryScreen] Syncing ${currentMovies?.length || 0} movies and ${currentShows?.length || 0} shows`);
|
||||
|
||||
const hasMovies = currentMovies && currentMovies.length > 0;
|
||||
const hasShows = currentShows && currentShows.length > 0;
|
||||
|
||||
if (!hasMovies && !hasShows) {
|
||||
logger.error('[LibraryScreen] No Trakt watchlist data available');
|
||||
showError(
|
||||
'Sync Failed',
|
||||
'No items found in your Trakt watchlist. Add some movies or shows to your Trakt watchlist first.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const tmdbService = TMDBService.getInstance();
|
||||
let addedCount = 0;
|
||||
let updatedCount = 0;
|
||||
let removedCount = 0;
|
||||
|
||||
const currentLibraryItems = await catalogService.getLibraryItems();
|
||||
const traktWatchlistImdbIds = new Set<string>();
|
||||
|
||||
// Collect IMDb IDs from watchlist (using FRESH refs)
|
||||
if (currentMovies) {
|
||||
currentMovies.forEach(item => {
|
||||
if (item.movie?.ids?.imdb) {
|
||||
traktWatchlistImdbIds.add(item.movie.ids.imdb);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (currentShows) {
|
||||
currentShows.forEach(item => {
|
||||
if (item.show?.ids?.imdb) {
|
||||
traktWatchlistImdbIds.add(item.show.ids.imdb);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove items not in watchlist
|
||||
for (const libraryItem of currentLibraryItems) {
|
||||
const imdbId = libraryItem.id;
|
||||
if (imdbId.startsWith('tt') && !traktWatchlistImdbIds.has(imdbId)) {
|
||||
logger.log(`[LibraryScreen] Removing: ${libraryItem.name}`);
|
||||
await catalogService.removeFromLibrary(libraryItem.type, imdbId);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Add/update movies (using FRESH refs)
|
||||
if (currentMovies) {
|
||||
for (const watchlistItem of currentMovies) {
|
||||
const movie = watchlistItem.movie;
|
||||
if (!movie?.ids?.imdb) continue;
|
||||
|
||||
const imdbId = movie.ids.imdb;
|
||||
const existingItem = currentLibraryItems.find(
|
||||
item => item.id === imdbId && item.type === 'movie'
|
||||
);
|
||||
|
||||
let posterUrl = 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image';
|
||||
let overview = '';
|
||||
let genres: string[] = [];
|
||||
let year = movie.year;
|
||||
|
||||
try {
|
||||
const tmdbId = await tmdbService.findTMDBIdByIMDB(imdbId);
|
||||
if (tmdbId) {
|
||||
const details = await tmdbService.getMovieDetails(String(tmdbId));
|
||||
if (details) {
|
||||
if (details.poster_path) {
|
||||
posterUrl = tmdbService.getImageUrl(details.poster_path, 'w500') || posterUrl;
|
||||
}
|
||||
overview = details.overview || '';
|
||||
genres = details.genres?.map((g: any) => g.name) || [];
|
||||
year = details.release_date ? new Date(details.release_date).getFullYear() : year;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch TMDB data for ${movie.title}:`, error);
|
||||
}
|
||||
|
||||
if (posterUrl === 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image' && movie.images) {
|
||||
const traktPosterUrl = TraktService.getTraktPosterUrl(movie.images);
|
||||
if (traktPosterUrl) posterUrl = traktPosterUrl;
|
||||
}
|
||||
|
||||
const contentToAdd: StreamingContent = {
|
||||
id: imdbId,
|
||||
type: 'movie',
|
||||
name: movie.title,
|
||||
poster: posterUrl,
|
||||
posterShape: 'poster',
|
||||
year,
|
||||
description: overview,
|
||||
genres,
|
||||
imdbRating: undefined,
|
||||
inLibrary: true,
|
||||
};
|
||||
|
||||
if (existingItem) {
|
||||
if (existingItem.poster !== posterUrl) {
|
||||
await catalogService.addToLibrary(contentToAdd);
|
||||
updatedCount++;
|
||||
}
|
||||
} else {
|
||||
await catalogService.addToLibrary(contentToAdd);
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add/update shows (using FRESH refs)
|
||||
if (currentShows) {
|
||||
for (const watchlistItem of currentShows) {
|
||||
const show = watchlistItem.show;
|
||||
if (!show?.ids?.imdb) continue;
|
||||
|
||||
const imdbId = show.ids.imdb;
|
||||
const existingItem = currentLibraryItems.find(
|
||||
item => item.id === imdbId && item.type === 'series'
|
||||
);
|
||||
|
||||
let posterUrl = 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image';
|
||||
let overview = '';
|
||||
let genres: string[] = [];
|
||||
let year = show.year;
|
||||
|
||||
try {
|
||||
const tmdbId = await tmdbService.findTMDBIdByIMDB(imdbId);
|
||||
if (tmdbId) {
|
||||
const details = await tmdbService.getTVShowDetails(tmdbId);
|
||||
if (details) {
|
||||
if (details.poster_path) {
|
||||
posterUrl = tmdbService.getImageUrl(details.poster_path, 'w500') || posterUrl;
|
||||
}
|
||||
overview = details.overview || '';
|
||||
genres = details.genres?.map((g: any) => g.name) || [];
|
||||
year = details.first_air_date ? new Date(details.first_air_date).getFullYear() : year;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch TMDB data for ${show.title}:`, error);
|
||||
}
|
||||
|
||||
if (posterUrl === 'https://via.placeholder.com/300x450/cccccc/666666?text=No+Image' && show.images) {
|
||||
const traktPosterUrl = TraktService.getTraktPosterUrl(show.images);
|
||||
if (traktPosterUrl) posterUrl = traktPosterUrl;
|
||||
}
|
||||
|
||||
const contentToAdd: StreamingContent = {
|
||||
id: imdbId,
|
||||
type: 'series',
|
||||
name: show.title,
|
||||
poster: posterUrl,
|
||||
posterShape: 'poster',
|
||||
year,
|
||||
description: overview,
|
||||
genres,
|
||||
imdbRating: undefined,
|
||||
inLibrary: true,
|
||||
};
|
||||
|
||||
if (existingItem) {
|
||||
if (existingItem.poster !== posterUrl) {
|
||||
await catalogService.addToLibrary(contentToAdd);
|
||||
updatedCount++;
|
||||
}
|
||||
} else {
|
||||
await catalogService.addToLibrary(contentToAdd);
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show result
|
||||
if (addedCount > 0 || updatedCount > 0 || removedCount > 0) {
|
||||
let message = '';
|
||||
if (addedCount > 0) message += `Added ${addedCount}`;
|
||||
if (updatedCount > 0) message += `${message ? ', updated ' : 'Updated '}${updatedCount}`;
|
||||
if (removedCount > 0) message += `${message ? ', removed ' : 'Removed '}${removedCount}`;
|
||||
showInfo('Sync Complete', message);
|
||||
logger.log(`[LibraryScreen] Sync complete: ${message}`);
|
||||
} else {
|
||||
showInfo('Sync Complete', 'Library is up to date');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[LibraryScreen] Sync failed:', error);
|
||||
showError('Sync Failed', 'Unable to sync. Please try again.');
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
}, [traktAuthenticated, loadAllCollections, showInfo, showError]);
|
||||
// Removed watchlistMovies and watchlistShows from deps - we access via refs!
|
||||
|
||||
// Automatic sync on first visit
|
||||
useEffect(() => {
|
||||
if (
|
||||
traktSyncMode === 'automatic' &&
|
||||
traktAuthenticated &&
|
||||
!hasAutoSyncedThisSession.current &&
|
||||
!showTraktContent &&
|
||||
!showSimklContent
|
||||
) {
|
||||
hasAutoSyncedThisSession.current = true;
|
||||
logger.log('[LibraryScreen] Performing automatic sync');
|
||||
syncTraktWatchlistToLibrary();
|
||||
}
|
||||
|
||||
}, [traktSyncMode, traktAuthenticated, showTraktContent, showSimklContent, syncTraktWatchlistToLibrary]);
|
||||
|
||||
const filteredItems = libraryItems.filter(item => {
|
||||
if (filter === 'movies') return item.type === 'movie';
|
||||
if (filter === 'series') return item.type === 'series';
|
||||
|
|
@ -465,7 +729,7 @@ const LibraryScreen = () => {
|
|||
];
|
||||
|
||||
return folders.filter(folder => folder.itemCount > 0);
|
||||
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
|
||||
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent, t]);
|
||||
|
||||
const simklFolders = useMemo((): TraktFolder[] => {
|
||||
if (!simklAuthenticated) return [];
|
||||
|
|
@ -1438,6 +1702,9 @@ const LibraryScreen = () => {
|
|||
return (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768);
|
||||
}, [width, height]);
|
||||
|
||||
// Show sync button only when mode is 'manual' and viewing local library
|
||||
const shouldShowSyncButton = traktSyncMode === 'manual' && !showTraktContent && !showSimklContent && traktAuthenticated;
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<ScreenHeader
|
||||
|
|
@ -1486,6 +1753,29 @@ const LibraryScreen = () => {
|
|||
{showTraktContent ? renderTraktContent() : showSimklContent ? renderSimklContent() : renderContent()}
|
||||
</View>
|
||||
|
||||
{/* Sync FAB - Bottom Right (only in manual mode) */}
|
||||
{shouldShowSyncButton && (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.syncFab,
|
||||
{
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
bottom: insets.bottom + 80,
|
||||
shadowColor: currentTheme.colors.black,
|
||||
}
|
||||
]}
|
||||
onPress={syncTraktWatchlistToLibrary}
|
||||
activeOpacity={0.8}
|
||||
disabled={isSyncing}
|
||||
>
|
||||
{isSyncing ? (
|
||||
<ActivityIndicator color={currentTheme.colors.white} size="small" />
|
||||
) : (
|
||||
<MaterialIcons name="sync" size={24} color={currentTheme.colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{selectedItem && (
|
||||
<DropUpMenu
|
||||
visible={menuVisible}
|
||||
|
|
@ -1831,6 +2121,19 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
syncFab: {
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
elevation: 6,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export default LibraryScreen;
|
||||
export default LibraryScreen;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -27,6 +27,8 @@ import { useSimklIntegration } from '../hooks/useSimklIntegration';
|
|||
import { colors } from '../styles';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BottomSheetModal, BottomSheetScrollView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
|
||||
import { mmkvStorage } from '../services/mmkvStorage';
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
|
|
@ -36,6 +38,7 @@ const TRAKT_CLIENT_ID = process.env.EXPO_PUBLIC_TRAKT_CLIENT_ID as string;
|
|||
if (!TRAKT_CLIENT_ID) {
|
||||
throw new Error('Missing EXPO_PUBLIC_TRAKT_CLIENT_ID environment variable');
|
||||
}
|
||||
|
||||
const discovery = {
|
||||
authorizationEndpoint: 'https://trakt.tv/oauth/authorize',
|
||||
tokenEndpoint: 'https://api.trakt.tv/oauth/token',
|
||||
|
|
@ -47,6 +50,15 @@ const redirectUri = makeRedirectUri({
|
|||
path: 'auth/trakt',
|
||||
});
|
||||
|
||||
// Library Sync Mode constants
|
||||
const TRAKT_LIBRARY_SYNC_MODE_KEY = 'trakt_library_sync_mode';
|
||||
|
||||
const LIBRARY_SYNC_MODE_OPTIONS = [
|
||||
{ value: 'off', label: 'Off', description: 'Disable Trakt library sync completely' },
|
||||
{ value: 'manual', label: 'Manual', description: 'Sync only when you tap the sync button' },
|
||||
{ value: 'automatic', label: 'Automatic', description: 'Sync automatically when you open Library' },
|
||||
];
|
||||
|
||||
const TraktSettingsScreen: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { settings, updateSetting } = useSettings();
|
||||
|
|
@ -80,6 +92,11 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
{ label: t('common.ok'), onPress: () => setAlertVisible(false) },
|
||||
]);
|
||||
|
||||
// Library Sync Mode state
|
||||
const [librarySyncMode, setLibrarySyncMode] = useState<'off' | 'manual' | 'automatic'>('off');
|
||||
const librarySyncSheetRef = useRef<BottomSheetModal>(null);
|
||||
const librarySyncSnapPoints = useMemo(() => ['45%'], []);
|
||||
|
||||
const openAlert = (
|
||||
title: string,
|
||||
message: string,
|
||||
|
|
@ -132,6 +149,26 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
checkAuthStatus();
|
||||
}, [checkAuthStatus]);
|
||||
|
||||
// Load library sync mode on mount
|
||||
useEffect(() => {
|
||||
const loadLibrarySyncMode = async () => {
|
||||
if (isAuthenticated) {
|
||||
try {
|
||||
const mode = await mmkvStorage.getItem(TRAKT_LIBRARY_SYNC_MODE_KEY);
|
||||
if (mode === 'manual' || mode === 'automatic') {
|
||||
setLibrarySyncMode(mode);
|
||||
} else {
|
||||
setLibrarySyncMode('off');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[TraktSettingsScreen] Failed to load library sync mode:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadLibrarySyncMode();
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Setup expo-auth-session hook with PKCE
|
||||
const [request, response, promptAsync] = useAuthRequest(
|
||||
{
|
||||
|
|
@ -230,6 +267,42 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
// Library Sync Mode handlers
|
||||
const renderBackdrop = useCallback(
|
||||
(props: any) => (
|
||||
<BottomSheetBackdrop
|
||||
{...props}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
opacity={0.5}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSelectLibrarySyncMode = async (mode: 'off' | 'manual' | 'automatic') => {
|
||||
try {
|
||||
setLibrarySyncMode(mode);
|
||||
await mmkvStorage.setItem(TRAKT_LIBRARY_SYNC_MODE_KEY, mode);
|
||||
librarySyncSheetRef.current?.dismiss();
|
||||
|
||||
// Show confirmation
|
||||
const modeLabel = LIBRARY_SYNC_MODE_OPTIONS.find(o => o.value === mode)?.label || mode;
|
||||
openAlert(
|
||||
'Library Sync Mode Updated',
|
||||
`Trakt library sync is now set to: ${modeLabel}`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('[TraktSettingsScreen] Failed to save library sync mode:', error);
|
||||
openAlert('Error', 'Failed to update library sync mode');
|
||||
}
|
||||
};
|
||||
|
||||
const getLibrarySyncModeLabel = (mode: string): string => {
|
||||
const option = LIBRARY_SYNC_MODE_OPTIONS.find(o => o.value === mode);
|
||||
return option?.label || 'Off';
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[
|
||||
styles.container,
|
||||
|
|
@ -502,6 +575,35 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Library Sync Mode Setting */}
|
||||
<View style={styles.settingItem}>
|
||||
<TouchableOpacity
|
||||
style={styles.settingContent}
|
||||
onPress={() => librarySyncSheetRef.current?.present()}
|
||||
>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[
|
||||
styles.settingLabel,
|
||||
{ color: currentTheme.colors.highEmphasis }
|
||||
]}>
|
||||
Library Sync Mode
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.settingDescription,
|
||||
{ color: currentTheme.colors.mediumEmphasis }
|
||||
]}>
|
||||
{getLibrarySyncModeLabel(librarySyncMode)} - Sync your Trakt watchlist to local library
|
||||
</Text>
|
||||
</View>
|
||||
<MaterialIcons
|
||||
name="chevron-right"
|
||||
size={24}
|
||||
color={currentTheme.colors.mediumEmphasis}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
|
|
@ -589,8 +691,6 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -606,6 +706,64 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
onClose={() => setAlertVisible(false)}
|
||||
actions={alertActions}
|
||||
/>
|
||||
|
||||
{/* Library Sync Mode Bottom Sheet */}
|
||||
<BottomSheetModal
|
||||
ref={librarySyncSheetRef}
|
||||
index={0}
|
||||
snapPoints={librarySyncSnapPoints}
|
||||
enableDynamicSizing={false}
|
||||
enablePanDownToClose={true}
|
||||
backdropComponent={renderBackdrop}
|
||||
backgroundStyle={{ backgroundColor: '#1a1a1a' }}
|
||||
handleIndicatorStyle={{ backgroundColor: 'rgba(255,255,255,0.3)' }}
|
||||
>
|
||||
<View style={styles.sheetHeader}>
|
||||
<Text style={[styles.sheetTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Library Sync Mode
|
||||
</Text>
|
||||
</View>
|
||||
<BottomSheetScrollView contentContainerStyle={styles.sheetContent}>
|
||||
{LIBRARY_SYNC_MODE_OPTIONS.map((option) => {
|
||||
const isSelected = option.value === librarySyncMode;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.value}
|
||||
style={[
|
||||
styles.sourceItem,
|
||||
isSelected && {
|
||||
backgroundColor: currentTheme.colors.primary + '20',
|
||||
borderColor: currentTheme.colors.primary
|
||||
}
|
||||
]}
|
||||
onPress={() => handleSelectLibrarySyncMode(option.value as 'off' | 'manual' | 'automatic')}
|
||||
>
|
||||
<View style={styles.sourceItemContent}>
|
||||
<Text style={[
|
||||
styles.sourceLabel,
|
||||
{ color: isSelected ? currentTheme.colors.primary : currentTheme.colors.highEmphasis }
|
||||
]}>
|
||||
{option.label}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.sourceDescription,
|
||||
{ color: currentTheme.colors.mediumEmphasis }
|
||||
]}>
|
||||
{option.description}
|
||||
</Text>
|
||||
</View>
|
||||
{isSelected && (
|
||||
<MaterialIcons
|
||||
name="check"
|
||||
size={20}
|
||||
color={currentTheme.colors.primary}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheetModal>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
|
@ -857,6 +1015,45 @@ const styles = StyleSheet.create({
|
|||
marginVertical: 20,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
// Bottom Sheet styles
|
||||
sheetHeader: {
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(255,255,255,0.1)',
|
||||
},
|
||||
sheetTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
sheetContent: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
sourceItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
marginBottom: 12,
|
||||
},
|
||||
sourceItemContent: {
|
||||
flex: 1,
|
||||
marginRight: 12,
|
||||
},
|
||||
sourceLabel: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
},
|
||||
sourceDescription: {
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
});
|
||||
|
||||
export default TraktSettingsScreen;
|
||||
Loading…
Reference in a new issue