317 lines
9.9 KiB
TypeScript
317 lines
9.9 KiB
TypeScript
import * as MediaLibrary from 'expo-media-library';
|
||
import React, { useState } from 'react';
|
||
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||
import i18n from '@/i18n';
|
||
import { PermissionService } from '@/lib/PermissionService';
|
||
|
||
interface MediaStats {
|
||
total: number;
|
||
photos: number;
|
||
videos: number;
|
||
audios: number;
|
||
others: number;
|
||
byMonth: Record<string, number>;
|
||
}
|
||
|
||
type TimeRange = 'day' | 'week' | 'month' | 'all';
|
||
|
||
const MediaStatsScreen = () => {
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [stats, setStats] = useState<MediaStats | null>(null);
|
||
const [timeRange, setTimeRange] = useState<TimeRange>('week'); // 默认显示一周
|
||
|
||
const getDateRange = (range: TimeRange) => {
|
||
const now = new Date();
|
||
const start = new Date(now);
|
||
|
||
switch (range) {
|
||
case 'day':
|
||
start.setDate(now.getDate() - 1);
|
||
break;
|
||
case 'week':
|
||
start.setDate(now.getDate() - 7);
|
||
break;
|
||
case 'month':
|
||
start.setMonth(now.getMonth() - 1);
|
||
break;
|
||
case 'all':
|
||
default:
|
||
return null; // 返回 null 表示不限制时间范围
|
||
}
|
||
|
||
return { start, end: now };
|
||
};
|
||
|
||
const getMediaStatistics = async () => {
|
||
setIsLoading(true);
|
||
try {
|
||
// 1. 请求媒体库权限
|
||
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||
if (status !== 'granted') {
|
||
PermissionService.show({ title: i18n.t('permission:title.permissionDenied'), message: i18n.t('permission:message.getStatsPermissionRequired') });
|
||
return;
|
||
}
|
||
|
||
// 2. 设置时间范围,直接使用Date对象
|
||
const dateRange = getDateRange(timeRange);
|
||
|
||
// 3. 分页获取媒体资源
|
||
let allAssets: MediaLibrary.Asset[] = [];
|
||
let hasNextPage = true;
|
||
let after: MediaLibrary.AssetRef | undefined = undefined;
|
||
const pageSize = 100; // 增加每次获取的数量以提高效率
|
||
|
||
while (hasNextPage) {
|
||
const media = await MediaLibrary.getAssetsAsync({
|
||
first: pageSize,
|
||
after,
|
||
sortBy: ['creationTime'],
|
||
mediaType: ['photo', 'video', 'audio'],
|
||
createdAfter: dateRange?.start,
|
||
createdBefore: dateRange?.end,
|
||
});
|
||
|
||
if (media.assets.length > 0) {
|
||
allAssets.push(...media.assets);
|
||
}
|
||
|
||
hasNextPage = media.hasNextPage;
|
||
after = media.endCursor;
|
||
|
||
// 可选:增加一个最大获取上限,防止无限循环
|
||
if (allAssets.length > 2000) {
|
||
console.warn('已达到2000个媒体文件的上限');
|
||
break;
|
||
}
|
||
}
|
||
|
||
console.log(`总共获取到 ${allAssets.length} 个媒体文件`);
|
||
|
||
// 4. 使用 reduce 进行统计,更高效
|
||
const stats = allAssets.reduce<MediaStats>((acc, asset) => {
|
||
acc.total++;
|
||
switch (asset.mediaType) {
|
||
case 'photo':
|
||
acc.photos++;
|
||
break;
|
||
case 'video':
|
||
acc.videos++;
|
||
break;
|
||
case 'audio':
|
||
acc.audios++;
|
||
break;
|
||
default:
|
||
acc.others++;
|
||
break;
|
||
}
|
||
|
||
if (asset.creationTime) {
|
||
const date = new Date(asset.creationTime);
|
||
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||
acc.byMonth[monthKey] = (acc.byMonth[monthKey] || 0) + 1;
|
||
}
|
||
return acc;
|
||
}, {
|
||
total: 0, photos: 0, videos: 0, audios: 0, others: 0, byMonth: {},
|
||
});
|
||
|
||
setStats(stats);
|
||
} catch (error) {
|
||
console.error('获取媒体库统计信息失败:', error);
|
||
PermissionService.show({ title: i18n.t('permission:title.error'), message: i18n.t('permission:message.getStatsFailed') });
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
// 时间范围选择器
|
||
const TimeRangeSelector = () => (
|
||
<View style={styles.timeRangeContainer}>
|
||
{(['day', 'week', 'month', 'all'] as TimeRange[]).map((range) => (
|
||
<TouchableOpacity
|
||
key={range}
|
||
style={[
|
||
styles.timeRangeButton,
|
||
timeRange === range && styles.timeRangeButtonActive
|
||
]}
|
||
onPress={() => {
|
||
setTimeRange(range);
|
||
}}
|
||
disabled={isLoading}
|
||
>
|
||
<Text style={[
|
||
styles.timeRangeButtonText,
|
||
timeRange === range && styles.timeRangeButtonTextActive
|
||
]}>
|
||
{{
|
||
day: '今天',
|
||
week: '本周',
|
||
month: '本月',
|
||
all: '全部'
|
||
}[range]}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
);
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<View style={styles.header}>
|
||
<Text style={styles.title}>媒体库统计</Text>
|
||
</View>
|
||
|
||
<TimeRangeSelector />
|
||
|
||
<TouchableOpacity
|
||
style={[styles.button, isLoading && styles.buttonDisabled]}
|
||
onPress={getMediaStatistics}
|
||
disabled={isLoading}
|
||
>
|
||
{isLoading ? (
|
||
<ActivityIndicator color="#fff" />
|
||
) : (
|
||
<Text style={styles.buttonText}>获取媒体库统计</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
|
||
{stats && (
|
||
<View style={styles.statsContainer}>
|
||
<View style={styles.statsRow}>
|
||
<StatItem label="总文件数" value={stats.total.toString()} />
|
||
<StatItem label="照片" value={stats.photos.toString()} />
|
||
</View>
|
||
<View style={styles.statsRow}>
|
||
<StatItem label="视频" value={stats.videos.toString()} />
|
||
<StatItem label="音频" value={stats.audios.toString()} />
|
||
<StatItem label="其他" value={stats.others.toString()} />
|
||
</View>
|
||
|
||
<View style={styles.monthlyContainer}>
|
||
<Text style={styles.sectionTitle}>按月统计</Text>
|
||
{Object.entries(stats.byMonth)
|
||
.sort(([a], [b]) => b.localeCompare(a))
|
||
.map(([month, count]) => (
|
||
<View key={month} style={styles.monthlyItem}>
|
||
<Text style={styles.monthText}>{month}</Text>
|
||
<Text style={styles.countText}>{count} 个文件</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
)}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
const StatItem = ({ label, value }: { label: string; value: string }) => (
|
||
<View style={styles.statItem}>
|
||
<Text style={styles.statValue}>{value}</Text>
|
||
<Text style={styles.statLabel}>{label}</Text>
|
||
</View>
|
||
);
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: '#fff',
|
||
padding: 16,
|
||
},
|
||
header: {
|
||
marginBottom: 16,
|
||
alignItems: 'center',
|
||
},
|
||
title: {
|
||
fontSize: 20,
|
||
fontWeight: 'bold',
|
||
color: '#333',
|
||
},
|
||
timeRangeContainer: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
marginBottom: 16,
|
||
paddingHorizontal: 8,
|
||
},
|
||
timeRangeButton: {
|
||
paddingVertical: 8,
|
||
paddingHorizontal: 16,
|
||
borderRadius: 16,
|
||
backgroundColor: '#f0f0f0',
|
||
},
|
||
timeRangeButtonActive: {
|
||
backgroundColor: '#007AFF',
|
||
},
|
||
timeRangeButtonText: {
|
||
color: '#666',
|
||
fontSize: 14,
|
||
},
|
||
timeRangeButtonTextActive: {
|
||
color: '#fff',
|
||
fontWeight: '600',
|
||
},
|
||
button: {
|
||
backgroundColor: '#007AFF',
|
||
paddingVertical: 12,
|
||
borderRadius: 8,
|
||
alignItems: 'center',
|
||
marginBottom: 20,
|
||
},
|
||
buttonDisabled: {
|
||
opacity: 0.6,
|
||
},
|
||
buttonText: {
|
||
color: '#fff',
|
||
fontSize: 16,
|
||
fontWeight: '600',
|
||
},
|
||
statsContainer: {
|
||
backgroundColor: '#f8f8f8',
|
||
borderRadius: 12,
|
||
padding: 16,
|
||
},
|
||
statsRow: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
marginBottom: 16,
|
||
},
|
||
statItem: {
|
||
alignItems: 'center',
|
||
flex: 1,
|
||
},
|
||
statValue: {
|
||
fontSize: 20,
|
||
fontWeight: 'bold',
|
||
color: '#007AFF',
|
||
marginBottom: 4,
|
||
},
|
||
statLabel: {
|
||
fontSize: 12,
|
||
color: '#666',
|
||
},
|
||
monthlyContainer: {
|
||
marginTop: 16,
|
||
},
|
||
sectionTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '600',
|
||
color: '#333',
|
||
marginBottom: 12,
|
||
},
|
||
monthlyItem: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
paddingVertical: 10,
|
||
borderBottomWidth: 1,
|
||
borderBottomColor: '#eee',
|
||
},
|
||
monthText: {
|
||
fontSize: 14,
|
||
color: '#333',
|
||
},
|
||
countText: {
|
||
fontSize: 14,
|
||
color: '#666',
|
||
},
|
||
});
|
||
|
||
export default MediaStatsScreen; |