From ec23dcc3cb16ee57f91094f198ef2af0b1adae10 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 5 Oct 2025 23:45:30 +0530 Subject: [PATCH] ui changes --- src/components/metadata/CommentsSection.tsx | 186 +++++++++++++++-- src/components/metadata/MetadataDetails.tsx | 16 +- src/utils/ageRatingColors.ts | 216 ++++++++++++++++++++ 3 files changed, 399 insertions(+), 19 deletions(-) create mode 100644 src/utils/ageRatingColors.ts diff --git a/src/components/metadata/CommentsSection.tsx b/src/components/metadata/CommentsSection.tsx index c390889a..c7dcb47f 100644 --- a/src/components/metadata/CommentsSection.tsx +++ b/src/components/metadata/CommentsSection.tsx @@ -10,6 +10,7 @@ import { Alert, ScrollView, Animated, + Linking, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import TraktIcon from '../../../assets/rating-icons/trakt.svg'; @@ -35,6 +36,142 @@ interface CommentItemProps { theme: any; } +// Minimal markdown renderer with inline spoiler handling +const MarkdownText: React.FC<{ + text: string; + theme: any; + numberOfLines?: number; + revealedInlineSpoilers: boolean; + onSpoilerPress?: () => void; + textStyle?: any; +}> = ({ text, theme, numberOfLines, revealedInlineSpoilers, onSpoilerPress, textStyle }) => { + // Regexes for simple markdown + const linkRegex = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g; // [text](url) + const boldRegex = /\*\*([^*]+)\*\*/g; // **bold** + const italicRegex = /\*([^*]+)\*/g; // *italic* + const codeRegex = /`([^`]+)`/g; // `code` + const spoilerRegex = /\[spoiler\]([\s\S]*?)\[\/spoiler\]/gi; + + // Tokenize spoilers first to keep nesting simple + const spoilerTokens: Array<{ type: 'spoiler' | 'text'; content: string }> = []; + let lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = spoilerRegex.exec(text)) !== null) { + if (match.index > lastIndex) { + spoilerTokens.push({ type: 'text', content: text.slice(lastIndex, match.index) }); + } + spoilerTokens.push({ type: 'spoiler', content: match[1] }); + lastIndex = spoilerRegex.lastIndex; + } + if (lastIndex < text.length) { + spoilerTokens.push({ type: 'text', content: text.slice(lastIndex) }); + } + + const renderInline = (segment: string, keyPrefix: string) => { + // Process code + const codeSplit = segment.split(codeRegex); + const codeNodes: React.ReactNode[] = []; + for (let i = 0; i < codeSplit.length; i++) { + if (i % 2 === 1) { + codeNodes.push( + + {codeSplit[i]} + + ); + } else { + // process bold and italic and links inside normal text + let chunk = codeSplit[i] ?? ''; + const parts: React.ReactNode[] = []; + + // Links + let cursor = 0; + let linkMatch: RegExpExecArray | null; + while ((linkMatch = linkRegex.exec(chunk)) !== null) { + const before = chunk.slice(cursor, linkMatch.index); + if (before) parts.push({before}); + const label = linkMatch[1]; + const url = linkMatch[2]; + parts.push( + Linking.openURL(url)} + suppressHighlighting + > + {label} + + ); + cursor = linkMatch.index + linkMatch[0].length; + } + if (cursor < chunk.length) { + parts.push({chunk.slice(cursor)}); + } + + // Wrap bold & italic via nested Text by replacing markers + const applyFormat = (nodes: React.ReactNode[]): React.ReactNode[] => { + return nodes.flatMap((node, idx) => { + if (typeof node !== 'string' && !(node as any).props?.children) return node; + const str = typeof node === 'string' ? node : (node as any).props.children as string; + if (typeof str !== 'string') return node; + + // bold + const boldSplit = str.split(boldRegex); + const boldNodes: React.ReactNode[] = []; + for (let b = 0; b < boldSplit.length; b++) { + if (b % 2 === 1) { + boldNodes.push({boldSplit[b]}); + } else { + // italic inside non-bold chunk + const italSplit = boldSplit[b].split(italicRegex); + for (let it = 0; it < italSplit.length; it++) { + if (it % 2 === 1) { + boldNodes.push({italSplit[it]}); + } else { + if (italSplit[it]) boldNodes.push({italSplit[it]}); + } + } + } + } + return boldNodes; + }); + }; + + codeNodes.push( + + {applyFormat(parts)} + + ); + } + } + return codeNodes; + }; + + return ( + + {spoilerTokens.map((tok, idx) => { + if (tok.type === 'text') { + return {renderInline(tok.content, `seg-${idx}`)}; + } + if (revealedInlineSpoilers) { + return {renderInline(tok.content, `spl-${idx}`)}; + } + return ( + + + + [spoiler] + + + + ); + })} + + ); +}; + // Compact comment card for horizontal scrolling const CompactCommentCard: React.FC<{ comment: TraktContentComment; @@ -67,9 +204,7 @@ const CompactCommentCard: React.FC<{ const hasSpoiler = comment.spoiler; const shouldBlurContent = hasSpoiler && !isSpoilerRevealed; - const truncatedComment = comment.comment.length > 100 - ? comment.comment.substring(0, 100) + '...' - : comment.comment; + // We render markdown with inline spoilers; limit lines to keep card compact // Format relative time const formatRelativeTime = (dateString: string) => { @@ -181,13 +316,18 @@ const CompactCommentCard: React.FC<{ {/* Comment Preview - Flexible area that fills space */} - - {shouldBlurContent ? '⚠️ This comment contains spoilers. Tap to reveal.' : truncatedComment} - + {shouldBlurContent ? ( + ⚠️ This comment contains spoilers. Tap to reveal. + ) : ( + + )} {/* Meta Info - Fixed at bottom */} @@ -371,7 +511,7 @@ const ExpandedCommentBottomSheet: React.FC<{ )} - {/* Full Comment */} + {/* Full Comment (Markdown with inline spoilers) */} {shouldBlurModalContent ? ( @@ -388,9 +528,14 @@ const ExpandedCommentBottomSheet: React.FC<{ ) : ( - - {comment.comment} - + + + )} {/* Comment Meta */} @@ -788,7 +933,7 @@ export const CommentBottomSheet: React.FC<{ )} - {/* Full Comment */} + {/* Full Comment (Markdown with inline spoilers) */} {shouldBlurModalContent ? ( @@ -805,9 +950,14 @@ export const CommentBottomSheet: React.FC<{ ) : ( - - {comment.comment} - + + + )} {/* Comment Meta */} diff --git a/src/components/metadata/MetadataDetails.tsx b/src/components/metadata/MetadataDetails.tsx index 2532b761..13626f29 100644 --- a/src/components/metadata/MetadataDetails.tsx +++ b/src/components/metadata/MetadataDetails.tsx @@ -19,6 +19,7 @@ import Animated, { } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; import { isMDBListEnabled } from '../../screens/MDBListSettingsScreen'; +import { getAgeRatingColor } from '../../utils/ageRatingColors'; // MetadataSourceSelector removed interface MetadataDetailsProps { @@ -153,7 +154,11 @@ function formatRuntime(runtime: string): string { )} {metadata.certification && ( - {metadata.certification} + {metadata.certification} )} {metadata.imdbRating && !isMDBEnabled && ( @@ -273,6 +278,15 @@ const styles = StyleSheet.create({ textTransform: 'uppercase', opacity: 0.9, }, + premiumOutlinedText: { + // Subtle premium outline effect for letters + textShadowColor: 'rgba(0, 0, 0, 0.3)', + textShadowOffset: { width: 0, height: 1 }, + textShadowRadius: 2, + // Enhanced letter definition + fontWeight: '800', + letterSpacing: 0.5, + }, ratingContainer: { flexDirection: 'row', alignItems: 'center', diff --git a/src/utils/ageRatingColors.ts b/src/utils/ageRatingColors.ts new file mode 100644 index 00000000..d0e2500f --- /dev/null +++ b/src/utils/ageRatingColors.ts @@ -0,0 +1,216 @@ +/** + * Premium color mapping for age ratings + * Provides consistent, visually appealing colors for different age rating systems + */ + +// Movie Ratings (MPA) - Premium Colors +export const MOVIE_RATING_COLORS = { + 'G': '#00C851', // Vibrant Green - General Audiences + 'PG': '#FFBB33', // Golden Yellow - Parental Guidance Suggested + 'PG-13': '#FF8800', // Premium Orange - Parents Strongly Cautioned + 'R': '#FF4444', // Premium Red - Restricted + 'NC-17': '#CC0000', // Deep Crimson - No One 17 and Under Admitted + 'UNRATED': '#666666', // Neutral Gray - Unrated content + 'NOT RATED': '#666666', // Neutral Gray - Not Rated content +} as const; + +// TV Ratings (TV Parental Guidelines) - Premium Colors +export const TV_RATING_COLORS = { + 'TV-Y': '#00C851', // Vibrant Green - All Children + 'TV-Y7': '#66BB6A', // Light Green - Directed to Older Children + 'TV-G': '#00C851', // Vibrant Green - General Audience + 'TV-PG': '#FFBB33', // Golden Yellow - Parental Guidance Suggested + 'TV-14': '#FF8800', // Premium Orange - Parents Strongly Cautioned + 'TV-MA': '#FF4444', // Premium Red - Mature Audience Only + 'NR': '#666666', // Neutral Gray - Not Rated + 'UNRATED': '#666666', // Neutral Gray - Unrated content +} as const; + +// Common/Generic age rating patterns that might appear +export const COMMON_RATING_PATTERNS = { + // Movie patterns + 'G': '#00C851', + 'PG': '#FFBB33', + 'PG-13': '#FF8800', + 'R': '#FF4444', + 'NC-17': '#CC0000', + 'UNRATED': '#666666', + 'NOT RATED': '#666666', + + // TV patterns + 'TV-Y': '#00C851', + 'TV-Y7': '#66BB6A', + 'TV-G': '#00C851', + 'TV-PG': '#FFBB33', + 'TV-14': '#FF8800', + 'TV-MA': '#FF4444', + 'NR': '#666666', + + // International/common patterns + 'U': '#00C851', // Universal (UK) - Green + 'U/A': '#00C851', // Universal/Adult (India) - Green + 'A': '#FF8800', // Adult (India) - Orange + 'S': '#FF4444', // Restricted (India) - Red + 'UA': '#FFBB33', // Parental Guidance (India) - Yellow + '12': '#FF8800', // 12+ (Various countries) - Orange + '12A': '#FFBB33', // 12A (UK) - Yellow + '15': '#FF4444', // 15+ (Various countries) - Red + '18': '#CC0000', // 18+ (Various countries) - Dark Red + '18+': '#CC0000', // 18+ - Dark Red + 'R18': '#CC0000', // R18 (Australia) - Dark Red + 'X': '#CC0000', // X (Adult) - Dark Red +} as const; + +/** + * Get the appropriate color for a movie rating + * @param rating - The movie rating (e.g., 'PG-13', 'R', 'G') + * @returns Hex color code for the rating + */ +export function getMovieRatingColor(rating: string | null | undefined): string { + if (!rating) return '#666666'; // Default gray for no rating + + const normalizedRating = rating.toUpperCase().trim(); + + // Direct lookup in movie ratings + if (normalizedRating in MOVIE_RATING_COLORS) { + return MOVIE_RATING_COLORS[normalizedRating as keyof typeof MOVIE_RATING_COLORS]; + } + + // Check common patterns + if (normalizedRating in COMMON_RATING_PATTERNS) { + return COMMON_RATING_PATTERNS[normalizedRating as keyof typeof COMMON_RATING_PATTERNS]; + } + + // Special handling for some common variations + if (normalizedRating.includes('PG') && normalizedRating.includes('13')) { + return '#FF8800'; // PG-13 variations + } + + if (normalizedRating.includes('TV') && normalizedRating.includes('MA')) { + return '#FF4444'; // TV-MA variations + } + + // Default fallback + return '#666666'; +} + +/** + * Get the appropriate color for a TV rating + * @param rating - The TV rating (e.g., 'TV-14', 'TV-MA', 'TV-Y') + * @returns Hex color code for the rating + */ +export function getTVRatingColor(rating: string | null | undefined): string { + if (!rating) return '#666666'; // Default gray for no rating + + const normalizedRating = rating.toUpperCase().trim(); + + // Direct lookup in TV ratings + if (normalizedRating in TV_RATING_COLORS) { + return TV_RATING_COLORS[normalizedRating as keyof typeof TV_RATING_COLORS]; + } + + // Check common patterns + if (normalizedRating in COMMON_RATING_PATTERNS) { + return COMMON_RATING_PATTERNS[normalizedRating as keyof typeof COMMON_RATING_PATTERNS]; + } + + // Special handling for TV rating variations + if (normalizedRating.startsWith('TV-')) { + const tvRating = normalizedRating as keyof typeof TV_RATING_COLORS; + if (tvRating in TV_RATING_COLORS) { + return TV_RATING_COLORS[tvRating]; + } + } + + // Default fallback + return '#666666'; +} + +/** + * Get the appropriate color for any content rating based on content type + * @param rating - The rating string + * @param contentType - 'movie' or 'series' to determine which rating system to use + * @returns Hex color code for the rating + */ +export function getAgeRatingColor(rating: string | null | undefined, contentType: 'movie' | 'series' = 'movie'): string { + if (!rating) return '#666666'; + + // For movies, prioritize movie rating system + if (contentType === 'movie') { + return getMovieRatingColor(rating); + } + + // For series/TV shows, check TV ratings first, then fall back to movie ratings + const tvColor = getTVRatingColor(rating); + if (tvColor !== '#666666') { + return tvColor; + } + + // Fallback to movie rating system for series + return getMovieRatingColor(rating); +} + +/** + * Get a human-readable description for an age rating + * @param rating - The rating string + * @param contentType - Content type for context + * @returns Description of what the rating means + */ +export function getAgeRatingDescription(rating: string | null | undefined, contentType: 'movie' | 'series' = 'movie'): string { + if (!rating) return 'Not Rated'; + + const normalizedRating = rating.toUpperCase().trim(); + + // Movie rating descriptions + const movieDescriptions: Record = { + 'G': 'General Audiences - All ages admitted', + 'PG': 'Parental Guidance Suggested - Some material may not be suitable for children', + 'PG-13': 'Parents Strongly Cautioned - Some material may be inappropriate for children under 13', + 'R': 'Restricted - Under 17 requires accompanying parent or adult guardian', + 'NC-17': 'No One 17 and Under Admitted - Clearly adult content', + 'UNRATED': 'Unrated - Content rating not assigned', + 'NOT RATED': 'Not Rated - Content rating not assigned', + }; + + // TV rating descriptions + const tvDescriptions: Record = { + 'TV-Y': 'All Children - Designed to be appropriate for all children', + 'TV-Y7': 'Directed to Older Children - Designed for children age 7 and above', + 'TV-G': 'General Audience - Most parents would find suitable for all ages', + 'TV-PG': 'Parental Guidance Suggested - May contain material unsuitable for younger children', + 'TV-14': 'Parents Strongly Cautioned - May contain material unsuitable for children under 14', + 'TV-MA': 'Mature Audience Only - Specifically designed for adults', + 'NR': 'Not Rated - Content rating not assigned', + 'UNRATED': 'Unrated - Content rating not assigned', + }; + + if (contentType === 'movie' && normalizedRating in movieDescriptions) { + return movieDescriptions[normalizedRating]; + } + + if (contentType === 'series' && normalizedRating in tvDescriptions) { + return tvDescriptions[normalizedRating]; + } + + // Fallback descriptions for common international ratings + const commonDescriptions: Record = { + 'U': 'Universal - Suitable for all ages', + 'U/A': 'Universal with Adult guidance - Parental discretion advised', + 'A': 'Adults only - Not suitable for children', + 'S': 'Restricted - Not suitable for children', + 'UA': 'Parental Guidance - Parental discretion advised', + '12': 'Suitable for ages 12 and above', + '12A': 'Suitable for ages 12 and above when accompanied by an adult', + '15': 'Suitable for ages 15 and above', + '18': 'Suitable for ages 18 and above only', + '18+': 'Adult content - 18 and above only', + 'R18': 'Restricted 18 - Adult content only', + 'X': 'Adult content - Explicit material', + }; + + if (normalizedRating in commonDescriptions) { + return commonDescriptions[normalizedRating]; + } + + return `${rating} - Rating information not available`; +}