333 lines
11 KiB
TypeScript
333 lines
11 KiB
TypeScript
import * as MediaLibrary from 'expo-media-library';
|
||
import React, { useState } from 'react';
|
||
import { ActivityIndicator, Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||
|
||
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') {
|
||
Alert.alert('权限被拒绝', '需要访问媒体库权限来获取统计信息');
|
||
return;
|
||
}
|
||
|
||
// 2. 设置时间范围
|
||
const dateRange = getDateRange(timeRange);
|
||
const createdAfter = dateRange ? Math.floor(dateRange.start.getTime() / 1000) : 0;
|
||
const endTime = dateRange?.end ? Math.floor(dateRange.end.getTime() / 1000) : undefined;
|
||
|
||
// 3. 分页获取媒体资源,每次10条
|
||
let hasNextPage = true;
|
||
let after = undefined;
|
||
let allAssets: MediaLibrary.Asset[] = [];
|
||
const pageSize = 10; // 每次获取10条
|
||
|
||
while (hasNextPage) {
|
||
const media = await MediaLibrary.getAssetsAsync({
|
||
first: pageSize,
|
||
after,
|
||
mediaType: ['photo', 'video', 'audio', 'unknown'],
|
||
sortBy: 'creationTime', // 按创建时间降序,最新的在前面
|
||
createdAfter: Date.now() - 24 * 30 * 12 * 60 * 60 * 1000, // 时间戳(毫秒)
|
||
createdBefore: Date.now(), // 时间戳(毫秒)
|
||
});
|
||
|
||
// 如果没有数据,直接退出
|
||
if (media.assets.length === 0) {
|
||
break;
|
||
}
|
||
|
||
// 检查每条记录是否在时间范围内
|
||
for (const asset of media.assets) {
|
||
const assetTime = asset.creationTime ? new Date(asset.creationTime).getTime() / 1000 : 0;
|
||
|
||
// 如果设置了结束时间,并且当前记录的时间早于开始时间,则停止
|
||
if (endTime && assetTime > endTime) {
|
||
continue; // 跳过这条记录
|
||
}
|
||
|
||
// 如果设置了开始时间,并且当前记录的时间早于开始时间,则停止
|
||
if (createdAfter && assetTime < createdAfter) {
|
||
hasNextPage = false;
|
||
break;
|
||
}
|
||
|
||
allAssets.push(asset);
|
||
}
|
||
|
||
// 更新游标和是否还有下一页
|
||
hasNextPage = media.hasNextPage && media.assets.length === pageSize;
|
||
after = media.endCursor;
|
||
|
||
// 如果没有更多数据或者已经获取了足够的数据
|
||
if (!hasNextPage || allAssets.length >= 1000) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
console.log(`总共获取到 ${allAssets.length} 个媒体文件`);
|
||
|
||
// 4. 统计不同类型媒体的数量
|
||
const stats: MediaStats = {
|
||
total: allAssets.length,
|
||
photos: 0,
|
||
videos: 0,
|
||
audios: 0,
|
||
others: 0,
|
||
byMonth: {},
|
||
};
|
||
|
||
allAssets.forEach(asset => {
|
||
// 统计类型
|
||
if (asset.mediaType === 'photo') stats.photos++;
|
||
else if (asset.mediaType === 'video') stats.videos++;
|
||
else if (asset.mediaType === 'audio') stats.audios++;
|
||
else stats.others++;
|
||
|
||
// 按月份统计
|
||
if (asset.creationTime) {
|
||
const date = new Date(asset.creationTime);
|
||
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||
stats.byMonth[monthKey] = (stats.byMonth[monthKey] || 0) + 1;
|
||
}
|
||
});
|
||
|
||
setStats(stats);
|
||
} catch (error) {
|
||
console.error('获取媒体库统计信息失败:', error);
|
||
Alert.alert('错误', '获取媒体库统计信息失败');
|
||
} 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; |