1087 lines
31 KiB
TypeScript
1087 lines
31 KiB
TypeScript
import { ThemedText } from "@/components/themed-text";
|
|
import { ThemedView } from "@/components/themed-view";
|
|
import { IconSymbol } from "@/components/ui/icon-symbol";
|
|
import { useTheme } from "@/context/ThemeContext";
|
|
import { BlurView } from "expo-blur";
|
|
import { LinearGradient } from "expo-linear-gradient";
|
|
import { Stack, useRouter } from "expo-router";
|
|
import React, { useEffect, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
Animated,
|
|
FlatList,
|
|
ScrollView,
|
|
StyleSheet,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
View
|
|
} from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
|
|
type SearchType = "team" | "player" | "league";
|
|
|
|
interface SearchEntity {
|
|
id: string;
|
|
type: SearchType;
|
|
name: string;
|
|
meta: {
|
|
country?: string;
|
|
league?: string;
|
|
season?: string;
|
|
team?: string;
|
|
pos?: string;
|
|
};
|
|
}
|
|
|
|
// 模拟数据
|
|
const ENTITIES: SearchEntity[] = [
|
|
{ id: "t_rm", type: "team", name: "Real Madrid", meta: { country: "Spain", league: "LaLiga", season: "2025/26" } },
|
|
{ id: "t_fcb", type: "team", name: "FC Barcelona", meta: { country: "Spain", league: "LaLiga", season: "2025/26" } },
|
|
{ id: "t_mci", type: "team", name: "Manchester City", meta: { country: "England", league: "Premier League", season: "2025/26" } },
|
|
{ id: "t_liv", type: "team", name: "Liverpool", meta: { country: "England", league: "Premier League", season: "2025/26" } },
|
|
{ id: "t_ars", type: "team", name: "Arsenal", meta: { country: "England", league: "Premier League", season: "2025/26" } },
|
|
{ id: "p_mbappe", type: "player", name: "Kylian Mbappé", meta: { country: "France", team: "Real Madrid", league: "LaLiga", pos: "FW", season: "2025/26" } },
|
|
{ id: "p_haaland", type: "player", name: "Erling Haaland", meta: { country: "Norway", team: "Manchester City", league: "Premier League", pos: "FW", season: "2025/26" } },
|
|
{ id: "p_salah", type: "player", name: "Mohamed Salah", meta: { country: "Egypt", team: "Liverpool", league: "Premier League", pos: "FW", season: "2025/26" } },
|
|
{ id: "p_curry", type: "player", name: "Stephen Curry", meta: { country: "USA", team: "Golden State Warriors", league: "NBA", pos: "G", season: "2025/26" } },
|
|
{ id: "l_epl", type: "league", name: "Premier League", meta: { country: "England", season: "2025/26" } },
|
|
{ id: "l_laliga", type: "league", name: "LaLiga", meta: { country: "Spain", season: "2025/26" } },
|
|
{ id: "l_ucl", type: "league", name: "UEFA Champions League", meta: { country: "Europe", season: "2025/26" } },
|
|
{ id: "l_nba", type: "league", name: "NBA", meta: { country: "USA", season: "2025/26" } },
|
|
];
|
|
|
|
// 生成首字母
|
|
function getInitials(name: string): string {
|
|
const s = name.trim();
|
|
if (!s) return "SN";
|
|
const clean = s.replace(/[^a-zA-Z0-9]+/g, " ").trim();
|
|
const parts = clean ? clean.split(/\s+/) : [s];
|
|
const a = (parts[0] || s).charAt(0) || "S";
|
|
const b = (parts[1] || "").charAt(0) || ((parts[0] || s).charAt(1) || "N");
|
|
return (a + b).toUpperCase();
|
|
}
|
|
|
|
// 生成颜色哈希
|
|
function hashString(str: string): number {
|
|
let hash = 2166136261;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash ^= str.charCodeAt(i);
|
|
hash = (hash * 16777619) >>> 0;
|
|
}
|
|
return hash >>> 0;
|
|
}
|
|
|
|
// HSL 转 RGB
|
|
function hslToRgb(h: number, s: number, l: number): string {
|
|
s /= 100;
|
|
l /= 100;
|
|
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
const m = l - c / 2;
|
|
let r = 0, g = 0, b = 0;
|
|
|
|
if (0 <= h && h < 60) {
|
|
r = c; g = x; b = 0;
|
|
} else if (60 <= h && h < 120) {
|
|
r = x; g = c; b = 0;
|
|
} else if (120 <= h && h < 180) {
|
|
r = 0; g = c; b = x;
|
|
} else if (180 <= h && h < 240) {
|
|
r = 0; g = x; b = c;
|
|
} else if (240 <= h && h < 300) {
|
|
r = x; g = 0; b = c;
|
|
} else if (300 <= h && h < 360) {
|
|
r = c; g = 0; b = x;
|
|
}
|
|
r = Math.round((r + m) * 255);
|
|
g = Math.round((g + m) * 255);
|
|
b = Math.round((b + m) * 255);
|
|
return `rgb(${r}, ${g}, ${b})`;
|
|
}
|
|
|
|
// 生成渐变颜色
|
|
function getLogoGradient(name: string): { color1: string; color2: string } {
|
|
const h = hashString(name);
|
|
const hue = h % 360;
|
|
const hue2 = (hue + 36 + (h % 24)) % 360;
|
|
return {
|
|
color1: hslToRgb(hue, 85, 58),
|
|
color2: hslToRgb(hue2, 85, 48),
|
|
};
|
|
}
|
|
|
|
// 获取副标题文本
|
|
function getSubText(entity: SearchEntity): string {
|
|
const { meta } = entity;
|
|
if (entity.type === "league") {
|
|
return `${meta.country || ""} · ${meta.season || ""}`;
|
|
}
|
|
if (entity.type === "player") {
|
|
return `${meta.team || ""} · ${meta.league || ""}`;
|
|
}
|
|
return `${meta.country || ""} · ${meta.league || ""}`;
|
|
}
|
|
|
|
export default function SearchScreen() {
|
|
const router = useRouter();
|
|
const { theme } = useTheme();
|
|
const { t } = useTranslation();
|
|
const insets = useSafeAreaInsets();
|
|
const isDark = theme === "dark";
|
|
|
|
const [searchType, setSearchType] = useState<SearchType>("team");
|
|
const [query, setQuery] = useState("");
|
|
const [recentSearches, setRecentSearches] = useState<string[]>([
|
|
"Real Madrid",
|
|
"Premier League",
|
|
"Curry",
|
|
]);
|
|
const [showDetailSheet, setShowDetailSheet] = useState(false);
|
|
const [selectedEntity, setSelectedEntity] = useState<SearchEntity | null>(null);
|
|
|
|
const sheetAnim = useRef(new Animated.Value(0)).current;
|
|
|
|
useEffect(() => {
|
|
if (showDetailSheet) {
|
|
Animated.spring(sheetAnim, {
|
|
toValue: 1,
|
|
useNativeDriver: true,
|
|
}).start();
|
|
} else {
|
|
Animated.timing(sheetAnim, {
|
|
toValue: 0,
|
|
duration: 200,
|
|
useNativeDriver: true,
|
|
}).start();
|
|
}
|
|
}, [showDetailSheet]);
|
|
|
|
const filteredEntities = ENTITIES.filter((e) => {
|
|
if (e.type !== searchType) return false;
|
|
if (!query.trim()) return true;
|
|
const q = query.toLowerCase();
|
|
return (
|
|
e.name.toLowerCase().includes(q) || getSubText(e).toLowerCase().includes(q)
|
|
);
|
|
});
|
|
|
|
const handleSearchTypeChange = (type: SearchType) => {
|
|
setSearchType(type);
|
|
setQuery("");
|
|
};
|
|
|
|
const handleRecentClick = (text: string) => {
|
|
setQuery(text);
|
|
};
|
|
|
|
const handleEntityPress = (entity: SearchEntity) => {
|
|
setSelectedEntity(entity);
|
|
setShowDetailSheet(true);
|
|
};
|
|
|
|
const handleClearRecent = () => {
|
|
setRecentSearches([]);
|
|
};
|
|
|
|
const handleSaveToRecent = () => {
|
|
if (selectedEntity) {
|
|
const newRecent = [
|
|
selectedEntity.name,
|
|
...recentSearches.filter((x) => x !== selectedEntity.name),
|
|
].slice(0, 10);
|
|
setRecentSearches(newRecent);
|
|
}
|
|
};
|
|
|
|
const handleOpenDetail = () => {
|
|
if (selectedEntity) {
|
|
handleSaveToRecent();
|
|
// TODO: Navigate to detail page
|
|
setShowDetailSheet(false);
|
|
}
|
|
};
|
|
|
|
const renderSearchBar = () => (
|
|
<View style={styles.searchWrap}>
|
|
<BlurView
|
|
intensity={80}
|
|
tint={isDark ? "dark" : "light"}
|
|
style={[
|
|
styles.search,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(0, 0, 0, 0.28)"
|
|
: "rgba(255, 255, 255, 0.8)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.12)"
|
|
: "rgba(0, 0, 0, 0.1)",
|
|
},
|
|
]}
|
|
>
|
|
<IconSymbol name="search" size={18} color={isDark ? "rgba(255,255,255,0.85)" : "rgba(0,0,0,0.6)"} />
|
|
<TextInput
|
|
style={[styles.searchInput, { color: isDark ? "#fff" : "#000" }]}
|
|
placeholder={t("search.placeholder")}
|
|
placeholderTextColor={isDark ? "rgba(255,255,255,0.4)" : "rgba(0,0,0,0.4)"}
|
|
value={query}
|
|
onChangeText={setQuery}
|
|
/>
|
|
{query.length > 0 && (
|
|
<TouchableOpacity onPress={() => setQuery("")}>
|
|
<IconSymbol name="close-circle" size={18} color={isDark ? "rgba(255,255,255,0.68)" : "rgba(0,0,0,0.5)"} />
|
|
</TouchableOpacity>
|
|
)}
|
|
</BlurView>
|
|
</View>
|
|
);
|
|
|
|
const renderChips = () => (
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
style={styles.chips}
|
|
contentContainerStyle={styles.chipsContent}
|
|
>
|
|
{(["team", "player", "league"] as SearchType[]).map((type) => (
|
|
<TouchableOpacity
|
|
key={type}
|
|
style={[
|
|
styles.chip,
|
|
searchType === type && styles.chipActive,
|
|
{
|
|
backgroundColor: isDark
|
|
? searchType === type
|
|
? "rgba(255, 255, 255, 0.12)"
|
|
: "rgba(255, 255, 255, 0.06)"
|
|
: searchType === type
|
|
? "rgba(0, 0, 0, 0.08)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? searchType === type
|
|
? "rgba(255, 255, 255, 0.14)"
|
|
: "rgba(255, 255, 255, 0.10)"
|
|
: searchType === type
|
|
? "rgba(0, 0, 0, 0.12)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
onPress={() => handleSearchTypeChange(type)}
|
|
>
|
|
<ThemedText
|
|
style={[
|
|
styles.chipText,
|
|
searchType === type && styles.chipTextActive,
|
|
]}
|
|
>
|
|
{t(`search.type.${type}`)}
|
|
</ThemedText>
|
|
</TouchableOpacity>
|
|
))}
|
|
</ScrollView>
|
|
);
|
|
|
|
const renderRecentSearches = () => {
|
|
if (recentSearches.length === 0 || query.trim()) return null;
|
|
|
|
return (
|
|
<>
|
|
<View style={styles.sectionHeader}>
|
|
<ThemedText style={styles.sectionTitle}>
|
|
{t("search.recent")}
|
|
</ThemedText>
|
|
<TouchableOpacity onPress={handleClearRecent}>
|
|
<ThemedText style={styles.sectionAction}>
|
|
{t("search.clear")}
|
|
</ThemedText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
style={styles.recentRow}
|
|
contentContainerStyle={styles.recentRowContent}
|
|
>
|
|
{recentSearches.map((item, index) => (
|
|
<TouchableOpacity
|
|
key={index}
|
|
style={[
|
|
styles.recentCard,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.05)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
onPress={() => handleRecentClick(item)}
|
|
>
|
|
<ThemedText style={styles.recentCardTitle} numberOfLines={1}>
|
|
{item}
|
|
</ThemedText>
|
|
<ThemedText style={styles.recentCardSub} numberOfLines={1}>
|
|
{t(`search.type.${searchType}`)}
|
|
</ThemedText>
|
|
</TouchableOpacity>
|
|
))}
|
|
</ScrollView>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const renderEntityItem = ({ item }: { item: SearchEntity }) => {
|
|
const gradient = getLogoGradient(item.name);
|
|
const initials = getInitials(item.name);
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.entityRow,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.06)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
onPress={() => handleEntityPress(item)}
|
|
>
|
|
<LinearGradient
|
|
colors={[gradient.color1, gradient.color2]}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={[
|
|
styles.logoDot,
|
|
{
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.14)"
|
|
: "rgba(255, 255, 255, 0.3)",
|
|
},
|
|
]}
|
|
>
|
|
<ThemedText style={styles.logoText}>{initials}</ThemedText>
|
|
</LinearGradient>
|
|
<View style={styles.entityMeta}>
|
|
<ThemedText style={styles.entityName} numberOfLines={1}>
|
|
{item.name}
|
|
</ThemedText>
|
|
<ThemedText style={styles.entitySub} numberOfLines={1}>
|
|
{getSubText(item)}
|
|
</ThemedText>
|
|
</View>
|
|
<ThemedText style={styles.entityBadge}>
|
|
{t(`search.type.${item.type}`)}
|
|
</ThemedText>
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
const renderEmptyState = () => (
|
|
<View
|
|
style={[
|
|
styles.emptyState,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.05)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
>
|
|
<ThemedText style={styles.emptyTitle}>
|
|
{t("search.no_results")}
|
|
</ThemedText>
|
|
<ThemedText style={styles.emptySub}>
|
|
{t("search.no_results_hint")}
|
|
</ThemedText>
|
|
</View>
|
|
);
|
|
|
|
const renderDetailSheet = () => {
|
|
if (!selectedEntity) return null;
|
|
|
|
const translateY = sheetAnim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [600, 0],
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<TouchableOpacity
|
|
style={styles.overlay}
|
|
activeOpacity={1}
|
|
onPress={() => setShowDetailSheet(false)}
|
|
>
|
|
<BlurView
|
|
intensity={20}
|
|
tint="dark"
|
|
style={StyleSheet.absoluteFill}
|
|
/>
|
|
</TouchableOpacity>
|
|
<Animated.View
|
|
style={[
|
|
styles.sheet,
|
|
{
|
|
transform: [{ translateY }],
|
|
backgroundColor: isDark
|
|
? "rgba(18, 20, 26, 0.96)"
|
|
: "rgba(255, 255, 255, 0.96)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.12)"
|
|
: "rgba(0, 0, 0, 0.1)",
|
|
},
|
|
{ paddingBottom: insets.bottom + 14 },
|
|
]}
|
|
>
|
|
<View style={styles.sheetHeader}>
|
|
<View style={styles.sheetTitleContainer}>
|
|
<ThemedText style={styles.sheetTitle}>
|
|
{selectedEntity.name}
|
|
</ThemedText>
|
|
<ThemedText style={styles.sheetSub}>
|
|
{t(`search.type.${selectedEntity.type}`)} ·{" "}
|
|
{getSubText(selectedEntity)}
|
|
</ThemedText>
|
|
</View>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.iconBtn,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.06)"
|
|
: "rgba(0, 0, 0, 0.06)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.10)",
|
|
},
|
|
]}
|
|
onPress={() => setShowDetailSheet(false)}
|
|
>
|
|
<IconSymbol
|
|
name="close"
|
|
size={18}
|
|
color={isDark ? "rgba(255,255,255,0.9)" : "rgba(0,0,0,0.9)"}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<View style={styles.kvContainer}>
|
|
{selectedEntity.type === "team" && (
|
|
<>
|
|
<View
|
|
style={[
|
|
styles.kvItem,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.05)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
>
|
|
<ThemedText style={styles.kvLabel}>
|
|
{t("search.detail.country")}
|
|
</ThemedText>
|
|
<ThemedText style={styles.kvValue} numberOfLines={1}>
|
|
{selectedEntity.meta.country || "-"}
|
|
</ThemedText>
|
|
</View>
|
|
<View
|
|
style={[
|
|
styles.kvItem,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.05)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
>
|
|
<ThemedText style={styles.kvLabel}>
|
|
{t("search.detail.league")}
|
|
</ThemedText>
|
|
<ThemedText style={styles.kvValue} numberOfLines={1}>
|
|
{selectedEntity.meta.league || "-"}
|
|
</ThemedText>
|
|
</View>
|
|
<View
|
|
style={[
|
|
styles.kvItem,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.05)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
>
|
|
<ThemedText style={styles.kvLabel}>
|
|
{t("search.detail.season")}
|
|
</ThemedText>
|
|
<ThemedText style={styles.kvValue} numberOfLines={1}>
|
|
{selectedEntity.meta.season || "-"}
|
|
</ThemedText>
|
|
</View>
|
|
</>
|
|
)}
|
|
{selectedEntity.type === "player" && (
|
|
<>
|
|
<View
|
|
style={[
|
|
styles.kvItem,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.05)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
>
|
|
<ThemedText style={styles.kvLabel}>
|
|
{t("search.detail.team")}
|
|
</ThemedText>
|
|
<ThemedText style={styles.kvValue} numberOfLines={1}>
|
|
{selectedEntity.meta.team || "-"}
|
|
</ThemedText>
|
|
</View>
|
|
<View
|
|
style={[
|
|
styles.kvItem,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.05)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
>
|
|
<ThemedText style={styles.kvLabel}>
|
|
{t("search.detail.league")}
|
|
</ThemedText>
|
|
<ThemedText style={styles.kvValue} numberOfLines={1}>
|
|
{selectedEntity.meta.league || "-"}
|
|
</ThemedText>
|
|
</View>
|
|
<View
|
|
style={[
|
|
styles.kvItem,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.05)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
>
|
|
<ThemedText style={styles.kvLabel}>
|
|
{t("search.detail.position")}
|
|
</ThemedText>
|
|
<ThemedText style={styles.kvValue} numberOfLines={1}>
|
|
{selectedEntity.meta.pos || "-"}
|
|
</ThemedText>
|
|
</View>
|
|
<View
|
|
style={[
|
|
styles.kvItem,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.05)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
>
|
|
<ThemedText style={styles.kvLabel}>
|
|
{t("search.detail.country")}
|
|
</ThemedText>
|
|
<ThemedText style={styles.kvValue} numberOfLines={1}>
|
|
{selectedEntity.meta.country || "-"}
|
|
</ThemedText>
|
|
</View>
|
|
</>
|
|
)}
|
|
{selectedEntity.type === "league" && (
|
|
<>
|
|
<View
|
|
style={[
|
|
styles.kvItem,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.05)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
>
|
|
<ThemedText style={styles.kvLabel}>
|
|
{t("search.detail.country")}
|
|
</ThemedText>
|
|
<ThemedText style={styles.kvValue} numberOfLines={1}>
|
|
{selectedEntity.meta.country || "-"}
|
|
</ThemedText>
|
|
</View>
|
|
<View
|
|
style={[
|
|
styles.kvItem,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.05)"
|
|
: "rgba(0, 0, 0, 0.04)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.08)",
|
|
},
|
|
]}
|
|
>
|
|
<ThemedText style={styles.kvLabel}>
|
|
{t("search.detail.season")}
|
|
</ThemedText>
|
|
<ThemedText style={styles.kvValue} numberOfLines={1}>
|
|
{selectedEntity.meta.season || "-"}
|
|
</ThemedText>
|
|
</View>
|
|
</>
|
|
)}
|
|
</View>
|
|
|
|
<View style={styles.sheetActions}>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.sheetBtn,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(0, 0, 0, 0.14)"
|
|
: "rgba(0, 0, 0, 0.06)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.12)"
|
|
: "rgba(0, 0, 0, 0.1)",
|
|
},
|
|
]}
|
|
onPress={handleOpenDetail}
|
|
>
|
|
<ThemedText style={styles.sheetBtnText}>
|
|
{t("search.detail.open")}
|
|
</ThemedText>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.sheetBtn,
|
|
styles.sheetBtnPrimary,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 210, 90, 0.12)"
|
|
: "rgba(255, 210, 90, 0.2)",
|
|
borderColor: isDark
|
|
? "rgba(255, 210, 90, 0.28)"
|
|
: "rgba(255, 210, 90, 0.4)",
|
|
},
|
|
]}
|
|
onPress={handleSaveToRecent}
|
|
>
|
|
<ThemedText
|
|
style={[
|
|
styles.sheetBtnText,
|
|
styles.sheetBtnTextPrimary,
|
|
]}
|
|
>
|
|
{t("search.detail.save")}
|
|
</ThemedText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</Animated.View>
|
|
</>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<ThemedView style={styles.container}>
|
|
<Stack.Screen
|
|
options={{
|
|
headerShown: false,
|
|
animation: "slide_from_right",
|
|
}}
|
|
/>
|
|
|
|
<BlurView
|
|
intensity={80}
|
|
tint={isDark ? "dark" : "light"}
|
|
style={[
|
|
styles.nav,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(7, 8, 11, 0.95)"
|
|
: "rgba(255, 255, 255, 0.95)",
|
|
},
|
|
{ paddingTop: insets.top + 10 },
|
|
]}
|
|
>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.iconBtn,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.06)"
|
|
: "rgba(0, 0, 0, 0.06)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.10)",
|
|
},
|
|
]}
|
|
onPress={() => router.back()}
|
|
>
|
|
<IconSymbol
|
|
name="chevron-back"
|
|
size={20}
|
|
color={isDark ? "rgba(255,255,255,0.9)" : "rgba(0,0,0,0.9)"}
|
|
/>
|
|
</TouchableOpacity>
|
|
<View style={styles.brand}>
|
|
<ThemedText style={styles.brandTitle}>{t("search.title")}</ThemedText>
|
|
<ThemedText style={styles.brandSub}>
|
|
{t("search.subtitle")}
|
|
</ThemedText>
|
|
</View>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.iconBtn,
|
|
{
|
|
backgroundColor: isDark
|
|
? "rgba(255, 255, 255, 0.06)"
|
|
: "rgba(0, 0, 0, 0.06)",
|
|
borderColor: isDark
|
|
? "rgba(255, 255, 255, 0.10)"
|
|
: "rgba(0, 0, 0, 0.10)",
|
|
},
|
|
]}
|
|
onPress={handleClearRecent}
|
|
>
|
|
<IconSymbol
|
|
name="refresh"
|
|
size={18}
|
|
color={isDark ? "rgba(255,255,255,0.9)" : "rgba(0,0,0,0.9)"}
|
|
/>
|
|
</TouchableOpacity>
|
|
</BlurView>
|
|
|
|
<View style={styles.content}>
|
|
<View style={styles.contentContainer}>
|
|
{renderSearchBar()}
|
|
{renderChips()}
|
|
{renderRecentSearches()}
|
|
|
|
<View style={styles.sectionHeader}>
|
|
<ThemedText style={styles.sectionTitle}>
|
|
{t("search.results")} · {t(`search.type.${searchType}`)}
|
|
</ThemedText>
|
|
<ThemedText style={styles.sectionAction}>
|
|
{query.trim()
|
|
? `${filteredEntities.length} ${t("search.found")}`
|
|
: t("search.tap_to_open")}
|
|
</ThemedText>
|
|
</View>
|
|
</View>
|
|
|
|
<FlatList
|
|
data={filteredEntities}
|
|
keyExtractor={(item) => item.id}
|
|
renderItem={renderEntityItem}
|
|
ListEmptyComponent={() => renderEmptyState()}
|
|
contentContainerStyle={[
|
|
styles.listContent,
|
|
{ paddingBottom: insets.bottom + 20 },
|
|
]}
|
|
showsVerticalScrollIndicator={false}
|
|
/>
|
|
</View>
|
|
|
|
{showDetailSheet && renderDetailSheet()}
|
|
</ThemedView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
nav: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: 10,
|
|
paddingHorizontal: 12,
|
|
paddingBottom: 12,
|
|
position: "sticky",
|
|
top: 0,
|
|
zIndex: 40,
|
|
},
|
|
iconBtn: {
|
|
width: 38,
|
|
height: 38,
|
|
borderRadius: 14,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
borderWidth: 1,
|
|
},
|
|
brand: {
|
|
flex: 1,
|
|
minWidth: 0,
|
|
gap: 2,
|
|
},
|
|
brandTitle: {
|
|
fontSize: 18,
|
|
fontWeight: "900",
|
|
letterSpacing: 0.2,
|
|
},
|
|
brandSub: {
|
|
fontSize: 12,
|
|
opacity: 0.42,
|
|
fontWeight: "800",
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
},
|
|
contentContainer: {
|
|
paddingHorizontal: 12,
|
|
paddingTop: 8,
|
|
},
|
|
listContent: {
|
|
paddingHorizontal: 12,
|
|
},
|
|
searchWrap: {
|
|
marginBottom: 10,
|
|
},
|
|
search: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
height: 46,
|
|
paddingHorizontal: 12,
|
|
borderRadius: 18,
|
|
borderWidth: 1,
|
|
overflow: "hidden",
|
|
},
|
|
searchInput: {
|
|
flex: 1,
|
|
minWidth: 0,
|
|
fontSize: 15,
|
|
},
|
|
chips: {
|
|
marginBottom: 8,
|
|
},
|
|
chipsContent: {
|
|
gap: 10,
|
|
paddingBottom: 2,
|
|
},
|
|
chip: {
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 8,
|
|
borderRadius: 999,
|
|
borderWidth: 1,
|
|
},
|
|
chipActive: {},
|
|
chipText: {
|
|
fontSize: 14,
|
|
fontWeight: "900",
|
|
opacity: 0.78,
|
|
},
|
|
chipTextActive: {
|
|
opacity: 1,
|
|
},
|
|
sectionHeader: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
marginTop: 12,
|
|
marginBottom: 8,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: 13,
|
|
opacity: 0.55,
|
|
fontWeight: "900",
|
|
},
|
|
sectionAction: {
|
|
fontSize: 12,
|
|
opacity: 0.42,
|
|
fontWeight: "800",
|
|
},
|
|
recentRow: {
|
|
marginBottom: 8,
|
|
},
|
|
recentRowContent: {
|
|
gap: 10,
|
|
paddingBottom: 2,
|
|
},
|
|
recentCard: {
|
|
minWidth: 190,
|
|
padding: 12,
|
|
borderRadius: 16,
|
|
borderWidth: 1,
|
|
},
|
|
recentCardTitle: {
|
|
fontWeight: "900",
|
|
},
|
|
recentCardSub: {
|
|
fontSize: 12,
|
|
opacity: 0.55,
|
|
marginTop: 4,
|
|
},
|
|
entityRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: 12,
|
|
padding: 12,
|
|
borderRadius: 18,
|
|
borderWidth: 1,
|
|
marginBottom: 10,
|
|
},
|
|
logoDot: {
|
|
width: 34,
|
|
height: 34,
|
|
borderRadius: 14,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
borderWidth: 1,
|
|
},
|
|
logoText: {
|
|
fontSize: 12,
|
|
fontWeight: "900",
|
|
color: "rgba(255, 255, 255, 0.92)",
|
|
},
|
|
entityMeta: {
|
|
flex: 1,
|
|
minWidth: 0,
|
|
},
|
|
entityName: {
|
|
fontWeight: "900",
|
|
},
|
|
entitySub: {
|
|
fontSize: 12,
|
|
opacity: 0.55,
|
|
marginTop: 3,
|
|
},
|
|
entityBadge: {
|
|
fontSize: 12,
|
|
opacity: 0.52,
|
|
fontWeight: "900",
|
|
},
|
|
emptyState: {
|
|
padding: 14,
|
|
borderRadius: 20,
|
|
borderWidth: 1,
|
|
},
|
|
emptyTitle: {
|
|
fontWeight: "900",
|
|
},
|
|
emptySub: {
|
|
marginTop: 6,
|
|
opacity: 0.6,
|
|
fontSize: 12,
|
|
fontWeight: "800",
|
|
lineHeight: 18,
|
|
},
|
|
overlay: {
|
|
...StyleSheet.absoluteFillObject,
|
|
zIndex: 80,
|
|
},
|
|
sheet: {
|
|
position: "absolute",
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
borderTopLeftRadius: 22,
|
|
borderTopRightRadius: 22,
|
|
borderWidth: 1,
|
|
padding: 12,
|
|
zIndex: 90,
|
|
shadowColor: "#000",
|
|
shadowOffset: { width: 0, height: -10 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 30,
|
|
elevation: 20,
|
|
},
|
|
sheetHeader: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
gap: 10,
|
|
marginBottom: 12,
|
|
},
|
|
sheetTitleContainer: {
|
|
flex: 1,
|
|
minWidth: 0,
|
|
},
|
|
sheetTitle: {
|
|
fontWeight: "900",
|
|
},
|
|
sheetSub: {
|
|
fontSize: 12,
|
|
opacity: 0.52,
|
|
fontWeight: "800",
|
|
marginTop: 2,
|
|
},
|
|
kvContainer: {
|
|
flexDirection: "row",
|
|
flexWrap: "wrap",
|
|
gap: 10,
|
|
marginBottom: 12,
|
|
},
|
|
kvItem: {
|
|
flex: 1,
|
|
minWidth: "45%",
|
|
padding: 12,
|
|
borderRadius: 18,
|
|
borderWidth: 1,
|
|
},
|
|
kvLabel: {
|
|
fontSize: 12,
|
|
opacity: 0.52,
|
|
fontWeight: "900",
|
|
},
|
|
kvValue: {
|
|
marginTop: 6,
|
|
fontWeight: "900",
|
|
},
|
|
sheetActions: {
|
|
flexDirection: "row",
|
|
gap: 10,
|
|
},
|
|
sheetBtn: {
|
|
flex: 1,
|
|
height: 36,
|
|
paddingHorizontal: 12,
|
|
borderRadius: 14,
|
|
borderWidth: 1,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
sheetBtnPrimary: {},
|
|
sheetBtnText: {
|
|
fontSize: 14,
|
|
fontWeight: "900",
|
|
opacity: 0.86,
|
|
},
|
|
sheetBtnTextPrimary: {
|
|
opacity: 1,
|
|
},
|
|
});
|