diff --git a/lib/models/github_event.dart b/lib/models/github_event.dart new file mode 100644 index 0000000..16beb1e --- /dev/null +++ b/lib/models/github_event.dart @@ -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 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 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 json) { + return Repo(id: json['id'], name: json['name'], url: json['url']); + } +} + +class Payload { + final String? action; + final int? pushId; + final List? commits; + + Payload({this.action, this.pushId, this.commits}); + + factory Payload.fromJson(Map json) { + return Payload( + action: json['action'], + pushId: json['push_id'], + commits: + json['commits'] != null + ? List.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 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 json) { + return Author(email: json['email'], name: json['name']); + } +} diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 674b498..8ed81d7 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -4,6 +4,7 @@ import '../services/auth_service.dart'; import '../widgets/leetcode_card.dart'; import '../widgets/gitea_card.dart'; import '../widgets/kodbox_card.dart'; +import '../widgets/github_card.dart'; import 'settings_screen.dart'; class HomeScreen extends StatefulWidget { @@ -15,7 +16,7 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { int _selectedIndex = 0; - final List _platforms = ['LeetCode', 'Gitea', 'KodBox']; + final List _platforms = ['LeetCode', 'Gitea', 'KodBox', 'GitHub']; @override void initState() { @@ -79,6 +80,8 @@ class _HomeScreenState extends State { return Icons.storage; case 'KodBox': return Icons.folder; + case 'GitHub': + return Icons.code; default: return Icons.help_outline; } @@ -92,6 +95,8 @@ class _HomeScreenState extends State { return const GiteaCard(); case 'KodBox': return const KodBoxCard(); + case 'GitHub': + return const GithubCard(); default: return const Center(child: Text('未知平台')); } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 9439015..f4e514c 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -16,6 +16,8 @@ class _SettingsScreenState extends State { final _giteaController = TextEditingController(); final _giteaUsernameController = TextEditingController(); final _kodboxController = TextEditingController(); + final _githubUsernameController = TextEditingController(); + final _githubTokenController = TextEditingController(); @override void initState() { @@ -30,6 +32,8 @@ class _SettingsScreenState extends State { _giteaController.dispose(); _giteaUsernameController.dispose(); _kodboxController.dispose(); + _githubUsernameController.dispose(); + _githubTokenController.dispose(); super.dispose(); } @@ -46,6 +50,10 @@ class _SettingsScreenState extends State { _giteaUsernameController.text = authService.credentials['gitea_username'] ?? ''; _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 { if (_kodboxController.text.isNotEmpty) { 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) { ScaffoldMessenger.of( @@ -161,6 +181,31 @@ class _SettingsScreenState extends State { ), ], ), + 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), ElevatedButton(onPressed: _saveSettings, child: const Text('保存设置')), ], diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 66e136f..dfbe085 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -22,6 +22,8 @@ class AuthService extends ChangeNotifier { final giteaToken = await _storage.read(key: 'gitea_token'); final giteaUserName = await _storage.read(key: 'gitea_username'); 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) { _credentials['leetcode_cookie'] = leetcodeCookie; @@ -38,6 +40,12 @@ class AuthService extends ChangeNotifier { if (kodboxToken != null) { _credentials['kodbox_token'] = kodboxToken; } + if (githubUserName != null) { + _credentials['github_username'] = githubUserName; + } + if (githubToken != null) { + _credentials['github_token'] = githubToken; + } _isAuthenticated = true; notifyListeners(); } diff --git a/lib/services/github_service.dart b/lib/services/github_service.dart new file mode 100644 index 0000000..d05c094 --- /dev/null +++ b/lib/services/github_service.dart @@ -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> 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 jsonList = json.decode(response.body); + return jsonList.map((json) => GithubEvent.fromJson(json)).toList(); + } else { + throw Exception('Failed to load GitHub events: ${response.statusCode}'); + } + } +} diff --git a/lib/utils/time_utils.dart b/lib/utils/time_utils.dart deleted file mode 100644 index 5d504ac..0000000 --- a/lib/utils/time_utils.dart +++ /dev/null @@ -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()}年前'; - } - } -} diff --git a/lib/widgets/github_card.dart b/lib/widgets/github_card.dart new file mode 100644 index 0000000..fd22651 --- /dev/null +++ b/lib/widgets/github_card.dart @@ -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 createState() => _GithubCardState(); +} + +class _GithubCardState extends State { + final GithubService _githubService = GithubService(); + late Future> _eventsFuture; + + @override + void initState() { + super.initState(); + _loadEvents(); + } + + void _loadEvents() { + final authService = Provider.of(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(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>( + 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} 操作'; + } + } +}