diff --git a/.gitignore b/.gitignore index f610ec0..ff09c93 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,7 @@ yarn-error.* *.tsbuildinfo app-example + +# Expo prebuild generated files +android/ +ios/ diff --git a/app.json b/app.json index 7e058a3..1945c17 100644 --- a/app.json +++ b/app.json @@ -4,7 +4,7 @@ "slug": "memowake", "version": "1.0.0", "orientation": "portrait", - "icon": "./assets/images/icon.png", + "icon": "", "scheme": "memowake", "userInterfaceStyle": "automatic", "newArchEnabled": true, @@ -13,7 +13,7 @@ }, "android": { "adaptiveIcon": { - "foregroundImage": "./assets/images/adaptive-icon.png", + "foregroundImage": "", "backgroundColor": "#ffffff" }, "edgeToEdgeEnabled": true, @@ -22,24 +22,26 @@ "web": { "bundler": "metro", "output": "static", - "favicon": "./assets/images/favicon.png" + "favicon": "" }, "plugins": [ "expo-router", + "expo-secure-store", [ - "expo-splash-screen", + "expo-font", { - "image": "./assets/images/splash-icon.png", - "imageWidth": 200, - "resizeMode": "contain", - "backgroundColor": "#ffffff" + "fonts": [ + "./assets/fonts/[font-file.ttf]" + ] } ], [ - "expo-secure-store", + "expo-splash-screen", { - "configureAndroidBackup": true, - "faceIDPermission": "Allow $(PRODUCT_NAME) to access your Face ID biometric data." + "image": "", + "imageWidth": 200, + "resizeMode": "contain", + "backgroundColor": "#ffffff" } ] ], diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 303acc9..ecf1e1a 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { Platform } from 'react-native'; import { HapticTab } from '@/components/HapticTab'; -import { IconSymbol } from '@/components/ui/IconSymbol'; import TabBarBackground from '@/components/ui/TabBarBackground'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; @@ -26,11 +25,58 @@ export default function TabLayout() { default: {}, }), }}> + {/* 落地页 */} , + tabBarButton: () => null, // 隐藏底部标签栏 + headerShown: false, // 隐藏导航栏 + tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 + }} + /> + {/* 登录 */} + null, // 隐藏底部标签栏 + headerShown: false, // 隐藏导航栏 + tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 + }} + /> + {/* 重置密码 */} + null, // 隐藏底部标签栏 + headerShown: false, // 隐藏导航栏 + tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 + }} + /> + {/* loading页面 */} + null, // 隐藏底部标签栏 + headerShown: false, // 隐藏导航栏 + tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 + }} + /> + {/* 用户信息收集 */} + null, // 隐藏底部标签栏 + headerShown: false, // 隐藏导航栏 + tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 }} /> diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index baff315..d6aca60 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,82 +1,112 @@ -import { ThemedText } from '@/components/ThemedText'; -import { ThemedView } from '@/components/ThemedView'; -import { useAuth } from '@/contexts/auth-context'; -import { fetchApi } from '@/lib/server-api-util'; -import { store } from '@/store'; -import { User } from '@/types/user'; -import { Ionicons } from '@expo/vector-icons'; -import { useTranslation } from 'react-i18next'; -import { TouchableOpacity, View } from 'react-native'; +import { useRouter } from 'expo-router'; +import { Text, TouchableOpacity, View } from 'react-native'; export default function HomeScreen() { - const { login } = useAuth(); - const token = store.getState().auth.token; - console.log(token); - const { t } = useTranslation(); - const handleApi = () => { - fetchApi('/iam/login/password-login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - account: "jinyaqiu@fairclip.cn", - password: "111111", - }), - }) - .then((data) => { - login(data as User, data.access_token || '') - }) - .catch((err) => console.log(err)); - // fetch('http://192.168.31.42/api/v1/iam/login/password-login', { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/json', - // }, - // body: JSON.stringify({ - // account: "jinyaqiu@fairclip.cn", - // password: "111111", - // }), - // }) - // .then((res) => res.json()) - // .then((data) => console.log(data)) - // .catch((err) => console.log(err)); - } + const router = useRouter(); + return ( - - - - {t('title', { ns: "example" })} - + + {/* 标题区域 */} + + + Awaken{"\n"}your{"\n"}precious memories + + + let every moment speak and feel alive + + - - - - - - MeMo - - - + {/* Memo 形象区域 */} + + {/* 气泡对话框 */} + + + + + Hi!{"\n"}I'm Memo + + + + + + + + + + + + + + + + + + + {/* Memo 形象 */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - Ready to wake up your memories? Just ask! Let MeMo bring them back to life! - - - - - - - - - - Explore More Memory Reel Made Just For You! - - + + {/* 介绍文本 */} + + I live deep inside your photo gallery, {"\n"}waiting for you to bring me back... + + + {/* 唤醒按钮 */} + router.push('/login')} + activeOpacity={0.8} + > + + Awake your Memo + + + ); -} +} \ No newline at end of file diff --git a/app/(tabs)/loading.tsx b/app/(tabs)/loading.tsx new file mode 100644 index 0000000..2e971d7 --- /dev/null +++ b/app/(tabs)/loading.tsx @@ -0,0 +1,55 @@ +import { View } from 'react-native'; + +export default function LoadingScreen() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Loading... + + + ); +} diff --git a/app/(tabs)/login.tsx b/app/(tabs)/login.tsx new file mode 100644 index 0000000..5468607 --- /dev/null +++ b/app/(tabs)/login.tsx @@ -0,0 +1,135 @@ +import LoginIP1 from '@/assets/icons/svg/loginIp1.svg'; +import LoginIP2 from '@/assets/icons/svg/loginIp2.svg'; +import ForgetPwd from '@/components/login/forgetPwd'; +import PhoneLogin from '@/components/login/phoneLogin'; +import SignUp from '@/components/login/signUp'; +import { ThemedText } from '@/components/ThemedText'; +import { ThemedView } from '@/components/ThemedView'; +import { useLocalSearchParams, useRouter } from 'expo-router'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { LayoutChangeEvent, TouchableOpacity, View, ViewStyle, useWindowDimensions } from 'react-native'; + +const LoginScreen = () => { + const router = useRouter(); + const { t } = useTranslation(); + const { status } = useLocalSearchParams(); + const [error, setError] = useState('123'); + const [containerHeight, setContainerHeight] = useState(0); + const { height: windowHeight } = useWindowDimensions(); + // 密码可视 + const [showPassword, setShowPassword] = useState(false); + const handleLayout = (event: LayoutChangeEvent) => { + const { height } = event.nativeEvent.layout; + setContainerHeight(height); + }; + + // 更新URL参数而不刷新页面 + const updateUrlParam = (key: string, value: string) => { + router.setParams({ [key]: value }); + } + + // 初始化 + useEffect(() => { + setError('123') + }, []) + + return ( + + + 0 ? windowHeight - containerHeight - 210 : 0, + transform: [{ translateX: -200 }] + }} + > + { + showPassword + ? + + : + + } + + 0 ? windowHeight - containerHeight - 1 : 0 + }} + > + + + + + + + + {/* 错误提示 */} + + + {error} + + + { + status === 'signUp' + ? + + : + status === 'forgetPwd' ? + + : + } + {status == 'login' && + + + {status === 'login' ? t('auth.agree.logintext', { ns: 'login' }) : t('auth.agree.singupText', { ns: 'login' })} + + { }}> + + {t('auth.agree.terms', { ns: 'login' })} + + + + {t('auth.agree.join', { ns: 'login' })} + + { }}> + + {t('auth.agree.privacyPolicy', { ns: 'login' })} + + + + } + + + ); +} + +export default LoginScreen \ No newline at end of file diff --git a/app/(tabs)/reset-password.tsx b/app/(tabs)/reset-password.tsx new file mode 100644 index 0000000..3995603 --- /dev/null +++ b/app/(tabs)/reset-password.tsx @@ -0,0 +1,162 @@ +import { ThemedText } from '@/components/ThemedText'; +import { ThemedView } from '@/components/ThemedView'; +import { useAuth } from '@/contexts/auth-context'; +import { fetchApi } from '@/lib/server-api-util'; +import { User } from '@/types/user'; +import { Ionicons } from '@expo/vector-icons'; +import { useLocalSearchParams, useRouter } from 'expo-router'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ActivityIndicator, KeyboardAvoidingView, Platform, ScrollView, TextInput, TouchableOpacity, View } from 'react-native'; + +const resetPassword = () => { + const { t } = useTranslation(); + const router = useRouter(); + const { session_id: resetPasswordSessionId, token } = useLocalSearchParams<{ session_id: string; token: string }>(); + // 使用 auth context 登录 + const { login } = useAuth(); + + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(''); + + const validatePassword = (pwd: string) => { + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; + return passwordRegex.test(pwd); + }; + + const handleSubmit = async () => { + if (!password) { + setError(t('auth.login.passwordPlaceholder', { ns: 'login' })); + return; + } + + if (password !== confirmPassword) { + setError(t('auth.signup.passwordNotMatch', { ns: 'login' })); + return; + } + + if (!validatePassword(password)) { + setError(t('auth.signup.passwordAuth', { ns: 'login' })); + return; + } + + setLoading(true); + setError(''); + + try { + const body = { + new_password: password, + reset_password_session_id: resetPasswordSessionId, + token + }; + + const response = await fetchApi('/iam/reset-password', { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json' + } + }); + + if (login) { + login(response, response.access_token || ''); + } + } catch (error) { + console.error('Reset password error:', error); + setError(t('auth.resetPwd.error', { ns: 'login' }) || 'Failed to reset password'); + } finally { + setLoading(false); + } + }; + + return ( + + + + + {t('auth.resetPwd.title', { ns: 'login' })} + + + {error ? ( + + {error} + + ) : null} + + + + + setShowPassword(!showPassword)} + className="p-2" + > + + + + + + + + setShowPassword(!showPassword)} + className="p-2" + > + + + + + + + {loading ? ( + + ) : ( + + {t('auth.resetPwd.resetButton', { ns: 'login' })} + + )} + + + + + ); +} + +export default resetPassword diff --git a/app/(tabs)/user-message.tsx b/app/(tabs)/user-message.tsx new file mode 100644 index 0000000..e370791 --- /dev/null +++ b/app/(tabs)/user-message.tsx @@ -0,0 +1,67 @@ +import { FileStatus } from '@/components/file-upload/file-uploader'; +import Choice from '@/components/user-message.tsx/choice'; +import Done from '@/components/user-message.tsx/done'; +import Look from '@/components/user-message.tsx/look'; +import UserName from '@/components/user-message.tsx/userName'; +import { fetchApi } from '@/lib/server-api-util'; +import { User } from '@/types/user'; +import { useEffect, useState } from 'react'; +import { View } from 'react-native'; +export type Steps = "userName" | "look" | "choice" | "done"; + +export default function UserMessage() { + // 步骤 + const [steps, setSteps] = useState("userName") + const [username, setUsername] = useState('') + const [avatar, setAvatar] = useState('') + const [fileData, setFileData] = useState([]) + const [isLoading, setIsLoading] = useState(false); + const [userInfo, setUserInfo] = useState(null); + // 获取用户信息 + const getUserInfo = async () => { + const res = await fetchApi("/iam/user-info"); + setUserInfo(res); + setUsername(res?.nickname || ''); + setAvatar(res?.avatar_file_url || ''); + } + + const handleUser = () => { + setIsLoading(true); + fetchApi("/iam/user/info", { + method: "POST", + body: JSON.stringify({ + username, + avatar_file_id: fileData?.[0]?.id + }) + }).then(() => { + setIsLoading(false); + getUserInfo(); + setSteps('done'); + }).catch(() => { + setIsLoading(false); + }); + }; + useEffect(() => { + getUserInfo(); + }, []); + + return ( + + { + steps == "userName" + ? + + : + steps == "look" + ? + + : + steps == "choice" + ? + + : + + } + + ); +} diff --git a/app/+not-found.tsx b/app/+not-found.tsx index 215b0ed..b8448c1 100644 --- a/app/+not-found.tsx +++ b/app/+not-found.tsx @@ -1,12 +1,12 @@ import { Link, Stack } from 'expo-router'; -import { StyleSheet } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { ThemedText } from '@/components/ThemedText'; import { ThemedView } from '@/components/ThemedView'; export default function NotFoundScreen() { return ( - <> + This screen does not exist. @@ -14,7 +14,7 @@ export default function NotFoundScreen() { Go to home screen! - + ); } diff --git a/app/_layout.tsx b/app/_layout.tsx index 30b9f88..d7ef45b 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -14,6 +14,13 @@ export default function RootLayout() { + diff --git a/assets/icons/svg/loginIp1.svg b/assets/icons/svg/loginIp1.svg new file mode 100644 index 0000000..3e834de --- /dev/null +++ b/assets/icons/svg/loginIp1.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/svg/loginIp2.svg b/assets/icons/svg/loginIp2.svg new file mode 100644 index 0000000..0a9dd34 --- /dev/null +++ b/assets/icons/svg/loginIp2.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/babel.config.js b/babel.config.js index 595f0ea..81fa6ba 100644 --- a/babel.config.js +++ b/babel.config.js @@ -5,5 +5,8 @@ module.exports = function (api) { ["babel-preset-expo", { jsxImportSource: "nativewind" }], "nativewind/babel", ], + plugins: [ + 'expo-router/babel', + ], }; }; \ No newline at end of file diff --git a/components/file-upload/file-item-phone.tsx b/components/file-upload/file-item-phone.tsx new file mode 100644 index 0000000..7304a25 --- /dev/null +++ b/components/file-upload/file-item-phone.tsx @@ -0,0 +1,117 @@ +import SvgIcon from "@/components/svg-icon"; +import React from 'react'; +import { useTranslation } from "react-i18next"; +import { Animated, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { FileStatus } from "./file-uploader"; + +interface FileItemProps { + fileStatus: FileStatus; + index: number; + onRemove: (file: File) => void; + formatFileSize: (bytes: number) => string; + disabled?: boolean; +} + +export default function FileItem({ + fileStatus, + index, + onRemove, + formatFileSize, + disabled = false +}: FileItemProps) { + const { t } = useTranslation(); + const fadeAnim = React.useRef(new Animated.Value(0)).current; + const translateY = React.useRef(new Animated.Value(10)).current; + + React.useEffect(() => { + Animated.parallel([ + Animated.timing(fadeAnim, { + toValue: 1, + duration: 200, + delay: index * 50, + useNativeDriver: true, + }), + Animated.timing(translateY, { + toValue: 0, + duration: 200, + delay: index * 50, + useNativeDriver: true, + }) + ]).start(); + }, []); + + return ( + + + + {fileStatus.file.name} + {formatFileSize(fileStatus.file.size)} + + {!disabled && ( + onRemove(fileStatus.file)} + style={styles.removeButton} + > + + + )} + + {fileStatus.progress !== undefined && fileStatus.progress < 100 && ( + + + + )} + + ); +} + +const styles = StyleSheet.create({ + card: { + backgroundColor: '#fff', + borderRadius: 8, + marginBottom: 8, + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 2, + }, + cardBody: { + padding: 12, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + fileInfo: { + flex: 1, + }, + fileName: { + fontSize: 14, + color: '#333', + marginBottom: 4, + }, + fileSize: { + fontSize: 12, + color: '#666', + }, + removeButton: { + padding: 4, + }, + progressContainer: { + height: 2, + backgroundColor: '#e9ecef', + width: '100%', + }, + progressBar: { + height: '100%', + backgroundColor: '#007bff', + }, +}); \ No newline at end of file diff --git a/components/file-upload/file-uploader.tsx b/components/file-upload/file-uploader.tsx new file mode 100644 index 0000000..3400402 --- /dev/null +++ b/components/file-upload/file-uploader.tsx @@ -0,0 +1,609 @@ +import { fetchApi } from "@/lib/server-api-util"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { v4 as uuidv4 } from 'uuid'; + +// 导入子组件 +import { ConfirmUpload } from "@/types/upload"; +import { View } from "react-native"; +import MultiFileUploader from "./multi-file-uploader"; +import SingleFileUploader from "./single-file-uploader"; +import UploadDropzone from "./upload-dropzone"; +import { extractVideoFirstFrame, getVideoDuration } from "./utils/videoUtils"; + +// 默认允许的文件类型 +export const DEFAULT_ALLOWED_FILE_TYPES = ["video/mp4", "video/quicktime", "video/x-msvideo", "video/x-matroska"]; +export const DEFAULT_MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB + +// 上传URL响应接口 +export interface UploadUrlResponse { + upload_url: string; + file_id: string; +} + +// 文件状态接口 +export interface FileStatus { + file: File; + id?: string; + progress: number; + error?: string; + status: 'pending' | 'uploading' | 'success' | 'error'; + url?: string; + thumbnailUrl?: string; // 添加缩略图URL +} + +interface FileUploaderProps { + onFilesUploaded?: (files: FileStatus[]) => void; + maxFiles?: number; + allowMultiple?: boolean; + disabled?: boolean; + className?: string; + allowedFileTypes?: string[]; // 外部传入的允许文件类型 + maxFileSize?: number; // 外部传入的最大文件大小 + thumbnailPropsUrl?: string; // 注册后返回ai处理展示缩略图 +} + +export default function FileUploader({ + onFilesUploaded, + maxFiles = 1, + allowMultiple = false, + disabled = false, + className = "", + allowedFileTypes = DEFAULT_ALLOWED_FILE_TYPES, + maxFileSize = DEFAULT_MAX_FILE_SIZE, + thumbnailPropsUrl = "" +}: FileUploaderProps) { + const { t } = useTranslation(); + const [files, setFiles] = useState([]); + const [isUploading, setIsUploading] = useState(false); + const fileInputRef = useRef(null); + // 图片的最小值 + const MIN_IMAGE_SIZE = 300; + + // 校验图片尺寸(异步) + function validateImageDimensions(file: File): Promise { + return new Promise((resolve) => { + const img = new window.Image(); + img.onload = () => { + if (img.width < MIN_IMAGE_SIZE || img.height < MIN_IMAGE_SIZE) { + resolve(`图片尺寸不能小于${MIN_IMAGE_SIZE}px,当前为${img.width}x${img.height}`); + } else { + resolve(null); + } + }; + img.onerror = () => resolve("无法读取图片尺寸"); + img.src = URL.createObjectURL(file); + }); + } + // 格式化文件大小 + const formatFileSize = (bytes: number): string => { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + }; + + // 验证文件 + const validateFile = (file: File): string | null => { + // 验证文件类型 + if (!allowedFileTypes.includes(file.type)) { + const errorMsg = t('validation.file.invalidType'); + // addToast({ + // title: t('fileUploader.invalidFileTitle'), + // description: errorMsg, + // color: "danger", + // }); + return errorMsg; + } + + // 验证文件大小 + // if (file.size > maxFileSize) { + // const errorMsg = t('validation.file.tooLarge'); + // addToast({ + // title: t('fileUploader.fileTooLargeTitle'), + // description: `${errorMsg} ${formatFileSize(maxFileSize)}`, + // color: "danger", + // }); + // return errorMsg; + // } + + return null; + }; + + // 创建缩略图 + const createThumbnail = (file: File): Promise => { + return new Promise((resolve, reject) => { + // 如果是图片文件,直接使用图片作为缩略图 + if (file.type.startsWith('image/')) { + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target && typeof e.target.result === 'string') { + resolve(e.target.result); + } else { + reject(new Error('图片加载失败')); + } + }; + reader.onerror = () => { + reject(new Error('图片读取失败')); + }; + reader.readAsDataURL(file); + return; + } + + // 如果是视频文件,创建视频缩略图 + const videoUrl = URL.createObjectURL(file); + const video = document.createElement('video'); + video.src = videoUrl; + video.crossOrigin = 'anonymous'; + video.muted = true; + video.preload = 'metadata'; + + video.onloadeddata = () => { + try { + // 设置视频时间到第一帧 + video.currentTime = 0.1; + } catch (e) { + URL.revokeObjectURL(videoUrl); + reject(e); + } + }; + + video.onseeked = () => { + try { + // 创建canvas并绘制视频帧 + const canvas = document.createElement('canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext('2d'); + + if (ctx) { + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + const thumbnailUrl = canvas.toDataURL('image/jpeg', 0.7); + URL.revokeObjectURL(videoUrl); + resolve(thumbnailUrl); + } else { + reject(new Error('无法创建canvas上下文')); + } + } catch (e) { + URL.revokeObjectURL(videoUrl); + reject(e); + } + }; + + video.onerror = () => { + URL.revokeObjectURL(videoUrl); + reject(new Error('视频加载失败')); + }; + }); + }; + // 压缩图片函数 + async function compressImageToFile( + file: File, + maxWidth = 600, + quality = 0.7, + outputType = 'image/png' + ): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (event: ProgressEvent) => { + const target = event.target as FileReader; + if (!target || !target.result) { + reject(new Error('Failed to read file')); + return; + } + const img = new Image(); + img.onload = () => { + // 计算压缩尺寸 + const canvas = document.createElement('canvas'); + let width = img.width; + let height = img.height; + + if (width > maxWidth) { + height = Math.round((height * maxWidth) / width); + width = maxWidth; + } + + canvas.width = width; + canvas.height = height; + + // 绘制压缩图片 + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Could not get 2D context from canvas'); + } + ctx.drawImage(img, 0, 0, width, height); + + // 转为 Blob 并生成 File 对象 + canvas.toBlob( + (blob: Blob | null) => { + if (!blob) { + reject(new Error('Failed to create blob from canvas')); + return; + } + let file_name = uuidv4() + ".png" + const compressedFile = new File([blob], file_name, { + type: outputType, + lastModified: Date.now() + }); + resolve(compressedFile); + }, + outputType, + quality + ); + }; + img.onerror = reject; + img.src = target.result as string; + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + // 新增素材 + const addMaterial = async (file: string, compressFile: string) => { + await fetchApi('/material', { + method: 'POST', + body: JSON.stringify([{ + "file_id": file, + "preview_file_id": compressFile + }]) + }).catch((error) => { + console.log(error); + }) + } + + // 上传单个文件 + const uploadFile = async (fileStatus: FileStatus, fileIndex: number, compressedFile: File) => { + // 创建新的文件状态对象,而不是修改原来的数组 + // 这样可以避免多个异步操作对同一个数组的并发修改 + let currentFileStatus: FileStatus = { + ...fileStatus, + status: 'uploading' as const, + progress: 0 + }; + + // 使用函数更新文件状态,确保每次更新都是原子的 + const updateFileStatus = (updates: Partial) => { + currentFileStatus = { ...currentFileStatus, ...updates }; + setFiles(prevFiles => { + const newFiles = [...prevFiles]; + newFiles[fileIndex] = currentFileStatus; + return newFiles; + }); + }; + + // 初始化上传状态 + updateFileStatus({ status: 'uploading', progress: 0 }); + + // 添加小延迟,确保初始状态能被看到 + await new Promise(resolve => setTimeout(resolve, 300)); + + try { + // 获取视频时长 + let metadata = {}; + if (fileStatus.file.type.startsWith('video/')) { + metadata = { + duration: (await getVideoDuration(fileStatus.file)).toString() + } + } + + // 获取上传URL + updateFileStatus({ progress: 10 }); + const uploadUrlData = await getUploadUrl(fileStatus.file, metadata); + const compressedFileData = await getUploadUrl(compressedFile, {}) + + // 确保正确更新文件ID + updateFileStatus({ id: uploadUrlData.file_id, progress: 20 }); + + // 上传文件到URL + await uploadFileToUrl( + compressedFile, + compressedFileData.upload_url, + (progress) => { + // 将实际进度映射到 60%-90% 区间 + const mappedProgress = 60 + (progress * 0.3); + updateFileStatus({ progress: Math.round(mappedProgress) }); + } + ); + + await uploadFileToUrl( + fileStatus.file, + uploadUrlData.upload_url, + (progress) => { + // 将实际进度映射到 60%-90% 区间 + const mappedProgress = 60 + (progress * 0.3); + updateFileStatus({ progress: Math.round(mappedProgress) }); + } + ); + + // 向服务端confirm上传 + await fetchApi('/file/confirm-upload', { + method: 'POST', + body: JSON.stringify({ + file_id: uploadUrlData.file_id + }) + }); + + await fetchApi('/file/confirm-upload', { + method: 'POST', + body: JSON.stringify({ + file_id: compressedFileData.file_id + }) + }); + + // 等待一些时间再标记为成功 + await new Promise(resolve => setTimeout(resolve, 300)); + + // 更新状态为成功 + updateFileStatus({ status: 'success', progress: 100, id: uploadUrlData.file_id }); + + await addMaterial(uploadUrlData.file_id, compressedFileData.file_id) + // 打印最终状态以进行调试 + // console.log('最终文件状态:', currentFileStatus); + // 调用回调函数 + if (onFilesUploaded) { + // 使用当前文件状态创建一个新的数组传递给回调函数 + const updatedFiles = [...files]; + updatedFiles[fileIndex] = { + ...currentFileStatus, + id: uploadUrlData.file_id, // 确保 ID 正确传递 + }; + + // 延迟调用回调函数,确保状态已更新 + setTimeout(() => { + // console.log('传递给回调的文件:', updatedFiles); + onFilesUploaded(updatedFiles); + }, 100); + } + } catch (error) { + console.error('Upload error:', error); + // 更新状态为错误 + updateFileStatus({ + status: 'error', + error: error instanceof Error ? error.message : '上传失败' + }); + } + }; + + // 处理文件选择 + const handleFileSelect = useCallback(async (selectedFiles: FileList | null) => { + if (!selectedFiles) return; + + setIsUploading(true); + + const newFiles: FileStatus[] = []; + + for (let i = 0; i < selectedFiles.length; i++) { + const file = selectedFiles[i]; + const error = validateFile(file); + let dimensionError: string | null = null; + if (!error && file.type.startsWith('image/')) { + dimensionError = await validateImageDimensions(file); + } + + let thumbnailUrl = ''; + // 只在文件验证通过且尺寸合格时创建缩略图 + if (!error && !dimensionError) { + try { + // 创建缩略图,支持图片和视频 + thumbnailUrl = await createThumbnail(file); + newFiles.push({ + file, + progress: 0, + error: error ?? undefined, + status: error ? 'error' : 'pending', + thumbnailUrl + }); + } catch (e) { + console.error('缩略图创建失败:', e); + } + } else { + // 添加警告 + // addToast({ + // title: t('fileUploader.fileTooSmallTitle'), + // description: t('fileUploader.fileTooSmall'), + // color: "warning", + // }); + } + } + + // 更新文件列表 + setFiles(prev => { + // 单文件模式下且已有文件时,替换现有文件 + if (maxFiles === 1 && prev.length > 0 && newFiles.length > 0) { + return [newFiles[0]]; // 只保留新选择的第一个文件 + } else { + // 多文件模式,合并并限制数量 + const combinedFiles = [...prev, ...newFiles]; + return combinedFiles.length > maxFiles + ? combinedFiles.slice(0, maxFiles) + : combinedFiles; + } + }); + + // 在状态更新后,使用 useEffect 来处理上传 + setIsUploading(false); + }, [maxFiles]); + + + // 获取上传URL + const getUploadUrl = async (file: File, metadata: { [key: string]: string }): Promise => { + const body = { + filename: file.name, + content_type: file.type, + file_size: file.size, + metadata + } + return await fetchApi("/file/generate-upload-url", { + method: 'POST', + body: JSON.stringify(body) + }); + }; + + // 上传文件到URL + const uploadFileToUrl = async (file: File, uploadUrl: string, onProgress: (progress: number) => void): Promise => { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + xhr.open('PUT', uploadUrl); + xhr.setRequestHeader('Content-Type', file.type); + + // 进度监听 + xhr.upload.onprogress = (event) => { + if (event.lengthComputable) { + const progress = Math.round((event.loaded / event.total) * 100); + onProgress(progress); + } + }; + + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + reject(new Error(`Upload failed with status ${xhr.status}`)); + } + }; + + xhr.onerror = () => { + reject(new Error('Network error during upload')); + }; + + xhr.send(file); + }); + }; + + // 移除文件 + const removeFile = (fileToRemove: File) => { + setFiles(prev => prev.filter(fileStatus => fileStatus.file !== fileToRemove)); + }; + + // 清除所有文件 + const clearFiles = () => { + setFiles([]); + }; + + // 打开文件选择器 + const openFileSelector = () => { + if (!disabled) { + if (fileInputRef.current) { + fileInputRef.current.click(); + } else { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = allowedFileTypes.join(','); + input.multiple = allowMultiple; + input.onchange = (e) => handleFileSelect((e.target as HTMLInputElement).files); + input.click(); + } + } + }; + + // 使用 useEffect 监听 files 变化,处理待上传的文件 + useEffect(() => { + const processFiles = async () => { + // Only process files that are in 'pending' status + const pendingFiles = files + .filter(f => f.status === 'pending') + .filter((fileStatus, index, self) => + index === self.findIndex(f => f.file === fileStatus.file) + ); // Remove duplicates + + if (pendingFiles.length === 0) return; + + // Create a new array with updated status to prevent infinite loops + setFiles(prevFiles => + prevFiles.map(file => + pendingFiles.some(pf => pf.file === file.file) + ? { ...file, status: 'uploading' as const } + : file + ) + ); + + // Process each file sequentially to avoid race conditions + for (const fileStatus of pendingFiles) { + try { + const fileIndex = files.findIndex(f => f.file === fileStatus.file); + if (fileIndex === -1) continue; + + let compressedFile: File; + if (fileStatus.file.type?.includes('video')) { + const frameFile = await extractVideoFirstFrame(fileStatus.file); + compressedFile = await compressImageToFile(frameFile, 600, 0.7); + } else { + compressedFile = fileStatus.file; + } + + await uploadFile( + { ...fileStatus, status: 'uploading' as const }, + fileIndex, + compressedFile + ); + } catch (error) { + console.error('Error processing file:', error); + setFiles(prevFiles => + prevFiles.map(f => + f.file === fileStatus.file + ? { + ...f, + status: 'error' as const, + error: error instanceof Error ? error.message : '处理文件失败' + } + : f + ) + ); + } + } + }; + + processFiles(); + }, [files]); // Only run when files array changes + + return ( + + {/* 隐藏的文件输入 */} + handleFileSelect(e.target.files)} + className="hidden" + /> + + {/* 文件上传区域 - 始终可见 */} + + {/* 上传区域 */} + {maxFiles === 1 && files.length === 1 ? ( + /* 单文件模式且已有文件 - 不添加外层的onClick事件 */ + + ) : thumbnailPropsUrl ? : ( + /* 多文件模式或无文件 - 只在组件上添加一个onClick事件 */ + + )} + + {/* 文件列表区域 - 仅在多文件模式下显示 */} + {maxFiles !== 1 && files.length > 0 && ( + + )} + + + ); +} \ No newline at end of file diff --git a/components/file-upload/multi-file-uploader.tsx b/components/file-upload/multi-file-uploader.tsx new file mode 100644 index 0000000..6564523 --- /dev/null +++ b/components/file-upload/multi-file-uploader.tsx @@ -0,0 +1,65 @@ +import useWindowSize from "@/hooks/useWindowSize"; +import { useTranslation } from "react-i18next"; +import { TouchableOpacity, View } from "react-native"; +import { ThemedText } from "../ThemedText"; +import FileItemPhone from "./file-item-phone"; +import { FileStatus } from "./file-uploader"; + +interface MultiFileUploaderProps { + files: FileStatus[]; + onRemove: (file: File) => void; + onClearAll: () => void; + formatFileSize: (bytes: number) => string; + disabled?: boolean; +} + +/** + * 多文件上传组件 - 用于显示文件列表和管理多个文件 + */ +export default function MultiFileUploader({ + files, + onRemove, + onClearAll, + formatFileSize, + disabled = false +}: MultiFileUploaderProps) { + const { t } = useTranslation(); + // 获取当前屏幕尺寸 + const { isMobile } = useWindowSize(); + return ( + + + + {t('fileUploader.uploadedFiles')} + + + + + {t('fileUploader.clearAll')} + + + + + + + + {files.map((fileStatus, index) => ( + ( + + ) + ))} + + + ); +} diff --git a/components/file-upload/single-file-uploader.tsx b/components/file-upload/single-file-uploader.tsx new file mode 100644 index 0000000..a9db1c0 --- /dev/null +++ b/components/file-upload/single-file-uploader.tsx @@ -0,0 +1,174 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, Image, StyleSheet } from 'react-native'; +import { useTranslation } from 'react-i18next'; +import { FileStatus } from './file-uploader'; +import Icon from 'react-native-vector-icons/MaterialIcons'; + +interface SingleFileUploaderProps { + file: FileStatus; + onReplace: () => void; + disabled?: boolean; + formatFileSize?: (bytes: number) => string; +} + +export default function SingleFileUploader({ + file, + onReplace, + disabled = false, + formatFileSize = (bytes) => `${bytes} B` +}: SingleFileUploaderProps) { + const { t } = useTranslation(); + + return ( + + {/* 缩略图容器 */} + + {file.thumbnailUrl ? ( + <> + + {/* 错误信息显示 */} + {file.error && ( + + + {file.error} + + + )} + + ) : ( + + + + )} + + {/* 显示替换按钮 */} + {file.thumbnailUrl && !disabled && ( + { + e.stopPropagation(); + onReplace(); + }} + disabled={disabled} + > + + + + {t('common.replace')} + + + + )} + + + {/* 文件信息 */} + + + + {file.file.name} + + + {formatFileSize(file.file.size)} • {file.status} + + + {file.status === 'uploading' && ( + + + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + width: '100%', + height: '100%', + }, + thumbnailContainer: { + width: '100%', + aspectRatio: 16/9, + backgroundColor: '#F3F4F6', + borderRadius: 6, + overflow: 'hidden', + position: 'relative', + }, + thumbnailImage: { + width: '100%', + height: '100%', + }, + errorContainer: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + backgroundColor: 'rgba(220, 38, 38, 0.8)', + padding: 4, + }, + errorText: { + color: 'white', + fontSize: 12, + }, + placeholderContainer: { + width: '100%', + height: '100%', + justifyContent: 'center', + alignItems: 'center', + }, + replaceButton: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + opacity: 0, + }, + replaceButtonContent: { + justifyContent: 'center', + alignItems: 'center', + }, + replaceButtonText: { + color: 'white', + fontSize: 14, + fontWeight: '500', + }, + fileInfo: { + marginTop: 8, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + fileInfoText: { + flex: 1, + minWidth: 0, + }, + fileName: { + fontSize: 14, + fontWeight: '500', + color: '#111827', + }, + fileSize: { + fontSize: 12, + color: '#6B7280', + }, + progressContainer: { + width: 96, + height: 8, + backgroundColor: '#E5E7EB', + borderRadius: 4, + marginLeft: 8, + overflow: 'hidden', + }, + progressBar: { + height: '100%', + backgroundColor: '#3B82F6', + }, +}); diff --git a/components/file-upload/upload-dropzone.tsx b/components/file-upload/upload-dropzone.tsx new file mode 100644 index 0000000..7f336a3 --- /dev/null +++ b/components/file-upload/upload-dropzone.tsx @@ -0,0 +1,77 @@ +import SvgIcon from "@/components/svg-icon"; +import { useTranslation } from "react-i18next"; +import { DEFAULT_ALLOWED_FILE_TYPES, DEFAULT_MAX_FILE_SIZE } from "./file-uploader"; + +interface UploadDropzoneProps { + onClick: () => void; + disabled?: boolean; + allowedFileTypes?: string[]; + maxFileSize?: number; +} + +/** + * 上传区域组件 - 用于显示文件拖放和选择区域 + */ +export default function UploadDropzone({ + onClick, + disabled = false, + allowedFileTypes = DEFAULT_ALLOWED_FILE_TYPES, + maxFileSize = DEFAULT_MAX_FILE_SIZE +}: UploadDropzoneProps) { + const { t } = useTranslation(); + + // 格式化文件类型显示 + const formatFileTypes = (types: string[]): string => { + return types.map(type => { + // 从 MIME 类型提取文件扩展名 + const extensions: Record = { + 'video/mp4': 'MP4', + 'video/quicktime': 'MOV', + 'video/x-msvideo': 'AVI', + 'video/x-matroska': 'MKV', + 'image/jpeg': 'JPG', + 'image/png': 'PNG', + 'image/gif': 'GIF', + 'image/webp': 'WEBP' + }; + return extensions[type] || type.split('/')[1]?.toUpperCase() || type; + }).join(', '); + }; + + // 格式化文件大小显示 + const formatFileSize = (bytes: number): string => { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB'; + }; + + return ( +
+
+
+ +
+ +
+

