mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
added trakt watch history.
This commit is contained in:
parent
70586e2b64
commit
6acf84677f
4 changed files with 554 additions and 59 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -36,3 +36,5 @@ yarn-error.*
|
|||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
plan.md
|
||||
release_announcement.md
|
||||
5
app.json
5
app.json
|
|
@ -41,8 +41,9 @@
|
|||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
"foregroundImage": "./assets/icon.png",
|
||||
"backgroundColor": "#020404",
|
||||
"monochromeImage": "./assets/icon.png"
|
||||
},
|
||||
"permissions": [
|
||||
"INTERNET",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
Animated as RNAnimated,
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
|
|
@ -25,13 +26,28 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
|||
import { logger } from '../utils/logger';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useTraktContext } from '../contexts/TraktContext';
|
||||
import TraktIcon from '../../assets/rating-icons/trakt.svg';
|
||||
import { TMDBService } from '../services/tmdbService';
|
||||
|
||||
// Types
|
||||
// Define interfaces for proper typing
|
||||
interface LibraryItem extends StreamingContent {
|
||||
progress?: number;
|
||||
lastWatched?: string;
|
||||
}
|
||||
|
||||
interface TraktDisplayItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'movie' | 'series';
|
||||
poster: string;
|
||||
year?: number;
|
||||
lastWatched: string;
|
||||
plays: number;
|
||||
imdbId?: string;
|
||||
traktId: number;
|
||||
}
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
|
|
@ -99,8 +115,18 @@ const LibraryScreen = () => {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [libraryItems, setLibraryItems] = useState<LibraryItem[]>([]);
|
||||
const [filter, setFilter] = useState<'all' | 'movies' | 'series'>('all');
|
||||
const [showTraktContent, setShowTraktContent] = useState(false);
|
||||
const insets = useSafeAreaInsets();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
// Trakt integration
|
||||
const {
|
||||
isAuthenticated: traktAuthenticated,
|
||||
isLoading: traktLoading,
|
||||
watchedMovies,
|
||||
watchedShows,
|
||||
loadWatchedItems
|
||||
} = useTraktContext();
|
||||
|
||||
// Force consistent status bar settings
|
||||
useEffect(() => {
|
||||
|
|
@ -151,6 +177,120 @@ const LibraryScreen = () => {
|
|||
return true;
|
||||
});
|
||||
|
||||
// Prepare Trakt items with proper poster URLs
|
||||
const traktItems = useMemo(() => {
|
||||
if (!traktAuthenticated || (!watchedMovies?.length && !watchedShows?.length)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: TraktDisplayItem[] = [];
|
||||
|
||||
// Process watched movies
|
||||
if (watchedMovies) {
|
||||
for (const watchedMovie of watchedMovies) {
|
||||
const movie = watchedMovie.movie;
|
||||
if (movie) {
|
||||
items.push({
|
||||
id: String(movie.ids.trakt),
|
||||
name: movie.title,
|
||||
type: 'movie',
|
||||
poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...',
|
||||
year: movie.year,
|
||||
lastWatched: new Date(watchedMovie.last_watched_at).toLocaleDateString(),
|
||||
plays: watchedMovie.plays,
|
||||
imdbId: movie.ids.imdb,
|
||||
traktId: movie.ids.trakt,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process watched shows
|
||||
if (watchedShows) {
|
||||
for (const watchedShow of watchedShows) {
|
||||
const show = watchedShow.show;
|
||||
if (show) {
|
||||
items.push({
|
||||
id: String(show.ids.trakt),
|
||||
name: show.title,
|
||||
type: 'series',
|
||||
poster: 'https://via.placeholder.com/300x450/cccccc/666666?text=Loading...',
|
||||
year: show.year,
|
||||
lastWatched: new Date(watchedShow.last_watched_at).toLocaleDateString(),
|
||||
plays: watchedShow.plays,
|
||||
imdbId: show.ids.imdb,
|
||||
traktId: show.ids.trakt,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by last watched date (most recent first)
|
||||
return items.sort((a, b) => new Date(b.lastWatched).getTime() - new Date(a.lastWatched).getTime());
|
||||
}, [traktAuthenticated, watchedMovies, watchedShows]);
|
||||
|
||||
// State for tracking poster URLs
|
||||
const [traktPostersMap, setTraktPostersMap] = useState<Map<string, string>>(new Map());
|
||||
|
||||
// Effect to fetch poster URLs for Trakt items
|
||||
useEffect(() => {
|
||||
const fetchTraktPosters = async () => {
|
||||
if (!traktAuthenticated || traktItems.length === 0) return;
|
||||
|
||||
const tmdbService = TMDBService.getInstance();
|
||||
|
||||
// Process items individually and update state as each poster is fetched
|
||||
for (const item of traktItems) {
|
||||
try {
|
||||
// Get TMDB ID from the original Trakt data
|
||||
let tmdbId: number | null = null;
|
||||
|
||||
if (item.type === 'movie' && watchedMovies) {
|
||||
const watchedMovie = watchedMovies.find(wm => wm.movie?.ids.trakt === item.traktId);
|
||||
tmdbId = watchedMovie?.movie?.ids.tmdb || null;
|
||||
} else if (item.type === 'series' && watchedShows) {
|
||||
const watchedShow = watchedShows.find(ws => ws.show?.ids.trakt === item.traktId);
|
||||
tmdbId = watchedShow?.show?.ids.tmdb || null;
|
||||
}
|
||||
|
||||
if (tmdbId) {
|
||||
// Fetch details from TMDB to get poster path
|
||||
let posterPath: string | null = null;
|
||||
|
||||
if (item.type === 'movie') {
|
||||
const movieDetails = await tmdbService.getMovieDetails(String(tmdbId));
|
||||
posterPath = movieDetails?.poster_path || null;
|
||||
} else {
|
||||
const showDetails = await tmdbService.getTVShowDetails(tmdbId);
|
||||
posterPath = showDetails?.poster_path || null;
|
||||
}
|
||||
|
||||
if (posterPath) {
|
||||
const fullPosterUrl = tmdbService.getImageUrl(posterPath, 'w500');
|
||||
if (fullPosterUrl) {
|
||||
// Update state immediately for this item
|
||||
setTraktPostersMap(prevMap => {
|
||||
const newMap = new Map(prevMap);
|
||||
newMap.set(item.id, fullPosterUrl);
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch poster for Trakt item ${item.id}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchTraktPosters();
|
||||
}, [traktItems, traktAuthenticated, watchedMovies, watchedShows]);
|
||||
|
||||
// Log when posters map updates
|
||||
useEffect(() => {
|
||||
// Removed debugging logs
|
||||
}, [traktPostersMap]);
|
||||
|
||||
const itemWidth = (width - 48) / 2; // 2 items per row with padding
|
||||
|
||||
const renderItem = ({ item }: { item: LibraryItem }) => (
|
||||
|
|
@ -208,8 +348,203 @@ const LibraryScreen = () => {
|
|||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const renderTraktFolder = () => (
|
||||
<TouchableOpacity
|
||||
style={[styles.itemContainer, { width: itemWidth }]}
|
||||
onPress={() => {
|
||||
if (!traktAuthenticated) {
|
||||
navigation.navigate('TraktSettings');
|
||||
} else {
|
||||
setShowTraktContent(true);
|
||||
}
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
<LinearGradient
|
||||
colors={['#E8254B', '#C41E3A']}
|
||||
style={styles.folderGradient}
|
||||
>
|
||||
<TraktIcon width={60} height={60} style={{ marginBottom: 12 }} />
|
||||
<Text style={[styles.folderTitle, { color: currentTheme.colors.white }]}>
|
||||
Trakt Collection
|
||||
</Text>
|
||||
{traktAuthenticated && traktItems.length > 0 && (
|
||||
<Text style={styles.folderCount}>
|
||||
{traktItems.length} items
|
||||
</Text>
|
||||
)}
|
||||
{!traktAuthenticated && (
|
||||
<Text style={styles.folderSubtitle}>
|
||||
Tap to connect
|
||||
</Text>
|
||||
)}
|
||||
</LinearGradient>
|
||||
|
||||
{/* Trakt badge */}
|
||||
<View style={[styles.badgeContainer, { backgroundColor: 'rgba(255,255,255,0.2)' }]}>
|
||||
<TraktIcon width={12} height={12} style={{ marginRight: 4 }} />
|
||||
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>
|
||||
Trakt
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const renderTraktItem = ({ item, customWidth }: { item: TraktDisplayItem; customWidth?: number }) => {
|
||||
const posterUrl = traktPostersMap.get(item.id) || 'https://via.placeholder.com/300x450/ff0000/ffffff?text=No+Poster';
|
||||
const width = customWidth || itemWidth;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.itemContainer, { width }]}
|
||||
onPress={() => {
|
||||
// Navigate using IMDB ID for Trakt items
|
||||
if (item.imdbId) {
|
||||
navigation.navigate('Metadata', { id: item.imdbId, type: item.type });
|
||||
}
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
<Image
|
||||
source={{ uri: posterUrl }}
|
||||
style={styles.poster}
|
||||
contentFit="cover"
|
||||
transition={300}
|
||||
/>
|
||||
<LinearGradient
|
||||
colors={['transparent', 'rgba(0,0,0,0.85)']}
|
||||
style={styles.posterGradient}
|
||||
>
|
||||
<Text
|
||||
style={[styles.itemTitle, { color: currentTheme.colors.white }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text style={styles.lastWatched}>
|
||||
Last watched: {item.lastWatched}
|
||||
</Text>
|
||||
{item.plays > 1 && (
|
||||
<Text style={styles.playsCount}>
|
||||
{item.plays} plays
|
||||
</Text>
|
||||
)}
|
||||
</LinearGradient>
|
||||
|
||||
{/* Trakt badge */}
|
||||
<View style={[styles.badgeContainer, { backgroundColor: 'rgba(232,37,75,0.9)' }]}>
|
||||
<TraktIcon width={12} height={12} style={{ marginRight: 4 }} />
|
||||
<Text style={[styles.badgeText, { color: currentTheme.colors.white }]}>
|
||||
{item.type === 'movie' ? 'Movie' : 'Series'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTraktContent = () => {
|
||||
if (traktLoading) {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
if (traktItems.length === 0) {
|
||||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
<TraktIcon width={80} height={80} style={{ opacity: 0.7, marginBottom: 16 }} />
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>No watched content</Text>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
Your Trakt watched history will appear here
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.exploreButton, {
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
shadowColor: currentTheme.colors.black
|
||||
}]}
|
||||
onPress={() => {
|
||||
loadWatchedItems();
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Refresh</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Separate movies and shows
|
||||
const movies = traktItems.filter(item => item.type === 'movie');
|
||||
const shows = traktItems.filter(item => item.type === 'series');
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={styles.sectionsContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.sectionsContent}
|
||||
>
|
||||
{movies.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<MaterialIcons
|
||||
name="movie"
|
||||
size={24}
|
||||
color={currentTheme.colors.white}
|
||||
style={styles.sectionIcon}
|
||||
/>
|
||||
<Text style={[styles.sectionTitle, { color: currentTheme.colors.white }]}>
|
||||
Movies ({movies.length})
|
||||
</Text>
|
||||
</View>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.horizontalScrollContent}
|
||||
>
|
||||
{movies.map((item) => (
|
||||
<View key={item.id} style={{ width: itemWidth * 0.8, marginRight: 12 }}>
|
||||
{renderTraktItem({ item, customWidth: itemWidth * 0.8 })}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{shows.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<MaterialIcons
|
||||
name="live-tv"
|
||||
size={24}
|
||||
color={currentTheme.colors.white}
|
||||
style={styles.sectionIcon}
|
||||
/>
|
||||
<Text style={[styles.sectionTitle, { color: currentTheme.colors.white }]}>
|
||||
TV Shows ({shows.length})
|
||||
</Text>
|
||||
</View>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.horizontalScrollContent}
|
||||
>
|
||||
{shows.map((item) => (
|
||||
<View key={item.id} style={{ width: itemWidth * 0.8, marginRight: 12 }}>
|
||||
{renderTraktItem({ item, customWidth: itemWidth * 0.8 })}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFilter = (filterType: 'all' | 'movies' | 'series', label: string, iconName: keyof typeof MaterialIcons.glyphMap) => {
|
||||
const isActive = filter === filterType;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
|
|
@ -239,6 +574,71 @@ const LibraryScreen = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading) {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
// Combine regular library items with Trakt folder
|
||||
const allItems = [];
|
||||
|
||||
// Add Trakt folder if authenticated or as connection prompt
|
||||
if (traktAuthenticated || !traktAuthenticated) {
|
||||
allItems.push({ type: 'trakt-folder', id: 'trakt-folder' });
|
||||
}
|
||||
|
||||
// Add filtered library items
|
||||
allItems.push(...filteredItems);
|
||||
|
||||
if (allItems.length === 0) {
|
||||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons
|
||||
name="video-library"
|
||||
size={80}
|
||||
color={currentTheme.colors.mediumGray}
|
||||
style={{ opacity: 0.7 }}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>Your library is empty</Text>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
Add content to your library to keep track of what you're watching
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.exploreButton, {
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
shadowColor: currentTheme.colors.black
|
||||
}]}
|
||||
onPress={() => navigation.navigate('Discover')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Explore Content</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={allItems}
|
||||
renderItem={({ item }) => {
|
||||
if (item.type === 'trakt-folder') {
|
||||
return renderTraktFolder();
|
||||
}
|
||||
return renderItem({ item: item as LibraryItem });
|
||||
}}
|
||||
keyExtractor={item => item.id}
|
||||
numColumns={2}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
columnWrapperStyle={styles.columnWrapper}
|
||||
initialNumToRender={6}
|
||||
maxToRenderPerBatch={6}
|
||||
windowSize={5}
|
||||
removeClippedSubviews={Platform.OS === 'android'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
|
||||
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
|
||||
const headerHeight = headerBaseHeight + topSpacing;
|
||||
|
|
@ -252,58 +652,48 @@ const LibraryScreen = () => {
|
|||
{/* Header Section with proper top spacing */}
|
||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
||||
<View style={styles.headerContent}>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Library</Text>
|
||||
{showTraktContent ? (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => setShowTraktContent(false)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="arrow-back"
|
||||
size={28}
|
||||
color={currentTheme.colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.headerTitleContainer}>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>
|
||||
Trakt Collection
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.headerSpacer} />
|
||||
</>
|
||||
) : (
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Library</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Content Container */}
|
||||
<View style={[styles.contentContainer, { backgroundColor: currentTheme.colors.darkBackground }]}>
|
||||
<View style={styles.filtersContainer}>
|
||||
{renderFilter('all', 'All', 'apps')}
|
||||
{renderFilter('movies', 'Movies', 'movie')}
|
||||
{renderFilter('series', 'TV Shows', 'live-tv')}
|
||||
</View>
|
||||
|
||||
{loading ? (
|
||||
<SkeletonLoader />
|
||||
) : filteredItems.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons
|
||||
name="video-library"
|
||||
size={80}
|
||||
color={currentTheme.colors.mediumGray}
|
||||
style={{ opacity: 0.7 }}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>Your library is empty</Text>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
Add content to your library to keep track of what you're watching
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.exploreButton, {
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
shadowColor: currentTheme.colors.black
|
||||
}]}
|
||||
onPress={() => navigation.navigate('Discover')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Explore Content</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={filteredItems}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.id}
|
||||
numColumns={2}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
columnWrapperStyle={styles.columnWrapper}
|
||||
initialNumToRender={6}
|
||||
maxToRenderPerBatch={6}
|
||||
windowSize={5}
|
||||
removeClippedSubviews={Platform.OS === 'android'}
|
||||
/>
|
||||
{!showTraktContent && (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.filtersContainer}
|
||||
style={styles.filtersScrollView}
|
||||
>
|
||||
{renderFilter('all', 'All', 'apps')}
|
||||
{renderFilter('movies', 'Movies', 'movie')}
|
||||
{renderFilter('series', 'TV Shows', 'live-tv')}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{showTraktContent ? renderTraktContent() : renderContent()}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -489,7 +879,100 @@ const styles = StyleSheet.create({
|
|||
exploreButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
}
|
||||
},
|
||||
playsCount: {
|
||||
fontSize: 11,
|
||||
color: 'rgba(255,255,255,0.6)',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 2,
|
||||
marginTop: 2,
|
||||
},
|
||||
filtersScrollView: {
|
||||
flexGrow: 0,
|
||||
},
|
||||
folderContainer: {
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
||||
aspectRatio: 2/3,
|
||||
elevation: 5,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 8,
|
||||
},
|
||||
folderGradient: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
padding: 16,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
folderTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
marginBottom: 4,
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 2,
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
folderCount: {
|
||||
fontSize: 12,
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 2,
|
||||
},
|
||||
folderSubtitle: {
|
||||
fontSize: 12,
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 2,
|
||||
},
|
||||
backButton: {
|
||||
padding: 8,
|
||||
},
|
||||
sectionsContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
sectionsContent: {
|
||||
paddingBottom: 90,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
sectionIcon: {
|
||||
marginRight: 8,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
horizontalScrollContent: {
|
||||
paddingLeft: 16,
|
||||
paddingRight: 4,
|
||||
},
|
||||
headerTitleContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerSpacer: {
|
||||
width: 44, // Match the back button width
|
||||
},
|
||||
});
|
||||
|
||||
export default LibraryScreen;
|
||||
|
|
@ -235,6 +235,11 @@ const SearchScreen = () => {
|
|||
|
||||
useEffect(() => {
|
||||
loadRecentSearches();
|
||||
|
||||
// Cleanup function to cancel pending searches on unmount
|
||||
return () => {
|
||||
debouncedSearch.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const animatedSearchBarStyle = useAnimatedStyle(() => {
|
||||
|
|
@ -299,13 +304,17 @@ const SearchScreen = () => {
|
|||
|
||||
const saveRecentSearch = async (searchQuery: string) => {
|
||||
try {
|
||||
const newRecentSearches = [
|
||||
searchQuery,
|
||||
...recentSearches.filter(s => s !== searchQuery)
|
||||
].slice(0, MAX_RECENT_SEARCHES);
|
||||
|
||||
setRecentSearches(newRecentSearches);
|
||||
await AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches));
|
||||
setRecentSearches(prevSearches => {
|
||||
const newRecentSearches = [
|
||||
searchQuery,
|
||||
...prevSearches.filter(s => s !== searchQuery)
|
||||
].slice(0, MAX_RECENT_SEARCHES);
|
||||
|
||||
// Save to AsyncStorage
|
||||
AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecentSearches));
|
||||
|
||||
return newRecentSearches;
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to save recent search:', error);
|
||||
}
|
||||
|
|
@ -334,7 +343,7 @@ const SearchScreen = () => {
|
|||
setSearching(false);
|
||||
}
|
||||
}, 800),
|
||||
[recentSearches]
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue