二次开发源码提交

This commit is contained in:
2026-05-15 08:52:48 +08:00
parent 4131f7321a
commit eb35a1d067
191 changed files with 34566 additions and 0 deletions
+263
View File
@@ -0,0 +1,263 @@
import 'dart:convert';
import 'dart:io';
import 'dart:ui';
import 'package:cloudreve4_flutter/core/utils/app_logger.dart';
import 'package:cloudreve4_flutter/presentation/widgets/desktop_title_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_single_instance/flutter_single_instance.dart';
import 'package:provider/provider.dart';
import 'package:media_kit/media_kit.dart';
import 'package:oktoast/oktoast.dart';
import 'package:window_manager/window_manager.dart';
import 'config/app_config.dart';
import 'presentation/providers/auth_provider.dart';
import 'presentation/providers/file_manager_provider.dart';
import 'presentation/providers/navigation_provider.dart';
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/theme_provider.dart';
import 'services/upload_service.dart';
import 'services/api_service.dart';
import 'services/server_service.dart';
import 'services/cache_manager_service.dart';
import 'services/avatar_cache_service.dart';
import 'core/utils/video_fullscreen.dart';
import 'services/desktop_service.dart';
import 'router/app_router.dart';
import 'presentation/widgets/toast_helper.dart';
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 捕获 flutter_cache_manager 在 Windows 上删除缓存文件时的文件占用异常
// 该异常是后台异步抛出的,无法通过 try-catch 拦截,需绑定错误处理器静默忽略
FlutterError.onError = (details) {
final msg = details.exceptionAsString();
if (msg.contains('PathAccessException') || msg.contains('errno = 32')) {
return; // Windows 文件占用,忽略
}
FlutterError.presentError(details);
};
PlatformDispatcher.instance.onError = (error, stack) {
if (error is PathAccessException) {
return true; // 已处理,不传播
}
return false;
};
// 初始化日志
await AppLogger.init();
AppLogger.i("应用启动,日志系统就绪");
// 桌面端初始化窗口管理和系统托盘
if (Platform.isWindows || Platform.isLinux) {
// 实例化 FlutterSingleInstance 获取单实例句柄
final singleInstance = FlutterSingleInstance();
final addr = FlutterSingleInstance.address as InternetAddress;
// 检查是否是第一个实例
final bool isFirstInstance = await singleInstance.isFirstInstance();
if (!isFirstInstance) {
AppLogger.i("程序已经在运行, 尝试唤醒...");
// 如果已经有实例在运行,通过focus回调发送消息给"第一个已启动的实例"
// 第一个实例的 listen 会收到一个字符串
await singleInstance.focus({"action": "bring_to_front_showWindow"});
// 退出当前新启动的进程
exit(0);
}
final String processName = await singleInstance.getProcessName(pid) ?? "Unknown";
final File? pidFile = await singleInstance.getPidFile(processName);
int port = FlutterSingleInstance.port;
if (await pidFile!.exists()) {
try {
final content = await pidFile.readAsString();
final Map<String, dynamic> data = jsonDecode(content);
port = data['port'] ?? 0;
} catch (e) {
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");
FlutterSingleInstance.onFocus = (metadata) async {
AppLogger.i("收到唤醒信号: $metadata");
await DesktopService.instance.showWindow();
};
await DesktopService.instance.initialize();
}
// 初始化MediaKit
MediaKit.ensureInitialized();
// 初始化服务器服务
await ServerService.instance.init();
// 初始化API服务
await ApiService.instance.init();
// 初始化缓存管理器
await CacheManagerService.instance.initialize();
// 初始化头像缓存服务
await AvatarCacheService.instance.init();
// 设置横竖屏方向(仅移动端)
if (Platform.isAndroid || Platform.isIOS) {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
}
// 设置状态栏样式
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
systemNavigationBarColor: Colors.transparent,
),
);
runApp(const CloudreveApp());
}
class CloudreveApp extends StatelessWidget {
const CloudreveApp({super.key});
@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(),
);
}
}
class AppView extends StatelessWidget {
const AppView({super.key});
@override
Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
final flutterThemeMode = switch (themeProvider.themeMode) {
AppThemeMode.light => ThemeMode.light,
AppThemeMode.dark => ThemeMode.dark,
AppThemeMode.system => ThemeMode.system,
};
return OKToast(
child: MaterialApp(
title: AppConfig.appName,
debugShowCheckedModeBanner: false,
theme: themeProvider.buildLightTheme(),
darkTheme: themeProvider.buildDarkTheme(),
themeMode: flutterThemeMode,
onGenerateRoute: AppRouter.generateRoute,
initialRoute: RouteNames.splash,
navigatorObservers: [routeObserver],
builder: (context, child) {
if (child == null) return const SizedBox.shrink();
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),
child: ValueListenableBuilder<bool>(
valueListenable: videoFullscreenNotifier,
builder: (context, isVideoFullscreen, child) {
if (isVideoFullscreen) {
return child!;
}
return Column(
children: [
const SizedBox(
height: 32,
child: DragToMoveArea(
child: DesktopTitleBar(),
),
),
Expanded(
child: child!,
),
],
);
},
child: currentWidget,
),
);
// 添加全局错误处理
currentWidget = FilterQualityWidget(child: currentWidget);
}
// 添加全局错误处理
return ErrorHandler(child: currentWidget);
},
),
);
}
}
// 定义一个简单的包装类,确保子组件在绘制时使用高画质滤镜走抗锯齿逻辑
// ImageFiltered 会强制渲染引擎进行重绘计算,解决 Windows 上部分 UI 边缘生硬的问题
class FilterQualityWidget extends StatelessWidget {
final Widget child;
const FilterQualityWidget({super.key, required this.child});
@override
Widget build(BuildContext context) {
return ImageFiltered(
// 在 Windows 上,此滤镜能强制 Skia 引擎重新计算像素边缘
imageFilter: ColorFilter.mode(Colors.transparent, BlendMode.multiply),
child: child,
);
}
}
/// 全局错误处理器
class ErrorHandler extends StatelessWidget {
final Widget child;
const ErrorHandler({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Consumer<AuthProvider>(
builder: (context, authProvider, child) {
// 检查是否有待处理的登录过期错误
if (authProvider.hasRefreshTokenExpired) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final navigator = Navigator.of(context);
ToastHelper.failure('登录已过期,请重新登录');
navigator.pushNamedAndRemoveUntil(
RouteNames.login,
(route) => false,
);
authProvider.clearRefreshTokenExpired();
});
}
return child!;
},
child: child,
);
}
}