+ {t('fileUploader.dragAndDropFiles')} +

+

+ {t('fileUploader.orClickToUpload')} +

+

+ {t('fileUploader.supportedFormats')}: {formatFileTypes(allowedFileTypes)} +

+

+ {t('fileUploader.maxFileSize')}: {formatFileSize(maxFileSize)} +

+
+
+
+ ); +} diff --git a/components/file-upload/utils/videoUtils.ts b/components/file-upload/utils/videoUtils.ts new file mode 100644 index 0000000..de63484 --- /dev/null +++ b/components/file-upload/utils/videoUtils.ts @@ -0,0 +1,138 @@ +/** + * 从视频文件中提取第一帧并返回为File对象 + * @param videoFile 视频文件 + * @returns 包含视频第一帧的File对象 + */ +export const extractVideoFirstFrame = (videoFile: File): Promise => { + return new Promise((resolve, reject) => { + const videoUrl = URL.createObjectURL(videoFile); + const video = document.createElement('video'); + video.src = videoUrl; + video.crossOrigin = 'anonymous'; + video.muted = true; + video.preload = 'metadata'; + + video.onloadeddata = () => { + try { + // 设置视频时间到第一帧 + video.currentTime = 0.1; + } catch (e) { + URL.revokeObjectURL(videoUrl); + reject(e); + } + }; + + video.onseeked = () => { + try { + const canvas = document.createElement('canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext('2d'); + + if (!ctx) { + throw new Error('无法获取canvas上下文'); + } + + // 绘制视频帧到canvas + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + + // 将canvas转换为DataURL + const dataUrl = canvas.toDataURL('image/jpeg'); + + // 将DataURL转换为Blob + const byteString = atob(dataUrl.split(',')[1]); + const mimeString = dataUrl.split(',')[0].split(':')[1].split(';')[0]; + const ab = new ArrayBuffer(byteString.length); + const ia = new Uint8Array(ab); + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + const blob = new Blob([ab], { type: mimeString }); + + // 创建File对象 + const frameFile = new File( + [blob], + `${videoFile.name.replace(/\.[^/.]+$/, '')}_frame.jpg`, + { type: 'image/jpeg' } + ); + + // 清理URL对象 + URL.revokeObjectURL(videoUrl); + resolve(frameFile); + } catch (e) { + URL.revokeObjectURL(videoUrl); + reject(e); + } + }; + + video.onerror = () => { + URL.revokeObjectURL(videoUrl); + reject(new Error('视频加载失败')); + }; + }); +}; + +// 获取视频时长 +export const getVideoDuration = (file: File): Promise => { + return new Promise((resolve) => { + const video = document.createElement('video'); + video.preload = 'metadata'; + + video.onloadedmetadata = () => { + URL.revokeObjectURL(video.src); + resolve(video.duration); + }; + + video.onerror = () => { + URL.revokeObjectURL(video.src); + resolve(0); // Return 0 if we can't get the duration + }; + + video.src = URL.createObjectURL(file); + }); +}; + +// 根据 mp4 的url来获取视频时长 +/** + * 根据视频URL获取视频时长 + * @param videoUrl 视频的URL + * @returns 返回一个Promise,解析为视频时长(秒) + */ +export const getVideoDurationFromUrl = async (videoUrl: string): Promise => { + return await new Promise((resolve, reject) => { + // 创建临时的video元素 + const video = document.createElement('video'); + + // 设置为只加载元数据,不加载整个视频 + video.preload = 'metadata'; + + // 处理加载成功 + video.onloadedmetadata = () => { + // 释放URL对象 + URL.revokeObjectURL(video.src); + // 返回视频时长(秒) + resolve(video.duration); + }; + + // 处理加载错误 + video.onerror = () => { + URL.revokeObjectURL(video.src); + reject(new Error('无法加载视频')); + }; + + // 处理网络错误 + video.onabort = () => { + URL.revokeObjectURL(video.src); + reject(new Error('视频加载被中止')); + }; + + // 设置视频源 + video.src = videoUrl; + + // 添加跨域属性(如果需要) + video.setAttribute('crossOrigin', 'anonymous'); + + // 开始加载元数据 + video.load(); + }); +}; diff --git a/components/login/code.tsx b/components/login/code.tsx new file mode 100644 index 0000000..5b8fb8d --- /dev/null +++ b/components/login/code.tsx @@ -0,0 +1,197 @@ +import { fetchApi } from "@/lib/server-api-util"; +import { User } from "@/types/user"; +import { router } from "expo-router"; +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { ActivityIndicator, Animated, TextInput as RNTextInput, TextInput, TouchableOpacity, View } from "react-native"; +import { useAuth } from "../../contexts/auth-context"; +import { ThemedText } from "../ThemedText"; +import { Steps } from "./phoneLogin"; + +interface LoginProps { + setSteps: (steps: Steps) => void; + phone: string; +} + +const Code = ({ setSteps, phone }: LoginProps) => { + const { t } = useTranslation(); + const { login } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const refs = useRef>(Array(6).fill(null)); + const shakeAnim = useRef(new Animated.Value(0)).current; + const [code, setCode] = useState(['', '', '', '', '', '']); + const [error, setError] = useState(''); + + const focusNext = (index: number, value: string) => { + if (value && index < 5) { + refs?.current?.[index + 1]?.focus(); + } + }; + const focusPrevious = (index: number, key: string) => { + if (key === 'Backspace' && index > 0 && !code[index]) { + refs?.current?.[index - 1]?.focus(); + } + }; + const handleCodeChange = (text: string, index: number) => { + setError(''); + const newCode = [...code]; + newCode[index] = text; + setCode(newCode); + focusNext(index, text); + }; + const sendVerificationCode = async () => { + try { + // 发送验证码 + await fetchApi(`/iam/veritification-code`, { + method: 'POST', + body: JSON.stringify({ phone: phone }), + }) + + } catch (error) { + console.error(t("auth.telLogin.sendCodeError", { ns: 'login' }), error); + } + } + + const handleTelLogin = async () => { + setError(''); + if (!code.join('')) { + setError(t("auth.telLogin.codeRequired", { ns: 'login' })); + return; + } + // 如果验证码不是六位,提示错误 + if (code.join('').length !== 6) { + setError(t("auth.telLogin.codeInvalid", { ns: 'login' })); + return; + } + + setIsLoading(true); + setCountdown(60); + try { + await fetchApi(`/iam/login/phone-login`, { + method: 'POST', + body: JSON.stringify({ phone: phone, code: code.join('') }), + }).then((res) => { + login(res, res.access_token || '') + router.replace('/user-message') + }).catch((error) => { + console.log(error); + setError(t("auth.telLogin.codeVaild", { ns: 'login' })); + }) + setIsLoading(false); + } catch (error) { + setIsLoading(false); + console.error(t("auth.telLogin.codeVaild", { ns: 'login' }), error); + } + } + // 60s倒计时 + const [countdown, setCountdown] = useState(0); + useEffect(() => { + if (countdown > 0) { + const timer = setTimeout(() => setCountdown(countdown - 1), 1000); + return () => clearTimeout(timer); + } + }, [countdown]); + + return ( + + + + + {t("auth.telLogin.title", { ns: 'login' })} + + + {t("auth.telLogin.secondTitle", { ns: 'login' })} + + + {phone} + + + + + {code.map((digit, index) => ( + { + if (ref) { + refs.current[index] = ref; + } + }} + style={{ width: 40, height: 40 }} + className="bg-[#FFF8DE] rounded-xl text-textTertiary text-3xl text-center" + keyboardType="number-pad" + maxLength={1} + value={digit} + onChangeText={text => handleCodeChange(text, index)} + onKeyPress={({ nativeEvent }) => focusPrevious(index, nativeEvent.key)} + selectTextOnFocus + caretHidden={true} + /> + ))} + + + + + + + + {error} + + + + {isLoading ? ( + + ) : ( + + {t("auth.telLogin.continue", { ns: 'login' })} + + )} + + + + + {t("auth.telLogin.sendAgain", { ns: 'login' })} + + { + if (countdown > 0) { + return + } else { + sendVerificationCode() + } + }}> + 0 ? '!text-gray-400' : ''}`}> + {countdown > 0 ? `${countdown}s${t("auth.telLogin.resend", { ns: 'login' })}` : t("auth.telLogin.resend", { ns: 'login' })} + + + + + + + setSteps('phone')} + > + + {t("auth.telLogin.goBack", { ns: 'login' })} + + + + + ) +} + + +export default Code \ No newline at end of file diff --git a/components/login/forgetPwd.tsx b/components/login/forgetPwd.tsx new file mode 100644 index 0000000..4618a42 --- /dev/null +++ b/components/login/forgetPwd.tsx @@ -0,0 +1,121 @@ +import { fetchApi } from "@/lib/server-api-util"; +import { User } from "@/types/user"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { ActivityIndicator, TextInput, TouchableOpacity, View } from "react-native"; +import { ThemedText } from "../ThemedText"; + +interface LoginProps { + setIsSignUp?: (isSignUp: string) => void; + closeModal?: () => void; + updateUrlParam?: (status: string, value: string) => void; + setError: (error: string) => void; +} + +const ForgetPwd = ({ setIsSignUp, updateUrlParam, setError }: LoginProps) => { + const { t } = useTranslation(); + const [loading, setLocading] = useState(false); + // 发送邮箱后把按钮变为disabled + const [isDisabled, setIsDisabled] = useState(false); + const [email, setEmail] = useState(''); + const [countdown, setCountdown] = useState(0); + + // 倒计时效果 + useEffect(() => { + if (countdown > 0) { + const timer = setTimeout(() => setCountdown(countdown - 1), 1000); + return () => clearTimeout(timer); + } else if (countdown === 0 && isDisabled) { + setIsDisabled(false); + } + }, [countdown, isDisabled]); + + // 发送邮件 + const handleSubmit = () => { + if (!email) { + setError(t('auth.forgetPwd.emailPlaceholder', { ns: 'login' })); + return; + } + + setLocading(true); + const body = { + email: email, + } + // 调接口确定邮箱是否正确,是否有该用户邮箱权限 + fetchApi('/iam/reset-password-session', { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json' + } + }) + .then((_) => { + console.log("Password reset email sent successfully"); + setIsDisabled(true); + setCountdown(60); // 开始60秒倒计时 + }) + .catch((error) => { + console.error('Failed to send reset email:', error); + setError(t('auth.forgetPwd.sendEmailError', { ns: 'login' })); + }) + .finally(() => { + setLocading(false); + }); + }; + // 返回登陆 + const handleBackToLogin = () => { + if (setIsSignUp) { + setIsSignUp('login'); + } + if (updateUrlParam) { + updateUrlParam('status', 'login'); + } + } + + return + {/* 邮箱输入框 */} + + + {t('auth.forgetPwd.title', { ns: 'login' })} + + + + {/* 发送重置密码邮件 */} + + {loading ? ( + + ) : ( + + {isDisabled + ? `${t("auth.forgetPwd.sendEmailBtnDisabled", { ns: "login" })} (${countdown}s)` + : t("auth.forgetPwd.sendEmailBtn", { ns: "login" })} + + + )} + + {/* 返回登陆 */} + + + {t('auth.forgetPwd.goback', { ns: 'login' })} + + + +} + + +export default ForgetPwd \ No newline at end of file diff --git a/components/login/login.tsx b/components/login/login.tsx new file mode 100644 index 0000000..39f2211 --- /dev/null +++ b/components/login/login.tsx @@ -0,0 +1,152 @@ +import { Ionicons } from "@expo/vector-icons"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { ActivityIndicator, TextInput, TouchableOpacity, View } from "react-native"; +import { useAuth } from "../../contexts/auth-context"; +import { fetchApi } from "../../lib/server-api-util"; +import { User } from "../../types/user"; +import { ThemedText } from "../ThemedText"; + +const REMEMBER_ACCOUNT_KEY = 'fairclip_remembered_account'; +interface LoginProps { + updateUrlParam: (status: string, value: string) => void; + setError: (error: string) => void; + setShowPassword: (showPassword: boolean) => void; + showPassword: boolean; +} + +const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: LoginProps) => { + const { t } = useTranslation(); + const { login } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [rememberMe, setRememberMe] = useState(false); + + const handleLogin = async () => { + if (!email) { + setError(t('auth.login.emailPlaceholder', { ns: 'login' })); + return; + }; + if (!password) { + setError(t('auth.login.passwordPlaceholder', { ns: 'login' })); + return; + } + setIsLoading(true); + try { + const body = { + account: email, + password: password, + }; + + const res = await fetchApi('/iam/login/password-login', { + method: 'POST', + body: JSON.stringify(body), + }); + const userInfo = await fetchApi('/iam/user-info'); + login({ ...res, email: res?.account }, res.access_token || ''); + + } catch (error) { + console.error('Login failed', error); + } finally { + setIsLoading(false); + } + }; + + const handleForgotPassword = () => { + updateUrlParam('status', 'forgetPwd'); + }; + + const handleSignUp = () => { + updateUrlParam('status', 'signUp'); + }; + return + {/* 邮箱输入框 */} + + + {t('auth.login.email', { ns: 'login' })} + + { + setEmail(text); + setError('123'); + }} + keyboardType="email-address" + autoCapitalize="none" + /> + + {/* 密码输入框 */} + + + {t('auth.login.password', { ns: 'login' })} + + + { + setPassword(text); + setError('123'); + }} + secureTextEntry={!showPassword} + /> + setShowPassword(!showPassword)} + > + + + + + + {/* 忘记密码链接 */} + + + {t('auth.login.forgotPassword', { ns: 'login' })} + + + + {/* 登录按钮 */} + + {isLoading ? ( + + ) : ( + + {t('auth.login.loginButton', { ns: 'login' })} + + )} + + + {/* 注册链接 */} + + + {t('auth.login.signUpMessage', { ns: 'login' })} + + + + {t('auth.login.signUp', { ns: 'login' })} + + + + +} + + +export default Login \ No newline at end of file diff --git a/components/login/phone.tsx b/components/login/phone.tsx new file mode 100644 index 0000000..9dfd7ed --- /dev/null +++ b/components/login/phone.tsx @@ -0,0 +1,85 @@ +import { fetchApi } from "@/lib/server-api-util"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { ActivityIndicator, TextInput, TouchableOpacity, View } from "react-native"; +import { ThemedText } from "../ThemedText"; +import { Steps } from "./phoneLogin"; + +interface LoginProps { + setSteps: (steps: Steps) => void; + setPhone: (phone: string) => void; + phone: string; +} + +const Phone = ({ setSteps, setPhone, phone }: LoginProps) => { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + // 发送验证码 + const sendVerificationCode = async () => { + if (!/^1[3-9]\d{9}$/.test(phone)) { + setError(t("auth.telLogin.phoneInvalid", { ns: 'login' })); + return; + } + + try { + setIsLoading(true); + + // 发送验证码 + await fetchApi(`/iam/veritification-code`, { + method: 'POST', + body: JSON.stringify({ phone: phone }), + }) + setSteps('code') + setIsLoading(false); + } catch (error) { + setPhone("") + setIsLoading(false); + console.error(t("auth.telLogin.sendCodeError", { ns: 'login' }), error); + } + }; + + return + {/* 手机号输入框 */} + + + + {t('auth.telLogin.title', { ns: 'login' })} + + + {error} + + + { + setPhone(text); + setError(''); + }} + keyboardType="email-address" + autoCapitalize="none" + /> + + + {/* 发送验证码 */} + + {isLoading ? ( + + ) : ( + + {t('auth.telLogin.sendCode', { ns: 'login' })} + + )} + + +} + + +export default Phone \ No newline at end of file diff --git a/components/login/phoneLogin.tsx b/components/login/phoneLogin.tsx new file mode 100644 index 0000000..241e6cf --- /dev/null +++ b/components/login/phoneLogin.tsx @@ -0,0 +1,24 @@ +import { useState } from "react"; +import { View } from "react-native"; +import Code from "./code"; +import Phone from "./phone"; + +interface LoginProps { + updateUrlParam: (status: string, value: string) => void; + setError: (error: string) => void +} +export type Steps = "phone" | "code"; + +const PhoneLogin = ({ updateUrlParam, setError }: LoginProps) => { + const [steps, setSteps] = useState("phone"); + const [phone, setPhone] = useState(''); + + return + { + steps === "phone" ? : + } + +} + + +export default PhoneLogin \ No newline at end of file diff --git a/components/login/signUp.tsx b/components/login/signUp.tsx new file mode 100644 index 0000000..fb35f4c --- /dev/null +++ b/components/login/signUp.tsx @@ -0,0 +1,325 @@ +import { Ionicons } from "@expo/vector-icons"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { useEffect, useState } from 'react'; +import { useTranslation } from "react-i18next"; +import { ActivityIndicator, TextInput, TouchableOpacity, View } from 'react-native'; +import { useAuth } from "../../contexts/auth-context"; +import { fetchApi } from "../../lib/server-api-util"; +import { User } from "../../types/user"; +import { ThemedText } from "../ThemedText"; + +interface LoginProps { + updateUrlParam: (status: string, value: string) => void; + setError: (error: string) => void; + setShowPassword: (showPassword: boolean) => void; + showPassword: boolean; +} + +const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword }: LoginProps) => { + const { t } = useTranslation(); + const { login } = useAuth(); + const router = useRouter(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [passwordsMatch, setPasswordsMatch] = useState(true); + const [loading, setLoading] = useState(false); + const [checked, setChecked] = useState(false); + + // 从 URL 参数中获取 task_id 和 steps + const params = useLocalSearchParams<{ task_id?: string; steps?: string }>(); + const taskId = params.task_id; + const steps = params.steps; + + const handlePasswordChange = (value: string) => { + setPassword(value); + // 当密码或确认密码变化时,检查是否匹配 + if (confirmPassword && value !== confirmPassword) { + setPasswordsMatch(false); + } else { + setPasswordsMatch(true); + } + }; + + const handleConfirmPasswordChange = (value: string) => { + setConfirmPassword(value); + // 当密码或确认密码变化时,检查是否匹配 + if (password && value !== password) { + setPasswordsMatch(false); + } else { + setPasswordsMatch(true); + } + }; + + const handleSubmit = async () => { + if (!email) { + setError(t('auth.signup.emailPlaceholder', { ns: 'login' })); + return; + } + if (!password) { + setError(t('auth.signup.passwordPlaceholder', { ns: 'login' })); + return; + } + // 验证两次密码是否一致 + if (password !== confirmPassword) { + setPasswordsMatch(false); + setError(t('auth.signup.passwordNotMatch', { ns: 'login' })); + return; + } + if (!checked) { + setError(t('auth.signup.checkedRequired', { ns: 'login' })); + return; + } + + if (email) { + // 校验是否符合邮箱规范 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + setError(t('auth.signup.emailAuth', { ns: 'login' })); + return; + } + } + if (password) { + // 校验密码是否符合规范 + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; + if (!passwordRegex.test(password)) { + setError(t('auth.signup.passwordAuth', { ns: 'login' })); + return; + } + } + + setLoading(true); + + try { + const body = { + email: email, + password: password + }; + + // 这里调用实际的注册API + const response = await fetchApi('/iam/register/email', { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json' + } + }, true, false); + + // 存储token + login(response as User, response.access_token || ''); + + // 如果有任务ID,跳转到上传页面 + if (taskId) { + // 使用字符串路径格式传递参数 + // router.push(`/upload?steps=${encodeURIComponent(steps || '')}&task_id=${encodeURIComponent(taskId)}`); + } else { + // 注册成功后跳转到首页 + router.replace('/user-message'); + } + setLoading(false); + } catch (error) { + console.error('Registration failed:', error); + // 这里可以添加错误处理逻辑 + setLoading(false); + } + }; + + useEffect(() => { + if (!passwordsMatch) { + setError(t('auth.login.passwordNotMatch', { ns: 'login' })); + } + }, [passwordsMatch]) + // 初始化 + useEffect(() => { + setShowPassword(false) + }, []) + + return + {/* 邮箱输入 */} + + + {t('auth.login.email', { ns: 'login' })} + + + { + setEmail(value) + setError('123') + }} + keyboardType="email-address" + autoCapitalize="none" + /> + + + + {/* 密码输入 */} + + + {t('auth.login.password', { ns: 'login' })} + + + { + handlePasswordChange(value) + setError('123') + }} + secureTextEntry={!showPassword} + /> + setShowPassword(!showPassword)} + className="px-3 py-2" + > + + + + + + {/* 确认密码 */} + + + {t('auth.signup.confirmPassword', { ns: 'login' })} + + + { + handleConfirmPasswordChange(value) + setError('123') + }} + secureTextEntry={!showPassword} + /> + setShowPassword(!showPassword)} + className="px-3 py-2" + > + + + + + + {/* 注册按钮 */} + + {loading ? ( + + ) : ( + + {t("auth.signup.signupButton", { ns: 'login' })} + + )} + + + { + const newValue = !checked; + setChecked(newValue); + if (!newValue) { + setError(t('auth.signup.checkedRequired', { ns: 'login' })); + return + } else { + setError("123") + } + + }} + style={{ + width: 20, + height: 20, + borderRadius: 10, + borderWidth: 2, + borderColor: checked ? '#E2793F' : '#ccc', + backgroundColor: checked ? '#E2793F' : 'transparent', + justifyContent: 'center', + alignItems: 'center', + marginRight: 8, + }} + > + {checked && ( + + )} + + + + {t("auth.telLogin.agree", { ns: 'login' })} + + router.push({ + pathname: '/agreement', + params: { type: 'service' } + } as any)}> + + {t("auth.telLogin.terms", { ns: 'login' })} + + + + {t("auth.telLogin.and", { ns: 'login' })} + + router.push({ + pathname: '/agreement', + params: { type: 'privacy' } + } as any)}> + + {t("auth.telLogin.privacyPolicy", { ns: 'login' })} + + + + {t("auth.telLogin.and", { ns: 'login' })} + + router.push({ + pathname: '/agreement', + params: { type: 'user' } + } as any)}> + + {t("auth.telLogin.userAgreement", { ns: 'login' })} + + + + {t("auth.telLogin.agreement", { ns: 'login' })} + + + {t("common.name")} + + + {t("auth.telLogin.getPhone", { ns: 'login' })} + + + + {/* 已有账号 */} + + + {t("auth.signup.haveAccount", { ns: 'login' })} + + { + updateUrlParam("status", "login"); + }} + > + + {t("auth.signup.login", { ns: 'login' })} + + + + +} + +export default SignUp \ No newline at end of file diff --git a/components/login/vetify.tsx b/components/login/vetify.tsx new file mode 100644 index 0000000..798e013 --- /dev/null +++ b/components/login/vetify.tsx @@ -0,0 +1,117 @@ +import React, { useRef, useState } from 'react'; +import { Animated, Keyboard, Text, TextInput, TouchableOpacity, View } from 'react-native'; + +const VerificationCodeInput = () => { + const [code, setCode] = useState(['', '', '', '', '', '']); + const refs = useRef([]); + const shakeAnim = useRef(new Animated.Value(0)).current; + + const focusNext = (index, value) => { + if (value && index < 5) { + refs.current[index + 1].focus(); + } + }; + + const focusPrevious = (index, key) => { + if (key === 'Backspace' && index > 0 && !code[index]) { + refs.current[index - 1].focus(); + } + }; + + const handleCodeChange = (text, index) => { + const newCode = [...code]; + newCode[index] = text; + setCode(newCode); + focusNext(index, text); + }; + + const handleSubmit = () => { + const fullCode = code.join(''); + if (fullCode.length === 5) { + Keyboard.dismiss(); + // 这里处理验证逻辑 + console.log('验证码:', fullCode); + } else { + // 抖动动画效果 + Animated.sequence([ + Animated.timing(shakeAnim, { + toValue: 10, + duration: 50, + useNativeDriver: true + }), + Animated.timing(shakeAnim, { + toValue: -10, + duration: 50, + useNativeDriver: true + }), + Animated.timing(shakeAnim, { + toValue: 10, + duration: 50, + useNativeDriver: true + }), + Animated.timing(shakeAnim, { + toValue: 0, + duration: 50, + useNativeDriver: true + }) + ]).start(); + } + }; + + const handleClear = () => { + setCode(['', '', '', '', '']); + refs.current[0].focus(); + }; + + return ( + + + {code.map((digit, index) => ( + refs.current[index] = ref} + className="w-16 h-16 bg-[#FFF8DE] rounded-xl text-textTertiary text-3xl text-center" + keyboardType="number-pad" + maxLength={1} + value={digit} + onChangeText={text => handleCodeChange(text, index)} + onKeyPress={({ nativeEvent: { key } }) => focusPrevious(index, key)} + selectTextOnFocus + caretHidden={true} + /> + ))} + + + + + 清除 + + + + 验证 + + + + + 未收到验证码? + + 重新发送 + + + + ); +}; + +export default VerificationCodeInput; \ No newline at end of file diff --git a/components/svg-icon.tsx b/components/svg-icon.tsx new file mode 100644 index 0000000..3f98491 --- /dev/null +++ b/components/svg-icon.tsx @@ -0,0 +1,23 @@ +export default function svgIcon({ + name, + prefix = 'icon', + color = '#333', + width = '16px', + height = '16px', + ...props +}: { + name: string; + prefix?: string; + color?: string; + width?: string; + height?: string; + [key: string]: any; +}) { + const symbolId = `#${prefix}-${name}` + + return ( + + ) +} diff --git a/components/user-message.tsx/choice.tsx b/components/user-message.tsx/choice.tsx new file mode 100644 index 0000000..8f020e0 --- /dev/null +++ b/components/user-message.tsx/choice.tsx @@ -0,0 +1,170 @@ +import { Steps } from '@/app/(tabs)/user-message'; +import { ThemedText } from '@/components/ThemedText'; +import { useState } from 'react'; +import { TouchableOpacity, View } from 'react-native'; + +interface Props { + setSteps: (steps: Steps) => void; +} + +type ChoiceOption = { + id: string; + emoji: string; + label: string; + description: string; +}; + +export default function Choice({ setSteps }: Props) { + const [selectedOption, setSelectedOption] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const handleContinue = () => { + if (!selectedOption) return; + setIsLoading(true); + // Simulate API call + setTimeout(() => { + setIsLoading(false); + setSteps('done'); + }, 500); + }; + + const options: ChoiceOption[] = [ + { + id: 'wakeup', + emoji: '⏰', + label: 'Wake Up', + description: 'Start your day right with a gentle wake-up' + }, + { + id: 'sleep', + emoji: '😴', + label: 'Sleep', + description: 'Drift off with calming sounds' + }, + { + id: 'focus', + emoji: '🎯', + label: 'Focus', + description: 'Enhance your concentration' + }, + { + id: 'relax', + emoji: '🧘', + label: 'Relax', + description: 'Unwind and de-stress' + }, + { + id: 'relax1', + emoji: '🧘', + label: 'Relax1', + description: 'Unwind and de-stress' + }, + { + id: 'relax11', + emoji: '🧘', + label: 'Relax11', + description: 'Unwind and de-stress' + }, + ]; + + return ( + + {/* Fixed Header */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Every memory matters. + + + Select a few to help Memo create better video capsules for you + + + + + {/* Scrollable Content */} + + + + {options.map((option) => ( + { + // 如果存在则删除,没有则添加,多选 + if (selectedOption.includes(option.id)) { + // 剔除 + setSelectedOption((prev) => prev.filter((id) => id !== option.id)); + } else { + // 添加 + setSelectedOption((prev) => ([...prev, option.id])); + } + }} + > + + {option.label} + + + ))} + + {/* Next Button */} + + + + Next + + + + + + + + ); +} diff --git a/components/user-message.tsx/done.tsx b/components/user-message.tsx/done.tsx new file mode 100644 index 0000000..e00b604 --- /dev/null +++ b/components/user-message.tsx/done.tsx @@ -0,0 +1,78 @@ +import { Steps } from '@/app/(tabs)/user-message'; +import { useTranslation } from 'react-i18next'; +import { TouchableOpacity, View } from 'react-native'; +import { ThemedText } from '../ThemedText'; + +interface Props { + setSteps: (steps: Steps) => void; +} +export default function Done(props: Props) { + const { setSteps } = props + const { t } = useTranslation(); + const handleContinue = () => { + + }; + return ( + + + + {t('auth.userMessage.allDone', { ns: 'login' })} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Next Button */} + + + + {t('auth.userMessage.next', { ns: 'login' })} + + + + + ) +} diff --git a/components/user-message.tsx/look.tsx b/components/user-message.tsx/look.tsx new file mode 100644 index 0000000..17e279d --- /dev/null +++ b/components/user-message.tsx/look.tsx @@ -0,0 +1,172 @@ +import { Steps } from '@/app/(tabs)/user-message'; +import { ThemedText } from '@/components/ThemedText'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Image, Modal, SafeAreaView, StyleSheet, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import FileUploader, { FileStatus } from '../file-upload/file-uploader'; +interface Props { + setSteps?: (steps: Steps) => void; + fileData: FileStatus[]; + setFileData: (fileData: FileStatus[]) => void; + isLoading: boolean; + handleUser: () => void; + avatar: string; +} + +export default function Look({ fileData, setFileData, isLoading, handleUser, avatar }: Props) { + const [isModalVisible, setIsModalVisible] = useState(false); + const { t } = useTranslation(); + + return ( + + + + + {t('auth.userMessage.look', { ns: 'login' })} + + + {t('auth.userMessage.avatarText', { ns: 'login' })} +
+ {t('auth.userMessage.avatorText2', { ns: 'login' })} +
+ + + { + fileData?.[0]?.thumbnailUrl + ? + + : + avatar + ? + + : + + + + + } + + { setIsModalVisible(true) }} + > + + + + + + {t('auth.userMessage.choosePhoto', { ns: 'login' })} + + + {/* 上传弹窗 */} + + + { + setIsModalVisible(false); + }} + > + + setIsModalVisible(false)}> + + + + {t('auth.userMessage.choosePhoto', { ns: 'login' })} + { + setFileData(file); + setIsModalVisible(false); + }} + allowedFileTypes={["image/png", "image/jpeg", "image/webp"]} + maxFileSize={1024 * 1024 * 10} + className="w-full" + /> + + + + + + +
+
+ + {/* Continue Button */} + + + + {t('auth.userMessage.next', { ns: 'login' })} + + + +
+ ); +} +const styles = StyleSheet.create({ + image: { + width: 215, + height: 215, + borderRadius: 107.5, + }, + centeredView: { + flex: 1, + }, + modalOverlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + }, + modalOverlayTouchable: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + modalContent: { + width: '90%', + maxWidth: 400, + backgroundColor: 'white', + borderRadius: 12, + padding: 20, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + modalTitle: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 15, + textAlign: 'center', + }, + // ... 其他样式保持不变 +}); \ No newline at end of file diff --git a/components/user-message.tsx/userName.tsx b/components/user-message.tsx/userName.tsx new file mode 100644 index 0000000..701459d --- /dev/null +++ b/components/user-message.tsx/userName.tsx @@ -0,0 +1,65 @@ +import { Steps } from '@/app/(tabs)/user-message'; +import { ThemedText } from '@/components/ThemedText'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ActivityIndicator, Platform, TextInput, TouchableOpacity, View } from 'react-native'; +import Toast from 'react-native-toast-message'; +interface Props { + setSteps: (steps: Steps) => void; + username: string; + setUsername: (username: string) => void; +} +export default function UserName(props: Props) { + const { setSteps, username, setUsername } = props + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false) + const handleUserName = () => { + if (!username) { + if (Platform.OS === 'web') { + Toast.show({ + type: 'error', + text1: 'Username is required' + }); + } + return; + } + setIsLoading(true) + setSteps("look") + setIsLoading(false) + } + + return ( + + + {/* Input container fixed at bottom */} + + + {t('auth.userMessage.title', { ns: 'login' })} + + {t('auth.userMessage.username', { ns: 'login' })} + + + + {isLoading ? ( + + ) : ( + + {t('auth.userMessage.next', { ns: 'login' })} + + )} + + + + + ) +} diff --git a/contexts/auth-context.tsx b/contexts/auth-context.tsx index 974c6e0..532681e 100644 --- a/contexts/auth-context.tsx +++ b/contexts/auth-context.tsx @@ -3,7 +3,9 @@ import { EVENT_TYPES, eventEmitter } from '@/lib/event-util'; import { fetchApi, refreshAuthToken } from '@/lib/server-api-util'; import { store } from '@/store'; import { User } from '@/types/user'; +import * as SecureStore from 'expo-secure-store'; import React, { createContext, ReactNode, useContext, useEffect } from 'react'; +import { Platform } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; interface AuthContextType { @@ -25,7 +27,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => useEffect(() => { // 检查 Redux store 中是否已有 token const refreshTokenAction = async () => { - const token = store.getState().auth.token; + let token = store.getState().auth.token; + if (Platform.OS === 'web') { + token = localStorage.getItem('token') || ""; + } else { + await SecureStore.getItemAsync('token').then((token) => { + token = token || ""; + }) + } + if (token) { // 验证当前 token 是否有效 fetchApi('/user/identity-check', {}, false) @@ -67,6 +77,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => token: newJwt })); + // 判断运行环境是web则存在localstorage或者expo-secure-store中 + if (Platform.OS === 'web') { + localStorage.setItem('user', JSON.stringify(newUser)); + localStorage.setItem('token', newJwt); + } else { + SecureStore.setItemAsync('user', JSON.stringify(newUser)); + SecureStore.setItemAsync('token', newJwt); + } + // 触发事件通知 eventEmitter.emit(EVENT_TYPES.USER_INFO_UPDATED, newUser); }; diff --git a/features/auth/authSlice.ts b/features/auth/authSlice.ts index 2a019db..f41d553 100644 --- a/features/auth/authSlice.ts +++ b/features/auth/authSlice.ts @@ -1,12 +1,14 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { User } from '@/types/user'; - +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import * as SecureStore from 'expo-secure-store'; +import { Platform } from 'react-native'; interface AuthState { user: User | null; token: string | null; isAuthenticated: boolean; task_id: string | null; url: string | null; + refresh_token: string | null; } const initialState: AuthState = { @@ -14,7 +16,8 @@ const initialState: AuthState = { token: null, isAuthenticated: false, task_id: null, - url: null + url: null, + refresh_token: null }; export const authSlice = createSlice({ @@ -34,6 +37,13 @@ export const authSlice = createSlice({ state.user = null; state.token = null; state.isAuthenticated = false; + if (Platform.OS === 'web') { + localStorage.setItem('user', ""); + localStorage.setItem('token', ""); + } else { + SecureStore.setItemAsync('user', ""); + SecureStore.setItemAsync('token', ""); + } }, setGuestTaskData: (state, action: PayloadAction<{ task_id: string; url: string }>) => { state.task_id = action.payload.task_id; diff --git a/i18n/locales/en/login.json b/i18n/locales/en/login.json index 1f53563..75d4c9c 100644 --- a/i18n/locales/en/login.json +++ b/i18n/locales/en/login.json @@ -1,28 +1,47 @@ -{"auth": { - "telLogin": { - "sendCode": "send code", - "login": "Login", - "codePlaceholder": "Enter verification code", - "sending": "Sending...", - "codeSeconds": "seconds", - "agree": "I agree to the ", - "terms": "Terms of Service", - "and": " and ", - "privacyPolicy": "Privacy Policy", - "userAgreement": "User Agreement", - "agreement": " and authorize ", - "getPhone": " to obtain my phone number", - "codeError": "Failed to send verification code, please try again", - "phoneRequired": "Please enter your phone number", - "phoneInvalid": "Please enter a valid phone number", - "codeRequired": "Please enter the verification code", - "codeInvalid": "Invalid verification code format", - "checkedRequired": "Please agree to the terms", - "loginError": "Login failed, please try again" -}, +{ + "auth": { + "userMessage": { + "title": "Choose Username", + "username": "Username", + "usernamePlaceholder": "Enter your username", + "next": "Next", + "look": "Choose Your Look", + "avatarText": "Choose an avatar to begin your journey", + "avatorText2": "You can always change it later", + "choosePhoto": "Choose Photo", + "allDone": "All Done!" + }, + "telLogin": { + "title": "Verify Your Identity", + "secondTitle": "We’ve sent an email with your code to:", + "sendCode": "send code", + "continue": " Continue", + "login": "Login", + "codePlaceholder": "Enter verification code", + "sending": "Sending...", + "codeSeconds": "seconds", + "agree": "I agree to the ", + "terms": "Terms of Service", + "and": " and ", + "privacyPolicy": "Privacy Policy", + "userAgreement": "User Agreement", + "agreement": " and authorize ", + "getPhone": " to obtain my phone number", + "codeError": "Failed to send verification code, please try again", + "phoneRequired": "Please enter your phone number", + "phoneInvalid": "Please enter a valid phone number", + "codeRequired": "Please enter the verification code", + "codeInvalid": "Invalid verification code format", + "checkedRequired": "Please agree to the terms", + "loginError": "Login failed, please try again", + "codeVaild": "The code you entered is invalid", + "sendAgain": "Did’nt receive a code?", + "resend": "Resend", + "goBack": "Go Back" + }, "login": { "title": "Log in", - "email": "Email", + "email": "Email Address", "account": "Account", "password": "Password", "emailPlaceholder": "Enter your email", @@ -34,13 +53,15 @@ "accountPlaceholder": "Enter your account or email", "signUpMessage": "Don’t have an account?", "signUp": "Sign up", - "phoneLogin":"Phone Login" + "phoneLogin": "Phone Login", + "passwordNotMatch": "Passwords do not match" }, "agree": { - "text": "By signing up, you agree to our", + "logintext": "By logging in, you agree to our", + "singupText": "By signing up, you agree to our", "terms": " Terms", "join": "&", - "privacyPolicy": " Privacy Policy." + "privacyPolicy": " Privacy Policy." }, "welcome": { "welcome": "Welcome to MemoWake", @@ -52,8 +73,8 @@ "emailPlaceholder": "Enter your email", "sendEmailBtn": "Send email", "goback": "Go back", - "success":"Email sent successfully, please check your email", - "sendEmailBtnDisabled": "Email sent" + "success": "Email sent successfully, please check your email", + "sendEmailBtnDisabled": "Email sent" }, "resetPwd": { "title": "Reset password", @@ -83,7 +104,10 @@ "verifyCodePlaceholder": "Enter 6-digit code", "sendCode": "Send Code", "resendCode": "Resend", - "codeExpireTime": "Code will expire in" + "codeExpireTime": "Code will expire in", + "checkedRequired": "Please agree to the terms", + "emailAuth": "Please enter a valid email address", + "passwordAuth": "Please enter a valid password" } } } \ No newline at end of file diff --git a/i18n/locales/zh/login.json b/i18n/locales/zh/login.json index 8a16a78..58edc9f 100644 --- a/i18n/locales/zh/login.json +++ b/i18n/locales/zh/login.json @@ -1,26 +1,44 @@ { - "auth": { - "telLogin":{ - "sendCode":"获取验证码", - "login":"登录", - "codePlaceholder":"请输入验证码", - "sending":"发送中...", - "codeSeconds":"秒", - "agree":"我已同意", - "terms":"《服务条款》", - "and":"和", - "privacyPolicy":"《隐私政策》", - "userAgreement":"《用户协议》", - "agreement":"并授权", - "getPhone":"获得本机号码", - "codeError":"发送验证码失败,请重试", - "phoneRequired":"请输入手机号", - "phoneInvalid":"请输入正确的手机号", - "codeRequired":"请输入验证码", - "codeInvalid":"验证码格式不正确", - "checkedRequired":"请勾选协议", - "loginError":"登录失败,请重试" - }, + "auth": { + "userMessage": { + "title": "设置用户名", + "username": "用户名", + "usernamePlaceholder": "请输入您的用户名", + "next": "下一步", + "look": "选择您的头像", + "avatarText": "选择一个头像开始您的旅程", + "avatorText2": "您可以随时更改", + "choosePhoto": "选择照片", + "allDone": "完成!" + }, + "telLogin": { + "title": "验证身份", + "secondTitle": "我们已发送验证码至:", + "sendCode": "发送验证码", + "continue": "继续", + "login": "登录", + "codePlaceholder": "输入验证码", + "sending": "发送中...", + "codeSeconds": "秒", + "agree": "我已同意", + "terms": "《服务条款》", + "and": "和", + "privacyPolicy": "《隐私政策》", + "userAgreement": "《用户协议》", + "agreement": "并授权", + "getPhone": "获取本机号码", + "codeError": "验证码发送失败,请重试", + "phoneRequired": "请输入手机号", + "phoneInvalid": "请输入有效的手机号", + "codeRequired": "请输入验证码", + "codeInvalid": "验证码格式不正确", + "checkedRequired": "请同意相关条款", + "loginError": "登录失败,请重试", + "codeValid": "您输入的验证码无效", + "sendAgain": "没有收到验证码?", + "resend": "重新发送", + "goBack": "返回" + }, "login": { "title": "登录", "account": "账号", @@ -35,10 +53,12 @@ "accountPlaceholder": "请输入您的账号或邮箱", "signUpMessage": "还没有账号?", "signUp": "注册", - "phoneLogin":"手机号登录" + "phoneLogin": "手机号登录", + "passwordNotMatch": "密码不一致" }, "agree": { - "text": "注册即表示您同意我们的", + "logintext": "登录即表示您同意我们的", + "singupText": "注册即表示您同意我们的", "terms": "服务条款", "join": "&", "privacyPolicy": "隐私政策" @@ -62,7 +82,7 @@ "signupButton": "注册", "resetButton": "重置", "goback": "返回登录", - "success":"邮件已发送,请注意查收" + "success": "邮件已发送,请注意查收" }, "signup": { "title": "注册", @@ -85,7 +105,10 @@ "verifyCodePlaceholder": "请输入6位验证码", "sendCode": "发送验证码", "resendCode": "重新发送", - "codeExpireTime": "验证码将在以下时间后过期" + "codeExpireTime": "验证码将在以下时间后过期", + "checkedRequired": "请勾选协议", + "emailAuth": "请输入一个有效的邮箱地址", + "passwordAuth": "请输入一个有效的密码" } } } \ No newline at end of file diff --git a/lib/server-api-util.ts b/lib/server-api-util.ts index f365ebc..11afe0d 100644 --- a/lib/server-api-util.ts +++ b/lib/server-api-util.ts @@ -1,7 +1,11 @@ -import { useAuth } from '@/contexts/auth-context'; import { setCredentials } from '@/features/auth/authSlice'; -import { store } from '@/store'; -import { User } from '@/types/user'; +import * as SecureStore from 'expo-secure-store'; +import { Platform } from 'react-native'; +import Toast from 'react-native-toast-message'; +import { useAuth } from '../contexts/auth-context'; +import { store } from '../store'; +import { User } from '../types/user'; + // 定义错误码常量 const ERROR_CODES = { UNAUTHORIZED: 1004010001, @@ -30,7 +34,12 @@ export const useAuthToken = async(message: string | null) => { // 如果接口报错,页面弹出来错误信息 if (apiResponse.code != 0) { - console.log(message || 'Unknown error'); + if (Platform.OS === 'web') { + Toast.show({ + type: 'error', + text1: apiResponse.message || 'Request failed' + }); + } throw new Error(message || 'Unknown error'); } else { const userData = apiResponse.data as User; @@ -47,15 +56,32 @@ export const useAuthToken = async(message: string | null) => { // 使用Redux存储token的刷新token函数 export const refreshAuthToken = async(message: string | null): Promise => { try { + let cookie = ""; + let userId = ""; + if (Platform.OS === 'web') { + cookie = localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') || "")?.refresh_token || "" : ""; + userId = localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') || "")?.user_id || "" : ""; + } else { + await SecureStore.getItemAsync('user').then((user: User) => { + cookie = user?.refresh_token || ""; + userId = user?.user_id || ""; + }) + } + // 退出刷新会重新填充数据 let response; - response = await fetch(`${API_ENDPOINT}/v1/iam/access-token-refresh`); + response = await fetch(`${API_ENDPOINT}/v1/iam/access-token-refresh`, { + method: "POST", + body: JSON.stringify({ + "refresh_token": cookie, + "user_id": userId + }), + headers: { + 'Content-Type': 'application/json', + } + }); const apiResponse: ApiResponse = await response.json(); if (apiResponse.code != 0) { - // addToast({ - // title: message || 'Unknown error', - // color: "danger", - // }) throw new Error(message || 'Unknown error'); } @@ -89,11 +115,19 @@ const handleApiError = (error: unknown, needToast = true, defaultMessage = 'Unkn export const fetchApi = async ( path: string, options: RequestInit = {}, - needToast = true + needToast = true, + needToken = true, ): Promise => { const makeRequest = async (isRetry = false): Promise> => { try { - const token = store.getState().auth.token; + let token = ""; + if (Platform.OS === 'web') { + token = localStorage.getItem('token') || ""; + } else { + await SecureStore.getItemAsync('token').then((token: string) => { + token = token || ""; + }) + } const headers = new Headers(options.headers); // 添加必要的 headers @@ -101,7 +135,7 @@ export const fetchApi = async ( headers.set('Content-Type', 'application/json'); } - if (token != null) { + if (token != null && needToken) { headers.set('Authorization', `Bearer ${token}`); } @@ -121,6 +155,13 @@ export const fetchApi = async ( // 处理其他错误 if (apiResponse.code !== 0) { + // 如果是web端则显示提示 + if (Platform.OS === 'web') { + Toast.show({ + type: 'error', + text1: apiResponse.message || 'Request failed' + }); + } throw new Error(apiResponse.message || 'Request failed'); } diff --git a/metro.config.js b/metro.config.js index 37cbbed..82942a8 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,6 +1,23 @@ -const { getDefaultConfig } = require("expo/metro-config"); +const { getDefaultConfig } = require('expo/metro-config'); const { withNativeWind } = require('nativewind/metro'); +const path = require('path'); -const config = getDefaultConfig(__dirname) +const config = getDefaultConfig(__dirname); -module.exports = withNativeWind(config, { input: './global.css' }) \ No newline at end of file +// SVG 转换配置 +config.transformer = { + ...config.transformer, + babelTransformerPath: require.resolve('react-native-svg-transformer'), +}; + +config.resolver = { + ...config.resolver, + assetExts: config.resolver.assetExts.filter(ext => ext !== 'svg'), + sourceExts: [...config.resolver.sourceExts, 'svg'], + alias: { + ...config.resolver?.alias, + '@/': path.resolve(__dirname, './'), + }, +}; + +module.exports = withNativeWind(config, { input: './global.css' }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7e973e5..d7f5fee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", "@reduxjs/toolkit": "^2.8.2", + "@types/react-redux": "^7.1.34", "expo": "~53.0.12", "expo-blur": "~14.1.5", "expo-constants": "~17.1.6", @@ -39,10 +40,13 @@ "react-native": "0.79.4", "react-native-gesture-handler": "~2.24.0", "react-native-reanimated": "~3.17.4", - "react-native-safe-area-context": "^5.4.0", + "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", + "react-native-svg": "15.11.2", + "react-native-toast-message": "^2.3.0", "react-native-web": "~0.20.0", - "react-native-webview": "13.13.5" + "react-native-webview": "13.13.5", + "react-redux": "^9.2.0" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -51,6 +55,7 @@ "eslint": "^9.25.0", "eslint-config-expo": "~9.2.0", "prettier-plugin-tailwindcss": "^0.5.14", + "react-native-svg-transformer": "^1.5.1", "tailwindcss": "^3.4.17", "ts-node": "^10.9.2", "tsx": "^4.20.3", @@ -3525,16 +3530,16 @@ "license": "MIT" }, "node_modules/@react-navigation/bottom-tabs": { - "version": "7.3.16", - "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.3.16.tgz", - "integrity": "sha512-BOVtrq5J3zV3T9CeQexX20JrjECWwIgUJj0Uj0DTVuZdvPIf1AHKHDQSWcG6XqDsbqFQu9yCAfRlnt0WwdRP2w==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.2.tgz", + "integrity": "sha512-jyBux5l3qqEucY5M/ZWxVvfA8TQu7DVl2gK+xB6iKqRUfLf7dSumyVxc7HemDwGFoz3Ug8dVZFvSMEs+mfrieQ==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.4.5", + "@react-navigation/elements": "^2.5.2", "color": "^4.2.3" }, "peerDependencies": { - "@react-navigation/native": "^7.1.12", + "@react-navigation/native": "^7.1.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -3542,9 +3547,9 @@ } }, "node_modules/@react-navigation/core": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.11.0.tgz", - "integrity": "sha512-LfYPtxsMjldJ80BBeedaDCN0LE81WU1NP4V9Ia3wSrCPTAXt11y6holaBUrmUMVQVqpEyPRQrjwrT1QkfGKquw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.12.1.tgz", + "integrity": "sha512-ir6s25CDkReufi0vQhSIAe+AAHHJN9zTgGlS6iDS1yqbwgl2MiBAZzpaOL1T5llYujie2jF/bODeLz2j4k80zw==", "license": "MIT", "dependencies": { "@react-navigation/routers": "^7.4.1", @@ -3560,17 +3565,18 @@ } }, "node_modules/@react-navigation/elements": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.4.5.tgz", - "integrity": "sha512-rzoQQ07dZGA3h608imB1nAZ2rPw9vZ2xc2K36XSZoV/7IZRDxI4BCIj38Wc4saQaYhfJIoeVssK4+6IwhZBedg==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.5.2.tgz", + "integrity": "sha512-aGC3ukF5+lXuiF5bK7bJyRuWCE+Tk4MZ3GoQpAb7u7+m0KmsquliDhj4UCWEUU5kUoCeoRAUvv+1lKcYKf+WTQ==", "license": "MIT", "dependencies": { "color": "^4.2.3", - "use-latest-callback": "^0.2.4" + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", - "@react-navigation/native": "^7.1.12", + "@react-navigation/native": "^7.1.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" @@ -3582,12 +3588,12 @@ } }, "node_modules/@react-navigation/native": { - "version": "7.1.12", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.12.tgz", - "integrity": "sha512-ezHzrZN+9SE4Co6/H8MgDWlBxfJbVc5xi8szRi2QW8eJlsZsAvgGqtKs4YECraV4Yr9zW8RCzNuUxYiQiPMtEQ==", + "version": "7.1.14", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.14.tgz", + "integrity": "sha512-X233/CNx41FpshlWe4uEAUN8CNem3ju4t5pnVKcdhDR0cTQT1rK6P0ZwjSylD9zXdnHvJttFjBhKTot6TcvSqA==", "license": "MIT", "dependencies": { - "@react-navigation/core": "^7.11.0", + "@react-navigation/core": "^7.12.1", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", @@ -3599,16 +3605,16 @@ } }, "node_modules/@react-navigation/native-stack": { - "version": "7.3.17", - "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.3.17.tgz", - "integrity": "sha512-nPJv5E/7MYZ5NPD0sFP9DjSawEQ1fYXe0sCZT1C5EHGWK08p3+5HkVScXofDDqUtI/q6UU23uE1YoxVWgRbDRw==", + "version": "7.3.21", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.3.21.tgz", + "integrity": "sha512-oNNZHzkxILEibesamRKLodfXAaDOUvMBITKXLLeblDxnTAyIB/Kf7CmV+8nwkdAgV04kURTxV0SQI+d8gLUm6g==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.4.5", + "@react-navigation/elements": "^2.5.2", "warn-once": "^0.1.1" }, "peerDependencies": { - "@react-navigation/native": "^7.1.12", + "@react-navigation/native": "^7.1.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -3693,6 +3699,368 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@svgr/core/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@svgr/core/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@svgr/plugin-svgo/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3795,6 +4163,16 @@ "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", "license": "MIT" }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", + "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -3833,9 +4211,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", - "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz", + "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", "license": "MIT", "dependencies": { "undici-types": "~7.8.0" @@ -3845,12 +4223,32 @@ "version": "19.0.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.14.tgz", "integrity": "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" } }, + "node_modules/@types/react-redux": { + "version": "7.1.34", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", + "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "node_modules/@types/react-redux/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3861,9 +4259,7 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/@types/yargs": { "version": "17.0.33", @@ -3881,17 +4277,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", - "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", + "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/type-utils": "8.34.1", - "@typescript-eslint/utils": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/type-utils": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -3905,7 +4301,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.34.1", + "@typescript-eslint/parser": "^8.35.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -3921,16 +4317,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", - "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", + "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/typescript-estree": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "debug": "^4.3.4" }, "engines": { @@ -3946,14 +4342,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", - "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.34.1", - "@typescript-eslint/types": "^8.34.1", + "@typescript-eslint/tsconfig-utils": "^8.35.0", + "@typescript-eslint/types": "^8.35.0", "debug": "^4.3.4" }, "engines": { @@ -3968,14 +4364,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", - "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", + "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1" + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3986,9 +4382,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", - "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", "dev": true, "license": "MIT", "engines": { @@ -4003,14 +4399,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", - "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", + "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.34.1", - "@typescript-eslint/utils": "8.34.1", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/utils": "8.35.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -4027,9 +4423,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", - "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", + "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", "dev": true, "license": "MIT", "engines": { @@ -4041,16 +4437,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", - "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.34.1", - "@typescript-eslint/tsconfig-utils": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/visitor-keys": "8.34.1", + "@typescript-eslint/project-service": "8.35.0", + "@typescript-eslint/tsconfig-utils": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -4109,16 +4505,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", - "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", + "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.34.1", - "@typescript-eslint/types": "8.34.1", - "@typescript-eslint/typescript-estree": "8.34.1" + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4133,13 +4529,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.34.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", - "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", + "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/types": "8.35.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -4151,9 +4547,9 @@ } }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.0.tgz", - "integrity": "sha512-h1T2c2Di49ekF2TE8ZCoJkb+jwETKUIPDJ/nO3tJBKlLFPu+fyd93f0rGP/BvArKx2k2HlRM4kqkNarj3dvZlg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.2.tgz", + "integrity": "sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==", "cpu": [ "arm" ], @@ -4165,9 +4561,9 @@ ] }, "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.0.tgz", - "integrity": "sha512-sG1NHtgXtX8owEkJ11yn34vt0Xqzi3k9TJ8zppDmyG8GZV4kVWw44FHwKwHeEFl07uKPeC4ZoyuQaGh5ruJYPA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.2.tgz", + "integrity": "sha512-MffGiZULa/KmkNjHeuuflLVqfhqLv1vZLm8lWIyeADvlElJ/GLSOkoUX+5jf4/EGtfwrNFcEaB8BRas03KT0/Q==", "cpu": [ "arm64" ], @@ -4179,9 +4575,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.0.tgz", - "integrity": "sha512-nJ9z47kfFnCxN1z/oYZS7HSNsFh43y2asePzTEZpEvK7kGyuShSl3RRXnm/1QaqFL+iP+BjMwuB+DYUymOkA5A==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.2.tgz", + "integrity": "sha512-dzJYK5rohS1sYl1DHdJ3mwfwClJj5BClQnQSyAgEfggbUwA9RlROQSSbKBLqrGfsiC/VyrDPtbO8hh56fnkbsQ==", "cpu": [ "arm64" ], @@ -4193,9 +4589,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.0.tgz", - "integrity": "sha512-TK+UA1TTa0qS53rjWn7cVlEKVGz2B6JYe0C++TdQjvWYIyx83ruwh0wd4LRxYBM5HeuAzXcylA9BH2trARXJTw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.2.tgz", + "integrity": "sha512-gaIMWK+CWtXcg9gUyznkdV54LzQ90S3X3dn8zlh+QR5Xy7Y+Efqw4Rs4im61K1juy4YNb67vmJsCDAGOnIeffQ==", "cpu": [ "x64" ], @@ -4207,9 +4603,9 @@ ] }, "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.0.tgz", - "integrity": "sha512-6uZwzMRFcD7CcCd0vz3Hp+9qIL2jseE/bx3ZjaLwn8t714nYGwiE84WpaMCYjU+IQET8Vu/+BNAGtYD7BG/0yA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.2.tgz", + "integrity": "sha512-S7QpkMbVoVJb0xwHFwujnwCAEDe/596xqY603rpi/ioTn9VDgBHnCCxh+UFrr5yxuMH+dliHfjwCZJXOPJGPnw==", "cpu": [ "x64" ], @@ -4221,9 +4617,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.0.tgz", - "integrity": "sha512-bPUBksQfrgcfv2+mm+AZinaKq8LCFvt5PThYqRotqSuuZK1TVKkhbVMS/jvSRfYl7jr3AoZLYbDkItxgqMKRkg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.2.tgz", + "integrity": "sha512-+XPUMCuCCI80I46nCDFbGum0ZODP5NWGiwS3Pj8fOgsG5/ctz+/zzuBlq/WmGa+EjWZdue6CF0aWWNv84sE1uw==", "cpu": [ "arm" ], @@ -4235,9 +4631,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.0.tgz", - "integrity": "sha512-uT6E7UBIrTdCsFQ+y0tQd3g5oudmrS/hds5pbU3h4s2t/1vsGWbbSKhBSCD9mcqaqkBwoqlECpUrRJCmldl8PA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.2.tgz", + "integrity": "sha512-sqvUyAd1JUpwbz33Ce2tuTLJKM+ucSsYpPGl2vuFwZnEIg0CmdxiZ01MHQ3j6ExuRqEDUCy8yvkDKvjYFPb8Zg==", "cpu": [ "arm" ], @@ -4249,9 +4645,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.0.tgz", - "integrity": "sha512-vdqBh911wc5awE2bX2zx3eflbyv8U9xbE/jVKAm425eRoOVv/VseGZsqi3A3SykckSpF4wSROkbQPvbQFn8EsA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.2.tgz", + "integrity": "sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==", "cpu": [ "arm64" ], @@ -4263,9 +4659,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.0.tgz", - "integrity": "sha512-/8JFZ/SnuDr1lLEVsxsuVwrsGquTvT51RZGvyDB/dOK3oYK2UqeXzgeyq6Otp8FZXQcEYqJwxb9v+gtdXn03eQ==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.2.tgz", + "integrity": "sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==", "cpu": [ "arm64" ], @@ -4277,9 +4673,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.0.tgz", - "integrity": "sha512-FkJjybtrl+rajTw4loI3L6YqSOpeZfDls4SstL/5lsP2bka9TiHUjgMBjygeZEis1oC8LfJTS8FSgpKPaQx2tQ==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.2.tgz", + "integrity": "sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==", "cpu": [ "ppc64" ], @@ -4291,9 +4687,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.0.tgz", - "integrity": "sha512-w/NZfHNeDusbqSZ8r/hp8iL4S39h4+vQMc9/vvzuIKMWKppyUGKm3IST0Qv0aOZ1rzIbl9SrDeIqK86ZpUK37w==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.2.tgz", + "integrity": "sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==", "cpu": [ "riscv64" ], @@ -4305,9 +4701,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.0.tgz", - "integrity": "sha512-bEPBosut8/8KQbUixPry8zg/fOzVOWyvwzOfz0C0Rw6dp+wIBseyiHKjkcSyZKv/98edrbMknBaMNJfA/UEdqw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.2.tgz", + "integrity": "sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==", "cpu": [ "riscv64" ], @@ -4319,9 +4715,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.0.tgz", - "integrity": "sha512-LDtMT7moE3gK753gG4pc31AAqGUC86j3AplaFusc717EUGF9ZFJ356sdQzzZzkBk1XzMdxFyZ4f/i35NKM/lFA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.2.tgz", + "integrity": "sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==", "cpu": [ "s390x" ], @@ -4333,9 +4729,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.0.tgz", - "integrity": "sha512-WmFd5KINHIXj8o1mPaT8QRjA9HgSXhN1gl9Da4IZihARihEnOylu4co7i/yeaIpcfsI6sYs33cNZKyHYDh0lrA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.2.tgz", + "integrity": "sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==", "cpu": [ "x64" ], @@ -4347,9 +4743,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.0.tgz", - "integrity": "sha512-CYuXbANW+WgzVRIl8/QvZmDaZxrqvOldOwlbUjIM4pQ46FJ0W5cinJ/Ghwa/Ng1ZPMJMk1VFdsD/XwmCGIXBWg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.2.tgz", + "integrity": "sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==", "cpu": [ "x64" ], @@ -4361,9 +4757,9 @@ ] }, "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.0.tgz", - "integrity": "sha512-6Rp2WH0OoitMYR57Z6VE8Y6corX8C6QEMWLgOV6qXiJIeZ1F9WGXY/yQ8yDC4iTraotyLOeJ2Asea0urWj2fKQ==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.2.tgz", + "integrity": "sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==", "cpu": [ "wasm32" ], @@ -4378,9 +4774,9 @@ } }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.0.tgz", - "integrity": "sha512-rknkrTRuvujprrbPmGeHi8wYWxmNVlBoNW8+4XF2hXUnASOjmuC9FNF1tGbDiRQWn264q9U/oGtixyO3BT8adQ==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.2.tgz", + "integrity": "sha512-EdFbGn7o1SxGmN6aZw9wAkehZJetFPao0VGZ9OMBwKx6TkvDuj6cNeLimF/Psi6ts9lMOe+Dt6z19fZQ9Ye2fw==", "cpu": [ "arm64" ], @@ -4392,9 +4788,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.0.tgz", - "integrity": "sha512-Ceymm+iBl+bgAICtgiHyMLz6hjxmLJKqBim8tDzpX61wpZOx2bPK6Gjuor7I2RiUynVjvvkoRIkrPyMwzBzF3A==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.2.tgz", + "integrity": "sha512-JY9hi1p7AG+5c/dMU8o2kWemM8I6VZxfGwn1GCtf3c5i+IKcMo2NQ8OjZ4Z3/itvY/Si3K10jOBQn7qsD/whUA==", "cpu": [ "ia32" ], @@ -4406,9 +4802,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.0.tgz", - "integrity": "sha512-k59o9ZyeyS0hAlcaKFezYSH2agQeRFEB7KoQLXl3Nb3rgkqT1NY9Vwy+SqODiLmYnEjxWJVRE/yq2jFVqdIxZw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.2.tgz", + "integrity": "sha512-ryoo+EB19lMxAd80ln9BVf8pdOAxLb97amrQ3SFN9OCRn/5M5wvwDgAe4i8ZjhpbiHoDeP8yavcTEnpKBo7lZg==", "cpu": [ "x64" ], @@ -5150,6 +5546,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/bplist-creator": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -5194,9 +5596,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", - "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "funding": [ { "type": "opencollective", @@ -5213,8 +5615,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001718", - "electron-to-chromium": "^1.5.160", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -5385,9 +5787,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001723", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz", - "integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==", + "version": "1.0.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", "funding": [ { "type": "opencollective", @@ -5845,12 +6247,12 @@ "license": "MIT" }, "node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", "license": "MIT", "dependencies": { - "node-fetch": "^2.7.0" + "node-fetch": "^2.6.12" } }, "node_modules/cross-spawn": { @@ -5885,6 +6287,56 @@ "hyphenate-style-name": "^1.0.3" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -5897,11 +6349,46 @@ "node": ">=4" } }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -6132,6 +6619,72 @@ "node": ">=0.10.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -6187,9 +6740,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.170", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz", - "integrity": "sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==", + "version": "1.5.173", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.173.tgz", + "integrity": "sha512-2bFhXP2zqSfQHugjqJIDFVwa+qIxyNApenmXTp9EjaKtdPrES5Qcn9/aSFy/NaP2E+fWG/zxKu/LBvY36p5VNQ==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -6207,6 +6760,18 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-editor": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", @@ -6631,9 +7196,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -6677,30 +7242,30 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -7458,6 +8023,15 @@ "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", "license": "MIT" }, + "node_modules/fbjs/node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/fbjs/node_modules/promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", @@ -8173,15 +8747,6 @@ "cross-fetch": "4.0.0" } }, - "node_modules/i18next-http-backend/node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.12" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -9118,6 +9683,13 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -9611,6 +10183,16 @@ "loose-envify": "cli.js" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -9652,6 +10234,12 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -10219,6 +10807,17 @@ "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==", "license": "MIT" }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -10296,6 +10895,18 @@ "node": ">=10" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -10740,6 +11351,13 @@ "node": ">= 0.8" } }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10795,6 +11413,16 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -11018,9 +11646,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz", + "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", "dev": true, "license": "MIT", "peer": true, @@ -11598,6 +12226,48 @@ "react-native": "*" } }, + "node_modules/react-native-svg": { + "version": "15.11.2", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz", + "integrity": "sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3", + "warn-once": "0.1.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-svg-transformer": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/react-native-svg-transformer/-/react-native-svg-transformer-1.5.1.tgz", + "integrity": "sha512-dFvBNR8A9VPum9KCfh+LE49YiJEF8zUSnEFciKQroR/bEOhlPoZA0SuQ0qNk7m2iZl2w59FYjdRe0pMHWMDl0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0", + "@svgr/plugin-svgo": "^8.1.0", + "path-dirname": "^1.0.2" + }, + "peerDependencies": { + "react-native": ">=0.59.0", + "react-native-svg": ">=12.0.0" + } + }, + "node_modules/react-native-toast-message": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.3.0.tgz", + "integrity": "sha512-d7LldTK1ei1Bl7RFhoOYw8hVQ4oKPQHORYI//xR9Pyz3HxSlFlvQbueE5X3KLoemRRgBrOUg3zY6DxXnxrVLRg==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-web": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz", @@ -11723,8 +12393,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -12704,6 +13372,17 @@ "node": ">=8.0.0" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -13134,6 +13813,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/svgo/node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", @@ -13238,9 +13971,9 @@ } }, "node_modules/terser": { - "version": "5.43.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.0.tgz", - "integrity": "sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==", + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -13487,8 +14220,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.20.3", @@ -13755,38 +14487,38 @@ } }, "node_modules/unrs-resolver": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.0.tgz", - "integrity": "sha512-wqaRu4UnzBD2ABTC1kLfBjAqIDZ5YUTr/MLGa7By47JV1bJDSW7jq/ZSLigB7enLe7ubNaJhtnBXgrc/50cEhg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.2.tgz", + "integrity": "sha512-VUyWiTNQD7itdiMuJy+EuLEErLj3uwX/EpHQF8EOf33Dq3Ju6VW1GXm+swk6+1h7a49uv9fKZ+dft9jU7esdLA==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "napi-postinstall": "^0.2.2" + "napi-postinstall": "^0.2.4" }, "funding": { "url": "https://opencollective.com/unrs-resolver" }, "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.9.0", - "@unrs/resolver-binding-android-arm64": "1.9.0", - "@unrs/resolver-binding-darwin-arm64": "1.9.0", - "@unrs/resolver-binding-darwin-x64": "1.9.0", - "@unrs/resolver-binding-freebsd-x64": "1.9.0", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.0", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.0", - "@unrs/resolver-binding-linux-arm64-gnu": "1.9.0", - "@unrs/resolver-binding-linux-arm64-musl": "1.9.0", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.0", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.0", - "@unrs/resolver-binding-linux-riscv64-musl": "1.9.0", - "@unrs/resolver-binding-linux-s390x-gnu": "1.9.0", - "@unrs/resolver-binding-linux-x64-gnu": "1.9.0", - "@unrs/resolver-binding-linux-x64-musl": "1.9.0", - "@unrs/resolver-binding-wasm32-wasi": "1.9.0", - "@unrs/resolver-binding-win32-arm64-msvc": "1.9.0", - "@unrs/resolver-binding-win32-ia32-msvc": "1.9.0", - "@unrs/resolver-binding-win32-x64-msvc": "1.9.0" + "@unrs/resolver-binding-android-arm-eabi": "1.9.2", + "@unrs/resolver-binding-android-arm64": "1.9.2", + "@unrs/resolver-binding-darwin-arm64": "1.9.2", + "@unrs/resolver-binding-darwin-x64": "1.9.2", + "@unrs/resolver-binding-freebsd-x64": "1.9.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.9.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.9.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.9.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-x64-musl": "1.9.2", + "@unrs/resolver-binding-wasm32-wasi": "1.9.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.9.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.9.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.9.2" } }, "node_modules/update-browserslist-db": { diff --git a/package.json b/package.json index 9515233..edf2429 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", "@reduxjs/toolkit": "^2.8.2", + "@types/react-redux": "^7.1.34", "expo": "~53.0.12", "expo-blur": "~14.1.5", "expo-constants": "~17.1.6", @@ -28,6 +29,7 @@ "expo-linking": "~7.1.5", "expo-localization": "^16.1.5", "expo-router": "~5.1.0", + "expo-secure-store": "~14.2.3", "expo-splash-screen": "~0.30.9", "expo-status-bar": "~2.2.3", "expo-symbols": "~0.4.5", @@ -43,11 +45,13 @@ "react-native": "0.79.4", "react-native-gesture-handler": "~2.24.0", "react-native-reanimated": "~3.17.4", - "react-native-safe-area-context": "^5.4.0", + "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", + "react-native-toast-message": "^2.3.0", "react-native-web": "~0.20.0", "react-native-webview": "13.13.5", - "expo-secure-store": "~14.2.3" + "react-redux": "^9.2.0", + "react-native-svg": "15.11.2" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -56,10 +60,18 @@ "eslint": "^9.25.0", "eslint-config-expo": "~9.2.0", "prettier-plugin-tailwindcss": "^0.5.14", + "react-native-svg-transformer": "^1.5.1", "tailwindcss": "^3.4.17", "ts-node": "^10.9.2", "tsx": "^4.20.3", "typescript": "~5.8.3" }, - "private": true + "private": true, + "expo": { + "doctor": { + "reactNativeDirectoryCheck": { + "listUnknownPackages": false + } + } + } } diff --git a/provider.tsx b/provider.tsx index 1f1999e..7960a54 100644 --- a/provider.tsx +++ b/provider.tsx @@ -1,21 +1,93 @@ import { I18nextProvider } from "react-i18next"; +import { Platform } from 'react-native'; +import Toast, { BaseToast, ErrorToast, ToastConfig } from 'react-native-toast-message'; import { Provider as ReduxProvider } from "react-redux"; import { AuthProvider } from "./contexts/auth-context"; import i18n from "./i18n"; import { LanguageProvider } from "./i18n/LanguageContext"; import { store } from "./store"; +// 自定义 Toast 配置 +const toastConfig: ToastConfig = { + /* + 覆盖默认 success 类型 + - 使用自定义组件 BaseToast + - 可以添加任何 props 到组件 + */ + success: (props) => ( + + ), + /* + 覆盖默认 error 类型 + */ + error: (props) => ( + + ), + /* + 自定义 info 类型 + */ + info: (props) => ( + + ), +}; + export function Provider({ children }: { children: React.ReactNode }) { return ( + {children} + + ); } diff --git a/src/svg.d.ts b/src/svg.d.ts new file mode 100644 index 0000000..a55dc7a --- /dev/null +++ b/src/svg.d.ts @@ -0,0 +1,6 @@ +declare module '*.svg' { + import React from 'react'; + import { SvgProps } from 'react-native-svg'; + const content: React.FC; + export default content; +} \ No newline at end of file diff --git a/src/utils/toast.ts b/src/utils/toast.ts new file mode 100644 index 0000000..81ed811 --- /dev/null +++ b/src/utils/toast.ts @@ -0,0 +1,44 @@ +import Toast from 'react-native-toast-message'; + +type ToastType = 'success' | 'error' | 'info'; + +export const showToast = ( + type: ToastType, + text1: string, + text2?: string, + options: { + position?: 'top' | 'bottom'; + visibilityTime?: number; + autoHide?: boolean; + topOffset?: number; + bottomOffset?: number; + } = {} +) => { + Toast.show({ + type, + text1, + text2, + position: options.position || 'bottom', + visibilityTime: options.visibilityTime || 3000, + autoHide: options.autoHide !== false, + topOffset: options.topOffset, + bottomOffset: options.bottomOffset, + }); +}; + +// 快捷方法 +export const showSuccess = (message: string, description?: string) => { + showToast('success', message, description); +}; + +export const showError = (message: string, description?: string) => { + showToast('error', message, description); +}; + +export const showInfo = (message: string, description?: string) => { + showToast('info', message, description); +}; + +export const hideToast = () => { + Toast.hide(); +}; diff --git a/tailwind.config.js b/tailwind.config.js index 8a57075..809fa28 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -12,6 +12,12 @@ module.exports = { beige: '#F5F5DC', saddlebrown: '#8B4513', darkred: '#8B0000', + bgPrimary: '#FFB645', + textPrimary: '#AC7E35', + textSecondary: '#4C320C', + inputBackground: '#FFF8DE', + textTertiary: '#4C320C', + buttonFill: '#E2793F' }, }, }, diff --git a/tsconfig.json b/tsconfig.json index 2f35dd4..662713a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,9 @@ { "extends": "expo/tsconfig.base", + "typeRoots": ["./node_modules/@types", "./src/types"], "compilerOptions": { "strict": true, + "baseUrl": ".", "paths": { "@/*": [ "./*" diff --git a/types/user.ts b/types/user.ts index 74dcd28..8cd1317 100644 --- a/types/user.ts +++ b/types/user.ts @@ -8,4 +8,6 @@ export interface User { nickname?: string email: string user_id?: string + refresh_token?: string + avatar_file_url?: string } \ No newline at end of file