feat(platform): 添加 GitHub 平台支持

- 在 HomeScreen 中添加 GitHub 卡片组件
- 在 SettingsScreen 中添加 GitHub 设置选项
- 在 AuthService 中添加 GitHub 用户名和 token 的存储和读取逻辑
- 删除未使用的 TimeUtils 类
This commit is contained in:
高手 2025-06-09 20:39:11 +08:00
parent 7720d44658
commit bfedef58f4
7 changed files with 321 additions and 24 deletions

View File

@ -0,0 +1,124 @@
class GithubEvent {
final String id;
final String type;
final Actor actor;
final Repo repo;
final Payload payload;
final bool public;
final DateTime createdAt;
GithubEvent({
required this.id,
required this.type,
required this.actor,
required this.repo,
required this.payload,
required this.public,
required this.createdAt,
});
factory GithubEvent.fromJson(Map<String, dynamic> json) {
return GithubEvent(
id: json['id'].toString(),
type: json['type'],
actor: Actor.fromJson(json['actor']),
repo: Repo.fromJson(json['repo']),
payload: Payload.fromJson(json['payload']),
public: json['public'],
createdAt: DateTime.parse(json['created_at']),
);
}
}
class Actor {
final int id;
final String login;
final String displayLogin;
final String avatarUrl;
Actor({
required this.id,
required this.login,
required this.displayLogin,
required this.avatarUrl,
});
factory Actor.fromJson(Map<String, dynamic> json) {
return Actor(
id: json['id'],
login: json['login'],
displayLogin: json['display_login'],
avatarUrl: json['avatar_url'],
);
}
}
class Repo {
final int id;
final String name;
final String url;
Repo({required this.id, required this.name, required this.url});
factory Repo.fromJson(Map<String, dynamic> json) {
return Repo(id: json['id'], name: json['name'], url: json['url']);
}
}
class Payload {
final String? action;
final int? pushId;
final List<Commit>? commits;
Payload({this.action, this.pushId, this.commits});
factory Payload.fromJson(Map<String, dynamic> json) {
return Payload(
action: json['action'],
pushId: json['push_id'],
commits:
json['commits'] != null
? List<Commit>.from(
json['commits'].map((x) => Commit.fromJson(x)),
)
: null,
);
}
}
class Commit {
final String sha;
final Author author;
final String message;
final bool distinct;
final String url;
Commit({
required this.sha,
required this.author,
required this.message,
required this.distinct,
required this.url,
});
factory Commit.fromJson(Map<String, dynamic> json) {
return Commit(
sha: json['sha'],
author: Author.fromJson(json['author']),
message: json['message'],
distinct: json['distinct'],
url: json['url'],
);
}
}
class Author {
final String email;
final String name;
Author({required this.email, required this.name});
factory Author.fromJson(Map<String, dynamic> json) {
return Author(email: json['email'], name: json['name']);
}
}

View File

