diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index d7b4488..9439015 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -12,6 +12,7 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { final _formKey = GlobalKey(); final _leetcodeController = TextEditingController(); + final _leetcodeUserSlugController = TextEditingController(); final _giteaController = TextEditingController(); final _giteaUsernameController = TextEditingController(); final _kodboxController = TextEditingController(); @@ -25,6 +26,7 @@ class _SettingsScreenState extends State { @override void dispose() { _leetcodeController.dispose(); + _leetcodeUserSlugController.dispose(); _giteaController.dispose(); _giteaUsernameController.dispose(); _kodboxController.dispose(); @@ -38,6 +40,8 @@ class _SettingsScreenState extends State { setState(() { _leetcodeController.text = authService.credentials['leetcode_cookie'] ?? ''; + _leetcodeUserSlugController.text = + authService.credentials['leetcode_user_slug'] ?? ''; _giteaController.text = authService.credentials['gitea_token'] ?? ''; _giteaUsernameController.text = authService.credentials['gitea_username'] ?? ''; @@ -55,6 +59,12 @@ class _SettingsScreenState extends State { _leetcodeController.text, ); } + if (_leetcodeUserSlugController.text.isNotEmpty) { + await authService.saveConfigs( + 'leetcode_user_slug', + _leetcodeUserSlugController.text, + ); + } if (_giteaController.text.isNotEmpty) { await authService.saveConfigs('gitea_token', _giteaController.text); } @@ -99,6 +109,15 @@ class _SettingsScreenState extends State { ), obscureText: true, ), + const SizedBox(height: 16), + TextFormField( + controller: _leetcodeUserSlugController, + decoration: const InputDecoration( + labelText: 'User Slug', + hintText: '请输入 LeetCode User Slug', + border: OutlineInputBorder(), + ), + ), ], ), const SizedBox(height: 16), diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 8d04765..66e136f 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -18,12 +18,17 @@ class AuthService extends ChangeNotifier { Future loadConfigs() async { final leetcodeCookie = await _storage.read(key: 'leetcode_cookie'); + final leetcodeUserSlug = await _storage.read(key: 'leetcode_user_slug'); final giteaToken = await _storage.read(key: 'gitea_token'); final giteaUserName = await _storage.read(key: 'gitea_username'); final kodboxToken = await _storage.read(key: 'kodbox_token'); + if (leetcodeCookie != null) { _credentials['leetcode_cookie'] = leetcodeCookie; } + if (leetcodeUserSlug != null) { + _credentials['leetcode_user_slug'] = leetcodeUserSlug; + } if (giteaToken != null) { _credentials['gitea_token'] = giteaToken; } diff --git a/lib/widgets/leetcode_card.dart b/lib/widgets/leetcode_card.dart index fc8b4af..656d2d7 100644 --- a/lib/widgets/leetcode_card.dart +++ b/lib/widgets/leetcode_card.dart @@ -14,10 +14,11 @@ class LeetCodeCard extends StatefulWidget { class _LeetCodeCardState extends State { List _submissions = []; + Map? _progress; bool _isLoading = false; String? _error; - Future _fetchSubmissions() async { + Future _fetchData() async { setState(() { _isLoading = true; _error = null; @@ -26,12 +27,17 @@ class _LeetCodeCardState extends State { try { final authService = Provider.of(context, listen: false); final cookie = authService.credentials['leetcode_cookie']; + final userSlug = authService.credentials['leetcode_user_slug']; if (cookie == null || cookie.isEmpty) { throw Exception('未配置 LeetCode Cookie'); } + if (userSlug == null || userSlug.isEmpty) { + throw Exception('未配置 LeetCode User Slug'); + } - final response = await http.post( + // 获取提交记录 + final submissionsResponse = await http.post( Uri.parse('https://leetcode.cn/graphql/noj-go/'), headers: {'Content-Type': 'application/json', 'Cookie': cookie}, body: jsonEncode({ @@ -49,19 +55,56 @@ class _LeetCodeCardState extends State { } } ''', - 'variables': {'userSlug': 'keen-wilsonv9s'}, + 'variables': {'userSlug': userSlug}, 'operationName': 'recentAcSubmissions', }), ); - if (response.statusCode == 200) { - final data = jsonDecode(response.body); + // 获取进度数据 + final progressResponse = await http.post( + Uri.parse('https://leetcode.cn/graphql/'), + headers: {'Content-Type': 'application/json', 'Cookie': cookie}, + body: jsonEncode({ + 'query': ''' + query userProfileUserQuestionProgressV2(\$userSlug: String!) { + userProfileUserQuestionProgressV2(userSlug: \$userSlug) { + numAcceptedQuestions { + count + difficulty + } + numFailedQuestions { + count + difficulty + } + numUntouchedQuestions { + count + difficulty + } + userSessionBeatsPercentage { + difficulty + percentage + } + totalQuestionBeatsPercentage + } + } + ''', + 'variables': {'userSlug': userSlug}, + 'operationName': 'userProfileUserQuestionProgressV2', + }), + ); + + if (submissionsResponse.statusCode == 200 && + progressResponse.statusCode == 200) { + final submissionsData = jsonDecode(submissionsResponse.body); + final progressData = jsonDecode(progressResponse.body); + setState(() { - _submissions = data['data']['recentACSubmissions']; + _submissions = submissionsData['data']['recentACSubmissions']; + _progress = progressData['data']['userProfileUserQuestionProgressV2']; _isLoading = false; }); } else { - throw Exception('请求失败: ${response.statusCode}'); + throw Exception('请求失败: ${submissionsResponse.statusCode}'); } } catch (e) { setState(() { @@ -74,7 +117,50 @@ class _LeetCodeCardState extends State { @override void initState() { super.initState(); - _fetchSubmissions(); + _fetchData(); + } + + Widget _buildProgressCard() { + if (_progress == null) return const SizedBox.shrink(); + + final acceptedQuestions = _progress!['numAcceptedQuestions'] as List; + final totalBeatsPercentage = + _progress!['totalQuestionBeatsPercentage'] as double; + + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '解题进度', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + ...acceptedQuestions.map((q) { + final difficulty = q['difficulty'] as String; + final count = q['count'] as int; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text(difficulty), Text('$count 题')], + ), + ); + }).toList(), + const Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('击败用户'), + Text('${totalBeatsPercentage.toStringAsFixed(1)}%'), + ], + ), + ], + ), + ), + ); } @override @@ -94,7 +180,7 @@ class _LeetCodeCardState extends State { ), IconButton( icon: const Icon(Icons.refresh), - onPressed: _isLoading ? null : _fetchSubmissions, + onPressed: _isLoading ? null : _fetchData, ), ], ), @@ -115,31 +201,45 @@ class _LeetCodeCardState extends State { ), ), ) - else if (_submissions.isEmpty) - const Center( - child: Padding( - padding: EdgeInsets.all(16.0), - child: Text('暂无提交记录'), - ), - ) else Expanded( - child: ListView.builder( - itemCount: _submissions.length, - itemBuilder: (context, index) { - final submission = _submissions[index]; - final question = submission['question']; + child: SingleChildScrollView( + child: Column( + children: [ + _buildProgressCard(), + const SizedBox(height: 16), + if (_submissions.isEmpty) + const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text('暂无提交记录'), + ), + ) + else + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _submissions.length, + itemBuilder: (context, index) { + final submission = _submissions[index]; + final question = submission['question']; - return ListTile( - title: Text( - question['translatedTitle'] ?? question['title'], - ), - subtitle: Text( - '提交时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(submission['submitTime'] * 1000), locale: 'zh_CN')}', - ), - trailing: Text('#${question['questionFrontendId']}'), - ); - }, + return ListTile( + title: Text( + question['translatedTitle'] ?? + question['title'], + ), + subtitle: Text( + '提交时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(submission['submitTime'] * 1000), locale: 'zh_CN')}', + ), + trailing: Text( + '#${question['questionFrontendId']}', + ), + ); + }, + ), + ], + ), ), ), ],