feat(platform): 添加 GitHub 平台支持
- 在 HomeScreen 中添加 GitHub 卡片组件 - 在 SettingsScreen 中添加 GitHub 设置选项 - 在 AuthService 中添加 GitHub 用户名和 token 的存储和读取逻辑 - 删除未使用的 TimeUtils 类
This commit is contained in:
parent
7720d44658
commit
bfedef58f4
124
lib/models/github_event.dart
Normal file
124
lib/models/github_event.dart
Normal 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']);
|
||||
}
|
||||
}
|
||||
@ -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<HomeScreen> {
|
||||
int _selectedIndex = 0;
|
||||
final List<String> _platforms = ['LeetCode', 'Gitea', 'KodBox'];
|
||||
final List<String> _platforms = ['LeetCode', 'Gitea', 'KodBox', 'GitHub'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -79,6 +80,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
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<HomeScreen> {
|
||||
return const GiteaCard();
|
||||
case 'KodBox':
|
||||
return const KodBoxCard();
|
||||
case 'GitHub':
|
||||
return const GithubCard();
|
||||
default:
|
||||
return const Center(child: Text('未知平台'));
|
||||
}
|
||||
|
||||
@ -16,6 +16,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
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<SettingsScreen> {
|
||||
_giteaController.dispose();
|
||||
_giteaUsernameController.dispose();
|
||||
_kodboxController.dispose();
|
||||
_githubUsernameController.dispose();
|
||||
_githubTokenController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -46,6 +50,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_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<SettingsScreen> {
|
||||
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<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),
|
||||
ElevatedButton(onPressed: _saveSettings, child: const Text('保存设置')),
|
||||
],
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
24
lib/services/github_service.dart
Normal file
24
lib/services/github_service.dart
Normal 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}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()}年前';
|
||||
}
|
||||
}
|
||||
}
|
||||
114
lib/widgets/github_card.dart
Normal file
114
lib/widgets/github_card.dart
Normal 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} 操作';
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user