diff --git a/app/(tabs)/memo-list.tsx b/app/(tabs)/memo-list.tsx index 9ab35d2..4b23114 100644 --- a/app/(tabs)/memo-list.tsx +++ b/app/(tabs)/memo-list.tsx @@ -3,26 +3,33 @@ import UploaderProgress from "@/components/file-upload/upload-progress/uploader- import AskNavbar from "@/components/layout/ask"; import { useUploadManager } from "@/hooks/useUploadManager"; import { fetchApi } from "@/lib/server-api-util"; -import { useAppDispatch, useAppSelector } from "@/store"; +import { useAppSelector } from "@/store"; import { Chat } from "@/types/ask"; -import { router } from "expo-router"; -import React, { useEffect, useState } from 'react'; -import { FlatList, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { router, useFocusEffect } from "expo-router"; +import React from 'react'; +import { FlatList, Platform, RefreshControl, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; const MemoList = () => { const insets = useSafeAreaInsets(); - const dispatch = useAppDispatch(); const uploadSessionStartTime = useAppSelector((state) => state.appState.uploadSessionStartTime); // 历史消息 const [historyList, setHistoryList] = React.useState([]); + // 刷新状态 + const [refreshing, setRefreshing] = React.useState(false); // 获取历史消息 const getHistoryList = async () => { - await fetchApi(`/chats`).then((res) => { - setHistoryList(res) - }) + try { + setRefreshing(true); + const res = await fetchApi(`/chats`); + setHistoryList(res); + } catch (error) { + console.error('Failed to fetch history:', error); + } finally { + setRefreshing(false); + } } // 获取对话历史消息 @@ -41,9 +48,12 @@ const MemoList = () => { getChatHistory(item.session_id) } - useEffect(() => { - getHistoryList() - }, []) + useFocusEffect( + React.useCallback(() => { + getHistoryList() + }, []) + ); + const { progressInfo, uploadSessionStartTime: uploadSessionStartTimeFromHook } = useUploadManager(); @@ -87,6 +97,15 @@ const MemoList = () => { ListHeaderComponent={renderHeader} data={historyList} keyExtractor={(item) => item.session_id} + // 下拉刷新 + refreshControl={ + + } ItemSeparatorComponent={() => ( )} diff --git a/app/(tabs)/owner.tsx b/app/(tabs)/owner.tsx index 8b3d2e3..fbe8411 100644 --- a/app/(tabs)/owner.tsx +++ b/app/(tabs)/owner.tsx @@ -1,24 +1,21 @@ +import ComeinSvg from '@/assets/icons/svg/comein.svg'; import ConversationsSvg from '@/assets/icons/svg/conversations.svg'; -import PointsSvg from '@/assets/icons/svg/points.svg'; import StoriesSvg from '@/assets/icons/svg/stories.svg'; -import UsedStorageSvg from '@/assets/icons/svg/usedStorage.svg'; import AskNavbar from '@/components/layout/ask'; import AlbumComponent from '@/components/owner/album'; -import CategoryComponent from '@/components/owner/category'; -import CountComponent from '@/components/owner/count'; +import CarouselComponent from '@/components/owner/carousel'; import CreateCountComponent from '@/components/owner/createCount'; import Ranking from '@/components/owner/ranking'; -import ResourceComponent from '@/components/owner/resource'; import SettingModal from '@/components/owner/setting'; import UserInfo from '@/components/owner/userName'; -import { formatDuration } from '@/components/utils/time'; +import { ThemedText } from '@/components/ThemedText'; import { checkAuthStatus } from '@/lib/auth'; import { fetchApi } from '@/lib/server-api-util'; import { CountData, UserInfoDetails } from '@/types/user'; import { useRouter } from 'expo-router'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { FlatList, ScrollView, StyleSheet, View } from 'react-native'; +import { FlatList, StyleSheet, View } from 'react-native'; import { useSafeAreaInsets } from "react-native-safe-area-context"; export default function OwnerPage() { @@ -88,31 +85,15 @@ export default function OwnerPage() { {/* 资源数据 */} - - } isFormatBytes={true} /> - } /> + + {t("generalSetting.premium", { ns: "personal" })} + {t("generalSetting.unlock", { ns: "personal" })} + - {/* 数据统计 */} - - {/* 分类 */} - - - {countData?.counter?.category_count && Object.entries(countData?.counter?.category_count).map(([key, value], index) => { - return ( - - ) - })} - + + {/* 作品数据 */} @@ -148,11 +129,20 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - gap: 16, - backgroundColor: "#FAF9F6", - padding: 16, + backgroundColor: "#4C320C", + paddingVertical: 8, + paddingHorizontal: 32, borderRadius: 18, - paddingTop: 20 + }, + text: { + fontSize: 12, + fontWeight: '700', + color: '#FFB645', + }, + secondText: { + fontSize: 16, + fontWeight: '700', + color: '#fff', }, userInfo: { flexDirection: 'row', diff --git a/assets/icons/svg/comein.svg b/assets/icons/svg/comein.svg new file mode 100644 index 0000000..a38cc64 --- /dev/null +++ b/assets/icons/svg/comein.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/svg/people.svg b/assets/icons/svg/people.svg new file mode 100644 index 0000000..db2a291 --- /dev/null +++ b/assets/icons/svg/people.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/svg/star.svg b/assets/icons/svg/star.svg new file mode 100644 index 0000000..af626a2 --- /dev/null +++ b/assets/icons/svg/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/svg/userinfoTotal.svg b/assets/icons/svg/userinfoTotal.svg new file mode 100644 index 0000000..104d1c7 --- /dev/null +++ b/assets/icons/svg/userinfoTotal.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/owner/album.tsx b/components/owner/album.tsx index d546198..855faf9 100644 --- a/components/owner/album.tsx +++ b/components/owner/album.tsx @@ -11,10 +11,10 @@ const AlbumComponent = ({ setModalVisible, style }: CategoryProps) => { const { t } = useTranslation(); return ( - + {t('generalSetting.album', { ns: 'personal' })} - + {t('generalSetting.shareProfile', { ns: 'personal' })} setModalVisible(true)} style={[styles.text, { flex: 1, alignItems: "center", paddingVertical: 6 }]}> diff --git a/components/owner/carousel.tsx b/components/owner/carousel.tsx new file mode 100644 index 0000000..b83be3d --- /dev/null +++ b/components/owner/carousel.tsx @@ -0,0 +1,153 @@ +import HandersSvg from "@/assets/icons/svg/handers.svg"; +import UserinfoTotalSvg from "@/assets/icons/svg/userinfoTotal.svg"; +import { Counter, UserCountData } from "@/types/user"; +import * as React from "react"; +import { Dimensions, StyleSheet, View, ViewStyle } from "react-native"; +import Carousel from "react-native-reanimated-carousel"; +import { ThemedText } from "../ThemedText"; +import { formatDuration } from "../utils/time"; +import CategoryComponent from "./category"; +interface Props { + data: Counter +} + +interface CarouselData { + key: string, + value: UserCountData + +}[] +const width = Dimensions.get("window").width; + +function CarouselComponent(props: Props) { + const { data } = props; + const [carouselDataValue, setCarouselDataValue] = React.useState([]); + const dataHandle = () => { + const carouselData = { ...data?.category_count, total_count: data?.total_count } + // 1. 转换为数组并过滤掉 'total' + const entries = Object?.entries(carouselData) + ?.filter(([key]) => key !== 'total_count') + ?.map(([key, value]) => ({ key, value })); + + // 2. 找到 total 数据 + const totalEntry = { + key: 'total_count', + value: carouselData?.total_count + }; + + // 3. 插入到中间位置 + const middleIndex = Math.floor((entries || [])?.length / 2); + entries?.splice(middleIndex, 0, totalEntry); + setCarouselDataValue(entries) + return entries; + } + + const totleItem = (data: UserCountData) => { + return + {Object?.entries(data)?.filter(([key]) => key !== 'cover_url')?.map((item, index) => ( + + {item[0]} + {item[1]} + + ))} + + + + + + + } + + React.useEffect(() => { + if (data) { + dataHandle() + } + }, [data]); + + return ( + + item?.key === 'total_count') - 1 || 0} + modeConfig={{ + parallaxScrollingScale: 1, + parallaxScrollingOffset: 160, + parallaxAdjacentItemScale: 0.7 + }} + renderItem={({ item, index }) => { + const style: ViewStyle = { + width: width, + height: width * 0.8, + alignItems: "center", + }; + return ( + + {item?.key === 'total_count' ? ( + totleItem(item.value) + ) : ( + + {CategoryComponent({ + title: item?.key, + data: [ + { title: 'Video', number: item?.value?.video_count }, + { title: 'Photo', number: item?.value?.photo_count }, + { title: 'Length', number: formatDuration(item?.value?.video_length || 0) } + ], + bgSvg: item?.value?.cover_url, + })} + + )} + + ) + }} + /> + + ); +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: "#FFB645", + paddingVertical: 8, + paddingHorizontal: 16, + borderRadius: 16, + display: "flex", + flexDirection: "column", + position: 'relative', + width: width * 0.6, + height: '85%' + }, + image: { + position: 'absolute', + bottom: 0, + right: 0, + left: 0, + alignItems: 'center', + justifyContent: 'flex-end', + }, + item: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingVertical: 8, + borderBottomColor: '#fff', + borderBottomWidth: 1, + }, + title: { + color: "#4C320C", + fontWeight: "700", + fontSize: 14, + }, + number: { + color: "#fff", + fontWeight: "700", + fontSize: 32, + textAlign: 'right', + flex: 1, + paddingTop: 8 + } +}) + +export default CarouselComponent; \ No newline at end of file diff --git a/components/owner/category.tsx b/components/owner/category.tsx index 06a0a15..2f91fa2 100644 --- a/components/owner/category.tsx +++ b/components/owner/category.tsx @@ -1,3 +1,4 @@ +import PeopleSvg from "@/assets/icons/svg/people.svg"; import { Image, StyleProp, StyleSheet, View, ViewStyle } from "react-native"; import { ThemedText } from "../ThemedText"; @@ -13,20 +14,27 @@ const CategoryComponent = ({ title, data, bgSvg, style }: CategoryProps) => { - {title} {data.map((item, index) => ( {item.title} {item.number} ))} + + {title} + + + ); @@ -37,45 +45,57 @@ const styles = StyleSheet.create({ borderRadius: 32, overflow: 'hidden', position: 'relative', + aspectRatio: 1, }, backgroundContainer: { ...StyleSheet.absoluteFillObject, - width: '100%', - height: '100%', + width: "100%", + height: "100%", }, overlay: { ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0, 0, 0, 0.4)', // 0% 不透明度的黑色 + backgroundColor: 'rgba(0, 0, 0, 0.2)', // 0% 不透明度的黑色 }, content: { - paddingHorizontal: 16, - paddingVertical: 8, - justifyContent: 'space-between', + paddingHorizontal: 40, + paddingVertical: 24, + justifyContent: 'flex-end', flex: 1 }, - title: { + titleContent: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + position: 'relative', width: '100%', - textAlign: "center", + marginTop: 16 + }, + title: { color: 'white', fontSize: 16, fontWeight: '700', textShadowColor: 'rgba(0, 0, 0, 0.5)', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 2, + position: 'absolute', + textAlign: 'center', + width: '100%', + zIndex: 1, }, item: { flexDirection: 'row', alignItems: 'center', + paddingVertical: 8, justifyContent: 'space-between', }, itemTitle: { color: 'white', - fontSize: 10, + fontSize: 14, fontWeight: '700', }, itemNumber: { color: 'white', - fontSize: 10, + fontSize: 14, } }); diff --git a/components/owner/ranking.tsx b/components/owner/ranking.tsx index 00ac830..823d369 100644 --- a/components/owner/ranking.tsx +++ b/components/owner/ranking.tsx @@ -27,7 +27,8 @@ const Ranking = ({ data }: { data: TitleRankings[] }) => { renderItem={({ item }) => ( No.{item.ranking} - {item.region}{item.display_name} + {item.display_name} + {item.region} )} /> diff --git a/components/owner/userName.tsx b/components/owner/userName.tsx index c75e889..2225312 100644 --- a/components/owner/userName.tsx +++ b/components/owner/userName.tsx @@ -1,15 +1,31 @@ import UserSvg from '@/assets/icons/svg/ataver.svg'; +import StarSvg from '@/assets/icons/svg/star.svg'; import { ThemedText } from '@/components/ThemedText'; import { UserInfoDetails } from '@/types/user'; import { useState } from 'react'; import { Image, ScrollView, View } from 'react-native'; +import CopyButton from '../copy'; export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) { // 添加状态来跟踪图片加载状态 const [imageError, setImageError] = useState(false); return ( + {/* 头像 */} + + {userInfo?.user_info?.avatar_file_url && !imageError ? ( + { + setImageError(true); + }} + /> + ) : ( + + )} + {/* 用户名 */} - + + + + {userInfo?.remain_points} + - - - User ID: {userInfo?.user_info?.user_id} - - {/* */} + + User ID:{userInfo?.user_info?.user_id} + + - - {/* 头像 */} - - {userInfo?.user_info?.avatar_file_url && !imageError ? ( - { - setImageError(true); - }} - /> - ) : ( - - )} - ); } diff --git a/i18n/locales/en/common.json b/i18n/locales/en/common.json index 0037b84..2c80c49 100644 --- a/i18n/locales/en/common.json +++ b/i18n/locales/en/common.json @@ -11,18 +11,18 @@ "common": { "search": "Search...", "title": "MemoWake - Home Video Memory, Powered by AI", - "name":"MemoWake", - "homepage":"HomePage", - "signup":"Sign up", - "login":"Login", - "trade":"copyright 2025 MemoWake - All rights reserved", - "logout":"Logout", - "self":"Personal Center" + "name": "MemoWake", + "homepage": "HomePage", + "signup": "Sign up", + "login": "Login", + "trade": "沪ICP备2025133004号-2A", + "logout": "Logout", + "self": "Personal Center" }, "welcome": { - "welcome": "Welcome to MemoWake~", - "slogan": "Preserve your love, laughter and precious moments forever", - "notice": "s back to live family portrait " + "welcome": "Welcome to MemoWake~", + "slogan": "Preserve your love, laughter and precious moments forever", + "notice": "s back to live family portrait " }, "imagePreview": { "zoomOut": "Zoom out", @@ -62,7 +62,7 @@ "file": { "invalidType": "Invalid file type", "tooLarge": "File too large", - "tooSmall":"File too small" + "tooSmall": "File too small" }, "email": { "required": "Email is required", @@ -110,4 +110,4 @@ } }, "loading": "Loading..." -} +} \ No newline at end of file diff --git a/i18n/locales/en/personal.json b/i18n/locales/en/personal.json index 0fbadeb..e813114 100644 --- a/i18n/locales/en/personal.json +++ b/i18n/locales/en/personal.json @@ -83,6 +83,8 @@ "videoLength": "Video Duration", "storiesCreated": "Stories Created", "conversationsWithMemo": "Conversations with Memo", - "setting": "Settings" + "setting": "Settings", + "premium": "Upgrade to Premium", + "unlock": "Unlock more memory magic" } } \ No newline at end of file diff --git a/i18n/locales/zh/common.json b/i18n/locales/zh/common.json index c977501..be43b74 100644 --- a/i18n/locales/zh/common.json +++ b/i18n/locales/zh/common.json @@ -11,13 +11,13 @@ "common": { "search": "搜索...", "title": "MemoWake - AI驱动的家庭「视频记忆」", - "name":"MemoWake", - "homepage":"首页", - "signup":"注册", - "login":"登录", - "trade": "沪ICP备2023032876号-4", - "logout":"退出登录", - "self":"个人中心" + "name": "MemoWake", + "homepage": "首页", + "signup": "注册", + "login": "登录", + "trade": "沪ICP备2025133004号-2A", + "logout": "退出登录", + "self": "个人中心" }, "welcome": { "welcome": "欢迎来到 MemoWake~", @@ -55,7 +55,7 @@ "invalidFileTitle": "不支持的文件格式", "fileTooLargeTitle": "文件过大", "uploadErrorTitle": "上传失败", - "fileTooSmallTitle": "文件过小", + "fileTooSmallTitle": "文件过小", "fileTooSmall": "文件过小,请上传大于300像素的图片" }, "validation": { @@ -109,4 +109,4 @@ } }, "loading": "加载中..." -} +} \ No newline at end of file diff --git a/i18n/locales/zh/personal.json b/i18n/locales/zh/personal.json index 3f0c5f9..ce221a5 100644 --- a/i18n/locales/zh/personal.json +++ b/i18n/locales/zh/personal.json @@ -83,6 +83,8 @@ "videoLength": "视频时长", "storiesCreated": "创作视频", "conversationsWithMemo": "Memo对话", - "setting": "设置" + "setting": "设置", + "premium": "升级至会员", + "unlock": "解锁更多记忆魔法" } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2d3c8d1..66a15e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "react-native-picker-select": "^9.3.1", "react-native-progress": "^5.0.1", "react-native-reanimated": "~3.17.4", + "react-native-reanimated-carousel": "^4.0.2", "react-native-render-html": "^6.3.4", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", @@ -14826,6 +14827,18 @@ "react-native": "*" } }, + "node_modules/react-native-reanimated-carousel": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/react-native-reanimated-carousel/-/react-native-reanimated-carousel-4.0.2.tgz", + "integrity": "sha512-vNpCfPlFoOVKHd+oB7B0luoJswp+nyz0NdJD8+LCrf25JiNQXfM22RSJhLaksBHqk3fm8R4fKWPNcfy5w7wL1Q==", + "license": "MIT", + "peerDependencies": { + "react": ">=18.0.0", + "react-native": ">=0.70.3", + "react-native-gesture-handler": ">=2.9.0", + "react-native-reanimated": ">=3.0.0" + } + }, "node_modules/react-native-reanimated/node_modules/react-native-is-edge-to-edge": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz", diff --git a/package.json b/package.json index 50c1a34..bf8c777 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "expo-audio": "~0.4.8", "expo-background-task": "^0.2.8", "expo-blur": "~14.1.5", + "expo-clipboard": "~7.1.5", "expo-constants": "~17.1.6", "expo-dev-client": "~5.2.4", "expo-device": "~7.1.4", @@ -33,6 +34,7 @@ "expo-haptics": "~14.1.4", "expo-image-manipulator": "~13.1.7", "expo-image-picker": "~16.1.4", + "expo-linear-gradient": "~14.1.5", "expo-linking": "~7.1.7", "expo-localization": "^16.1.5", "expo-location": "~18.1.5", @@ -64,6 +66,7 @@ "react-native-picker-select": "^9.3.1", "react-native-progress": "^5.0.1", "react-native-reanimated": "~3.17.4", + "react-native-reanimated-carousel": "^4.0.2", "react-native-render-html": "^6.3.4", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", @@ -72,9 +75,7 @@ "react-native-uuid": "^2.0.3", "react-native-web": "~0.20.0", "react-native-webview": "13.13.5", - "react-redux": "^9.2.0", - "expo-clipboard": "~7.1.5", - "expo-linear-gradient": "~14.1.5" + "react-redux": "^9.2.0" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/types/user.ts b/types/user.ts index ff1665d..bb3a291 100644 --- a/types/user.ts +++ b/types/user.ts @@ -12,7 +12,7 @@ export interface User { avatar_file_url?: string } -interface UserCountData { +export interface UserCountData { video_count: number, photo_count: number, live_count: number, @@ -31,7 +31,7 @@ export interface CountData { } } -interface Counter { +export interface Counter { user_id: number, total_count: UserCountData, category_count: {