From 1d9e0a831f0fc82b2ac03c78c9bb559aad1537bb Mon Sep 17 00:00:00 2001 From: gongyun Date: Thu, 21 May 2026 10:31:43 +0800 Subject: [PATCH] Android Feature Optimization and Updates --- lib/core/constants/quick_access_defaults.dart | 86 ++- lib/data/models/login_config_model.dart | 29 + lib/main.dart | 60 +- .../pages/auth/captcha_challenge_page.dart | 496 +++++++++++++++ .../pages/auth/forgot_password_page.dart | 55 +- lib/presentation/pages/auth/login_page.dart | 161 +++-- .../pages/auth/register_page.dart | 73 ++- .../pages/files/category_files_page.dart | 587 ++++++++++++++++++ .../overview/widgets/quick_access_grid.dart | 359 ++++------- .../settings/quick_access_settings_page.dart | 452 +++++++------- lib/presentation/providers/auth_provider.dart | 8 +- .../providers/quick_access_provider.dart | 91 +++ lib/presentation/widgets/file_breadcrumb.dart | 35 +- lib/router/app_router.dart | 29 + lib/services/auth_service.dart | 60 +- lib/services/captcha_service.dart | 395 ++++++++++++ lib/services/file_service.dart | 59 +- pubspec.lock | 32 + pubspec.yaml | 1 + 19 files changed, 2506 insertions(+), 562 deletions(-) create mode 100644 lib/data/models/login_config_model.dart create mode 100644 lib/presentation/pages/auth/captcha_challenge_page.dart create mode 100644 lib/presentation/pages/files/category_files_page.dart create mode 100644 lib/presentation/providers/quick_access_provider.dart create mode 100644 lib/services/captcha_service.dart diff --git a/lib/core/constants/quick_access_defaults.dart b/lib/core/constants/quick_access_defaults.dart index c486e1c..609b9d4 100644 --- a/lib/core/constants/quick_access_defaults.dart +++ b/lib/core/constants/quick_access_defaults.dart @@ -19,23 +19,55 @@ class QuickAccessConfig { this.isDefault = false, }); - QuickAccessConfig copyWith({String? label, String? path, IconData? icon, Color? color}) => - QuickAccessConfig( - id: id, - label: label ?? this.label, - icon: icon ?? this.icon, - path: path ?? this.path, - color: color ?? this.color, - isDefault: isDefault, - ); + QuickAccessConfig copyWith({ + String? label, + String? path, + IconData? icon, + Color? color, + }) => QuickAccessConfig( + id: id, + label: label ?? this.label, + icon: icon ?? this.icon, + path: path ?? this.path, + color: color ?? this.color, + isDefault: isDefault, + ); static const storageKey = 'quick_access_shortcuts_v2'; static const defaults = [ - QuickAccessConfig(id: 'img', label: '图片', icon: LucideIcons.image, path: '/Images', color: Color(0xFFF0ABFC), isDefault: true), - QuickAccessConfig(id: 'vid', label: '视频', icon: LucideIcons.video, path: '/Videos', color: Color(0xFFFCD34D), isDefault: true), - QuickAccessConfig(id: 'doc', label: '文档', icon: LucideIcons.fileText, path: '/Documents', color: Color(0xFF93C5FD), isDefault: true), - QuickAccessConfig(id: 'mus', label: '音乐', icon: LucideIcons.music, path: '/Music', color: Color(0xFF86EFAC), isDefault: true), + QuickAccessConfig( + id: 'img', + label: '图片', + icon: LucideIcons.image, + path: 'cloudreve://my?category=image', + color: Color(0xFFF0ABFC), + isDefault: true, + ), + QuickAccessConfig( + id: 'vid', + label: '视频', + icon: LucideIcons.video, + path: 'cloudreve://my?category=video', + color: Color(0xFFFCD34D), + isDefault: true, + ), + QuickAccessConfig( + id: 'doc', + label: '文档', + icon: LucideIcons.fileText, + path: 'cloudreve://my?category=document', + color: Color(0xFF93C5FD), + isDefault: true, + ), + QuickAccessConfig( + id: 'mus', + label: '音乐', + icon: LucideIcons.music, + path: 'cloudreve://my?category=audio', + color: Color(0xFF86EFAC), + isDefault: true, + ), ]; static const iconPool = [ @@ -65,22 +97,24 @@ class QuickAccessConfig { ]; Map toJson() => { - 'id': id, - 'label': label, - 'iconCode': icon.codePoint, - 'path': path, - 'color': color.toARGB32(), - 'isDefault': isDefault, - }; + 'id': id, + 'label': label, + 'iconCode': icon.codePoint, + 'path': path, + 'color': color.toARGB32(), + 'isDefault': isDefault, + }; factory QuickAccessConfig.fromJson(Map json) { final iconCode = json['iconCode'] as int? ?? LucideIcons.folder.codePoint; final matchedIcon = iconPool.cast().firstWhere( - (i) => i!.codePoint == iconCode, - orElse: () => LucideIcons.folder, - )!; + (i) => i!.codePoint == iconCode, + orElse: () => LucideIcons.folder, + )!; return QuickAccessConfig( - id: json['id'] as String? ?? DateTime.now().millisecondsSinceEpoch.toString(), + id: + json['id'] as String? ?? + DateTime.now().millisecondsSinceEpoch.toString(), label: json['label'] as String, icon: matchedIcon, path: json['path'] as String, @@ -92,7 +126,9 @@ class QuickAccessConfig { static List parseSaved(String saved) { try { final list = jsonDecode(saved) as List; - return list.map((e) => QuickAccessConfig.fromJson(e as Map)).toList(); + return list + .map((e) => QuickAccessConfig.fromJson(e as Map)) + .toList(); } catch (_) { return List.from(defaults); } diff --git a/lib/data/models/login_config_model.dart b/lib/data/models/login_config_model.dart new file mode 100644 index 0000000..f87f0e3 --- /dev/null +++ b/lib/data/models/login_config_model.dart @@ -0,0 +1,29 @@ +/// 登录配置模型,来自 GET /site/config/login +class LoginConfigModel { + final bool loginCaptcha; + final bool regCaptcha; + final bool forgetCaptcha; + final bool registerEnabled; + final String? tosUrl; + final String? privacyPolicyUrl; + + const LoginConfigModel({ + this.loginCaptcha = false, + this.regCaptcha = false, + this.forgetCaptcha = false, + this.registerEnabled = true, + this.tosUrl, + this.privacyPolicyUrl, + }); + + factory LoginConfigModel.fromJson(Map json) { + return LoginConfigModel( + loginCaptcha: json['login_captcha'] as bool? ?? false, + regCaptcha: json['reg_captcha'] as bool? ?? false, + forgetCaptcha: json['forget_captcha'] as bool? ?? false, + registerEnabled: json['register_enabled'] as bool? ?? true, + tosUrl: json['tos_url'] as String?, + privacyPolicyUrl: json['privacy_policy_url'] as String?, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 1b69301..22f1982 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,6 +18,7 @@ import 'presentation/providers/upload_manager_provider.dart'; import 'presentation/providers/download_manager_provider.dart'; import 'presentation/providers/user_setting_provider.dart'; import 'presentation/providers/admin_provider.dart'; +import 'presentation/providers/quick_access_provider.dart'; import 'presentation/providers/theme_provider.dart'; import 'services/upload_service.dart'; import 'services/api_service.dart'; @@ -70,7 +71,8 @@ void main() async { // 退出当前新启动的进程 exit(0); } - final String processName = await singleInstance.getProcessName(pid) ?? "Unknown"; + final String processName = + await singleInstance.getProcessName(pid) ?? "Unknown"; final File? pidFile = await singleInstance.getPidFile(processName); int port = FlutterSingleInstance.port; @@ -80,17 +82,21 @@ void main() async { final Map data = jsonDecode(content); port = data['port'] ?? 0; } catch (e) { - AppLogger.e("Get FlutterSingleInstance port has error: ${e.toString()}"); + AppLogger.e( + "Get FlutterSingleInstance port has error: ${e.toString()}", + ); } } - AppLogger.i("processName: $processName \npid: $pid \npidFile: ${pidFile.path.toString()} \nSingleInstance RPC address:port: ${addr.address}:$port"); + AppLogger.i( + "processName: $processName \npid: $pid \npidFile: ${pidFile.path.toString()} \nSingleInstance RPC address:port: ${addr.address}:$port", + ); FlutterSingleInstance.onFocus = (metadata) async { AppLogger.i("收到唤醒信号: $metadata"); await DesktopService.instance.showWindow(); }; - + await DesktopService.instance.initialize(); } @@ -134,21 +140,25 @@ class CloudreveApp extends StatelessWidget { @override Widget build(BuildContext context) { - return - MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => ThemeProvider()..init()), - ChangeNotifierProvider(create: (_) => AuthProvider()..init()), - ChangeNotifierProvider(create: (_) => FileManagerProvider()), - ChangeNotifierProvider(create: (_) => NavigationProvider()), - ChangeNotifierProvider(create: (_) => UploadService()), - ChangeNotifierProvider(create: (_) => UploadManagerProvider()..initialize()), - ChangeNotifierProvider(create: (_) => DownloadManagerProvider()..initialize()), - ChangeNotifierProvider(create: (_) => UserSettingProvider()), - ChangeNotifierProvider(create: (_) => AdminProvider()), - ], - child: const AppView(), - ); + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => ThemeProvider()..init()), + ChangeNotifierProvider(create: (_) => AuthProvider()..init()), + ChangeNotifierProvider(create: (_) => FileManagerProvider()), + ChangeNotifierProvider(create: (_) => NavigationProvider()), + ChangeNotifierProvider(create: (_) => UploadService()), + ChangeNotifierProvider( + create: (_) => UploadManagerProvider()..initialize(), + ), + ChangeNotifierProvider( + create: (_) => DownloadManagerProvider()..initialize(), + ), + ChangeNotifierProvider(create: (_) => UserSettingProvider()), + ChangeNotifierProvider(create: (_) => AdminProvider()), + ChangeNotifierProvider(create: (_) => QuickAccessProvider()..load()), + ], + child: const AppView(), + ); } } @@ -179,7 +189,9 @@ class AppView extends StatelessWidget { Widget currentWidget = child; if (Platform.isWindows || Platform.isLinux) { currentWidget = Material( - color: themeProvider.isDark ? Colors.black.withValues(alpha: 0.92) : Colors.white.withValues(alpha: 0.92), + color: themeProvider.isDark + ? Colors.black.withValues(alpha: 0.92) + : Colors.white.withValues(alpha: 0.92), child: ValueListenableBuilder( valueListenable: videoFullscreenNotifier, builder: (context, isVideoFullscreen, child) { @@ -190,13 +202,9 @@ class AppView extends StatelessWidget { children: [ const SizedBox( height: 32, - child: DragToMoveArea( - child: DesktopTitleBar(), - ), - ), - Expanded( - child: child!, + child: DragToMoveArea(child: DesktopTitleBar()), ), + Expanded(child: child!), ], ); }, diff --git a/lib/presentation/pages/auth/captcha_challenge_page.dart b/lib/presentation/pages/auth/captcha_challenge_page.dart new file mode 100644 index 0000000..b0b0545 --- /dev/null +++ b/lib/presentation/pages/auth/captcha_challenge_page.dart @@ -0,0 +1,496 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class CaptchaWebConfig { + final String type; + final String displayName; + final String? siteKey; + final String? instanceUrl; + final String? assetServer; + + const CaptchaWebConfig._({ + required this.type, + required this.displayName, + this.siteKey, + this.instanceUrl, + this.assetServer, + }); + + const CaptchaWebConfig.recaptchaV2({ + required String siteKey, + String displayName = 'reCAPTCHA V2', + }) : this._(type: 'recaptcha', displayName: displayName, siteKey: siteKey); + + const CaptchaWebConfig.turnstile({ + required String siteKey, + String displayName = 'Cloudflare Turnstile', + }) : this._(type: 'turnstile', displayName: displayName, siteKey: siteKey); + + const CaptchaWebConfig.cap({ + required String instanceUrl, + required String siteKey, + String? assetServer, + String displayName = 'Cap', + }) : this._( + type: 'cap', + displayName: displayName, + instanceUrl: instanceUrl, + siteKey: siteKey, + assetServer: assetServer, + ); +} + +class CaptchaChallengePage extends StatefulWidget { + final CaptchaWebConfig config; + final String baseUrl; + + const CaptchaChallengePage({ + super.key, + required this.config, + required this.baseUrl, + }); + + @override + State createState() => _CaptchaChallengePageState(); +} + +class _CaptchaChallengePageState extends State { + late final WebViewController _controller; + bool _isLoading = true; + int _progress = 0; + String? _errorMessage; + String? _statusText; + + @override + void initState() { + super.initState(); + + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(Colors.transparent) + ..addJavaScriptChannel( + 'CaptchaBridge', + onMessageReceived: (message) { + _handleBridgeMessage(message.message); + }, + ) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (progress) { + if (mounted) setState(() => _progress = progress); + }, + onPageFinished: (_) { + if (mounted) setState(() => _isLoading = false); + }, + onWebResourceError: (error) { + if (mounted) { + setState(() { + _isLoading = false; + _errorMessage = '${error.errorCode}: ${error.description}' + .trim(); + }); + } + }, + ), + ); + + _loadCaptcha(); + } + + Future _loadCaptcha() async { + setState(() { + _isLoading = true; + _errorMessage = null; + _statusText = null; + _progress = 0; + }); + + final html = _buildHtml(widget.config); + await _controller.loadHtmlString(html, baseUrl: widget.baseUrl); + } + + void _handleBridgeMessage(String rawMessage) { + try { + final decoded = jsonDecode(rawMessage); + if (decoded is! Map) return; + + final type = decoded['type']?.toString(); + + if (type == 'success') { + final token = decoded['token']?.toString() ?? ''; + if (token.isNotEmpty && mounted) { + Navigator.of(context).pop(token); + } + return; + } + + if (type == 'progress') { + final progress = decoded['progress']?.toString(); + if (mounted) { + setState(() { + _statusText = progress == null ? '正在验证...' : '正在验证... $progress'; + }); + } + return; + } + + if (type == 'error') { + if (mounted) { + setState(() { + _errorMessage = decoded['message']?.toString() ?? '验证码加载失败'; + }); + } + return; + } + + if (type == 'expired') { + if (mounted) { + setState(() { + _statusText = '验证码已过期,请重新验证'; + }); + } + return; + } + } catch (_) { + // 忽略非 JSON 消息。 + } + } + + String _buildHtml(CaptchaWebConfig config) { + switch (config.type) { + case 'turnstile': + return _buildTurnstileHtml(config.siteKey!); + case 'recaptcha': + return _buildRecaptchaHtml(config.siteKey!); + case 'cap': + return _buildCapHtml( + instanceUrl: config.instanceUrl!, + siteKey: config.siteKey!, + assetServer: config.assetServer, + ); + default: + return _buildErrorHtml('不支持的验证码类型: ${config.type}'); + } + } + + String _baseHtml({ + required String title, + required String body, + required String script, + }) { + final safeTitle = const HtmlEscape().convert(title); + return ''' + + + + + + $safeTitle + + + +
+
+

$safeTitle

+

完成验证后会自动返回登录页。

+ $body +
正在加载验证码...
+
+
+ + + +'''; + } + + String _buildTurnstileHtml(String siteKey) { + return _baseHtml( + title: 'Cloudflare Turnstile', + body: '
', + script: + ''' + function onTurnstileLoad() { + try { + turnstile.render('#widget', { + sitekey: '${_js(siteKey)}', + callback: function(token) { solved(token); }, + 'error-callback': function() { failed('Turnstile 验证失败,请重试'); }, + 'expired-callback': function() { expired(); } + }); + markStatus('请完成人机验证', false); + } catch (e) { + failed(e && e.message ? e.message : String(e)); + } + } + + + + + + +