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

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',
},
});