@ -4,6 +4,7 @@ import '../services/auth_service.dart';
import '../widgets/leetcode_card.dart'; import '../widgets/leetcode_card.dart';
import '../widgets/gitea_card.dart'; import '../widgets/gitea_card.dart';
import '../widgets/kodbox_card.dart'; import '../widgets/kodbox_card.dart';
import '../widgets/github_card.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
@ -15,7 +16,7 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> { class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0; int _selectedIndex = 0;
final List<String> _platforms = ['LeetCode', 'Gitea', 'KodBox']; final List<String> _platforms = ['LeetCode', 'Gitea', 'KodBox', 'GitHub'];
@override @override
void initState() { void initState() {
@ -79,6 +80,8 @@ class _HomeScreenState extends State<HomeScreen> {
return Icons.storage; return Icons.storage;
case 'KodBox': case 'KodBox':
return Icons.folder; return Icons.folder;
case 'GitHub':
return Icons.code;
default: default:
return Icons.help_outline; return Icons.help_outline;
} }
@ -92,6 +95,8 @@ class _HomeScreenState extends State<HomeScreen> {
return const GiteaCard(); return const GiteaCard();
case 'KodBox': case 'KodBox':
return const KodBoxCard(); return const KodBoxCard();
case 'GitHub':
return const GithubCard();
default: default:
return const Center(child: Text('未知平台')); return const Center(child: Text('未知平台'));
} }

View File

@ -16,6 +16,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
final _giteaController = TextEditingController(); final _giteaController = TextEditingController();
final _giteaUsernameController = TextEditingController(); final _giteaUsernameController = TextEditingController();
final _kodboxController = TextEditingController(); final _kodboxController = TextEditingController();
final _githubUsernameController = TextEditingController();
final _githubTokenController = TextEditingController();
@override @override
void initState() { void initState() {
@ -30,6 +32,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
_giteaController.dispose(); _giteaController.dispose();
_giteaUsernameController.dispose(); _giteaUsernameController.dispose();
_kodboxController.dispose(); _kodboxController.dispose();
_githubUsernameController.dispose();
_githubTokenController.dispose();
super.dispose(); super.dispose();
} }
@ -46,6 +50,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
_giteaUsernameController.text = _giteaUsernameController.text =
authService.credentials['gitea_username'] ?? ''; authService.credentials['gitea_username'] ?? '';
_kodboxController.text = authService.credentials['kodbox_token'] ?? ''; _kodboxController.text = authService.credentials['kodbox_token'] ?? '';
_githubUsernameController.text =
authService.credentials['github_username'] ?? '';
_githubTokenController.text =
authService.credentials['github_token'] ?? '';
}); });
} }
@ -77,6 +85,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (_kodboxController.text.isNotEmpty) { if (_kodboxController.text.isNotEmpty) {
await authService.saveConfigs('kodbox_token', _kodboxController.text); await authService.saveConfigs('kodbox_token', _kodboxController.text);
} }
if (_githubUsernameController.text.isNotEmpty) {
await authService.saveConfigs(
'github_username',
_githubUsernameController.text,
);
}
if (_githubTokenController.text.isNotEmpty) {
await authService.saveConfigs(
'github_token',
_githubTokenController.text,
);
}
if (mounted) { if (mounted) {
ScaffoldMessenger.of( ScaffoldMessenger.of(
@ -161,6 +181,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
], ],
), ),
const SizedBox(height: 16),
_buildPlatformSettings(
title: 'GitHub 设置',
icon: Icons.code,
children: [
TextFormField(
controller: _githubUsernameController,
decoration: const InputDecoration(
labelText: '用户名',
hintText: '请输入 GitHub 用户名',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _githubTokenController,
decoration: const InputDecoration(
labelText: 'Personal Access Token',
hintText: '请输入 GitHub Personal Access Token',
border: OutlineInputBorder(),
),
obscureText: true,
),
],
),
const SizedBox(height: 24), const SizedBox(height: 24),
ElevatedButton(onPressed: _saveSettings, child: const Text('保存设置')), ElevatedButton(onPressed: _saveSettings, child: const Text('保存设置')),
], ],

View File

@ -22,6 +22,8 @@ class AuthService extends ChangeNotifier {
final giteaToken = await _storage.read(key: 'gitea_token'); final giteaToken = await _storage.read(key: 'gitea_token');
final giteaUserName = await _storage.read(key: 'gitea_username'); final giteaUserName = await _storage.read(key: 'gitea_username');
final kodboxToken = await _storage.read(key: 'kodbox_token'); final kodboxToken = await _storage.read(key: 'kodbox_token');
final githubUserName = await _storage.read(key: 'github_username');
final githubToken = await _storage.read(key: 'github_token');
if (leetcodeCookie != null) { if (leetcodeCookie != null) {
_credentials['leetcode_cookie'] = leetcodeCookie; _credentials['leetcode_cookie'] = leetcodeCookie;
@ -38,6 +40,12 @@ class AuthService extends ChangeNotifier {
if (kodboxToken != null) { if (kodboxToken != null) {
_credentials['kodbox_token'] = kodboxToken; _credentials['kodbox_token'] = kodboxToken;
} }
if (githubUserName != null) {
_credentials['github_username'] = githubUserName;
}
if (githubToken != null) {
_credentials['github_token'] = githubToken;
}
_isAuthenticated = true; _isAuthenticated = true;
notifyListeners(); notifyListeners();
} }

View File

@ -0,0 +1,24 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/github_event.dart';
class GithubService {
static const String _baseUrl = 'https://api.github.com';
Future<List<GithubEvent>> getUserEvents(String username, String token) async {
final response = await http.get(
Uri.parse('$_baseUrl/users/$username/events'),
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/vnd.github.v3+json',
},
);
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
return jsonList.map((json) => GithubEvent.fromJson(json)).toList();
} else {
throw Exception('Failed to load GitHub events: ${response.statusCode}');
}
}
}

