owner #11
7
app.json
@ -4,7 +4,7 @@
|
||||
"slug": "memowake",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "",
|
||||
"icon": "./assets/icons/png/app.png",
|
||||
"scheme": "memowake",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
@ -48,7 +48,7 @@
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "static",
|
||||
"favicon": ""
|
||||
"favicon": "./assets/icons/png/app.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
@ -96,8 +96,7 @@
|
||||
"router": {},
|
||||
"eas": {
|
||||
"projectId": "04721dd4-6b15-495a-b9ec-98187c613172"
|
||||
},
|
||||
"API_ENDPOINT": "http://192.168.31.115:18080/api"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import * as Notifications from 'expo-notifications';
|
||||
import { Tabs } from 'expo-router';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
interface PollingData {
|
||||
@ -17,6 +18,7 @@ interface PollingData {
|
||||
extra: any;
|
||||
}
|
||||
export default function TabLayout() {
|
||||
const { t } = useTranslation();
|
||||
const colorScheme = useColorScheme();
|
||||
const [pollingData, setPollingData] = useState<PollingData[]>([]);
|
||||
const pollingInterval = useRef<NodeJS.Timeout | number>(null);
|
||||
@ -280,6 +282,27 @@ export default function TabLayout() {
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
{/* 隐私协议 */}
|
||||
<Tabs.Screen
|
||||
name="privacy-policy"
|
||||
options={{
|
||||
title: 'privacy-policy',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Support Screen */}
|
||||
<Tabs.Screen
|
||||
name="support"
|
||||
options={{
|
||||
title: t('tabTitle', { ns: 'support' }),
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Debug Screen - only in development */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
|
||||
@ -18,6 +18,8 @@ export default function HomeScreen() {
|
||||
router.replace('/ask')
|
||||
}, false).then(() => {
|
||||
setIsLoading(false);
|
||||
}).catch(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@ -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<Chat[]>([]);
|
||||
// 刷新状态
|
||||
const [refreshing, setRefreshing] = React.useState(false);
|
||||
|
||||
// 获取历史消息
|
||||
const getHistoryList = async () => {
|
||||
await fetchApi<Chat[]>(`/chats`).then((res) => {
|
||||
setHistoryList(res)
|
||||
})
|
||||
try {
|
||||
setRefreshing(true);
|
||||
const res = await fetchApi<Chat[]>(`/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={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={getHistoryList}
|
||||
colors={['#FFB645']} // Android
|
||||
tintColor="#FFB645" // iOS
|
||||
/>
|
||||
}
|
||||
ItemSeparatorComponent={() => (
|
||||
<View style={styles.separator} />
|
||||
)}
|
||||
|
||||
@ -1,25 +1,21 @@
|
||||
import ComeinSvg from '@/assets/icons/svg/comein.svg';
|
||||
import ConversationsSvg from '@/assets/icons/svg/conversations.svg';
|
||||
import MoreArrowSvg from '@/assets/icons/svg/moreArrow.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() {
|
||||
@ -89,34 +85,15 @@ export default function OwnerPage() {
|
||||
|
||||
{/* 资源数据 */}
|
||||
<View style={styles.resourceContainer}>
|
||||
<View style={{ gap: 16, width: "80%" }}>
|
||||
<ResourceComponent title={t("generalSetting.usedStorage", { ns: "personal" })} data={{ all: userInfoDetails.total_bytes, used: countData.used_bytes }} icon={<UsedStorageSvg />} isFormatBytes={true} />
|
||||
<ResourceComponent title={t("generalSetting.remainingPoints", { ns: "personal" })} data={{ all: userInfoDetails.total_points, used: userInfoDetails.remain_points }} icon={<PointsSvg />} />
|
||||
</View>
|
||||
<View style={{ alignItems: 'flex-end', flex: 1 }}>
|
||||
<MoreArrowSvg />
|
||||
<View style={{ gap: 4 }}>
|
||||
<ThemedText style={styles.text}>{t("generalSetting.premium", { ns: "personal" })}</ThemedText>
|
||||
<ThemedText style={styles.secondText}>{t("generalSetting.unlock", { ns: "personal" })}</ThemedText>
|
||||
</View>
|
||||
<ComeinSvg width={24} height={24} />
|
||||
</View>
|
||||
{/* 数据统计 */}
|
||||
<CountComponent
|
||||
data={[{ title: t("generalSetting.totalVideo", { ns: "personal" }), number: countData?.counter?.total_count?.video_count || 0 }, { title: t("generalSetting.totalPhoto", { ns: "personal" }), number: countData?.counter?.total_count?.photo_count || 0 }, { title: t("generalSetting.live", { ns: "personal" }), number: countData?.counter?.total_count?.live_count || 0 }, { title: t("generalSetting.videoLength", { ns: "personal" }), number: formatDuration(countData?.counter?.total_count?.video_length || 0) }]}
|
||||
/>
|
||||
|
||||
{/* 分类 */}
|
||||
<View style={{ height: 145 }}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 16 }} >
|
||||
{countData?.counter?.category_count && Object.entries(countData?.counter?.category_count).map(([key, value], index) => {
|
||||
return (
|
||||
<CategoryComponent
|
||||
key={index}
|
||||
title={key}
|
||||
data={[{ title: 'Video', number: value.video_count }, { title: 'Photo', number: value.photo_count }, { title: 'Length', number: formatDuration(value.video_length || 0) }]}
|
||||
bgSvg={value.cover_url}
|
||||
style={{ aspectRatio: 1, flex: 1 }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ScrollView>
|
||||
<View style={{ marginHorizontal: -16, marginBottom: -16 }}>
|
||||
<CarouselComponent data={userInfoDetails?.material_counter} />
|
||||
</View>
|
||||
|
||||
{/* 作品数据 */}
|
||||
@ -152,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',
|
||||
|
||||
159
app/(tabs)/privacy-policy.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import { fetchApi } from "@/lib/server-api-util";
|
||||
import { Policy } from "@/types/personal-info";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
||||
import RenderHtml from 'react-native-render-html';
|
||||
|
||||
const PrivacyPolicy = () => {
|
||||
const [article, setArticle] = useState<Policy>({} as Policy);
|
||||
useEffect(() => {
|
||||
const loadArticle = async () => {
|
||||
fetchApi<Policy>(`/system-config/policy/privacy_policy`).then((res: any) => {
|
||||
setArticle(res)
|
||||
}).catch((error: any) => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
loadArticle();
|
||||
}, []);
|
||||
|
||||
if (!article) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.centeredView}>
|
||||
<View style={styles.modalView}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={{ opacity: 0 }}>Settings</Text>
|
||||
<Text style={styles.modalTitle}>{'Privacy Policy'}</Text>
|
||||
<TouchableOpacity style={{ opacity: 0 }}>
|
||||
<Text style={styles.closeButton}>×</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
|
||||
<RenderHtml
|
||||
source={{ html: article.content }}
|
||||
tagsStyles={{
|
||||
p: { fontSize: 16, lineHeight: 24 },
|
||||
strong: { fontWeight: 'bold' },
|
||||
em: { fontStyle: 'italic' },
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivacyPolicy;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
centeredView: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
modalView: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'white',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: '#4C320C',
|
||||
},
|
||||
closeButton: {
|
||||
fontSize: 28,
|
||||
color: '#4C320C',
|
||||
padding: 10,
|
||||
},
|
||||
modalContent: {
|
||||
flex: 1,
|
||||
},
|
||||
modalText: {
|
||||
fontSize: 16,
|
||||
color: '#4C320C',
|
||||
},
|
||||
premium: {
|
||||
backgroundColor: "#FAF9F6",
|
||||
padding: 16,
|
||||
borderRadius: 24,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
backgroundColor: '#FAF9F6',
|
||||
borderRadius: 24,
|
||||
paddingVertical: 8
|
||||
},
|
||||
item: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
itemText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#4C320C',
|
||||
},
|
||||
upgradeButton: {
|
||||
backgroundColor: '#E2793F',
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
upgradeButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: "600"
|
||||
},
|
||||
switchContainer: {
|
||||
width: 50,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 2,
|
||||
},
|
||||
switchOn: {
|
||||
backgroundColor: '#E2793F',
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
switchOff: {
|
||||
backgroundColor: '#E5E5E5',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
switchCircle: {
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 13,
|
||||
},
|
||||
switchCircleOn: {
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
switchCircleOff: {
|
||||
backgroundColor: '#A5A5A5',
|
||||
},
|
||||
});
|
||||
67
app/(tabs)/support.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Head from 'expo-router/head';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Linking, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
const SupportScreen = () => {
|
||||
const { t } = useTranslation('support');
|
||||
const handleWeChatSupport = () => {
|
||||
Linking.openURL('https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd');
|
||||
};
|
||||
|
||||
const handleEmailSupport = () => {
|
||||
Linking.openURL('mailto:memowake@fairclip.cn');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t('pageTitle')}</title>
|
||||
</Head>
|
||||
<LinearGradient
|
||||
colors={['#FFB645', '#E2793F']}
|
||||
className="flex-1 items-center justify-center p-6"
|
||||
>
|
||||
<View className="items-center mb-12">
|
||||
<Text className="text-white text-5xl font-extrabold tracking-tight">
|
||||
MemoWake
|
||||
</Text>
|
||||
<Text className="text-white/90 text-2xl mt-4 text-center max-w-xs">
|
||||
{t('title')}
|
||||
</Text>
|
||||
<Text className="text-white/90 text-lg mt-4 text-center max-w-xs">
|
||||
{t('description')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="w-full max-w-xs">
|
||||
<TouchableOpacity
|
||||
className="bg-white/90 rounded-xl px-6 py-4 flex-row items-center justify-center shadow-lg mb-5"
|
||||
onPress={handleWeChatSupport}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Ionicons name="chatbubbles-outline" size={24} color="black" />
|
||||
<Text className="text-black font-bold text-lg ml-3">
|
||||
{t('onlineSupport')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
className="bg-black/80 rounded-xl px-6 py-4 flex-row items-center justify-center shadow-lg"
|
||||
onPress={handleEmailSupport}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Ionicons name="mail-outline" size={24} color="white" />
|
||||
<Text className="text-white font-bold text-lg ml-3">
|
||||
{t('emailSupport')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportScreen;
|
||||
BIN
assets/icons/png/app.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
61
assets/icons/svg/app.svg
Normal file
@ -0,0 +1,61 @@
|
||||
<svg width="578" height="577" viewBox="0 0 578 577" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_215_188)">
|
||||
<g clip-path="url(#clip0_215_188)">
|
||||
<rect x="3" width="572.333" height="572.333" rx="111.784" fill="white"/>
|
||||
<rect x="3" width="572.333" height="572.333" fill="#AC7E35"/>
|
||||
<path d="M34.4206 192.01C32.8885 178.266 65.8095 177.448 82.4616 178.758L56.5555 209.322C48.4291 212.492 35.9527 205.754 34.4206 192.01Z" fill="#FFDBA3"/>
|
||||
<path d="M41.5631 191.094C39.1999 179.937 62.1246 182.378 73.8823 184.994L60.656 199.713C55.2763 201.489 43.9263 202.252 41.5631 191.094Z" fill="#AC7E35"/>
|
||||
<path d="M198.913 27.5173C185.168 25.9852 184.351 58.9062 185.661 75.5583L216.225 49.6522C219.395 41.5258 212.657 29.0493 198.913 27.5173Z" fill="#FFDBA3"/>
|
||||
<path d="M197.997 34.6598C186.84 32.2966 189.281 55.2212 191.897 66.979L206.616 53.7527C208.392 48.373 209.154 37.0229 197.997 34.6598Z" fill="#AC7E35"/>
|
||||
<path d="M30.7421 448.573C-38.1574 191.436 197.139 -43.8603 454.275 25.0392L629.664 72.0346C886.801 140.934 972.926 462.355 784.689 650.592L656.295 778.986C468.058 967.223 146.637 881.099 77.7375 623.962L30.7421 448.573Z" fill="#FFD18A"/>
|
||||
<rect x="217.479" y="240.655" width="13.6147" height="19.0606" rx="6.80735" transform="rotate(135 217.479 240.655)" fill="#4C320C"/>
|
||||
<rect x="252.138" y="205.996" width="13.6147" height="19.0606" rx="6.80735" transform="rotate(135 252.138 205.996)" fill="#4C320C"/>
|
||||
<path d="M192.499 462.813C162.296 299.481 305.191 156.586 468.523 186.789L654.544 221.189C842.135 255.878 913.874 486.75 778.979 621.646L627.356 773.269C492.46 908.164 261.588 836.425 226.899 648.835L192.499 462.813Z" fill="#FFF8DE"/>
|
||||
<g filter="url(#filter1_i_215_188)">
|
||||
<ellipse cx="447.564" cy="174.232" rx="223.281" ry="159.292" transform="rotate(-45 447.564 174.232)" fill="#FFF8DE"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_i_215_188)">
|
||||
<ellipse cx="178.97" cy="442.827" rx="221.92" ry="159.292" transform="rotate(-45 178.97 442.827)" fill="#FFF8DE"/>
|
||||
</g>
|
||||
<ellipse cx="256.948" cy="253.172" rx="16.3376" ry="12.2532" transform="rotate(135 256.948 253.172)" fill="#FFB8B9"/>
|
||||
<ellipse cx="38.9009" cy="15.5185" rx="38.9009" ry="15.5185" transform="matrix(0.934357 -0.356338 -0.356338 -0.934357 493.079 394.049)" fill="#FFD38D"/>
|
||||
<ellipse cx="358.82" cy="530.763" rx="38.9009" ry="15.5185" transform="rotate(110.875 358.82 530.763)" fill="#FFD38D"/>
|
||||
<path d="M264.909 264.467C264.366 262.443 266.219 260.59 268.243 261.132L272.799 262.353C274.824 262.896 275.502 265.426 274.02 266.909L270.685 270.244C269.203 271.726 266.672 271.048 266.129 269.023L264.909 264.467Z" fill="#4C320C"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_215_188" x="0.764322" y="0" width="576.805" height="576.805" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2.23568"/>
|
||||
<feGaussianBlur stdDeviation="1.11784"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_215_188"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_215_188" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_i_215_188" x="248.485" y="-19.7266" width="393.038" height="415.794" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-5.1201" dy="27.8761"/>
|
||||
<feGaussianBlur stdDeviation="22.4643"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713726 0 0 0 0 0.270588 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_215_188"/>
|
||||
</filter>
|
||||
<filter id="filter2_i_215_188" x="-14.2056" y="232.015" width="411.952" height="403.987" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="25.6005" dy="-17.6359"/>
|
||||
<feGaussianBlur stdDeviation="14.8767"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713974 0 0 0 0 0.272498 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_215_188"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_215_188">
|
||||
<rect x="3" width="572.333" height="572.333" rx="111.784" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
3
assets/icons/svg/comein.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="11" height="20" viewBox="0 0 11 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.0002 1L9.77832 9.77812L1.0002 18.5562" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 235 B |
3
assets/icons/svg/people.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="140" height="32" viewBox="0 0 140 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="140" height="32" rx="16" fill="#FFB645" fill-opacity="0.2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 179 B |
3
assets/icons/svg/send.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 296 B |
3
assets/icons/svg/star.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 0C8 0 8.4414 5.71995 10.1607 7.43927C11.88 9.1586 16 9.6 16 9.6C16 9.6 11.88 10.0414 10.1607 11.7607C8.4414 13.4801 8 16 8 16C8 16 7.5586 13.4801 5.83927 11.7607C4.11995 10.0414 0 9.6 0 9.6C0 9.6 4.11995 9.1586 5.83927 7.43927C7.5586 5.71995 8 0 8 0Z" fill="#FFB645"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 384 B |
45
assets/icons/svg/userinfoTotal.svg
Normal file
@ -0,0 +1,45 @@
|
||||
<svg width="303" height="100" viewBox="0 0 403 135" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_2610_1468" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="403" height="135">
|
||||
<rect width="403" height="135" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2610_1468)">
|
||||
<path d="M162.293 51.3874C165.003 47.9968 172.492 55.1223 175.897 59.1089L163.363 60.1427C160.856 59.0426 159.582 54.7779 162.293 51.3874Z" fill="#FFDBA3"/>
|
||||
<path d="M164.084 52.7696C166.036 49.7687 170.582 55.3988 172.611 58.589L166.409 58.9204C164.82 58.1206 162.132 55.7706 164.084 52.7696Z" fill="#AC7E35"/>
|
||||
<path d="M242.275 51.3874C239.565 47.9968 232.077 55.1223 228.671 59.1089L241.205 60.1427C243.712 59.0426 244.986 54.7779 242.275 51.3874Z" fill="#FFDBA3"/>
|
||||
<path d="M240.487 52.7696C238.535 49.7687 233.989 55.3988 231.96 58.589L238.162 58.9204C239.75 58.1206 242.439 55.7706 240.487 52.7696Z" fill="#AC7E35"/>
|
||||
<path d="M107.996 103.821C149.775 31.4558 254.225 31.4559 296.004 103.821L324.502 153.179C366.282 225.544 314.057 316 230.497 316H173.503C89.9431 316 37.7184 225.544 79.4981 153.179L107.996 103.821Z" fill="#FFD18A"/>
|
||||
<path d="M196.064 69.1265C196.176 67.9873 195.281 67 194.137 67C192.992 67 192.097 67.9873 192.209 69.1265L192.437 71.4418C192.523 72.3163 193.258 72.9829 194.137 72.9829C195.015 72.9829 195.751 72.3163 195.837 71.4418L196.064 69.1265Z" fill="#4C320C"/>
|
||||
<path d="M210.769 69.1265C210.881 67.9873 209.986 67 208.842 67C207.697 67 206.802 67.9873 206.914 69.1265L207.142 71.4418C207.228 72.3163 207.963 72.9829 208.842 72.9829C209.72 72.9829 210.456 72.3163 210.542 71.4418L210.769 69.1265Z" fill="#4C320C"/>
|
||||
<path d="M140.736 142.883C170.285 99.9274 233.716 99.9274 263.265 142.883L296.918 191.806C330.855 241.142 295.535 308.307 235.654 308.307H168.347C108.466 308.307 73.1464 241.142 107.083 191.806L140.736 142.883Z" fill="#FFF8DE"/>
|
||||
<g filter="url(#filter0_i_2610_1468)">
|
||||
<ellipse cx="261.402" cy="108.682" rx="70.0855" ry="50" fill="#FFF8DE"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_i_2610_1468)">
|
||||
<ellipse cx="142.17" cy="108.682" rx="69.6581" ry="50" fill="#FFF8DE"/>
|
||||
</g>
|
||||
<ellipse cx="201.572" cy="83.8953" rx="5.12821" ry="3.84615" transform="rotate(180 201.572 83.8953)" fill="#FFB8B9"/>
|
||||
<path d="M200.833 88.1688C201.162 87.599 201.984 87.599 202.313 88.1688L203.053 89.4508C203.382 90.0206 202.971 90.7329 202.313 90.7329H200.833C200.175 90.7329 199.764 90.0206 200.093 89.4508L200.833 88.1688Z" fill="#4C320C"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_2610_1468" x="183.624" y="58.6816" width="147.863" height="102.564" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-7.69231" dy="2.5641"/>
|
||||
<feGaussianBlur stdDeviation="7.05128"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713726 0 0 0 0 0.270588 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2610_1468"/>
|
||||
</filter>
|
||||
<filter id="filter1_i_2610_1468" x="72.5122" y="58.6816" width="147.863" height="100" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="8.54701"/>
|
||||
<feGaussianBlur stdDeviation="4.70086"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713974 0 0 0 0 0.272498 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2610_1468"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import VoiceSvg from '@/assets/icons/svg/vioce.svg';
|
||||
import SendSvg from '@/assets/icons/svg/send.svg';
|
||||
import React, { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Keyboard,
|
||||
@ -125,9 +125,12 @@ export default function SendMessage(props: Props) {
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.voiceButton}
|
||||
onPress={handleSubmit}
|
||||
className={`absolute right-0 top-1/2 -translate-y-1/2 `} // 使用绝对定位将按钮放在输入框内右侧
|
||||
>
|
||||
<VoiceSvg />
|
||||
<View style={{ transform: [{ rotate: '330deg' }] }}>
|
||||
<SendSvg color={'white'} width={24} height={24} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@ -156,6 +159,6 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: '#FF9500',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 8, // 添加一点右边距
|
||||
marginRight: 8, // 添加一点
|
||||
},
|
||||
});
|
||||
153
components/owner/carousel.tsx
Normal file
@ -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<CarouselData[]>([]);
|
||||
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 <View style={styles.container}>
|
||||
{Object?.entries(data)?.filter(([key]) => key !== 'cover_url')?.map((item, index) => (
|
||||
<View style={index === Object?.entries(data)?.length - 2 ? { ...styles.item, borderBottomWidth: 0 } : styles.item} key={index}>
|
||||
<ThemedText style={styles.title}>{item[0]}</ThemedText>
|
||||
<ThemedText style={styles.number}>{item[1]}</ThemedText>
|
||||
</View>
|
||||
))}
|
||||
<View style={styles.image}>
|
||||
|
||||
<UserinfoTotalSvg />
|
||||
<View style={{ position: 'absolute', bottom: -5, right: 0, left: 0, justifyContent: 'center', alignItems: 'center' }}><HandersSvg /></View>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (data) {
|
||||
dataHandle()
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Carousel
|
||||
width={width}
|
||||
height={width * 0.75}
|
||||
data={carouselDataValue || []}
|
||||
mode="parallax"
|
||||
defaultIndex={carouselDataValue?.findIndex((item) => 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 (
|
||||
<View key={index} style={style}>
|
||||
{item?.key === 'total_count' ? (
|
||||
totleItem(item.value)
|
||||
) : (
|
||||
<View style={{ flex: 1, width: width * 0.65 }}>
|
||||
{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,
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@ -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) => {
|
||||
<View style={[styles.container, style]}>
|
||||
<View style={styles.backgroundContainer}>
|
||||
<Image
|
||||
source={bgSvg !== "" && bgSvg !== null ? { uri: bgSvg } : require('@/assets/images/png/owner/animals.png')}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
resizeMode="cover"
|
||||
source={bgSvg !== "" && bgSvg !== null ? { uri: bgSvg } : require('@/assets/images/png/owner/people.png')}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
resizeMode: "cover"
|
||||
}}
|
||||
/>
|
||||
<View style={styles.overlay} />
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
<ThemedText style={styles.title}>{title}</ThemedText>
|
||||
{data.map((item, index) => (
|
||||
<View style={styles.item} key={index}>
|
||||
<ThemedText style={styles.itemTitle}>{item.title}</ThemedText>
|
||||
<ThemedText style={styles.itemNumber}>{item.number}</ThemedText>
|
||||
</View>
|
||||
))}
|
||||
<View style={styles.titleContent}>
|
||||
<ThemedText style={styles.title}>{title}</ThemedText>
|
||||
<PeopleSvg />
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@ -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,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -42,9 +42,10 @@ const PrivacyModal = (props: { modalVisible: boolean, setModalVisible: (visible:
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
loadArticle();
|
||||
}, []);
|
||||
if (type) {
|
||||
loadArticle();
|
||||
}
|
||||
}, [type]);
|
||||
|
||||
if (!article) {
|
||||
return (
|
||||
|
||||
@ -27,7 +27,8 @@ const Ranking = ({ data }: { data: TitleRankings[] }) => {
|
||||
renderItem={({ item }) => (
|
||||
<View style={styles.item}>
|
||||
<ThemedText style={styles.rank}>No.{item.ranking}</ThemedText>
|
||||
<ThemedText style={styles.title}>{item.region}{item.display_name}</ThemedText>
|
||||
<ThemedText style={[styles.title]}>{item.display_name}</ThemedText>
|
||||
<ThemedText style={styles.title}>{item.region}</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -73,7 +73,7 @@ const UserInfo = (props: UserInfoProps) => {
|
||||
useEffect(() => {
|
||||
if (modalVisible) {
|
||||
getLocation();
|
||||
if (Object.keys(currentLocation).length === 0) {
|
||||
if (currentLocation && Object?.keys(currentLocation)?.length === 0) {
|
||||
getCurrentLocation();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<View className='flex flex-row justify-between items-center mt-[1rem] gap-[1rem] w-full'>
|
||||
{/* 头像 */}
|
||||
<View className='w-auto'>
|
||||
{userInfo?.user_info?.avatar_file_url && !imageError ? (
|
||||
<Image
|
||||
source={{ uri: userInfo.user_info.avatar_file_url }}
|
||||
style={{ width: 70, height: 70, borderRadius: 40 }}
|
||||
onError={() => {
|
||||
setImageError(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<UserSvg width={70} height={70} />
|
||||
)}
|
||||
</View>
|
||||
{/* 用户名 */}
|
||||
<View className='flex flex-col gap-4 w-[75%]'>
|
||||
<View className='flex flex-col w-[75%] gap-1'>
|
||||
<View className='flex flex-row items-center justify-between w-full'>
|
||||
<View className='flex flex-row items-center gap-2 w-full'>
|
||||
<ThemedText
|
||||
@ -39,38 +55,18 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
||||
))
|
||||
}
|
||||
</ScrollView>
|
||||
<View className='flex flex-row items-center gap-2 border border-bgPrimary px-2 py-1 rounded-full'>
|
||||
<StarSvg />
|
||||
<ThemedText style={{ color: 'bgPrimary', fontSize: 14, fontWeight: '700' }}>{userInfo?.remain_points}</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View>
|
||||
<ScrollView
|
||||
className='max-w-[85%]'
|
||||
horizontal // 水平滚动
|
||||
showsHorizontalScrollIndicator={false} // 隐藏滚动条
|
||||
contentContainerStyle={{
|
||||
flexDirection: 'row',
|
||||
gap: 8 // 间距
|
||||
}}
|
||||
>
|
||||
<ThemedText style={{ color: '#AC7E35', fontSize: 12, fontWeight: '600' }}>User ID: {userInfo?.user_info?.user_id}</ThemedText>
|
||||
</ScrollView>
|
||||
{/* <CopyButton textToCopy={userInfo?.user_info?.user_id || ""} /> */}
|
||||
<View style={{ display: "flex", flexDirection: "row", gap: 2, alignItems: "center" }}>
|
||||
<ThemedText style={{ color: '#AC7E35', fontSize: 12, fontWeight: '600' }}>User ID:{userInfo?.user_info?.user_id}</ThemedText>
|
||||
<CopyButton textToCopy={userInfo?.user_info?.user_id || ""} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
{/* 头像 */}
|
||||
<View className='w-auto'>
|
||||
{userInfo?.user_info?.avatar_file_url && !imageError ? (
|
||||
<Image
|
||||
source={{ uri: userInfo.user_info.avatar_file_url }}
|
||||
style={{ width: 80, height: 80, borderRadius: 40 }}
|
||||
onError={() => {
|
||||
setImageError(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<UserSvg width={80} height={80} />
|
||||
)}
|
||||
</View>
|
||||
</View >
|
||||
);
|
||||
}
|
||||
|
||||
@ -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..."
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
8
i18n/locales/en/support.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"title": "Support & Help",
|
||||
"description": "If you encounter any issues or have any suggestions, please contact us through the following methods.",
|
||||
"onlineSupport": "Online Support",
|
||||
"emailSupport": "Email Support",
|
||||
"pageTitle": "Support & Help - MemoWake",
|
||||
"tabTitle": "Support"
|
||||
}
|
||||
@ -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": "加载中..."
|
||||
}
|
||||
}
|
||||
@ -83,6 +83,8 @@
|
||||
"videoLength": "视频时长",
|
||||
"storiesCreated": "创作视频",
|
||||
"conversationsWithMemo": "Memo对话",
|
||||
"setting": "设置"
|
||||
"setting": "设置",
|
||||
"premium": "升级至会员",
|
||||
"unlock": "解锁更多记忆魔法"
|
||||
}
|
||||
}
|
||||
8
i18n/locales/zh/support.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"title": "支持与帮助",
|
||||
"description": "如果您在使用中遇到任何问题,或有任何建议,请通过以下方式联系我们。",
|
||||
"onlineSupport": "在线客服",
|
||||
"emailSupport": "邮件联系",
|
||||
"pageTitle": "支持与帮助 - MemoWake",
|
||||
"tabTitle": "支持"
|
||||
}
|
||||
@ -4,6 +4,7 @@ import enAdmin from './locales/en/admin.json';
|
||||
import enAsk from './locales/en/ask.json';
|
||||
import enCommon from './locales/en/common.json';
|
||||
import enDownload from './locales/en/download.json';
|
||||
import enSupport from './locales/en/support.json';
|
||||
import enExample from './locales/en/example.json';
|
||||
import enFairclip from './locales/en/fairclip.json';
|
||||
import enLanding from './locales/en/landing.json';
|
||||
@ -14,6 +15,7 @@ import zhAdmin from './locales/zh/admin.json';
|
||||
import zhAsk from './locales/zh/ask.json';
|
||||
import zhCommon from './locales/zh/common.json';
|
||||
import zhDownload from './locales/zh/download.json';
|
||||
import zhSupport from './locales/zh/support.json';
|
||||
import zhExample from './locales/zh/example.json';
|
||||
import zhFairclip from './locales/zh/fairclip.json';
|
||||
import zhLanding from './locales/zh/landing.json';
|
||||
@ -27,6 +29,7 @@ const translations = {
|
||||
ask: enAsk,
|
||||
common: enCommon,
|
||||
download: enDownload,
|
||||
support: enSupport,
|
||||
example: enExample,
|
||||
fairclip: enFairclip,
|
||||
landing: enLanding,
|
||||
@ -39,6 +42,7 @@ const translations = {
|
||||
ask: zhAsk,
|
||||
common: zhCommon,
|
||||
download: zhDownload,
|
||||
support: zhSupport,
|
||||
example: zhExample,
|
||||
fairclip: zhFairclip,
|
||||
landing: zhLanding,
|
||||
|
||||
23
lib/database/database-factory.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { DatabaseInterface } from './types';
|
||||
import { SQLiteDatabase } from './sqlite-database';
|
||||
|
||||
class DatabaseFactory {
|
||||
private static instance: DatabaseInterface | null = null;
|
||||
|
||||
static getInstance(): DatabaseInterface {
|
||||
if (!this.instance) {
|
||||
// Metro 会根据平台自动选择正确的文件
|
||||
// Web: sqlite-database.web.ts
|
||||
// Native: sqlite-database.ts
|
||||
this.instance = new SQLiteDatabase();
|
||||
}
|
||||
return this.instance!;
|
||||
}
|
||||
|
||||
// 用于测试或重置实例
|
||||
static resetInstance(): void {
|
||||
this.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const database = DatabaseFactory.getInstance();
|
||||
60
lib/database/database-test.ts
Normal file
@ -0,0 +1,60 @@
|
||||
// 数据库架构测试文件
|
||||
import { database } from './database-factory';
|
||||
import { UploadTask } from './types';
|
||||
|
||||
// 测试数据库基本功能
|
||||
export async function testDatabase() {
|
||||
console.log('开始测试数据库功能...');
|
||||
|
||||
try {
|
||||
// 初始化数据库
|
||||
await database.initUploadTable();
|
||||
console.log('✓ 数据库初始化成功');
|
||||
|
||||
// 测试插入任务
|
||||
const testTask: Omit<UploadTask, 'created_at'> = {
|
||||
uri: 'test://example.jpg',
|
||||
filename: 'example.jpg',
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
file_id: undefined
|
||||
};
|
||||
|
||||
await database.insertUploadTask(testTask);
|
||||
console.log('✓ 任务插入成功');
|
||||
|
||||
// 测试查询任务
|
||||
const retrievedTask = await database.getUploadTaskStatus(testTask.uri);
|
||||
if (retrievedTask) {
|
||||
console.log('✓ 任务查询成功:', retrievedTask);
|
||||
} else {
|
||||
console.log('✗ 任务查询失败');
|
||||
}
|
||||
|
||||
// 测试更新任务状态
|
||||
await database.updateUploadTaskStatus(testTask.uri, 'success', 'file123');
|
||||
console.log('✓ 任务状态更新成功');
|
||||
|
||||
// 测试获取所有任务
|
||||
const allTasks = await database.getUploadTasks();
|
||||
console.log('✓ 获取所有任务成功,数量:', allTasks.length);
|
||||
|
||||
// 测试应用状态
|
||||
await database.setAppState('test_key', 'test_value');
|
||||
const stateValue = await database.getAppState('test_key');
|
||||
console.log('✓ 应用状态测试成功:', stateValue);
|
||||
|
||||
// 清理测试数据
|
||||
await database.cleanUpUploadTasks();
|
||||
console.log('✓ 数据清理成功');
|
||||
|
||||
console.log('🎉 所有数据库测试通过!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库测试失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出测试函数供调用
|
||||
export default testDatabase;
|
||||
9
lib/database/empty-sqlite.js
Normal file
@ -0,0 +1,9 @@
|
||||
// 空的 SQLite 模块,用于 Web 环境
|
||||
console.warn('SQLite is not available in web environment');
|
||||
|
||||
// 导出空的对象,避免导入错误
|
||||
module.exports = {
|
||||
openDatabaseSync: () => {
|
||||
throw new Error('SQLite is not available in web environment');
|
||||
}
|
||||
};
|
||||
6
lib/database/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// 数据库模块统一导出
|
||||
export { DatabaseInterface, UploadTask } from './types';
|
||||
export { WebDatabase } from './web-database';
|
||||
export { SQLiteDatabase } from './sqlite-database';
|
||||
export { database } from './database-factory';
|
||||
export { testDatabase } from './database-test';
|
||||
156
lib/database/sqlite-database.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { DatabaseInterface, UploadTask } from './types';
|
||||
|
||||
export class SQLiteDatabase implements DatabaseInterface {
|
||||
private db: any;
|
||||
|
||||
constructor() {
|
||||
// 动态导入,避免在Web环境下加载
|
||||
try {
|
||||
const SQLite = require('expo-sqlite');
|
||||
this.db = SQLite.openDatabaseSync('upload_status.db');
|
||||
this.db.execSync('PRAGMA busy_timeout = 5000;');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize SQLite:', error);
|
||||
throw new Error('SQLite is not available in this environment');
|
||||
}
|
||||
}
|
||||
|
||||
async initUploadTable(): Promise<void> {
|
||||
console.log('Initializing upload tasks table...');
|
||||
await this.db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS upload_tasks (
|
||||
uri TEXT PRIMARY KEY NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
progress INTEGER NOT NULL DEFAULT 0,
|
||||
file_id TEXT,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
`);
|
||||
|
||||
// Add created_at column to existing table if it doesn't exist
|
||||
const columns = await this.db.getAllAsync('PRAGMA table_info(upload_tasks);');
|
||||
const columnExists = columns.some((column: any) => column.name === 'created_at');
|
||||
|
||||
if (!columnExists) {
|
||||
console.log('Adding created_at column to upload_tasks table...');
|
||||
await this.db.execAsync(`ALTER TABLE upload_tasks ADD COLUMN created_at INTEGER;`);
|
||||
await this.db.execAsync(`UPDATE upload_tasks SET created_at = (strftime('%s', 'now')) WHERE created_at IS NULL;`);
|
||||
console.log('created_at column added and populated.');
|
||||
}
|
||||
console.log('Upload tasks table initialized');
|
||||
|
||||
await this.db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS app_state (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT
|
||||
);
|
||||
`);
|
||||
console.log('App state table initialized');
|
||||
}
|
||||
|
||||
async insertUploadTask(task: Omit<UploadTask, 'created_at'>): Promise<void> {
|
||||
console.log('Inserting upload task:', task.uri);
|
||||
await this.db.runAsync(
|
||||
'INSERT OR REPLACE INTO upload_tasks (uri, filename, status, progress, file_id, created_at) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[task.uri, task.filename, task.status, task.progress, task.file_id ?? null, Math.floor(Date.now() / 1000)]
|
||||
);
|
||||
}
|
||||
|
||||
async getUploadTaskStatus(uri: string): Promise<UploadTask | null> {
|
||||
console.log('Checking upload task status for:', uri);
|
||||
const result = await this.db.getFirstAsync(
|
||||
'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks WHERE uri = ?;',
|
||||
uri
|
||||
);
|
||||
return result || null;
|
||||
}
|
||||
|
||||
async updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise<void> {
|
||||
if (file_id) {
|
||||
await this.db.runAsync('UPDATE upload_tasks SET status = ?, file_id = ? WHERE uri = ?', [status, file_id, uri]);
|
||||
} else {
|
||||
await this.db.runAsync('UPDATE upload_tasks SET status = ? WHERE uri = ?', [status, uri]);
|
||||
}
|
||||
}
|
||||
|
||||
async updateUploadTaskProgress(uri: string, progress: number): Promise<void> {
|
||||
await this.db.runAsync('UPDATE upload_tasks SET progress = ? WHERE uri = ?', [progress, uri]);
|
||||
}
|
||||
|
||||
async getUploadTasks(): Promise<UploadTask[]> {
|
||||
console.log('Fetching all upload tasks... time:', new Date().toLocaleString());
|
||||
const results = await this.db.getAllAsync(
|
||||
'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks ORDER BY created_at DESC;'
|
||||
);
|
||||
return results;
|
||||
}
|
||||
|
||||
async cleanUpUploadTasks(): Promise<void> {
|
||||
console.log('Cleaning up completed/failed upload tasks...');
|
||||
await this.db.runAsync(
|
||||
"DELETE FROM upload_tasks WHERE status = 'success' OR status = 'failed' OR status = 'skipped';"
|
||||
);
|
||||
}
|
||||
|
||||
async getUploadTasksSince(timestamp: number): Promise<UploadTask[]> {
|
||||
const rows = await this.db.getAllAsync(
|
||||
'SELECT * FROM upload_tasks WHERE created_at >= ? ORDER BY created_at DESC',
|
||||
[timestamp]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async exist_pending_tasks(): Promise<boolean> {
|
||||
const rows = await this.db.getAllAsync(
|
||||
'SELECT * FROM upload_tasks WHERE status = "pending" OR status = "uploading"'
|
||||
);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
async filterExistingFiles(fileUris: string[]): Promise<string[]> {
|
||||
if (fileUris.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const placeholders = fileUris.map(() => '?').join(',');
|
||||
const query = `SELECT uri FROM upload_tasks WHERE uri IN (${placeholders}) AND status = 'success'`;
|
||||
|
||||
const existingFiles = await this.db.getAllAsync(query, fileUris);
|
||||
const existingUris = new Set(existingFiles.map((f: { uri: string }) => f.uri));
|
||||
const newFileUris = fileUris.filter(uri => !existingUris.has(uri));
|
||||
|
||||
console.log(`[DB] Total files: ${fileUris.length}, Existing successful files: ${existingUris.size}, New files to upload: ${newFileUris.length}`);
|
||||
|
||||
return newFileUris;
|
||||
}
|
||||
|
||||
async setAppState(key: string, value: string | null): Promise<void> {
|
||||
console.log(`Setting app state: ${key} = ${value}`);
|
||||
await this.db.runAsync('INSERT OR REPLACE INTO app_state (key, value) VALUES (?, ?)', [key, value]);
|
||||
}
|
||||
|
||||
async getAppState(key: string): Promise<string | null> {
|
||||
const result = await this.db.getFirstAsync('SELECT value FROM app_state WHERE key = ?;', key);
|
||||
return result?.value ?? null;
|
||||
}
|
||||
|
||||
async executeSql(sql: string, params: any[] = []): Promise<any> {
|
||||
try {
|
||||
const isSelect = sql.trim().toLowerCase().startsWith('select');
|
||||
if (isSelect) {
|
||||
const results = this.db.getAllSync(sql, params);
|
||||
return results;
|
||||
} else {
|
||||
const result = this.db.runSync(sql, params);
|
||||
return {
|
||||
changes: result.changes,
|
||||
lastInsertRowId: result.lastInsertRowId,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error executing SQL:", error);
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
140
lib/database/sqlite-database.web.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { DatabaseInterface, UploadTask } from './types';
|
||||
|
||||
// Web 环境下的 SQLite 数据库实现(实际使用 localStorage)
|
||||
export class SQLiteDatabase implements DatabaseInterface {
|
||||
private getStorageKey(table: string): string {
|
||||
return `memowake_${table}`;
|
||||
}
|
||||
|
||||
private getUploadTasksFromStorage(): UploadTask[] {
|
||||
const data = localStorage.getItem(this.getStorageKey('upload_tasks'));
|
||||
return data ? JSON.parse(data) : [];
|
||||
}
|
||||
|
||||
private saveUploadTasks(tasks: UploadTask[]): void {
|
||||
localStorage.setItem(this.getStorageKey('upload_tasks'), JSON.stringify(tasks));
|
||||
}
|
||||
|
||||
private getAppStateData(): Record<string, string> {
|
||||
const data = localStorage.getItem(this.getStorageKey('app_state'));
|
||||
return data ? JSON.parse(data) : {};
|
||||
}
|
||||
|
||||
private saveAppStateData(state: Record<string, string>): void {
|
||||
localStorage.setItem(this.getStorageKey('app_state'), JSON.stringify(state));
|
||||
}
|
||||
|
||||
async initUploadTable(): Promise<void> {
|
||||
console.log('Initializing web storage tables (SQLite fallback)...');
|
||||
// Web端不需要初始化表结构,localStorage会自动处理
|
||||
}
|
||||
|
||||
async insertUploadTask(task: Omit<UploadTask, 'created_at'>): Promise<void> {
|
||||
console.log('Inserting upload task:', task.uri);
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
const existingIndex = tasks.findIndex(t => t.uri === task.uri);
|
||||
const newTask: UploadTask = {
|
||||
...task,
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
tasks[existingIndex] = newTask;
|
||||
} else {
|
||||
tasks.push(newTask);
|
||||
}
|
||||
|
||||
this.saveUploadTasks(tasks);
|
||||
}
|
||||
|
||||
async getUploadTaskStatus(uri: string): Promise<UploadTask | null> {
|
||||
console.log('Checking upload task status for:', uri);
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
return tasks.find(t => t.uri === uri) || null;
|
||||
}
|
||||
|
||||
async updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise<void> {
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
const taskIndex = tasks.findIndex(t => t.uri === uri);
|
||||
if (taskIndex >= 0) {
|
||||
tasks[taskIndex].status = status;
|
||||
if (file_id) {
|
||||
tasks[taskIndex].file_id = file_id;
|
||||
}
|
||||
this.saveUploadTasks(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
async updateUploadTaskProgress(uri: string, progress: number): Promise<void> {
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
const taskIndex = tasks.findIndex(t => t.uri === uri);
|
||||
if (taskIndex >= 0) {
|
||||
tasks[taskIndex].progress = progress;
|
||||
this.saveUploadTasks(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
async getUploadTasks(): Promise<UploadTask[]> {
|
||||
console.log('Fetching all upload tasks... time:', new Date().toLocaleString());
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
return tasks.sort((a, b) => b.created_at - a.created_at);
|
||||
}
|
||||
|
||||
async cleanUpUploadTasks(): Promise<void> {
|
||||
console.log('Cleaning up completed/failed upload tasks...');
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
const filteredTasks = tasks.filter(t =>
|
||||
t.status !== 'success' && t.status !== 'failed' && t.status !== 'skipped'
|
||||
);
|
||||
this.saveUploadTasks(filteredTasks);
|
||||
}
|
||||
|
||||
async getUploadTasksSince(timestamp: number): Promise<UploadTask[]> {
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
const filteredTasks = tasks.filter(t => t.created_at >= timestamp);
|
||||
return filteredTasks.sort((a, b) => b.created_at - a.created_at);
|
||||
}
|
||||
|
||||
async exist_pending_tasks(): Promise<boolean> {
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
return tasks.some(t => t.status === 'pending' || t.status === 'uploading');
|
||||
}
|
||||
|
||||
async filterExistingFiles(fileUris: string[]): Promise<string[]> {
|
||||
if (fileUris.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
const successfulUris = new Set(
|
||||
tasks.filter(t => t.status === 'success').map(t => t.uri)
|
||||
);
|
||||
|
||||
const newFileUris = fileUris.filter(uri => !successfulUris.has(uri));
|
||||
|
||||
console.log(`[WebDB] Total files: ${fileUris.length}, Existing successful files: ${successfulUris.size}, New files to upload: ${newFileUris.length}`);
|
||||
|
||||
return newFileUris;
|
||||
}
|
||||
|
||||
async setAppState(key: string, value: string | null): Promise<void> {
|
||||
console.log(`Setting app state: ${key} = ${value}`);
|
||||
const state = this.getAppStateData();
|
||||
if (value === null) {
|
||||
delete state[key];
|
||||
} else {
|
||||
state[key] = value;
|
||||
}
|
||||
this.saveAppStateData(state);
|
||||
}
|
||||
|
||||
async getAppState(key: string): Promise<string | null> {
|
||||
const state = this.getAppStateData();
|
||||
return state[key] || null;
|
||||
}
|
||||
|
||||
async executeSql(sql: string, params: any[] = []): Promise<any> {
|
||||
console.warn('SQL execution not supported in web environment:', sql);
|
||||
return { error: 'SQL execution not supported in web environment' };
|
||||
}
|
||||
}
|
||||
24
lib/database/types.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export type UploadTask = {
|
||||
uri: string;
|
||||
filename: string;
|
||||
status: 'pending' | 'uploading' | 'success' | 'failed' | 'skipped';
|
||||
progress: number; // 0-100
|
||||
file_id?: string; // 后端返回的文件ID
|
||||
created_at: number; // unix timestamp
|
||||
};
|
||||
|
||||
export interface DatabaseInterface {
|
||||
initUploadTable(): Promise<void>;
|
||||
insertUploadTask(task: Omit<UploadTask, 'created_at'>): Promise<void>;
|
||||
getUploadTaskStatus(uri: string): Promise<UploadTask | null>;
|
||||
updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise<void>;
|
||||
updateUploadTaskProgress(uri: string, progress: number): Promise<void>;
|
||||
getUploadTasks(): Promise<UploadTask[]>;
|
||||
cleanUpUploadTasks(): Promise<void>;
|
||||
getUploadTasksSince(timestamp: number): Promise<UploadTask[]>;
|
||||
exist_pending_tasks(): Promise<boolean>;
|
||||
filterExistingFiles(fileUris: string[]): Promise<string[]>;
|
||||
setAppState(key: string, value: string | null): Promise<void>;
|
||||
getAppState(key: string): Promise<string | null>;
|
||||
executeSql(sql: string, params?: any[]): Promise<any>;
|
||||
}
|
||||
139
lib/database/web-database.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { DatabaseInterface, UploadTask } from './types';
|
||||
|
||||
export class WebDatabase implements DatabaseInterface {
|
||||
private getStorageKey(table: string): string {
|
||||
return `memowake_${table}`;
|
||||
}
|
||||
|
||||
private getUploadTasksFromStorage(): UploadTask[] {
|
||||
const data = localStorage.getItem(this.getStorageKey('upload_tasks'));
|
||||
return data ? JSON.parse(data) : [];
|
||||
}
|
||||
|
||||
private saveUploadTasks(tasks: UploadTask[]): void {
|
||||
localStorage.setItem(this.getStorageKey('upload_tasks'), JSON.stringify(tasks));
|
||||
}
|
||||
|
||||
private getAppStateData(): Record<string, string> {
|
||||
const data = localStorage.getItem(this.getStorageKey('app_state'));
|
||||
return data ? JSON.parse(data) : {};
|
||||
}
|
||||
|
||||
private saveAppStateData(state: Record<string, string>): void {
|
||||
localStorage.setItem(this.getStorageKey('app_state'), JSON.stringify(state));
|
||||
}
|
||||
|
||||
async initUploadTable(): Promise<void> {
|
||||
console.log('Initializing web storage tables...');
|
||||
// Web端不需要初始化表结构,localStorage会自动处理
|
||||
}
|
||||
|
||||
async insertUploadTask(task: Omit<UploadTask, 'created_at'>): Promise<void> {
|
||||
console.log('Inserting upload task:', task.uri);
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
const existingIndex = tasks.findIndex(t => t.uri === task.uri);
|
||||
const newTask: UploadTask = {
|
||||
...task,
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
tasks[existingIndex] = newTask;
|
||||
} else {
|
||||
tasks.push(newTask);
|
||||
}
|
||||
|
||||
this.saveUploadTasks(tasks);
|
||||
}
|
||||
|
||||
async getUploadTaskStatus(uri: string): Promise<UploadTask | null> {
|
||||
console.log('Checking upload task status for:', uri);
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
return tasks.find(t => t.uri === uri) || null;
|
||||
}
|
||||
|
||||
async updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string): Promise<void> {
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
const taskIndex = tasks.findIndex(t => t.uri === uri);
|
||||
if (taskIndex >= 0) {
|
||||
tasks[taskIndex].status = status;
|
||||
if (file_id) {
|
||||
tasks[taskIndex].file_id = file_id;
|
||||
}
|
||||
this.saveUploadTasks(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
async updateUploadTaskProgress(uri: string, progress: number): Promise<void> {
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
const taskIndex = tasks.findIndex(t => t.uri === uri);
|
||||
if (taskIndex >= 0) {
|
||||
tasks[taskIndex].progress = progress;
|
||||
this.saveUploadTasks(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
async getUploadTasks(): Promise<UploadTask[]> {
|
||||
console.log('Fetching all upload tasks... time:', new Date().toLocaleString());
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
return tasks.sort((a, b) => b.created_at - a.created_at);
|
||||
}
|
||||
|
||||
async cleanUpUploadTasks(): Promise<void> {
|
||||
console.log('Cleaning up completed/failed upload tasks...');
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
const filteredTasks = tasks.filter(t =>
|
||||
t.status !== 'success' && t.status !== 'failed' && t.status !== 'skipped'
|
||||
);
|
||||
this.saveUploadTasks(filteredTasks);
|
||||
}
|
||||
|
||||
async getUploadTasksSince(timestamp: number): Promise<UploadTask[]> {
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
const filteredTasks = tasks.filter(t => t.created_at >= timestamp);
|
||||
return filteredTasks.sort((a, b) => b.created_at - a.created_at);
|
||||
}
|
||||
|
||||
async exist_pending_tasks(): Promise<boolean> {
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
return tasks.some(t => t.status === 'pending' || t.status === 'uploading');
|
||||
}
|
||||
|
||||
async filterExistingFiles(fileUris: string[]): Promise<string[]> {
|
||||
if (fileUris.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tasks = this.getUploadTasksFromStorage();
|
||||
const successfulUris = new Set(
|
||||
tasks.filter(t => t.status === 'success').map(t => t.uri)
|
||||
);
|
||||
|
||||
const newFileUris = fileUris.filter(uri => !successfulUris.has(uri));
|
||||
|
||||
console.log(`[WebDB] Total files: ${fileUris.length}, Existing successful files: ${successfulUris.size}, New files to upload: ${newFileUris.length}`);
|
||||
|
||||
return newFileUris;
|
||||
}
|
||||
|
||||
async setAppState(key: string, value: string | null): Promise<void> {
|
||||
console.log(`Setting app state: ${key} = ${value}`);
|
||||
const state = this.getAppStateData();
|
||||
if (value === null) {
|
||||
delete state[key];
|
||||
} else {
|
||||
state[key] = value;
|
||||
}
|
||||
this.saveAppStateData(state);
|
||||
}
|
||||
|
||||
async getAppState(key: string): Promise<string | null> {
|
||||
const state = this.getAppStateData();
|
||||
return state[key] || null;
|
||||
}
|
||||
|
||||
async executeSql(sql: string, params: any[] = []): Promise<any> {
|
||||
console.warn('SQL execution not supported in web environment:', sql);
|
||||
return { error: 'SQL execution not supported in web environment' };
|
||||
}
|
||||
}
|
||||
195
lib/db.ts
@ -1,176 +1,23 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
// 使用数据库接口架构,支持 Web 和移动端
|
||||
import { database } from './database/database-factory';
|
||||
import { UploadTask } from './database/types';
|
||||
|
||||
const db = SQLite.openDatabaseSync('upload_status.db');
|
||||
|
||||
// Set a busy timeout to handle concurrent writes and avoid "database is locked" errors.
|
||||
// This will make SQLite wait for 5 seconds if the database is locked by another process.
|
||||
db.execSync('PRAGMA busy_timeout = 5000;');
|
||||
|
||||
export type UploadTask = {
|
||||
uri: string;
|
||||
filename: string;
|
||||
status: 'pending' | 'uploading' | 'success' | 'failed' | 'skipped';
|
||||
progress: number; // 0-100
|
||||
file_id?: string; // 后端返回的文件ID
|
||||
created_at: number; // unix timestamp
|
||||
};
|
||||
|
||||
// 初始化表
|
||||
export async function initUploadTable() {
|
||||
console.log('Initializing upload tasks table...');
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS upload_tasks (
|
||||
uri TEXT PRIMARY KEY NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
progress INTEGER NOT NULL DEFAULT 0,
|
||||
file_id TEXT,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
`);
|
||||
|
||||
// Add created_at column to existing table if it doesn't exist
|
||||
const columns = await db.getAllAsync('PRAGMA table_info(upload_tasks);');
|
||||
const columnExists = columns.some((column: any) => column.name === 'created_at');
|
||||
|
||||
if (!columnExists) {
|
||||
console.log('Adding created_at column to upload_tasks table...');
|
||||
// SQLite doesn't support non-constant DEFAULT values on ALTER TABLE.
|
||||
// So we add the column, then update existing rows.
|
||||
await db.execAsync(`ALTER TABLE upload_tasks ADD COLUMN created_at INTEGER;`);
|
||||
await db.execAsync(`UPDATE upload_tasks SET created_at = (strftime('%s', 'now')) WHERE created_at IS NULL;`);
|
||||
console.log('created_at column added and populated.');
|
||||
}
|
||||
console.log('Upload tasks table initialized');
|
||||
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS app_state (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT
|
||||
);
|
||||
`);
|
||||
console.log('App state table initialized');
|
||||
}
|
||||
|
||||
// 插入新的上传任务
|
||||
export async function insertUploadTask(task: Omit<UploadTask, 'created_at'>) {
|
||||
console.log('Inserting upload task:', task.uri);
|
||||
await db.runAsync(
|
||||
'INSERT OR REPLACE INTO upload_tasks (uri, filename, status, progress, file_id, created_at) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[task.uri, task.filename, task.status, task.progress, task.file_id ?? null, Math.floor(Date.now() / 1000)]
|
||||
);
|
||||
}
|
||||
|
||||
// 检查文件是否已上传或正在上传
|
||||
export async function getUploadTaskStatus(uri: string): Promise<UploadTask | null> {
|
||||
console.log('Checking upload task status for:', uri);
|
||||
const result = await db.getFirstAsync<UploadTask>(
|
||||
'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks WHERE uri = ?;',
|
||||
uri
|
||||
);
|
||||
return result || null;
|
||||
}
|
||||
|
||||
// 更新上传任务的状态
|
||||
export async function updateUploadTaskStatus(uri: string, status: UploadTask['status'], file_id?: string) {
|
||||
if (file_id) {
|
||||
await db.runAsync('UPDATE upload_tasks SET status = ?, file_id = ? WHERE uri = ?', [status, file_id, uri]);
|
||||
} else {
|
||||
await db.runAsync('UPDATE upload_tasks SET status = ? WHERE uri = ?', [status, uri]);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新上传任务的进度
|
||||
export async function updateUploadTaskProgress(uri: string, progress: number) {
|
||||
await db.runAsync('UPDATE upload_tasks SET progress = ? WHERE uri = ?', [progress, uri]);
|
||||
}
|
||||
|
||||
// 获取所有上传任务
|
||||
export async function getUploadTasks(): Promise<UploadTask[]> {
|
||||
console.log('Fetching all upload tasks... time:', new Date().toLocaleString());
|
||||
const results = await db.getAllAsync<UploadTask>(
|
||||
'SELECT uri, filename, status, progress, file_id, created_at FROM upload_tasks ORDER BY created_at DESC;'
|
||||
);
|
||||
return results;
|
||||
}
|
||||
|
||||
// 清理已完成或失败的任务 (可选,根据需求添加)
|
||||
export async function cleanUpUploadTasks(): Promise<void> {
|
||||
console.log('Cleaning up completed/failed upload tasks...');
|
||||
await db.runAsync(
|
||||
"DELETE FROM upload_tasks WHERE status = 'success' OR status = 'failed' OR status = 'skipped';"
|
||||
);
|
||||
}
|
||||
|
||||
// 获取某个时间点之后的所有上传任务
|
||||
export async function getUploadTasksSince(timestamp: number): Promise<UploadTask[]> {
|
||||
const rows = await db.getAllAsync<UploadTask>(
|
||||
'SELECT * FROM upload_tasks WHERE created_at >= ? ORDER BY created_at DESC',
|
||||
[timestamp]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function exist_pending_tasks(): Promise<boolean> {
|
||||
const rows = await db.getAllAsync<UploadTask>(
|
||||
'SELECT * FROM upload_tasks WHERE status = "pending" OR status = "uploading"'
|
||||
);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
// 检查一组文件URI,返回那些在数据库中不存在或是未成功上传的文件的URI
|
||||
export async function filterExistingFiles(fileUris: string[]): Promise<string[]> {
|
||||
if (fileUris.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 创建占位符字符串 '?,?,?'
|
||||
const placeholders = fileUris.map(() => '?').join(',');
|
||||
|
||||
// 查询已经存在且状态为 'success' 的任务
|
||||
const query = `SELECT uri FROM upload_tasks WHERE uri IN (${placeholders}) AND status = 'success'`;
|
||||
|
||||
const existingFiles = await db.getAllAsync<{ uri: string }>(query, fileUris);
|
||||
const existingUris = new Set(existingFiles.map(f => f.uri));
|
||||
|
||||
// 过滤出新文件
|
||||
const newFileUris = fileUris.filter(uri => !existingUris.has(uri));
|
||||
|
||||
console.log(`[DB] Total files: ${fileUris.length}, Existing successful files: ${existingUris.size}, New files to upload: ${newFileUris.length}`);
|
||||
|
||||
return newFileUris;
|
||||
}
|
||||
|
||||
// 设置全局状态值
|
||||
export async function setAppState(key: string, value: string | null): Promise<void> {
|
||||
console.log(`Setting app state: ${key} = ${value}`);
|
||||
await db.runAsync('INSERT OR REPLACE INTO app_state (key, value) VALUES (?, ?)', [key, value]);
|
||||
}
|
||||
|
||||
// 获取全局状态值
|
||||
export async function getAppState(key: string): Promise<string | null> {
|
||||
const result = await db.getFirstAsync<{ value: string }>('SELECT value FROM app_state WHERE key = ?;', key);
|
||||
return result?.value ?? null;
|
||||
}
|
||||
|
||||
// for debug page
|
||||
export async function executeSql(sql: string, params: any[] = []): Promise<any> {
|
||||
try {
|
||||
// Trim and check if it's a SELECT query
|
||||
const isSelect = sql.trim().toLowerCase().startsWith('select');
|
||||
if (isSelect) {
|
||||
const results = db.getAllSync(sql, params);
|
||||
return results;
|
||||
} else {
|
||||
const result = db.runSync(sql, params);
|
||||
return {
|
||||
changes: result.changes,
|
||||
lastInsertRowId: result.lastInsertRowId,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error executing SQL:", error);
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
// 重新导出类型
|
||||
export type { UploadTask };
|
||||
|
||||
// 重新导出所有数据库函数,使用统一接口
|
||||
export const initUploadTable = () => database.initUploadTable();
|
||||
export const insertUploadTask = (task: Omit<UploadTask, 'created_at'>) => database.insertUploadTask(task);
|
||||
export const getUploadTaskStatus = (uri: string) => database.getUploadTaskStatus(uri);
|
||||
export const updateUploadTaskStatus = (uri: string, status: UploadTask['status'], file_id?: string) =>
|
||||
database.updateUploadTaskStatus(uri, status, file_id);
|
||||
export const updateUploadTaskProgress = (uri: string, progress: number) =>
|
||||
database.updateUploadTaskProgress(uri, progress);
|
||||
export const getUploadTasks = () => database.getUploadTasks();
|
||||
export const cleanUpUploadTasks = () => database.cleanUpUploadTasks();
|
||||
export const getUploadTasksSince = (timestamp: number) => database.getUploadTasksSince(timestamp);
|
||||
export const exist_pending_tasks = () => database.exist_pending_tasks();
|
||||
export const filterExistingFiles = (fileUris: string[]) => database.filterExistingFiles(fileUris);
|
||||
export const setAppState = (key: string, value: string | null) => database.setAppState(key, value);
|
||||
export const getAppState = (key: string) => database.getAppState(key);
|
||||
export const executeSql = (sql: string, params: any[] = []) => database.executeSql(sql, params);
|
||||
|
||||
@ -19,6 +19,15 @@ config.resolver = {
|
||||
...config.resolver?.alias,
|
||||
'@/': path.resolve(__dirname, './'),
|
||||
},
|
||||
platforms: ['ios', 'android', 'native', 'web'],
|
||||
};
|
||||
|
||||
// Web 环境下的模块别名
|
||||
if (process.env.EXPO_PLATFORM === 'web') {
|
||||
config.resolver.alias = {
|
||||
...config.resolver.alias,
|
||||
'expo-sqlite': path.resolve(__dirname, './lib/database/empty-sqlite.js'),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = withNativeWind(config, { input: './global.css' });
|
||||
13
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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: {
|
||||
|
||||