2 Commits

29 changed files with 319 additions and 98 deletions

View File

@@ -1,11 +1,11 @@
{ {
"expo": { "expo": {
"name": "Hey Mama", "name": "client",
"slug": "hey-mama", "slug": "client",
"version": "1.0.0", "version": "1.0.0",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "heymama", "scheme": "client",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
"newArchEnabled": true, "newArchEnabled": true,
"splash": { "splash": {
@@ -15,7 +15,7 @@
}, },
"ios": { "ios": {
"supportsTablet": true, "supportsTablet": true,
"bundleIdentifier": "com.heymama.app" "bundleIdentifier": "com.anonymous.client"
}, },
"android": { "android": {
"adaptiveIcon": { "adaptiveIcon": {

View File

@@ -1,5 +1,5 @@
import { useEffect, useLayoutEffect, useMemo, useState, useCallback, useRef } from 'react'; import { useEffect, useLayoutEffect, useMemo, useState, useCallback, useRef } from 'react';
import { StyleSheet, View, Dimensions, Text, Pressable, PanResponder, Animated as RNAnimated } from 'react-native'; import { StyleSheet, View, Dimensions, Text, Pressable, PanResponder, Animated as RNAnimated, ImageBackground } from 'react-native';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigation, useFocusEffect } from 'expo-router'; import { useNavigation, useFocusEffect } from 'expo-router';
import Animated, { import Animated, {
@@ -31,6 +31,40 @@ import LikeIcon from '@/assets/images/icon/like_icon.svg';
const { height: SCREEN_HEIGHT } = Dimensions.get('window'); const { height: SCREEN_HEIGHT } = Dimensions.get('window');
// 预定义风景图列表
const NATURE_IMAGES = [
require('@/assets/theme/nature/1.png'),
require('@/assets/theme/nature/2.png'),
require('@/assets/theme/nature/3.png'),
require('@/assets/theme/nature/4.png'),
require('@/assets/theme/nature/5.png'),
require('@/assets/theme/nature/6.png'),
require('@/assets/theme/nature/7.png'),
require('@/assets/theme/nature/8.png'),
require('@/assets/theme/nature/9.png'),
require('@/assets/theme/nature/10.png'),
require('@/assets/theme/nature/11.png'),
require('@/assets/theme/nature/12.png'),
require('@/assets/theme/nature/13.png'),
require('@/assets/theme/nature/14.png'),
require('@/assets/theme/nature/15.png'),
require('@/assets/theme/nature/17.png'),
require('@/assets/theme/nature/18.png'),
require('@/assets/theme/nature/19.png'),
require('@/assets/theme/nature/20.png'),
require('@/assets/theme/nature/22.png'),
];
// 预定义颜色列表
const THEME_COLORS = [
'#F7D9BF',
'#CBF2D8',
'#F5CDDE',
'#F2ECCB',
'#E2CBF2',
'#CBD9F2',
];
export default function HomeScreen() { export default function HomeScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigation = useNavigation(); const navigation = useNavigation();
@@ -66,12 +100,26 @@ export default function HomeScreen() {
}, []) }, [])
); );
const backgroundColor = themeMode === 'color' ? '#F3D0E1' : '#F4D6C2'; const backgroundColor = useMemo(() => {
if (themeMode === 'color') {
const colorIndex = Math.floor(index / 10) % THEME_COLORS.length;
return THEME_COLORS[colorIndex];
}
return '#F4D6C2'; // 风景模式下的默认底色(图片加载前显示)
}, [themeMode, index]);
// 计算当前应该显示的风景图索引(滑动 10 次切换一张)
const natureImageIndex = useMemo(() => {
return Math.floor(index / 10) % NATURE_IMAGES.length;
}, [index]);
const currentNatureImage = NATURE_IMAGES[natureImageIndex];
useLayoutEffect(() => { useLayoutEffect(() => {
navigation.setOptions({ navigation.setOptions({
headerShadowVisible: false, headerShadowVisible: false,
headerStyle: { backgroundColor }, headerStyle: { backgroundColor: themeMode === 'scenery' ? 'transparent' : backgroundColor },
headerTransparent: themeMode === 'scenery',
headerRight: () => ( headerRight: () => (
<View style={styles.headerRight}> <View style={styles.headerRight}>
<CircleIconButton <CircleIconButton
@@ -89,7 +137,7 @@ export default function HomeScreen() {
</View> </View>
), ),
}); });
}, [backgroundColor, navigation, t]); }, [backgroundColor, themeMode, navigation, t]);
const textAnimatedStyle = useAnimatedStyle(() => ({ const textAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }], transform: [{ translateY: translateY.value }],
@@ -176,10 +224,11 @@ export default function HomeScreen() {
// 2. 保存到收藏夹,包含当前背景信息 // 2. 保存到收藏夹,包含当前背景信息
await addFavorite({ await addFavorite({
favId: String(Date.now()), // 生成唯一 ID
id: item.id, id: item.id,
date: dateStr, date: dateStr,
themeMode: themeMode, themeMode: themeMode,
background: backgroundColor, // 目前存储的是颜色值 background: themeMode === 'scenery' ? String(natureImageIndex) : backgroundColor,
}); });
// 3. 爱心缩放动画 // 3. 爱心缩放动画
@@ -202,8 +251,15 @@ export default function HomeScreen() {
return ( return (
<View style={[styles.container, { backgroundColor }]} {...panResponder.panHandlers}> <View style={[styles.container, { backgroundColor }]} {...panResponder.panHandlers}>
<Animated.View style={[styles.card, textAnimatedStyle]}> {themeMode === 'scenery' && (
<Text style={styles.text}>{item.text}</Text> <ImageBackground
source={currentNatureImage}
style={StyleSheet.absoluteFill}
resizeMode="cover"
/>
)}
<Animated.View style={[styles.card, textAnimatedStyle, themeMode === 'scenery' && styles.sceneryCard]}>
<Text style={[styles.text, themeMode === 'scenery' && styles.sceneryText]}>{item.text}</Text>
</Animated.View> </Animated.View>
<View style={styles.actions}> <View style={styles.actions}>
@@ -216,9 +272,13 @@ export default function HomeScreen() {
style={styles.reactionInner} style={styles.reactionInner}
> >
{likeFilled ? ( {likeFilled ? (
<LikeFilledIcon width={35} height={36} /> <LikeFilledIcon width={35} height={36} style={{ color: '#EA6969' }} />
) : ( ) : (
<LikeIcon width={35} height={36} /> <LikeIcon
width={35}
height={36}
style={{ color: themeMode === 'scenery' ? '#FFFFFF' : '#5E2A28' }}
/>
)} )}
</Pressable> </Pressable>
</Animated.View> </Animated.View>
@@ -277,9 +337,15 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
}, },
card: { card: {
paddingHorizontal: 30, position: 'absolute',
alignItems: 'center', top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 30,
zIndex: 5, // 降低层级,防止遮挡底部按钮
}, },
text: { text: {
fontSize: 22, fontSize: 22,
@@ -288,6 +354,16 @@ const styles = StyleSheet.create({
fontWeight: '700', fontWeight: '700',
textAlign: 'center', textAlign: 'center',
}, },
sceneryCard: {
// 风景模式下稍微收窄文案宽度,增加呼吸感
paddingHorizontal: 50,
},
sceneryText: {
color: '#FFFFFF',
textShadowColor: 'rgba(0, 0, 0, 0.5)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 4,
},
actions: { actions: {
position: 'absolute', position: 'absolute',
bottom: SCREEN_HEIGHT * 0.16, bottom: SCREEN_HEIGHT * 0.16,
@@ -295,6 +371,7 @@ const styles = StyleSheet.create({
right: 0, right: 0,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
zIndex: 20, // 提升层级,确保在最顶层可点击
}, },
reactionButton: { reactionButton: {
alignItems: 'center', alignItems: 'center',

View File

@@ -97,7 +97,8 @@ export default function OnboardingScreen() {
} }
}; };
const onSkip = () => { const onSkip = async () => {
await setOnboardingCompleted(true);
router.replace('/(app)/home'); router.replace('/(app)/home');
}; };

View File

@@ -14,9 +14,9 @@ export default function Index() {
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
(async () => { (async () => {
// 【完全重置】:清除本地存储的所有数据(收藏、设置、引导状态等) // 【临时清除数据】:用于测试完整流程
await AsyncStorage.clear(); await AsyncStorage.clear();
console.log('AsyncStorage has been cleared.'); console.log('AsyncStorage has been cleared for testing.');
// 1. 检查是否同意协议 // 1. 检查是否同意协议
const consentAccepted = await getConsentAccepted(); const consentAccepted = await getConsentAccepted();
@@ -27,9 +27,17 @@ export default function Index() {
return; return;
} }
// 2. 检查 Onboarding // 2. 检查 Onboarding 是否已完成
const completed = await getOnboardingCompleted(); const completed = await getOnboardingCompleted();
router.replace(completed ? '/(app)/home' : '/(onboarding)/onboarding'); if (cancelled) return;
if (completed) {
// 如果已经完成过流程,直接进 Home
router.replace('/(app)/home');
} else {
// 如果是首次进入(或未完成流程),进入 Onboarding
router.replace('/(onboarding)/onboarding');
}
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

View File

@@ -50,6 +50,29 @@ type Props = {
type Page = 'root' | 'favorites' | 'dailyReminder' | 'widget' | 'language' | 'widgetHowTo'; type Page = 'root' | 'favorites' | 'dailyReminder' | 'widget' | 'language' | 'widgetHowTo';
type NavDirection = 'forward' | 'back'; type NavDirection = 'forward' | 'back';
const NATURE_IMAGES = [
require('@/assets/theme/nature/1.png'),
require('@/assets/theme/nature/2.png'),
require('@/assets/theme/nature/3.png'),
require('@/assets/theme/nature/4.png'),
require('@/assets/theme/nature/5.png'),
require('@/assets/theme/nature/6.png'),
require('@/assets/theme/nature/7.png'),
require('@/assets/theme/nature/8.png'),
require('@/assets/theme/nature/9.png'),
require('@/assets/theme/nature/10.png'),
require('@/assets/theme/nature/11.png'),
require('@/assets/theme/nature/12.png'),
require('@/assets/theme/nature/13.png'),
require('@/assets/theme/nature/14.png'),
require('@/assets/theme/nature/15.png'),
require('@/assets/theme/nature/17.png'),
require('@/assets/theme/nature/18.png'),
require('@/assets/theme/nature/19.png'),
require('@/assets/theme/nature/20.png'),
require('@/assets/theme/nature/22.png'),
];
export default function ProfileModal({ visible, name: propName, onClose }: Props) { export default function ProfileModal({ visible, name: propName, onClose }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -260,11 +283,11 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
setFavorites(list); setFavorites(list);
} }
async function handleRemove(id: string) { async function handleRemove(favId: string) {
// 1. 调用存储层移除收藏 // 1. 调用存储层移除收藏
await removeFavorite(id); await removeFavorite(favId);
// 2. 更新本地状态 // 2. 更新本地状态
setFavorites(prev => prev.filter(item => item.id !== id)); setFavorites(prev => prev.filter(item => item.favId !== favId));
} }
return ( return (
@@ -274,7 +297,7 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
) : ( ) : (
<FlatList <FlatList
data={favorites} data={favorites}
keyExtractor={(it) => it.id} keyExtractor={(it) => it.favId}
contentContainerStyle={styles.favList} contentContainerStyle={styles.favList}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
renderItem={({ item }) => ( renderItem={({ item }) => (
@@ -290,11 +313,30 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) {
<View style={styles.favRight}> <View style={styles.favRight}>
<View style={[ <View style={[
styles.favThumb, styles.favThumb,
{ backgroundColor: item.background } // 动态同步 Home 页的背景 item.themeMode === 'scenery' ? {} : { backgroundColor: item.background }
]}> ]}>
<Text style={styles.favThumbText} numberOfLines={4}>{item.text}</Text> {item.themeMode === 'scenery' ? (
<View style={StyleSheet.absoluteFill}>
<Image
source={NATURE_IMAGES[parseInt(item.background)]}
style={{
width: width * 0.6,
height: 800, // 假设原图较高,设置一个较大的高度
position: 'absolute',
bottom: 0, // 关键:将图片底部对齐容器底部
}}
resizeMode="cover"
/>
</View>
) : null}
<Text style={[
styles.favThumbText,
item.themeMode === 'scenery' && { color: '#FFFFFF', textShadowColor: 'rgba(0,0,0,0.5)', textShadowOffset: {width:0, height:1}, textShadowRadius: 3 }
]} numberOfLines={4}>
{item.text}
</Text>
<Pressable <Pressable
onPress={() => handleRemove(item.id)} onPress={() => handleRemove(item.favId)}
style={styles.favRemoveBtn} style={styles.favRemoveBtn}
hitSlop={10} hitSlop={10}
> >
@@ -765,6 +807,7 @@ const styles = StyleSheet.create({
position: 'relative', position: 'relative',
borderWidth: 1, borderWidth: 1,
borderColor: 'rgba(119, 47, 0, 0.05)', borderColor: 'rgba(119, 47, 0, 0.05)',
overflow: 'hidden',
}, },
favThumbText: { favThumbText: {
fontSize: 15, fontSize: 15,

View File

@@ -1,54 +1,28 @@
import WidgetKit import WidgetKit
import SwiftUI import SwiftUI
// V2 + Small/Medium/Large + Home // V1Small/Medium/Large + Home
struct EmotionProvider: TimelineProvider { struct EmotionProvider: TimelineProvider {
private let quotes = [
"你已经很努力了,今天也值得被温柔对待。",
"轻轻呼吸,感受当下的每一刻。",
"所有的压力,都会在深呼吸中慢慢消散。",
"给生活一点留白,给自己一点温柔。",
"不要走得太快,等一等落下的灵魂。",
"世界虽嘈杂,但你可以拥有一颗宁静的心。",
"每一个瞬间,都是生命最好的安排。",
"抱抱自己,辛苦了,亲爱的。",
"慢一点也没关系,只要你在前行。",
"今天,你对自己微笑了吗?",
"愿你历经山河,仍觉得人间值得。",
"心简单,世界就简单;心平顺,生活就平顺。",
"即使生活偶尔晦暗,你也要成为自己的光。",
"别让琐事挤走生活的快乐,别让压力消磨奋斗的激情。"
]
func placeholder(in context: Context) -> EmotionEntry { func placeholder(in context: Context) -> EmotionEntry {
EmotionEntry(date: Date(), text: quotes[0]) EmotionEntry(date: Date())
} }
func getSnapshot(in context: Context, completion: @escaping (EmotionEntry) -> ()) { func getSnapshot(in context: Context, completion: @escaping (EmotionEntry) -> ()) {
let entry = EmotionEntry(date: Date(), text: quotes.randomElement() ?? quotes[0]) completion(EmotionEntry(date: Date()))
completion(entry)
} }
func getTimeline(in context: Context, completion: @escaping (Timeline<EmotionEntry>) -> ()) { func getTimeline(in context: Context, completion: @escaping (Timeline<EmotionEntry>) -> ()) {
var entries: [EmotionEntry] = [] // V1
let currentDate = Date() let entry = EmotionEntry(date: Date())
let nextUpdate = Calendar.current.date(byAdding: .day, value: 7, to: Date())
// 24 6 4 ?? Date().addingTimeInterval(60 * 60 * 24 * 7)
for hourOffset in 0..<6 { completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset * 4, to: currentDate)!
let entry = EmotionEntry(date: entryDate, text: quotes.randomElement() ?? quotes[0])
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
} }
} }
struct EmotionEntry: TimelineEntry { struct EmotionEntry: TimelineEntry {
let date: Date let date: Date
let text: String
} }
struct EmotionWidgetView: View { struct EmotionWidgetView: View {
@@ -56,37 +30,160 @@ struct EmotionWidgetView: View {
@Environment(\.widgetFamily) var family @Environment(\.widgetFamily) var family
private let title = "正念" private let title = "正念"
private let text = "你已经很努力了,今天也值得被温柔对待。"
private let deepLink = URL(string: "client:///(app)/home") private let deepLink = URL(string: "client:///(app)/home")
// #F7D9BF
private let backgroundColor = Color(red: 247/255, green: 217/255, blue: 191/255)
//
private let textColor = Color(red: 74/255, green: 52/255, blue: 40/255)
var body: some View { var body: some View {
VStack(alignment: .center, spacing: 0) { switch family {
Spacer(minLength: 0) case .systemSmall:
smallView()
case .systemMedium:
mediumView()
case .systemLarge:
largeView()
default:
smallView()
}
}
Text(entry.text) // iOS 15
.font(.system(size: family == .systemSmall ? 17 : 20, weight: .medium)) private func cardBackground(colors: [Color]) -> some View {
.foregroundColor(textColor) ZStack {
.lineSpacing(6) LinearGradient(
.multilineTextAlignment(.center) colors: colors,
.minimumScaleFactor(0.7) startPoint: .topLeading,
.fixedSize(horizontal: false, vertical: true) endPoint: .bottomTrailing
)
//
RadialGradient(
gradient: Gradient(colors: [Color.white.opacity(0.16), Color.white.opacity(0.0)]),
center: .topTrailing,
startRadius: 10,
endRadius: 180
)
}
.overlay(
RoundedRectangle(cornerRadius: 18, style: .continuous)
.stroke(Color.white.opacity(0.14), lineWidth: 1)
)
.cornerRadius(18)
}
private func chip(_ text: String) -> some View {
Text(text)
.font(.system(size: 12, weight: .semibold))
.foregroundColor(Color.white.opacity(0.9))
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color.white.opacity(0.14))
.cornerRadius(999)
}
private func smallView() -> some View {
ZStack {
cardBackground(colors: [
Color(red: 0.06, green: 0.08, blue: 0.12),
Color(red: 0.13, green: 0.16, blue: 0.22),
])
VStack(alignment: .leading, spacing: 10) {
HStack {
chip(title)
Spacer(minLength: 0)
}
Text(text)
.font(.system(size: 15, weight: .semibold))
.foregroundColor(Color.white.opacity(0.92))
.lineSpacing(2)
.lineLimit(4)
Spacer(minLength: 0) Spacer(minLength: 0)
if family != .systemSmall { Text("点我回到 App")
Text("Hey Mama") .font(.system(size: 11, weight: .medium))
.font(.system(size: 10, weight: .semibold)) .foregroundColor(Color.white.opacity(0.65))
.foregroundColor(textColor.opacity(0.3)) }
.padding(.bottom, 4) .padding(14)
}
.widgetURL(deepLink)
}
private func mediumView() -> some View {
ZStack {
cardBackground(colors: [
Color(red: 0.06, green: 0.08, blue: 0.12),
Color(red: 0.09, green: 0.11, blue: 0.17),
])
HStack(alignment: .top, spacing: 14) {
VStack(alignment: .leading, spacing: 10) {
chip(title)
Text(text)
.font(.system(size: 17, weight: .semibold))
.foregroundColor(Color.white.opacity(0.92))
.lineSpacing(3)
.lineLimit(5)
Spacer(minLength: 0)
Text("轻轻呼吸,回到当下")
.font(.system(size: 12, weight: .medium))
.foregroundColor(Color.white.opacity(0.7))
}
//
VStack(alignment: .trailing, spacing: 8) {
Text(entry.date, style: .time)
.font(.system(size: 12, weight: .semibold))
.foregroundColor(Color.white.opacity(0.8))
Spacer(minLength: 0)
Text("今日")
.font(.system(size: 28, weight: .bold))
.foregroundColor(Color.white.opacity(0.12))
} }
} }
.padding(family == .systemSmall ? 16 : 24) .padding(16)
.frame(maxWidth: .infinity, maxHeight: .infinity) // }
.background(backgroundColor) // .widgetURL(deepLink)
}
private func largeView() -> some View {
ZStack {
cardBackground(colors: [
Color(red: 0.06, green: 0.08, blue: 0.12),
Color(red: 0.14, green: 0.18, blue: 0.28),
])
VStack(alignment: .leading, spacing: 14) {
HStack {
chip(title)
Spacer(minLength: 0)
Text(entry.date, style: .time)
.font(.system(size: 12, weight: .semibold))
.foregroundColor(Color.white.opacity(0.78))
}
Text(text)
.font(.system(size: 20, weight: .semibold))
.foregroundColor(Color.white.opacity(0.92))
.lineSpacing(4)
.lineLimit(8)
Spacer(minLength: 0)
HStack {
Text("点我回到 Home")
.font(.system(size: 12, weight: .medium))
.foregroundColor(Color.white.opacity(0.7))
Spacer(minLength: 0)
Text("🌿")
.font(.system(size: 18))
.opacity(0.9)
}
}
.padding(18)
}
.widgetURL(deepLink) .widgetURL(deepLink)
} }
} }
@@ -97,13 +194,7 @@ struct EmotionWidget: Widget {
var body: some WidgetConfiguration { var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: EmotionProvider()) { entry in StaticConfiguration(kind: kind, provider: EmotionProvider()) { entry in
if #available(iOS 17.0, *) {
EmotionWidgetView(entry: entry) EmotionWidgetView(entry: entry)
.containerBackground(Color(red: 247/255, green: 217/255, blue: 191/255), for: .widget)
} else {
EmotionWidgetView(entry: entry)
.background(Color(red: 247/255, green: 217/255, blue: 191/255))
}
} }
.configurationDisplayName("情绪小组件") .configurationDisplayName("情绪小组件")
.description("一段温柔提醒,陪你回到当下。") .description("一段温柔提醒,陪你回到当下。")

View File

@@ -70,6 +70,7 @@ export async function setReaction(contentId: string, reaction: Reaction): Promis
} }
export type FavoriteItem = { export type FavoriteItem = {
favId: string; // 唯一标识,支持重复点赞同一文案
id: string; id: string;
date: string; date: string;
themeMode: ThemeMode; themeMode: ThemeMode;
@@ -82,15 +83,15 @@ export async function getFavorites(): Promise<FavoriteItem[]> {
export async function addFavorite(item: FavoriteItem): Promise<void> { export async function addFavorite(item: FavoriteItem): Promise<void> {
const list = await getFavorites(); const list = await getFavorites();
if (list.some(i => i.id === item.id)) return; // 允许重复点赞,不再根据 id 去重
const newList = [item, ...list]; const newList = [item, ...list];
console.log('Adding to favorites, new list size:', newList.length); console.log('Adding to favorites, new list size:', newList.length);
await setJson(KEY_FAVORITES_ITEMS, newList); await setJson(KEY_FAVORITES_ITEMS, newList);
} }
export async function removeFavorite(contentId: string): Promise<void> { export async function removeFavorite(favId: string): Promise<void> {
const list = await getFavorites(); const list = await getFavorites();
const next = list.filter(item => item.id !== contentId); const next = list.filter(item => item.favId !== favId);
await setJson(KEY_FAVORITES_ITEMS, next); await setJson(KEY_FAVORITES_ITEMS, next);
} }