无图片默认渐变色头像
This commit is contained in:
286
app/search.tsx
286
app/search.tsx
@@ -5,6 +5,7 @@ import { Colors } from "@/constants/theme";
|
|||||||
import { useAppState } from "@/context/AppStateContext";
|
import { useAppState } from "@/context/AppStateContext";
|
||||||
import { useTheme } from "@/context/ThemeContext";
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
import { fetchSearch } from "@/lib/api";
|
import { fetchSearch } from "@/lib/api";
|
||||||
|
import { getInitials, getLogoGradient } from "@/lib/avatar-utils";
|
||||||
import { SearchLeague, SearchPlayer, SearchTeam } from "@/types/api";
|
import { SearchLeague, SearchPlayer, SearchTeam } from "@/types/api";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
@@ -78,71 +79,12 @@ function convertTeamToEntity(team: SearchTeam): SearchEntity {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成首字母
|
|
||||||
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 {
|
function getSubText(entity: SearchEntity): string {
|
||||||
const { meta } = entity;
|
const { meta } = entity;
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
if (entity.type === "league") {
|
if (entity.type === "league") {
|
||||||
// 联赛:显示国家
|
// 联赛:显示国家
|
||||||
if (meta.country) parts.push(meta.country);
|
if (meta.country) parts.push(meta.country);
|
||||||
@@ -159,7 +101,7 @@ function getSubText(entity: SearchEntity): string {
|
|||||||
if (meta.country) parts.push(meta.country);
|
if (meta.country) parts.push(meta.country);
|
||||||
if (meta.league) parts.push(meta.league);
|
if (meta.league) parts.push(meta.league);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.filter(Boolean).join(" · ") || "";
|
return parts.filter(Boolean).join(" · ") || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +113,8 @@ export default function SearchScreen() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
const cardBg = isDark ? "#1C1C1E" : "#FFFFFF";
|
||||||
|
const borderColor = isDark ? "#2C2C2E" : "rgba(0,0,0,0.06)";
|
||||||
const { state } = useAppState();
|
const { state } = useAppState();
|
||||||
|
|
||||||
const [searchType, setSearchType] = useState<SearchType>("all");
|
const [searchType, setSearchType] = useState<SearchType>("all");
|
||||||
@@ -181,6 +125,7 @@ export default function SearchScreen() {
|
|||||||
const [entities, setEntities] = useState<SearchEntity[]>([]);
|
const [entities, setEntities] = useState<SearchEntity[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [hasSearched, setHasSearched] = useState(false);
|
||||||
|
|
||||||
const sheetAnim = useRef(new Animated.Value(0)).current;
|
const sheetAnim = useRef(new Animated.Value(0)).current;
|
||||||
const searchTimeoutRef = useRef<number | null>(null);
|
const searchTimeoutRef = useRef<number | null>(null);
|
||||||
@@ -226,6 +171,7 @@ export default function SearchScreen() {
|
|||||||
if (!query.trim()) {
|
if (!query.trim()) {
|
||||||
setEntities([]);
|
setEntities([]);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setHasSearched(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +191,7 @@ export default function SearchScreen() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
setEntities(allEntities);
|
setEntities(allEntities);
|
||||||
|
setHasSearched(true);
|
||||||
if (allEntities.length > 0) {
|
if (allEntities.length > 0) {
|
||||||
const searchQuery = query.trim();
|
const searchQuery = query.trim();
|
||||||
setRecentSearches((prev) => {
|
setRecentSearches((prev) => {
|
||||||
@@ -260,6 +207,7 @@ export default function SearchScreen() {
|
|||||||
console.error("Search error:", err);
|
console.error("Search error:", err);
|
||||||
setError(err?.message || t("search.error"));
|
setError(err?.message || t("search.error"));
|
||||||
setEntities([]);
|
setEntities([]);
|
||||||
|
setHasSearched(true);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -328,12 +276,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.search,
|
styles.search,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(0, 0, 0, 0.28)"
|
borderColor: borderColor,
|
||||||
: "rgba(255, 255, 255, 0.8)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.12)"
|
|
||||||
: "rgba(0, 0, 0, 0.1)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -368,20 +312,8 @@ export default function SearchScreen() {
|
|||||||
styles.chip,
|
styles.chip,
|
||||||
searchType === type && styles.chipActive,
|
searchType === type && styles.chipActive,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? searchType === type
|
borderColor: borderColor,
|
||||||
? "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)}
|
onPress={() => handleSearchTypeChange(type)}
|
||||||
@@ -426,12 +358,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.recentCard,
|
styles.recentCard,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(255, 255, 255, 0.05)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.04)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.10)"
|
|
||||||
: "rgba(0, 0, 0, 0.08)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => handleRecentClick(item)}
|
onPress={() => handleRecentClick(item)}
|
||||||
@@ -458,12 +386,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.entityRow,
|
styles.entityRow,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(255, 255, 255, 0.06)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.04)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.10)"
|
|
||||||
: "rgba(0, 0, 0, 0.08)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => handleEntityPress(item)}
|
onPress={() => handleEntityPress(item)}
|
||||||
@@ -503,12 +427,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.emptyState,
|
styles.emptyState,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(255, 255, 255, 0.05)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.04)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.10)"
|
|
||||||
: "rgba(0, 0, 0, 0.08)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -547,12 +467,8 @@ export default function SearchScreen() {
|
|||||||
styles.sheet,
|
styles.sheet,
|
||||||
{
|
{
|
||||||
transform: [{ translateY }],
|
transform: [{ translateY }],
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(18, 20, 26, 0.96)"
|
borderColor: borderColor,
|
||||||
: "rgba(255, 255, 255, 0.96)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.12)"
|
|
||||||
: "rgba(0, 0, 0, 0.1)",
|
|
||||||
},
|
},
|
||||||
{ paddingBottom: insets.bottom + 14 },
|
{ paddingBottom: insets.bottom + 14 },
|
||||||
]}
|
]}
|
||||||
@@ -571,12 +487,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.iconBtn,
|
styles.iconBtn,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(255, 255, 255, 0.06)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.06)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.10)"
|
|
||||||
: "rgba(0, 0, 0, 0.10)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => setShowDetailSheet(false)}
|
onPress={() => setShowDetailSheet(false)}
|
||||||
@@ -597,12 +509,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.kvItem,
|
styles.kvItem,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(255, 255, 255, 0.05)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.04)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.10)"
|
|
||||||
: "rgba(0, 0, 0, 0.08)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -619,12 +527,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.kvItem,
|
styles.kvItem,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(255, 255, 255, 0.05)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.04)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.10)"
|
|
||||||
: "rgba(0, 0, 0, 0.08)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -645,12 +549,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.kvItem,
|
styles.kvItem,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(255, 255, 255, 0.05)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.04)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.10)"
|
|
||||||
: "rgba(0, 0, 0, 0.08)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -667,12 +567,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.kvItem,
|
styles.kvItem,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(255, 255, 255, 0.05)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.04)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.10)"
|
|
||||||
: "rgba(0, 0, 0, 0.08)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -689,12 +585,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.kvItem,
|
styles.kvItem,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(255, 255, 255, 0.05)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.04)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.10)"
|
|
||||||
: "rgba(0, 0, 0, 0.08)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -711,12 +603,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.kvItem,
|
styles.kvItem,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(255, 255, 255, 0.05)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.04)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.10)"
|
|
||||||
: "rgba(0, 0, 0, 0.08)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -737,12 +625,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.kvItem,
|
styles.kvItem,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(255, 255, 255, 0.05)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.04)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.10)"
|
|
||||||
: "rgba(0, 0, 0, 0.08)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -763,12 +647,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.sheetBtn,
|
styles.sheetBtn,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(0, 0, 0, 0.14)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.06)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.12)"
|
|
||||||
: "rgba(0, 0, 0, 0.1)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={handleOpenDetail}
|
onPress={handleOpenDetail}
|
||||||
@@ -822,9 +702,7 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.nav,
|
styles.nav,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(7, 8, 11, 0.95)"
|
|
||||||
: "rgba(255, 255, 255, 0.95)",
|
|
||||||
},
|
},
|
||||||
{ paddingTop: insets.top + 10 },
|
{ paddingTop: insets.top + 10 },
|
||||||
]}
|
]}
|
||||||
@@ -833,12 +711,8 @@ export default function SearchScreen() {
|
|||||||
style={[
|
style={[
|
||||||
styles.iconBtn,
|
styles.iconBtn,
|
||||||
{
|
{
|
||||||
backgroundColor: isDark
|
backgroundColor: cardBg,
|
||||||
? "rgba(255, 255, 255, 0.06)"
|
borderColor: borderColor,
|
||||||
: "rgba(0, 0, 0, 0.06)",
|
|
||||||
borderColor: isDark
|
|
||||||
? "rgba(255, 255, 255, 0.10)"
|
|
||||||
: "rgba(0, 0, 0, 0.10)",
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => router.back()}
|
onPress={() => router.back()}
|
||||||
@@ -873,8 +747,8 @@ export default function SearchScreen() {
|
|||||||
{loading
|
{loading
|
||||||
? t("search.loading")
|
? t("search.loading")
|
||||||
: error
|
: error
|
||||||
? t("search.error")
|
? t("search.error")
|
||||||
: `${filteredEntities.length} ${t("search.found")}`}
|
: `${filteredEntities.length} ${t("search.found")}`}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -892,12 +766,13 @@ export default function SearchScreen() {
|
|||||||
{t("search.error_hint")}
|
{t("search.error_hint")}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
|
) : hasSearched && filteredEntities.length === 0 ? (
|
||||||
|
renderEmptyState()
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={filteredEntities}
|
data={filteredEntities}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
renderItem={renderEntityItem}
|
renderItem={renderEntityItem}
|
||||||
ListEmptyComponent={() => renderEmptyState()}
|
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
styles.listContent,
|
styles.listContent,
|
||||||
{ paddingBottom: insets.bottom + 20 },
|
{ paddingBottom: insets.bottom + 20 },
|
||||||
@@ -942,13 +817,13 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
brandTitle: {
|
brandTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: "900",
|
fontWeight: "700",
|
||||||
letterSpacing: 0.2,
|
letterSpacing: 0.2,
|
||||||
},
|
},
|
||||||
brandSub: {
|
brandSub: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
opacity: 0.42,
|
opacity: 0.5,
|
||||||
fontWeight: "800",
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -993,9 +868,9 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
chipActive: {},
|
chipActive: {},
|
||||||
chipText: {
|
chipText: {
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
fontWeight: "900",
|
fontWeight: "700",
|
||||||
opacity: 0.78,
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
chipTextActive: {
|
chipTextActive: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
@@ -1008,14 +883,14 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 13,
|
fontSize: 12,
|
||||||
opacity: 0.55,
|
opacity: 0.8,
|
||||||
fontWeight: "900",
|
fontWeight: "700",
|
||||||
},
|
},
|
||||||
sectionAction: {
|
sectionAction: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
opacity: 0.42,
|
opacity: 0.5,
|
||||||
fontWeight: "800",
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
recentRow: {
|
recentRow: {
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
@@ -1031,12 +906,14 @@ const styles = StyleSheet.create({
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
recentCardTitle: {
|
recentCardTitle: {
|
||||||
fontWeight: "900",
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
recentCardSub: {
|
recentCardSub: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
opacity: 0.55,
|
opacity: 0.5,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
entityRow: {
|
entityRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -1057,7 +934,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
logoText: {
|
logoText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: "900",
|
fontWeight: "700",
|
||||||
color: "rgba(255, 255, 255, 0.92)",
|
color: "rgba(255, 255, 255, 0.92)",
|
||||||
},
|
},
|
||||||
entityMeta: {
|
entityMeta: {
|
||||||
@@ -1065,17 +942,19 @@ const styles = StyleSheet.create({
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
entityName: {
|
entityName: {
|
||||||
fontWeight: "900",
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
entitySub: {
|
entitySub: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
opacity: 0.55,
|
opacity: 0.5,
|
||||||
marginTop: 3,
|
marginTop: 3,
|
||||||
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
entityBadge: {
|
entityBadge: {
|
||||||
fontSize: 12,
|
fontSize: 10,
|
||||||
opacity: 0.52,
|
opacity: 0.9,
|
||||||
fontWeight: "900",
|
fontWeight: "700",
|
||||||
},
|
},
|
||||||
center: {
|
center: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -1089,13 +968,14 @@ const styles = StyleSheet.create({
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
emptyTitle: {
|
emptyTitle: {
|
||||||
fontWeight: "900",
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
emptySub: {
|
emptySub: {
|
||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
opacity: 0.6,
|
opacity: 0.5,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: "800",
|
fontWeight: "500",
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
@@ -1130,12 +1010,13 @@ const styles = StyleSheet.create({
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
sheetTitle: {
|
sheetTitle: {
|
||||||
fontWeight: "900",
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
sheetSub: {
|
sheetSub: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
opacity: 0.52,
|
opacity: 0.5,
|
||||||
fontWeight: "800",
|
fontWeight: "500",
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
kvContainer: {
|
kvContainer: {
|
||||||
@@ -1153,12 +1034,13 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
kvLabel: {
|
kvLabel: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
opacity: 0.52,
|
opacity: 0.5,
|
||||||
fontWeight: "900",
|
fontWeight: "700",
|
||||||
},
|
},
|
||||||
kvValue: {
|
kvValue: {
|
||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
fontWeight: "900",
|
fontSize: 12,
|
||||||
|
fontWeight: "600",
|
||||||
},
|
},
|
||||||
sheetActions: {
|
sheetActions: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -1175,9 +1057,9 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
sheetBtnPrimary: {},
|
sheetBtnPrimary: {},
|
||||||
sheetBtnText: {
|
sheetBtnText: {
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
fontWeight: "900",
|
fontWeight: "700",
|
||||||
opacity: 0.86,
|
opacity: 0.9,
|
||||||
},
|
},
|
||||||
sheetBtnTextPrimary: {
|
sheetBtnTextPrimary: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { Colors } from "@/constants/theme";
|
|||||||
import { useAppState } from "@/context/AppStateContext";
|
import { useAppState } from "@/context/AppStateContext";
|
||||||
import { useTheme } from "@/context/ThemeContext";
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
import { addFavorite, removeFavorite } from "@/lib/api";
|
import { addFavorite, removeFavorite } from "@/lib/api";
|
||||||
|
import { getInitials, getLogoGradient } from "@/lib/avatar-utils";
|
||||||
import { Match } from "@/types/api";
|
import { Match } from "@/types/api";
|
||||||
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Image, Pressable, StyleSheet, TouchableOpacity, View } from "react-native";
|
import { Image, Pressable, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||||
@@ -179,14 +181,31 @@ export function MatchCardLeague({
|
|||||||
|
|
||||||
<View style={styles.teamsColumn}>
|
<View style={styles.teamsColumn}>
|
||||||
<View style={styles.teamRow}>
|
<View style={styles.teamRow}>
|
||||||
<Image
|
{(() => {
|
||||||
source={{
|
const teamName = isTennis ? (match as any).eventFirstPlayer : (match.home || match.homeTeamName);
|
||||||
uri: isTennis
|
const logoUri = isTennis
|
||||||
? (match as any).eventFirstPlayerLogo
|
? (match as any).eventFirstPlayerLogo
|
||||||
: ((match as any).homeLogo || match.homeTeamLogo || "https://placehold.co/24x24/png")
|
: ((match as any).homeLogo || match.homeTeamLogo);
|
||||||
}}
|
const hasLogo = logoUri && logoUri.trim() !== "" && !logoUri.includes("placehold");
|
||||||
style={styles.teamLogo}
|
const gradient = getLogoGradient(teamName || "");
|
||||||
/>
|
const initials = getInitials(teamName || "");
|
||||||
|
|
||||||
|
return hasLogo ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: logoUri }}
|
||||||
|
style={styles.teamLogo}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LinearGradient
|
||||||
|
colors={[gradient.color1, gradient.color2]}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 1 }}
|
||||||
|
style={styles.teamLogoGradient}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.teamLogoText}>{initials}</ThemedText>
|
||||||
|
</LinearGradient>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
<View style={styles.teamNameContainer}>
|
<View style={styles.teamNameContainer}>
|
||||||
<ThemedText style={[styles.teamName, { color: textColor }]} numberOfLines={1}>
|
<ThemedText style={[styles.teamName, { color: textColor }]} numberOfLines={1}>
|
||||||
{isTennis ? (match as any).eventFirstPlayer : (match.home || match.homeTeamName)}
|
{isTennis ? (match as any).eventFirstPlayer : (match.home || match.homeTeamName)}
|
||||||
@@ -197,14 +216,31 @@ export function MatchCardLeague({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={[styles.teamRow, { marginTop: 10 }]}>
|
<View style={[styles.teamRow, { marginTop: 10 }]}>
|
||||||
<Image
|
{(() => {
|
||||||
source={{
|
const teamName = isTennis ? (match as any).eventSecondPlayer : (match.away || match.awayTeamName);
|
||||||
uri: isTennis
|
const logoUri = isTennis
|
||||||
? (match as any).eventSecondPlayerLogo
|
? (match as any).eventSecondPlayerLogo
|
||||||
: ((match as any).awayLogo || match.awayTeamLogo || "https://placehold.co/24x24/png")
|
: ((match as any).awayLogo || match.awayTeamLogo);
|
||||||
}}
|
const hasLogo = logoUri && logoUri.trim() !== "" && !logoUri.includes("placehold");
|
||||||
style={styles.teamLogo}
|
const gradient = getLogoGradient(teamName || "");
|
||||||
/>
|
const initials = getInitials(teamName || "");
|
||||||
|
|
||||||
|
return hasLogo ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: logoUri }}
|
||||||
|
style={styles.teamLogo}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LinearGradient
|
||||||
|
colors={[gradient.color1, gradient.color2]}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 1 }}
|
||||||
|
style={styles.teamLogoGradient}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.teamLogoText}>{initials}</ThemedText>
|
||||||
|
</LinearGradient>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
<View style={styles.teamNameContainer}>
|
<View style={styles.teamNameContainer}>
|
||||||
<ThemedText style={[styles.teamName, { color: textColor }]} numberOfLines={1}>
|
<ThemedText style={[styles.teamName, { color: textColor }]} numberOfLines={1}>
|
||||||
{isTennis ? (match as any).eventSecondPlayer : (match.away || match.awayTeamName)}
|
{isTennis ? (match as any).eventSecondPlayer : (match.away || match.awayTeamName)}
|
||||||
@@ -310,13 +346,26 @@ const styles = StyleSheet.create({
|
|||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
backgroundColor: "#E5E5E5",
|
backgroundColor: "#E5E5E5",
|
||||||
},
|
},
|
||||||
|
teamLogoGradient: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 14,
|
||||||
|
marginRight: 12,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
teamLogoText: {
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "rgba(255, 255, 255, 0.92)",
|
||||||
|
},
|
||||||
teamNameContainer: {
|
teamNameContainer: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
teamName: {
|
teamName: {
|
||||||
fontSize: 15,
|
fontSize: 13,
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
cardBadge: {
|
cardBadge: {
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { ThemedText } from "@/components/themed-text";
|
|||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
import { useTheme } from "@/context/ThemeContext";
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
import { fetchLeagues, fetchTodayMatches } from "@/lib/api";
|
import { fetchLeagues, fetchTodayMatches } from "@/lib/api";
|
||||||
|
import { getInitials, getLogoGradient } from "@/lib/avatar-utils";
|
||||||
import { League, Match } from "@/types/api";
|
import { League, Match } from "@/types/api";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@@ -208,12 +210,27 @@ export function MatchesByLeague({
|
|||||||
style={[styles.leagueHeaderWrapper, { backgroundColor: headerBg }]}
|
style={[styles.leagueHeaderWrapper, { backgroundColor: headerBg }]}
|
||||||
>
|
>
|
||||||
<View style={styles.leagueHeaderLeft}>
|
<View style={styles.leagueHeaderLeft}>
|
||||||
<Image
|
{(() => {
|
||||||
source={{
|
const hasLogo = league.logo && league.logo.trim() !== "" && !league.logo.includes("placehold");
|
||||||
uri: league.logo || "https://placehold.co/40x40/png",
|
const gradient = getLogoGradient(league.name || "");
|
||||||
}}
|
const initials = getInitials(league.name || "");
|
||||||
style={[styles.leagueLogo, { backgroundColor: isDark ? "#3A3A3C" : "#E5E5E5" }]}
|
|
||||||
/>
|
return hasLogo ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: league.logo }}
|
||||||
|
style={[styles.leagueLogo, { backgroundColor: isDark ? "#3A3A3C" : "#E5E5E5" }]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LinearGradient
|
||||||
|
colors={[gradient.color1, gradient.color2]}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 1 }}
|
||||||
|
style={styles.leagueLogoGradient}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.leagueLogoText}>{initials}</ThemedText>
|
||||||
|
</LinearGradient>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
<View style={styles.leagueInfoText}>
|
<View style={styles.leagueInfoText}>
|
||||||
<ThemedText
|
<ThemedText
|
||||||
@@ -326,6 +343,20 @@ const styles = StyleSheet.create({
|
|||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
marginLeft: 14,
|
marginLeft: 14,
|
||||||
},
|
},
|
||||||
|
leagueLogoGradient: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginRight: 12,
|
||||||
|
marginLeft: 14,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
leagueLogoText: {
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: "rgba(255, 255, 255, 0.92)",
|
||||||
|
},
|
||||||
leagueInfoText: {
|
leagueInfoText: {
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
|
|||||||
55
lib/avatar-utils.ts
Normal file
55
lib/avatar-utils.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user