feat(kodbox): 使用用户名和密码替代 API Token(#4)

- 移除 KodBox API Token 相关代码
- 添加 KodBox 用户名和密码输入字段
- 实现 KodBox 有效 token 获取逻辑
- 更新 KodBox 日志获取和展示逻辑
- 添加文件路径点击打开功能
- 更新相关测试和依赖
This commit is contained in:
高手 2025-06-10 15:23:39 +08:00
parent 85d741e8d1
commit f8aaf86cf8
12 changed files with 245 additions and 42 deletions

View File

@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<application
android:label="dashboard"
android:name="${applicationName}"

View File

@ -15,7 +15,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
final _leetcodeUserSlugController = TextEditingController();
final _giteaController = TextEditingController();
final _giteaUsernameController = TextEditingController();
final _kodboxController = TextEditingController();
final _kodboxUsernameController = TextEditingController();
final _kodboxPasswordController = TextEditingController();
final _githubUsernameController = TextEditingController();
final _githubTokenController = TextEditingController();
final _wakatimeTokenController = TextEditingController();
@ -32,7 +33,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
_leetcodeUserSlugController.dispose();
_giteaController.dispose();
_giteaUsernameController.dispose();
_kodboxController.dispose();
_kodboxUsernameController.dispose();
_kodboxPasswordController.dispose();
_githubUsernameController.dispose();
_githubTokenController.dispose();
_wakatimeTokenController.dispose();
@ -51,7 +53,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
_giteaController.text = authService.credentials['gitea_token'] ?? '';
_giteaUsernameController.text =
authService.credentials['gitea_username'] ?? '';
_kodboxController.text = authService.credentials['kodbox_token'] ?? '';
_kodboxUsernameController.text =
authService.credentials['kodbox_username'] ?? '';
_kodboxPasswordController.text =
authService.credentials['kodbox_password'] ?? '';
_githubUsernameController.text =
authService.credentials['github_username'] ?? '';
_githubTokenController.text =
@ -86,8 +91,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
_giteaUsernameController.text,
);
}
if (_kodboxController.text.isNotEmpty) {
await authService.saveConfigs('kodbox_token', _kodboxController.text);
if (_kodboxUsernameController.text.isNotEmpty) {
await authService.saveConfigs(
'kodbox_username',
_kodboxUsernameController.text,
);
}
if (_kodboxPasswordController.text.isNotEmpty) {
await authService.saveConfigs(
'kodbox_password',
_kodboxPasswordController.text,
);
}
if (_githubUsernameController.text.isNotEmpty) {
await authService.saveConfigs(
@ -181,10 +195,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
icon: Icons.folder,
children: [
TextFormField(
controller: _kodboxController,
controller: _kodboxUsernameController,
decoration: const InputDecoration(
labelText: 'API Token',
hintText: '请输入 KodBox API Token',
labelText: '用户名',
hintText: '请输入 KodBox 用户名',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _kodboxPasswordController,
decoration: const InputDecoration(
labelText: '密码',
hintText: '请输入 KodBox 密码',
border: OutlineInputBorder(),
),
obscureText: true,

View File

@ -25,6 +25,8 @@ class AuthService extends ChangeNotifier {
final githubUserName = await _storage.read(key: 'github_username');
final githubToken = await _storage.read(key: 'github_token');
final wakatimeToken = await _storage.read(key: 'wakatime_token');
final kodboxUname = await _storage.read(key: 'kodbox_username');
final kodboxPwd = await _storage.read(key: 'kodbox_password');
if (leetcodeCookie != null) {
_credentials['leetcode_cookie'] = leetcodeCookie;
@ -50,6 +52,12 @@ class AuthService extends ChangeNotifier {
if (wakatimeToken != null) {
_credentials['wakatime_token'] = wakatimeToken;
}
if (kodboxUname != null) {
_credentials['kodbox_username'] = kodboxUname;
}
if (kodboxPwd != null) {
_credentials['kodbox_password'] = kodboxPwd;
}
_isAuthenticated = true;
notifyListeners();
}

View File

@ -0,0 +1,65 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'auth_service.dart';
class KodBoxService {
static const String baseUrl = 'https://cloud.jdysya.top';
Future<String> getToken(String username, String password) async {
final response = await http.get(
Uri.parse(
'$baseUrl/?user/index/loginSubmit&name=$username&password=$password'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['code'] == true) {
return data['data']['accessToken'];
} else {
throw Exception('获取token失败: ${data['info'] ?? '未知错误'}');
}
} else {
throw Exception('请求失败: ${response.statusCode}');
}
}
Future<bool> validateToken(String token) async {
try {
final response = await http.get(
Uri.parse('$baseUrl/?admin/log/get&accessToken=$token'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['code'] != '10001';
}
return false;
} catch (e) {
return false;
}
}
Future<String> getValidToken(AuthService authService) async {
final username = authService.credentials['kodbox_username'];
final password = authService.credentials['kodbox_password'];
final currentToken = authService.credentials['kodbox_token'];
if (username == null || password == null) {
throw Exception('请先在设置中配置 KodBox 用户名和密码');
}
// token
if (currentToken != null) {
final isValid = await validateToken(currentToken);
if (isValid) {
return currentToken;
}
}
// token
final newToken = await getToken(username, password);
await authService.saveConfigs('kodbox_token', newToken);
return newToken;
}
}

View File

@ -1,9 +1,13 @@
import 'dart:ffi';
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';
import '../services/kodbox_service.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'package:url_launcher/url_launcher.dart';
class KodBoxCard extends StatefulWidget {
const KodBoxCard({super.key});
@ -16,6 +20,7 @@ class _KodBoxCardState extends State<KodBoxCard> {
List<dynamic> _logs = [];
bool _isLoading = false;
String? _error;
final _kodboxService = KodBoxService();
Future<void> _fetchLogs() async {
setState(() {
@ -25,11 +30,7 @@ class _KodBoxCardState extends State<KodBoxCard> {
try {
final authService = Provider.of<AuthService>(context, listen: false);
final token = authService.credentials['kodbox_token'];
if (token == null || token.isEmpty) {
throw Exception('未配置 KodBox Token');
}
final token = await _kodboxService.getValidToken(authService);
// 7
final now = DateTime.now();
@ -39,7 +40,7 @@ class _KodBoxCardState extends State<KodBoxCard> {
final request = http.MultipartRequest(
'POST',
Uri.parse('https://cloud.jdysya.top/?admin/log/get&accessToken=$token'),
Uri.parse('${KodBoxService.baseUrl}/?admin/log/get&accessToken=$token'),
);
request.fields.addAll({
@ -87,47 +88,76 @@ class _KodBoxCardState extends State<KodBoxCard> {
return '未知路径';
}
int? _getSourceID(Map<String, dynamic> desc) {
if (desc['sourceInfo'] == false) {
//
return desc['parentInfo']?['sourceID'];
} else if (desc['sourceInfo'] != null) {
return desc['sourceInfo']?['sourceID'];
}
return null;
}
Future<void> _launchFileUrl(int sourceID) async {
final url = Uri.parse('${KodBoxService.baseUrl}/#explorer&sidf=$sourceID');
if (await canLaunchUrl(url)) {
await launchUrl(url);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('无法打开文件链接')),
);
}
}
}
Widget _buildLogItem(Map<String, dynamic> log) {
final desc = log['desc'];
final isFileOperation =
desc != null &&
final isFileOperation = desc != null &&
(desc['sourceInfo'] != null || desc['parentInfo'] != null);
final sourceID = isFileOperation ? _getSourceID(desc) : null;
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(log['nickName']?[0].toUpperCase() ?? '?'),
),
const SizedBox(width: 8),
child: InkWell(
onTap: sourceID != null ? () => _launchFileUrl(sourceID) : null,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 16,
child: Text(log['nickName']?[0].toUpperCase() ?? '?'),
),
const SizedBox(width: 8),
Text(
log['nickName'] ?? log['name'] ?? '未知用户',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
Text(log['title'] ?? '未知操作'),
],
),
if (isFileOperation) ...[
const SizedBox(height: 8),
Text(
log['nickName'] ?? log['name'] ?? '未知用户',
style: const TextStyle(fontWeight: FontWeight.bold),
'文件路径: ${_getFilePath(desc)}',
style: TextStyle(
color: sourceID != null ? Colors.blue : Colors.black,
),
),
const SizedBox(width: 8),
Text(log['title'] ?? '未知操作'),
],
),
if (isFileOperation) ...[
const SizedBox(height: 8),
Text(
'文件路径: ${_getFilePath(desc)}',
style: const TextStyle(color: Colors.blue),
'时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(int.parse(log['createTime']) * 1000), locale: 'zh_CN')}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
const SizedBox(height: 8),
Text(
'时间: ${timeago.format(DateTime.fromMillisecondsSinceEpoch(int.parse(log['createTime']) * 1000), locale: 'zh_CN')}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
),
);

View File

@ -7,9 +7,13 @@
#include "generated_plugin_registrant.h"
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -9,10 +9,12 @@ import flutter_secure_storage_macos
import path_provider_foundation
import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@ -581,6 +581,70 @@ packages:
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "6.3.16"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "6.3.3"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.2.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.1.4"
uuid:
dependency: transitive
description:

View File

@ -42,6 +42,7 @@ dependencies:
cached_network_image: ^3.3.1
timeago: ^3.7.1
fl_chart: ^0.65.0
url_launcher: ^6.2.4
dev_dependencies:
flutter_test:

View File

@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h"
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST