feat(gitea): 添加 Gitea 数据卡片并更新相关设置

- 在 HomeScreen 中添加 GiteaCard 组件
- 在 SettingsScreen 中增加 Gitea 用户名和 API Token 设置
- 优化 LeetCodeCard 组件,增加空 cookie 检查
- 调整 SettingsScreen 中平台设置的布局结构
This commit is contained in:
高手 2025-06-09 16:10:40 +08:00
parent 1945d3207a
commit 89c0dfc1aa
4 changed files with 322 additions and 39 deletions

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/auth_service.dart';
import '../widgets/leetcode_card.dart';
import '../widgets/gitea_card.dart';
import 'settings_screen.dart';
class HomeScreen extends StatefulWidget {
@ -87,7 +88,7 @@ class _HomeScreenState extends State<HomeScreen> {
case 'LeetCode':
return const LeetCodeCard();
case 'Gitea':
return const Center(child: Text('Gitea 数据卡片开发中...'));
return const GiteaCard();
case 'KodBox':
return const Center(child: Text('KodBox 数据卡片开发中...'));
default:

View File

@ -13,6 +13,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
final _formKey = GlobalKey<FormState>();
final _leetcodeController = TextEditingController();
final _giteaController = TextEditingController();
final _giteaUsernameController = TextEditingController();
final _kodboxController = TextEditingController();
@override
@ -25,6 +26,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
void dispose() {
_leetcodeController.dispose();
_giteaController.dispose();
_giteaUsernameController.dispose();
_kodboxController.dispose();
super.dispose();
}
@ -36,6 +38,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {
_leetcodeController.text = authService.credentials['leetcode'] ?? '';
_giteaController.text = authService.credentials['gitea'] ?? '';
_giteaUsernameController.text =
authService.credentials['gitea_username'] ?? '';
_kodboxController.text = authService.credentials['kodbox'] ?? '';
});
}
@ -50,6 +54,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (_giteaController.text.isNotEmpty) {
await authService.saveCredentials('gitea', _giteaController.text);
}
if (_giteaUsernameController.text.isNotEmpty) {
await authService.saveCredentials(
'gitea_username',
_giteaUsernameController.text,
);
}
if (_kodboxController.text.isNotEmpty) {
await authService.saveCredentials('kodbox', _kodboxController.text);
}
@ -75,22 +85,58 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildPlatformSettings(
title: 'LeetCode 设置',
icon: Icons.code,
controller: _leetcodeController,
hintText: '请输入 LeetCode Cookie',
children: [
TextFormField(
controller: _leetcodeController,
decoration: const InputDecoration(
labelText: 'Cookie',
hintText: '请输入 LeetCode Cookie',
border: OutlineInputBorder(),
),
obscureText: true,
),
],
),
const SizedBox(height: 16),
_buildPlatformSettings(
title: 'Gitea 设置',
icon: Icons.storage,
controller: _giteaController,
hintText: '请输入 Gitea API Token',
children: [
TextFormField(
controller: _giteaUsernameController,
decoration: const InputDecoration(
labelText: '用户名',
hintText: '请输入 Gitea 用户名',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _giteaController,
decoration: const InputDecoration(
labelText: 'API Token',
hintText: '请输入 Gitea API Token',
border: OutlineInputBorder(),
),
obscureText: true,
),
],
),
const SizedBox(height: 16),
_buildPlatformSettings(
title: 'KodBox 设置',
icon: Icons.folder,
controller: _kodboxController,
hintText: '请输入 KodBox API Token',
children: [
TextFormField(
controller: _kodboxController,
decoration: const InputDecoration(
labelText: 'API Token',
hintText: '请输入 KodBox API Token',
border: OutlineInputBorder(),
),
obscureText: true,
),
],
),
const SizedBox(height: 24),
ElevatedButton(onPressed: _saveSettings, child: const Text('保存设置')),
@ -103,8 +149,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget _buildPlatformSettings({
required String title,
required IconData icon,
required TextEditingController controller,
required String hintText,
required List<Widget> children,
}) {
return Card(
child: Padding(
@ -126,15 +171,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
],
),
const SizedBox(height: 16),
TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: '认证信息',
hintText: hintText,
border: const OutlineInputBorder(),
),
obscureText: true,
),
...children,
],
),
),

245
lib/widgets/gitea_card.dart Normal file
View File

