├── lib ├── l10n │ ├── intl_en.arb │ └── intl_zh.arb ├── debug.dart ├── common │ ├── common.dart │ └── router.dart ├── database │ ├── database.dart │ ├── environment.dart │ ├── project.dart │ └── model │ │ ├── project.dart │ │ ├── environment.dart │ │ └── package.dart ├── provider │ ├── config.dart │ ├── window.dart │ ├── setting.dart │ ├── theme.dart │ ├── environment.dart │ └── project.dart ├── tool │ ├── tool.dart │ ├── image.dart │ ├── project │ │ └── platform │ │ │ ├── linux.dart │ │ │ ├── windows.dart │ │ │ ├── web.dart │ │ │ └── macos.dart │ ├── template.dart │ └── download.dart ├── page │ └── home │ │ ├── package │ │ └── index.dart │ │ ├── knowledge │ │ └── index.dart │ │ ├── project │ │ └── detail │ │ │ ├── platform │ │ │ ├── linux.dart │ │ │ ├── web.dart │ │ │ ├── macos.dart │ │ │ ├── windows.dart │ │ │ ├── ios.dart │ │ │ ├── android.dart │ │ │ └── widgets │ │ │ │ ├── base.dart │ │ │ │ ├── platform_item.dart │ │ │ │ ├── logo_platform_item.dart │ │ │ │ ├── options_platform_item.dart │ │ │ │ ├── label_platform_item.dart │ │ │ │ └── package_platform_item.dart │ │ │ ├── tabbar.dart │ │ │ ├── index.dart │ │ │ └── appbar.dart │ │ ├── settings │ │ └── item.dart │ │ └── index.dart ├── widget │ ├── env_badge.dart │ ├── context_menu_region.dart │ ├── form_field │ │ ├── check_field.dart │ │ ├── color_picker.dart │ │ ├── local_path.dart │ │ ├── project_logo.dart │ │ └── project_logo_panel.dart │ ├── color_item.dart │ ├── dialog │ │ ├── alert_message.dart │ │ ├── project │ │ │ ├── build.dart │ │ │ ├── asset.dart │ │ │ ├── font.dart │ │ │ ├── logo.dart │ │ │ └── permission.dart │ │ ├── color_picker.dart │ │ └── environment │ │ │ ├── import_local.dart │ │ │ └── remote_list.dart │ ├── project_logo.dart │ ├── fab_menu.dart │ ├── empty_box.dart │ ├── popup_menu_button.dart │ ├── app_bar.dart │ ├── drop_file.dart │ └── scheme_picker.dart ├── model │ ├── env_package.dart │ └── create_template.dart └── main.dart ├── .gitattributes ├── windows ├── runner │ ├── resources │ │ └── app_icon.ico │ ├── resource.h │ ├── runner.exe.manifest │ ├── utils.h │ ├── flutter_window.h │ ├── main.cpp │ ├── CMakeLists.txt │ ├── utils.cpp │ ├── flutter_window.cpp │ ├── Runner.rc │ └── win32_window.h ├── .gitignore ├── flutter │ ├── generated_plugin_registrant.h │ ├── generated_plugins.cmake │ ├── generated_plugin_registrant.cc │ └── CMakeLists.txt └── CMakeLists.txt ├── README.md ├── .gitignore ├── .metadata ├── analysis_options.yaml └── pubspec.yaml /lib/l10n/intl_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "Flutter管理" 3 | } -------------------------------------------------------------------------------- /lib/l10n/intl_zh.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "Flutter管理" 3 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WuXuBaiYang/flutter_manager/HEAD/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /lib/debug.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /* 5 | * 调试 6 | * @author wuxubaiyang 7 | * @Time 2024/7/31 18:06 8 | */ 9 | class Debug { 10 | // 构建调试按钮 11 | static Widget buildDebugButton(BuildContext context) { 12 | if (!kDebugMode) return const SizedBox(); 13 | return IconButton( 14 | onPressed: () => debug(context), 15 | icon: const Icon(Icons.bug_report_outlined), 16 | ); 17 | } 18 | 19 | static Future debug(BuildContext context) async { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flutter_manager 2 | 3 | flutter project manager 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/common/common.dart: -------------------------------------------------------------------------------- 1 | import 'package:jtech_base/jtech_base.dart'; 2 | 3 | /* 4 | * 静态资源/通用静态变量 5 | * @author wuxubaiyang 6 | * @Time 2022/9/8 14:54 7 | */ 8 | class Common { 9 | // 默认缓存名称 10 | static const String defaultCacheName = 'flutter_manager'; 11 | 12 | // git下载地址 13 | static const String gitDownloadUrl = 'https://git-scm.com/downloads'; 14 | 15 | // 模板项目地址 16 | static const String templateUrl = 17 | 'https://github.com/WuXuBaiYang/jtech_base.git'; 18 | 19 | // 模板项目名称 20 | static String get templateName => 21 | basename(templateUrl).replaceAll('.git', ''); 22 | 23 | // 模板创建脚本 24 | static const String templateCreateScript = 'create_project'; 25 | } 26 | -------------------------------------------------------------------------------- /lib/database/database.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_manager/objectbox.g.dart'; 2 | 3 | import 'environment.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | import 'project.dart'; 6 | 7 | /* 8 | * 数据库入口 9 | * @author wuxubaiyang 10 | * @Time 2024/4/28 9:17 11 | */ 12 | class Database extends BaseDatabase with EnvironmentDatabase, ProjectDatabase { 13 | static final Database _instance = Database._internal(); 14 | 15 | factory Database() => _instance; 16 | 17 | Database._internal(); 18 | 19 | @override 20 | Future createStore(String directory) async { 21 | return openStore(directory: directory); 22 | } 23 | } 24 | 25 | // 全局单例入口 26 | final database = Database(); 27 | -------------------------------------------------------------------------------- /lib/provider/config.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:jtech_base/jtech_base.dart'; 3 | 4 | part 'config.g.dart'; 5 | 6 | part 'config.freezed.dart'; 7 | 8 | /* 9 | * 全局设置 10 | * @author wuxubaiyang 11 | * @Time 2022/3/17 14:14 12 | */ 13 | class ConfigProvider extends BaseConfigProvider { 14 | ConfigProvider(super.context) 15 | : super( 16 | creator: (e) => AppConfig.fromJson(e), 17 | serializer: (e) => e.toJson()); 18 | } 19 | 20 | // 配置文件对象 21 | @freezed 22 | abstract class AppConfig with _$AppConfig { 23 | const factory AppConfig() = _AppConfig; 24 | 25 | factory AppConfig.fromJson(Map json) => 26 | _$AppConfigFromJson(json); 27 | } 28 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /lib/tool/tool.dart: -------------------------------------------------------------------------------- 1 | // 扩展字符串 2 | extension StringExtension on String { 3 | // 正则匹配第一个分组 4 | String regFirstGroup(String source, [int index = 0, bool trim = true]) { 5 | final match = RegExp(source).firstMatch(this); 6 | final result = match?.group(index) ?? ''; 7 | if (!trim) return result; 8 | return result.trim(); 9 | } 10 | } 11 | 12 | // 扩展工具方法 13 | class XTool { 14 | // 判断输入字符串是否为ip地址 15 | static bool isIP(String ip) => RegExp( 16 | r'^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$', 17 | ).hasMatch(ip); 18 | 19 | // 判断输入字符串是否为http/https地址 20 | static bool isHttp(String url) => RegExp( 21 | r'^https?:\/\/(([a-zA-Z0-9_-])+(\.)?)*(:\d+)?(\/((\.)?(\?)?=?&?[a-zA-Z0-9_-](\?)?)*)*$', 22 | ).hasMatch(url); 23 | } 24 | -------------------------------------------------------------------------------- /lib/page/home/package/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/widget/empty_box.dart'; 3 | import 'package:jtech_base/jtech_base.dart'; 4 | 5 | /* 6 | * 打包页 7 | * @author wuxubaiyang 8 | * @Time 2023/11/24 14:26 9 | */ 10 | class HomePackageView extends ProviderView { 11 | HomePackageView({super.key}); 12 | 13 | @override 14 | HomePackageProvider? createProvider(BuildContext context) => 15 | HomePackageProvider(context); 16 | 17 | @override 18 | Widget buildWidget(BuildContext context) { 19 | return Scaffold( 20 | body: const EmptyBoxView( 21 | isEmpty: true, 22 | hint: '功能施工中', 23 | iconData: Icons.build, 24 | child: SizedBox(), 25 | ), 26 | ); 27 | } 28 | } 29 | 30 | class HomePackageProvider extends BaseProvider { 31 | HomePackageProvider(super.context); 32 | } 33 | -------------------------------------------------------------------------------- /lib/page/home/knowledge/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/widget/empty_box.dart'; 3 | import 'package:jtech_base/jtech_base.dart'; 4 | 5 | /* 6 | * 首页-知识库分页 7 | * @author wuxubaiyang 8 | * @Time 2023/11/26 18:21 9 | */ 10 | class HomeKnowledgeView extends ProviderView { 11 | HomeKnowledgeView({super.key}); 12 | 13 | @override 14 | HomeKnowledgeProvider? createProvider(BuildContext context) => 15 | HomeKnowledgeProvider(context); 16 | 17 | @override 18 | Widget buildWidget(BuildContext context) { 19 | return Scaffold( 20 | body: const EmptyBoxView( 21 | isEmpty: true, 22 | hint: '功能施工中', 23 | iconData: Icons.build, 24 | child: SizedBox(), 25 | ), 26 | ); 27 | } 28 | } 29 | 30 | class HomeKnowledgeProvider extends BaseProvider { 31 | HomeKnowledgeProvider(super.context); 32 | } 33 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/platform/linux.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/tool/project/platform/linux.dart'; 3 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 4 | import 'widgets/base.dart'; 5 | import 'widgets/label_platform_item.dart'; 6 | import 'widgets/options_platform_item.dart'; 7 | 8 | /* 9 | * 项目详情-linux平台信息页 10 | * @author wuxubaiyang 11 | * @Time 2023/11/30 17:04 12 | */ 13 | class ProjectPlatformLinuxView 14 | extends ProjectPlatformView { 15 | const ProjectPlatformLinuxView({ 16 | super.key, 17 | super.platform = PlatformType.linux, 18 | }); 19 | 20 | @override 21 | List buildPlatformItems(BuildContext context, 22 | PlatformInfo? platformInfo) { 23 | return [ 24 | LabelPlatformItem( 25 | platform: platform, 26 | label: platformInfo?.label ?? '', 27 | ), 28 | OptionsPlatformItem(platform: platform), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/app/debug 42 | /android/app/profile 43 | /android/app/release 44 | 45 | # 生成文件忽略 46 | /lib/generated/ 47 | /lib/gen/ 48 | /venv/ 49 | pubspec.lock 50 | 51 | # 忽略签名文件 52 | *.keystore 53 | *.g.dart 54 | *.freezed.dart -------------------------------------------------------------------------------- /lib/widget/env_badge.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/database/model/environment.dart'; 3 | 4 | /* 5 | * 环境标签组件 6 | * @author wuxubaiyang 7 | * @Time 2023/11/28 18:57 8 | */ 9 | class EnvBadge extends StatelessWidget { 10 | // 环境信息 11 | final Environment? env; 12 | 13 | const EnvBadge({ 14 | super.key, 15 | required this.env, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | if (env == null) return const SizedBox(); 21 | return Container( 22 | padding: const EdgeInsets.symmetric(horizontal: 8), 23 | decoration: BoxDecoration( 24 | color: Colors.blueAccent.withValues(alpha: 0.3), 25 | borderRadius: BorderRadius.circular(100), 26 | ), 27 | child: Row(children: [ 28 | const FlutterLogo(size: 12), 29 | const SizedBox(width: 6), 30 | Text( 31 | env!.version, 32 | style: Theme.of(context).textTheme.bodyMedium, 33 | ), 34 | ]), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | desktop_drop 7 | file_selector_windows 8 | objectbox_flutter_libs 9 | open_dir_windows 10 | permission_handler_windows 11 | screen_retriever_windows 12 | url_launcher_windows 13 | window_manager 14 | ) 15 | 16 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 17 | ) 18 | 19 | set(PLUGIN_BUNDLED_LIBRARIES) 20 | 21 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 23 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 26 | endforeach(plugin) 27 | 28 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 29 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 30 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 31 | endforeach(ffi_plugin) 32 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "5c1433509f997e9059d230942bdf83f1b88a179e" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e 17 | base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e 18 | - platform: ios 19 | create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e 20 | base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /lib/provider/window.dart: -------------------------------------------------------------------------------- 1 | import 'package:window_manager/window_manager.dart'; 2 | import 'package:jtech_base/jtech_base.dart'; 3 | 4 | /* 5 | * 窗口提供者 6 | * @author wuxubaiyang 7 | * @Time 2023/11/26 18:56 8 | */ 9 | class WindowProvider extends BaseProvider with WindowListener { 10 | // 窗口最大化状态 11 | bool _maximized = false; 12 | 13 | // 窗口是否最大化 14 | bool get maximized => _maximized; 15 | 16 | WindowProvider(super.context) { 17 | // 监听窗口变化 18 | windowManager.addListener(this); 19 | } 20 | 21 | // 最大化窗口 22 | void maximize({bool vertically = false}) { 23 | _maximized = true; 24 | windowManager.maximize(vertically: vertically); 25 | notifyListeners(); 26 | } 27 | 28 | // 最小化窗口 29 | void unMaximize() { 30 | _maximized = false; 31 | windowManager.unmaximize(); 32 | notifyListeners(); 33 | } 34 | 35 | @override 36 | void onWindowMaximize() => maximize(); 37 | 38 | @override 39 | void onWindowUnmaximize() => unMaximize(); 40 | 41 | @override 42 | void dispose() { 43 | windowManager.removeListener(this); 44 | super.dispose(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/common/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_manager/database/model/project.dart'; 2 | import 'package:flutter_manager/page/home/index.dart'; 3 | import 'package:flutter_manager/page/home/project/detail/index.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | 6 | /* 7 | * 路由管理 8 | * @author wuxubaiyang 9 | * @Time 2022/9/8 14:55 10 | */ 11 | class Router extends BaseRouter { 12 | static final Router _instance = Router._internal(); 13 | 14 | factory Router() => _instance; 15 | 16 | Router._internal(); 17 | 18 | @override 19 | List get routes => [ 20 | GoRoute( 21 | path: '/', 22 | builder: (_, state) => HomePage(state: state), 23 | routes: [ 24 | GoRoute( 25 | path: '/project/detail', 26 | builder: (_, state) => ProjectDetailPage(state: state), 27 | ), 28 | ], 29 | ), 30 | ]; 31 | 32 | // 跳转首页 33 | void goHome() => push('/'); 34 | 35 | // 跳转项目详情页 36 | Future goProjectDetail(Project project) => 37 | push('/project/detail', extra: project); 38 | } 39 | 40 | // 全局单例 41 | final router = Router(); 42 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/platform/web.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 3 | import 'package:flutter_manager/tool/project/platform/web.dart'; 4 | import 'widgets/base.dart'; 5 | import 'widgets/label_platform_item.dart'; 6 | import 'widgets/logo_platform_item.dart'; 7 | import 'widgets/options_platform_item.dart'; 8 | 9 | /* 10 | * 项目详情-web平台信息页 11 | * @author wuxubaiyang 12 | * @Time 2023/11/30 17:03 13 | */ 14 | class ProjectPlatformWebView extends ProjectPlatformView { 15 | const ProjectPlatformWebView({ 16 | super.key, 17 | super.platform = PlatformType.web, 18 | }); 19 | 20 | @override 21 | List buildPlatformItems(BuildContext context, 22 | PlatformInfo? platformInfo) { 23 | return [ 24 | LabelPlatformItem( 25 | platform: platform, 26 | label: platformInfo?.label ?? '', 27 | ), 28 | LogoPlatformItem( 29 | platform: platform, 30 | mainAxisExtent: 250, 31 | logos: platformInfo?.logos ?? [], 32 | ), 33 | OptionsPlatformItem(platform: platform), 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/platform/macos.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/tool/project/platform/macos.dart'; 3 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 4 | import 'widgets/base.dart'; 5 | import 'widgets/label_platform_item.dart'; 6 | import 'widgets/logo_platform_item.dart'; 7 | import 'widgets/options_platform_item.dart'; 8 | 9 | /* 10 | * 项目详情-macos平台信息页 11 | * @author wuxubaiyang 12 | * @Time 2023/11/30 17:04 13 | */ 14 | class ProjectPlatformMacosView 15 | extends ProjectPlatformView { 16 | const ProjectPlatformMacosView({ 17 | super.key, 18 | super.platform = PlatformType.macos, 19 | }); 20 | 21 | @override 22 | List buildPlatformItems(BuildContext context, 23 | PlatformInfo? platformInfo) { 24 | return [ 25 | LabelPlatformItem( 26 | platform: platform, 27 | label: platformInfo?.label ?? '', 28 | ), 29 | LogoPlatformItem( 30 | platform: platform, 31 | mainAxisExtent: 340, 32 | logos: platformInfo?.logos ?? [], 33 | ), 34 | OptionsPlatformItem(platform: platform), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/widget/context_menu_region.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_context_menu/flutter_context_menu.dart'; 3 | 4 | /* 5 | * 自定义上下文菜单区域 6 | * @author wuxubaiyang 7 | * @Time 2023/11/28 9:16 8 | */ 9 | class CustomContextMenuRegion extends StatelessWidget { 10 | final ContextMenu contextMenu; 11 | final Widget child; 12 | final void Function(dynamic value)? onItemSelected; 13 | 14 | const CustomContextMenuRegion({ 15 | super.key, 16 | required this.contextMenu, 17 | required this.child, 18 | this.onItemSelected, 19 | }); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | Offset mousePosition = Offset.zero; 24 | 25 | return Listener( 26 | onPointerDown: (event) { 27 | mousePosition = event.position; 28 | }, 29 | child: GestureDetector( 30 | onSecondaryTap: () => _showMenu(context, mousePosition), 31 | child: child, 32 | ), 33 | ); 34 | } 35 | 36 | void _showMenu(BuildContext context, Offset mousePosition) async { 37 | final menu = 38 | contextMenu.copyWith(position: contextMenu.position ?? mousePosition); 39 | final value = await showContextMenu(context, contextMenu: menu); 40 | onItemSelected?.call(value); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/database/environment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_manager/objectbox.g.dart'; 2 | import 'package:jtech_base/jtech_base.dart'; 3 | import 'model/environment.dart'; 4 | 5 | /* 6 | * 环境相关数据库操作 7 | * @author wuxubaiyang 8 | * @Time 2024/4/28 9:19 9 | */ 10 | mixin EnvironmentDatabase on BaseDatabase { 11 | // 环境数据盒子 12 | late final envBox = getBox(); 13 | 14 | // 根据id获取环境信息 15 | Environment? getEnvById(int? id) { 16 | if (id == null) return null; 17 | return envBox.get(id); 18 | } 19 | 20 | // 获取环境数量 21 | int get envCount { 22 | return envBox.count(); 23 | } 24 | 25 | // 获取全部环境列表 26 | List getEnvList({bool desc = false}) { 27 | final flags = desc ? Order.descending : 0; 28 | return envBox 29 | .query() 30 | .order(Environment_.order, flags: flags) 31 | .build() 32 | .find(); 33 | } 34 | 35 | // 添加/更新环境 36 | Future updateEnv(Environment env) { 37 | return envBox.putAndGetAsync( 38 | env..order = envCount, 39 | ); 40 | } 41 | 42 | // 更新环境集合 43 | Future> updateEnvs(List envs) { 44 | int index = 0; 45 | return envBox.putAndGetManyAsync(envs.map((e) { 46 | return e..order = index++; 47 | }).toList()); 48 | } 49 | 50 | // 移除环境 51 | bool removeEnv(int id) => envBox.remove(id); 52 | } 53 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"flutter_manager", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /lib/database/project.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_manager/objectbox.g.dart'; 2 | import 'package:jtech_base/jtech_base.dart'; 3 | import 'model/project.dart'; 4 | 5 | /* 6 | * 项目相关数据库操作 7 | * @author wuxubaiyang 8 | * @Time 2024/4/28 9:19 9 | */ 10 | mixin ProjectDatabase on BaseDatabase { 11 | // 项目数据盒 12 | late final projectBox = getBox(); 13 | 14 | // 获取项目数量 15 | int get projectCount => projectBox.count(); 16 | 17 | // 根据环境id获取项目列表 18 | List getProjectsByEnvironmentId(int id) { 19 | return projectBox.query(Project_.environmentDB.equals(id)).build().find(); 20 | } 21 | 22 | // 获取项目列表 23 | List getProjectList({bool desc = false, bool pinned = false}) { 24 | final flags = desc ? Order.descending : 0; 25 | return projectBox 26 | .query(Project_.pinned.equals(pinned)) 27 | .order(Project_.order, flags: flags) 28 | .build() 29 | .find(); 30 | } 31 | 32 | // 添加/更新项目 33 | Future updateProject(Project project) { 34 | return projectBox.putAndGetAsync( 35 | project..order = projectCount, 36 | ); 37 | } 38 | 39 | // 更新项目排序 40 | Future> updateProjects(List projects) { 41 | int index = 0; 42 | return projectBox.putAndGetManyAsync(projects.map((e) { 43 | return e..order = index++; 44 | }).toList()); 45 | } 46 | 47 | // 移除项目 48 | bool removeProject(int id) { 49 | return projectBox.remove(id); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/provider/setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:jtech_base/jtech_base.dart'; 3 | 4 | /* 5 | * 设置提供者 6 | * @author wuxubaiyang 7 | * @Time 2023/11/28 10:54 8 | */ 9 | class SettingProvider extends BaseProvider { 10 | // 设置项key 11 | final environmentKey = GlobalObjectKey(genDateSign()), 12 | environmentCacheKey = GlobalObjectKey(genDateSign()), 13 | projectPlatformSortKey = GlobalObjectKey(genDateSign()), 14 | themeModeKey = GlobalObjectKey(genDateSign()), 15 | themeSchemeKey = GlobalObjectKey(genDateSign()); 16 | 17 | // 选中的设置项key 18 | GlobalObjectKey? _selectedKey; 19 | 20 | // 获取选中的key 21 | GlobalObjectKey? get selectedKey => _selectedKey; 22 | 23 | SettingProvider(super.context); 24 | 25 | // 跳转到flutter环境设置 26 | void goEnvironment() => _goSetting(environmentKey); 27 | 28 | // 跳转到flutter环境缓存设置 29 | void goEnvironmentCache() => _goSetting(environmentCacheKey); 30 | 31 | // 跳转到项目平台排序设置 32 | void goProjectPlatformSort() => _goSetting(projectPlatformSortKey); 33 | 34 | // 跳转到配色模式设置 35 | void goThemeMode() => _goSetting(themeModeKey); 36 | 37 | // 跳转到配色方案设置 38 | void goThemeScheme() => _goSetting(themeSchemeKey); 39 | 40 | // 取消选中 41 | void cancelSelected() { 42 | _selectedKey = null; 43 | notifyListeners(); 44 | } 45 | 46 | // 跳转到指定设置项 47 | void _goSetting(GlobalObjectKey key) { 48 | _selectedKey = key; 49 | notifyListeners(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/platform/windows.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 3 | import 'package:flutter_manager/tool/project/platform/windows.dart'; 4 | import 'widgets/base.dart'; 5 | import 'widgets/label_platform_item.dart'; 6 | import 'widgets/logo_platform_item.dart'; 7 | import 'widgets/options_platform_item.dart'; 8 | 9 | /* 10 | * 项目详情-windows平台信息页 11 | * @author wuxubaiyang 12 | * @Time 2023/11/30 17:04 13 | */ 14 | class ProjectPlatformWindowsView 15 | extends ProjectPlatformView { 16 | const ProjectPlatformWindowsView({ 17 | super.key, 18 | super.platform = PlatformType.windows, 19 | }); 20 | 21 | @override 22 | List buildPlatformItems(BuildContext context, 23 | PlatformInfo? platformInfo) { 24 | return [ 25 | LabelPlatformItem( 26 | platform: platform, 27 | label: platformInfo?.label ?? '', 28 | validator: (value) { 29 | // 验证输入内容是否为纯英文 30 | if (!WindowsPlatformTool.labelValidatorRegExp.hasMatch(value ?? '')) { 31 | return '仅支持英文、数字、下划线'; 32 | } 33 | return null; 34 | }, 35 | ), 36 | LogoPlatformItem( 37 | platform: platform, 38 | crossAxisCellCount: 2, 39 | logos: platformInfo?.logos ?? [], 40 | ), 41 | OptionsPlatformItem(platform: platform), 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/widget/form_field/check_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /* 4 | * 选择表单项 5 | * @author wuxubaiyang 6 | * @Time 2023/12/4 19:50 7 | */ 8 | class CheckFormField extends StatelessWidget { 9 | // 表单项key 10 | final Key? fieldKey; 11 | 12 | // 初始化值 13 | final bool? initialValue; 14 | 15 | // 标题 16 | final String title; 17 | 18 | // 保存回调 19 | final FormFieldSetter? onSaved; 20 | 21 | const CheckFormField({ 22 | super.key, 23 | required this.title, 24 | this.onSaved, 25 | this.fieldKey, 26 | this.initialValue, 27 | }); 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return FormField( 32 | key: fieldKey, 33 | onSaved: onSaved, 34 | initialValue: initialValue, 35 | builder: (field) { 36 | return _buildFormField(context, field); 37 | }, 38 | ); 39 | } 40 | 41 | // 构建表单字段 42 | Widget _buildFormField(BuildContext context, FormFieldState field) { 43 | final inputDecoration = InputDecoration( 44 | border: InputBorder.none, 45 | errorText: field.errorText, 46 | contentPadding: EdgeInsets.zero, 47 | ); 48 | return InputDecorator( 49 | decoration: inputDecoration, 50 | child: CheckboxListTile( 51 | title: Text(title), 52 | onChanged: field.didChange, 53 | value: field.value ?? false, 54 | contentPadding: const EdgeInsets.only(right: 4), 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /lib/widget/color_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /* 4 | * 颜色选择器Item组件 5 | * @author wuxubaiyang 6 | * @Time 2023/11/27 15:27 7 | */ 8 | class ColorPickerItem extends StatelessWidget { 9 | // 色值 10 | final Color color; 11 | 12 | // 大小 13 | final double size; 14 | 15 | // 内间距 16 | final EdgeInsetsGeometry padding; 17 | 18 | // 点击事件 19 | final VoidCallback? onPressed; 20 | 21 | // 是否已选中 22 | final bool isSelected; 23 | 24 | // 自定义tooltip 25 | final String? tooltip; 26 | 27 | const ColorPickerItem({ 28 | super.key, 29 | required this.color, 30 | this.tooltip, 31 | this.size = 45, 32 | this.onPressed, 33 | this.isSelected = false, 34 | this.padding = const EdgeInsets.all(4), 35 | }); 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return SizedBox.fromSize( 40 | size: Size.square(size), 41 | child: _buildItem(color), 42 | ); 43 | } 44 | 45 | // 构建子项 46 | Widget _buildItem(Color color) { 47 | if (isSelected) { 48 | return IconButton.outlined( 49 | tooltip: tooltip, 50 | padding: padding, 51 | onPressed: onPressed, 52 | icon: _buildItemSub(color), 53 | ); 54 | } 55 | return IconButton( 56 | tooltip: tooltip, 57 | onPressed: onPressed, 58 | padding: EdgeInsets.zero, 59 | icon: _buildItemSub(color), 60 | ); 61 | } 62 | 63 | // 构建子项sub 64 | Widget _buildItemSub(Color color) { 65 | return CircleAvatar( 66 | backgroundColor: color, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/platform/ios.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/tool/project/platform/ios.dart'; 3 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 4 | import 'widgets/base.dart'; 5 | import 'widgets/label_platform_item.dart'; 6 | import 'widgets/logo_platform_item.dart'; 7 | import 'widgets/options_platform_item.dart'; 8 | import 'widgets/package_platform_item.dart'; 9 | import 'widgets/permission_platform_item.dart'; 10 | 11 | /* 12 | * 项目详情-ios平台信息页 13 | * @author wuxubaiyang 14 | * @Time 2023/11/30 17:03 15 | */ 16 | class ProjectPlatformIosView extends ProjectPlatformView { 17 | const ProjectPlatformIosView({ 18 | super.key, 19 | super.platform = PlatformType.ios, 20 | }); 21 | 22 | @override 23 | List buildPlatformItems(BuildContext context, 24 | PlatformInfo? platformInfo) => 25 | [ 26 | LabelPlatformItem( 27 | platform: platform, 28 | label: platformInfo?.label ?? '', 29 | ), 30 | PackagePlatformItem( 31 | platform: platform, 32 | package: platformInfo?.package ?? '', 33 | ), 34 | OptionsPlatformItem(platform: platform), 35 | PermissionPlatformItem( 36 | platform: platform, 37 | permissions: platformInfo?.permissions ?? [], 38 | ), 39 | LogoPlatformItem( 40 | mainAxisExtent: 610, 41 | platform: platform, 42 | logos: platformInfo?.logos ?? [], 43 | ), 44 | ]; 45 | } 46 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | void RegisterPlugins(flutter::PluginRegistry* registry) { 19 | DesktopDropPluginRegisterWithRegistrar( 20 | registry->GetRegistrarForPlugin("DesktopDropPlugin")); 21 | FileSelectorWindowsRegisterWithRegistrar( 22 | registry->GetRegistrarForPlugin("FileSelectorWindows")); 23 | ObjectboxFlutterLibsPluginRegisterWithRegistrar( 24 | registry->GetRegistrarForPlugin("ObjectboxFlutterLibsPlugin")); 25 | OpenDirWindowsPluginCApiRegisterWithRegistrar( 26 | registry->GetRegistrarForPlugin("OpenDirWindowsPluginCApi")); 27 | PermissionHandlerWindowsPluginRegisterWithRegistrar( 28 | registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); 29 | ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( 30 | registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); 31 | UrlLauncherWindowsRegisterWithRegistrar( 32 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 33 | WindowManagerPluginRegisterWithRegistrar( 34 | registry->GetRegistrarForPlugin("WindowManagerPlugin")); 35 | } 36 | -------------------------------------------------------------------------------- /lib/widget/dialog/alert_message.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:jtech_base/jtech_base.dart'; 3 | 4 | // 展示消息确认弹窗 5 | Future showAlertMessage( 6 | BuildContext context, { 7 | String? title, 8 | required String content, 9 | String? confirmText, 10 | String? cancelText, 11 | Function? confirm, 12 | Function? cancel, 13 | }) { 14 | return showDialog( 15 | context: context, 16 | barrierDismissible: false, 17 | builder: (_) => AlertMessageDialog( 18 | title: title, 19 | content: content, 20 | confirm: confirm, 21 | cancel: cancel, 22 | ), 23 | ); 24 | } 25 | 26 | /* 27 | * 消息确认弹窗 28 | * @author wuxubaiyang 29 | * @Time 2023/12/15 9:00 30 | */ 31 | class AlertMessageDialog extends StatelessWidget { 32 | // 标题 33 | final String? title; 34 | 35 | // 内容 36 | final String content; 37 | 38 | // 确认按钮事件 39 | final Function? confirm; 40 | 41 | // 取消按钮事件 42 | final Function? cancel; 43 | 44 | const AlertMessageDialog({ 45 | super.key, 46 | this.title, 47 | required this.content, 48 | this.confirm, 49 | this.cancel, 50 | }); 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | return CustomDialog( 55 | title: title == null ? null : Text(title!), 56 | content: Text(content), 57 | actions: [ 58 | TextButton( 59 | child: const Text('取消'), 60 | onPressed: () { 61 | cancel?.call(); 62 | Navigator.maybePop(context); 63 | }, 64 | ), 65 | TextButton( 66 | child: const Text('确认'), 67 | onPressed: () { 68 | confirm?.call(); 69 | Navigator.pop(context, true); 70 | }, 71 | ), 72 | ], 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/widget/dialog/project/build.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/database/model/project.dart'; 3 | import 'package:flutter_manager/widget/empty_box.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | 6 | // 展示项目构建弹窗 7 | Future showProjectBuild(BuildContext context, 8 | {required Project project}) async { 9 | return showDialog( 10 | context: context, 11 | barrierDismissible: false, 12 | builder: (_) => ProjectBuildDialog( 13 | project: project, 14 | ), 15 | ); 16 | } 17 | 18 | /* 19 | * 项目构建弹窗 20 | * @author wuxubaiyang 21 | * @Time 2023/12/1 9:17 22 | */ 23 | class ProjectBuildDialog extends ProviderView { 24 | // 项目信息 25 | final Project project; 26 | 27 | ProjectBuildDialog({super.key, required this.project}); 28 | 29 | @override 30 | ProjectBuildDialogProvider? createProvider(BuildContext context) => 31 | ProjectBuildDialogProvider(context); 32 | 33 | @override 34 | Widget buildWidget(BuildContext context) { 35 | return CustomDialog( 36 | title: const Text('打包'), 37 | content: _buildContent(context), 38 | actions: [ 39 | TextButton( 40 | onPressed: context.pop, 41 | child: const Text('取消'), 42 | ), 43 | TextButton( 44 | child: const Text('确定'), 45 | onPressed: () {}, 46 | ), 47 | ], 48 | ); 49 | } 50 | 51 | // 构建内容 52 | Widget _buildContent(BuildContext context) { 53 | return const EmptyBoxView( 54 | isEmpty: true, 55 | hint: '功能施工中', 56 | iconData: Icons.build, 57 | child: SizedBox(), 58 | ); 59 | } 60 | } 61 | 62 | class ProjectBuildDialogProvider extends BaseProvider { 63 | ProjectBuildDialogProvider(super.context); 64 | } 65 | -------------------------------------------------------------------------------- /lib/widget/dialog/project/asset.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/database/model/project.dart'; 3 | import 'package:flutter_manager/widget/empty_box.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | 6 | // 展示asset资源管理弹窗 7 | Future showProjectAsset(BuildContext context, 8 | {required Project project}) async { 9 | return showDialog( 10 | context: context, 11 | barrierDismissible: false, 12 | builder: (_) => ProjectAssetDialog( 13 | project: project, 14 | ), 15 | ); 16 | } 17 | 18 | /* 19 | * 项目asset资源管理 20 | * @author wuxubaiyang 21 | * @Time 2023/12/1 9:17 22 | */ 23 | class ProjectAssetDialog extends ProviderView { 24 | // 项目信息 25 | final Project project; 26 | 27 | ProjectAssetDialog({super.key, required this.project}); 28 | 29 | @override 30 | ProjectAssetDialogProvider? createProvider(BuildContext context) => 31 | ProjectAssetDialogProvider(context, project); 32 | 33 | @override 34 | Widget buildWidget(BuildContext context) { 35 | return CustomDialog( 36 | title: const Text('Asset管理'), 37 | content: _buildContent(context), 38 | actions: [ 39 | TextButton( 40 | onPressed: context.pop, 41 | child: const Text('取消'), 42 | ), 43 | const TextButton( 44 | onPressed: null, 45 | child: Text('确定'), 46 | ), 47 | ], 48 | ); 49 | } 50 | 51 | // 构建内容 52 | Widget _buildContent(BuildContext context) { 53 | return const EmptyBoxView( 54 | hint: '无可用平台', 55 | isEmpty: true, 56 | child: SizedBox(), 57 | ); 58 | } 59 | } 60 | 61 | class ProjectAssetDialogProvider extends BaseProvider { 62 | // 项目信息 63 | final Project project; 64 | 65 | ProjectAssetDialogProvider(super.context, this.project); 66 | } 67 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/platform/android.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/tool/project/platform/android.dart'; 3 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 4 | import 'package:flutter_manager/widget/dialog/android_sign/create.dart'; 5 | import 'widgets/base.dart'; 6 | import 'widgets/label_platform_item.dart'; 7 | import 'widgets/logo_platform_item.dart'; 8 | import 'widgets/options_platform_item.dart'; 9 | import 'widgets/package_platform_item.dart'; 10 | import 'widgets/permission_platform_item.dart'; 11 | 12 | /* 13 | * 项目详情-android平台信息页 14 | * @author wuxubaiyang 15 | * @Time 2023/11/30 17:02 16 | */ 17 | class ProjectPlatformAndroidView 18 | extends ProjectPlatformView { 19 | const ProjectPlatformAndroidView({ 20 | super.key, 21 | super.platform = PlatformType.android, 22 | }); 23 | 24 | @override 25 | List buildPlatformItems(BuildContext context, 26 | PlatformInfo? platformInfo) => 27 | [ 28 | LabelPlatformItem( 29 | platform: platform, 30 | label: platformInfo?.label ?? '', 31 | ), 32 | PackagePlatformItem( 33 | platform: platform, 34 | package: platformInfo?.package ?? '', 35 | ), 36 | OptionsPlatformItem(platform: platform, actions: [ 37 | IconButton.filled( 38 | tooltip: '创建签名', 39 | isSelected: false, 40 | icon: const Icon(Icons.key_rounded), 41 | onPressed: () => showCreateAndroidSign(context), 42 | ), 43 | ]), 44 | PermissionPlatformItem( 45 | platform: platform, 46 | permissions: platformInfo?.permissions ?? [], 47 | ), 48 | LogoPlatformItem( 49 | platform: platform, 50 | logos: platformInfo?.logos ?? [], 51 | ), 52 | ]; 53 | } 54 | -------------------------------------------------------------------------------- /lib/widget/dialog/project/font.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/database/model/project.dart'; 3 | import 'package:flutter_manager/widget/empty_box.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | 6 | // 展示字体资源管理弹窗 7 | Future showProjectFont(BuildContext context, 8 | {required Project project}) async { 9 | return showDialog( 10 | context: context, 11 | barrierDismissible: false, 12 | builder: (_) => ProjectFontDialog( 13 | project: project, 14 | ), 15 | ); 16 | } 17 | 18 | /* 19 | * 项目字体资源管理 20 | * @author wuxubaiyang 21 | * @Time 2023/12/1 9:17 22 | */ 23 | class ProjectFontDialog extends ProviderView { 24 | // 项目信息 25 | final Project project; 26 | 27 | ProjectFontDialog({super.key, required this.project}); 28 | 29 | @override 30 | ProjectFontDialogProvider? createProvider(BuildContext context) => 31 | ProjectFontDialogProvider(context, project); 32 | 33 | @override 34 | Widget buildWidget(BuildContext context) { 35 | return CustomDialog( 36 | title: const Text('字体管理'), 37 | content: _buildContent(context), 38 | actions: [ 39 | TextButton( 40 | onPressed: context.pop, 41 | child: const Text('取消'), 42 | ), 43 | const TextButton( 44 | onPressed: null, 45 | child: Text('确定'), 46 | ), 47 | ], 48 | ); 49 | } 50 | 51 | // 构建内容 52 | Widget _buildContent(BuildContext context) { 53 | return const EmptyBoxView( 54 | hint: '无可用平台', 55 | isEmpty: true, 56 | child: SizedBox(), 57 | ); 58 | } 59 | } 60 | 61 | /* 62 | * 项目字体资源管理数据提供者 63 | * @author wuxubaiyang 64 | * @Time 2023/12/5 14:51 65 | */ 66 | class ProjectFontDialogProvider extends BaseProvider { 67 | // 项目信息 68 | final Project project; 69 | 70 | ProjectFontDialogProvider(super.context, this.project); 71 | } 72 | -------------------------------------------------------------------------------- /lib/model/env_package.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:jtech_base/jtech_base.dart'; 3 | 4 | part 'env_package.g.dart'; 5 | 6 | part 'env_package.freezed.dart'; 7 | 8 | // flutter环境安装包 9 | @freezed 10 | abstract class EnvPackage with _$EnvPackage { 11 | const EnvPackage._(); 12 | 13 | const factory EnvPackage({ 14 | required String url, 15 | required String fileName, 16 | required String channel, 17 | required String version, 18 | required String dartVersion, 19 | required String dartArch, 20 | String? buildPath, 21 | String? downloadPath, 22 | String? tempPath, 23 | }) = _EnvPackage; 24 | 25 | factory EnvPackage.fromJson(Map json) => 26 | _$EnvPackageFromJson(json); 27 | 28 | static EnvPackage create( 29 | Map json, { 30 | required String baseUrl, 31 | String? fileName, 32 | String? tempPath, 33 | String? buildPath, 34 | String? downloadPath, 35 | }) { 36 | return EnvPackage( 37 | url: '$baseUrl/${json['archive'] ?? ''}', 38 | fileName: fileName ?? basename(json['archive'] ?? ''), 39 | channel: json['channel'] ?? '', 40 | version: json['version'] ?? '', 41 | dartVersion: json['dart_sdk_version'] ?? '', 42 | dartArch: json['dart_sdk_arch'] ?? '', 43 | ); 44 | } 45 | 46 | // 判断当前是否满足导入条件(包含部署地址与安装包地址) 47 | bool get canImport => buildPath != null && downloadPath != null; 48 | 49 | // 判断是否存在已下载文件路径 50 | bool get hasDownload => downloadPath != null; 51 | 52 | // 判断是否存在已下载临时文件路径 53 | bool get hasTemp => tempPath != null; 54 | 55 | // 获取标题 56 | String get title => 'Flutter · $version · $channel'; 57 | 58 | // 根据条件搜索判断是否符合要求 59 | bool search(String keyword) { 60 | if (keyword.isEmpty) return true; 61 | return title.contains(keyword) || 62 | dartVersion.contains(keyword) || 63 | dartArch.contains(keyword); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/platform/widgets/base.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 3 | import 'package:flutter_manager/widget/empty_box.dart'; 4 | import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; 5 | import 'package:jtech_base/jtech_base.dart'; 6 | 7 | import 'provider.dart'; 8 | 9 | /* 10 | * 项目平台信息页面基类 11 | * @author wuxubaiyang 12 | * @Time 2023/12/1 9:41 13 | */ 14 | abstract class ProjectPlatformView extends StatelessWidget { 15 | // 当前平台 16 | final PlatformType platform; 17 | 18 | const ProjectPlatformView({super.key, required this.platform}); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Selector?>( 23 | selector: (_, provider) => provider.getPlatform(platform), 24 | builder: (_, platform, _) { 25 | return EmptyBoxView( 26 | hint: '无平台信息', 27 | isEmpty: platform == null, 28 | builder: (_, _) { 29 | if (platform == null) return const SizedBox(); 30 | return _buildPlatformWidget(context, platform); 31 | }, 32 | ); 33 | }, 34 | ); 35 | } 36 | 37 | // 构建平台信息 38 | Widget _buildPlatformWidget( 39 | BuildContext context, PlatformInfo platformInfo) { 40 | final children = buildPlatformItems(context, platformInfo); 41 | return EmptyBoxView( 42 | hint: '暂无方法', 43 | isEmpty: children.isEmpty, 44 | child: SingleChildScrollView( 45 | padding: const EdgeInsets.all(4), 46 | child: StaggeredGrid.extent( 47 | mainAxisSpacing: 4, 48 | crossAxisSpacing: 4, 49 | maxCrossAxisExtent: 100, 50 | children: children, 51 | ), 52 | ), 53 | ); 54 | } 55 | 56 | // 获取平台构造项 57 | List buildPlatformItems( 58 | BuildContext context, PlatformInfo platformInfo); 59 | } 60 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | unsigned int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length == 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/platform/widgets/platform_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; 3 | 4 | /* 5 | * 项目平台信息项组件 6 | * @author wuxubaiyang 7 | * @Time 2023/12/1 17:30 8 | */ 9 | class ProjectPlatformItem extends StatelessWidget { 10 | // 主轴方向的单元格大小 11 | final double? mainAxisExtent; 12 | 13 | // 主轴方向的单元格数量 14 | final int crossAxisCellCount; 15 | 16 | // 子元素 17 | final Widget content; 18 | 19 | // 标题 20 | final String? title; 21 | 22 | // 动作按钮集合 23 | final List? actions; 24 | 25 | // 点击事件 26 | final GestureTapCallback? onTap; 27 | 28 | const ProjectPlatformItem({ 29 | super.key, 30 | required this.crossAxisCellCount, 31 | required this.mainAxisExtent, 32 | required this.content, 33 | this.title, 34 | this.onTap, 35 | this.actions, 36 | }); 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return StaggeredGridTile.extent( 41 | crossAxisCellCount: crossAxisCellCount, 42 | mainAxisExtent: mainAxisExtent!, 43 | child: _buildPlatformItem(context), 44 | ); 45 | } 46 | 47 | // 构建平台信息项 48 | Widget _buildPlatformItem(BuildContext context) { 49 | return Card( 50 | child: InkWell( 51 | onTap: () => onTap?.call(), 52 | child: Container( 53 | constraints: const BoxConstraints.expand(), 54 | padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 14), 55 | child: Column( 56 | crossAxisAlignment: CrossAxisAlignment.stretch, 57 | children: [ 58 | Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ 59 | if (title != null) 60 | Text(title!, 61 | maxLines: 1, 62 | overflow: TextOverflow.ellipsis, 63 | style: Theme.of(context).textTheme.bodyMedium), 64 | ...actions ?? [] 65 | ]), 66 | Expanded(child: content), 67 | ], 68 | ), 69 | ), 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/widget/project_logo.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import '../tool/project/platform/platform.dart'; 4 | 5 | /* 6 | * 项目图标网格组件 7 | * @author wuxubaiyang 8 | * @Time 2023/12/4 16:27 9 | */ 10 | class ProjectLogoGrid extends StatelessWidget { 11 | // 最大尺寸 12 | final Size maxSize; 13 | 14 | // 图标列表 15 | final List logoList; 16 | 17 | // 点击事件 18 | final ValueChanged? onTap; 19 | 20 | // 内间距 21 | final EdgeInsetsGeometry padding; 22 | 23 | // 对齐 24 | final WrapAlignment alignment; 25 | 26 | const ProjectLogoGrid({ 27 | super.key, 28 | required this.logoList, 29 | this.onTap, 30 | this.maxSize = const Size.square(55), 31 | this.alignment = WrapAlignment.center, 32 | this.padding = const EdgeInsets.all(8), 33 | }); 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return Padding( 38 | padding: padding, 39 | child: Wrap( 40 | spacing: 8, 41 | runSpacing: 8, 42 | alignment: alignment, 43 | crossAxisAlignment: WrapCrossAlignment.end, 44 | children: List.generate(logoList.length, (i) { 45 | final item = logoList[i]; 46 | return _buildLogoItem(context, item); 47 | }), 48 | ), 49 | ); 50 | } 51 | 52 | // 构建图标项 53 | Widget _buildLogoItem(BuildContext context, PlatformLogo item) { 54 | return InkWell( 55 | borderRadius: BorderRadius.circular(4), 56 | onTap: onTap != null ? () => onTap?.call(item) : null, 57 | child: Padding( 58 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 59 | child: Column(mainAxisSize: MainAxisSize.min, children: [ 60 | ConstrainedBox( 61 | constraints: BoxConstraints.loose(maxSize), 62 | child: Image(image: FileImage(File(item.path))..evict())), 63 | const SizedBox(height: 4), 64 | Text( 65 | item.name, 66 | maxLines: 1, 67 | overflow: TextOverflow.ellipsis, 68 | style: Theme.of(context).textTheme.bodySmall, 69 | ), 70 | ]), 71 | ), 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/page/home/settings/item.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_manager/main.dart'; 4 | import 'package:flutter_manager/provider/setting.dart'; 5 | import 'package:jtech_base/jtech_base.dart'; 6 | 7 | /* 8 | * 设置项子项 9 | * @author wuxubaiyang 10 | * @Time 2023/11/28 11:18 11 | */ 12 | class SettingItem extends StatefulWidget { 13 | // 别名 14 | final String label; 15 | 16 | // 内容体 17 | final Widget? content; 18 | 19 | // 子元素 20 | final Widget? child; 21 | 22 | // 最大闪烁次数 23 | final int maxBlinkCount; 24 | 25 | const SettingItem({ 26 | super.key, 27 | required this.label, 28 | this.child, 29 | this.content, 30 | this.maxBlinkCount = 2, 31 | }); 32 | 33 | @override 34 | State createState() => _SettingItem(); 35 | } 36 | 37 | /* 38 | * 设置项子项状态 39 | * @author wuxubaiyang 40 | * @Time 2023/11/29 12:05 41 | */ 42 | class _SettingItem extends State { 43 | // 缓存计时器 44 | Timer? _timer; 45 | 46 | // 选中状态 47 | bool _selected = false; 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | return Selector( 52 | selector: (_, provider) => provider.selectedKey, 53 | builder: (_, key, _) { 54 | if (key == widget.key) _startTimer(context); 55 | return ListTile( 56 | selected: _selected, 57 | trailing: widget.child, 58 | subtitle: widget.content, 59 | title: Text(widget.label), 60 | isThreeLine: widget.content != null, 61 | ); 62 | }, 63 | ); 64 | } 65 | 66 | // 启动定时器刷新 67 | void _startTimer(BuildContext context) { 68 | if (_timer != null) return; 69 | var times = 0; 70 | const duration = Duration(milliseconds: 400); 71 | _timer = Timer.periodic(duration, (t) { 72 | setState(() => _selected = !_selected); 73 | if (++times > widget.maxBlinkCount * 2 - 1) { 74 | context.setting.cancelSelected(); 75 | _timer?.cancel(); 76 | _timer = null; 77 | } 78 | }); 79 | } 80 | 81 | @override 82 | void dispose() { 83 | _timer?.cancel(); 84 | super.dispose(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/tool/image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:image/image.dart'; 6 | 7 | /* 8 | * 图片处理工具 9 | * @author wuxubaiyang 10 | * @Time 2023/12/6 8:51 11 | */ 12 | class ImageTool { 13 | // 获取图片尺寸 14 | static Future getSize(String path) async { 15 | final image = (await getInfo(path))?.image; 16 | if (image == null) return null; 17 | return Size(image.width.toDouble(), image.height.toDouble()); 18 | } 19 | 20 | // 获取图片信息 21 | static Future getInfo(String path) { 22 | final c = Completer(); 23 | FileImage(File(path)) 24 | .resolve(const ImageConfiguration()) 25 | .addListener(ImageStreamListener((info, _) => c.complete(info))); 26 | return c.future; 27 | } 28 | 29 | // 修改图片尺寸并保存 30 | static Future resizeFile(String path, String savePath, 31 | {ImageType imageType = ImageType.png, int? width, int? height}) async { 32 | try { 33 | await _executeCommand( 34 | Command() 35 | ..decodeImageFile(path) 36 | ..copyResize(width: width, height: height), 37 | savePath, 38 | imageType, 39 | ); 40 | } catch (_) {} 41 | return null; 42 | } 43 | 44 | // 格式化图片并保存 45 | static Future saveData(Uint8List data, String savePath, 46 | [ImageType imageType = ImageType.png]) async { 47 | try { 48 | await _executeCommand( 49 | Command()..decodeImage(data), 50 | savePath, 51 | imageType, 52 | ); 53 | return savePath; 54 | } catch (_) {} 55 | return null; 56 | } 57 | 58 | // 执行命令 59 | static Future _executeCommand(Command cmd, String savePath, 60 | [ImageType imageType = ImageType.png]) { 61 | switch (imageType) { 62 | case ImageType.png: 63 | cmd = cmd..encodePngFile(savePath); 64 | case ImageType.jpg: 65 | cmd = cmd..encodeJpgFile(savePath); 66 | case ImageType.ico: 67 | cmd = cmd..encodeIcoFile(savePath); 68 | } 69 | return cmd.executeThread(); 70 | } 71 | } 72 | 73 | // 图片格式枚举 74 | enum ImageType { png, jpg, ico } 75 | -------------------------------------------------------------------------------- /lib/widget/form_field/color_picker.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/widget/color_item.dart'; 3 | import 'package:flutter_manager/widget/dialog/color_picker.dart'; 4 | 5 | /* 6 | * 颜色选择表单项 7 | * @author wuxubaiyang 8 | * @Time 2023/12/4 19:50 9 | */ 10 | class ColorPickerFormField extends StatelessWidget { 11 | // 表单项key 12 | final Key? fieldKey; 13 | 14 | // 初始化值 15 | final Color? initialValue; 16 | 17 | // 保存回调 18 | final FormFieldSetter? onSaved; 19 | 20 | // 颜色示例大小 21 | final double size; 22 | 23 | const ColorPickerFormField({ 24 | super.key, 25 | this.onSaved, 26 | this.fieldKey, 27 | this.size = 30, 28 | this.initialValue, 29 | }); 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return FormField( 34 | key: fieldKey, 35 | onSaved: onSaved, 36 | initialValue: initialValue, 37 | builder: (field) { 38 | return _buildFormField(context, field); 39 | }, 40 | ); 41 | } 42 | 43 | // 构建表单字段 44 | Widget _buildFormField(BuildContext context, FormFieldState field) { 45 | final inputDecoration = InputDecoration( 46 | border: InputBorder.none, 47 | errorText: field.errorText, 48 | contentPadding: EdgeInsets.zero, 49 | ); 50 | final color = field.value ?? Colors.transparent; 51 | return InputDecorator( 52 | decoration: inputDecoration, 53 | child: ListTile( 54 | title: const Text('颜色'), 55 | onTap: () => _showColorPicker(context, field), 56 | contentPadding: const EdgeInsets.only(right: 4), 57 | trailing: ColorPickerItem( 58 | size: size, 59 | color: color, 60 | isSelected: true, 61 | onPressed: () => _showColorPicker(context, field), 62 | ), 63 | ), 64 | ); 65 | } 66 | 67 | // 展示颜色选择器 68 | Future _showColorPicker( 69 | BuildContext context, FormFieldState field) async { 70 | final result = await showColorPicker( 71 | context, 72 | current: field.value, 73 | useTransparent: true, 74 | colors: Colors.primaries, 75 | ); 76 | if (result != null) field.didChange(result); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/widget/fab_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /* 4 | * fab菜单按钮 5 | * @author wuxubaiyang 6 | * @Time 2025/6/17 11:02 7 | */ 8 | class FabMenuButton extends StatefulWidget { 9 | // 菜单元素集合 10 | final List items; 11 | 12 | // 按钮元素 13 | final Widget child; 14 | 15 | // 动画时长 16 | final Duration duration; 17 | 18 | // 约束 19 | final BoxConstraints constraints; 20 | 21 | // 颜色 22 | final Color? color; 23 | 24 | // 菜单颜色 25 | final Color? menuColor; 26 | 27 | const FabMenuButton({ 28 | super.key, 29 | required this.items, 30 | required this.child, 31 | this.color, 32 | this.menuColor, 33 | this.duration = const Duration(milliseconds: 180), 34 | this.constraints = const BoxConstraints( 35 | maxWidth: 140, 36 | maxHeight: 240, 37 | minWidth: 50, 38 | minHeight: 50, 39 | ), 40 | }); 41 | 42 | @override 43 | State createState() => _FabMenuButtonState(); 44 | } 45 | 46 | class _FabMenuButtonState extends State { 47 | // 鼠标是否悬停 48 | bool isHover = false; 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | final primaryColor = Theme.of(context).colorScheme.primaryContainer; 53 | final color = widget.color ?? primaryColor; 54 | final menuColor = widget.menuColor ?? primaryColor; 55 | return MouseRegion( 56 | onExit: (_) => updateHover(false), 57 | onEnter: (_) => updateHover(true), 58 | child: ConstrainedBox( 59 | constraints: widget.constraints, 60 | child: Card( 61 | margin: EdgeInsets.zero, 62 | clipBehavior: Clip.hardEdge, 63 | color: isHover ? menuColor : color, 64 | child: AnimatedSize( 65 | duration: widget.duration, 66 | child: AnimatedSwitcher( 67 | duration: widget.duration, 68 | child: _buildContent(), 69 | ), 70 | ), 71 | ), 72 | ), 73 | ); 74 | } 75 | 76 | // 构建子元素内容 77 | Widget _buildContent() { 78 | if (!isHover) return widget.child; 79 | return Column(mainAxisSize: MainAxisSize.min, children: widget.items); 80 | } 81 | 82 | // 更新悬停状态 83 | void updateHover(bool isHover) { 84 | setState(() => this.isHover = isHover); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/platform/widgets/logo_platform_item.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 4 | import 'package:flutter_manager/widget/project_logo.dart'; 5 | import 'package:flutter_manager/widget/dialog/image_editor.dart'; 6 | import 'package:jtech_base/jtech_base.dart'; 7 | import 'platform_item.dart'; 8 | import 'provider.dart'; 9 | 10 | /* 11 | * 项目平台logo项组件 12 | * @author wuxubaiyang 13 | * @Time 2023/12/8 9:55 14 | */ 15 | class LogoPlatformItem extends StatelessWidget { 16 | // 平台 17 | final PlatformType platform; 18 | 19 | // 图标列表 20 | final List logos; 21 | 22 | // 水平风向占用格子数 23 | final int crossAxisCellCount; 24 | 25 | // 垂直方向高度 26 | final double mainAxisExtent; 27 | 28 | const LogoPlatformItem({ 29 | super.key, 30 | required this.platform, 31 | required this.logos, 32 | this.crossAxisCellCount = 5, 33 | this.mainAxisExtent = 150, 34 | }); 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return ProjectPlatformItem( 39 | title: '项目图标', 40 | actions: [ 41 | _buildEditLogoButton(context), 42 | ], 43 | crossAxisCellCount: crossAxisCellCount, 44 | mainAxisExtent: mainAxisExtent, 45 | content: ProjectLogoGrid(logoList: logos), 46 | ); 47 | } 48 | 49 | // 构建编辑图标按钮 50 | Widget _buildEditLogoButton(BuildContext context) { 51 | final platformProvider = context.read(); 52 | return IconButton( 53 | iconSize: 14, 54 | icon: const Icon(Icons.edit), 55 | visualDensity: VisualDensity.compact, 56 | onPressed: () async { 57 | var result = await Picker.image(dialogTitle: '选择项目图标'); 58 | if (result == null || !context.mounted) return; 59 | result = await showImageEditor(context, 60 | path: result, absoluteRatio: CropAspectRatio.ratio1_1); 61 | if (result == null || !context.mounted) return; 62 | final controller = StreamController(); 63 | platformProvider 64 | .updateLogo(platform, result, 65 | progressCallback: (c, t) => controller.add(c / t)) 66 | .loading(context, dismissible: false); 67 | }, 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/platform/widgets/options_platform_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 3 | import 'package:flutter_manager/widget/dialog/alert_message.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | import 'platform_item.dart'; 6 | import 'provider.dart'; 7 | 8 | /* 9 | * 项目平台操作项组件 10 | * @author wuxubaiyang 11 | * @Time 2023/12/8 9:55 12 | */ 13 | class OptionsPlatformItem extends StatelessWidget { 14 | // 平台 15 | final PlatformType platform; 16 | 17 | // 水平风向占用格子数 18 | final int crossAxisCellCount; 19 | 20 | // 垂直方向高度 21 | final double mainAxisExtent; 22 | 23 | // 添加操作集合 24 | final List? actions; 25 | 26 | const OptionsPlatformItem({ 27 | super.key, 28 | required this.platform, 29 | this.crossAxisCellCount = 2, 30 | this.mainAxisExtent = 110, 31 | this.actions, 32 | }); 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return ProjectPlatformItem( 37 | crossAxisCellCount: crossAxisCellCount, 38 | mainAxisExtent: mainAxisExtent, 39 | content: _buildOptions(context), 40 | ); 41 | } 42 | 43 | // 构建操作项 44 | Widget _buildOptions(BuildContext context) { 45 | final provider = context.read(); 46 | return Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ 47 | ...actions ?? [], 48 | IconButton.filled( 49 | isSelected: false, 50 | tooltip: '刷新平台信息', 51 | icon: const Icon(Icons.refresh_rounded), 52 | onPressed: () => provider 53 | .updatePlatformInfo(platform) 54 | .loading(context) 55 | .then((_) => provider.clearCacheByPlatform(platform)), 56 | ), 57 | IconButton.filled( 58 | tooltip: '删除平台', 59 | isSelected: false, 60 | color: Colors.redAccent.withValues(alpha: 0.6), 61 | icon: const Icon(Icons.delete_outline_rounded), 62 | onPressed: () => showAlertMessage( 63 | context, 64 | title: '删除平台', 65 | content: '是否删除当前平台?', 66 | ).then((result) { 67 | if (result != true || !context.mounted) return; 68 | provider.removePlatform(platform).loading(context); 69 | }), 70 | ), 71 | ]); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/widget/dialog/color_picker.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/widget/color_item.dart'; 3 | import 'package:jtech_base/jtech_base.dart'; 4 | 5 | // 展示颜色选择弹窗 6 | Future showColorPicker( 7 | BuildContext context, { 8 | required List colors, 9 | bool useTransparent = false, 10 | Color? current, 11 | }) { 12 | return showDialog( 13 | context: context, 14 | builder: (context) => ColorPickerDialog( 15 | colors: colors, 16 | current: current, 17 | useTransparent: useTransparent, 18 | ), 19 | ); 20 | } 21 | 22 | /* 23 | * 颜色选择对话框 24 | * @author wuxubaiyang 25 | * @Time 2023/11/25 19:38 26 | */ 27 | class ColorPickerDialog extends StatelessWidget { 28 | // 色值列表 29 | final List colors; 30 | 31 | // 当前颜色 32 | final Color? current; 33 | 34 | // 是否使用透明 35 | final bool useTransparent; 36 | 37 | const ColorPickerDialog({ 38 | super.key, 39 | required this.colors, 40 | this.current, 41 | this.useTransparent = false, 42 | }); 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return CustomDialog( 47 | scrollable: true, 48 | title: const Text('选择颜色'), 49 | content: _buildContent(context), 50 | style: CustomDialogStyle( 51 | constraints: const BoxConstraints.tightFor(width: 340), 52 | ), 53 | ); 54 | } 55 | 56 | // 构建内容 57 | Widget _buildContent(BuildContext context) { 58 | return Wrap( 59 | spacing: 14, 60 | runSpacing: 14, 61 | children: [ 62 | ...colors.map((item) { 63 | return ColorPickerItem( 64 | color: item, 65 | isSelected: item == current, 66 | onPressed: () => Navigator.pop(context, item), 67 | ); 68 | }), 69 | if (useTransparent) _buildTransparent(context), 70 | ], 71 | ); 72 | } 73 | 74 | // 构建透明色块 75 | Widget _buildTransparent(BuildContext context) { 76 | final opacity = 77 | Theme.of(context).brightness == Brightness.dark ? 0.05 : 0.2; 78 | return ColorPickerItem( 79 | isSelected: current == Colors.transparent, 80 | color: Theme.of(context).splashColor.withValues(alpha: opacity), 81 | onPressed: () => Navigator.pop(context, Colors.transparent), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/tool/project/platform/linux.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_manager/tool/tool.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | import 'platform.dart'; 6 | 7 | // linux平台参数元组 8 | typedef LinuxPlatformInfo = (); 9 | 10 | /* 11 | * linux平台工具类 12 | * @author wuxubaiyang 13 | * @Time 2023/11/29 14:58 14 | */ 15 | class LinuxPlatformTool extends PlatformTool { 16 | // main.cc相对路径 17 | final String _mainCCPath = 'main.cc'; 18 | 19 | // my_application.cc相对路径 20 | final String _myApplicationPath = 'my_application.cc'; 21 | 22 | // label标签替换正则集合 23 | final _labelRegExpList = [ 24 | RegExp(r'gtk_header_bar_set_title\(header_bar, "(.*)"\);'), 25 | RegExp(r'gtk_window_set_title\(window, "(.*)"\);'), 26 | ]; 27 | 28 | @override 29 | PlatformType get platform => PlatformType.linux; 30 | 31 | @override 32 | bool isPathAvailable(String projectPath) => 33 | File(join(getPlatformPath(projectPath), _mainCCPath)).existsSync(); 34 | 35 | @override 36 | Future?> getPlatformInfo( 37 | String projectPath) async { 38 | if (!isPathAvailable(projectPath)) return null; 39 | return ( 40 | path: getPlatformPath(projectPath), 41 | label: await getLabel(projectPath) ?? '', 42 | package: await getPackage(projectPath) ?? '', 43 | logos: await getLogos(projectPath) ?? [], 44 | permissions: [], 45 | info: (), 46 | ); 47 | } 48 | 49 | @override 50 | Future getLabel(String projectPath) async { 51 | if (!isPathAvailable(projectPath)) return null; 52 | final content = await readPlatformFile(projectPath, _myApplicationPath); 53 | for (var e in _labelRegExpList) { 54 | final value = content.regFirstGroup(e.pattern, 1); 55 | if (value.isNotEmpty) return value; 56 | } 57 | return null; 58 | } 59 | 60 | @override 61 | Future setLabel(String projectPath, String label) async { 62 | if (!isPathAvailable(projectPath)) return false; 63 | var content = await readPlatformFile(projectPath, _myApplicationPath); 64 | for (var reg in _labelRegExpList) { 65 | final temp = reg.pattern.replaceFirst('(.*)', label); 66 | content = content.replaceFirst(reg, temp.replaceAll('\\', '')); 67 | } 68 | await writePlatformFile(projectPath, _myApplicationPath, content); 69 | return true; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /lib/widget/empty_box.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /* 4 | * 空盒子 5 | * @author wuxubaiyang 6 | * @Time 2023/11/28 15:13 7 | */ 8 | class EmptyBoxView extends StatelessWidget { 9 | // 子元素 10 | final Widget? child; 11 | 12 | // 构造器 13 | final TransitionBuilder? builder; 14 | 15 | // 是否为空 16 | final bool isEmpty; 17 | 18 | // 提示 19 | final String hint; 20 | 21 | // 提示字体大小 22 | final TextStyle? hintStyle; 23 | 24 | // 自定义颜色 25 | final Color? color; 26 | 27 | // 空图片尺寸 28 | final double placeholderSize; 29 | 30 | // 动画时长 31 | final Duration duration; 32 | 33 | // 自定义图标 34 | final IconData? iconData; 35 | 36 | // 自定义子元素(与iconData互斥) 37 | final Widget? icon; 38 | 39 | const EmptyBoxView({ 40 | super.key, 41 | required this.isEmpty, 42 | this.icon, 43 | this.child, 44 | this.color, 45 | this.builder, 46 | this.iconData, 47 | this.hint = '', 48 | this.hintStyle, 49 | this.placeholderSize = 100, 50 | this.duration = const Duration(milliseconds: 150), 51 | }); 52 | 53 | @override 54 | Widget build(BuildContext context) { 55 | final crossFadeState = 56 | isEmpty ? CrossFadeState.showSecond : CrossFadeState.showFirst; 57 | return AnimatedCrossFade( 58 | duration: duration, 59 | crossFadeState: crossFadeState, 60 | secondChild: _buildPlaceholder(context), 61 | firstChild: builder?.call(context, child) ?? child ?? const SizedBox(), 62 | layoutBuilder: (topChild, topChildKey, bottomChild, bottomChildKey) { 63 | return Stack( 64 | fit: StackFit.expand, 65 | clipBehavior: Clip.none, 66 | children: [ 67 | Positioned.fill(key: bottomChildKey, child: bottomChild), 68 | Positioned.fill(key: topChildKey, child: topChild), 69 | ], 70 | ); 71 | }, 72 | ); 73 | } 74 | 75 | // 构建空白占位图 76 | Widget _buildPlaceholder(BuildContext context) { 77 | final titleStyle = Theme.of(context).textTheme.titleLarge; 78 | final color = this.color ?? titleStyle?.color?.withValues(alpha: 0.1); 79 | return Center( 80 | child: Column( 81 | mainAxisSize: MainAxisSize.min, 82 | children: [ 83 | icon ?? 84 | Icon( 85 | color: color, 86 | size: placeholderSize, 87 | iconData ?? Icons.inbox, 88 | ), 89 | const SizedBox(height: 14), 90 | Text(hint, 91 | textAlign: TextAlign.center, 92 | style: (hintStyle ?? titleStyle)?.copyWith(color: color)), 93 | ], 94 | ), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/widget/form_field/local_path.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:jtech_base/jtech_base.dart'; 4 | 5 | /* 6 | * 本地路径文本输入框组件 7 | * @author wuxubaiyang 8 | * @Time 2023/11/27 10:58 9 | */ 10 | class LocalPathFormField extends StatelessWidget { 11 | // 表单项key 12 | final GlobalKey> fieldKey; 13 | 14 | // 输入框控制器 15 | final TextEditingController? controller; 16 | 17 | // 验证器 18 | final FormFieldValidator? validator; 19 | 20 | // 路径选择更新回调 21 | final ValueChanged? onPathSelected; 22 | 23 | // 保存回调 24 | final FormFieldSetter? onSaved; 25 | 26 | // 标签 27 | final String label; 28 | 29 | // 提示 30 | final String hint; 31 | 32 | // 初始值 33 | final String? initialValue; 34 | 35 | // 是否只读 36 | final bool readOnly; 37 | 38 | // 是否选择路径 39 | final bool pickDirectory; 40 | 41 | LocalPathFormField({ 42 | super.key, 43 | GlobalKey>? fieldKey, 44 | this.onSaved, 45 | this.hint = '', 46 | this.validator, 47 | this.label = '', 48 | this.onPathSelected, 49 | this.initialValue, 50 | this.controller, 51 | this.readOnly = false, 52 | this.pickDirectory = true, 53 | }) : fieldKey = fieldKey ?? GlobalKey>(); 54 | 55 | @override 56 | Widget build(BuildContext context) { 57 | return TextFormField( 58 | key: fieldKey, 59 | onSaved: onSaved, 60 | readOnly: readOnly, 61 | controller: controller, 62 | initialValue: initialValue, 63 | validator: (v) { 64 | if (v == null || v.isEmpty) { 65 | return '路径不能为空'; 66 | } 67 | return validator?.call(v); 68 | }, 69 | decoration: InputDecoration( 70 | hintText: hint, 71 | labelText: label, 72 | suffixIcon: IconButton( 73 | onPressed: _pickLocalPath, 74 | icon: const Icon(Icons.folder), 75 | ), 76 | ), 77 | ); 78 | } 79 | 80 | // 选择本地路径 81 | Future _pickLocalPath() async { 82 | final initDir = controller?.text ?? fieldKey.currentState?.value; 83 | final initFileDir = initDir != null ? File(initDir).parent.path : ''; 84 | final result = await (pickDirectory 85 | ? Picker.directory(dialogTitle: label, initialDirectory: initDir) 86 | : Picker.file(dialogTitle: label, initialDirectory: initFileDir)); 87 | if (result == null) return; 88 | if (controller != null) { 89 | controller!.text = result; 90 | } else { 91 | fieldKey.currentState?.didChange(result); 92 | } 93 | onPathSelected?.call(result); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/provider/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/widget/scheme_picker.dart'; 3 | import 'package:jtech_base/jtech_base.dart'; 4 | 5 | /* 6 | * 主题提供者 7 | * @author wuxubaiyang 8 | * @Time 2022/3/17 14:14 9 | */ 10 | class ThemeProvider extends BaseThemeProvider { 11 | ThemeProvider(super.context); 12 | 13 | @override 14 | ThemeData createTheme(ThemeData themeData, Brightness brightness) { 15 | return themeData.copyWith( 16 | focusColor: Colors.transparent, 17 | appBarTheme: AppBarTheme( 18 | scrolledUnderElevation: 0, 19 | ), 20 | cardTheme: CardThemeData( 21 | shadowColor: Colors.black26, 22 | clipBehavior: Clip.antiAlias, 23 | ), 24 | dividerTheme: const DividerThemeData( 25 | space: 0, 26 | thickness: 0.3, 27 | ), 28 | bottomSheetTheme: const BottomSheetThemeData( 29 | showDragHandle: true, 30 | ), 31 | searchBarTheme: SearchBarThemeData( 32 | elevation: WidgetStatePropertyAll(2), 33 | constraints: BoxConstraints.tightFor(height: 45), 34 | // backgroundColor: WidgetStatePropertyAll(cardColor), 35 | ), 36 | tabBarTheme: const TabBarThemeData( 37 | dividerHeight: 0, 38 | ), 39 | sliderTheme: const SliderThemeData( 40 | trackHeight: 2, 41 | thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6), 42 | overlayShape: RoundSliderOverlayShape(overlayRadius: 14), 43 | showValueIndicator: ShowValueIndicator.onlyForContinuous, 44 | ), 45 | ); 46 | } 47 | 48 | @override 49 | CustomTheme? createCustomTheme(ThemeData themeData, Brightness brightness) { 50 | return CustomTheme( 51 | customDialogTheme: CustomDialogThemeData( 52 | style: CustomDialogStyle( 53 | constraints: const BoxConstraints(maxWidth: 380, maxHeight: 380), 54 | ), 55 | ), 56 | ); 57 | } 58 | 59 | // 展示主题切换对话框 60 | Future showSchemePickerDialog(BuildContext context) async { 61 | final result = await showSchemePicker(context, 62 | current: scheme, themeSchemes: themeSchemes); 63 | return changeThemeScheme(result); 64 | } 65 | } 66 | 67 | /* 68 | * 扩展主题模式枚举类,添加中文名称属性 69 | * 70 | * @author wuxubaiyang 71 | * @Time 2023/11/24 15:47 72 | */ 73 | extension ThemeModeExtension on ThemeMode { 74 | // 获取主题模式名称 75 | String get label => switch (this) { 76 | ThemeMode.light => '浅色模式', 77 | ThemeMode.dark => '深色模式', 78 | ThemeMode.system => '跟随系统', 79 | }; 80 | } 81 | 82 | /* 83 | * 扩展亮度枚举类,添加中文名称属性 84 | * @author wuxubaiyang 85 | * @Time 2023/11/24 15:49 86 | */ 87 | extension BrightnessExtension on Brightness { 88 | // 获取亮度名称 89 | String get label => switch (this) { 90 | Brightness.light => '浅色', 91 | Brightness.dark => '深色', 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /lib/tool/project/platform/windows.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter_manager/tool/image.dart'; 3 | import 'package:flutter_manager/tool/tool.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | import 'platform.dart'; 6 | 7 | // windows平台参数元组 8 | typedef WindowsPlatformInfo = (); 9 | 10 | /* 11 | * windows平台工具类 12 | * @author wuxubaiyang 13 | * @Time 2023/11/29 14:59 14 | */ 15 | class WindowsPlatformTool extends PlatformTool { 16 | // mainCPP文件路径 17 | final String _mainCPPPath = 'runner/main.cpp'; 18 | 19 | // 资源相对路径 20 | final String _resPath = 'runner/resources'; 21 | 22 | // label字段匹配 23 | final _labelRegExp = RegExp(r'window.Create\(L"(.*)", origin, size\)'); 24 | 25 | // label输入限制 26 | static final labelValidatorRegExp = RegExp(r'^[a-zA-Z1-9_]+$'); 27 | 28 | @override 29 | PlatformType get platform => PlatformType.windows; 30 | 31 | @override 32 | bool isPathAvailable(String projectPath) => 33 | File(join(getPlatformPath(projectPath), _mainCPPPath)).existsSync(); 34 | 35 | @override 36 | Future?> getPlatformInfo( 37 | String projectPath) async { 38 | if (!isPathAvailable(projectPath)) return null; 39 | return ( 40 | path: getPlatformPath(projectPath), 41 | label: await getLabel(projectPath) ?? '', 42 | package: await getPackage(projectPath) ?? '', 43 | logos: await getLogos(projectPath) ?? [], 44 | permissions: [], 45 | info: (), 46 | ); 47 | } 48 | 49 | @override 50 | Future getLabel(String projectPath) async { 51 | if (!isPathAvailable(projectPath)) return null; 52 | final content = await readPlatformFile(projectPath, _mainCPPPath); 53 | return content.regFirstGroup(_labelRegExp.pattern, 1); 54 | } 55 | 56 | @override 57 | Future setLabel(String projectPath, String label) async { 58 | if (!isPathAvailable(projectPath)) return false; 59 | // 如果输入的label不合法,直接返回false 60 | if (!labelValidatorRegExp.hasMatch(label)) return false; 61 | var content = await readPlatformFile(projectPath, _mainCPPPath); 62 | final temp = _labelRegExp.pattern.replaceFirst('(.*)', label); 63 | content = content.replaceFirst(_labelRegExp, temp.replaceAll('\\', '')); 64 | await writePlatformFile(projectPath, _mainCPPPath, content); 65 | return true; 66 | } 67 | 68 | @override 69 | Future?> getLogos(String projectPath) async { 70 | if (!isPathAvailable(projectPath)) return null; 71 | final dir = Directory(getPlatformFilePath(projectPath, _resPath)); 72 | final result = []; 73 | for (final file in dir.listSync()) { 74 | final path = file.path; 75 | final name = basename(path); 76 | final size = await ImageTool.getSize(path); 77 | if (size == null) continue; 78 | result.add((name: name, path: path, size: size)); 79 | } 80 | return result; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/widget/popup_menu_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /* 4 | * 自定义PopupMenuButton 5 | * @author wuxubaiyang 6 | * @Time 2023/12/6 8:48 7 | */ 8 | class CustomPopupMenuButton extends PopupMenuButton { 9 | const CustomPopupMenuButton.filled({ 10 | super.key, 11 | required super.itemBuilder, 12 | super.initialValue, 13 | super.onOpened, 14 | super.onSelected, 15 | super.onCanceled, 16 | super.tooltip, 17 | super.elevation, 18 | super.shadowColor, 19 | super.surfaceTintColor, 20 | super.padding = const EdgeInsets.all(8.0), 21 | super.child, 22 | super.splashRadius, 23 | super.icon, 24 | super.iconSize, 25 | super.offset = Offset.zero, 26 | super.enabled = true, 27 | super.shape, 28 | super.color, 29 | super.iconColor, 30 | super.enableFeedback, 31 | super.constraints, 32 | super.position, 33 | super.clipBehavior = Clip.none, 34 | }); 35 | 36 | @override 37 | PopupMenuButtonState createState() => _CustomPopupMenuButtonState(); 38 | } 39 | 40 | /* 41 | * 自定义PopupMenuButton-状态 42 | * @author wuxubaiyang 43 | * @Time 2023/12/6 8:48 44 | */ 45 | class _CustomPopupMenuButtonState extends PopupMenuButtonState { 46 | @override 47 | Widget build(BuildContext context) { 48 | final IconThemeData iconTheme = IconTheme.of(context); 49 | final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); 50 | final bool enableFeedback = widget.enableFeedback ?? 51 | PopupMenuTheme.of(context).enableFeedback ?? 52 | true; 53 | 54 | assert(debugCheckHasMaterialLocalizations(context)); 55 | 56 | if (widget.child != null) { 57 | return Tooltip( 58 | message: 59 | widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, 60 | child: InkWell( 61 | onTap: widget.enabled ? showButtonMenu : null, 62 | canRequestFocus: _canRequestFocus, 63 | radius: widget.splashRadius, 64 | enableFeedback: enableFeedback, 65 | child: widget.child, 66 | ), 67 | ); 68 | } 69 | 70 | return IconButton.filled( 71 | icon: widget.icon ?? Icon(Icons.adaptive.more), 72 | padding: widget.padding, 73 | splashRadius: widget.splashRadius, 74 | visualDensity: VisualDensity.compact, 75 | iconSize: widget.iconSize ?? popupMenuTheme.iconSize ?? iconTheme.size, 76 | tooltip: 77 | widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, 78 | onPressed: widget.enabled ? showButtonMenu : null, 79 | enableFeedback: enableFeedback, 80 | ); 81 | } 82 | 83 | bool get _canRequestFocus { 84 | final NavigationMode mode = 85 | MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional; 86 | switch (mode) { 87 | case NavigationMode.traditional: 88 | return widget.enabled; 89 | case NavigationMode.directional: 90 | return true; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/provider/environment.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:archive/archive_io.dart'; 3 | import 'package:flutter_manager/database/database.dart'; 4 | import 'package:flutter_manager/database/model/environment.dart'; 5 | import 'package:flutter_manager/model/env_package.dart'; 6 | import 'package:flutter_manager/tool/project/environment.dart'; 7 | import 'package:jtech_base/jtech_base.dart'; 8 | 9 | /* 10 | * 环境变量提供者 11 | * @author wuxubaiyang 12 | * @Time 2023/11/26 13:29 13 | */ 14 | class EnvironmentProvider extends BaseProvider { 15 | EnvironmentProvider(super.context); 16 | 17 | // 环境变量集合 18 | late List _environments = database.getEnvList(desc: true); 19 | 20 | // 获取环境变量集合 21 | List get environments => _environments; 22 | 23 | // 判断是否存在环境信息 24 | bool get hasEnvironment => _environments.isNotEmpty; 25 | 26 | // 导入环境变量 27 | Future import(String path) async { 28 | final result = await EnvironmentTool.getInfo(path); 29 | if (result == null) throw Exception('查询flutter信息失败'); 30 | return update(result); 31 | } 32 | 33 | // 导入压缩包的环境变量 34 | Future importArchive(EnvPackage package) async { 35 | if (!package.canImport) return null; 36 | var buildPath = package.buildPath; 37 | await extractFileToDisk( 38 | package.downloadPath!, 39 | buildPath!, 40 | ); 41 | final list = Directory(buildPath).listSync(); 42 | return import(list.length <= 1 ? list.first.path : buildPath); 43 | } 44 | 45 | // 刷新环境变量 46 | Future refresh(Environment env) async { 47 | final result = await EnvironmentTool.getInfo(env.path); 48 | if (result == null) throw Exception('查询flutter信息失败'); 49 | return update(result..id = env.id); 50 | } 51 | 52 | // 添加环境变量 53 | Future update(Environment item) async { 54 | final result = await database.updateEnv(item); 55 | reload(); 56 | return result; 57 | } 58 | 59 | // 移除环境变量 60 | bool remove(Environment item) { 61 | final result = database.removeEnv(item.id); 62 | if (result) reload(); 63 | return result; 64 | } 65 | 66 | // 验证是否可移除环境变量 67 | String? removeValidator(Environment item) { 68 | final result = database.getProjectsByEnvironmentId(item.id); 69 | if (result.isEmpty) return null; 70 | final length = result.length; 71 | final label = result.first.label; 72 | return '${length > 1 ? '$label 等 ${length - 1} 个' : label} 项目正在依赖该环境'; 73 | } 74 | 75 | // 环境重排序 76 | Future reorder(int oldIndex, int newIndex) async { 77 | final temp = environments.reversed.toList().swapReorder(oldIndex, newIndex); 78 | temp.asMap().forEach((i, e) => e.order = i); 79 | temp.sort((a, b) => a.order.compareTo(b.order)); 80 | _environments = temp.reversed.toList(); 81 | notifyListeners(); 82 | await database.updateEnvs(temp); 83 | } 84 | 85 | // 重新加载环境变量列表 86 | void reload() { 87 | _environments = database.getEnvList(desc: true); 88 | notifyListeners(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/tool/project/platform/web.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_manager/tool/image.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | import 'platform.dart'; 6 | 7 | // web平台参数元组 8 | typedef WebPlatformInfo = (); 9 | 10 | /* 11 | * web平台工具类 12 | * @author wuxubaiyang 13 | * @Time 2023/11/29 14:59 14 | */ 15 | class WebPlatformTool extends PlatformTool { 16 | // 入口页路径 17 | final String _indexPath = 'index.html'; 18 | 19 | // manifest.json相对路径 20 | final String _manifestPath = 'manifest.json'; 21 | 22 | // favicon.ico相对路径 23 | final String _faviconPath = 'favicon.png'; 24 | 25 | // 读取manifest文件信息 26 | Future _getManifestJson(String projectPath) => 27 | readPlatformFileJson(projectPath, _manifestPath); 28 | 29 | @override 30 | PlatformType get platform => PlatformType.web; 31 | 32 | @override 33 | bool isPathAvailable(String projectPath) => 34 | File(join(getPlatformPath(projectPath), _indexPath)).existsSync(); 35 | 36 | @override 37 | Future?> getPlatformInfo( 38 | String projectPath) async { 39 | if (!isPathAvailable(projectPath)) return null; 40 | return ( 41 | path: getPlatformPath(projectPath), 42 | label: await getLabel(projectPath) ?? '', 43 | package: await getPackage(projectPath) ?? '', 44 | logos: await getLogos(projectPath) ?? [], 45 | permissions: [], 46 | info: (), 47 | ); 48 | } 49 | 50 | @override 51 | Future getLabel(String projectPath) async { 52 | if (!isPathAvailable(projectPath)) return null; 53 | final json = await _getManifestJson(projectPath); 54 | return json['name']; 55 | } 56 | 57 | @override 58 | Future setLabel(String projectPath, String label) async { 59 | if (!isPathAvailable(projectPath)) return false; 60 | final json = await _getManifestJson(projectPath); 61 | json['name'] = label; 62 | await writePlatformFileJson(projectPath, _manifestPath, json); 63 | return true; 64 | } 65 | 66 | @override 67 | Future?> getLogos(String projectPath) async { 68 | if (!isPathAvailable(projectPath)) return null; 69 | final json = await _getManifestJson(projectPath); 70 | final faviconSize = 71 | await ImageTool.getSize(getPlatformFilePath(projectPath, _faviconPath)); 72 | final result = [ 73 | if (faviconSize != null) 74 | ( 75 | name: 'favicon', 76 | path: getPlatformFilePath(projectPath, _faviconPath), 77 | size: faviconSize, 78 | ) 79 | ]; 80 | for (final item in json['icons'] ?? []) { 81 | final src = item['src']; 82 | final entries = (item as Map)..removeWhere((_, value) => value == src); 83 | final name = entries.values.join('_'); 84 | final path = getPlatformFilePath(projectPath, src); 85 | final size = await ImageTool.getSize(path); 86 | if (size == null) continue; 87 | result.add((name: name, path: path, size: size)); 88 | } 89 | return result; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/widget/form_field/project_logo.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/widget/dialog/image_editor.dart'; 3 | import 'package:jtech_base/jtech_base.dart'; 4 | 5 | /* 6 | * 项目图标表单字段组件 7 | * @author wuxubaiyang 8 | * @Time 2023/12/4 19:31 9 | */ 10 | class ProjectLogoFormField extends StatelessWidget { 11 | // 持有key 12 | final Key? fieldKey; 13 | 14 | // 保存回调 15 | final FormFieldSetter? onSaved; 16 | 17 | // 图标尺寸 18 | final Size logoSize; 19 | 20 | // 初始值 21 | final String? initialValue; 22 | 23 | // 圆角 24 | final BorderRadius borderRadius; 25 | 26 | const ProjectLogoFormField({ 27 | super.key, 28 | this.onSaved, 29 | this.fieldKey, 30 | this.initialValue, 31 | this.logoSize = const Size.square(55), 32 | this.borderRadius = const BorderRadius.all(Radius.circular(4)), 33 | }); 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return FormField( 38 | key: fieldKey, 39 | onSaved: onSaved, 40 | initialValue: initialValue, 41 | validator: (v) { 42 | if (v?.isEmpty ?? true) { 43 | return '请选择图标'; 44 | } 45 | return null; 46 | }, 47 | builder: (field) { 48 | return Tooltip( 49 | message: '选择项目图标', 50 | child: _buildFormField(context, field), 51 | ); 52 | }, 53 | ); 54 | } 55 | 56 | // 构建表单字段 57 | Widget _buildFormField(BuildContext context, FormFieldState field) { 58 | final logoPath = field.value; 59 | final inputBorder = logoPath?.isEmpty ?? true 60 | ? const OutlineInputBorder() 61 | : InputBorder.none; 62 | return ClipRRect( 63 | borderRadius: borderRadius, 64 | child: InkWell( 65 | onTap: () => _changeLogoFile(context, field), 66 | child: ConstrainedBox( 67 | constraints: BoxConstraints.tightFor(width: logoSize.width), 68 | child: InputDecorator( 69 | decoration: InputDecoration( 70 | border: inputBorder, 71 | errorText: field.errorText, 72 | contentPadding: const EdgeInsets.symmetric(vertical: 4), 73 | ), 74 | child: SizedBox.fromSize( 75 | size: logoSize, 76 | child: _buildFormFieldLogo(logoPath), 77 | ), 78 | ), 79 | ), 80 | ), 81 | ); 82 | } 83 | 84 | // 切换项目图标 85 | Future _changeLogoFile( 86 | BuildContext context, FormFieldState field) async { 87 | var result = await Picker.image(dialogTitle: '选择项目图标'); 88 | if (result == null || !context.mounted) return; 89 | result = await showImageEditor(context, 90 | path: result, absoluteRatio: CropAspectRatio.ratio1_1); 91 | if (result == null) return; 92 | field.didChange(result); 93 | field.validate(); 94 | } 95 | 96 | // 构建表单项图标 97 | Widget _buildFormFieldLogo(String? logoPath) { 98 | if (logoPath?.isEmpty ?? true) return const Icon(Icons.add); 99 | return CustomImage.file(logoPath ?? '', size: logoSize); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/tool/template.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:flutter_manager/common/common.dart'; 6 | import 'package:flutter_manager/model/create_template.dart'; 7 | import 'package:jtech_base/jtech_base.dart'; 8 | 9 | /* 10 | * 从模板创建项目 11 | * @author wuxubaiyang 12 | * @Time 2025/6/18 13:59 13 | */ 14 | class TemplateCreate { 15 | // 开始创建项目(返回创建项目地址) 16 | static Future start( 17 | CreateTemplate template, { 18 | StreamSink? stdOut, 19 | StreamSink? stdErr, 20 | }) async { 21 | stdOut?.add('*st:检查git环境'); 22 | if (!await checkGit()) throw Exception('未检测到git环境'); 23 | // 检查缓存目录是否存在模板项目,已存在则clone更新,不存在则从github克隆 24 | final cacheDir = await getApplicationCacheDirectory(); 25 | final templatePath = join(cacheDir.path, Common.templateName); 26 | stdOut?.add('*st:拉取/更新模板项目'); 27 | if (!await _updateTemplate(templatePath)) { 28 | throw Exception('模板拉取/更新失败,请重试'); 29 | } 30 | // 执行模板项目中的创建脚本 31 | final process = await Process.start( 32 | join(templatePath, Common.templateCreateScript), 33 | template.toCommand(), 34 | runInShell: true, 35 | workingDirectory: templatePath, 36 | ); 37 | stdOut?.addStream(process.stdout.transform(utf8.decoder)); 38 | stdErr?.addStream(process.stderr.transform(utf8.decoder)); 39 | if (await process.exitCode != 0) return null; 40 | return join(template.targetDir, template.projectName); 41 | } 42 | 43 | // 更新/拉取最新模板源码 44 | static Future _updateTemplate(String templatePath) async { 45 | final hasCache = await _checkTemplateCache(templatePath); 46 | // 存在缓存且缓存正确则放弃并更新源码 47 | // 不存在缓存则克隆源码 48 | final cmd = hasCache 49 | ? ['git', 'pull', 'origin'] 50 | : ['git', 'clone', Common.templateUrl, templatePath]; 51 | // 如果不存在模板源码则创建目录再克隆 52 | if (!hasCache) Directory(templatePath).createSync(recursive: true); 53 | final result = await _execCommand(cmd, workingDirectory: templatePath); 54 | return result.exitCode == 0; 55 | } 56 | 57 | // 检查模板项目本地是否已缓存(.git是否存在,git的远程地址是否正确) 58 | static Future _checkTemplateCache(String projectPath) async { 59 | if (!Directory(join(projectPath, '.git')).existsSync()) return false; 60 | // 检查目标路径项目源是否正确,不匹配需要抛出异常 61 | final cmd = ['git', 'remote', 'get-url', 'origin']; 62 | final result = await _execCommand(cmd, workingDirectory: projectPath); 63 | if (result.exitCode != 0) return false; 64 | if (result.stdout.trim() != Common.templateUrl) { 65 | throw Exception('源不匹配,请删除($projectPath)整个目录后重试'); 66 | } 67 | return true; 68 | } 69 | 70 | // 执行命令 71 | static Future _execCommand( 72 | List command, { 73 | String? workingDirectory, 74 | }) async { 75 | return Process.run( 76 | command[0], 77 | command.sublist(1), 78 | runInShell: true, 79 | stderrEncoding: utf8, 80 | stdoutEncoding: utf8, 81 | workingDirectory: workingDirectory, 82 | ); 83 | } 84 | 85 | // 检查是否存在git 86 | static Future checkGit() async { 87 | try { 88 | final result = await Process.run('git', ['--version']); 89 | if (result.exitCode == 0) return true; 90 | } catch (e) { 91 | Log.w('未检测到git'); 92 | } 93 | return false; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/tabbar.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 4 | import 'package:flutter_manager/widget/dialog/project/label.dart'; 5 | import 'package:flutter_manager/widget/dialog/project/logo.dart'; 6 | import 'package:jtech_base/jtech_base.dart'; 7 | import 'platform/widgets/provider.dart'; 8 | 9 | /* 10 | * 项目详情页TabBar组件 11 | * @author wuxubaiyang 12 | * @Time 2023/12/13 16:22 13 | */ 14 | class ProjectDetailTabBar extends StatelessWidget { 15 | // 平台集合 16 | final List platforms; 17 | 18 | const ProjectDetailTabBar({super.key, required this.platforms}); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final tabs = platforms.map((e) => Tab(text: e.name)).toList(); 23 | return Row(children: [ 24 | TabBar( 25 | isScrollable: true, 26 | tabAlignment: TabAlignment.start, 27 | splashBorderRadius: BorderRadius.circular(4), 28 | tabs: [ 29 | ...tabs, 30 | if (tabs.isEmpty) const Tab(text: '暂无平台'), 31 | ], 32 | ), 33 | Expanded(child: _buildActions(context, platforms)), 34 | const SizedBox(width: 8), 35 | ]); 36 | } 37 | 38 | // 构建平台聚合操作项 39 | Widget _buildActions(BuildContext context, List platforms) { 40 | final provider = context.read(); 41 | final createPlatforms = 42 | PlatformType.values.where((e) => !platforms.contains(e)).toList(); 43 | return Row(children: [ 44 | if (createPlatforms.isNotEmpty) 45 | PopupMenuButton( 46 | iconSize: 20, 47 | tooltip: '创建平台', 48 | icon: const Icon(Icons.add), 49 | onSelected: (v) => provider.createPlatform(v).loading(context), 50 | itemBuilder: (_) => createPlatforms 51 | .map((e) => PopupMenuItem( 52 | value: e, 53 | child: Text(e.name), 54 | )) 55 | .toList(), 56 | ), 57 | const Spacer(), 58 | Tooltip( 59 | message: '替换项目名', 60 | child: TextButton.icon( 61 | label: const Text('名称'), 62 | icon: const Icon(Icons.edit_attributes_rounded, size: 18), 63 | onPressed: () async { 64 | final result = await showProjectLabel( 65 | context, 66 | labelMap: provider.labelMap, 67 | ); 68 | if (result == null || !context.mounted) return; 69 | provider.updateLabels(result).loading(context, dismissible: false); 70 | }, 71 | ), 72 | ), 73 | Tooltip( 74 | message: '替换图标', 75 | child: TextButton.icon( 76 | label: const Text('图标'), 77 | onPressed: () async { 78 | final result = await showProjectLogo( 79 | context, 80 | logoMap: provider.logoMap, 81 | ); 82 | if (result == null || !context.mounted) return; 83 | final controller = StreamController(); 84 | provider 85 | .updateLogos(result, controller: controller) 86 | .loading(context, dismissible: false); 87 | }, 88 | icon: const Icon(Icons.imagesearch_roller_rounded, size: 18), 89 | ), 90 | ), 91 | ]); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/tool/download.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'package:jtech_base/jtech_base.dart'; 4 | 5 | // 下载进度回调 6 | typedef DownloaderProgressCallback = void Function( 7 | int count, int total, int speed); 8 | 9 | // 下载进度元组 10 | typedef DownloadInfo = ({ 11 | double progress, 12 | int speed, 13 | int total, 14 | int count, 15 | }); 16 | 17 | /* 18 | * 基于dio实现的文件下载器 19 | * @author wuxubaiyang 20 | * @Time 2023/11/26 20:50 21 | */ 22 | class Downloader { 23 | // 文件下载 24 | static Future start( 25 | String downloadUrl, 26 | String savePath, { 27 | CancelToken? cancelToken, 28 | DownloaderProgressCallback? onReceiveProgress, 29 | StreamController? downloadStream, 30 | Duration streamDelay = const Duration(seconds: 1), 31 | }) async { 32 | int beginIndex = 0; 33 | final file = File(savePath); 34 | // 如果存在已下载文件则获取文件长度 35 | if (file.existsSync()) beginIndex = file.lengthSync(); 36 | final resp = await Dio().get( 37 | downloadUrl, 38 | options: Options( 39 | followRedirects: false, 40 | responseType: ResponseType.stream, 41 | headers: {"range": "bytes=$beginIndex-"}, 42 | ), 43 | ); 44 | final supportPause = _supportPause(resp); 45 | if (!supportPause) beginIndex = 0; 46 | final completer = Completer(); 47 | final raf = file.openSync( 48 | mode: supportPause ? FileMode.append : FileMode.write, 49 | ); 50 | int lastReceived = beginIndex, 51 | received = beginIndex, 52 | total = _getContentLength(resp); 53 | if (supportPause) total += beginIndex; 54 | int tempSpeed = 0; 55 | final subscription = resp.data?.stream.listen((data) { 56 | raf.writeFromSync(data); 57 | received += data.length; 58 | tempSpeed += received - lastReceived; 59 | lastReceived = received; 60 | Throttle.c(() { 61 | onReceiveProgress?.call(received, total, tempSpeed); 62 | downloadStream?.add(( 63 | progress: received / total, 64 | speed: tempSpeed, 65 | total: total, 66 | count: received, 67 | )); 68 | tempSpeed = 0; 69 | }, 'download_update', delay: streamDelay); 70 | }, onDone: () { 71 | raf.close(); 72 | if (completer.isCompleted) return; 73 | completer.complete(file); 74 | }, onError: (e) { 75 | raf.close(); 76 | if (completer.isCompleted) return; 77 | completer.complete(null); 78 | }, cancelOnError: true); 79 | // 监听取消事件 80 | cancelToken?.whenCancel.then((_) async { 81 | subscription?.cancel(); 82 | raf.close(); 83 | if (completer.isCompleted) return; 84 | completer.complete(null); 85 | }); 86 | return completer.future; 87 | } 88 | 89 | // 获取文件大小 90 | static int _getContentLength(Response resp) { 91 | var contentLength = resp.headers.value(HttpHeaders.contentLengthHeader); 92 | return int.tryParse(contentLength ?? '') ?? 0; 93 | } 94 | 95 | // 判断是否支持断点续传 96 | static bool _supportPause(Response resp) { 97 | final keys = resp.headers.map.keys; 98 | return [ 99 | HttpHeaders.contentRangeHeader, 100 | HttpHeaders.ifRangeHeader, 101 | HttpHeaders.rangeHeader, 102 | ].any(keys.contains); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/database/model/project.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/database/model/environment.dart'; 3 | import 'package:jtech_base/jtech_base.dart'; 4 | 5 | @Entity() 6 | class Project { 7 | int id = 0; 8 | 9 | // 项目名(自定义) 10 | String label = ''; 11 | 12 | // 项目图标(自定义) 13 | String logo = ''; 14 | 15 | // 项目路径 16 | String path = ''; 17 | 18 | // 项目颜色 19 | @Transient() 20 | Color color = Colors.primaries.last; 21 | 22 | // 是否订到顶部 23 | bool pinned = false; 24 | 25 | // 项目排序 26 | int order = 0; 27 | 28 | // 环境信息 29 | final environmentDB = ToOne(); 30 | 31 | // 创建时间 32 | @Property(type: PropertyType.date) 33 | DateTime createAt = DateTime.now(); 34 | 35 | // 更新时间 36 | @Property(type: PropertyType.date) 37 | DateTime updateAt = DateTime.now(); 38 | 39 | Project(); 40 | 41 | Project.c({ 42 | required this.id, 43 | required this.label, 44 | required this.logo, 45 | required this.path, 46 | required this.color, 47 | required this.pinned, 48 | required this.order, 49 | required this.createAt, 50 | required this.updateAt, 51 | required Environment? environment, 52 | }) { 53 | environmentDB.target = environment; 54 | } 55 | 56 | Project.create({ 57 | required this.label, 58 | required this.logo, 59 | required this.path, 60 | required this.pinned, 61 | required this.color, 62 | required Environment environment, 63 | }) { 64 | environmentDB.target = environment; 65 | } 66 | 67 | // 设置数据库颜色 68 | set colorDB(int value) => color = Color(value); 69 | 70 | // 获取数据库颜色 71 | int get colorDB => color.toARGB32(); 72 | 73 | // 获取环境 74 | @Transient() 75 | Environment? get environment => environmentDB.target; 76 | 77 | // 获取环境id 78 | @Transient() 79 | int? get envId => environment?.id; 80 | 81 | // 实现copyWith 82 | Project copyWith({ 83 | String? label, 84 | String? logo, 85 | String? path, 86 | Color? color, 87 | bool? pinned, 88 | int? order, 89 | DateTime? createAt, 90 | DateTime? updateAt, 91 | Environment? environment, 92 | }) { 93 | return Project.c( 94 | id: id, 95 | label: label ?? this.label, 96 | logo: logo ?? this.logo, 97 | path: path ?? this.path, 98 | color: color ?? this.color, 99 | pinned: pinned ?? this.pinned, 100 | order: order ?? this.order, 101 | createAt: createAt ?? this.createAt, 102 | updateAt: updateAt ?? this.updateAt, 103 | environment: environment ?? this.environment, 104 | ); 105 | } 106 | 107 | @override 108 | int get hashCode => 109 | id.hashCode & 110 | label.hashCode & 111 | logo.hashCode & 112 | path.hashCode & 113 | environment.hashCode & 114 | color.hashCode & 115 | pinned.hashCode & 116 | order.hashCode & 117 | createAt.hashCode & 118 | updateAt.hashCode; 119 | 120 | @override 121 | bool operator ==(Object other) => 122 | other is Project && 123 | id == other.id && 124 | label == other.label && 125 | logo == other.logo && 126 | path == other.path && 127 | environment == other.environment && 128 | color == other.color && 129 | pinned == other.pinned && 130 | order == other.order && 131 | createAt == other.createAt && 132 | updateAt == other.updateAt; 133 | } 134 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "flutter_manager" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "flutter_manager" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "flutter_manager.exe" "\0" 98 | VALUE "ProductName", "flutter_manager" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_localizations/flutter_localizations.dart'; 3 | import 'package:flutter_manager/common/common.dart'; 4 | import 'package:flutter_manager/database/database.dart'; 5 | import 'package:flutter_manager/provider/theme.dart'; 6 | import 'package:jtech_base/jtech_base.dart'; 7 | import 'package:window_manager/window_manager.dart'; 8 | import 'common/router.dart'; 9 | import 'generated/l10n.dart'; 10 | import 'provider/config.dart'; 11 | import 'provider/environment.dart'; 12 | import 'provider/project.dart'; 13 | import 'provider/setting.dart'; 14 | import 'provider/window.dart'; 15 | 16 | void main() async { 17 | WidgetsFlutterBinding.ensureInitialized(); 18 | // 初始化工具方法 19 | await localCache.initialize(); 20 | await database.initialize(Common.defaultCacheName); 21 | // 初始化窗口管理 22 | const windowSize = Size(800, 600); 23 | await windowManager.ensureInitialized(); 24 | WindowOptions windowOptions = const WindowOptions( 25 | center: true, 26 | size: windowSize, 27 | skipTaskbar: false, 28 | minimumSize: windowSize, 29 | titleBarStyle: TitleBarStyle.hidden, 30 | ); 31 | windowManager.waitUntilReadyToShow(windowOptions, () async { 32 | await windowManager.show(); 33 | await windowManager.focus(); 34 | }); 35 | runApp(MyApp()); 36 | } 37 | 38 | class MyApp extends ProviderView { 39 | MyApp({super.key}); 40 | 41 | @override 42 | List loadProviders(BuildContext context) => [ 43 | ChangeNotifierProvider(create: (context) => ThemeProvider(context)), 44 | ChangeNotifierProvider(create: (context) => WindowProvider(context)), 45 | ChangeNotifierProvider( 46 | create: (context) => EnvironmentProvider(context), 47 | ), 48 | ChangeNotifierProvider(create: (context) => ProjectProvider(context)), 49 | ChangeNotifierProvider(create: (context) => SettingProvider(context)), 50 | ChangeNotifierProvider( 51 | create: (context) => ConfigProvider(context), 52 | ), 53 | ]; 54 | 55 | @override 56 | Widget buildWidget(BuildContext context) { 57 | return Consumer( 58 | builder: (_, theme, _) { 59 | return MaterialApp.router( 60 | title: 'Flutter管理', 61 | theme: theme.themeData, 62 | themeMode: theme.themeMode, 63 | darkTheme: theme.darkThemeData, 64 | debugShowCheckedModeBanner: false, 65 | routerConfig: router.createRouter(), 66 | supportedLocales: S.delegate.supportedLocales, 67 | localizationsDelegates: const [ 68 | S.delegate, 69 | GlobalWidgetsLocalizations.delegate, 70 | GlobalMaterialLocalizations.delegate, 71 | GlobalCupertinoLocalizations.delegate, 72 | ], 73 | ); 74 | }, 75 | ); 76 | } 77 | } 78 | 79 | // 扩展context 80 | extension GlobeProviderExtension on BuildContext { 81 | // 获取主题provider 82 | ThemeProvider get theme => Provider.of(this, listen: false); 83 | 84 | // 获取窗口provider 85 | WindowProvider get window => Provider.of(this, listen: false); 86 | 87 | // 获取环境provider 88 | EnvironmentProvider get env => 89 | Provider.of(this, listen: false); 90 | 91 | // 获取项目provider 92 | ProjectProvider get project => 93 | Provider.of(this, listen: false); 94 | 95 | // 获取设置provider 96 | SettingProvider get setting => 97 | Provider.of(this, listen: false); 98 | 99 | // 获取配置provider 100 | ConfigProvider get config => Provider.of(this, listen: false); 101 | } 102 | -------------------------------------------------------------------------------- /lib/widget/dialog/environment/import_local.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/database/model/environment.dart'; 3 | import 'package:flutter_manager/main.dart'; 4 | import 'package:flutter_manager/tool/project/environment.dart'; 5 | import 'package:flutter_manager/widget/form_field/local_path.dart'; 6 | import 'package:jtech_base/jtech_base.dart'; 7 | 8 | // 导入本地环境弹窗 9 | Future showImportEnvLocal( 10 | BuildContext context, { 11 | Environment? env, 12 | }) { 13 | return showDialog( 14 | context: context, 15 | barrierDismissible: false, 16 | builder: (_) => ImportEnvLocalDialog(env: env), 17 | ); 18 | } 19 | 20 | /* 21 | * 本地环境导入弹窗 22 | * @author wuxubaiyang 23 | * @Time 2023/11/26 10:17 24 | */ 25 | class ImportEnvLocalDialog extends ProviderView { 26 | // 环境对象 27 | final Environment? env; 28 | 29 | ImportEnvLocalDialog({super.key, this.env}); 30 | 31 | @override 32 | ImportEnvLocalDialogProvider createProvider(BuildContext context) => 33 | ImportEnvLocalDialogProvider(context, env ?? Environment()); 34 | 35 | // 判断是否为编辑状态 36 | bool get _isEdite => env != null; 37 | 38 | @override 39 | Widget buildWidget(BuildContext context) { 40 | return CustomDialog( 41 | scrollable: true, 42 | content: _buildContent(context), 43 | title: Text('${_isEdite ? '编辑' : '添加'}环境'), 44 | actions: [ 45 | TextButton( 46 | onPressed: context.pop, 47 | child: const Text('取消'), 48 | ), 49 | TextButton( 50 | child: Text(_isEdite ? '修改' : '添加'), 51 | onPressed: () => provider.submit(_isEdite).loading(context), 52 | ), 53 | ], 54 | ); 55 | } 56 | 57 | // 构建内容 58 | Widget _buildContent(BuildContext context) { 59 | return createSelector< Environment>( 60 | selector: (_, provider) => provider.env, 61 | builder: (_, env, _) { 62 | return Form( 63 | key: provider.formKey, 64 | child: Column(mainAxisSize: MainAxisSize.min, children: [ 65 | _buildFormFieldPath(context, env), 66 | ]), 67 | ); 68 | }, 69 | ); 70 | } 71 | 72 | // 构建表单项-flutter路径 73 | Widget _buildFormFieldPath(BuildContext context, Environment env) { 74 | return LocalPathFormField( 75 | label: 'flutter路径', 76 | initialValue: env.path, 77 | hint: '请选择flutter路径', 78 | onSaved: (v) => provider.updateFormData(path: v), 79 | validator: (v) { 80 | if (!EnvironmentTool.isAvailable(v)) { 81 | return '路径不可用'; 82 | } 83 | return null; 84 | }, 85 | ); 86 | } 87 | } 88 | 89 | class ImportEnvLocalDialogProvider extends BaseProvider { 90 | // 表单key 91 | final formKey = GlobalKey(); 92 | 93 | ImportEnvLocalDialogProvider(super.context, this._env); 94 | 95 | // 环境数据对象 96 | Environment _env; 97 | 98 | // 获取环境 99 | Environment get env => _env; 100 | 101 | // 导入环境 102 | Future submit(bool isEdite) async { 103 | try { 104 | final formState = formKey.currentState; 105 | if (formState == null || !formState.validate()) return null; 106 | formState.save(); 107 | final result = await (isEdite 108 | ? context.env.refresh(_env) 109 | : context.env.import(_env.path)); 110 | if (context.mounted) context.pop(); 111 | return result; 112 | } catch (e) { 113 | showNoticeError(e.toString(), title: '环境导入失败'); 114 | } 115 | return null; 116 | } 117 | 118 | // 更新表单数据 119 | void updateFormData({String? path}) { 120 | _env = _env.copyWith(path: path); 121 | notifyListeners(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/tool/project/platform/macos.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_manager/tool/image.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | import 'package:xml/xml.dart'; 6 | import 'platform.dart'; 7 | 8 | // macos平台参数元组 9 | typedef MacosPlatformInfo = (); 10 | 11 | /* 12 | * macos平台工具类 13 | * @author wuxubaiyang 14 | * @Time 2023/11/29 14:59 15 | */ 16 | class MacosPlatformTool extends PlatformTool { 17 | // macos信息配置文件 18 | final String _infoPlistPath = 'Runner/Info.plist'; 19 | 20 | // 图标资源路径 21 | final String _iconPath = 'Runner/Assets.xcassets/AppIcon.appiconset'; 22 | 23 | // 图标信息文件相对路径 24 | late final String _iconInfoPath = '$_iconPath/Contents.json'; 25 | 26 | // 读取plist文件信息 27 | Future _getPlistDocument(String projectPath) => 28 | readPlatformFileXml(projectPath, _infoPlistPath); 29 | 30 | // 获取plist文件fragment 31 | Future _getPlistFragment(String projectPath) => 32 | readPlatformFileXmlFragment(projectPath, _infoPlistPath); 33 | 34 | // 读取图标信息文件信息 35 | Future _getIconInfoJson(String projectPath) => 36 | readPlatformFileJson(projectPath, _iconInfoPath); 37 | 38 | @override 39 | PlatformType get platform => PlatformType.macos; 40 | 41 | @override 42 | bool isPathAvailable(String projectPath) => 43 | File(join(getPlatformPath(projectPath), _infoPlistPath)).existsSync(); 44 | 45 | @override 46 | Future?> getPlatformInfo( 47 | String projectPath) async { 48 | if (!isPathAvailable(projectPath)) return null; 49 | return ( 50 | path: getPlatformPath(projectPath), 51 | label: await getLabel(projectPath) ?? '', 52 | package: await getPackage(projectPath) ?? '', 53 | logos: await getLogos(projectPath) ?? [], 54 | permissions: [], 55 | info: (), 56 | ); 57 | } 58 | 59 | @override 60 | Future getLabel(String projectPath) async { 61 | if (!isPathAvailable(projectPath)) return null; 62 | return (await _getPlistDocument(projectPath)) 63 | .getElement('plist') 64 | ?.getElement('dict') 65 | ?.childElements 66 | .where((e) => e.innerText == 'CFBundleName') 67 | .firstOrNull 68 | ?.nextElementSibling 69 | ?.innerText; 70 | } 71 | 72 | @override 73 | Future setLabel(String projectPath, String label) async { 74 | if (!isPathAvailable(projectPath)) return false; 75 | return writePlatformFileXml( 76 | projectPath, 77 | _infoPlistPath, 78 | (await _getPlistFragment(projectPath)) 79 | ..getElement('plist') 80 | ?.getElement('dict') 81 | ?.childElements 82 | .where((e) => e.innerText == 'CFBundleName') 83 | .firstOrNull 84 | ?.nextElementSibling 85 | ?.innerText = label, 86 | indentAttribute: false, 87 | ); 88 | } 89 | 90 | @override 91 | Future?> getLogos(String projectPath) async { 92 | if (!isPathAvailable(projectPath)) return null; 93 | final json = await _getIconInfoJson(projectPath); 94 | final resPath = getPlatformFilePath(projectPath, _iconPath); 95 | final result = []; 96 | for (final item in json['images'] ?? []) { 97 | final filename = item['filename']; 98 | final entries = (item as Map) 99 | ..removeWhere((_, value) => value == filename); 100 | final name = entries.values.join('_'); 101 | final path = join(resPath, filename); 102 | final size = await ImageTool.getSize(path); 103 | if (size == null) continue; 104 | result.add((name: name, path: path, size: size)); 105 | } 106 | return result; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/platform/widgets/label_platform_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | import 'platform_item.dart'; 6 | import 'provider.dart'; 7 | 8 | /* 9 | * 项目平台label组件项 10 | * @author wuxubaiyang 11 | * @Time 2023/12/8 9:24 12 | */ 13 | class LabelPlatformItem extends StatelessWidget { 14 | // 当前平台 15 | final PlatformType platform; 16 | 17 | // label 18 | final String label; 19 | 20 | // 水平风向占用格子数 21 | final int crossAxisCellCount; 22 | 23 | // 垂直方向高度 24 | final double mainAxisExtent; 25 | 26 | // 选择器 27 | final FormFieldValidator? validator; 28 | 29 | const LabelPlatformItem({ 30 | super.key, 31 | required this.label, 32 | required this.platform, 33 | this.crossAxisCellCount = 3, 34 | this.mainAxisExtent = 110, 35 | this.validator, 36 | }); 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | final formKey = GlobalKey(); 41 | return Form( 42 | key: formKey, 43 | child: ProjectPlatformItem( 44 | title: '项目名', 45 | mainAxisExtent: mainAxisExtent, 46 | crossAxisCellCount: crossAxisCellCount, 47 | content: _buildLabelItem(context, formKey), 48 | ), 49 | ); 50 | } 51 | 52 | // 恢复label控制器 53 | TextEditingController _restoreLabelController(BuildContext context) { 54 | final cacheKey = 'label_$platform'; 55 | final provider = context.read(); 56 | final controller = provider.restoreCache(cacheKey) ?? 57 | TextEditingController(text: label) 58 | ..selection = TextSelection.fromPosition( 59 | TextPosition(offset: label.length), 60 | ); 61 | return provider.cache(cacheKey, controller); 62 | } 63 | 64 | // 构建标签项 65 | Widget _buildLabelItem(BuildContext context, GlobalKey formKey) { 66 | final controller = _restoreLabelController(context); 67 | updateLabel() { 68 | final currentState = formKey.currentState; 69 | if (currentState == null || !currentState.validate()) return; 70 | context 71 | .read() 72 | .updateLabel(platform, controller.text) 73 | .loading(context, dismissible: false); 74 | } 75 | 76 | return KeyboardListener( 77 | focusNode: FocusNode(), 78 | onKeyEvent: (event) { 79 | if (event.runtimeType != KeyDownEvent) return; 80 | if (event.logicalKey.keyId != LogicalKeyboardKey.enter.keyId) return; 81 | updateLabel(); 82 | }, 83 | child: StatefulBuilder(builder: (_, setState) { 84 | final isEditing = controller.text != label; 85 | return TextFormField( 86 | controller: controller, 87 | onChanged: (_) => setState(() {}), 88 | autovalidateMode: AutovalidateMode.onUserInteraction, 89 | decoration: InputDecoration( 90 | hintText: '请输入项目名', 91 | suffixIcon: AnimatedOpacity( 92 | opacity: isEditing ? 1 : 0, 93 | duration: const Duration(milliseconds: 80), 94 | child: IconButton( 95 | iconSize: 18, 96 | onPressed: updateLabel, 97 | padding: EdgeInsets.zero, 98 | icon: const Icon(Icons.done), 99 | visualDensity: VisualDensity.compact, 100 | ), 101 | ), 102 | ), 103 | validator: (value) { 104 | if (value?.isEmpty ?? true) { 105 | return '请输入项目名'; 106 | } 107 | return validator?.call(value); 108 | }, 109 | ); 110 | }), 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/widget/app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/main.dart'; 3 | import 'package:flutter_manager/provider/theme.dart'; 4 | import 'package:flutter_manager/provider/window.dart'; 5 | import 'package:jtech_base/jtech_base.dart'; 6 | import 'package:window_manager/window_manager.dart'; 7 | 8 | /* 9 | * 自定义标题栏 10 | * @author wuxubaiyang 11 | * @Time 2023/12/13 15:59 12 | */ 13 | class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { 14 | // 标题 15 | final Widget? title; 16 | 17 | // 左侧控件 18 | final Widget? leading; 19 | 20 | // 动作按钮集合 21 | final List actions; 22 | 23 | // 背景色 24 | final Color? backgroundColor; 25 | 26 | // 是否自动显示返回按钮 27 | final bool automaticallyImplyLeading; 28 | 29 | // 操作按钮大小 30 | final Size actionSize; 31 | 32 | const CustomAppBar({ 33 | super.key, 34 | this.title, 35 | this.leading, 36 | this.backgroundColor, 37 | this.actions = const [], 38 | this.actionSize = const Size(40, 35), 39 | this.automaticallyImplyLeading = true, 40 | }); 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return Consumer( 45 | builder: (_, theme, _) { 46 | final brightness = theme.brightness; 47 | return DragToMoveArea( 48 | child: AppBar( 49 | title: title, 50 | leading: leading, 51 | backgroundColor: backgroundColor, 52 | automaticallyImplyLeading: automaticallyImplyLeading, 53 | actions: [ 54 | ...actions, 55 | SizedBox(width: 14), 56 | SizedBox.fromSize( 57 | size: actionSize, 58 | child: ClipRRect( 59 | clipBehavior: Clip.antiAlias, 60 | borderRadius: BorderRadius.circular(4), 61 | child: WindowCaptionButton.minimize( 62 | brightness: brightness, 63 | onPressed: windowManager.minimize, 64 | ), 65 | ), 66 | ), 67 | SizedBox(width: 8), 68 | SizedBox.fromSize( 69 | size: actionSize, 70 | child: ClipRRect( 71 | clipBehavior: Clip.antiAlias, 72 | borderRadius: BorderRadius.circular(4), 73 | child: _buildMaximizeButton(brightness), 74 | ), 75 | ), 76 | SizedBox(width: 8), 77 | SizedBox.fromSize( 78 | size: actionSize, 79 | child: ClipRRect( 80 | clipBehavior: Clip.antiAlias, 81 | borderRadius: BorderRadius.circular(4), 82 | child: WindowCaptionButton.close( 83 | brightness: brightness, 84 | onPressed: windowManager.close, 85 | ), 86 | ), 87 | ), 88 | SizedBox(width: 8), 89 | ], 90 | ), 91 | ); 92 | }, 93 | ); 94 | } 95 | 96 | // 构建窗口最大化按钮 97 | Widget _buildMaximizeButton(Brightness brightness) { 98 | return Selector( 99 | selector: (_, provider) => provider.maximized, 100 | builder: (context, isMaximized, _) { 101 | if (isMaximized) { 102 | return WindowCaptionButton.unmaximize( 103 | brightness: brightness, 104 | onPressed: context.window.unMaximize, 105 | ); 106 | } 107 | return WindowCaptionButton.maximize( 108 | brightness: brightness, 109 | onPressed: context.window.maximize, 110 | ); 111 | }, 112 | ); 113 | } 114 | 115 | @override 116 | Size get preferredSize => const Size.fromHeight(kToolbarHeight); 117 | } 118 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates a win32 window with |title| that is positioned and sized using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size this function will scale the inputted width and height as 35 | // as appropriate for the default monitor. The window is invisible until 36 | // |Show| is called. Returns true if the window was created successfully. 37 | bool Create(const std::wstring& title, const Point& origin, const Size& size); 38 | 39 | // Show the current window. Returns true if the window was successfully shown. 40 | bool Show(); 41 | 42 | // Release OS resources associated with window. 43 | void Destroy(); 44 | 45 | // Inserts |content| into the window tree. 46 | void SetChildContent(HWND content); 47 | 48 | // Returns the backing Window handle to enable clients to set icon and other 49 | // window properties. Returns nullptr if the window has been destroyed. 50 | HWND GetHandle(); 51 | 52 | // If true, closing this window will quit the application. 53 | void SetQuitOnClose(bool quit_on_close); 54 | 55 | // Return a RECT representing the bounds of the current client area. 56 | RECT GetClientArea(); 57 | 58 | protected: 59 | // Processes and route salient window messages for mouse handling, 60 | // size change and DPI. Delegates handling of these to member overloads that 61 | // inheriting classes can handle. 62 | virtual LRESULT MessageHandler(HWND window, 63 | UINT const message, 64 | WPARAM const wparam, 65 | LPARAM const lparam) noexcept; 66 | 67 | // Called when CreateAndShow is called, allowing subclass window-related 68 | // setup. Subclasses should return false if setup fails. 69 | virtual bool OnCreate(); 70 | 71 | // Called when Destroy is called. 72 | virtual void OnDestroy(); 73 | 74 | private: 75 | friend class WindowClassRegistrar; 76 | 77 | // OS callback called by message pump. Handles the WM_NCCREATE message which 78 | // is passed when the non-client area is being created and enables automatic 79 | // non-client DPI scaling so that the non-client area automatically 80 | // responds to changes in DPI. All other messages are handled by 81 | // MessageHandler. 82 | static LRESULT CALLBACK WndProc(HWND const window, 83 | UINT const message, 84 | WPARAM const wparam, 85 | LPARAM const lparam) noexcept; 86 | 87 | // Retrieves a class instance pointer for |window| 88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 89 | 90 | // Update the window frame's theme to match the system theme. 91 | static void UpdateTheme(HWND const window); 92 | 93 | bool quit_on_close_ = false; 94 | 95 | // window handle for top level window. 96 | HWND window_handle_ = nullptr; 97 | 98 | // window handle for hosted content. 99 | HWND child_content_ = nullptr; 100 | }; 101 | 102 | #endif // RUNNER_WIN32_WINDOW_H_ 103 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/platform/widgets/package_platform_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | import 'platform_item.dart'; 6 | import 'provider.dart'; 7 | 8 | /* 9 | * 项目平台package组件项 10 | * @author wuxubaiyang 11 | * @Time 2023/12/8 9:24 12 | */ 13 | class PackagePlatformItem extends StatelessWidget { 14 | // 当前平台 15 | final PlatformType platform; 16 | 17 | // package 18 | final String package; 19 | 20 | // 水平风向占用格子数 21 | final int crossAxisCellCount; 22 | 23 | // 垂直方向高度 24 | final double mainAxisExtent; 25 | 26 | // 选择器 27 | final FormFieldValidator? validator; 28 | 29 | const PackagePlatformItem({ 30 | super.key, 31 | required this.platform, 32 | required this.package, 33 | this.validator, 34 | this.mainAxisExtent = 110, 35 | this.crossAxisCellCount = 3, 36 | }); 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | final formKey = GlobalKey(); 41 | return Form( 42 | key: formKey, 43 | child: ProjectPlatformItem( 44 | title: '包名', 45 | mainAxisExtent: mainAxisExtent, 46 | crossAxisCellCount: crossAxisCellCount, 47 | content: _buildPackageItem(context, formKey), 48 | ), 49 | ); 50 | } 51 | 52 | // 恢复package控制器 53 | TextEditingController _restorePackageController(BuildContext context) { 54 | final cacheKey = 'package_$platform'; 55 | final provider = context.read(); 56 | final controller = provider.restoreCache(cacheKey) ?? 57 | TextEditingController(text: package) 58 | ..selection = TextSelection.fromPosition( 59 | TextPosition(offset: package.length), 60 | ); 61 | return provider.cache(cacheKey, controller); 62 | } 63 | 64 | // 构建包名项 65 | Widget _buildPackageItem(BuildContext context, GlobalKey formKey) { 66 | final controller = _restorePackageController(context); 67 | updatePackage() { 68 | final currentState = formKey.currentState; 69 | if (currentState == null || !currentState.validate()) return; 70 | context 71 | .read() 72 | .updatePackage(platform, controller.text) 73 | .loading(context, dismissible: false); 74 | } 75 | 76 | return KeyboardListener( 77 | focusNode: FocusNode(), 78 | onKeyEvent: (event) { 79 | if (event.runtimeType != KeyDownEvent) return; 80 | if (event.logicalKey.keyId != LogicalKeyboardKey.enter.keyId) return; 81 | updatePackage(); 82 | }, 83 | child: StatefulBuilder(builder: (_, setState) { 84 | final isEditing = controller.text != package; 85 | return TextFormField( 86 | controller: controller, 87 | onChanged: (_) => setState(() {}), 88 | autovalidateMode: AutovalidateMode.onUserInteraction, 89 | decoration: InputDecoration( 90 | hintText: '请输入项目名', 91 | suffixIcon: AnimatedOpacity( 92 | opacity: isEditing ? 1 : 0, 93 | duration: const Duration(milliseconds: 80), 94 | child: IconButton( 95 | iconSize: 18, 96 | onPressed: updatePackage, 97 | padding: EdgeInsets.zero, 98 | icon: const Icon(Icons.done), 99 | visualDensity: VisualDensity.compact, 100 | ), 101 | ), 102 | ), 103 | validator: (value) { 104 | if (value?.isEmpty ?? true) { 105 | return '请输入包名'; 106 | } 107 | final regExp = RegExp( 108 | r'^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*$', 109 | ); 110 | if (!regExp.hasMatch(value!)) { 111 | return '包名格式不正确'; 112 | } 113 | return validator?.call(value); 114 | }, 115 | ); 116 | }), 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/database/model/environment.dart: -------------------------------------------------------------------------------- 1 | import 'package:jtech_base/jtech_base.dart'; 2 | 3 | @Entity() 4 | class Environment { 5 | int id = 0; 6 | 7 | // 环境目录 8 | String path = ''; 9 | 10 | // flutter版本号 11 | String version = ''; 12 | 13 | // flutter分支 14 | String channel = ''; 15 | 16 | // git地址 17 | String gitUrl = ''; 18 | 19 | // 框架版本 20 | String frameworkReversion = ''; 21 | 22 | // 引擎版本 23 | String engineReversion = ''; 24 | 25 | // dart版本号 26 | String dartVersion = ''; 27 | 28 | // 开发版本 29 | String devToolsVersion = ''; 30 | 31 | // sdk更新时间 32 | String updateTime = ''; 33 | 34 | // 排序 35 | int order = 0; 36 | 37 | // 创建时间 38 | @Property(type: PropertyType.date) 39 | DateTime createAt = DateTime.now(); 40 | 41 | // 更新时间 42 | @Property(type: PropertyType.date) 43 | DateTime updateAt = DateTime.now(); 44 | 45 | Environment(); 46 | 47 | Environment.c({ 48 | required this.path, 49 | required this.version, 50 | required this.channel, 51 | required this.gitUrl, 52 | required this.frameworkReversion, 53 | required this.engineReversion, 54 | required this.dartVersion, 55 | required this.devToolsVersion, 56 | required this.order, 57 | required this.updateTime, 58 | required this.createAt, 59 | required this.updateAt, 60 | }); 61 | 62 | Environment.create({ 63 | required this.path, 64 | required this.version, 65 | required this.channel, 66 | required this.gitUrl, 67 | required this.frameworkReversion, 68 | required this.engineReversion, 69 | required this.dartVersion, 70 | required this.devToolsVersion, 71 | required this.updateTime, 72 | }); 73 | 74 | Environment.createImport(this.path); 75 | 76 | // 获取环境信息标题 77 | @Transient() 78 | String get title => 'Flutter · $version · $channel'; 79 | 80 | // 获取bin路径 81 | @Transient() 82 | String get binPath => join(path, 'bin'); 83 | 84 | Environment copyWith({ 85 | String? path, 86 | String? version, 87 | String? channel, 88 | String? gitUrl, 89 | String? frameworkReversion, 90 | String? engineReversion, 91 | String? dartVersion, 92 | String? devToolsVersion, 93 | int? order, 94 | String? updateTime, 95 | DateTime? updateAt, 96 | DateTime? createAt, 97 | }) { 98 | return Environment.c( 99 | path: path ?? this.path, 100 | version: version ?? this.version, 101 | channel: channel ?? this.channel, 102 | gitUrl: gitUrl ?? this.gitUrl, 103 | frameworkReversion: frameworkReversion ?? this.frameworkReversion, 104 | engineReversion: engineReversion ?? this.engineReversion, 105 | dartVersion: dartVersion ?? this.dartVersion, 106 | devToolsVersion: devToolsVersion ?? this.devToolsVersion, 107 | order: order ?? this.order, 108 | updateTime: updateTime ?? this.updateTime, 109 | createAt: createAt ?? this.createAt, 110 | updateAt: updateAt ?? this.updateAt, 111 | ); 112 | } 113 | 114 | @override 115 | int get hashCode => 116 | id.hashCode & 117 | path.hashCode & 118 | version.hashCode & 119 | channel.hashCode & 120 | gitUrl.hashCode & 121 | frameworkReversion.hashCode & 122 | engineReversion.hashCode & 123 | dartVersion.hashCode & 124 | devToolsVersion.hashCode & 125 | updateTime.hashCode & 126 | updateAt.hashCode & 127 | createAt.hashCode & 128 | order.hashCode; 129 | 130 | @override 131 | bool operator ==(Object other) => 132 | other is Environment && 133 | id == other.id && 134 | path == other.path && 135 | version == other.version && 136 | channel == other.channel && 137 | gitUrl == other.gitUrl && 138 | frameworkReversion == other.frameworkReversion && 139 | engineReversion == other.engineReversion && 140 | dartVersion == other.dartVersion && 141 | devToolsVersion == other.devToolsVersion && 142 | updateTime == other.updateTime && 143 | updateAt == other.updateAt && 144 | createAt == other.createAt && 145 | order == other.order; 146 | } 147 | -------------------------------------------------------------------------------- /lib/widget/drop_file.dart: -------------------------------------------------------------------------------- 1 | import 'package:desktop_drop/desktop_drop.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:jtech_base/jtech_base.dart'; 4 | import 'empty_box.dart'; 5 | 6 | // 文件拖拽回调 7 | typedef DropFileValidator = Future Function(T value); 8 | 9 | /* 10 | * 文件拖拽组件 11 | * @author wuxubaiyang 12 | * @Time 2023/11/29 10:31 13 | */ 14 | class DropFileView extends ProviderView { 15 | // 拖拽进入校验回调 16 | final DropFileValidator? onEnterValidator; 17 | 18 | // 拖拽退出校验回调 19 | final DropFileValidator? onExitValidator; 20 | 21 | // 拖拽更新校验回调 22 | final DropFileValidator? onUpdateValidator; 23 | 24 | // 拖拽完成校验回调 25 | final DropFileValidator> onDoneValidator; 26 | 27 | // 子元素 28 | final Widget child; 29 | 30 | // 默认提示 31 | final String hint; 32 | 33 | // 延迟退出时间 34 | final Duration delayExit; 35 | 36 | // 是否启用 37 | final bool enable; 38 | 39 | DropFileView({ 40 | super.key, 41 | required this.child, 42 | required this.onDoneValidator, 43 | this.enable = true, 44 | this.onEnterValidator, 45 | this.onExitValidator, 46 | this.onUpdateValidator, 47 | this.hint = '放到此处', 48 | this.delayExit = const Duration(seconds: 1), 49 | }); 50 | 51 | @override 52 | DropFileViewProvider createProvider(BuildContext context) => 53 | DropFileViewProvider(context); 54 | 55 | @override 56 | Widget buildWidget(BuildContext context) { 57 | final provider = context.read(); 58 | return DropTarget( 59 | enable: enable, 60 | onDragEntered: (details) async { 61 | final message = await onEnterValidator?.call(details.globalPosition); 62 | provider.updateDropState(message != null, message ?? hint); 63 | }, 64 | onDragExited: (details) async { 65 | final message = await onExitValidator?.call(details.globalPosition); 66 | provider.updateWarningState(message); 67 | if (message != null) await Future.delayed(delayExit); 68 | provider.dropExited(); 69 | }, 70 | onDragUpdated: (details) async { 71 | if (onUpdateValidator == null) return; 72 | final message = await onUpdateValidator?.call(details.globalPosition); 73 | provider.updateDropState(message != null, message ?? hint); 74 | }, 75 | onDragDone: (details) async { 76 | final paths = details.files.map((e) => e.path).toList(); 77 | final message = await onDoneValidator.call(paths); 78 | provider.updateWarningState(message); 79 | if (message != null) await Future.delayed(delayExit); 80 | provider.dropExited(); 81 | }, 82 | child: createSelector< FileDropState?>( 83 | selector: (_, provider) => provider.dropState, 84 | builder: (_, dropState, _) { 85 | final color = dropState?.warning == true 86 | ? Colors.red.withValues(alpha: 0.25) 87 | : null; 88 | return EmptyBoxView( 89 | color: color, 90 | isEmpty: dropState != null, 91 | hint: dropState?.message ?? hint, 92 | child: child, 93 | ); 94 | }, 95 | )); 96 | } 97 | } 98 | 99 | // 文件拖拽状态元组 100 | typedef FileDropState = ({bool warning, String message}); 101 | 102 | /* 103 | * 文件拖拽组件状态管理 104 | * @author wuxubaiyang 105 | * @Time 2023/11/29 10:32 106 | */ 107 | class DropFileViewProvider extends BaseProvider { 108 | // 文件拖拽状态 109 | FileDropState? _dropState; 110 | 111 | // 获取文件拖拽状态 112 | FileDropState? get dropState => _dropState; 113 | 114 | DropFileViewProvider(super.context); 115 | 116 | // 更新拖拽状态 117 | void updateDropState(bool warning, String message) { 118 | _dropState = (warning: warning, message: message); 119 | notifyListeners(); 120 | } 121 | 122 | // 更新异常状态 123 | void updateWarningState(String? message) { 124 | if (message != null) updateDropState(true, message); 125 | } 126 | 127 | // 文件拖拽退出区域 128 | void dropExited() { 129 | _dropState = null; 130 | notifyListeners(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/provider/project.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_manager/database/database.dart'; 4 | import 'package:flutter_manager/database/model/project.dart'; 5 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 6 | import 'package:flutter_manager/tool/project/project.dart'; 7 | import 'package:jtech_base/jtech_base.dart'; 8 | 9 | /* 10 | * 项目提供者 11 | * @author wuxubaiyang 12 | * @Time 2023/11/26 18:56 13 | */ 14 | class ProjectProvider extends BaseProvider { 15 | // 最大置顶数量 16 | static const int maxPinnedCount = 4; 17 | 18 | // 项目集合 19 | late List _projects = 20 | database.getProjectList(desc: true, pinned: false); 21 | 22 | // 获取项目集合 23 | List get projects => _projects; 24 | 25 | // 获取置顶项目集合 26 | late List _pinnedProjects = 27 | database.getProjectList(desc: true, pinned: true); 28 | 29 | // 获取置顶项目集合 30 | List get pinnedProjects => _pinnedProjects; 31 | 32 | // 判断是否存在项目 33 | bool get hasProject => _projects.isNotEmpty || _pinnedProjects.isNotEmpty; 34 | 35 | // 默认项目平台排序表 36 | late List _platforms = ProjectTool.getPlatformSort(); 37 | 38 | // 获取默认项目平台排序表 39 | List get platforms => _platforms; 40 | 41 | ProjectProvider(super.context); 42 | 43 | // 刷新所有项目 44 | void refresh() { 45 | _updateProjects(); 46 | _updatePinnedProjects(); 47 | } 48 | 49 | // 调换项目平台排序 50 | void swapPlatformSort(int oldIndex, int newIndex) { 51 | _platforms = [..._platforms].swapReorder(oldIndex, newIndex); 52 | notifyListeners(); 53 | } 54 | 55 | // 更新项目平台排序 56 | Future updatePlatformSort(List platforms) async { 57 | _platforms = platforms; 58 | notifyListeners(); 59 | return ProjectTool.cachePlatformSort(platforms); 60 | } 61 | 62 | // 添加/编辑项目信息 63 | Future update(Project item) async { 64 | dynamic cacheFile = item.logo; 65 | if (File(item.logo).existsSync()) { 66 | cacheFile = await FileTool.cache(item.logo); 67 | } 68 | final result = await database.updateProject( 69 | item..logo = cacheFile ?? '', 70 | ); 71 | _updateProjects(); 72 | _updatePinnedProjects(); 73 | return result; 74 | } 75 | 76 | // 项目置顶 77 | Future togglePinned(Project project) async { 78 | await database.updateProject( 79 | project..pinned = !project.pinned, 80 | ); 81 | _updateProjects(); 82 | _updatePinnedProjects(); 83 | } 84 | 85 | // 移除项目 86 | bool remove(Project project) { 87 | final result = database.removeProject(project.id); 88 | _updateProjects(); 89 | _updatePinnedProjects(); 90 | return result; 91 | } 92 | 93 | // 对置顶项目重排序 94 | Future> reorderPinned(int oldIndex, int newIndex) { 95 | final temp = _swapAndOrder( 96 | pinnedProjects.reversed.toList(), 97 | oldIndex, 98 | newIndex, 99 | ); 100 | _updatePinnedProjects(projects: temp.reversed.toList()); 101 | return database.updateProjects(temp); 102 | } 103 | 104 | // 项目重排序 105 | Future> reorder(int oldIndex, int newIndex) { 106 | final temp = _swapAndOrder( 107 | projects.reversed.toList(), 108 | oldIndex, 109 | newIndex, 110 | ); 111 | _updateProjects(projects: temp.reversed.toList()); 112 | return database.updateProjects(temp); 113 | } 114 | 115 | // 交换并重排序 116 | List _swapAndOrder(List list, int oldIndex, int newIndex) { 117 | final temp = list.swapReorder(oldIndex, newIndex); 118 | temp.asMap().forEach((i, e) => e.order = i); 119 | temp.sort((a, b) => a.order.compareTo(b.order)); 120 | return temp; 121 | } 122 | 123 | // 更新项目集合 124 | void _updateProjects({List? projects, bool desc = true}) { 125 | projects ??= database.getProjectList(desc: desc, pinned: false); 126 | _projects = projects; 127 | notifyListeners(); 128 | } 129 | 130 | // 更新置顶项目集合 131 | void _updatePinnedProjects({List? projects, bool desc = true}) { 132 | projects ??= database.getProjectList(desc: desc, pinned: true); 133 | _pinnedProjects = projects; 134 | notifyListeners(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /lib/database/model/package.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 2 | import 'package:jtech_base/jtech_base.dart'; 3 | 4 | import 'project.dart'; 5 | 6 | @Entity() 7 | class Package { 8 | int id = 0; 9 | 10 | // 输出包名 11 | String name = ''; 12 | 13 | // 打包指令后缀 14 | String suffix = ''; 15 | 16 | // 安装包备份地址(为空代表不执行备份) 17 | String? backupPath; 18 | 19 | // 打包失败异常日志 20 | String? error; 21 | 22 | // 打包用时 23 | @Transient() 24 | Duration? duration; 25 | 26 | // 打包状态 27 | @Transient() 28 | PackageStatus status = PackageStatus.none; 29 | 30 | // 打包平台 31 | @Transient() 32 | PlatformType platformType = PlatformType.android; 33 | 34 | // 项目信息 35 | final projectDB = ToOne(); 36 | 37 | // 创建时间 38 | @Property(type: PropertyType.date) 39 | DateTime createAt = DateTime.now(); 40 | 41 | // 更新时间 42 | @Property(type: PropertyType.date) 43 | DateTime updateAt = DateTime.now(); 44 | 45 | Package(); 46 | 47 | Package.c({ 48 | required this.name, 49 | required this.suffix, 50 | required this.backupPath, 51 | required this.error, 52 | required this.duration, 53 | required this.status, 54 | required this.platformType, 55 | required this.createAt, 56 | required this.updateAt, 57 | required Project? project, 58 | }) { 59 | projectDB.target = project; 60 | } 61 | 62 | Package.create({ 63 | required this.name, 64 | required this.suffix, 65 | required this.platformType, 66 | required Project project, 67 | this.backupPath, 68 | }) { 69 | projectDB.target = project; 70 | } 71 | 72 | // 获取数据库打包时间 73 | int get durationDB => duration?.inMilliseconds ?? 0; 74 | 75 | // 设置数据库打包时间 76 | set durationDB(int value) => duration = Duration(milliseconds: value); 77 | 78 | // 获取数据库打包状态 79 | int get statusDB => status.index; 80 | 81 | // 设置数据库打包状态 82 | set statusDB(int value) => status = PackageStatus.values[value]; 83 | 84 | // 获取数据库平台类型 85 | int get platformTypeDB => platformType.index; 86 | 87 | // 设置数据库平台类型 88 | set platformTypeDB(int value) => platformType = PlatformType.values[value]; 89 | 90 | // 获取项目对象 91 | @Transient() 92 | Project? get project => projectDB.target; 93 | 94 | // 获取项目ID 95 | @Transient() 96 | int? get projectId => project?.id; 97 | 98 | Package copyWith({ 99 | String? name, 100 | String? suffix, 101 | String? backupPath, 102 | String? error, 103 | Duration? duration, 104 | PackageStatus? status, 105 | PlatformType? platformType, 106 | Project? project, 107 | DateTime? createAt, 108 | DateTime? updateAt, 109 | }) { 110 | return Package.c( 111 | name: name ?? this.name, 112 | suffix: suffix ?? this.suffix, 113 | backupPath: backupPath ?? this.backupPath, 114 | error: error ?? this.error, 115 | duration: duration ?? this.duration, 116 | status: status ?? this.status, 117 | platformType: platformType ?? this.platformType, 118 | createAt: createAt ?? this.createAt, 119 | updateAt: updateAt ?? this.updateAt, 120 | project: project ?? this.project, 121 | ); 122 | } 123 | 124 | @override 125 | int get hashCode => 126 | id.hashCode & 127 | name.hashCode & 128 | suffix.hashCode & 129 | backupPath.hashCode & 130 | error.hashCode & 131 | duration.hashCode & 132 | status.hashCode & 133 | platformType.hashCode & 134 | project.hashCode & 135 | createAt.hashCode & 136 | updateAt.hashCode; 137 | 138 | @override 139 | bool operator ==(Object other) => 140 | other is Package && 141 | other.id == id && 142 | other.name == name && 143 | other.suffix == suffix && 144 | other.backupPath == backupPath && 145 | other.error == error && 146 | other.duration == duration && 147 | other.status == status && 148 | other.platformType == platformType && 149 | other.project == project && 150 | other.createAt == createAt && 151 | other.updateAt == updateAt; 152 | } 153 | 154 | // 打包状态枚举 155 | enum PackageStatus { 156 | prepare, 157 | building, 158 | success, 159 | fail, 160 | none, 161 | } 162 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /lib/widget/form_field/project_logo_panel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 3 | import 'package:flutter_manager/widget/project_logo.dart'; 4 | 5 | // 项目图标折叠展示表单项值元组 6 | typedef LogoPanelField = ({ 7 | PlatformType? expanded, 8 | List platforms, 9 | }); 10 | 11 | /* 12 | * 项目图标折叠展示表单项组件 13 | * @author wuxubaiyang 14 | * @Time 2023/12/4 19:50 15 | */ 16 | class ProjectLogoPanelFormField extends StatelessWidget { 17 | // 表单项key 18 | final Key? fieldKey; 19 | 20 | // 初始化值 21 | final LogoPanelField? initialValue; 22 | 23 | // 保存回调 24 | final FormFieldSetter? onSaved; 25 | 26 | // 平台与图标表 27 | final Map> platformLogoMap; 28 | 29 | const ProjectLogoPanelFormField({ 30 | super.key, 31 | this.fieldKey, 32 | required this.platformLogoMap, 33 | this.onSaved, 34 | this.initialValue, 35 | }); 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return FormField( 40 | key: fieldKey, 41 | onSaved: onSaved, 42 | initialValue: initialValue, 43 | validator: (v) { 44 | if (v?.platforms.isEmpty ?? true) { 45 | return '请选择平台'; 46 | } 47 | return null; 48 | }, 49 | builder: (field) { 50 | return _buildFormField(context, field); 51 | }, 52 | ); 53 | } 54 | 55 | // 构建表单字段 56 | Widget _buildFormField( 57 | BuildContext context, FormFieldState field) { 58 | final inputDecoration = InputDecoration( 59 | border: InputBorder.none, 60 | errorText: field.errorText, 61 | ); 62 | return InputDecorator( 63 | decoration: inputDecoration, 64 | child: ExpansionPanelList.radio( 65 | elevation: 0, 66 | materialGapSize: 8, 67 | initialOpenPanelValue: field.value?.expanded, 68 | expandedHeaderPadding: const EdgeInsets.symmetric(vertical: 8), 69 | expansionCallback: (index, isExpanded) { 70 | _onExpansionPanelChanged(index, isExpanded, field); 71 | }, 72 | children: List.generate(platformLogoMap.length, (i) { 73 | final item = platformLogoMap.entries.elementAt(i); 74 | return _buildExpansionPanelItem(context, item, field); 75 | }), 76 | ), 77 | ); 78 | } 79 | 80 | // 构建展开项 81 | ExpansionPanelRadio _buildExpansionPanelItem( 82 | BuildContext context, 83 | MapEntry> item, 84 | FormFieldState field) { 85 | final checked = field.value?.platforms.contains(item.key) ?? false; 86 | return ExpansionPanelRadio( 87 | value: item.key, 88 | canTapOnHeader: true, 89 | headerBuilder: (context, isExpanded) { 90 | return ListTile( 91 | leading: Tooltip( 92 | message: '替换该平台', 93 | child: Checkbox( 94 | value: checked, 95 | isError: field.hasError, 96 | onChanged: (v) { 97 | _onCheckboxChanged(v == true, field, item.key); 98 | }, 99 | ), 100 | ), 101 | title: Text(item.key.name), 102 | trailing: Tooltip( 103 | message: '图标数量', 104 | child: Text('${item.value.length}'), 105 | ), 106 | ); 107 | }, 108 | body: ProjectLogoGrid( 109 | logoList: item.value, 110 | ), 111 | ); 112 | } 113 | 114 | // 展开项事件 115 | void _onExpansionPanelChanged( 116 | int index, bool isExpanded, FormFieldState field) { 117 | field.didChange(( 118 | expanded: isExpanded ? platformLogoMap.keys.elementAt(index) : null, 119 | platforms: field.value?.platforms ?? [] 120 | )); 121 | } 122 | 123 | // 多选框事件 124 | void _onCheckboxChanged(bool checked, 125 | FormFieldState field, PlatformType platform) { 126 | final temp = field.value?.platforms ?? []; 127 | field.didChange(( 128 | expanded: field.value?.expanded, 129 | platforms: [ 130 | if (!temp.contains(platform)) platform, 131 | ...temp.where((e) => e != platform), 132 | ], 133 | )); 134 | field.validate(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_manager 2 | description: "flutter project manager" 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.0.0+1 20 | 21 | environment: 22 | sdk: ^3.8.0 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | 34 | flutter_localizations: 35 | sdk: flutter 36 | 37 | xml: ^6.5.0 38 | intl: ^0.20.2 39 | image: ^4.5.4 40 | archive: ^4.0.7 41 | open_dir: ^0.0.2+1 42 | jtech_base: ^0.6.6+6 43 | flutter_gen: ^5.10.0 44 | url_launcher: ^6.3.1 45 | desktop_drop: ^0.6.0 46 | window_manager: ^0.5.0 47 | json_annotation: ^4.9.0 48 | custom_image_crop: ^0.1.1 49 | freezed_annotation: ^3.0.0 50 | flutter_context_menu: ^0.2.4 51 | reorderable_grid_view: ^2.2.8 52 | flutter_staggered_grid_view: ^0.7.0 53 | 54 | dev_dependencies: 55 | freezed: ^3.0.6 56 | build_runner: ^2.5.3 57 | flutter_lints: ^6.0.0 58 | json_serializable: ^6.9.5 59 | flutter_gen_runner: ^5.10.0 60 | objectbox_generator: ^4.3.0 61 | 62 | # For information on the generic Dart part of this file, see the 63 | # following page: https://dart.dev/tools/pub/pubspec 64 | 65 | # The following section is specific to Flutter packages. 66 | flutter: 67 | 68 | # The following line ensures that the Material Icons font is 69 | # included with your application, so that you can use the icons in 70 | # the material Icons class. 71 | uses-material-design: true 72 | 73 | # To add assets to your application, add an assets section, like this: 74 | assets: 75 | - assets/permission/ 76 | 77 | # An image asset can refer to one or more resolution-specific "variants", see 78 | # https://flutter.dev/assets-and-images/#resolution-aware 79 | 80 | # For details regarding adding assets from package dependencies, see 81 | # https://flutter.dev/assets-and-images/#from-packages 82 | 83 | # To add custom fonts to your application, add a fonts section here, 84 | # in this "flutter" section. Each entry in this list should have a 85 | # "family" key with the font family name, and a "fonts" key with a 86 | # list giving the asset and other descriptors for the font. For 87 | # example: 88 | # fonts: 89 | # - family: Schyler 90 | # fonts: 91 | # - asset: fonts/Schyler-Regular.ttf 92 | # - asset: fonts/Schyler-Italic.ttf 93 | # style: italic 94 | # - family: Trajan Pro 95 | # fonts: 96 | # - asset: fonts/TrajanPro.ttf 97 | # - asset: fonts/TrajanPro_Bold.ttf 98 | # weight: 700 99 | # 100 | # For details regarding fonts from package dependencies, 101 | # see https://flutter.dev/custom-fonts/#from-packages 102 | flutter_intl: 103 | enabled: true -------------------------------------------------------------------------------- /lib/widget/dialog/project/logo.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 3 | import 'package:flutter_manager/widget/empty_box.dart'; 4 | import 'package:flutter_manager/widget/form_field/project_logo.dart'; 5 | import 'package:flutter_manager/widget/form_field/project_logo_panel.dart'; 6 | import 'package:jtech_base/jtech_base.dart'; 7 | 8 | // 展示修改图标弹窗 9 | Future showProjectLogo(BuildContext context, 10 | {required Map> logoMap}) async { 11 | return showDialog( 12 | context: context, 13 | barrierDismissible: false, 14 | builder: (_) => ProjectLogoDialog( 15 | logoMap: logoMap, 16 | ), 17 | ); 18 | } 19 | 20 | /* 21 | * 项目修改图标弹窗 22 | * @author wuxubaiyang 23 | * @Time 2023/12/1 9:17 24 | */ 25 | class ProjectLogoDialog extends ProviderView { 26 | // 平台与图标表 27 | final Map> logoMap; 28 | 29 | ProjectLogoDialog({super.key, required this.logoMap}); 30 | 31 | @override 32 | ProjectLogoDialogProvider createProvider(context) => 33 | ProjectLogoDialogProvider(context, logoMap); 34 | 35 | @override 36 | Widget buildWidget(BuildContext context) { 37 | return CustomDialog( 38 | title: const Text('图标'), 39 | content: _buildContent(context), 40 | style: CustomDialogStyle( 41 | constraints: BoxConstraints(minWidth: 380, maxHeight: 320), 42 | ), 43 | actions: [ 44 | TextButton( 45 | onPressed: context.pop, 46 | child: const Text('取消'), 47 | ), 48 | TextButton( 49 | onPressed: provider.submit, 50 | child: const Text('确定'), 51 | ), 52 | ], 53 | ); 54 | } 55 | 56 | // 构建内容 57 | Widget _buildContent(BuildContext context) { 58 | return EmptyBoxView( 59 | hint: '暂无可用平台', 60 | isEmpty: logoMap.isEmpty, 61 | child: Form( 62 | key: provider.formKey, 63 | child: SingleChildScrollView( 64 | child: Column(children: [ 65 | _buildFieldLogo(context), 66 | const SizedBox(height: 14), 67 | _buildFieldPlatforms(context), 68 | ]), 69 | ), 70 | ), 71 | ); 72 | } 73 | 74 | // 构建图标选择 75 | Widget _buildFieldLogo(BuildContext context) { 76 | return ProjectLogoFormField( 77 | logoSize: const Size.square(100), 78 | onSaved: (v) => provider.updateFormData(logo: v), 79 | ); 80 | } 81 | 82 | // 构建展开列表 83 | Widget _buildFieldPlatforms(BuildContext context) { 84 | return ProjectLogoPanelFormField( 85 | platformLogoMap: logoMap, 86 | onSaved: (v) => provider.updateFormData(platforms: v?.platforms), 87 | initialValue: (expanded: null, platforms: provider.formData.platforms), 88 | ); 89 | } 90 | } 91 | 92 | // 项目修改图标弹窗表单数据元组 93 | typedef ProjectLogoDialogForm = ({ 94 | String logo, 95 | List platforms, 96 | }); 97 | 98 | /* 99 | * 项目修改图标弹窗数据提供者 100 | * @author wuxubaiyang 101 | * @Time 2023/12/4 15:26 102 | */ 103 | class ProjectLogoDialogProvider extends BaseProvider { 104 | // 表单key 105 | final formKey = GlobalKey(); 106 | 107 | ProjectLogoDialogProvider( 108 | super.context, Map> logoMap) 109 | : _formData = (logo: '', platforms: logoMap.keys.toList()); 110 | 111 | // 表单数据 112 | ProjectLogoDialogForm _formData; 113 | 114 | // 获取表单数据 115 | ProjectLogoDialogForm get formData => _formData; 116 | 117 | // 验证表单并返回 118 | Future submit() async { 119 | try { 120 | final formState = formKey.currentState; 121 | if (formState == null || !formState.validate()) return null; 122 | formState.save(); 123 | context.pop(_formData); 124 | return _formData; 125 | } catch (e) { 126 | showNoticeError(e.toString(), title: '操作失败'); 127 | } 128 | return null; 129 | } 130 | 131 | // 更新表单数据 132 | void updateFormData({ 133 | String? logo, 134 | List? platforms, 135 | }) { 136 | _formData = ( 137 | logo: logo ?? _formData.logo, 138 | platforms: platforms ?? _formData.platforms, 139 | ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lib/widget/dialog/environment/remote_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/model/env_package.dart'; 3 | 4 | /* 5 | * 远程环境安装包列表组件 6 | * @author wuxubaiyang 7 | * @Time 2023/11/27 9:53 8 | */ 9 | class EnvironmentRemoteList extends StatelessWidget { 10 | // 缓存搜索框控制器 11 | final _searchControllerMap = {}; 12 | 13 | // 渠道安装包信息 14 | final Map> channelPackages; 15 | 16 | // 启动下载回调 17 | final ValueChanged? onStartDownload; 18 | 19 | // 默认展示下标 20 | final int initialIndex; 21 | 22 | // 复制链接回调 23 | final ValueChanged? onCopyLink; 24 | 25 | EnvironmentRemoteList({ 26 | super.key, 27 | required this.channelPackages, 28 | this.onCopyLink, 29 | this.onStartDownload, 30 | String initialChannel = 'stable', 31 | }) : initialIndex = channelPackages.keys.toList().indexOf(initialChannel); 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return DefaultTabController( 36 | initialIndex: initialIndex, 37 | length: channelPackages.length, 38 | child: Column(children: [ 39 | TabBar( 40 | tabs: List.generate(channelPackages.length, 41 | (i) => Tab(text: channelPackages.keys.elementAt(i))), 42 | ), 43 | Expanded( 44 | child: TabBarView( 45 | children: List.generate(channelPackages.length, (i) { 46 | final entity = channelPackages.entries.elementAt(i); 47 | return _buildPackageList(context, entity.key, entity.value); 48 | }), 49 | ), 50 | ), 51 | ]), 52 | ); 53 | } 54 | 55 | // 根据渠道key获取搜索框控制器 56 | TextEditingController _getSearchController(String key) => 57 | _searchControllerMap[key] ??= TextEditingController(); 58 | 59 | // 构建安装包渠道列表 60 | Widget _buildPackageList( 61 | BuildContext context, String channel, List packages) { 62 | final controller = _getSearchController(channel); 63 | return StatefulBuilder( 64 | builder: (_, setState) { 65 | final tempList = controller.text.isNotEmpty 66 | ? packages.where((e) => e.search(controller.text)).toList() 67 | : packages; 68 | return Column( 69 | children: [ 70 | Padding( 71 | padding: const EdgeInsets.all(8), 72 | child: SearchBar( 73 | hintText: '输入过滤条件', 74 | controller: controller, 75 | onChanged: (_) => setState(() {}), 76 | ), 77 | ), 78 | Expanded( 79 | child: ListView.separated( 80 | shrinkWrap: true, 81 | itemCount: tempList.length, 82 | separatorBuilder: (_, _) => const Divider(), 83 | itemBuilder: (_, i) { 84 | final item = tempList[i]; 85 | return _buildPackageListItem(context, item); 86 | }, 87 | ), 88 | ), 89 | ], 90 | ); 91 | }, 92 | ); 93 | } 94 | 95 | // 构建包列表子项 96 | Widget _buildPackageListItem(BuildContext context, EnvPackage item) { 97 | final iconData = item.hasDownload 98 | ? Icons.download_done_rounded 99 | : (item.hasTemp 100 | ? Icons.download_for_offline_rounded 101 | : Icons.download_rounded); 102 | final subText = 'Dart · ${item.dartVersion} · ${item.dartArch}'; 103 | final startTooltip = { 104 | Icons.download_rounded: '下载', 105 | Icons.download_done_rounded: '已下载', 106 | Icons.download_for_offline_rounded: '继续下载', 107 | }[iconData]; 108 | return ListTile( 109 | title: Text(item.title), 110 | subtitle: Text(subText), 111 | trailing: Row(mainAxisSize: MainAxisSize.min, children: [ 112 | IconButton( 113 | tooltip: '复制下载链接', 114 | icon: const Icon(Icons.copy_rounded), 115 | onPressed: () => onCopyLink?.call(item), 116 | ), 117 | IconButton( 118 | icon: Icon(iconData), 119 | tooltip: startTooltip, 120 | onPressed: () => onStartDownload?.call(item), 121 | ), 122 | ]), 123 | onTap: () => onStartDownload?.call(item), 124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/widget/scheme_picker.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | 6 | // 展示主题配色弹窗 7 | Future showSchemePicker( 8 | BuildContext context, { 9 | required Map themeSchemes, 10 | FlexScheme? current, 11 | }) { 12 | return showDialog( 13 | context: context, 14 | builder: (context) => SchemePickerDialog( 15 | current: current, 16 | themeSchemes: themeSchemes, 17 | ), 18 | ); 19 | } 20 | 21 | /* 22 | * 主题配色对话框 23 | * @author wuxubaiyang 24 | * @Time 2023/11/25 19:38 25 | */ 26 | class SchemePickerDialog extends StatelessWidget { 27 | // 配色方案对照表 28 | final Map themeSchemes; 29 | 30 | // 当前主题配色方案 31 | final FlexScheme? current; 32 | 33 | const SchemePickerDialog({ 34 | super.key, 35 | required this.themeSchemes, 36 | this.current, 37 | }); 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return CustomDialog( 42 | scrollable: true, 43 | title: const Text('选择主题配色'), 44 | content: _buildContent(context), 45 | ); 46 | } 47 | 48 | // 构建内容 49 | Widget _buildContent(BuildContext context) { 50 | final brightness = Theme.of(context).brightness; 51 | return Wrap( 52 | spacing: 14, 53 | runSpacing: 14, 54 | children: themeSchemes.entries.map((e) { 55 | final colorScheme = switch (brightness) { 56 | Brightness.light => e.value.light, 57 | Brightness.dark => e.value.dark, 58 | }; 59 | return ThemeSchemeItem( 60 | isSelected: e.key == current, 61 | primary: colorScheme.primary, 62 | secondary: colorScheme.secondary, 63 | onPressed: () => Navigator.pop(context, e.key), 64 | ); 65 | }).toList(), 66 | ); 67 | } 68 | } 69 | 70 | /* 71 | * 主题配色项 72 | * @author wuxubaiyang 73 | * @Time 2023/11/24 15:36 74 | */ 75 | class ThemeSchemeItem extends StatelessWidget { 76 | // 主色 77 | final Color primary; 78 | 79 | // 次色 80 | final Color secondary; 81 | 82 | // 旋转角度(0-12) 83 | final double angle; 84 | 85 | // 大小 86 | final double size; 87 | 88 | // 内间距 89 | final EdgeInsetsGeometry padding; 90 | 91 | // 点击事件 92 | final VoidCallback? onPressed; 93 | 94 | // 是否已选中 95 | final bool isSelected; 96 | 97 | // 自定义tooltip 98 | final String? tooltip; 99 | 100 | const ThemeSchemeItem({ 101 | super.key, 102 | required this.primary, 103 | required this.secondary, 104 | this.tooltip, 105 | this.size = 45, 106 | this.onPressed, 107 | this.angle = 7, 108 | this.isSelected = false, 109 | this.padding = const EdgeInsets.all(4), 110 | }); 111 | 112 | @override 113 | Widget build(BuildContext context) { 114 | return Transform.rotate( 115 | angle: angle, 116 | child: SizedBox.fromSize( 117 | size: Size.square(size), 118 | child: _buildItem(), 119 | ), 120 | ); 121 | } 122 | 123 | // 构建子项 124 | Widget _buildItem() { 125 | if (isSelected) { 126 | return IconButton.outlined( 127 | padding: padding, 128 | onPressed: onPressed, 129 | icon: _buildItemSub(), 130 | ); 131 | } 132 | return IconButton( 133 | padding: padding, 134 | onPressed: onPressed, 135 | icon: _buildItemSub(), 136 | ); 137 | } 138 | 139 | // 构建子项sub 140 | Widget _buildItemSub() { 141 | return CustomPaint( 142 | size: Size.infinite, 143 | painter: HalfCirclePainter((primary, secondary)), 144 | ); 145 | } 146 | } 147 | 148 | /* 149 | * 半圆形绘制器 150 | * @author wuxubaiyang 151 | * @Time 2023/11/24 15:17 152 | */ 153 | class HalfCirclePainter extends CustomPainter { 154 | // 传入要绘制的颜色 155 | final (Color, Color) colors; 156 | 157 | HalfCirclePainter(this.colors); 158 | 159 | @override 160 | void paint(Canvas canvas, Size size) { 161 | final paint1 = Paint()..color = colors.$1; 162 | final paint2 = Paint()..color = colors.$2; 163 | 164 | final rect = Rect.fromCircle( 165 | center: Offset(size.width / 2, size.height / 2), 166 | radius: size.width / 2, 167 | ); 168 | canvas.drawArc(rect, 0, pi, true, paint1); 169 | canvas.drawArc(rect, pi, pi, true, paint2); 170 | } 171 | 172 | @override 173 | bool shouldRepaint(covariant CustomPainter oldDelegate) => false; 174 | } 175 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(flutter_manager LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "flutter_manager") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(VERSION 3.14...3.25) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | 56 | # Generated plugin build rules, which manage building the plugins and adding 57 | # them to the application. 58 | include(flutter/generated_plugins.cmake) 59 | 60 | 61 | # === Installation === 62 | # Support files are copied into place next to the executable, so that it can 63 | # run in place. This is done instead of making a separate bundle (as on Linux) 64 | # so that building and running from within Visual Studio will work. 65 | set(BUILD_BUNDLE_DIR "$") 66 | # Make the "install" step default, as it's required to run. 67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 70 | endif() 71 | 72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 74 | 75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 76 | COMPONENT Runtime) 77 | 78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 82 | COMPONENT Runtime) 83 | 84 | if(PLUGIN_BUNDLED_LIBRARIES) 85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 87 | COMPONENT Runtime) 88 | endif() 89 | 90 | # Copy the native assets provided by the build.dart from all packages. 91 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") 92 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 93 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 94 | COMPONENT Runtime) 95 | 96 | # Fully re-copy the assets directory on each build to avoid having stale files 97 | # from a previous install. 98 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 99 | install(CODE " 100 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 101 | " COMPONENT Runtime) 102 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 103 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 104 | 105 | # Install the AOT library on non-Debug builds only. 106 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 107 | CONFIGURATIONS Profile;Release 108 | COMPONENT Runtime) 109 | -------------------------------------------------------------------------------- /lib/model/create_template.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'create_template.g.dart'; 5 | 6 | part 'create_template.freezed.dart'; 7 | 8 | // 模板创建参数对象 9 | @freezed 10 | abstract class CreateTemplate with _$CreateTemplate { 11 | const CreateTemplate._(); 12 | 13 | const factory CreateTemplate({ 14 | required String flutterBin, 15 | required String projectName, 16 | required String devUrl, 17 | required String targetDir, 18 | required Map platforms, 19 | String? appName, 20 | String? dbName, 21 | String? prodUrl, 22 | String? description, 23 | bool? openWhenFinish, 24 | }) = _CreateTemplate; 25 | 26 | factory CreateTemplate.fromJson(Map json) => 27 | _$CreateTemplateFromJson(json); 28 | 29 | static CreateTemplate empty() => CreateTemplate( 30 | flutterBin: '', 31 | projectName: '', 32 | devUrl: '', 33 | targetDir: '', 34 | platforms: {}, 35 | ); 36 | 37 | // 将参数转化成命令 38 | List toCommand() => [ 39 | '--flutter-bin', 40 | flutterBin, 41 | '--project-name', 42 | projectName, 43 | '--app-name', 44 | appName ?? projectName, 45 | '--db-name', 46 | dbName ?? projectName, 47 | '--dev-url', 48 | devUrl, 49 | '--prod-url', 50 | prodUrl ?? devUrl, 51 | '--target-dir', 52 | targetDir, 53 | '--description', 54 | description ?? '', 55 | '--platforms', 56 | platforms.entries.map((e) => e.key.name).join(','), 57 | for (MapEntry e in platforms.entries) ...e.value.toCommand(), 58 | if (openWhenFinish == true) '--open-when-finish', 59 | ]; 60 | } 61 | 62 | // 模板平台(基类) 63 | @freezed 64 | abstract class TemplatePlatform with _$TemplatePlatform { 65 | const TemplatePlatform._(); 66 | 67 | const factory TemplatePlatform({required PlatformType type}) = 68 | _TemplatePlatform; 69 | 70 | factory TemplatePlatform.fromJson(Map json) => 71 | _$TemplatePlatformFromJson(json); 72 | 73 | static TemplatePlatform create({required PlatformType type}) => 74 | switch (type) { 75 | PlatformType.android => TemplatePlatformAndroid.create(packageName: ''), 76 | PlatformType.ios => TemplatePlatformIos.create(bundleId: ''), 77 | PlatformType.macos => TemplatePlatformMacos.create(bundleId: ''), 78 | _ => TemplatePlatform(type: type), 79 | }; 80 | 81 | // 获取所有参数命令行 82 | List toCommand() => []; 83 | } 84 | 85 | // android平台 86 | @freezed 87 | abstract class TemplatePlatformAndroid 88 | with _$TemplatePlatformAndroid 89 | implements TemplatePlatform { 90 | const TemplatePlatformAndroid._(); 91 | 92 | const factory TemplatePlatformAndroid({ 93 | required PlatformType type, 94 | required String packageName, 95 | }) = _TemplatePlatformAndroid; 96 | 97 | factory TemplatePlatformAndroid.fromJson(Map json) => 98 | _$TemplatePlatformAndroidFromJson(json); 99 | 100 | static TemplatePlatformAndroid create({required String packageName}) => 101 | TemplatePlatformAndroid( 102 | type: PlatformType.android, 103 | packageName: packageName, 104 | ); 105 | 106 | // 获取所有参数命令行 107 | @override 108 | List toCommand() => ['--android-package', packageName]; 109 | } 110 | 111 | // ios平台 112 | @freezed 113 | abstract class TemplatePlatformIos 114 | with _$TemplatePlatformIos 115 | implements TemplatePlatform { 116 | const TemplatePlatformIos._(); 117 | 118 | const factory TemplatePlatformIos({ 119 | required PlatformType type, 120 | required String bundleId, 121 | }) = _TemplatePlatformIos; 122 | 123 | factory TemplatePlatformIos.fromJson(Map json) => 124 | _$TemplatePlatformIosFromJson(json); 125 | 126 | static TemplatePlatformIos create({required String bundleId}) => 127 | TemplatePlatformIos(type: PlatformType.ios, bundleId: bundleId); 128 | 129 | // 获取所有参数命令行 130 | @override 131 | List toCommand() => ['--ios-bundle-id', bundleId]; 132 | } 133 | 134 | // macos平台 135 | @freezed 136 | abstract class TemplatePlatformMacos 137 | with _$TemplatePlatformMacos 138 | implements TemplatePlatform { 139 | const TemplatePlatformMacos._(); 140 | 141 | const factory TemplatePlatformMacos({ 142 | required PlatformType type, 143 | required String bundleId, 144 | }) = _TemplatePlatformMacos; 145 | 146 | factory TemplatePlatformMacos.fromJson(Map json) => 147 | _$TemplatePlatformMacosFromJson(json); 148 | 149 | static TemplatePlatformMacos create({required String bundleId}) => 150 | TemplatePlatformMacos(type: PlatformType.macos, bundleId: bundleId); 151 | 152 | // 获取所有参数命令行 153 | @override 154 | List toCommand() => ['--macos-bundle-id', bundleId]; 155 | } 156 | -------------------------------------------------------------------------------- /lib/page/home/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/database/model/environment.dart'; 3 | import 'package:flutter_manager/debug.dart'; 4 | import 'package:flutter_manager/generated/l10n.dart'; 5 | import 'package:flutter_manager/main.dart'; 6 | import 'package:flutter_manager/tool/project/environment.dart'; 7 | import 'package:flutter_manager/tool/project/project.dart'; 8 | import 'package:flutter_manager/widget/app_bar.dart'; 9 | import 'package:flutter_manager/widget/dialog/environment/import_local.dart'; 10 | import 'package:flutter_manager/widget/dialog/project/import.dart'; 11 | import 'package:flutter_manager/widget/drop_file.dart'; 12 | import 'package:jtech_base/jtech_base.dart'; 13 | import 'knowledge/index.dart'; 14 | import 'package/index.dart'; 15 | import 'project/index.dart'; 16 | import 'settings/index.dart'; 17 | 18 | /* 19 | * 首页 20 | * @author wuxubaiyang 21 | * @Time 2023/11/21 13:57 22 | */ 23 | class HomePage extends ProviderPage { 24 | HomePage({super.key, super.state}); 25 | 26 | @override 27 | HomeProvider createPageProvider(BuildContext context, GoRouterState? state) => 28 | HomeProvider(context, state); 29 | 30 | @override 31 | Widget buildWidget(BuildContext context) { 32 | return Scaffold( 33 | appBar: CustomAppBar( 34 | title: Text(S.current.appName), 35 | ), 36 | body: DropFileView( 37 | hint: '可导入项目/环境', 38 | onDoneValidator: provider.dropDone, 39 | child: _buildContent(context), 40 | ), 41 | ); 42 | } 43 | 44 | // 构建内容 45 | Widget _buildContent(BuildContext context) { 46 | final children = provider.pages.map((e) { 47 | return e.child ?? const SizedBox(); 48 | }).toList(); 49 | return createSelector( 50 | selector: (_, provider) => provider.currentIndex, 51 | builder: (_, currentIndex, _) { 52 | return Row(children: [ 53 | _buildNavigation(context, currentIndex), 54 | const VerticalDivider(), 55 | Expanded( 56 | child: IndexedStack( 57 | index: currentIndex, 58 | children: children, 59 | ), 60 | ), 61 | ]); 62 | }, 63 | ); 64 | } 65 | 66 | // 构建导航栏 67 | Widget _buildNavigation(BuildContext context, int currentIndex) { 68 | return NavigationRail( 69 | selectedIndex: currentIndex, 70 | trailing: _buildNavigationTrailing(context), 71 | onDestinationSelected: provider.setCurrentIndex, 72 | destinations: provider.pages.map((e) { 73 | return NavigationRailDestination( 74 | padding: EdgeInsets.only(top: 8), 75 | icon: e.icon ?? Icon(Icons.error), 76 | label: Text(e.label), 77 | ); 78 | }).toList(), 79 | ); 80 | } 81 | 82 | // 构建导航侧栏尾部 83 | Widget _buildNavigationTrailing(BuildContext context) { 84 | return Expanded( 85 | child: Column(mainAxisAlignment: MainAxisAlignment.end, children: [ 86 | Debug.buildDebugButton(context), 87 | FutureBuilder( 88 | future: Tool.version, 89 | builder: (_, snap) { 90 | return TextButton( 91 | onPressed: provider.checkAppVersion, 92 | child: Text('v${snap.data}'), 93 | ); 94 | }, 95 | ), 96 | const SizedBox(height: 14), 97 | ]), 98 | ); 99 | } 100 | } 101 | 102 | /* 103 | * 首页状态管理 104 | * @author wuxubaiyang 105 | * @Time 2023/11/21 14:02 106 | */ 107 | class HomeProvider extends PageProvider { 108 | // 导航分页集合 109 | late final pages = [ 110 | OptionItem( 111 | label: '项目', 112 | icon: Icon(Icons.home_rounded), 113 | child: HomeProjectView(), 114 | ), 115 | OptionItem( 116 | label: '打包', 117 | icon: Icon(Icons.build), 118 | child: HomePackageView(), 119 | ), 120 | OptionItem( 121 | label: '知识库', 122 | icon: Icon(Icons.document_scanner), 123 | child: HomeKnowledgeView(), 124 | ), 125 | OptionItem( 126 | label: '设置', 127 | icon: Icon(Icons.settings), 128 | child: HomeSettingsView(), 129 | ), 130 | ]; 131 | 132 | HomeProvider(super.context, super.state) { 133 | // 注册设置跳转方法 134 | context.setting.addListener(() { 135 | if (context.setting.selectedKey != null) { 136 | setCurrentIndex(pages.length - 1); 137 | } 138 | }); 139 | } 140 | 141 | // 导航下标管理 142 | int _currentIndex = 0; 143 | 144 | // 获取导航下标 145 | int get currentIndex => _currentIndex; 146 | 147 | // 文件拖拽完成 148 | Future dropDone(List paths) async { 149 | for (var e in paths) { 150 | // 判断是否为环境信息 151 | if (EnvironmentTool.isAvailable(e)) { 152 | final env = Environment.createImport(e); 153 | await showImportEnvLocal(context, env: env); 154 | } else { 155 | // 判断是否为项目信息(如果没有环境则无法导入项目) 156 | final project = await ProjectTool.getProjectInfo(e); 157 | if (project == null || !context.mounted) continue; 158 | if (!context.env.hasEnvironment) return '请先添加环境信息'; 159 | await showImportProject(context, project: project); 160 | } 161 | } 162 | return null; 163 | } 164 | 165 | // 检查版本更新 166 | void checkAppVersion() { 167 | /// TODO 检查版本更新 168 | } 169 | 170 | // 设置导航下标 171 | void setCurrentIndex(int index) { 172 | if (index < 0 || index >= pages.length) return; 173 | _currentIndex = index; 174 | notifyListeners(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/database/model/project.dart'; 3 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 4 | import 'package:flutter_manager/widget/dialog/project/import.dart'; 5 | import 'package:flutter_manager/widget/empty_box.dart'; 6 | import 'package:jtech_base/jtech_base.dart'; 7 | 8 | import 'appbar.dart'; 9 | import 'platform/android.dart'; 10 | import 'platform/ios.dart'; 11 | import 'platform/linux.dart'; 12 | import 'platform/macos.dart'; 13 | import 'platform/web.dart'; 14 | import 'platform/widgets/provider.dart'; 15 | import 'platform/windows.dart'; 16 | import 'tabbar.dart'; 17 | 18 | /* 19 | * 项目详情页 20 | * @author wuxubaiyang 21 | * @Time 2023/11/30 16:35 22 | */ 23 | class ProjectDetailPage extends ProviderPage { 24 | ProjectDetailPage({super.key, super.state}); 25 | 26 | @override 27 | ProjectDetailProvider createPageProvider( 28 | BuildContext context, GoRouterState? state) => 29 | ProjectDetailProvider(context, state); 30 | 31 | @override 32 | List extensionProviders() => [ 33 | ChangeNotifierProxyProvider( 34 | create: (context) => PlatformProvider(context, null), 35 | update: (context, provider, platformProvider) { 36 | if (provider.project != platformProvider?.project) { 37 | return PlatformProvider(context, provider.project?.copyWith()); 38 | } 39 | return platformProvider!; 40 | }, 41 | ), 42 | ]; 43 | 44 | @override 45 | Widget buildWidget(BuildContext context) { 46 | final project = provider.project; 47 | return Scaffold( 48 | body: EmptyBoxView( 49 | hint: '项目不存在', 50 | isEmpty: project == null, 51 | builder: (_, _) { 52 | if (project == null) return const SizedBox(); 53 | return _buildContent(context, project); 54 | }, 55 | ), 56 | ); 57 | } 58 | 59 | // 构建内容 60 | Widget _buildContent(BuildContext context, Project project) { 61 | return Selector>( 62 | selector: (_, provider) => provider.platformList, 63 | builder: (_, platforms, _) { 64 | return DefaultTabController( 65 | length: range(platforms.length, 1, PlatformType.values.length), 66 | child: NestedScrollView( 67 | controller: provider.scrollController, 68 | headerSliverBuilder: (_, _) => 69 | [_buildAppBar(context, platforms, project)], 70 | body: _buildTabBarView(context, platforms), 71 | ), 72 | ); 73 | }, 74 | ); 75 | } 76 | 77 | // 构建AppBar 78 | Widget _buildAppBar( 79 | BuildContext context, List platforms, Project project) { 80 | return createSelector< bool>( 81 | selector: (_, provider) => provider.isScrollTop, 82 | builder: (_, isScrollTop, _) { 83 | return ProjectDetailAppBar( 84 | project: project, 85 | isCollapsed: isScrollTop, 86 | bottom: _buildTabBar(context, platforms), 87 | onProjectEdit: () => showImportProject( 88 | context, 89 | project: project, 90 | ).then(provider.updateProject), 91 | ); 92 | }, 93 | ); 94 | } 95 | 96 | // 构建TabBar 97 | PreferredSize _buildTabBar( 98 | BuildContext context, List platforms) { 99 | return PreferredSize( 100 | preferredSize: const Size.fromHeight(kToolbarHeight), 101 | child: ProjectDetailTabBar( 102 | platforms: platforms, 103 | ), 104 | ); 105 | } 106 | 107 | // 构建TabBarView 108 | Widget _buildTabBarView(BuildContext context, List platforms) { 109 | final views = platforms.map(provider.getPlatformView).toList(); 110 | return TabBarView(children: [ 111 | ...views, 112 | if (views.isEmpty) 113 | const EmptyBoxView( 114 | isEmpty: true, 115 | hint: '暂无平台', 116 | ), 117 | ]); 118 | } 119 | } 120 | 121 | /* 122 | * 项目详情页状态管理 123 | * @author wuxubaiyang 124 | * @Time 2023/11/30 16:35 125 | */ 126 | class ProjectDetailProvider extends PageProvider { 127 | // 头部内容高度 128 | final headerHeight = 165.0; 129 | 130 | // 滚动控制器 131 | final scrollController = ScrollController(); 132 | 133 | // 根据平台类型获取对应的页面 134 | Widget getPlatformView(PlatformType platform) => switch (platform) { 135 | PlatformType.android => ProjectPlatformAndroidView(), 136 | PlatformType.ios => ProjectPlatformIosView(), 137 | PlatformType.web => ProjectPlatformWebView(), 138 | PlatformType.macos => ProjectPlatformMacosView(), 139 | PlatformType.windows => ProjectPlatformWindowsView(), 140 | PlatformType.linux => ProjectPlatformLinuxView(), 141 | }; 142 | 143 | ProjectDetailProvider(super.context, super.state) { 144 | // 监听滚动状态 145 | scrollController.addListener(_updateScrollTop); 146 | } 147 | 148 | // 记录是否滚动到顶部状态 149 | bool _isScrollTop = false; 150 | 151 | // 判断当前是否已经滚动到顶部 152 | bool get isScrollTop => _isScrollTop; 153 | 154 | // 缓存项目信息 155 | late Project? _project = getExtra(); 156 | 157 | // 项目信息 158 | Project? get project => _project; 159 | 160 | // 更新项目信息 161 | void updateProject(Project? project) { 162 | if (project == null) return; 163 | _project = project; 164 | notifyListeners(); 165 | } 166 | 167 | // 更新滚动到顶部状态 168 | void _updateScrollTop() { 169 | final isScrollTop = scrollController.offset >= headerHeight; 170 | if (_isScrollTop == isScrollTop) return; 171 | _isScrollTop = isScrollTop; 172 | notifyListeners(); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/widget/dialog/project/permission.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/tool/project/platform/platform.dart'; 3 | import 'package:flutter_manager/tool/project/project.dart'; 4 | import 'package:jtech_base/jtech_base.dart'; 5 | 6 | // 展示项目权限选择弹窗 7 | Future?> showProjectPermission( 8 | BuildContext context, { 9 | required PlatformType platform, 10 | List permissions = const [], 11 | }) { 12 | return showDialog>( 13 | context: context, 14 | builder: (context) => ProjectPermissionDialog( 15 | platform: platform, 16 | permissions: permissions, 17 | ), 18 | ); 19 | } 20 | 21 | /* 22 | * 权限选择列表弹窗 23 | * @author wuxubaiyang 24 | * @Time 2023/11/25 19:38 25 | */ 26 | class ProjectPermissionDialog 27 | extends ProviderView { 28 | // 所选平台 29 | final PlatformType platform; 30 | 31 | // 已选权限集合 32 | final List permissions; 33 | 34 | ProjectPermissionDialog( 35 | {super.key, required this.platform, this.permissions = const []}); 36 | 37 | @override 38 | ProjectPermissionDialogProvider? createProvider(BuildContext context) => 39 | ProjectPermissionDialogProvider(context, permissions); 40 | 41 | @override 42 | Widget buildWidget(BuildContext context) { 43 | return CustomDialog( 44 | title: _buildTitle(context), 45 | content: _buildContent(context), 46 | style: CustomDialogStyle( 47 | constraints: const BoxConstraints.tightFor(width: 340), 48 | ), 49 | actions: [ 50 | TextButton( 51 | onPressed: context.pop, 52 | child: const Text('取消'), 53 | ), 54 | TextButton( 55 | child: const Text('确定'), 56 | onPressed: () => context.pop(provider.selectPermissions), 57 | ), 58 | ], 59 | ); 60 | } 61 | 62 | // 构建标题 63 | Widget _buildTitle(BuildContext context) { 64 | return createSelector( 65 | selector: (_, provider) => provider.selectPermissions.length, 66 | builder: (_, count, _) { 67 | return Text('${platform.name}权限($count)'); 68 | }, 69 | ); 70 | } 71 | 72 | // 构建内容 73 | Widget _buildContent(BuildContext context) { 74 | final searchController = provider.searchController; 75 | return LoadingFutureBuilder?>( 76 | future: ProjectTool.getFullPermissions(platform), 77 | builder: (_, permissions, _) { 78 | if (permissions == null) return const SizedBox(); 79 | return createSelector>( 80 | selector: (_, provider) => provider.selectPermissions, 81 | builder: (_, selectPermissions, _) { 82 | return StatefulBuilder( 83 | builder: (_, setState) { 84 | final tempList = searchController.text.isNotEmpty 85 | ? permissions 86 | .where((e) => e.search(searchController.text)) 87 | .toList() 88 | : permissions; 89 | final checked = (tempList.length != selectPermissions.length) 90 | ? (selectPermissions.isEmpty ? false : null) 91 | : true; 92 | return Column(children: [ 93 | CheckboxListTile( 94 | value: checked, 95 | tristate: true, 96 | title: SearchBar( 97 | hintText: '输入过滤条件', 98 | controller: searchController, 99 | onChanged: (v) => setState(() {}), 100 | ), 101 | onChanged: (v) => provider.selectPermissions = 102 | !(checked ?? false) ? permissions : [], 103 | ), 104 | Expanded( 105 | child: _buildPermissionList( 106 | context, 107 | tempList, 108 | selectPermissions, 109 | ), 110 | ), 111 | ]); 112 | }, 113 | ); 114 | }, 115 | ); 116 | }, 117 | ); 118 | } 119 | 120 | // 构建权限列表 121 | Widget _buildPermissionList( 122 | BuildContext context, 123 | List permissions, 124 | List selectPermissions, 125 | ) { 126 | return ListView.separated( 127 | shrinkWrap: true, 128 | itemCount: permissions.length, 129 | separatorBuilder: (_, _) => const Divider(), 130 | itemBuilder: (_, i) { 131 | final item = permissions[i]; 132 | return CheckboxListTile( 133 | value: selectPermissions.contains(item), 134 | onChanged: (_) => provider.selectPermission(item), 135 | title: Text(item.name, maxLines: 1, overflow: TextOverflow.ellipsis), 136 | subtitle: Text( 137 | item.desc, 138 | maxLines: 2, 139 | overflow: TextOverflow.ellipsis, 140 | ), 141 | ); 142 | }, 143 | ); 144 | } 145 | } 146 | 147 | class ProjectPermissionDialogProvider extends BaseProvider { 148 | // 搜索控制器 149 | final searchController = TextEditingController(); 150 | 151 | ProjectPermissionDialogProvider(super.context, this._selectPermissions); 152 | 153 | // 已选权限集合 154 | List _selectPermissions; 155 | 156 | // 获取已选权限集合 157 | List get selectPermissions => _selectPermissions; 158 | 159 | // 设置已选权限集合 160 | set selectPermissions(List value) { 161 | _selectPermissions = value; 162 | notifyListeners(); 163 | } 164 | 165 | // 选择权限 166 | void selectPermission(PlatformPermission permission) { 167 | _selectPermissions = [ 168 | if (!selectPermissions.contains(permission)) permission, 169 | ...selectPermissions.where((e) => e != permission), 170 | ]; 171 | notifyListeners(); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/page/home/project/detail/appbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_manager/database/model/project.dart'; 3 | import 'package:flutter_manager/widget/dialog/project/asset.dart'; 4 | import 'package:flutter_manager/widget/dialog/project/build.dart'; 5 | import 'package:flutter_manager/widget/dialog/project/font.dart'; 6 | import 'package:flutter_manager/widget/env_badge.dart'; 7 | import 'package:flutter_manager/widget/app_bar.dart'; 8 | import 'package:jtech_base/jtech_base.dart'; 9 | import 'package:open_dir/open_dir.dart'; 10 | 11 | /* 12 | * 项目详情页appBar 13 | * @author wuxubaiyang 14 | * @Time 2023/12/13 15:54 15 | */ 16 | class ProjectDetailAppBar extends StatelessWidget { 17 | // 项目信息 18 | final Project project; 19 | 20 | // 折叠头部高度 21 | final double expandedHeight; 22 | 23 | // 底部组件 24 | final PreferredSizeWidget? bottom; 25 | 26 | // 是否为展开模式 27 | final bool isCollapsed; 28 | 29 | // 项目编辑回调 30 | final VoidCallback? onProjectEdit; 31 | 32 | const ProjectDetailAppBar({ 33 | super.key, 34 | required this.project, 35 | this.expandedHeight = 165.0, 36 | this.isCollapsed = false, 37 | this.onProjectEdit, 38 | this.bottom, 39 | }); 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | final color = project.color; 44 | final hasColor = color != Colors.transparent; 45 | return SliverAppBar( 46 | pinned: true, 47 | bottom: bottom, 48 | titleSpacing: 6, 49 | expandedHeight: expandedHeight, 50 | automaticallyImplyLeading: false, 51 | scrolledUnderElevation: hasColor ? 8 : 1, 52 | surfaceTintColor: hasColor ? color : null, 53 | title: CustomAppBar( 54 | automaticallyImplyLeading: false, 55 | backgroundColor: Colors.transparent, 56 | actions: [ 57 | const BackButton(), 58 | _buildCollapsedTitle(context), 59 | const Spacer(), 60 | ], 61 | ), 62 | flexibleSpace: _buildFlexibleSpace( 63 | context, hasColor ? color.withValues(alpha: 0.2) : null), 64 | ); 65 | } 66 | 67 | // 构建收缩后的标题 68 | Widget _buildCollapsedTitle(BuildContext context) { 69 | final borderRadius = BorderRadius.circular(8); 70 | return AnimatedOpacity( 71 | opacity: isCollapsed ? 1 : 0, 72 | duration: const Duration(milliseconds: 200), 73 | child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [ 74 | ClipRRect( 75 | borderRadius: borderRadius, 76 | child: CustomImage.file( 77 | project.logo, 78 | size: const Size.square(30), 79 | ), 80 | ), 81 | const SizedBox(width: 14), 82 | Text(project.label), 83 | const SizedBox(width: 8), 84 | EnvBadge(env: project.environment), 85 | ]), 86 | ); 87 | } 88 | 89 | // 构建FlexibleSpace 90 | Widget _buildFlexibleSpace(BuildContext context, Color? color) { 91 | return FlexibleSpaceBar( 92 | background: Card( 93 | child: Container( 94 | color: color, 95 | child: Row(children: [ 96 | Expanded(child: _buildProjectInfo(context)), 97 | _buildActions(context), 98 | ]), 99 | ), 100 | ), 101 | ); 102 | } 103 | 104 | // 构建项目信息 105 | Widget _buildProjectInfo(BuildContext context) { 106 | var bodyStyle = Theme.of(context).textTheme.bodySmall; 107 | final color = bodyStyle?.color?.withValues(alpha: 0.4); 108 | bodyStyle = bodyStyle?.copyWith(color: color); 109 | return ListTile( 110 | isThreeLine: true, 111 | title: Row(children: [ 112 | ConstrainedBox( 113 | constraints: BoxConstraints.loose(const Size.fromWidth(220)), 114 | child: 115 | Text(project.label, maxLines: 1, overflow: TextOverflow.ellipsis), 116 | ), 117 | const SizedBox(width: 8), 118 | EnvBadge(env: project.environment), 119 | const SizedBox(width: 4), 120 | IconButton( 121 | iconSize: 14, 122 | onPressed: onProjectEdit, 123 | icon: const Icon(Icons.edit), 124 | visualDensity: VisualDensity.compact, 125 | ), 126 | ]), 127 | leading: ClipRRect( 128 | borderRadius: BorderRadius.circular(8), 129 | child: CustomImage.file( 130 | project.logo, 131 | size: const Size.square(55), 132 | ), 133 | ), 134 | subtitle: Text(project.path, 135 | maxLines: 1, style: bodyStyle, overflow: TextOverflow.ellipsis), 136 | ); 137 | } 138 | 139 | // 构建操作按钮 140 | Widget _buildActions(BuildContext context) { 141 | return Row(spacing: 14, children: [ 142 | IconButton.outlined( 143 | iconSize: 20, 144 | tooltip: 'Asset管理', 145 | icon: const Icon(Icons.assessment_outlined), 146 | onPressed: () => showProjectAsset(context, project: project), 147 | ), 148 | IconButton.outlined( 149 | iconSize: 20, 150 | tooltip: '字体管理', 151 | icon: const Icon(Icons.font_download_outlined), 152 | onPressed: () => showProjectFont(context, project: project), 153 | ), 154 | IconButton.outlined( 155 | iconSize: 20, 156 | tooltip: '打开项目目录', 157 | icon: const Icon(Icons.file_open_outlined), 158 | onPressed: () => OpenDir().openNativeDir(path: project.path), 159 | ), 160 | FilledButton.icon( 161 | label: const Text('打包'), 162 | icon: const Icon(Icons.build), 163 | style: ButtonStyle( 164 | fixedSize: WidgetStatePropertyAll(const Size.fromHeight(55)), 165 | shape: WidgetStatePropertyAll( 166 | RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), 167 | textStyle: 168 | WidgetStatePropertyAll(Theme.of(context).textTheme.bodyLarge), 169 | ), 170 | onPressed: () => showProjectBuild(context, project: project), 171 | ), 172 | SizedBox(), 173 | ]); 174 | } 175 | } 176 | --------------------------------------------------------------------------------