feat(gitea): 添加 Gitea 数据卡片并更新相关设置
- 在 HomeScreen 中添加 GiteaCard 组件 - 在 SettingsScreen 中增加 Gitea 用户名和 API Token 设置 - 优化 LeetCodeCard 组件,增加空 cookie 检查 - 调整 SettingsScreen 中平台设置的布局结构
This commit is contained in:
parent
1945d3207a
commit
89c0dfc1aa
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
import '../widgets/leetcode_card.dart';
|
import '../widgets/leetcode_card.dart';
|
||||||
|
import '../widgets/gitea_card.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
@ -87,7 +88,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
case 'LeetCode':
|
case 'LeetCode':
|
||||||
return const LeetCodeCard();
|
return const LeetCodeCard();
|
||||||
case 'Gitea':
|
case 'Gitea':
|
||||||
return const Center(child: Text('Gitea 数据卡片开发中...'));
|
return const GiteaCard();
|
||||||
case 'KodBox':
|
case 'KodBox':
|
||||||
return const Center(child: Text('KodBox 数据卡片开发中...'));
|
return const Center(child: Text('KodBox 数据卡片开发中...'));
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -13,6 +13,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final _leetcodeController = TextEditingController();
|
final _leetcodeController = TextEditingController();
|
||||||
final _giteaController = TextEditingController();
|
final _giteaController = TextEditingController();
|
||||||
|
final _giteaUsernameController = TextEditingController();
|
||||||
final _kodboxController = TextEditingController();
|
final _kodboxController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -25,6 +26,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_leetcodeController.dispose();
|
_leetcodeController.dispose();
|
||||||
_giteaController.dispose();
|
_giteaController.dispose();
|
||||||
|
_giteaUsernameController.dispose();
|
||||||
_kodboxController.dispose();
|
_kodboxController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -36,6 +38,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_leetcodeController.text = authService.credentials['leetcode'] ?? '';
|
_leetcodeController.text = authService.credentials['leetcode'] ?? '';
|
||||||
_giteaController.text = authService.credentials['gitea'] ?? '';
|
_giteaController.text = authService.credentials['gitea'] ?? '';
|
||||||
|
_giteaUsernameController.text =
|
||||||
|
authService.credentials['gitea_username'] ?? '';
|
||||||
_kodboxController.text = authService.credentials['kodbox'] ?? '';
|
_kodboxController.text = authService.credentials['kodbox'] ?? '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -50,6 +54,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (_giteaController.text.isNotEmpty) {
|
if (_giteaController.text.isNotEmpty) {
|
||||||
await authService.saveCredentials('gitea', _giteaController.text);
|
await authService.saveCredentials('gitea', _giteaController.text);
|
||||||
}
|
}
|
||||||
|
if (_giteaUsernameController.text.isNotEmpty) {
|
||||||
|
await authService.saveCredentials(
|
||||||
|
'gitea_username',
|
||||||
|
_giteaUsernameController.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (_kodboxController.text.isNotEmpty) {
|
if (_kodboxController.text.isNotEmpty) {
|
||||||
await authService.saveCredentials('kodbox', _kodboxController.text);
|
await authService.saveCredentials('kodbox', _kodboxController.text);
|
||||||
}
|
}
|
||||||
@ -75,22 +85,58 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_buildPlatformSettings(
|
_buildPlatformSettings(
|
||||||
title: 'LeetCode 设置',
|
title: 'LeetCode 设置',
|
||||||
icon: Icons.code,
|
icon: Icons.code,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
controller: _leetcodeController,
|
controller: _leetcodeController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Cookie',
|
||||||
hintText: '请输入 LeetCode Cookie',
|
hintText: '请输入 LeetCode Cookie',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
obscureText: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildPlatformSettings(
|
_buildPlatformSettings(
|
||||||
title: 'Gitea 设置',
|
title: 'Gitea 设置',
|
||||||
icon: Icons.storage,
|
icon: Icons.storage,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _giteaUsernameController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: '用户名',
|
||||||
|
hintText: '请输入 Gitea 用户名',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
controller: _giteaController,
|
controller: _giteaController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'API Token',
|
||||||
hintText: '请输入 Gitea API Token',
|
hintText: '请输入 Gitea API Token',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
obscureText: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildPlatformSettings(
|
_buildPlatformSettings(
|
||||||
title: 'KodBox 设置',
|
title: 'KodBox 设置',
|
||||||
icon: Icons.folder,
|
icon: Icons.folder,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
controller: _kodboxController,
|
controller: _kodboxController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'API Token',
|
||||||
hintText: '请输入 KodBox API Token',
|
hintText: '请输入 KodBox API 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('保存设置')),
|
||||||
@ -103,8 +149,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
Widget _buildPlatformSettings({
|
Widget _buildPlatformSettings({
|
||||||
required String title,
|
required String title,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required TextEditingController controller,
|
required List<Widget> children,
|
||||||
required String hintText,
|
|
||||||
}) {
|
}) {
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -126,15 +171,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
...children,
|
||||||
controller: controller,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: '认证信息',
|
|
||||||
hintText: hintText,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
obscureText: true,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
245
lib/widgets/gitea_card.dart
Normal file
245
lib/widgets/gitea_card.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,7 +26,7 @@ class _LeetCodeCardState extends State<LeetCodeCard> {
|
|||||||
final authService = Provider.of<AuthService>(context, listen: false);
|
final authService = Provider.of<AuthService>(context, listen: false);
|
||||||
final cookie = authService.credentials['leetcode'];
|
final cookie = authService.credentials['leetcode'];
|
||||||
|
|
||||||
if (cookie == null) {
|
if (cookie == null || cookie.isEmpty) {
|
||||||
throw Exception('未配置 LeetCode Cookie');
|
throw Exception('未配置 LeetCode Cookie');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,9 +122,8 @@ class _LeetCodeCardState extends State<LeetCodeCard> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
ListView.builder(
|
Expanded(
|
||||||
shrinkWrap: true,
|
child: ListView.builder(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
itemCount: _submissions.length,
|
itemCount: _submissions.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final submission = _submissions[index];
|
final submission = _submissions[index];
|
||||||
@ -144,6 +143,7 @@ class _LeetCodeCardState extends State<LeetCodeCard> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user