@ -0,0 +1,245 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../services/auth_service.dart';
class GiteaCard extends StatefulWidget {
const GiteaCard({super.key});
@override
State<GiteaCard> createState() => _GiteaCardState();
}
class _GiteaCardState extends State<GiteaCard> {
List<dynamic> _activities = [];
bool _isLoading = false;
String? _error;
String _getOperationTypeText(String opType) {
switch (opType) {
case 'create_repo':
return '创建仓库';
case 'commit_repo':
return '提交代码';
case 'push_tag':
return '推送标签';
case 'create_issue':
return '创建 Issue';
case 'comment_issue':
return '评论 Issue';
case 'create_pull_request':
return '创建 Pull Request';
case 'merge_pull_request':
return '合并 Pull Request';
default:
return opType;
}
}
Widget _buildCommitContent(String content) {
try {
final data = jsonDecode(content);
final commits = data['Commits'] as List;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children:
commits.map((commit) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
commit['Message'],
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
'作者: ${commit['AuthorName']} <${commit['AuthorEmail']}>',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
Text(
'提交时间: ${commit['Timestamp']}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
);
}).toList(),
);
} catch (e) {
return Text(content);
}
}
Future<void> _fetchActivities() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final authService = Provider.of<AuthService>(context, listen: false);
final token = authService.credentials['gitea'];
final username = authService.credentials['gitea_username'];
if (token == null || token.isEmpty) {
throw Exception('未配置 Gitea Token');
}
if (username == null || username.isEmpty) {
throw Exception('未配置 Gitea 用户名');
}
final response = await http.get(
Uri.parse(
'https://git.jdysya.top/api/v1/users/$username/activities/feeds',
),
headers: {'Authorization': 'token $token'},
);
if (response.statusCode == 200) {
setState(() {
_activities = jsonDecode(response.body);
_isLoading = false;
});
} else {
throw Exception('请求失败: ${response.statusCode}');
}
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
void initState() {
super.initState();
_fetchActivities();
}
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Gitea 最近活动',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _fetchActivities,
),
],
),
if (_isLoading)
const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
)
else if (_error != null)
Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
_error!,
style: const TextStyle(color: Colors.red),
),
),
)
else if (_activities.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text('暂无活动记录'),
),
)
else
Expanded(
child: ListView.builder(
itemCount: _activities.length,
itemBuilder: (context, index) {
final activity = _activities[index];
final repo = activity['repo'];
final created = DateTime.parse(activity['created']);
return Card(
margin: const EdgeInsets.symmetric(vertical: 4.0),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 16,
child: Text(
activity['act_user']['login'][0]
.toUpperCase(),
),
),
const SizedBox(width: 8),
Text(
activity['act_user']['login'],
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Text(
_getOperationTypeText(activity['op_type']),
),
],
),
const SizedBox(height: 8),
if (repo != null)
Text(
repo['full_name'],
style: const TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
if (activity['content'] != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child:
activity['op_type'] == 'commit_repo'
? _buildCommitContent(
activity['content'],
)
: Text(activity['content']),
),
const SizedBox(height: 8),
Text(
'时间: ${created.toString().substring(0, 19)}',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
),
);
},
),
),
],
),
),
);
}
}

View File

@ -26,7 +26,7 @@ class _LeetCodeCardState extends State<LeetCodeCard> {
final authService = Provider.of<AuthService>(context, listen: false);
final cookie = authService.credentials['leetcode'];
if (cookie == null) {
if (cookie == null || cookie.isEmpty) {
throw Exception('未配置 LeetCode Cookie');
}
@ -122,27 +122,27 @@ class _LeetCodeCardState extends State<LeetCodeCard> {
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _submissions.length,
itemBuilder: (context, index) {
final submission = _submissions[index];
final question = submission['question'];
final submitTime = DateTime.fromMillisecondsSinceEpoch(
submission['submitTime'] * 1000,
);
Expanded(
child: ListView.builder(
itemCount: _submissions.length,
itemBuilder: (context, index) {
final submission = _submissions[index];
final question = submission['question'];
final submitTime = DateTime.fromMillisecondsSinceEpoch(
submission['submitTime'] * 1000,
);
return ListTile(
title: Text(
question['translatedTitle'] ?? question['title'],
),
subtitle: Text(
'提交时间: ${submitTime.toString().substring(0, 19)}',
),
trailing: Text('#${question['questionFrontendId']}'),
);
},
return ListTile(
title: Text(
question['translatedTitle'] ?? question['title'],
),
subtitle: Text(
'提交时间: ${submitTime.toString().substring(0, 19)}',
),
trailing: Text('#${question['questionFrontendId']}'),
);
},
),
),
],
),