feat: initial commit - GitHub Stars Manager

This commit is contained in:
EchoZenith
2026-04-27 23:58:12 +08:00
commit 9283271b48
20 changed files with 12781 additions and 0 deletions

134
components/RepoItem.js Normal file
View File

@@ -0,0 +1,134 @@
import { StyleSheet, Text, View, TouchableOpacity, Image } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
// 仓库列表项卡片组件显示仓库头像、名称、描述、星标数、Fork 数、分类标签
export default function RepoItem({ item, onPress, onLongPress, showCategory }) {
const categories = item.categories || [];
return (
<TouchableOpacity
style={styles.repoItem}
onPress={() => onPress?.(item)}
onLongPress={() => onLongPress?.(item)}
>
<View style={styles.header}>
{item.owner_avatar_url ? (
<Image
source={{ uri: item.owner_avatar_url }}
style={styles.avatar}
/>
) : null}
<View style={styles.headerText}>
<Text style={styles.repoName}>{item.full_name}</Text>
{item.language ? (
<Text style={styles.language}>{item.language}</Text>
) : null}
</View>
</View>
<Text style={styles.repoDesc} numberOfLines={2}>
{item.description || '暂无描述'}
</Text>
<View style={styles.statsRow}>
<View style={styles.statItem}>
<Ionicons name="star" size={14} color="#f0ad4e" />
<Text style={styles.stat}>{item.stargazers_count}</Text>
</View>
<View style={styles.statItem}>
<Ionicons name="git-branch-outline" size={14} color="#555" />
<Text style={styles.stat}>{item.forks_count}</Text>
</View>
</View>
{showCategory && categories.length > 0 ? (
<View style={styles.badgesRow}>
{categories.map((cat) => (
<View
key={cat.id}
style={[styles.categoryBadge, { backgroundColor: cat.color || '#0366d6' }]}
>
<Text style={styles.categoryBadgeText}>{cat.name}</Text>
</View>
))}
</View>
) : null}
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
repoItem: {
backgroundColor: '#fff',
padding: 14,
marginVertical: 5,
marginHorizontal: 12,
borderRadius: 10,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 3,
},
header: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 6,
},
avatar: {
width: 28,
height: 28,
borderRadius: 14,
marginRight: 8,
},
headerText: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
repoName: {
fontSize: 15,
fontWeight: '600',
color: '#0366d6',
flex: 1,
},
language: {
fontSize: 12,
color: '#888',
marginLeft: 8,
},
repoDesc: {
marginTop: 4,
color: '#666',
fontSize: 13,
lineHeight: 18,
},
statsRow: {
flexDirection: 'row',
marginTop: 8,
},
statItem: {
flexDirection: 'row',
alignItems: 'center',
marginRight: 16,
gap: 4,
},
stat: {
fontSize: 13,
color: '#555',
},
badgesRow: {
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 8,
gap: 6,
},
categoryBadge: {
paddingHorizontal: 10,
paddingVertical: 3,
borderRadius: 12,
},
categoryBadgeText: {
color: '#fff',
fontSize: 11,
fontWeight: '500',
},
});

294
components/TokenInput.js Normal file
View File

