feat: 完善饮水 widget

This commit is contained in:
richarjiang
2025-09-09 14:26:16 +08:00
parent cacfde064f
commit e56ebe3636
13 changed files with 984 additions and 62 deletions

284
utils/widgetDataSync.ts Normal file
View File

@@ -0,0 +1,284 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { NativeModules } from 'react-native';
// Widget数据同步服务
// 用于在主App和iOS Widget之间同步饮水数据
// App Group标识符
const APP_GROUP_ID = 'group.com.anonymous.digitalpilates';
// Widget数据存储键
const WIDGET_DATA_KEYS = {
CURRENT_WATER_INTAKE: 'widget_current_water_intake',
DAILY_WATER_GOAL: 'widget_daily_water_goal',
QUICK_ADD_AMOUNT: 'widget_quick_add_amount',
LAST_SYNC_TIME: 'widget_last_sync_time',
PENDING_WATER_RECORDS: 'widget_pending_water_records',
} as const;
export interface WidgetWaterData {
currentIntake: number;
dailyGoal: number;
quickAddAmount: number;
lastSyncTime: string;
}
// 默认数据
const DEFAULT_WIDGET_DATA: WidgetWaterData = {
currentIntake: 0,
dailyGoal: 2000,
quickAddAmount: 150,
lastSyncTime: new Date().toISOString(),
};
/**
* 创建Native Module来访问App Group UserDefaults
* 这需要在iOS原生代码中实现
*/
interface AppGroupUserDefaults {
setString: (groupId: string, key: string, value: string) => Promise<void>;
getString: (groupId: string, key: string) => Promise<string | null>;
setNumber: (groupId: string, key: string, value: number) => Promise<void>;
getNumber: (groupId: string, key: string) => Promise<number>;
removeKey: (groupId: string, key: string) => Promise<void>;
setArray: (groupId: string, key: string, value: any[]) => Promise<void>;
getArray: (groupId: string, key: string) => Promise<any[] | null>;
}
// Widget待同步水记录接口
export interface PendingWaterRecord {
amount: number;
recordedAt: string;
source: string;
widgetAdded: boolean;
}
// 尝试获取原生模块如果不存在则使用fallback
const AppGroupDefaults: AppGroupUserDefaults | null = NativeModules.AppGroupUserDefaults || null;
/**
* 将饮水数据同步到Widget
*/
export const syncWaterDataToWidget = async (data: Partial<WidgetWaterData>): Promise<void> => {
try {
if (!AppGroupDefaults) {
console.warn('AppGroupUserDefaults native module not available, falling back to AsyncStorage');
// Fallback到AsyncStorage (仅主App可用)
const currentData = await getWidgetDataFromAsyncStorage();
const updatedData = { ...currentData, ...data, lastSyncTime: new Date().toISOString() };
await saveWidgetDataToAsyncStorage(updatedData);
return;
}
// 使用App Group UserDefaults
if (data.currentIntake !== undefined) {
await AppGroupDefaults.setNumber(APP_GROUP_ID, WIDGET_DATA_KEYS.CURRENT_WATER_INTAKE, data.currentIntake);
}
if (data.dailyGoal !== undefined) {
await AppGroupDefaults.setNumber(APP_GROUP_ID, WIDGET_DATA_KEYS.DAILY_WATER_GOAL, data.dailyGoal);
}
if (data.quickAddAmount !== undefined) {
await AppGroupDefaults.setNumber(APP_GROUP_ID, WIDGET_DATA_KEYS.QUICK_ADD_AMOUNT, data.quickAddAmount);
}
// 更新同步时间
await AppGroupDefaults.setString(APP_GROUP_ID, WIDGET_DATA_KEYS.LAST_SYNC_TIME, new Date().toISOString());
console.log('Widget data synced successfully:', data);
} catch (error) {
console.error('Failed to sync data to widget:', error);
throw error;
}
};
/**
* 从Widget获取饮水数据
*/
export const getWidgetWaterData = async (): Promise<WidgetWaterData> => {
try {
if (!AppGroupDefaults) {
console.warn('AppGroupUserDefaults native module not available, falling back to AsyncStorage');
return await getWidgetDataFromAsyncStorage();
}
// 从App Group UserDefaults读取数据
const currentIntake = await AppGroupDefaults.getNumber(APP_GROUP_ID, WIDGET_DATA_KEYS.CURRENT_WATER_INTAKE);
const dailyGoal = await AppGroupDefaults.getNumber(APP_GROUP_ID, WIDGET_DATA_KEYS.DAILY_WATER_GOAL);
const quickAddAmount = await AppGroupDefaults.getNumber(APP_GROUP_ID, WIDGET_DATA_KEYS.QUICK_ADD_AMOUNT);
const lastSyncTime = await AppGroupDefaults.getString(APP_GROUP_ID, WIDGET_DATA_KEYS.LAST_SYNC_TIME);
return {
currentIntake: currentIntake || DEFAULT_WIDGET_DATA.currentIntake,
dailyGoal: dailyGoal || DEFAULT_WIDGET_DATA.dailyGoal,
quickAddAmount: quickAddAmount || DEFAULT_WIDGET_DATA.quickAddAmount,
lastSyncTime: lastSyncTime || DEFAULT_WIDGET_DATA.lastSyncTime,
};
} catch (error) {
console.error('Failed to get widget data:', error);
return DEFAULT_WIDGET_DATA;
}
};
/**
* 清除Widget数据
*/
export const clearWidgetData = async (): Promise<void> => {
try {
if (!AppGroupDefaults) {
await AsyncStorage.multiRemove(Object.values(WIDGET_DATA_KEYS));
return;
}
// 清除App Group UserDefaults中的数据
await Promise.all([
AppGroupDefaults.removeKey(APP_GROUP_ID, WIDGET_DATA_KEYS.CURRENT_WATER_INTAKE),
AppGroupDefaults.removeKey(APP_GROUP_ID, WIDGET_DATA_KEYS.DAILY_WATER_GOAL),
AppGroupDefaults.removeKey(APP_GROUP_ID, WIDGET_DATA_KEYS.QUICK_ADD_AMOUNT),
AppGroupDefaults.removeKey(APP_GROUP_ID, WIDGET_DATA_KEYS.LAST_SYNC_TIME),
]);
console.log('Widget data cleared successfully');
} catch (error) {
console.error('Failed to clear widget data:', error);
throw error;
}
};
/**
* Fallback: 使用AsyncStorage存储Widget数据
*/
const saveWidgetDataToAsyncStorage = async (data: WidgetWaterData): Promise<void> => {
const dataToStore = [
[WIDGET_DATA_KEYS.CURRENT_WATER_INTAKE, data.currentIntake.toString()],
[WIDGET_DATA_KEYS.DAILY_WATER_GOAL, data.dailyGoal.toString()],
[WIDGET_DATA_KEYS.QUICK_ADD_AMOUNT, data.quickAddAmount.toString()],
[WIDGET_DATA_KEYS.LAST_SYNC_TIME, data.lastSyncTime],
];
await AsyncStorage.multiSet(dataToStore);
};
/**
* Fallback: 从AsyncStorage读取Widget数据
*/
const getWidgetDataFromAsyncStorage = async (): Promise<WidgetWaterData> => {
const keys = Object.values(WIDGET_DATA_KEYS);
const values = await AsyncStorage.multiGet(keys);
const data: any = {};
values.forEach(([key, value]) => {
if (value !== null) {
if (key === WIDGET_DATA_KEYS.LAST_SYNC_TIME) {
data[key] = value;
} else {
data[key] = parseInt(value, 10);
}
}
});
return {
currentIntake: data[WIDGET_DATA_KEYS.CURRENT_WATER_INTAKE] || DEFAULT_WIDGET_DATA.currentIntake,
dailyGoal: data[WIDGET_DATA_KEYS.DAILY_WATER_GOAL] || DEFAULT_WIDGET_DATA.dailyGoal,
quickAddAmount: data[WIDGET_DATA_KEYS.QUICK_ADD_AMOUNT] || DEFAULT_WIDGET_DATA.quickAddAmount,
lastSyncTime: data[WIDGET_DATA_KEYS.LAST_SYNC_TIME] || DEFAULT_WIDGET_DATA.lastSyncTime,
};
};
/**
* 触发Widget刷新
* 通知iOS系统更新Widget
*/
export const refreshWidget = async (): Promise<void> => {
try {
if (NativeModules.WidgetManager?.reloadTimelines) {
await NativeModules.WidgetManager.reloadTimelines();
console.log('Widget refresh triggered');
} else {
console.warn('WidgetManager native module not available');
}
} catch (error) {
console.error('Failed to refresh widget:', error);
}
};
/**
* 获取待同步的水记录
*/
export const getPendingWaterRecords = async (): Promise<PendingWaterRecord[]> => {
try {
if (!AppGroupDefaults) {
// Fallback: 从 AsyncStorage 读取
const pendingRecordsJson = await AsyncStorage.getItem(WIDGET_DATA_KEYS.PENDING_WATER_RECORDS);
return pendingRecordsJson ? JSON.parse(pendingRecordsJson) : [];
}
// 从 App Group UserDefaults 读取
const pendingRecords = await AppGroupDefaults.getArray(APP_GROUP_ID, WIDGET_DATA_KEYS.PENDING_WATER_RECORDS);
return pendingRecords || [];
} catch (error) {
console.error('Failed to get pending water records:', error);
return [];
}
};
/**
* 清除待同步的水记录
*/
export const clearPendingWaterRecords = async (): Promise<void> => {
try {
if (!AppGroupDefaults) {
// Fallback: 从 AsyncStorage 清除
await AsyncStorage.removeItem(WIDGET_DATA_KEYS.PENDING_WATER_RECORDS);
return;
}
// 从 App Group UserDefaults 清除
await AppGroupDefaults.removeKey(APP_GROUP_ID, WIDGET_DATA_KEYS.PENDING_WATER_RECORDS);
console.log('Pending water records cleared successfully');
} catch (error) {
console.error('Failed to clear pending water records:', error);
}
};
/**
* 从Widget同步待处理的数据更改到主App
* 检查Widget中的数据更改并同步到主App的数据存储
*/
export const syncPendingWidgetChanges = async (): Promise<{
hasPendingChanges: boolean;
pendingRecords?: PendingWaterRecord[];
lastSyncTime?: string;
}> => {
try {
// 获取待同步的水记录
const pendingRecords = await getPendingWaterRecords();
if (pendingRecords.length > 0) {
console.log(`发现 ${pendingRecords.length} 条待同步的水记录`);
return {
hasPendingChanges: true,
pendingRecords,
lastSyncTime: new Date().toISOString(),
};
}
return { hasPendingChanges: false };
} catch (error) {
console.error('Failed to sync pending widget changes:', error);
return { hasPendingChanges: false };
}
};
/**
* 标记Widget数据已被主App同步
*/
export const markWidgetDataSynced = async (): Promise<void> => {
try {
const currentTime = new Date().toISOString();
await AsyncStorage.setItem('last_app_widget_sync', currentTime);
} catch (error) {
console.error('Failed to mark widget data as synced:', error);
}
};