406 lines
11 KiB
JavaScript
406 lines
11 KiB
JavaScript
import { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
View, Text, FlatList, StyleSheet, TouchableOpacity,
|
|
Alert, ActivityIndicator, RefreshControl,
|
|
ScrollView, Platform, BackHandler
|
|
} from 'react-native';
|
|
import { StatusBar } from 'expo-status-bar';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import RepoItem from '../components/RepoItem';
|
|
import { fetchStarredRepos, TokenExpiredError } from '../services/github';
|
|
import {
|
|
initDatabase, getAllCategories, saveRepos, getAllRepos, getReposByCategory,
|
|
getUncategorizedRepos, getGitHubToken, batchSetRepoCategories
|
|
} from '../services/database';
|
|
import { runAutoCategorize } from '../services/categorizer';
|
|
|
|
// 首页:仓库列表、分类标签栏、同步入口
|
|
export default function HomeScreen({ onTokenExpired, onOpenSettings, onOpenRepoDetail, onOpenCategoryManage }) {
|
|
const [categories, setCategories] = useState([]);
|
|
// selectedCategory: null=全部, 0=未分类, >0=具体分类 ID
|
|
const [selectedCategory, setSelectedCategory] = useState(null);
|
|
const [repos, setRepos] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [syncing, setSyncing] = useState(false);
|
|
const [syncInfo, setSyncInfo] = useState('');
|
|
|
|
// 按分类加载仓库列表
|
|
const loadRepos = useCallback(async (catId) => {
|
|
if (catId === null || catId === undefined) {
|
|
const allRepos = await getAllRepos();
|
|
setRepos(allRepos);
|
|
} else if (catId === 0) {
|
|
const uncatRepos = await getUncategorizedRepos();
|
|
setRepos(uncatRepos);
|
|
} else {
|
|
const catRepos = await getReposByCategory(catId);
|
|
setRepos(catRepos);
|
|
}
|
|
}, []);
|
|
|
|
// 加载所有数据:分类 + 仓库列表
|
|
const loadData = useCallback(async () => {
|
|
try {
|
|
await initDatabase();
|
|
const cats = await getAllCategories();
|
|
setCategories(cats);
|
|
await loadRepos(selectedCategory);
|
|
} catch (e) {
|
|
console.error('加载数据失败:', e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [selectedCategory, loadRepos]);
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, [loadData]);
|
|
|
|
// Android 硬件返回按钮退出应用
|
|
useEffect(() => {
|
|
const onBackPress = () => {
|
|
Alert.alert('退出应用', '确定要退出吗?', [
|
|
{ text: '取消', style: 'cancel' },
|
|
{ text: '退出', style: 'destructive', onPress: () => BackHandler.exitApp() },
|
|
]);
|
|
return true;
|
|
};
|
|
const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress);
|
|
return () => subscription.remove();
|
|
}, []);
|
|
|
|
const onSelectCategory = async (catId) => {
|
|
setSelectedCategory(catId);
|
|
await loadRepos(catId);
|
|
};
|
|
|
|
// 从 GitHub 同步星标仓库,保存到本地后执行自动分类
|
|
const handleSync = async () => {
|
|
const token = await getGitHubToken();
|
|
if (!token) {
|
|
onTokenExpired();
|
|
return;
|
|
}
|
|
setSyncing(true);
|
|
setSyncInfo('正在从 GitHub 获取星标仓库...');
|
|
try {
|
|
const reposData = await fetchStarredRepos(token);
|
|
const count = await saveRepos(reposData);
|
|
const cats = await getAllCategories();
|
|
await runAutoCategorize(cats, getUncategorizedRepos, batchSetRepoCategories);
|
|
setSyncInfo(`同步完成,新增 ${count} 个仓库(共 ${reposData.length} 个)`);
|
|
setCategories(cats);
|
|
await loadRepos(selectedCategory);
|
|
setTimeout(() => setSyncInfo(''), 3000);
|
|
} catch (e) {
|
|
if (e instanceof TokenExpiredError) {
|
|
onTokenExpired();
|
|
} else {
|
|
Alert.alert('同步失败', e.message);
|
|
}
|
|
} finally {
|
|
setSyncing(false);
|
|
}
|
|
};
|
|
|
|
const handleRefresh = async () => {
|
|
setRefreshing(true);
|
|
try {
|
|
await handleSync();
|
|
} finally {
|
|
setRefreshing(false);
|
|
}
|
|
};
|
|
|
|
// 长按仓库时提示去分类管理页操作
|
|
const handleRepoLongPress = () => {
|
|
Alert.alert('管理分类', '请前往分类管理页面设置仓库分类', [
|
|
{ text: '取消', style: 'cancel' },
|
|
{ text: '前往', onPress: onOpenCategoryManage },
|
|
]);
|
|
};
|
|
|
|
const currentCategoryName = selectedCategory === null
|
|
? '全部仓库'
|
|
: selectedCategory === 0
|
|
? '未分类'
|
|
: categories.find(c => c.id === selectedCategory)?.name || '全部仓库';
|
|
|
|
if (loading) {
|
|
return (
|
|
<View style={styles.center}>
|
|
<ActivityIndicator size="large" color="#0366d6" />
|
|
<Text style={styles.loadingText}>加载中...</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<StatusBar style="dark" />
|
|
|
|
<View style={styles.header}>
|
|
<View style={styles.headerTop}>
|
|
<Text style={styles.headerTitle}>GitHub Stars</Text>
|
|
<View style={styles.headerActions}>
|
|
<TouchableOpacity
|
|
style={styles.headerBtn}
|
|
onPress={handleSync}
|
|
disabled={syncing}
|
|
>
|
|
<Ionicons
|
|
name={syncing ? 'sync' : 'cloud-download'}
|
|
size={22}
|
|
color="#0366d6"
|
|
/>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={styles.headerBtn}
|
|
onPress={onOpenCategoryManage}
|
|
>
|
|
<Ionicons name="folder-open" size={22} color="#0366d6" />
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={styles.headerBtn}
|
|
onPress={onOpenSettings}
|
|
>
|
|
<Ionicons name="settings-outline" size={22} color="#0366d6" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
{syncInfo ? (
|
|
<Text style={styles.syncInfo}>{syncInfo}</Text>
|
|
) : null}
|
|
</View>
|
|
|
|
<View style={styles.categoryTabs}>
|
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.categoryTab,
|
|
selectedCategory === null && styles.categoryTabActive,
|
|
]}
|
|
onPress={() => onSelectCategory(null)}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.categoryTabText,
|
|
selectedCategory === null && styles.categoryTabTextActive,
|
|
]}
|
|
>
|
|
全部
|
|
</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.categoryTab,
|
|
selectedCategory === 0 && styles.categoryTabActive,
|
|
]}
|
|
onPress={() => onSelectCategory(0)}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.categoryTabText,
|
|
selectedCategory === 0 && styles.categoryTabTextActive,
|
|
]}
|
|
>
|
|
未分类
|
|
</Text>
|
|
</TouchableOpacity>
|
|
{categories.map((cat) => (
|
|
<TouchableOpacity
|
|
key={cat.id}
|
|
style={[
|
|
styles.categoryTab,
|
|
selectedCategory === cat.id && styles.categoryTabActive,
|
|
]}
|
|
onPress={() => onSelectCategory(cat.id)}
|
|
>
|
|
<View
|
|
style={[styles.categoryDot, { backgroundColor: cat.color }]}
|
|
/>
|
|
<Text
|
|
style={[
|
|
styles.categoryTabText,
|
|
selectedCategory === cat.id && styles.categoryTabTextActive,
|
|
]}
|
|
>
|
|
{cat.name}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</ScrollView>
|
|
</View>
|
|
|
|
<View style={styles.sectionHeader}>
|
|
<Text style={styles.sectionTitle}>{currentCategoryName}</Text>
|
|
<Text style={styles.sectionCount}>{repos.length} 个仓库</Text>
|
|
</View>
|
|
|
|
{syncing ? (
|
|
<View style={styles.syncingBar}>
|
|
<ActivityIndicator size="small" color="#fff" />
|
|
<Text style={styles.syncingText}>同步中...</Text>
|
|
</View>
|
|
) : null}
|
|
|
|
<FlatList
|
|
data={repos}
|
|
renderItem={({ item }) => (
|
|
<RepoItem
|
|
item={item}
|
|
showCategory={selectedCategory === null || selectedCategory === 0}
|
|
onPress={onOpenRepoDetail}
|
|
onLongPress={handleRepoLongPress}
|
|
/>
|
|
)}
|
|
keyExtractor={(item) => String(item.id)}
|
|
refreshControl={
|
|
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
|
}
|
|
ListEmptyComponent={
|
|
<View style={styles.empty}>
|
|
<Ionicons name="star-outline" size={48} color="#ccc" />
|
|
<Text style={styles.emptyText}>
|
|
{selectedCategory === 0
|
|
? '没有未分类的仓库'
|
|
: '暂无仓库数据\n点击右上角 ☁️ 按钮从 GitHub 同步'}
|
|
</Text>
|
|
</View>
|
|
}
|
|
contentContainerStyle={repos.length === 0 ? styles.emptyContainer : styles.listContent}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#f5f5f5',
|
|
},
|
|
center: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
loadingText: {
|
|
marginTop: 10,
|
|
color: '#888',
|
|
fontSize: 14,
|
|
},
|
|
header: {
|
|
backgroundColor: '#fff',
|
|
paddingTop: Platform.OS === 'ios' ? 50 : 40,
|
|
paddingBottom: 8,
|
|
paddingHorizontal: 16,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#e8e8e8',
|
|
},
|
|
headerTop: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
},
|
|
headerTitle: {
|
|
fontSize: 24,
|
|
fontWeight: '700',
|
|
color: '#1a1a1a',
|
|
},
|
|
headerActions: {
|
|
flexDirection: 'row',
|
|
gap: 8,
|
|
},
|
|
headerBtn: {
|
|
padding: 8,
|
|
borderRadius: 8,
|
|
backgroundColor: '#f0f7ff',
|
|
},
|
|
syncInfo: {
|
|
marginTop: 6,
|
|
fontSize: 12,
|
|
color: '#28a745',
|
|
},
|
|
categoryTabs: {
|
|
backgroundColor: '#fff',
|
|
paddingVertical: 10,
|
|
paddingLeft: 12,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#eee',
|
|
},
|
|
categoryTab: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 7,
|
|
marginRight: 8,
|
|
borderRadius: 20,
|
|
backgroundColor: '#f0f0f0',
|
|
},
|
|
categoryTabActive: {
|
|
backgroundColor: '#0366d6',
|
|
},
|
|
categoryTabText: {
|
|
fontSize: 13,
|
|
color: '#666',
|
|
fontWeight: '500',
|
|
},
|
|
categoryTabTextActive: {
|
|
color: '#fff',
|
|
},
|
|
categoryDot: {
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 4,
|
|
marginRight: 5,
|
|
},
|
|
sectionHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 10,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
color: '#333',
|
|
},
|
|
sectionCount: {
|
|
fontSize: 13,
|
|
color: '#999',
|
|
},
|
|
syncingBar: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: '#0366d6',
|
|
paddingVertical: 6,
|
|
gap: 8,
|
|
},
|
|
syncingText: {
|
|
color: '#fff',
|
|
fontSize: 13,
|
|
},
|
|
listContent: {
|
|
paddingBottom: 20,
|
|
},
|
|
emptyContainer: {
|
|
flexGrow: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
empty: {
|
|
alignItems: 'center',
|
|
paddingVertical: 40,
|
|
},
|
|
emptyText: {
|
|
marginTop: 12,
|
|
fontSize: 14,
|
|
color: '#999',
|
|
textAlign: 'center',
|
|
lineHeight: 20,
|
|
},
|
|
});
|