From 89c0dfc1aa2bdc21e81225e1a5717900320b5199 Mon Sep 17 00:00:00 2001 From: jdysya <1912377458@qq.com> Date: Mon, 9 Jun 2025 16:10:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(gitea):=20=E6=B7=BB=E5=8A=A0=20Gitea=20?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=8D=A1=E7=89=87=E5=B9=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 HomeScreen 中添加 GiteaCard 组件 - 在 SettingsScreen 中增加 Gitea 用户名和 API Token 设置 - 优化 LeetCodeCard 组件,增加空 cookie 检查 - 调整 SettingsScreen 中平台设置的布局结构 --- lib/screens/home_screen.dart | 3 +- lib/screens/settings_screen.dart | 71 ++++++--- lib/widgets/gitea_card.dart | 245 +++++++++++++++++++++++++++++++ lib/widgets/leetcode_card.dart | 42 +++--- 4 files changed, 322 insertions(+), 39 deletions(-) create mode 100644 lib/widgets/gitea_card.dart diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 8cf58df..b65e437 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -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 { 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: diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 59dde31..d4d879e 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -13,6 +13,7 @@ class _SettingsScreenState extends State { final _formKey = GlobalKey(); final _leetcodeController = TextEditingController(); final _giteaController = TextEditingController(); + final _giteaUsernameController = TextEditingController(); final _kodboxController = TextEditingController(); @override @@ -25,6 +26,7 @@ class _SettingsScreenState extends State { void dispose() { _leetcodeController.dispose(); _giteaController.dispose(); + _giteaUsernameController.dispose(); _kodboxController.dispose(); super.dispose(); } @@ -36,6 +38,8 @@ class _SettingsScreenState extends State { 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 { 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 { _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 { Widget _buildPlatformSettings({ required String title, required IconData icon, - required TextEditingController controller, - required String hintText, + required List children, }) { return Card( child: Padding( @@ -126,15 +171,7 @@ class _SettingsScreenState extends State { ], ), const SizedBox(height: 16), - TextFormField( - controller: controller, - decoration: InputDecoration( - labelText: '认证信息', - hintText: hintText, - border: const OutlineInputBorder(), - ), - obscureText: true, - ), + ...children, ], ), ), diff --git a/lib/widgets/gitea_card.dart b/lib/widgets/gitea_card.dart new file mode 100644 index 0000000..80f7789 --- /dev/null +++ b/lib/widgets/gitea_card.dart @@ -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 createState() => _GiteaCardState(); +} + +class _GiteaCardState extends State { + List _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 _fetchActivities() async { + setState(() { + _isLoading = true; + _error = null; + }); + + try { + final authService = Provider.of(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, + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/leetcode_card.dart b/lib/widgets/leetcode_card.dart index fa62d0d..39c4d94 100644 --- a/lib/widgets/leetcode_card.dart +++ b/lib/widgets/leetcode_card.dart @@ -26,7 +26,7 @@ class _LeetCodeCardState extends State { final authService = Provider.of(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 { ), ) 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']}'), + ); + }, + ), ), ], ),