mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
ui changes
This commit is contained in:
parent
13d72338a5
commit
ec23dcc3cb
3 changed files with 399 additions and 19 deletions
|
|
@ -10,6 +10,7 @@ import {
|
||||||
Alert,
|
Alert,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Animated,
|
Animated,
|
||||||
|
Linking,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import TraktIcon from '../../../assets/rating-icons/trakt.svg';
|
import TraktIcon from '../../../assets/rating-icons/trakt.svg';
|
||||||
|
|
@ -35,6 +36,142 @@ interface CommentItemProps {
|
||||||
theme: any;
|
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(
|
||||||
|
<Text key={`${keyPrefix}-code-${i}`} style={[{ fontFamily: 'Courier', backgroundColor: theme.colors.card, paddingHorizontal: 3, borderRadius: 3 }, textStyle]}>
|
||||||
|
{codeSplit[i]}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
} 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(<Text key={`${keyPrefix}-lnk-before-${cursor}`} style={textStyle}>{before}</Text>);
|
||||||
|
const label = linkMatch[1];
|
||||||
|
const url = linkMatch[2];
|
||||||
|
parts.push(
|
||||||
|
<Text
|
||||||
|
key={`${keyPrefix}-link-${cursor}`}
|
||||||
|
style={[{ color: theme.colors.primary }, textStyle]}
|
||||||
|
onPress={() => Linking.openURL(url)}
|
||||||
|
suppressHighlighting
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
cursor = linkMatch.index + linkMatch[0].length;
|
||||||
|
}
|
||||||
|
if (cursor < chunk.length) {
|
||||||
|
parts.push(<Text key={`${keyPrefix}-lnk-tail`} style={textStyle}>{chunk.slice(cursor)}</Text>);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(<Text key={`${keyPrefix}-b-${idx}-${b}`} style={[{ fontWeight: '700' }, textStyle]}>{boldSplit[b]}</Text>);
|
||||||
|
} 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(<Text key={`${keyPrefix}-i-${idx}-${b}-${it}`} style={[{ fontStyle: 'italic' }, textStyle]}>{italSplit[it]}</Text>);
|
||||||
|
} else {
|
||||||
|
if (italSplit[it]) boldNodes.push(<Text key={`${keyPrefix}-t-${idx}-${b}-${it}`} style={textStyle}>{italSplit[it]}</Text>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return boldNodes;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
codeNodes.push(
|
||||||
|
<Text key={`${keyPrefix}-txt-${i}`} style={[{ color: theme.colors.highEmphasis }, textStyle]}>
|
||||||
|
{applyFormat(parts)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return codeNodes;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text numberOfLines={numberOfLines} ellipsizeMode="tail" style={[{ color: theme.colors.highEmphasis }, textStyle]}>
|
||||||
|
{spoilerTokens.map((tok, idx) => {
|
||||||
|
if (tok.type === 'text') {
|
||||||
|
return <Text key={`seg-${idx}`} style={textStyle}>{renderInline(tok.content, `seg-${idx}`)}</Text>;
|
||||||
|
}
|
||||||
|
if (revealedInlineSpoilers) {
|
||||||
|
return <Text key={`spl-${idx}`} style={textStyle}>{renderInline(tok.content, `spl-${idx}`)}</Text>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Text key={`splmask-${idx}`} style={textStyle}>
|
||||||
|
<Text style={textStyle}> </Text>
|
||||||
|
<Text
|
||||||
|
onPress={onSpoilerPress}
|
||||||
|
style={[{ color: theme.colors.error, fontWeight: '700' }, textStyle]}
|
||||||
|
>
|
||||||
|
[spoiler]
|
||||||
|
</Text>
|
||||||
|
<Text style={textStyle}> </Text>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Compact comment card for horizontal scrolling
|
// Compact comment card for horizontal scrolling
|
||||||
const CompactCommentCard: React.FC<{
|
const CompactCommentCard: React.FC<{
|
||||||
comment: TraktContentComment;
|
comment: TraktContentComment;
|
||||||
|
|
@ -67,9 +204,7 @@ const CompactCommentCard: React.FC<{
|
||||||
const hasSpoiler = comment.spoiler;
|
const hasSpoiler = comment.spoiler;
|
||||||
const shouldBlurContent = hasSpoiler && !isSpoilerRevealed;
|
const shouldBlurContent = hasSpoiler && !isSpoilerRevealed;
|
||||||
|
|
||||||
const truncatedComment = comment.comment.length > 100
|
// We render markdown with inline spoilers; limit lines to keep card compact
|
||||||
? comment.comment.substring(0, 100) + '...'
|
|
||||||
: comment.comment;
|
|
||||||
|
|
||||||
// Format relative time
|
// Format relative time
|
||||||
const formatRelativeTime = (dateString: string) => {
|
const formatRelativeTime = (dateString: string) => {
|
||||||
|
|
@ -181,13 +316,18 @@ const CompactCommentCard: React.FC<{
|
||||||
|
|
||||||
{/* Comment Preview - Flexible area that fills space */}
|
{/* Comment Preview - Flexible area that fills space */}
|
||||||
<View style={[styles.commentContainer, shouldBlurContent ? styles.blurredContent : undefined]}>
|
<View style={[styles.commentContainer, shouldBlurContent ? styles.blurredContent : undefined]}>
|
||||||
<Text
|
{shouldBlurContent ? (
|
||||||
style={[styles.compactComment, { color: theme.colors.highEmphasis }]}
|
<Text style={[styles.compactComment, { color: theme.colors.highEmphasis }]}>⚠️ This comment contains spoilers. Tap to reveal.</Text>
|
||||||
numberOfLines={3}
|
) : (
|
||||||
ellipsizeMode="tail"
|
<MarkdownText
|
||||||
>
|
text={comment.comment}
|
||||||
{shouldBlurContent ? '⚠️ This comment contains spoilers. Tap to reveal.' : truncatedComment}
|
theme={theme}
|
||||||
</Text>
|
numberOfLines={3}
|
||||||
|
revealedInlineSpoilers={isSpoilerRevealed}
|
||||||
|
onSpoilerPress={onSpoilerPress}
|
||||||
|
textStyle={styles.compactComment}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Meta Info - Fixed at bottom */}
|
{/* Meta Info - Fixed at bottom */}
|
||||||
|
|
@ -371,7 +511,7 @@ const ExpandedCommentBottomSheet: React.FC<{
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Full Comment */}
|
{/* Full Comment (Markdown with inline spoilers) */}
|
||||||
{shouldBlurModalContent ? (
|
{shouldBlurModalContent ? (
|
||||||
<View style={styles.spoilerContainer}>
|
<View style={styles.spoilerContainer}>
|
||||||
<View style={[styles.spoilerIcon, { backgroundColor: theme.colors.card }]}>
|
<View style={[styles.spoilerIcon, { backgroundColor: theme.colors.card }]}>
|
||||||
|
|
@ -388,9 +528,14 @@ const ExpandedCommentBottomSheet: React.FC<{
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Text style={[styles.modalComment, { color: theme.colors.highEmphasis }]}>
|
<View style={{ marginBottom: 16 }}>
|
||||||
{comment.comment}
|
<MarkdownText
|
||||||
</Text>
|
text={comment.comment}
|
||||||
|
theme={theme}
|
||||||
|
revealedInlineSpoilers={true}
|
||||||
|
textStyle={styles.modalComment}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Comment Meta */}
|
{/* Comment Meta */}
|
||||||
|
|
@ -788,7 +933,7 @@ export const CommentBottomSheet: React.FC<{
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Full Comment */}
|
{/* Full Comment (Markdown with inline spoilers) */}
|
||||||
{shouldBlurModalContent ? (
|
{shouldBlurModalContent ? (
|
||||||
<View style={styles.spoilerContainer}>
|
<View style={styles.spoilerContainer}>
|
||||||
<View style={[styles.spoilerIcon, { backgroundColor: theme.colors.card }]}>
|
<View style={[styles.spoilerIcon, { backgroundColor: theme.colors.card }]}>
|
||||||
|
|
@ -805,9 +950,14 @@ export const CommentBottomSheet: React.FC<{
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Text style={[styles.modalComment, { color: theme.colors.highEmphasis }]}>
|
<View style={{ marginBottom: 16 }}>
|
||||||
{comment.comment}
|
<MarkdownText
|
||||||
</Text>
|
text={comment.comment}
|
||||||
|
theme={theme}
|
||||||
|
revealedInlineSpoilers={true}
|
||||||
|
textStyle={styles.modalComment}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Comment Meta */}
|
{/* Comment Meta */}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import Animated, {
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { isMDBListEnabled } from '../../screens/MDBListSettingsScreen';
|
import { isMDBListEnabled } from '../../screens/MDBListSettingsScreen';
|
||||||
|
import { getAgeRatingColor } from '../../utils/ageRatingColors';
|
||||||
// MetadataSourceSelector removed
|
// MetadataSourceSelector removed
|
||||||
|
|
||||||
interface MetadataDetailsProps {
|
interface MetadataDetailsProps {
|
||||||
|
|
@ -153,7 +154,11 @@ function formatRuntime(runtime: string): string {
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{metadata.certification && (
|
{metadata.certification && (
|
||||||
<Text style={[styles.metaText, { color: currentTheme.colors.text }]}>{metadata.certification}</Text>
|
<Text style={[
|
||||||
|
styles.metaText,
|
||||||
|
styles.premiumOutlinedText,
|
||||||
|
{ color: getAgeRatingColor(metadata.certification, type === 'series' ? 'series' : 'movie') }
|
||||||
|
]}>{metadata.certification}</Text>
|
||||||
)}
|
)}
|
||||||
{metadata.imdbRating && !isMDBEnabled && (
|
{metadata.imdbRating && !isMDBEnabled && (
|
||||||
<View style={styles.ratingContainer}>
|
<View style={styles.ratingContainer}>
|
||||||
|
|
@ -273,6 +278,15 @@ const styles = StyleSheet.create({
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
opacity: 0.9,
|
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: {
|
ratingContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
|
||||||
216
src/utils/ageRatingColors.ts
Normal file
216
src/utils/ageRatingColors.ts
Normal file
|
|
@ -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<string, string> = {
|
||||||
|
'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<string, string> = {
|
||||||
|
'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<string, string> = {
|
||||||
|
'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`;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue