From 9985e0517f9912e1ed57d2114f4380debc247e3f Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Wed, 23 Jul 2025 14:58:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E6=9D=83=E9=99=90?= =?UTF-8?q?=E7=94=B3=E8=AF=B7=E6=8F=90=E7=A4=BA=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/owner.tsx | 7 +- app/_layout.tsx | 11 +- components/common/PermissionAlert.tsx | 111 +++++++ components/download/qrCode.tsx | 10 +- components/file-upload/files-uploader.tsx | 36 +-- components/file-upload/getTotal.tsx | 8 +- components/file-upload/images-picker.tsx | 23 +- components/owner/album.tsx | 8 +- components/owner/setting.tsx | 334 +++++++++++----------- components/owner/utils.ts | 93 ++---- context/PermissionContext.tsx | 82 ++++++ i18n/generate-imports.ts | 1 + i18n/index.ts | 8 +- i18n/locales/en/common.json | 4 +- i18n/locales/en/permission.json | 29 ++ i18n/locales/zh/common.json | 4 +- i18n/locales/zh/permission.json | 29 ++ i18n/translations-generated.ts | 12 +- lib/PermissionService.ts | 21 ++ lib/background-uploader/manual.ts | 7 +- 20 files changed, 550 insertions(+), 288 deletions(-) create mode 100644 components/common/PermissionAlert.tsx create mode 100644 context/PermissionContext.tsx create mode 100644 i18n/locales/en/permission.json create mode 100644 i18n/locales/zh/permission.json create mode 100644 lib/PermissionService.ts diff --git a/app/(tabs)/owner.tsx b/app/(tabs)/owner.tsx index fbe8411..e95ccc7 100644 --- a/app/(tabs)/owner.tsx +++ b/app/(tabs)/owner.tsx @@ -36,6 +36,7 @@ export default function OwnerPage() { // 设置弹窗 const [modalVisible, setModalVisible] = useState(false); + // 数据统计 const [countData, setCountData] = useState({} as CountData); @@ -107,8 +108,10 @@ export default function OwnerPage() { } /> - {/* 设置弹窗 */} - + {/* 设置弹窗 - 使用条件渲染避免层级冲突 */} + {modalVisible && ( + + )} {/* 导航栏 */} diff --git a/app/_layout.tsx b/app/_layout.tsx index b9af5a4..675444b 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -7,6 +7,7 @@ import { StatusBar } from 'expo-status-bar'; import { useEffect } from 'react'; import 'react-native-reanimated'; import '../global.css'; +import { PermissionProvider } from '@/context/PermissionContext'; import { Provider } from "../provider"; export default function RootLayout() { @@ -30,8 +31,9 @@ export default function RootLayout() { return ( - - + + + - - + + + ); diff --git a/components/common/PermissionAlert.tsx b/components/common/PermissionAlert.tsx new file mode 100644 index 0000000..0472d55 --- /dev/null +++ b/components/common/PermissionAlert.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Pressable, StyleSheet, Text, View } from 'react-native'; + +interface PermissionAlertProps { + visible: boolean; + onConfirm: () => void; + onCancel: () => void; + title: string; + message: string; + confirmText?: string; + cancelText?: string; +} + +const PermissionAlert: React.FC = ({ visible, onConfirm, onCancel, title, message, confirmText, cancelText }) => { + const { t } = useTranslation(); + + if (!visible) { + return null; + } + + return ( + + + + {title} + {message} + + + {cancelText || t('cancel', { ns: 'permission' })} + + + {confirmText || t('goToSettings', { ns: 'permission' })} + + + + + + ); +}; + +const styles = StyleSheet.create({ + overlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: 99, + }, + centeredView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0,0,0,0.5)', + zIndex: 9999, + }, + modalView: { + width: '80%', + backgroundColor: 'white', + borderRadius: 16, + padding: 24, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + modalTitle: { + fontSize: 20, + fontWeight: 'bold', + color: '#4C320C', + marginBottom: 12, + }, + modalMessage: { + fontSize: 16, + color: '#4C320C', + textAlign: 'center', + marginBottom: 24, + }, + buttonContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', + }, + button: { + borderRadius: 20, + paddingVertical: 12, + flex: 1, + alignItems: 'center', + }, + cancelButton: { + backgroundColor: '#F5F5F5', + marginRight: 8, + }, + confirmButton: { + backgroundColor: '#E2793F', + marginLeft: 8, + }, + buttonText: { + color: '#4C320C', + fontWeight: '600', + }, + confirmButtonText: { + color: 'white', + fontWeight: '600', + }, +}); + +export default PermissionAlert; diff --git a/components/download/qrCode.tsx b/components/download/qrCode.tsx index 440ce64..36d2531 100644 --- a/components/download/qrCode.tsx +++ b/components/download/qrCode.tsx @@ -1,7 +1,9 @@ import * as Haptics from 'expo-haptics'; import * as MediaLibrary from 'expo-media-library'; import React, { useRef } from 'react'; -import { Alert, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import i18n from '@/i18n'; +import { PermissionService } from '@/lib/PermissionService'; import QRCode from 'react-native-qrcode-svg'; import { captureRef } from 'react-native-view-shot'; @@ -17,7 +19,7 @@ export default function QRDownloadScreen(prop: { url: string }) { // 请求相册写入权限 const { status } = await MediaLibrary.requestPermissionsAsync(); if (status !== 'granted') { - Alert.alert('权限被拒绝', '需要保存图片到相册的权限'); + PermissionService.show({ title: i18n.t('permission:title.permissionDenied'), message: i18n.t('permission:message.saveToAlbumPermissionRequired') }); return; } @@ -33,10 +35,10 @@ export default function QRDownloadScreen(prop: { url: string }) { // 保存到相册 await MediaLibrary.saveToLibraryAsync(uri); - Alert.alert('✅ 成功', '二维码已保存到相册!'); + PermissionService.show({ title: i18n.t('permission:title.success'), message: i18n.t('permission:message.qrCodeSaved') }); } catch (error) { console.error('保存失败:', error); - Alert.alert('❌ 失败', '无法保存图片,请重试'); + PermissionService.show({ title: i18n.t('permission:title.error'), message: i18n.t('permission:message.saveImageFailed') }); } }; diff --git a/components/file-upload/files-uploader.tsx b/components/file-upload/files-uploader.tsx index 75aa81c..4938efa 100644 --- a/components/file-upload/files-uploader.tsx +++ b/components/file-upload/files-uploader.tsx @@ -4,7 +4,8 @@ import { uploadFileWithProgress } from '@/lib/background-uploader/uploader'; import { compressImage } from '@/lib/image-process/imageCompress'; import { createVideoThumbnailFile } from '@/lib/video-process/videoThumbnail'; import * as ImagePicker from 'expo-image-picker'; -import * as Location from 'expo-location'; +import { requestLocationPermission, requestMediaLibraryPermission } from '@/components/owner/utils'; +import { PermissionService } from '@/lib/PermissionService'; import * as MediaLibrary from 'expo-media-library'; import React, { useEffect, useState } from 'react'; import { Alert, Button, Platform, TouchableOpacity, View } from 'react-native'; @@ -26,23 +27,6 @@ export const ImagesUploader: React.FC = ({ const [files, setFiles] = useState([]); const [uploadQueue, setUploadQueue] = useState([]); - // 请求权限 - const requestPermissions = async () => { - if (Platform.OS !== 'web') { - const { status: mediaStatus } = await ImagePicker.requestMediaLibraryPermissionsAsync(); - if (mediaStatus !== 'granted') { - Alert.alert('需要媒体库权限', '请允许访问媒体库以选择图片'); - return false; - } - - const { status: locationStatus } = await Location.requestForegroundPermissionsAsync();; - if (locationStatus !== 'granted') { - Alert.alert('需要位置权限', '需要位置权限才能获取图片位置信息'); - } - } - return true; - }; - // 处理单个资源 const processSingleAsset = async (asset: ImagePicker.ImagePickerAsset): Promise => { console.log("asset111111", asset); @@ -158,7 +142,7 @@ export const ImagesUploader: React.FC = ({ try { // 统一通过 lib 的 uploadFileWithProgress 实现上传 const uploadUrlData = await getUploadUrl(task.file, { ...task.metadata, GPSVersionID: undefined }); - const taskIndex = uploadTasks.indexOf(task); + const taskIndex = uploadTasks.indexOf(task); const totalTasks = uploadTasks.length; const baseProgress = (taskIndex / totalTasks) * 100; @@ -261,9 +245,13 @@ export const ImagesUploader: React.FC = ({ const pickImage = async () => { try { setIsLoading(true); - const hasPermission = await requestPermissions(); - console.log("hasPermission", hasPermission); - if (!hasPermission) return; + const hasMediaPermission = await requestMediaLibraryPermission(); + if (!hasMediaPermission) { + setIsLoading(false); + return; + } + // 请求位置权限,但不强制要求 + await requestLocationPermission(); const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: fileType, @@ -290,13 +278,13 @@ export const ImagesUploader: React.FC = ({ } })); } catch (error) { - Alert.alert('错误', '部分文件处理失败,请重试'); + PermissionService.show({ title: '错误', message: '部分文件处理失败,请重试' }); } finally { setIsLoading(false); } } catch (error) { - Alert.alert('错误', '选择图片时出错,请重试'); + PermissionService.show({ title: '错误', message: '选择图片时出错,请重试' }); } finally { setIsLoading(false); } diff --git a/components/file-upload/getTotal.tsx b/components/file-upload/getTotal.tsx index cb2b3d4..6865841 100644 --- a/components/file-upload/getTotal.tsx +++ b/components/file-upload/getTotal.tsx @@ -1,6 +1,8 @@ import * as MediaLibrary from 'expo-media-library'; import React, { useState } from 'react'; -import { ActivityIndicator, Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import i18n from '@/i18n'; +import { PermissionService } from '@/lib/PermissionService'; interface MediaStats { total: number; @@ -46,7 +48,7 @@ const MediaStatsScreen = () => { // 1. 请求媒体库权限 const { status } = await MediaLibrary.requestPermissionsAsync(); if (status !== 'granted') { - Alert.alert('权限被拒绝', '需要访问媒体库权限来获取统计信息'); + PermissionService.show({ title: i18n.t('permission:title.permissionDenied'), message: i18n.t('permission:message.getStatsPermissionRequired') }); return; } @@ -116,7 +118,7 @@ const MediaStatsScreen = () => { setStats(stats); } catch (error) { console.error('获取媒体库统计信息失败:', error); - Alert.alert('错误', '获取媒体库统计信息失败'); + PermissionService.show({ title: i18n.t('permission:title.error'), message: i18n.t('permission:message.getStatsFailed') }); } finally { setIsLoading(false); } diff --git a/components/file-upload/images-picker.tsx b/components/file-upload/images-picker.tsx index f68b0cd..36826ff 100644 --- a/components/file-upload/images-picker.tsx +++ b/components/file-upload/images-picker.tsx @@ -2,10 +2,11 @@ import { fetchApi } from '@/lib/server-api-util'; import { ConfirmUpload, defaultExifData, ExifData, FileStatus, ImagesPickerProps, UploadResult, UploadUrlResponse } from '@/types/upload'; import * as ImageManipulator from 'expo-image-manipulator'; import * as ImagePicker from 'expo-image-picker'; -import * as Location from 'expo-location'; +import { requestLocationPermission, requestMediaLibraryPermission } from '@/components/owner/utils'; +import { PermissionService } from '@/lib/PermissionService'; import * as MediaLibrary from 'expo-media-library'; import React, { useEffect, useState } from 'react'; -import { Alert, Button, Platform, TouchableOpacity, View } from 'react-native'; +import { Button, Platform, TouchableOpacity, View } from 'react-native'; import * as Progress from 'react-native-progress'; export const ImagesPicker: React.FC = ({ @@ -24,17 +25,13 @@ export const ImagesPicker: React.FC = ({ // 请求权限 const requestPermissions = async () => { if (Platform.OS !== 'web') { - - const { status: mediaStatus } = await ImagePicker.requestMediaLibraryPermissionsAsync(); - if (mediaStatus !== 'granted') { - Alert.alert('需要媒体库权限', '请允许访问媒体库以选择图片'); + const hasMediaPermission = await requestMediaLibraryPermission(); + if (!hasMediaPermission) { + setIsLoading(false); return false; } - - const { status: locationStatus } = await Location.requestForegroundPermissionsAsync();; - if (locationStatus !== 'granted') { - Alert.alert('需要位置权限', '需要位置权限才能获取图片位置信息'); - } + // 请求位置权限,但不强制要求 + await requestLocationPermission(); } return true; }; @@ -295,10 +292,10 @@ export const ImagesPicker: React.FC = ({ throw error; // 重新抛出错误,让外层 catch 处理 } } catch (error) { - Alert.alert('错误', '处理图片时出错'); + PermissionService.show({ title: '错误', message: '处理图片时出错' }); } } catch (error) { - Alert.alert('错误', '选择图片时出错,请重试'); + PermissionService.show({ title: '错误', message: '选择图片时出错,请重试' }); } finally { setIsLoading(false); } diff --git a/components/owner/album.tsx b/components/owner/album.tsx index 972680f..e89b393 100644 --- a/components/owner/album.tsx +++ b/components/owner/album.tsx @@ -20,7 +20,13 @@ const AlbumComponent = ({ setModalVisible, style }: CategoryProps) => { {t('generalSetting.shareProfile', { ns: 'personal' })} - setModalVisible(true)} style={[styles.text, { flex: 1, alignItems: "center", paddingVertical: 6 }]}> + { + setModalVisible(true); + }} + activeOpacity={0.7} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + style={[styles.text, { flex: 1, alignItems: "center", paddingVertical: 6, zIndex: 999 }]}> diff --git a/components/owner/setting.tsx b/components/owner/setting.tsx index bda9cde..356a3b7 100644 --- a/components/owner/setting.tsx +++ b/components/owner/setting.tsx @@ -19,6 +19,7 @@ import { checkNotificationPermission, getLocationPermission, getPermissions, req const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: boolean) => void, userInfo: User }) => { const { modalVisible, setModalVisible, userInfo } = props; + const { t } = useTranslation(); const [modalType, setModalType] = useState<'ai' | 'terms' | 'privacy' | 'user'>('ai'); // 协议弹窗 @@ -36,28 +37,31 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: }; // 通知消息权限开关 const [notificationsEnabled, setNotificationsEnabled] = useState(false); - const toggleNotifications = () => { + const toggleNotifications = async () => { if (notificationsEnabled) { // 引导去设置关闭权限 openAppSettings() } else { - console.log('请求通知权限'); - requestNotificationPermission().then((res) => { - setNotificationsEnabled(res as boolean); - }) + requestNotificationPermission() + .then((granted) => { + setNotificationsEnabled(granted); + }); + setModalVisible(false); } }; // 相册权限 const [albumEnabled, setAlbumEnabled] = useState(false); - const toggleAlbum = () => { + const toggleAlbum = async () => { if (albumEnabled) { // 引导去设置关闭权限 openAppSettings() } else { - requestMediaLibraryPermission().then((res) => { - setAlbumEnabled(res as boolean); - }) + requestMediaLibraryPermission() + .then((granted) => { + setAlbumEnabled(granted); + }); + setModalVisible(false); } } @@ -66,12 +70,14 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: // 位置权限更改 const toggleLocation = async () => { if (locationEnabled) { - // 引导去设置关闭权限 - openAppSettings() + // 如果权限已开启,点击则引导用户去设置关闭 + openAppSettings(); } else { - requestLocationPermission().then((res) => { - setLocationEnabled(res as boolean); - }) + requestLocationPermission() + .then((granted) => { + setLocationEnabled(granted); + }); + setModalVisible(false); } }; // 正在获取位置信息 @@ -92,16 +98,18 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: let currentStatus = await getLocationPermission(); console.log('当前权限状态:', currentStatus); - // 2. 如果没有权限,则请求权限 + // 2. 如果没有权限,则跳过获取位置 if (!currentStatus) { - const newStatus = await requestLocationPermission(); - setLocationEnabled(newStatus); - currentStatus = newStatus; + console.log('没有权限,跳过获取位置') + return; + // const newStatus = await requestLocationPermission(); + // setLocationEnabled(newStatus); + // currentStatus = newStatus; - if (!currentStatus) { - alert('需要位置权限才能继续'); - return; - } + // if (!currentStatus) { + // // alert('需要位置权限才能继续'); + // return; + // } } // 3. 确保位置服务已启用 @@ -161,10 +169,12 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: if (modalVisible) { // 位置权限 getLocationPermission().then((res) => { + console.log('位置权限:', res); setLocationEnabled(res); }) // 媒体库权限 getPermissions().then((res) => { + console.log('媒体库权限:', res); setAlbumEnabled(res); }) // 通知权限 @@ -176,40 +186,41 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: }, [modalVisible]) return ( - { - setModalVisible(!modalVisible); - }}> - setModalVisible(false)}> + <> + { + setModalVisible(false); + }}> e.stopPropagation()}> - - Settings - {t('generalSetting.allTitle', { ns: 'personal' })} - setModalVisible(false)}> - × - - - - {/* 用户信息 */} - - {/* 升级版本 */} - {/* + style={styles.centeredView} + onPress={() => setModalVisible(false)}> + e.stopPropagation()}> + + Settings + {t('generalSetting.allTitle', { ns: 'personal' })} + setModalVisible(false)}> + × + + + + {/* 用户信息 */} + + {/* 升级版本 */} + {/* {t('generalSetting.subscription', { ns: 'personal' })} @@ -228,8 +239,8 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: */} - {/* 消息通知 */} - {/* + {/* 消息通知 */} + {/* {t('permission.pushNotification', { ns: 'personal' })} @@ -241,42 +252,42 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: /> */} - {/* 权限信息 */} - - {t('permission.permissionManagement', { ns: 'personal' })} - - {/* 相册权限 */} - - {t('permission.galleryAccess', { ns: 'personal' })} - - - {/* 分割线 */} - - {/* 位置权限 */} - - - {t('permission.locationPermission', { ns: 'personal' })} + {/* 权限信息 */} + + {t('permission.permissionManagement', { ns: 'personal' })} + + {/* 相册权限 */} + + {t('permission.galleryAccess', { ns: 'personal' })} + - - - - - - {t('permission.pushNotification', { ns: 'personal' })} + {/* 分割线 */} + + {/* 位置权限 */} + + + {t('permission.locationPermission', { ns: 'personal' })} + + - - - {/* 相册成片权限 */} - {/* + + + + {t('permission.pushNotification', { ns: 'personal' })} + + + + {/* 相册成片权限 */} + {/* Opus Permission @@ -285,10 +296,10 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: toggleSwitch={toggleAlbum} /> */} + - - {/* 账号 */} - {/* + {/* 账号 */} + {/* Account @@ -307,78 +318,75 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: */} - {/* 协议 */} - - {t('lcenses.title', { ns: 'personal' })} - - { setModalType('privacy'); setPrivacyModalVisible(true) }} > - {t('lcenses.privacyPolicy', { ns: 'personal' })} - - - - { setModalType('terms'); setPrivacyModalVisible(true) }} > - {t('lcenses.applyPermission', { ns: 'personal' })} - - - - { setModalType('user'); setPrivacyModalVisible(true) }} > - {t('lcenses.userAgreement', { ns: 'personal' })} - - - - { setModalType('ai'); setPrivacyModalVisible(true) }} > - {t('lcenses.aiPolicy', { ns: 'personal' })} - - - - { setLcensesModalVisible(true) }} > - {t('lcenses.qualification', { ns: 'personal' })} - - - - Linking.openURL("https://beian.miit.gov.cn/")} > - {t('lcenses.ICP', { ns: 'personal' })}沪ICP备2023032876号-4 - - - - - {/* 其他信息 */} - - {t('generalSetting.otherInformation', { ns: 'personal' })} - - Linking.openURL("https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd")} > - {t('generalSetting.contactUs', { ns: 'personal' })} - {/* */} - - - - {t('generalSetting.version', { ns: 'personal' })} - {"0.5.0"} + {/* 协议 */} + + {t('lcenses.title', { ns: 'personal' })} + + { setModalType('privacy'); setPrivacyModalVisible(true) }} > + {t('lcenses.privacyPolicy', { ns: 'personal' })} + + + + { setModalType('terms'); setPrivacyModalVisible(true) }} > + {t('lcenses.applyPermission', { ns: 'personal' })} + + + + { setModalType('user'); setPrivacyModalVisible(true) }} > + {t('lcenses.userAgreement', { ns: 'personal' })} + + + + { setModalType('ai'); setPrivacyModalVisible(true) }} > + {t('lcenses.aiPolicy', { ns: 'personal' })} + + + + { setLcensesModalVisible(true) }} > + {t('lcenses.qualification', { ns: 'personal' })} + + + + Linking.openURL("https://beian.miit.gov.cn/")} > + {t('lcenses.ICP', { ns: 'personal' })}沪ICP备2023032876号-4 + + - - {/* 退出 */} - - {t('generalSetting.logout', { ns: 'personal' })} - - - {/* 注销账号 */} - setDeleteModalVisible(true)}> - {t('generalSetting.deleteAccount', { ns: 'personal' })} - - - + {/* 其他信息 */} + + {t('generalSetting.otherInformation', { ns: 'personal' })} + + Linking.openURL("https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd")} > + {t('generalSetting.contactUs', { ns: 'personal' })} + {/* */} + + + + {t('generalSetting.version', { ns: 'personal' })} + {"0.5.0"} + + + + {/* 退出 */} + + {t('generalSetting.logout', { ns: 'personal' })} + + + {/* 注销账号 */} + setDeleteModalVisible(true)}> + {t('generalSetting.deleteAccount', { ns: 'personal' })} + + + + - - {/* 协议弹窗 */} - - {/* 许可证弹窗 */} - - {/* 通知 */} - {/* */} - {/* 退出登录 */} - - + + + + + + ); }; diff --git a/components/owner/utils.ts b/components/owner/utils.ts index 7059c42..cd731e1 100644 --- a/components/owner/utils.ts +++ b/components/owner/utils.ts @@ -1,10 +1,12 @@ // 地理位置逆编码 +import i18n from '@/i18n'; +import { PermissionService } from '@/lib/PermissionService'; import { fetchApi } from '@/lib/server-api-util'; import * as ImagePicker from 'expo-image-picker'; import * as Location from 'expo-location'; import * as Notifications from 'expo-notifications'; import * as SecureStore from 'expo-secure-store'; -import { Alert, Linking, Platform } from 'react-native'; +import { Linking, Platform } from 'react-native'; interface Address { id: number; @@ -69,27 +71,13 @@ export const requestLocationPermission = async () => { // 3. 如果用户之前选择了"拒绝且不再询问" if (status === 'denied' && !canAskAgain) { // 显示提示,引导用户去设置 - const openSettings = await new Promise(resolve => { - Alert.alert( - '需要位置权限', - '您之前拒绝了位置权限。要使用此功能,请在设置中启用位置权限。', - [ - { - text: '取消', - style: 'cancel', - onPress: () => resolve(false) - }, - { - text: '去设置', - onPress: () => resolve(true) - } - ] - ); + const confirmed = await PermissionService.show({ + title: i18n.t('permission:title.locationPermissionRequired'), + message: i18n.t('permission:message.locationPreviouslyDenied'), }); - if (openSettings) { - // 打开应用设置 - await Linking.openSettings(); + if (confirmed) { + openAppSettings(); } return false; } @@ -100,24 +88,25 @@ export const requestLocationPermission = async () => { console.log('新权限状态:', newStatus); if (newStatus !== 'granted') { - Alert.alert('需要位置权限', '请允许访问位置以使用此功能'); return false; } return true; } catch (error) { console.error('请求位置权限时出错:', error); - Alert.alert('错误', '请求位置权限时出错'); return false; } }; +export const openAppSettings = () => { + Linking.openSettings(); +}; + // 获取媒体库权限 export const getPermissions = async () => { if (Platform.OS !== 'web') { const { status: mediaStatus } = await ImagePicker.getMediaLibraryPermissionsAsync(); if (mediaStatus !== 'granted') { - // Alert.alert('需要媒体库权限', '请允许访问媒体库以继续'); return false; } return true; @@ -130,7 +119,6 @@ export const requestPermissions = async () => { if (Platform.OS !== 'web') { const mediaStatus = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!mediaStatus.granted) { - // Alert.alert('需要媒体库权限', '请允许访问媒体库以继续'); return false; } return true; @@ -182,20 +170,10 @@ export const requestMediaLibraryPermission = async (showAlert: boolean = true): // 3. 如果之前被拒绝且不能再次询问 if (existingStatus === 'denied' && !canAskAgain) { if (showAlert) { - const openSettings = await new Promise(resolve => { - Alert.alert( - '需要媒体库权限', - '您之前拒绝了媒体库访问权限。要选择照片,请在设置中启用媒体库权限。', - [ - { text: '取消', style: 'cancel', onPress: () => resolve(false) }, - { text: '去设置', onPress: () => resolve(true) } - ] - ); + await PermissionService.show({ + title: i18n.t('permission:title.mediaLibraryPermissionRequired'), + message: i18n.t('permission:message.mediaLibraryPreviouslyDenied'), }); - - if (openSettings) { - await Linking.openSettings(); - } } return false; } @@ -204,14 +182,20 @@ export const requestMediaLibraryPermission = async (showAlert: boolean = true): const { status: newStatus } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (newStatus !== 'granted' && showAlert) { - Alert.alert('需要媒体库权限', '请允许访问媒体库以方便后续操作'); + await PermissionService.show({ + title: i18n.t('permission:title.mediaLibraryPermissionRequired'), + message: i18n.t('permission:message.mediaLibraryPermissionRequired'), + }); } return newStatus === 'granted'; } catch (error) { console.error('请求媒体库权限时出错:', error); if (showAlert) { - Alert.alert('错误', '请求媒体库权限时出错'); + await PermissionService.show({ + title: i18n.t('permission:title.error'), + message: i18n.t('permission:message.requestPermissionError'), + }); } return false; } @@ -240,28 +224,10 @@ export const requestNotificationPermission = async () => { // 3. 如果用户之前选择了"拒绝且不再询问" if (status === 'denied' && !canAskAgain) { // 显示提示,引导用户去设置 - const openSettings = await new Promise(resolve => { - Alert.alert( - '需要通知权限', - '您之前拒绝了通知权限。要使用此功能,请在设置中启用通知权限。', - [ - { - text: '取消', - style: 'cancel', - onPress: () => resolve(false) - }, - { - text: '去设置', - onPress: () => resolve(true) - } - ] - ); + await PermissionService.show({ + title: i18n.t('permission:title.notificationPermissionRequired'), + message: i18n.t('permission:message.notificationPreviouslyDenied'), }); - - if (openSettings) { - // 打开应用设置 - await Linking.openSettings(); - } return false; } @@ -271,14 +237,17 @@ export const requestNotificationPermission = async () => { console.log('新通知权限状态:', newStatus); if (newStatus !== 'granted') { - Alert.alert('需要通知权限', '请允许通知以使用此功能'); + PermissionService.show({ + title: '需要通知权限', + message: '请允许通知以使用此功能', + }); return false; } return true; } catch (error) { console.error('请求通知权限时出错:', error); - Alert.alert('错误', '请求通知权限时出错'); + PermissionService.show({ title: '错误', message: '请求通知权限时出错' }); return false; } }; diff --git a/context/PermissionContext.tsx b/context/PermissionContext.tsx new file mode 100644 index 0000000..a1f5031 --- /dev/null +++ b/context/PermissionContext.tsx @@ -0,0 +1,82 @@ +import PermissionAlert from '@/components/common/PermissionAlert'; +import i18n from '@/i18n'; +import { PermissionService } from '@/lib/PermissionService'; +import React, { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react'; +import { Linking } from 'react-native'; + +interface PermissionAlertOptions { + title: string; + message: string; + confirmText?: string; + cancelText?: string; +} + +interface PermissionContextType { + showPermissionAlert: (options: PermissionAlertOptions) => Promise; +} + +interface AlertData { + options: PermissionAlertOptions; + resolve: (value: boolean) => void; +} + +const PermissionContext = createContext(undefined); + +export const PermissionProvider = ({ children }: { children: ReactNode }) => { + const [alertData, setAlertData] = useState(null); + + const showPermissionAlert = useCallback((options: PermissionAlertOptions) => { + return new Promise((resolve) => { + setAlertData({ options, resolve }); + }); + }, []); + + useEffect(() => { + PermissionService.set(showPermissionAlert); + + // Cleanup on unmount + return () => { + PermissionService.set(null as any); // or a no-op function + }; + }, [showPermissionAlert]); + + const handleConfirm = () => { + Linking.openSettings(); + if (alertData?.resolve) { + alertData.resolve(true); + } + setAlertData(null); + }; + + const handleCancel = () => { + if (alertData?.resolve) { + alertData.resolve(false); + } + setAlertData(null); + }; + + return ( + + {children} + {alertData && ( + + )} + + ); +}; + +export const usePermission = (): PermissionContextType => { + const context = useContext(PermissionContext); + if (!context) { + throw new Error('usePermission must be used within a PermissionProvider'); + } + return context; +}; diff --git a/i18n/generate-imports.ts b/i18n/generate-imports.ts index c01390b..2c59925 100644 --- a/i18n/generate-imports.ts +++ b/i18n/generate-imports.ts @@ -7,6 +7,7 @@ import * as path from 'path'; function generateImports() { const localesPath = path.join(__dirname, 'locales'); + const namespaces = ['common', 'home', 'login', 'settings', 'upload', 'chat', 'me', 'permission']; const languages = fs.readdirSync(localesPath); let imports = ''; let translationsMap = 'const translations = {\n'; diff --git a/i18n/index.ts b/i18n/index.ts index 2f047c4..a66e823 100644 --- a/i18n/index.ts +++ b/i18n/index.ts @@ -32,7 +32,7 @@ i18n resources: translations, // 支持命名空间 - ns: ['common', 'example', 'download'], + ns: ['common', 'example', 'download', 'permission'], defaultNS: 'common', // 设置默认语言为中文 @@ -96,14 +96,16 @@ export const preloadCommonTranslations = async () => { // 预加载 common 和 example 命名空间 await Promise.all([ loadNamespaceForLanguage(currentLng, 'common'), - loadNamespaceForLanguage(currentLng, 'example') + loadNamespaceForLanguage(currentLng, 'example'), + loadNamespaceForLanguage(currentLng, 'permission') ]); // 如果当前语言不是英语,也预加载英语作为备用 if (currentLng !== 'en') { await Promise.all([ loadNamespaceForLanguage('en', 'common'), - loadNamespaceForLanguage('en', 'example') + loadNamespaceForLanguage('en', 'example'), + loadNamespaceForLanguage('en', 'permission') ]); } }; diff --git a/i18n/locales/en/common.json b/i18n/locales/en/common.json index 2c80c49..8a8d96e 100644 --- a/i18n/locales/en/common.json +++ b/i18n/locales/en/common.json @@ -17,7 +17,9 @@ "login": "Login", "trade": "沪ICP备2025133004号-2A", "logout": "Logout", - "self": "Personal Center" + "self": "Personal Center", + "goToSettings": "Go to Settings", + "cancel": "Cancel" }, "welcome": { "welcome": "Welcome to MemoWake~", diff --git a/i18n/locales/en/permission.json b/i18n/locales/en/permission.json new file mode 100644 index 0000000..8446892 --- /dev/null +++ b/i18n/locales/en/permission.json @@ -0,0 +1,29 @@ +{ + "title": { + "permissionDenied": "Permission Denied", + "locationPermissionRequired": "Location Permission Required", + "mediaLibraryPermissionRequired": "Media Library Permission Required", + "notificationPermissionRequired": "Notification Permission Required", + "success": "✅ Success", + "error": "❌ Error", + "getMediaFailed": "Failed to Get Media" + }, + "message": { + "locationPreviouslyDenied": "You have previously denied location permissions. To use this feature, please enable it in settings.", + "mediaLibraryPreviouslyDenied": "You have previously denied media library permissions. To use this feature, please enable it in settings.", + "notificationPreviouslyDenied": "You have previously denied notification permissions. To use this feature, please enable it in settings.", + "saveToAlbumPermissionRequired": "Permission is required to save images to the album.", + "qrCodeSaved": "QR code has been saved to the album!", + "saveImageFailed": "Failed to save the image, please try again.", + "getStatsPermissionRequired": "Permission to access the media library is required to get statistics.", + "getStatsFailed": "Failed to get media library statistics.", + "noMediaFound": "Could not retrieve any media. Please check permissions or your media library.", + "uploadError": "An error occurred during the upload process." + }, + "button": { + "cancel": "Cancel", + "goToSettings": "Go to Settings", + "ok": "OK", + "confirm": "Go to Settings" + } +} diff --git a/i18n/locales/zh/common.json b/i18n/locales/zh/common.json index be43b74..9f4c00f 100644 --- a/i18n/locales/zh/common.json +++ b/i18n/locales/zh/common.json @@ -17,7 +17,9 @@ "login": "登录", "trade": "沪ICP备2025133004号-2A", "logout": "退出登录", - "self": "个人中心" + "self": "个人中心", + "goToSettings": "去设置", + "cancel": "取消" }, "welcome": { "welcome": "欢迎来到 MemoWake~", diff --git a/i18n/locales/zh/permission.json b/i18n/locales/zh/permission.json new file mode 100644 index 0000000..70d0b33 --- /dev/null +++ b/i18n/locales/zh/permission.json @@ -0,0 +1,29 @@ +{ + "title": { + "permissionDenied": "权限被拒绝", + "locationPermissionRequired": "需要位置权限", + "mediaLibraryPermissionRequired": "需要媒体库权限", + "notificationPermissionRequired": "需要通知权限", + "success": "✅ 成功", + "error": "❌ 失败", + "getMediaFailed": "获取媒体资源失败" + }, + "message": { + "locationPreviouslyDenied": "您之前拒绝了位置权限。要使用此功能,请在设置中启用位置权限。", + "mediaLibraryPreviouslyDenied": "您之前拒绝了媒体库权限。要使用此功能,请在设置中启用它。", + "notificationPreviouslyDenied": "您之前拒绝了通知权限。要使用此功能,请在设置中启用它。", + "saveToAlbumPermissionRequired": "需要保存图片到相册的权限", + "qrCodeSaved": "二维码已保存到相册!", + "saveImageFailed": "无法保存图片,请重试", + "getStatsPermissionRequired": "需要访问媒体库权限来获取统计信息", + "getStatsFailed": "获取媒体库统计信息失败", + "noMediaFound": "未能获取到任何媒体资源,请检查权限或媒体库。", + "uploadError": "上传过程中出现错误。" + }, + "button": { + "cancel": "取消", + "goToSettings": "去设置", + "ok": "好的", + "confirm": "去设置" + } +} diff --git a/i18n/translations-generated.ts b/i18n/translations-generated.ts index 68a331f..650a3b3 100644 --- a/i18n/translations-generated.ts +++ b/i18n/translations-generated.ts @@ -4,23 +4,25 @@ import enAdmin from './locales/en/admin.json'; import enAsk from './locales/en/ask.json'; import enCommon from './locales/en/common.json'; import enDownload from './locales/en/download.json'; -import enSupport from './locales/en/support.json'; import enExample from './locales/en/example.json'; import enFairclip from './locales/en/fairclip.json'; import enLanding from './locales/en/landing.json'; import enLogin from './locales/en/login.json'; +import enPermission from './locales/en/permission.json'; import enPersonal from './locales/en/personal.json'; +import enSupport from './locales/en/support.json'; import enUpload from './locales/en/upload.json'; import zhAdmin from './locales/zh/admin.json'; import zhAsk from './locales/zh/ask.json'; import zhCommon from './locales/zh/common.json'; import zhDownload from './locales/zh/download.json'; -import zhSupport from './locales/zh/support.json'; import zhExample from './locales/zh/example.json'; import zhFairclip from './locales/zh/fairclip.json'; import zhLanding from './locales/zh/landing.json'; import zhLogin from './locales/zh/login.json'; +import zhPermission from './locales/zh/permission.json'; import zhPersonal from './locales/zh/personal.json'; +import zhSupport from './locales/zh/support.json'; import zhUpload from './locales/zh/upload.json'; const translations = { @@ -29,12 +31,13 @@ const translations = { ask: enAsk, common: enCommon, download: enDownload, - support: enSupport, example: enExample, fairclip: enFairclip, landing: enLanding, login: enLogin, + permission: enPermission, personal: enPersonal, + support: enSupport, upload: enUpload }, zh: { @@ -42,12 +45,13 @@ const translations = { ask: zhAsk, common: zhCommon, download: zhDownload, - support: zhSupport, example: zhExample, fairclip: zhFairclip, landing: zhLanding, login: zhLogin, + permission: zhPermission, personal: zhPersonal, + support: zhSupport, upload: zhUpload }, }; diff --git a/lib/PermissionService.ts b/lib/PermissionService.ts new file mode 100644 index 0000000..2f42491 --- /dev/null +++ b/lib/PermissionService.ts @@ -0,0 +1,21 @@ +interface PermissionAlertOptions { + title: string; + message: string; +} + +type ShowPermissionAlertFunction = (options: PermissionAlertOptions) => Promise; + +let showPermissionAlertRef: ShowPermissionAlertFunction | null = null; + +export const PermissionService = { + set: (fn: ShowPermissionAlertFunction) => { + showPermissionAlertRef = fn; + }, + show: (options: PermissionAlertOptions): Promise => { + if (!showPermissionAlertRef) { + console.error("PermissionAlert has not been set. Please ensure PermissionProvider is used at the root of your app."); + return Promise.resolve(false); + } + return showPermissionAlertRef(options); + }, +}; diff --git a/lib/background-uploader/manual.ts b/lib/background-uploader/manual.ts index d33f406..b3b76f0 100644 --- a/lib/background-uploader/manual.ts +++ b/lib/background-uploader/manual.ts @@ -1,5 +1,6 @@ import pLimit from 'p-limit'; -import { Alert } from 'react-native'; +import { PermissionService } from '../PermissionService'; +import i18n from '@/i18n'; import { getUploadTaskStatus, insertUploadTask } from '../db'; import { getMediaByDateRange } from './media'; import { ExtendedAsset } from './types'; @@ -24,7 +25,7 @@ export const triggerManualUpload = async ( try { const media = await getMediaByDateRange(startDate, endDate); if (media.length === 0) { - Alert.alert('提示', '在指定时间范围内未找到媒体文件'); + PermissionService.show({ title: i18n.t('permission:title.getMediaFailed'), message: i18n.t('permission:message.noMediaFound') }); return []; } @@ -76,7 +77,7 @@ export const triggerManualUpload = async ( return finalResults; } catch (error) { console.error('手动上传过程中出现错误:', error); - Alert.alert('错误', '上传过程中出现错误'); + PermissionService.show({ title: i18n.t('permission:title.error'), message: i18n.t('permission:message.uploadError') }); throw error; } }; \ No newline at end of file