feat(app): add version check system and enhance internationalization support

Add comprehensive app update checking functionality with:
- New VersionCheckContext for managing update detection and notifications
- VersionUpdateModal UI component for presenting update information
- Version service API integration with platform-specific update URLs
- Version check menu item in personal settings with manual/automatic checking

Enhance internationalization across workout features:
- Complete workout type translations for English and Chinese
- Localized workout detail modal with proper date/time formatting
- Locale-aware date formatting in fitness rings detail
- Workout notification improvements with deep linking to specific workout details

Improve UI/UX with better chart rendering, sizing fixes, and enhanced navigation flow. Update app version to 1.1.3 and include app version in API headers for better tracking.
This commit is contained in:
2025-11-29 20:47:16 +08:00
parent 83b77615cf
commit a309123b35
19 changed files with 1132 additions and 159 deletions

View File

@@ -3,6 +3,7 @@ import { useFocusEffect } from '@react-navigation/native';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';
import { LinearGradient } from 'expo-linear-gradient';
import { useLocalSearchParams } from 'expo-router';
import React, { useCallback, useMemo, useState } from 'react';
import {
ActivityIndicator,
@@ -274,6 +275,7 @@ function groupWorkouts(workouts: WorkoutData[]): WorkoutSection[] {
export default function WorkoutHistoryScreen() {
const { t } = useI18n();
const { workoutId: workoutIdParam } = useLocalSearchParams<{ workoutId?: string | string[] }>();
const [sections, setSections] = useState<WorkoutSection[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -285,9 +287,20 @@ export default function WorkoutHistoryScreen() {
const [selectedIntensity, setSelectedIntensity] = useState<IntensityBadge | null>(null);
const [monthOccurrenceText, setMonthOccurrenceText] = useState<string | null>(null);
const [monthlyStats, setMonthlyStats] = useState<MonthlyStatsInfo | null>(null);
const [pendingWorkoutId, setPendingWorkoutId] = useState<string | null>(null);
const safeAreaTop = useSafeAreaTop();
React.useEffect(() => {
if (!workoutIdParam) {
return;
}
const idParam = Array.isArray(workoutIdParam) ? workoutIdParam[0] : workoutIdParam;
if (idParam) {
setPendingWorkoutId(idParam);
}
}, [workoutIdParam]);
const loadHistory = useCallback(async () => {
setIsLoading(true);
setError(null);
@@ -484,6 +497,22 @@ export default function WorkoutHistoryScreen() {
loadWorkoutDetail(workout);
}, [computeMonthlyOccurrenceText, loadWorkoutDetail]);
React.useEffect(() => {
if (!pendingWorkoutId || isLoading) {
return;
}
const allWorkouts = sections.flatMap((section) => section.data);
const targetWorkout = allWorkouts.find((workout) => workout.id === pendingWorkoutId);
if (targetWorkout) {
handleWorkoutPress(targetWorkout);
}
// 清理待处理状态,避免重复触发
setPendingWorkoutId(null);
}, [pendingWorkoutId, isLoading, sections, handleWorkoutPress]);
const handleRetryDetail = useCallback(() => {
if (selectedWorkout) {
loadWorkoutDetail(selectedWorkout);