From ad2e1816dc2c57dc3fe98d504e2511245f1ba63a Mon Sep 17 00:00:00 2001 From: tapframe Date: Fri, 8 Aug 2025 00:15:10 +0530 Subject: [PATCH] some ui changes --- src/components/metadata/HeroSection.tsx | 345 ++++++++++++------------ src/hooks/useDominantColor.ts | 192 +++++++++---- src/screens/MetadataScreen.tsx | 34 ++- 3 files changed, 331 insertions(+), 240 deletions(-) diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 17c839c4..5f32b6ab 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -786,16 +786,9 @@ const HeroSection: React.FC = memo(({ const logoScale = hasProgress ? 0.85 : 1; return { - opacity: logoOpacity.value, + opacity: logoOpacity.value, transform: [ - { - translateY: interpolate( - scrollY.value, - [0, 100], - [0, -20], - Extrapolate.CLAMP - ) - }, + // Keep logo stable by not applying translateY based on scroll { scale: withTiming(logoScale, { duration: 300 }) } ] }; @@ -944,6 +937,22 @@ const HeroSection: React.FC = memo(({ locations={[0, 0.3, 0.55, 0.75, 0.9, 1]} style={styles.heroGradient} > + {/* Enhanced bottom fade with stronger gradient */} + {/* Optimized Title/Logo */} @@ -998,23 +1007,6 @@ const HeroSection: React.FC = memo(({ /> - - {/* Ultra-subtle bottom fade for feather-light seamless blend */} - ); }); @@ -1043,15 +1035,15 @@ const styles = StyleSheet.create({ bottom: 0, left: 0, right: 0, - height: 200, - zIndex: -1, + height: 400, + zIndex: 1, }, heroContent: { padding: 16, paddingTop: 8, paddingBottom: 8, position: 'relative', - zIndex: 5, + zIndex: 2, }, logoContainer: { alignItems: 'center', @@ -1106,7 +1098,6 @@ const styles = StyleSheet.create({ justifyContent: 'center', width: '100%', position: 'relative', - zIndex: 10, }, actionButton: { flexDirection: 'row', @@ -1276,153 +1267,153 @@ const styles = StyleSheet.create({ borderRadius: 25, backgroundColor: 'rgba(255,255,255,0.15)', }, - watchedIndicator: { - position: 'absolute', - top: 4, - right: 4, - backgroundColor: 'rgba(0,0,0,0.6)', - borderRadius: 8, - width: 16, - height: 16, - alignItems: 'center', - justifyContent: 'center', - }, - watchedPlayButton: { - backgroundColor: '#1e1e1e', - borderWidth: 1, - borderColor: 'rgba(255,255,255,0.3)', - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 4, - }, - watchedPlayButtonText: { - color: '#fff', - fontWeight: '700', - marginLeft: 6, - fontSize: 15, - }, - // Enhanced progress indicator styles - progressShimmer: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - borderRadius: 2, - backgroundColor: 'rgba(255,255,255,0.1)', - }, - completionGlow: { - position: 'absolute', - top: -2, - left: -2, - right: -2, - bottom: -2, - borderRadius: 4, - backgroundColor: 'rgba(0,255,136,0.2)', - }, - completionIndicator: { - position: 'absolute', - right: 4, - top: -6, - bottom: -6, - width: 16, - height: 16, - borderRadius: 8, - alignItems: 'center', - justifyContent: 'center', - }, - completionGradient: { - width: 16, - height: 16, - borderRadius: 8, - alignItems: 'center', - justifyContent: 'center', - }, - sparkleContainer: { - position: 'absolute', - top: -10, - left: 0, - right: 0, - bottom: -10, - borderRadius: 2, - }, - sparkle: { - position: 'absolute', - width: 8, - height: 8, - borderRadius: 4, - alignItems: 'center', - justifyContent: 'center', - }, - progressInfoMain: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - marginBottom: 2, - }, - watchProgressMainText: { - fontSize: 11, - fontWeight: '600', - textAlign: 'center', - }, - watchProgressSubText: { - fontSize: 9, - textAlign: 'center', - opacity: 0.8, - marginBottom: 1, - }, - syncStatusContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - marginTop: 2, - width: '100%', - flexWrap: 'wrap', - }, - syncStatusText: { - fontSize: 9, - marginLeft: 4, - fontWeight: '500', - }, - traktSyncButtonEnhanced: { - position: 'absolute', - top: 8, - right: 8, - width: 24, - height: 24, - borderRadius: 12, - overflow: 'hidden', - }, - traktSyncButtonInline: { - marginLeft: 8, - width: 20, - height: 20, - borderRadius: 10, - overflow: 'hidden', - }, - syncButtonGradient: { - width: 24, - height: 24, - borderRadius: 12, - alignItems: 'center', - justifyContent: 'center', - }, - syncButtonGradientInline: { - width: 20, - height: 20, - borderRadius: 10, - alignItems: 'center', - justifyContent: 'center', - }, - traktIndicatorGradient: { - width: 16, - height: 16, - borderRadius: 8, - alignItems: 'center', - justifyContent: 'center', - }, + watchedIndicator: { + position: 'absolute', + top: 4, + right: 4, + backgroundColor: 'rgba(0,0,0,0.6)', + borderRadius: 8, + width: 16, + height: 16, + alignItems: 'center', + justifyContent: 'center', + }, + watchedPlayButton: { + backgroundColor: '#1e1e1e', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.3)', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 4, + }, + watchedPlayButtonText: { + color: '#fff', + fontWeight: '700', + marginLeft: 6, + fontSize: 15, + }, + // Enhanced progress indicator styles + progressShimmer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + borderRadius: 2, + backgroundColor: 'rgba(255,255,255,0.1)', + }, + completionGlow: { + position: 'absolute', + top: -2, + left: -2, + right: -2, + bottom: -2, + borderRadius: 4, + backgroundColor: 'rgba(0,255,136,0.2)', + }, + completionIndicator: { + position: 'absolute', + right: 4, + top: -6, + bottom: -6, + width: 16, + height: 16, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, + completionGradient: { + width: 16, + height: 16, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, + sparkleContainer: { + position: 'absolute', + top: -10, + left: 0, + right: 0, + bottom: -10, + borderRadius: 2, + }, + sparkle: { + position: 'absolute', + width: 8, + height: 8, + borderRadius: 4, + alignItems: 'center', + justifyContent: 'center', + }, + progressInfoMain: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 2, + }, + watchProgressMainText: { + fontSize: 11, + fontWeight: '600', + textAlign: 'center', + }, + watchProgressSubText: { + fontSize: 9, + textAlign: 'center', + opacity: 0.8, + marginBottom: 1, + }, + syncStatusContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginTop: 2, + width: '100%', + flexWrap: 'wrap', + }, + syncStatusText: { + fontSize: 9, + marginLeft: 4, + fontWeight: '500', + }, + traktSyncButtonEnhanced: { + position: 'absolute', + top: 8, + right: 8, + width: 24, + height: 24, + borderRadius: 12, + overflow: 'hidden', + }, + traktSyncButtonInline: { + marginLeft: 8, + width: 20, + height: 20, + borderRadius: 10, + overflow: 'hidden', + }, + syncButtonGradient: { + width: 24, + height: 24, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + }, + syncButtonGradientInline: { + width: 20, + height: 20, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + }, + traktIndicatorGradient: { + width: 16, + height: 16, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, }); export default HeroSection; \ No newline at end of file diff --git a/src/hooks/useDominantColor.ts b/src/hooks/useDominantColor.ts index def0a07c..96a55bc3 100644 --- a/src/hooks/useDominantColor.ts +++ b/src/hooks/useDominantColor.ts @@ -11,6 +11,133 @@ interface DominantColorResult { // Simple in-memory cache for extracted colors const colorCache = new Map(); +// Helper function to calculate color vibrancy +const calculateVibrancy = (hex: string): number => { + const r = parseInt(hex.substr(1, 2), 16); + const g = parseInt(hex.substr(3, 2), 16); + const b = parseInt(hex.substr(5, 2), 16); + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const saturation = max === 0 ? 0 : (max - min) / max; + + return saturation * (max / 255); +}; + +// Helper function to calculate color brightness +const calculateBrightness = (hex: string): number => { + const r = parseInt(hex.substr(1, 2), 16); + const g = parseInt(hex.substr(3, 2), 16); + const b = parseInt(hex.substr(5, 2), 16); + + return (r * 299 + g * 587 + b * 114) / 1000; +}; + +// Helper function to darken a color +const darkenColor = (hex: string, factor: number = 0.1): string => { + const r = parseInt(hex.substr(1, 2), 16); + const g = parseInt(hex.substr(3, 2), 16); + const b = parseInt(hex.substr(5, 2), 16); + + const newR = Math.floor(r * factor); + const newG = Math.floor(g * factor); + const newB = Math.floor(b * factor); + + return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`; +}; + +// Enhanced color selection logic +const selectBestColor = (result: ImageColorsResult): string => { + let candidates: string[] = []; + + if (result.platform === 'android') { + // Collect all available colors + candidates = [ + result.dominant, + result.vibrant, + result.darkVibrant, + result.muted, + result.darkMuted, + result.lightVibrant, + result.lightMuted, + result.average + ].filter(Boolean); + } else if (result.platform === 'ios') { + candidates = [ + result.primary, + result.secondary, + result.background, + result.detail + ].filter(Boolean); + } else if (result.platform === 'web') { + candidates = [ + result.dominant, + result.vibrant, + result.darkVibrant, + result.muted, + result.darkMuted, + result.lightVibrant, + result.lightMuted + ].filter(Boolean); + } + + if (candidates.length === 0) { + return '#1a1a1a'; + } + + // Score each color based on vibrancy and appropriateness for backgrounds + const scoredColors = candidates.map(color => { + const brightness = calculateBrightness(color); + const vibrancy = calculateVibrancy(color); + + // Prefer colors that are: + // 1. Not too bright (good for backgrounds) + // 2. Have decent vibrancy (not too gray) + // 3. Not too dark (still visible) + let score = 0; + + // Brightness scoring (prefer medium-dark colors) + if (brightness >= 30 && brightness <= 120) { + score += 3; + } else if (brightness >= 15 && brightness <= 150) { + score += 2; + } else if (brightness >= 5) { + score += 1; + } + + // Vibrancy scoring (prefer some color over pure gray) + if (vibrancy >= 0.3) { + score += 3; + } else if (vibrancy >= 0.15) { + score += 2; + } else if (vibrancy >= 0.05) { + score += 1; + } + + return { color, score, brightness, vibrancy }; + }); + + // Sort by score (highest first) + scoredColors.sort((a, b) => b.score - a.score); + + // Get the best color + let bestColor = scoredColors[0].color; + const bestBrightness = scoredColors[0].brightness; + + // Apply more aggressive darkening to make colors darker overall + if (bestBrightness > 60) { + bestColor = darkenColor(bestColor, 0.18); + } else if (bestBrightness > 40) { + bestColor = darkenColor(bestColor, 0.3); + } else if (bestBrightness > 20) { + bestColor = darkenColor(bestColor, 0.5); + } else { + bestColor = darkenColor(bestColor, 0.7); + } + + return bestColor; +}; + // Preload function to start extraction early export const preloadDominantColor = async (imageUri: string | null) => { if (!imageUri || colorCache.has(imageUri)) return; @@ -22,34 +149,11 @@ export const preloadDominantColor = async (imageUri: string | null) => { fallback: '#1a1a1a', cache: true, key: imageUri, - quality: 'low', + quality: 'high', // Use higher quality for better color extraction + pixelSpacing: 3, // Better sampling (Android only) }); - let extractedColor = '#1a1a1a'; - - if (result.platform === 'android') { - extractedColor = result.darkMuted || result.muted || result.darkVibrant || result.dominant || '#1a1a1a'; - } else if (result.platform === 'ios') { - extractedColor = result.background || result.primary || '#1a1a1a'; - } else if (result.platform === 'web') { - extractedColor = result.darkMuted || result.muted || result.dominant || '#1a1a1a'; - } - - // Apply darkening logic - const hex = extractedColor.replace('#', ''); - const r = parseInt(hex.substr(0, 2), 16); - const g = parseInt(hex.substr(2, 2), 16); - const b = parseInt(hex.substr(4, 2), 16); - const brightness = (r * 299 + g * 587 + b * 114) / 1000; - - if (brightness > 50) { - const darkenFactor = 0.15; - const newR = Math.floor(r * darkenFactor); - const newG = Math.floor(g * darkenFactor); - const newB = Math.floor(b * darkenFactor); - extractedColor = `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`; - } - + const extractedColor = selectBestColor(result); colorCache.set(imageUri, extractedColor); } catch (err) { console.warn('[preloadDominantColor] Failed to preload color:', err); @@ -92,41 +196,11 @@ export const useDominantColor = (imageUri: string | null): DominantColorResult = fallback: '#1a1a1a', cache: true, key: uri, - quality: 'low', // Use low quality for better performance + quality: 'high', // Use higher quality for better accuracy + pixelSpacing: 3, // Better pixel sampling (Android only) }); - let extractedColor = '#1a1a1a'; // Default fallback - - // Handle different platform results - if (result.platform === 'android') { - // Prefer darker, more muted colors for background - extractedColor = result.darkMuted || result.muted || result.darkVibrant || result.dominant || '#1a1a1a'; - } else if (result.platform === 'ios') { - // Use background color from iOS, or fallback to primary - extractedColor = result.background || result.primary || '#1a1a1a'; - } else if (result.platform === 'web') { - // Use muted colors for web - extractedColor = result.darkMuted || result.muted || result.dominant || '#1a1a1a'; - } - - // Ensure the color is dark enough for a background - // Convert hex to RGB to check brightness - const hex = extractedColor.replace('#', ''); - const r = parseInt(hex.substr(0, 2), 16); - const g = parseInt(hex.substr(2, 2), 16); - const b = parseInt(hex.substr(4, 2), 16); - - // Calculate brightness (0-255) - const brightness = (r * 299 + g * 587 + b * 114) / 1000; - - // If too bright, darken it significantly - if (brightness > 50) { - const darkenFactor = 0.15; - const newR = Math.floor(r * darkenFactor); - const newG = Math.floor(g * darkenFactor); - const newB = Math.floor(b * darkenFactor); - extractedColor = `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`; - } + const extractedColor = selectBestColor(result); // Cache the extracted color for future use colorCache.set(uri, extractedColor); diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index ccbe77a3..b991e3df 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -30,6 +30,7 @@ import Animated, { useSharedValue, withTiming, runOnJS, + Easing, } from 'react-native-reanimated'; import { RouteProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; @@ -115,13 +116,36 @@ const MetadataScreen: React.FC = () => { const { dominantColor, loading: colorLoading } = useDominantColor(heroImageUri); - // Memoized background color with immediate fallback and smooth transition + // Create a shared value for animated background color transitions + const backgroundColorShared = useSharedValue(currentTheme.colors.darkBackground); + + // Update the shared value when dominant color changes + useEffect(() => { + if (dominantColor && dominantColor !== '#1a1a1a' && dominantColor !== null && dominantColor !== currentTheme.colors.darkBackground) { + // Smoothly transition to the new color + backgroundColorShared.value = withTiming(dominantColor, { + duration: 800, // Longer duration for smoother transition + easing: Easing.bezier(0.25, 0.1, 0.25, 1), // Smooth easing curve + }); + } else { + // Transition back to theme background if needed + backgroundColorShared.value = withTiming(currentTheme.colors.darkBackground, { + duration: 800, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), + }); + } + }, [dominantColor, currentTheme.colors.darkBackground]); + + // Create an animated style for the background color + const animatedBackgroundStyle = useAnimatedStyle(() => ({ + backgroundColor: backgroundColorShared.value, + })); + + // For compatibility with existing code, maintain the static value as well const dynamicBackgroundColor = useMemo(() => { - // Start with theme background, then use extracted color when available and different from fallback if (dominantColor && dominantColor !== '#1a1a1a' && dominantColor !== null && dominantColor !== currentTheme.colors.darkBackground) { return dominantColor; } - // Always return theme background as immediate fallback return currentTheme.colors.darkBackground; }, [dominantColor, currentTheme.colors.darkBackground]); @@ -465,8 +489,9 @@ const MetadataScreen: React.FC = () => { } return ( + @@ -581,6 +606,7 @@ const MetadataScreen: React.FC = () => { /> )} + ); };