@@ -0,0 +1,294 @@
import { useState, useEffect } from 'react';
import {
View, Text, TextInput, TouchableOpacity,
StyleSheet, Alert, ActivityIndicator, Linking,
Platform, KeyboardAvoidingView, BackHandler
} from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { Ionicons } from '@expo/vector-icons';
import { setGitHubToken } from '../services/database';
import { fetchStarredRepos } from '../services/github';
// Token 输入页:输入/验证 GitHub Personal Access Token
export default function TokenInput({ onTokenSaved, onBack }) {
const [token, setToken] = useState('');
const [verifying, setVerifying] = useState(false);
const [visible, setVisible] = useState(false);
const [showHelp, setShowHelp] = useState(false);
// Android 硬件返回按钮
useEffect(() => {
const onBackPress = () => {
if (onBack) {
onBack();
return true;
}
Alert.alert('退出应用', '确定要退出吗?', [
{ text: '取消', style: 'cancel' },
{ text: '退出', style: 'destructive', onPress: () => BackHandler.exitApp() },
]);
return true;
};
const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => subscription.remove();
}, [onBack]);
// 验证 Token调用 GitHub API 确认有效后再保存
const handleVerify = async () => {
const trimmed = token.trim();
if (!trimmed) {
Alert.alert('提示', '请输入 GitHub Token');
return;
}
setVerifying(true);
try {
await fetchStarredRepos(trimmed);
await setGitHubToken(trimmed);
onTokenSaved();
} catch (e) {
Alert.alert('验证失败', e.message);
} finally {
setVerifying(false);
}
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<StatusBar style="dark" />
{onBack ? (
<View style={styles.topBar}>
<TouchableOpacity style={styles.topBarBack} onPress={onBack}>
<Ionicons name="arrow-back" size={24} color="#333" />
</TouchableOpacity>
<Text style={styles.topBarTitle}>设置 Token</Text>
<View style={styles.topBarBack} />
</View>
) : null}
<View style={styles.content}>
<View style={styles.iconWrap}>
<Ionicons name="logo-github" size={64} color="#0366d6" />
</View>
<Text style={styles.title}>GitHub Stars</Text>
<Text style={styles.subtitle}>
请输入你的 GitHub Personal Access Token 以同步星标仓库
</Text>
<View style={styles.inputWrap}>
<TextInput
style={styles.input}
value={token}
onChangeText={setToken}
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
placeholderTextColor="#bbb"
secureTextEntry={!visible}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.eyeBtn}
onPress={() => setVisible(!visible)}
>
<Ionicons
name={visible ? 'eye-off' : 'eye'}
size={20}
color="#999"
/>
</TouchableOpacity>
</View>
<TouchableOpacity
style={[styles.verifyBtn, (!token.trim() || verifying) && styles.verifyBtnDisabled]}
onPress={handleVerify}
disabled={!token.trim() || verifying}
>
{verifying ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<>
<Ionicons name="checkmark-circle" size={20} color="#fff" />
<Text style={styles.verifyBtnText}>验证并保存</Text>
</>
)}
</TouchableOpacity>
<TouchableOpacity
style={styles.helpBtn}
onPress={() => setShowHelp(!showHelp)}
>
<Ionicons name="help-circle-outline" size={16} color="#0366d6" />
<Text style={styles.helpBtnText}>如何创建 GitHub Token</Text>
<Ionicons
name={showHelp ? 'chevron-up' : 'chevron-down'}
size={16}
color="#0366d6"
/>
</TouchableOpacity>
{showHelp ? (
<View style={styles.helpPanel}>
<Text style={styles.helpStep}>
1. 访问 GitHub Settings Developer settings Personal access tokens
</Text>
<Text style={styles.helpStep}>
2. 点击 "Generate new token (classic)"
</Text>
<Text style={styles.helpStep}>
3. 选择权限范围repo user
</Text>
<Text style={styles.helpStep}>
4. 复制生成的 token 并粘贴到上方输入框
</Text>
<TouchableOpacity
style={styles.helpLink}
onPress={() => Linking.openURL('https://github.com/settings/tokens')}
>
<Ionicons name="open-outline" size={14} color="#0366d6" />
<Text style={styles.helpLinkText}>前往 GitHub 生成 Token</Text>
</TouchableOpacity>
</View>
) : null}
</View>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
topBar: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: Platform.OS === 'ios' ? 50 : 40,
paddingBottom: 10,
paddingHorizontal: 16,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e8e8e8',
},
topBarBack: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
topBarTitle: {
fontSize: 17,
fontWeight: '600',
color: '#1a1a1a',
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 32,
},
iconWrap: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: '#f0f7ff',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 20,
},
title: {
fontSize: 26,
fontWeight: '700',
color: '#1a1a1a',
marginBottom: 10,
},
subtitle: {
fontSize: 14,
color: '#888',
textAlign: 'center',
lineHeight: 20,
marginBottom: 28,
paddingHorizontal: 10,
},
inputWrap: {
flexDirection: 'row',
alignItems: 'center',
width: '100%',
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 12,
backgroundColor: '#fff',
marginBottom: 16,
},
input: {
flex: 1,
padding: 14,
fontSize: 14,
color: '#333',
},
eyeBtn: {
padding: 14,
},
verifyBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#0366d6',
padding: 16,
borderRadius: 12,
width: '100%',
gap: 8,
},
verifyBtnDisabled: {
backgroundColor: '#99c9ff',
},
verifyBtnText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
helpBtn: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 20,
gap: 4,
},
helpBtnText: {
fontSize: 13,
color: '#0366d6',
},
helpPanel: {
width: '100%',
backgroundColor: '#f0f7ff',
borderRadius: 12,
padding: 16,
marginTop: 12,
borderWidth: 1,
borderColor: '#d0e3ff',
},
helpStep: {
fontSize: 13,
color: '#444',
lineHeight: 20,
marginBottom: 8,
paddingLeft: 4,
},
helpLink: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: 8,
paddingVertical: 10,
backgroundColor: '#fff',
borderRadius: 8,
borderWidth: 1,
borderColor: '#0366d6',
gap: 6,
},
helpLinkText: {
fontSize: 13,
color: '#0366d6',
fontWeight: '500',
},
});