feat: 统一权限申请提示组件
This commit is contained in:
parent
4a052844e9
commit
9985e0517f
@ -36,6 +36,7 @@ export default function OwnerPage() {
|
||||
// 设置弹窗
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
|
||||
|
||||
// 数据统计
|
||||
const [countData, setCountData] = useState<CountData>({} as CountData);
|
||||
|
||||
@ -107,8 +108,10 @@ export default function OwnerPage() {
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
{/* 设置弹窗 */}
|
||||
{/* 设置弹窗 - 使用条件渲染避免层级冲突 */}
|
||||
{modalVisible && (
|
||||
<SettingModal modalVisible={modalVisible} setModalVisible={setModalVisible} userInfo={userInfoDetails.user_info} />
|
||||
)}
|
||||
|
||||
{/* 导航栏 */}
|
||||
<AskNavbar />
|
||||
|
||||
@ -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,6 +31,7 @@ export default function RootLayout() {
|
||||
|
||||
return (
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<PermissionProvider>
|
||||
<Provider>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
@ -43,6 +45,7 @@ export default function RootLayout() {
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
</Provider>
|
||||
</PermissionProvider>
|
||||
<StatusBar style="auto" />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
111
components/common/PermissionAlert.tsx
Normal file
111
components/common/PermissionAlert.tsx
Normal file
@ -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<PermissionAlertProps> = ({ visible, onConfirm, onCancel, title, message, confirmText, cancelText }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.centeredView}>
|
||||
<View style={styles.modalView}>
|
||||
<Text style={styles.modalTitle}>{title}</Text>
|
||||
<Text style={styles.modalMessage}>{message}</Text>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Pressable style={[styles.button, styles.cancelButton]} onPress={onCancel}>
|
||||
<Text style={styles.buttonText}>{cancelText || t('cancel', { ns: 'permission' })}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={[styles.button, styles.confirmButton]} onPress={onConfirm}>
|
||||
<Text style={styles.confirmButtonText}>{confirmText || t('goToSettings', { ns: 'permission' })}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@ -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') });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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<ImagesuploaderProps> = ({
|
||||
const [files, setFiles] = useState<FileUploadItem[]>([]);
|
||||
const [uploadQueue, setUploadQueue] = useState<FileUploadItem[]>([]);
|
||||
|
||||
// 请求权限
|
||||
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<UploadResult | null> => {
|
||||
console.log("asset111111", asset);
|
||||
@ -261,9 +245,13 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
|
||||
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<ImagesuploaderProps> = ({
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '部分文件处理失败,请重试');
|
||||
PermissionService.show({ title: '错误', message: '部分文件处理失败,请重试' });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '选择图片时出错,请重试');
|
||||
PermissionService.show({ title: '错误', message: '选择图片时出错,请重试' });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<ImagesPickerProps> = ({
|
||||
@ -24,17 +25,13 @@ export const ImagesPicker: React.FC<ImagesPickerProps> = ({
|
||||
// 请求权限
|
||||
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<ImagesPickerProps> = ({
|
||||
throw error; // 重新抛出错误,让外层 catch 处理
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '处理图片时出错');
|
||||
PermissionService.show({ title: '错误', message: '处理图片时出错' });
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '选择图片时出错,请重试');
|
||||
PermissionService.show({ title: '错误', message: '选择图片时出错,请重试' });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@ -20,7 +20,13 @@ const AlbumComponent = ({ setModalVisible, style }: CategoryProps) => {
|
||||
<TouchableOpacity style={{ flex: 3 }}>
|
||||
<ThemedText style={styles.text}>{t('generalSetting.shareProfile', { ns: 'personal' })}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setModalVisible(true)} style={[styles.text, { flex: 1, alignItems: "center", paddingVertical: 6 }]}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
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 }]}>
|
||||
<SettingSvg />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@ -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;
|
||||
|
||||
if (!currentStatus) {
|
||||
alert('需要位置权限才能继续');
|
||||
console.log('没有权限,跳过获取位置')
|
||||
return;
|
||||
}
|
||||
// const newStatus = await requestLocationPermission();
|
||||
// setLocationEnabled(newStatus);
|
||||
// currentStatus = newStatus;
|
||||
|
||||
// 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,12 +186,13 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible:
|
||||
}, [modalVisible])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
visible={modalVisible}
|
||||
onRequestClose={() => {
|
||||
setModalVisible(!modalVisible);
|
||||
setModalVisible(false);
|
||||
}}>
|
||||
<Pressable
|
||||
style={styles.centeredView}
|
||||
@ -370,15 +381,12 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible:
|
||||
</ScrollView>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
{/* 协议弹窗 */}
|
||||
|
||||
<PrivacyModal modalVisible={privacyModalVisible} setModalVisible={setPrivacyModalVisible} type={modalType} />
|
||||
{/* 许可证弹窗 */}
|
||||
<LcensesModal modalVisible={lcensesModalVisible} setModalVisible={setLcensesModalVisible} />
|
||||
{/* 通知 */}
|
||||
{/* <AuthNotifications setNotificationsEnabled={setNotificationsEnabled} notificationsEnabled={notificationsEnabled} /> */}
|
||||
{/* 退出登录 */}
|
||||
<DeleteModal modalVisible={deleteModalVisible} setModalVisible={setDeleteModalVisible} setSettingModalVisible={setModalVisible} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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<boolean>(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;
|
||||
}
|
||||
};
|
||||
|
||||
82
context/PermissionContext.tsx
Normal file
82
context/PermissionContext.tsx
Normal file
@ -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<boolean>;
|
||||
}
|
||||
|
||||
interface AlertData {
|
||||
options: PermissionAlertOptions;
|
||||
resolve: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const PermissionContext = createContext<PermissionContextType | undefined>(undefined);
|
||||
|
||||
export const PermissionProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [alertData, setAlertData] = useState<AlertData | null>(null);
|
||||
|
||||
const showPermissionAlert = useCallback((options: PermissionAlertOptions) => {
|
||||
return new Promise<boolean>((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 (
|
||||
<PermissionContext.Provider value={{ showPermissionAlert }}>
|
||||
{children}
|
||||
{alertData && (
|
||||
<PermissionAlert
|
||||
visible={!!alertData}
|
||||
title={alertData.options.title}
|
||||
message={alertData.options.message}
|
||||
confirmText={alertData.options.confirmText || i18n.t('permission:button.confirm')}
|
||||
cancelText={alertData.options.cancelText || i18n.t('permission:button.cancel')}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
</PermissionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePermission = (): PermissionContextType => {
|
||||
const context = useContext(PermissionContext);
|
||||
if (!context) {
|
||||
throw new Error('usePermission must be used within a PermissionProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@ -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';
|
||||
|
||||
@ -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')
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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~",
|
||||
|
||||
29
i18n/locales/en/permission.json
Normal file
29
i18n/locales/en/permission.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
@ -17,7 +17,9 @@
|
||||
"login": "登录",
|
||||
"trade": "沪ICP备2025133004号-2A",
|
||||
"logout": "退出登录",
|
||||
"self": "个人中心"
|
||||
"self": "个人中心",
|
||||
"goToSettings": "去设置",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"welcome": {
|
||||
"welcome": "欢迎来到 MemoWake~",
|
||||
|
||||
29
i18n/locales/zh/permission.json
Normal file
29
i18n/locales/zh/permission.json
Normal file
@ -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": "去设置"
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
},
|
||||
};
|
||||
|
||||
21
lib/PermissionService.ts
Normal file
21
lib/PermissionService.ts
Normal file
@ -0,0 +1,21 @@
|
||||
interface PermissionAlertOptions {
|
||||
title: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
type ShowPermissionAlertFunction = (options: PermissionAlertOptions) => Promise<boolean>;
|
||||
|
||||
let showPermissionAlertRef: ShowPermissionAlertFunction | null = null;
|
||||
|
||||
export const PermissionService = {
|
||||
set: (fn: ShowPermissionAlertFunction) => {
|
||||
showPermissionAlertRef = fn;
|
||||
},
|
||||
show: (options: PermissionAlertOptions): Promise<boolean> => {
|
||||
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);
|
||||
},
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user