Merge branch 'main' of https://git.ambigrat.com/shenyan/physical-expo
This commit is contained in:
207
app/profile.tsx
207
app/profile.tsx
@@ -15,24 +15,59 @@ import {
|
|||||||
|
|
||||||
import { ThemedText } from "@/components/themed-text";
|
import { ThemedText } from "@/components/themed-text";
|
||||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||||
|
import { useAppState } from "@/context/AppStateContext";
|
||||||
import { useTheme } from "@/context/ThemeContext";
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
import { changeLanguage } from "@/i18n";
|
import { changeLanguage } from "@/i18n";
|
||||||
import { appleSignIn, fetchUserProfile, logout } from "@/lib/api";
|
import { appleSignIn, fetchUserProfile, logout } from "@/lib/api";
|
||||||
import { storage } from "@/lib/storage";
|
import { storage } from "@/lib/storage";
|
||||||
import type { UserProfile } from "@/types/api";
|
import type { UserProfile } from "@/types/api";
|
||||||
|
|
||||||
|
const BOOKMAKERS = [
|
||||||
|
"10Bet",
|
||||||
|
"WilliamHill",
|
||||||
|
"bet365",
|
||||||
|
"Marathon",
|
||||||
|
"Unibet",
|
||||||
|
"Betfair",
|
||||||
|
"188bet",
|
||||||
|
"Pncl",
|
||||||
|
"Sbo",
|
||||||
|
];
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const { theme, toggleTheme, setTheme, isSystemTheme, useSystemTheme } =
|
const { theme, toggleTheme, setTheme, isSystemTheme, useSystemTheme } =
|
||||||
useTheme();
|
useTheme();
|
||||||
|
const { state, updateOddsSettings } = useAppState();
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
const [appleAvailable, setAppleAvailable] = React.useState(false);
|
const [appleAvailable, setAppleAvailable] = React.useState(false);
|
||||||
const [user, setUser] = React.useState<UserProfile | null>(null);
|
const [user, setUser] = React.useState<UserProfile | null>(null);
|
||||||
const [loginModalVisible, setLoginModalVisible] = React.useState(false);
|
const [loginModalVisible, setLoginModalVisible] = React.useState(false);
|
||||||
|
const [oddsModalVisible, setOddsModalVisible] = React.useState(false);
|
||||||
|
|
||||||
const currentLanguage = i18n.language;
|
const currentLanguage = i18n.language;
|
||||||
|
|
||||||
|
const toggleOdds = () => {
|
||||||
|
updateOddsSettings({
|
||||||
|
...state.oddsSettings,
|
||||||
|
enabled: !state.oddsSettings.enabled,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectBookmaker = (name: string) => {
|
||||||
|
const current = state.oddsSettings.selectedBookmakers;
|
||||||
|
let next: string[];
|
||||||
|
if (current.includes(name)) {
|
||||||
|
next = current.filter((b) => b !== name);
|
||||||
|
} else {
|
||||||
|
next = [...current, name].slice(-2); // Keep last 2
|
||||||
|
}
|
||||||
|
updateOddsSettings({
|
||||||
|
...state.oddsSettings,
|
||||||
|
selectedBookmakers: next,
|
||||||
|
});
|
||||||
|
};
|
||||||
const toggleLanguage = () => {
|
const toggleLanguage = () => {
|
||||||
const nextLang = currentLanguage.startsWith("en") ? "zh" : "en";
|
const nextLang = currentLanguage.startsWith("en") ? "zh" : "en";
|
||||||
changeLanguage(nextLang);
|
changeLanguage(nextLang);
|
||||||
@@ -65,7 +100,8 @@ export default function ProfileScreen() {
|
|||||||
platformVersion,
|
platformVersion,
|
||||||
},
|
},
|
||||||
user:
|
user:
|
||||||
credential.fullName && (credential.fullName.givenName || credential.fullName.familyName)
|
credential.fullName &&
|
||||||
|
(credential.fullName.givenName || credential.fullName.familyName)
|
||||||
? {
|
? {
|
||||||
name: {
|
name: {
|
||||||
firstName: credential.fullName.givenName || undefined,
|
firstName: credential.fullName.givenName || undefined,
|
||||||
@@ -178,7 +214,9 @@ export default function ProfileScreen() {
|
|||||||
<>
|
<>
|
||||||
<View style={styles.profileHeader}>
|
<View style={styles.profileHeader}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: user?.avatar || "https://via.placeholder.com/100" }}
|
source={{
|
||||||
|
uri: user?.avatar || "https://via.placeholder.com/100",
|
||||||
|
}}
|
||||||
style={styles.avatar}
|
style={styles.avatar}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
/>
|
/>
|
||||||
@@ -201,7 +239,9 @@ export default function ProfileScreen() {
|
|||||||
]}
|
]}
|
||||||
onPress={handleLogout}
|
onPress={handleLogout}
|
||||||
>
|
>
|
||||||
<ThemedText style={{ color: "#FF3B30" }}>登出</ThemedText>
|
<ThemedText style={{ color: "#FF3B30" }}>
|
||||||
|
{t("settings.logout")}
|
||||||
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -212,9 +252,11 @@ export default function ProfileScreen() {
|
|||||||
>
|
>
|
||||||
<View style={styles.loginIcon} />
|
<View style={styles.loginIcon} />
|
||||||
<View style={styles.loginInfo}>
|
<View style={styles.loginInfo}>
|
||||||
<ThemedText style={styles.loginTitle}>登录</ThemedText>
|
<ThemedText style={styles.loginTitle}>
|
||||||
|
{t("settings.login")}
|
||||||
|
</ThemedText>
|
||||||
<ThemedText style={{ color: subTextColor }}>
|
<ThemedText style={{ color: subTextColor }}>
|
||||||
点击登录
|
{t("settings.click_to_login")}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -310,6 +352,83 @@ export default function ProfileScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<ThemedText style={styles.sectionTitle}>
|
||||||
|
{t("settings.odds_title")}
|
||||||
|
</ThemedText>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.section,
|
||||||
|
{ backgroundColor: isDark ? "#1c1c1e" : "#fff" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.settingItem}>
|
||||||
|
<View style={styles.settingLabel}>
|
||||||
|
<IconSymbol
|
||||||
|
name="stats-chart"
|
||||||
|
size={20}
|
||||||
|
color={iconColor}
|
||||||
|
style={{ marginRight: 10 }}
|
||||||
|
/>
|
||||||
|
<ThemedText>{t("settings.odds_show")}</ThemedText>
|
||||||
|
</View>
|
||||||
|
<View style={styles.settingControl}>
|
||||||
|
<TouchableOpacity onPress={toggleOdds} style={styles.button}>
|
||||||
|
<ThemedText>
|
||||||
|
{state.oddsSettings.enabled
|
||||||
|
? t("settings.odds_enabled")
|
||||||
|
: t("settings.odds_disabled")}
|
||||||
|
</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.settingItem,
|
||||||
|
{
|
||||||
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderTopColor: isDark ? "#38383a" : "#c6c6c8",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.settingItemContent}
|
||||||
|
onPress={() => setOddsModalVisible(true)}
|
||||||
|
disabled={!state.oddsSettings.enabled}
|
||||||
|
>
|
||||||
|
<View style={styles.settingLabel}>
|
||||||
|
<IconSymbol
|
||||||
|
name="list"
|
||||||
|
size={20}
|
||||||
|
color={state.oddsSettings.enabled ? iconColor : subTextColor}
|
||||||
|
style={{ marginRight: 10 }}
|
||||||
|
/>
|
||||||
|
<ThemedText
|
||||||
|
style={{
|
||||||
|
color: state.oddsSettings.enabled
|
||||||
|
? textColor
|
||||||
|
: subTextColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("settings.odds_select_company")}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||||
|
<ThemedText style={{ color: subTextColor, marginRight: 4 }}>
|
||||||
|
{state.oddsSettings.selectedBookmakers.join(", ") ||
|
||||||
|
t("settings.odds_unselected")}
|
||||||
|
</ThemedText>
|
||||||
|
<IconSymbol
|
||||||
|
name="chevron-forward"
|
||||||
|
size={16}
|
||||||
|
color={subTextColor}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* <ThemedText style={styles.sectionTitle}>登录</ThemedText>
|
{/* <ThemedText style={styles.sectionTitle}>登录</ThemedText>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
@@ -367,7 +486,9 @@ export default function ProfileScreen() {
|
|||||||
{ backgroundColor: isDark ? "#1c1c1e" : "#fff" },
|
{ backgroundColor: isDark ? "#1c1c1e" : "#fff" },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<ThemedText style={styles.modalTitle}>选择登录方式</ThemedText>
|
<ThemedText style={styles.modalTitle}>
|
||||||
|
{t("settings.select_login_method")}
|
||||||
|
</ThemedText>
|
||||||
{appleAvailable && (
|
{appleAvailable && (
|
||||||
<AppleAuthentication.AppleAuthenticationButton
|
<AppleAuthentication.AppleAuthenticationButton
|
||||||
buttonType={
|
buttonType={
|
||||||
@@ -384,13 +505,76 @@ export default function ProfileScreen() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity style={styles.googleButton} onPress={() => {}}>
|
<TouchableOpacity style={styles.googleButton} onPress={() => {}}>
|
||||||
<ThemedText>Google 登录</ThemedText>
|
<ThemedText>{t("settings.google_login")}</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.modalCancel}
|
style={styles.modalCancel}
|
||||||
onPress={() => setLoginModalVisible(false)}
|
onPress={() => setLoginModalVisible(false)}
|
||||||
>
|
>
|
||||||
<ThemedText>取消</ThemedText>
|
<ThemedText>{t("settings.cancel")}</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
visible={oddsModalVisible}
|
||||||
|
transparent
|
||||||
|
animationType="slide"
|
||||||
|
onRequestClose={() => setOddsModalVisible(false)}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.modalMask}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={() => setOddsModalVisible(false)}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.modalCard,
|
||||||
|
{
|
||||||
|
backgroundColor: isDark ? "#1c1c1e" : "#fff",
|
||||||
|
maxHeight: "70%",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.modalTitle}>
|
||||||
|
{t("settings.odds_modal_title")}
|
||||||
|
</ThemedText>
|
||||||
|
<ScrollView style={{ marginVertical: 10 }}>
|
||||||
|
{BOOKMAKERS.map((name) => {
|
||||||
|
const isSelected =
|
||||||
|
state.oddsSettings.selectedBookmakers.includes(name);
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={name}
|
||||||
|
style={[
|
||||||
|
styles.bookmakerItem,
|
||||||
|
{ borderColor: isDark ? "#38383a" : "#eee" },
|
||||||
|
]}
|
||||||
|
onPress={() => selectBookmaker(name)}
|
||||||
|
>
|
||||||
|
<ThemedText
|
||||||
|
style={{
|
||||||
|
color: isSelected ? "#FF9500" : textColor,
|
||||||
|
fontWeight: isSelected ? "bold" : "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</ThemedText>
|
||||||
|
{isSelected && (
|
||||||
|
<IconSymbol name="checkmark" size={18} color="#FF9500" />
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.modalCancel}
|
||||||
|
onPress={() => setOddsModalVisible(false)}
|
||||||
|
>
|
||||||
|
<ThemedText type="defaultSemiBold">
|
||||||
|
{t("settings.odds_confirm")}
|
||||||
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -502,6 +686,13 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
|
bookmakerItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
|
},
|
||||||
logoutButton: {
|
logoutButton: {
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { ThemedText } from "@/components/themed-text";
|
import { ThemedText } from "@/components/themed-text";
|
||||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
|
import { useAppState } from "@/context/AppStateContext";
|
||||||
import { useTheme } from "@/context/ThemeContext";
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
import { addFavorite, removeFavorite } from "@/lib/api";
|
import { addFavorite, fetchOdds, removeFavorite } from "@/lib/api";
|
||||||
import { Match } from "@/types/api";
|
import { Match, OddsItem } from "@/types/api";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Pressable, StyleSheet, TouchableOpacity, View } from "react-native";
|
import { Pressable, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||||
|
|
||||||
interface MatchCardProps {
|
interface MatchCardProps {
|
||||||
@@ -23,12 +24,36 @@ export function MatchCard({
|
|||||||
}: MatchCardProps) {
|
}: MatchCardProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
const { state } = useAppState();
|
||||||
const [isFav, setIsFav] = useState(match.fav);
|
const [isFav, setIsFav] = useState(match.fav);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [odds, setOdds] = useState<OddsItem[]>(match.odds || []);
|
||||||
// console.log("MatchCard render:", JSON.stringify(match));
|
// console.log("MatchCard render:", JSON.stringify(match));
|
||||||
|
|
||||||
|
const oddsSettings = state.oddsSettings;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
oddsSettings.enabled &&
|
||||||
|
oddsSettings.selectedBookmakers.length > 0 &&
|
||||||
|
!match.odds
|
||||||
|
) {
|
||||||
|
fetchOdds(match.sportId || 1, parseInt(match.id))
|
||||||
|
.then((res) => {
|
||||||
|
const matchOdds = res[match.id]?.data || [];
|
||||||
|
setOdds(matchOdds);
|
||||||
|
})
|
||||||
|
.catch((err) => console.log("Fetch match card odds error:", err));
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
oddsSettings.enabled,
|
||||||
|
oddsSettings.selectedBookmakers,
|
||||||
|
match.id,
|
||||||
|
match.odds,
|
||||||
|
]);
|
||||||
|
|
||||||
// 当外部传入的 match.fav 改变时,更新内部状态
|
// 当外部传入的 match.fav 改变时,更新内部状态
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
setIsFav(match.fav);
|
setIsFav(match.fav);
|
||||||
}, [match.fav]);
|
}, [match.fav]);
|
||||||
|
|
||||||
@@ -39,6 +64,8 @@ export function MatchCard({
|
|||||||
const scoreBorder = isDark ? "rgba(255,255,255,0.18)" : "rgba(0,0,0,0.12)";
|
const scoreBorder = isDark ? "rgba(255,255,255,0.18)" : "rgba(0,0,0,0.12)";
|
||||||
const scoreBg = isDark ? "rgba(255,255,255,0.04)" : "rgba(255,255,255,0.6)";
|
const scoreBg = isDark ? "rgba(255,255,255,0.04)" : "rgba(255,255,255,0.6)";
|
||||||
|
|
||||||
|
const oddBadgeBg = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.03)";
|
||||||
|
|
||||||
const isLive = React.useMemo(() => {
|
const isLive = React.useMemo(() => {
|
||||||
return !!match.isLive;
|
return !!match.isLive;
|
||||||
}, [match.isLive]);
|
}, [match.isLive]);
|
||||||
@@ -149,42 +176,103 @@ export function MatchCard({
|
|||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Middle: Teams */}
|
{/* Middle: Teams & Odds */}
|
||||||
<View style={styles.middle}>
|
<View style={styles.middle}>
|
||||||
<View style={styles.teamRow}>
|
<View style={styles.teamContainer}>
|
||||||
{match.homeTeamLogo ? (
|
<View style={styles.teamRow}>
|
||||||
<Image
|
{match.homeTeamLogo ? (
|
||||||
source={{ uri: match.homeTeamLogo }}
|
<Image
|
||||||
style={styles.teamLogo}
|
source={{ uri: match.homeTeamLogo }}
|
||||||
contentFit="contain"
|
style={styles.teamLogo}
|
||||||
/>
|
contentFit="contain"
|
||||||
) : null}
|
/>
|
||||||
<ThemedText
|
) : null}
|
||||||
type="defaultSemiBold"
|
<ThemedText
|
||||||
style={styles.teamLine}
|
type="defaultSemiBold"
|
||||||
numberOfLines={1}
|
style={styles.teamLine}
|
||||||
ellipsizeMode="tail"
|
numberOfLines={1}
|
||||||
>
|
ellipsizeMode="tail"
|
||||||
{match.homeTeamName || match.home}
|
>
|
||||||
</ThemedText>
|
{match.homeTeamName || match.home}
|
||||||
</View>
|
</ThemedText>
|
||||||
<View style={styles.teamRow}>
|
</View>
|
||||||
{match.awayTeamLogo ? (
|
<View style={styles.teamRow}>
|
||||||
<Image
|
{match.awayTeamLogo ? (
|
||||||
source={{ uri: match.awayTeamLogo }}
|
<Image
|
||||||
style={styles.teamLogo}
|
source={{ uri: match.awayTeamLogo }}
|
||||||
contentFit="contain"
|
style={styles.teamLogo}
|
||||||
/>
|
contentFit="contain"
|
||||||
) : null}
|
/>
|
||||||
<ThemedText
|
) : null}
|
||||||
type="defaultSemiBold"
|
<ThemedText
|
||||||
style={styles.teamLine}
|
type="defaultSemiBold"
|
||||||
numberOfLines={1}
|
style={styles.teamLine}
|
||||||
ellipsizeMode="tail"
|
numberOfLines={1}
|
||||||
>
|
ellipsizeMode="tail"
|
||||||
{match.awayTeamName || match.away}
|
>
|
||||||
</ThemedText>
|
{match.awayTeamName || match.away}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Odds Section */}
|
||||||
|
{oddsSettings.enabled && odds.length > 0 && (
|
||||||
|
<View style={styles.oddsContainer}>
|
||||||
|
{oddsSettings.selectedBookmakers.map((bookmaker, idx) => {
|
||||||
|
const item = odds.find((o) => o.odd_bookmakers === bookmaker);
|
||||||
|
if (!item) return null;
|
||||||
|
// Pick 3 values to display. Using odd_1, odd_x, odd_2 for example.
|
||||||
|
// Or try to match the screenshot's 3-column style.
|
||||||
|
const val1 = item.odd_1 || item.ah0_1 || "-";
|
||||||
|
const val2 = item.odd_x || "0" || "-";
|
||||||
|
const val3 = item.odd_2 || item.ah0_2 || "-";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View key={bookmaker} style={styles.bookmakerOddsRow}>
|
||||||
|
<View
|
||||||
|
style={[styles.oddBadge, { backgroundColor: oddBadgeBg }]}
|
||||||
|
>
|
||||||
|
<ThemedText
|
||||||
|
style={[
|
||||||
|
styles.oddText,
|
||||||
|
{ color: isDark ? "#fff" : "#000" },
|
||||||
|
idx === 0 && styles.oddTextHighlight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{val1}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[styles.oddBadge, { backgroundColor: oddBadgeBg }]}
|
||||||
|
>
|
||||||
|
<ThemedText
|
||||||
|
style={[
|
||||||
|
styles.oddText,
|
||||||
|
{ color: isDark ? "#fff" : "#000" },
|
||||||
|
idx === 0 && styles.oddTextHighlight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{val2}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[styles.oddBadge, { backgroundColor: oddBadgeBg }]}
|
||||||
|
>
|
||||||
|
<ThemedText
|
||||||
|
style={[
|
||||||
|
styles.oddText,
|
||||||
|
{ color: isDark ? "#fff" : "#000" },
|
||||||
|
idx === 0 && styles.oddTextHighlight,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{val3}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Right: Score box + favorite */}
|
{/* Right: Score box + favorite */}
|
||||||
@@ -270,6 +358,14 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
},
|
},
|
||||||
middle: {
|
middle: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 8,
|
||||||
|
minWidth: 0,
|
||||||
|
},
|
||||||
|
teamContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
gap: 8,
|
gap: 8,
|
||||||
@@ -289,6 +385,31 @@ const styles = StyleSheet.create({
|
|||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
oddsContainer: {
|
||||||
|
gap: 8,
|
||||||
|
alignItems: "flex-end",
|
||||||
|
},
|
||||||
|
bookmakerOddsRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
oddBadge: {
|
||||||
|
backgroundColor: "rgba(0,0,0,0.03)",
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 6,
|
||||||
|
minWidth: 40,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
oddText: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: "600",
|
||||||
|
opacity: 0.8,
|
||||||
|
},
|
||||||
|
oddTextHighlight: {
|
||||||
|
color: "#FF9500",
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
right: {
|
right: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import React, { createContext, ReactNode, useContext, useState } from "react";
|
import { OddsSettings, storage } from "@/lib/storage";
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
selectedSportId: number | null;
|
selectedSportId: number | null;
|
||||||
selectedDate: Date;
|
selectedDate: Date;
|
||||||
selectedLeagueKey: string | null;
|
selectedLeagueKey: string | null;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
|
oddsSettings: OddsSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppStateContextType {
|
interface AppStateContextType {
|
||||||
@@ -13,10 +21,11 @@ interface AppStateContextType {
|
|||||||
updateDate: (date: Date) => void;
|
updateDate: (date: Date) => void;
|
||||||
updateLeagueKey: (leagueKey: string | null) => void;
|
updateLeagueKey: (leagueKey: string | null) => void;
|
||||||
updateTimezone: (timezone: string) => void;
|
updateTimezone: (timezone: string) => void;
|
||||||
|
updateOddsSettings: (settings: OddsSettings) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppStateContext = createContext<AppStateContextType | undefined>(
|
const AppStateContext = createContext<AppStateContextType | undefined>(
|
||||||
undefined
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
export function AppStateProvider({ children }: { children: ReactNode }) {
|
export function AppStateProvider({ children }: { children: ReactNode }) {
|
||||||
@@ -25,8 +34,16 @@ export function AppStateProvider({ children }: { children: ReactNode }) {
|
|||||||
selectedDate: new Date(),
|
selectedDate: new Date(),
|
||||||
selectedLeagueKey: null,
|
selectedLeagueKey: null,
|
||||||
timezone: "UTC",
|
timezone: "UTC",
|
||||||
|
oddsSettings: { enabled: false, selectedBookmakers: [] },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initial load of odds settings
|
||||||
|
storage.getOddsSettings().then((settings) => {
|
||||||
|
setState((prev) => ({ ...prev, oddsSettings: settings }));
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const updateSportId = (sportId: number | null) => {
|
const updateSportId = (sportId: number | null) => {
|
||||||
setState((prev) => ({ ...prev, selectedSportId: sportId }));
|
setState((prev) => ({ ...prev, selectedSportId: sportId }));
|
||||||
};
|
};
|
||||||
@@ -43,6 +60,11 @@ export function AppStateProvider({ children }: { children: ReactNode }) {
|
|||||||
setState((prev) => ({ ...prev, timezone }));
|
setState((prev) => ({ ...prev, timezone }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateOddsSettings = (settings: OddsSettings) => {
|
||||||
|
setState((prev) => ({ ...prev, oddsSettings: settings }));
|
||||||
|
storage.setOddsSettings(settings);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppStateContext.Provider
|
<AppStateContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -51,6 +73,7 @@ export function AppStateProvider({ children }: { children: ReactNode }) {
|
|||||||
updateDate,
|
updateDate,
|
||||||
updateLeagueKey,
|
updateLeagueKey,
|
||||||
updateTimezone,
|
updateTimezone,
|
||||||
|
updateOddsSettings,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -23,7 +23,21 @@
|
|||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"english": "English",
|
"english": "English",
|
||||||
"chinese": "Chinese"
|
"chinese": "Chinese",
|
||||||
|
"odds_title": "Odds Settings",
|
||||||
|
"odds_show": "Show Odds",
|
||||||
|
"odds_select_company": "Select Bookmakers (Max 2)",
|
||||||
|
"odds_enabled": "On",
|
||||||
|
"odds_disabled": "Off",
|
||||||
|
"odds_unselected": "Unselected",
|
||||||
|
"odds_modal_title": "Select Bookmakers (Max 2)",
|
||||||
|
"odds_confirm": "Confirm",
|
||||||
|
"login": "Login",
|
||||||
|
"click_to_login": "Click to login",
|
||||||
|
"logout": "Logout",
|
||||||
|
"select_login_method": "Select login method",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"google_login": "Google Login"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "ScoreNow",
|
"title": "ScoreNow",
|
||||||
|
|||||||
@@ -23,7 +23,21 @@
|
|||||||
"dark": "深色",
|
"dark": "深色",
|
||||||
"system": "跟随系统",
|
"system": "跟随系统",
|
||||||
"english": "英文",
|
"english": "英文",
|
||||||
"chinese": "中文"
|
"chinese": "中文",
|
||||||
|
"odds_title": "赔率设置",
|
||||||
|
"odds_show": "展示赔率",
|
||||||
|
"odds_select_company": "选择公司 (最多2个)",
|
||||||
|
"odds_enabled": "开启",
|
||||||
|
"odds_disabled": "关闭",
|
||||||
|
"odds_unselected": "未选择",
|
||||||
|
"odds_modal_title": "选择赔率公司 (最多2项)",
|
||||||
|
"odds_confirm": "确定",
|
||||||
|
"login": "登录",
|
||||||
|
"click_to_login": "点击登录",
|
||||||
|
"logout": "登出",
|
||||||
|
"select_login_method": "选择登录方式",
|
||||||
|
"cancel": "取消",
|
||||||
|
"google_login": "Google 登录"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "ScoreNow",
|
"title": "ScoreNow",
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
||||||
import type { UserProfile } from "@/types/api";
|
import type { UserProfile } from "@/types/api";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
|
||||||
const STORAGE_KEYS = {
|
const STORAGE_KEYS = {
|
||||||
ACCESS_TOKEN: "access_token",
|
ACCESS_TOKEN: "access_token",
|
||||||
REFRESH_TOKEN: "refresh_token",
|
REFRESH_TOKEN: "refresh_token",
|
||||||
USER: "user",
|
USER: "user",
|
||||||
|
ODDS_SETTINGS: "odds_settings",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface OddsSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
selectedBookmakers: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export const storage = {
|
export const storage = {
|
||||||
async setAccessToken(token: string): Promise<void> {
|
async setAccessToken(token: string): Promise<void> {
|
||||||
await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, token);
|
await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, token);
|
||||||
@@ -38,11 +44,29 @@ export const storage = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async setOddsSettings(settings: OddsSettings): Promise<void> {
|
||||||
|
await AsyncStorage.setItem(
|
||||||
|
STORAGE_KEYS.ODDS_SETTINGS,
|
||||||
|
JSON.stringify(settings),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getOddsSettings(): Promise<OddsSettings> {
|
||||||
|
const settingsStr = await AsyncStorage.getItem(STORAGE_KEYS.ODDS_SETTINGS);
|
||||||
|
if (!settingsStr) return { enabled: false, selectedBookmakers: [] };
|
||||||
|
try {
|
||||||
|
return JSON.parse(settingsStr) as OddsSettings;
|
||||||
|
} catch {
|
||||||
|
return { enabled: false, selectedBookmakers: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async clear(): Promise<void> {
|
async clear(): Promise<void> {
|
||||||
await AsyncStorage.multiRemove([
|
await AsyncStorage.multiRemove([
|
||||||
STORAGE_KEYS.ACCESS_TOKEN,
|
STORAGE_KEYS.ACCESS_TOKEN,
|
||||||
STORAGE_KEYS.REFRESH_TOKEN,
|
STORAGE_KEYS.REFRESH_TOKEN,
|
||||||
STORAGE_KEYS.USER,
|
STORAGE_KEYS.USER,
|
||||||
|
STORAGE_KEYS.ODDS_SETTINGS,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface Match {
|
|||||||
leagueId?: number;
|
leagueId?: number;
|
||||||
isLive?: boolean;
|
isLive?: boolean;
|
||||||
meta?: string;
|
meta?: string;
|
||||||
|
odds?: OddsItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LiveScoreMatch {
|
export interface LiveScoreMatch {
|
||||||
|
|||||||
Reference in New Issue
Block a user