View File

@ -1,23 +0,0 @@
class TimeUtils {
static String getRelativeTime(String timestamp) {
final now = DateTime.now();
final date = DateTime.fromMillisecondsSinceEpoch(
(double.parse(timestamp) * 1000).round(),
);
final difference = now.difference(date);
if (difference.inSeconds < 60) {
return '刚刚';
} else if (difference.inMinutes < 60) {
return '${difference.inMinutes}分钟前';
} else if (difference.inHours < 24) {
return '${difference.inHours}小时前';
} else if (difference.inDays < 30) {
return '${difference.inDays}天前';
} else if (difference.inDays < 365) {
return '${(difference.inDays / 30).round()}个月前';
} else {
return '${(difference.inDays / 365).round()}年前';
}
}
}

View File

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/auth_service.dart';
import '../services/github_service.dart';
import '../models/github_event.dart';
import 'package:timeago/timeago.dart' as timeago;
class GithubCard extends StatefulWidget {
const GithubCard({super.key});
@override
State<GithubCard> createState() => _GithubCardState();
}
class _GithubCardState extends State<GithubCard> {
final GithubService _githubService = GithubService();
late Future<List<GithubEvent>> _eventsFuture;
@override
void initState() {
super.initState();
_loadEvents();
}
void _loadEvents() {
final authService = Provider.of<AuthService>(context, listen: false);
final username = authService.credentials['github_username'];
final token = authService.credentials['github_token'];
if (username != null && token != null) {
_eventsFuture = _githubService.getUserEvents(username, token);
}
}
@override
Widget build(BuildContext context) {
final authService = Provider.of<AuthService>(context);
final username = authService.credentials['github_username'];
final token = authService.credentials['github_token'];
if (username == null || token == null) {
return const Center(child: Text('请在设置中配置 GitHub 用户名和 Token'));
}
return FutureBuilder<List<GithubEvent>>(
future: _eventsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('加载失败: ${snapshot.error}'));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('暂无活动数据'));
}
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final event = snapshot.data![index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(event.actor.avatarUrl),
),
title: Text(
_getEventTitle(event),
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('仓库: ${event.repo.name}'),
Text(
'时间: ${timeago.format(event.createdAt, locale: "zh_CN")}',
),
if (event.type == 'PushEvent' &&
event.payload.commits != null)
...event.payload.commits!.map(
(commit) => Text(
'提交: ${commit.message}',
style: const TextStyle(fontSize: 12),
),
),
],
),
isThreeLine: true,
),
);
},
);
},
);
}
String _getEventTitle(GithubEvent event) {
switch (event.type) {
case 'WatchEvent':
return '${event.actor.login} 关注了 ${event.repo.name}';
case 'PushEvent':
return '${event.actor.login} 推送了代码到 ${event.repo.name}';
case 'CreateEvent':
return '${event.actor.login} 创建了 ${event.repo.name}';
case 'ForkEvent':
return '${event.actor.login} Fork 了 ${event.repo.name}';
default:
return '${event.actor.login}${event.repo.name} 进行了 ${event.type} 操作';
}
}
}