├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── analysis_options.yaml ├── assets ├── app_icon.ico ├── flags │ ├── de.svg │ ├── en.svg │ ├── pl.svg │ └── ru.svg ├── i18n │ ├── bg.json │ ├── de.json │ ├── en.json │ ├── fr.json │ ├── hu.json │ ├── pl.json │ └── ru.json └── logos │ └── kyber.svg ├── build.yaml ├── crowdin.yml ├── installer ├── app_icon.ico └── installer.iss ├── lib ├── api │ ├── backend │ │ └── download_info.dart │ └── kyber │ │ ├── proxy.dart │ │ └── server_response.dart ├── constants │ ├── api_constants.dart │ ├── maps.dart │ ├── mod_categories.dart │ ├── modes.dart │ └── routes.dart ├── logic │ ├── event_cubic.dart │ ├── frosty_cubic.dart │ ├── game_status_cubic.dart │ ├── status_cubit.dart │ └── widget_cubic.dart ├── main.dart ├── screens │ ├── cosmetic_mods │ │ └── cosmetic_mods.dart │ ├── dialogs │ │ ├── battlefront_options_dialog.dart │ │ ├── installed_mod_dialog.dart │ │ ├── join_server_dialog │ │ │ ├── join_dialog.dart │ │ │ └── widgets │ │ │ │ ├── download_screen.dart │ │ │ │ ├── password_input.dart │ │ │ │ ├── required_mods.dart │ │ │ │ └── team_selector.dart │ │ ├── kyber_release_channel_dialog.dart │ │ ├── outdated_frosty_dialog.dart │ │ ├── update_dialog │ │ │ └── update_dialog.dart │ │ └── walk_through │ │ │ ├── walk_through.dart │ │ │ └── widgets │ │ │ ├── frosty_selector.dart │ │ │ ├── installed_mods.dart │ │ │ └── nexusmods_login.dart │ ├── discord_events │ │ └── discord_events.dart │ ├── errors │ │ ├── battlefront_not_installed.dart │ │ ├── chromium_not_found.dart │ │ ├── missing_permissions.dart │ │ └── no_executable.dart │ ├── feedback.dart │ ├── installed_mods.dart │ ├── map_rotation_creator │ │ ├── map_rotation_creator.dart │ │ ├── map_rotation_export_dialog.dart │ │ └── widgets │ │ │ ├── map_rotation_active_map.dart │ │ │ └── map_rotation_mode_maps.dart │ ├── mod_browser.dart │ ├── mod_profiles │ │ ├── edit_profile.dart │ │ ├── frosty_profile.dart │ │ ├── mod_profiles.dart │ │ └── widgets │ │ │ ├── active_mods.dart │ │ │ ├── export_profile_dialog.dart │ │ │ ├── installed_mods.dart │ │ │ └── mod_category.dart │ ├── run_battlefront │ │ ├── run_battlefront.dart │ │ └── run_dialog.dart │ ├── saved_profiles.dart │ ├── server_browser │ │ ├── server_browser.dart │ │ └── widgets │ │ │ └── server.dart │ ├── server_host │ │ ├── hosting_dialog.dart │ │ └── server_host.dart │ ├── settings │ │ ├── settings.dart │ │ └── widgets │ │ │ ├── platform_selector.dart │ │ │ ├── settings_card.dart │ │ │ └── symlinks_dialog.dart │ └── troubleshooting.dart ├── utils │ ├── app_locale.dart │ ├── auto_updater.dart │ ├── battlefront_options.dart │ ├── custom_logger.dart │ ├── dll_injector.dart │ ├── helpers │ │ ├── map_helper.dart │ │ ├── origin_helper.dart │ │ ├── path_helper.dart │ │ ├── platform_helper.dart │ │ ├── puppeteer_helper.dart │ │ ├── storage_helper.dart │ │ ├── system_tasks.dart │ │ ├── unzip_helper.dart │ │ └── window_helper.dart │ ├── services │ │ ├── api_service.dart │ │ ├── download_service.dart │ │ ├── frosty_profile_service.dart │ │ ├── frosty_service.dart │ │ ├── kyber_api_service.dart │ │ ├── mod_installer_service.dart │ │ ├── mod_service.dart │ │ ├── navigator_service.dart │ │ ├── nexusmods_api_service.dart │ │ ├── nexusmods_login_service.dart │ │ ├── notification_service.dart │ │ ├── profile_service.dart │ │ └── rpc_service.dart │ ├── translation │ │ ├── translate_preferences.dart │ │ └── translation_delegate.dart │ └── types │ │ ├── freezed │ │ ├── discord_event.dart │ │ ├── frosty_collection.dart │ │ ├── frosty_cubic_state.dart │ │ ├── frosty_profile.dart │ │ ├── frosty_version.dart │ │ ├── game_status.dart │ │ ├── github_asset.dart │ │ ├── kyber_server.dart │ │ ├── mod.dart │ │ ├── mod_profile.dart │ │ └── nexus_mods_search_result.dart │ │ ├── frosty_config.dart │ │ ├── map.dart │ │ ├── mod_info.dart │ │ ├── mode.dart │ │ ├── pack_type.dart │ │ ├── process_details.dart │ │ └── saved_profile.dart └── widgets │ ├── button_text.dart │ ├── custom_button.dart │ ├── custom_tooltip.dart │ ├── navigation_bar.dart │ └── unordered_list.dart ├── packages └── dynamic_env │ ├── .gitignore │ ├── .metadata │ ├── analysis_options.yaml │ ├── lib │ ├── dynamic_env.dart │ ├── dynamic_env_method_channel.dart │ └── dynamic_env_platform_interface.dart │ ├── pubspec.yaml │ └── windows │ ├── .gitignore │ ├── CMakeLists.txt │ ├── dynamic_env_plugin.cpp │ ├── dynamic_env_plugin.h │ ├── dynamic_env_plugin_c_api.cpp │ └── include │ └── dynamic_env │ └── dynamic_env_plugin_c_api.h ├── pubspec.lock ├── pubspec.yaml └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | /bin 34 | .local-chromium/ 35 | *.freezed.dart 36 | *.g.dart 37 | 38 | # Web related 39 | lib/generated_plugin_registrant.dart 40 | 41 | # Symbolication related 42 | app.*.symbols 43 | 44 | # Obfuscation related 45 | app.*.map.json 46 | 47 | # Android Studio will place build artifacts here 48 | /android/app/debug 49 | /android/app/profile 50 | /android/app/release 51 | /installers/ 52 | /.local-chromium/ 53 | /.local-chrome/ 54 | -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 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: ee4e09cce01d6f2d7f4baebd247fde02e5008851 17 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 18 | - platform: windows 19 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 20 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Kyber Mod Manager 3 |

4 | 5 | 6 |

A Mod Manager build for Kyber.

7 |

This app is not affiliated with Kyber or any of its creators.

8 | 9 |

10 | 11 | Discord 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

23 | 24 |

25 | Key Features • 26 | Download • 27 | Screenshots • 28 | Credits • 29 | License 30 |

31 | 32 | # IMPORTANT 33 | 34 | #### In order for the Mod Manager to work you need the following applications installed: 35 | 36 | * [WinRAR](https://www.win-rar.com/) or [7-Zip](https://www.7-zip.org/) 37 | 38 | ## Key Features 39 | 40 | * Automatic mod downloading (Mods are directly downloaded from [NexusMods](https://www.nexusmods.com/)) 41 | * Hosting & Joining servers 42 | * Automatic Kyber injection 43 | * FrostyFix like function 44 | * Automatic Mod Profile creation 45 | * It creates a Frosty-Pack and applies the mods automatically. 46 | 47 | ## General Information 48 | 49 | * As soon as **Kyber v2** is getting released this Mod Manager will be updated. 50 | * For the best experience it is recommended to use **EA-Desktop**. 51 | * PRs are welcome 52 | 53 | ## Download 54 | 55 | #### [Windows ![windows](https://media.discordapp.net/attachments/810799100940255260/838488668816932965/ezgif-6-ac9683508192.png)](https://github.com/7reax/kyber-mod-manager/releases/latest) 56 | 57 | - Download the exe, click More Info > Run Anyway > Open Kyber Mod Manager 58 | 59 | ## Screenshots 60 | 61 | |

Pages

|

| 62 | |:---------------------------------:|:--------------------------------------------:| 63 | | Server Browser | | 64 | | Hosting Page | | 65 | | Mod Profile Edit Page | | 66 | | Settings | | 67 | 68 | ## For developers: How to modify the Mod Manager 69 | 70 | To clone and run this application, you'll need [Git](https://git-scm.com) and [Flutter](https://docs.flutter.dev/get-started/install) installed on your computer. From your command line: 71 | 72 | ```bash 73 | # Clone this repository 74 | $ git clone https://github.com/7reax/kyber-mod-manager.git 75 | 76 | # Go into the repository 77 | $ cd kyber-mod-manager 78 | 79 | # Install dependencies 80 | $ flutter pub get 81 | 82 | # Run the generator 83 | $ flutter pub run build_runner build 84 | 85 | # Run the app 86 | $ flutter run lib/main.dart 87 | 88 | # Build the app 89 | $ flutter build windows 90 | ``` 91 | 92 | ## Credits 93 | 94 | This software uses the following open source projects: 95 | 96 | - [Flutter](https://flutter.com/) 97 | - [Fluent UI](https://pub.dev/packages/fluent_ui) 98 | 99 | ## License 100 | 101 | MIT 102 | 103 | --- 104 | 105 | > [reax.at](https://reax.at)  ·  106 | > GitHub [@7reax](https://github.com/7reax)  ·  107 | > Discord [liam#2306](https://discord.gg/6WMrYRwqhr) 108 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | linter: 4 | rules: 5 | -------------------------------------------------------------------------------- /assets/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArmchairDevelopers/KyberModManager/6a665597132f7e7cebd8664ca5853cdea7848fca/assets/app_icon.ico -------------------------------------------------------------------------------- /assets/flags/de.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/flags/en.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/flags/pl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/flags/ru.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/logos/kyber.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | json_serializable: 5 | options: 6 | any_map: false 7 | checked: false 8 | constructor: "" 9 | create_factory: true 10 | create_to_json: true 11 | disallow_unrecognized_keys: false 12 | explicit_to_json: true 13 | field_rename: none 14 | generic_argument_factories: false 15 | ignore_unannotated: false 16 | include_if_null: true 17 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /assets/i18n/en.json 3 | translation: /assets/i18n/%two_letters_code%.json 4 | -------------------------------------------------------------------------------- /installer/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArmchairDevelopers/KyberModManager/6a665597132f7e7cebd8664ca5853cdea7848fca/installer/app_icon.ico -------------------------------------------------------------------------------- /installer/installer.iss: -------------------------------------------------------------------------------- 1 | #pragma include __INCLUDE__ + ";" + ReadReg(HKLM, "Software\Mitrich Software\Inno Download Plugin", "InstallDir") 2 | 3 | #define MyAppName "Kyber Mod Manager" 4 | #define MyAppVersion "1.0.11" 5 | #define MyAppPublisher "liam" 6 | #define MyAppURL "https://reax.at" 7 | #define AppId "Kyber Mod Manager" 8 | #define MyAppExeName "kyber_mod_manager.exe" 9 | 10 | [Setup] 11 | AppId={#AppId} 12 | AppName={#MyAppName} 13 | AppVersion={#MyAppVersion} 14 | AppPublisher={#MyAppPublisher} 15 | AppPublisherURL={#MyAppURL} 16 | AppSupportURL={#MyAppURL} 17 | AppUpdatesURL={#MyAppURL} 18 | DefaultDirName={autopf}\{#MyAppName} 19 | DisableProgramGroupPage=yes 20 | SetupIconFile=app_icon.ico 21 | OutputDir=./ 22 | OutputBaseFilename=KMM Setup 23 | Compression=lzma 24 | SolidCompression=yes 25 | WizardStyle=modern 26 | 27 | 28 | #include 29 | 30 | [Languages] 31 | Name: "english"; MessagesFile: "compiler:Default.isl" 32 | 33 | [Tasks] 34 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 35 | 36 | [Files] 37 | Source: "..\build\windows\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion 38 | Source: "..\build\windows\runner\Release\dart_discord_rpc_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 39 | Source: "..\build\windows\runner\Release\discord-rpc.dll"; DestDir: "{app}"; Flags: ignoreversion 40 | Source: "..\build\windows\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 41 | Source: "..\build\windows\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion 42 | Source: "..\build\windows\runner\Release\kyber_mod_Manager.exe"; DestDir: "{app}"; Flags: ignoreversion 43 | Source: "..\build\windows\runner\Release\sentry_flutter_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 44 | Source: "..\build\windows\runner\Release\windows_taskbar_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 45 | Source: "..\build\windows\runner\Release\flutter_platform_alert_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 46 | Source: "..\build\windows\runner\Release\system_theme_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 47 | Source: "..\build\windows\runner\Release\auto_update_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 48 | Source: "..\build\windows\runner\Release\desktop_drop_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 49 | Source: "..\build\windows\runner\Release\flutter_acrylic_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 50 | Source: "..\build\windows\runner\Release\screen_retriever_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 51 | Source: "..\build\windows\runner\Release\system_tray_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 52 | Source: "..\build\windows\runner\Release\webview_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 53 | Source: "..\build\windows\runner\Release\WebView2Loader.dll"; DestDir: "{app}"; Flags: ignoreversion 54 | Source: "..\build\windows\runner\Release\dynamic_env_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 55 | Source: "..\build\windows\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 56 | Source: "..\build\windows\runner\Release\file_selector_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 57 | Source: "..\build\windows\runner\Release\protocol_handler_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion 58 | Source: "..\build\windows\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs 59 | Source: "{tmp}\chrome-win.zip"; DestDir: "{app}"; Flags: external deleteafterinstall ignoreversion skipifsourcedoesntexist; ExternalSize: 186331554 60 | Source: "7za.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall; 61 | 62 | [Icons] 63 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; AppUserModelID: "reax.KMM" 64 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 65 | 66 | [UninstallDelete] 67 | Type: filesandordirs; Name: "{app}\970485" 68 | 69 | [Code] 70 | function NextButtonClick(CurPageID: Integer): Boolean; 71 | var 72 | ResultCode: Integer; 73 | begin 74 | Result := True 75 | if (CurPageID = 9) and not DirExists(ExpandConstant('{app}\970485\chrome-win')) then 76 | begin 77 | idpAddFile('https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/970485/chrome-win.zip', ExpandConstant('{tmp}\chrome-win.zip')); 78 | idpDownloadAfter(wpReady); 79 | Result:= True; 80 | end; 81 | end; 82 | 83 | [Run] 84 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 85 | Filename: {tmp}\7za.exe; Parameters: "x ""{tmp}\chrome-win.zip"" -o""{app}\970485"" * -r -aoa"; Flags: runhidden runascurrentuser; 86 | -------------------------------------------------------------------------------- /lib/api/backend/download_info.dart: -------------------------------------------------------------------------------- 1 | class DownloadInfo { 2 | DownloadInfo({ 3 | required this.fileId, 4 | required this.fileName, 5 | required this.fileUrl, 6 | this.link, 7 | }); 8 | 9 | String? link; 10 | String fileName; 11 | String fileUrl; 12 | String fileId; 13 | 14 | factory DownloadInfo.fromJson(Map json) => DownloadInfo( 15 | link: json['link'], 16 | fileName: json['fileName'], 17 | fileUrl: json['fileUrl'], 18 | fileId: json['fileId'], 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /lib/api/kyber/proxy.dart: -------------------------------------------------------------------------------- 1 | class KyberProxy { 2 | KyberProxy({ 3 | required this.ip, 4 | required this.name, 5 | required this.flag, 6 | this.ping, 7 | }); 8 | 9 | String ip; 10 | String name; 11 | String flag; 12 | int? ping; 13 | 14 | factory KyberProxy.fromJson(Map json) => KyberProxy(ip: json["ip"], name: json["name"], flag: json["flag"], ping: json["ping"]); 15 | 16 | Map toJson() => {"ip": ip, "name": name, "flag": flag, "ping": ping}; 17 | } 18 | -------------------------------------------------------------------------------- /lib/api/kyber/server_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:kyber_mod_manager/utils/types/freezed/kyber_server.dart'; 2 | 3 | class ServerResponse { 4 | ServerResponse({ 5 | required this.page, 6 | required this.pageCount, 7 | required this.serverCount, 8 | required this.servers, 9 | }); 10 | 11 | late final int page; 12 | late final int pageCount; 13 | late final int serverCount; 14 | late final List servers; 15 | 16 | ServerResponse.fromJson(Map json) { 17 | page = json['page']; 18 | pageCount = json['pageCount']; 19 | serverCount = json['serverCount']; 20 | servers = List.from(json['servers']).map((e) => KyberServer.fromJson(e)).toList(); 21 | } 22 | 23 | Map toJson() { 24 | final _data = {}; 25 | _data['page'] = page; 26 | _data['pageCount'] = pageCount; 27 | _data['serverCount'] = serverCount; 28 | _data['servers'] = servers.map((e) => e.toJson()).toList(); 29 | return _data; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/constants/api_constants.dart: -------------------------------------------------------------------------------- 1 | const String _KYBER_BASE_URL = "https://kyber.gg"; 2 | const String KYBER_API_BASE_URL = "$_KYBER_BASE_URL/api"; 3 | const String KYBER_STATIC_URL = "$_KYBER_BASE_URL/static"; 4 | const String KYBER_LOGO_URL = "$_KYBER_BASE_URL/logo.svg"; 5 | const String BACKEND_API_BASE_URL = "https://kyber.reax.at/v2"; 6 | const String KYBER_DLL_LINK = "$KYBER_API_BASE_URL/downloads/distributions/stable/dll"; 7 | -------------------------------------------------------------------------------- /lib/constants/maps.dart: -------------------------------------------------------------------------------- 1 | final List> maps = [ 2 | {"map": "S5_1/Levels/MP/Geonosis_01/Geonosis_01", "name": "Geonosis"}, 3 | {"map": "S6_2/Geonosis_02/Levels/Geonosis_02/Geonosis_02", "name": "Geonosis"}, 4 | {"map": "Levels/MP/Kamino_01/Kamino_01", "name": "Kamino"}, 5 | {"map": "S7_1/Levels/Kamino_03/Kamino_03", "name": "Kamino"}, 6 | {"map": "Levels/MP/Naboo_01/Naboo_01", "name": "Naboo"}, 7 | {"map": "Levels/MP/Naboo_02/Naboo_02", "name": "Naboo"}, 8 | {"map": "S7_2/Levels/Naboo_03/Naboo_03", "name": "Naboo"}, 9 | {"map": "Levels/MP/Kashyyyk_01/Kashyyyk_01", "name": "Kashyyyk"}, 10 | {"map": "S7/Levels/Kashyyyk_02/Kashyyyk_02", "name": "Kashyyyk"}, 11 | {"map": "S8/Felucia/Levels/MP/Felucia_01/Felucia_01", "name": "Felucia"}, 12 | {"map": "S3/Levels/Kessel_01/Kessel_01", "name": "Kessel"}, 13 | {"map": "S9_3/Scarif/Levels/MP/Scarif_02/Scarif_02", "name": "Scarif"}, 14 | {"map": "Levels/MP/Tatooine_01/Tatooine_01", "name": "Tatooine"}, 15 | {"map": "S9_3/Tatooine_02/Tatooine_02", "name": "Tatooine"}, 16 | {"map": "Levels/MP/Yavin_01/Yavin_01", "name": "Yavin"}, 17 | {"map": "Levels/MP/Hoth_01/Hoth_01", "name": "Hoth"}, 18 | {"map": "S9_3/Hoth_02/Hoth_02", "name": "Hoth"}, 19 | {"map": "S2/Levels/CloudCity_01/CloudCity_01", "name": "Bespin"}, 20 | {"map": "S2_2/Levels/JabbasPalace_01/JabbasPalace_01", "name": "Tatooine"}, 21 | {"map": "Levels/MP/Endor_01/Endor_01", "name": "Endor"}, 22 | {"map": "S2_1/Levels/Endor_02/Endor_02", "name": "Endor - Ewok Village"}, 23 | {"map": "S8_1/Endor_04/Endor_04", "name": "Endor - Research Station 9"}, 24 | {"map": "Levels/MP/DeathStar02_01/DeathStar02_01", "name": "Death Star II"}, 25 | {"map": "Levels/MP/Jakku_01/Jakku_01", "name": "Jakku"}, 26 | {"map": "S9/Jakku_02/Jakku_02", "name": "Jakku"}, 27 | {"map": "Levels/MP/Takodana_01/Takodana_01", "name": "Takodana"}, 28 | {"map": "S9/Takodana_02/Takodana_02", "name": "Takodana"}, 29 | {"map": "Levels/MP/StarKiller_01/StarKiller_01", "name": "Starkiller Base"}, 30 | {"map": "S9/StarKiller_02/StarKiller_02", "name": "Starkiller Base"}, 31 | {"map": "S1/Levels/Crait_01/Crait_01", "name": "Crait"}, 32 | {"map": "S9_3/Crait/Crait_02", "name": "Crait"}, 33 | {"map": "S9/Paintball/Levels/MP/Paintball_01/Paintball_01", "name": "Ajan Kloss"}, 34 | {"map": "S9_3/COOP_NT_MC85/COOP_NT_MC85", "name": "MC85 Star Cruiser"}, 35 | {"map": "S9_3/COOP_NT_FOSD/COOP_NT_FOSD", "name": "First Order Star Destroyer"}, 36 | {"map": "Levels/Space/SB_DroidBattleShip_01/SB_DroidBattleShip_01", "name": "Ryloth"}, 37 | {"map": "Levels/Space/SB_Kamino_01/SB_Kamino_01", "name": "Kamino"}, 38 | {"map": "Levels/Space/SB_Fondor_01/SB_Fondor_01", "name": "Fondor"}, 39 | {"map": "Levels/Space/SB_Endor_01/SB_Endor_01", "name": "Endor"}, 40 | {"map": "Levels/Space/SB_Resurgent_01/SB_Resurgent_01", "name": "Unknown Regions"}, 41 | {"map": "S1/Levels/Space/SB_SpaceBear_01/SB_SpaceBear_01", "name": "D'Qar"} 42 | ]; 43 | -------------------------------------------------------------------------------- /lib/constants/mod_categories.dart: -------------------------------------------------------------------------------- 1 | final kyber_mod_categories = ['Gameplay', 'Server Host', 'Frosty Collections']; 2 | -------------------------------------------------------------------------------- /lib/constants/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:kyber_mod_manager/screens/feedback.dart' as feedback; 3 | import 'package:kyber_mod_manager/screens/mod_profiles/mod_profiles.dart'; 4 | import 'package:kyber_mod_manager/screens/run_battlefront/run_battlefront.dart'; 5 | import 'package:kyber_mod_manager/screens/saved_profiles.dart'; 6 | import 'package:kyber_mod_manager/screens/server_browser/server_browser.dart'; 7 | import 'package:kyber_mod_manager/screens/server_host/server_host.dart'; 8 | import 'package:kyber_mod_manager/screens/settings/settings.dart'; 9 | 10 | final routes = { 11 | '/server-browser': (context) => const ServerBrowser(), 12 | '/host-server': (context) => const ServerHost(), 13 | '/mod-profiles': (context) => const ModProfiles(), 14 | '/saved-profiles': (context) => const SavedProfiles(), 15 | '/run-battlefront': (context) => const RunBattlefront(), 16 | '/feedback': (context) => const feedback.Feedback(), 17 | '/settings': (context) => const Settings(), 18 | }; 19 | 20 | Route onGenerateRoute(RouteSettings settings) { 21 | final route = routes[settings.name]; 22 | if (route != null) { 23 | return FluentPageRoute(builder: (context) => route(settings.arguments)); 24 | } 25 | return FluentPageRoute(builder: (context) => const Text('error')); 26 | } 27 | -------------------------------------------------------------------------------- /lib/logic/event_cubic.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:kyber_mod_manager/utils/services/api_service.dart'; 5 | import 'package:kyber_mod_manager/utils/types/freezed/discord_event.dart'; 6 | 7 | class EventCubic extends Cubit { 8 | Timer? _timer; 9 | 10 | EventCubic() : super(EventCubicState(events: [])) { 11 | _loadEvents(); 12 | } 13 | 14 | @override 15 | Future close() { 16 | _timer?.cancel(); 17 | return super.close(); 18 | } 19 | 20 | void _loadEvents() async { 21 | state.events = await ApiService.getEvents(); 22 | emit(state); 23 | _timer = Timer.periodic(const Duration(minutes: 5), (_) => _loadEvents); 24 | } 25 | } 26 | 27 | class EventCubicState { 28 | EventCubicState({required this.events}); 29 | 30 | List events = []; 31 | } 32 | -------------------------------------------------------------------------------- /lib/logic/frosty_cubic.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:kyber_mod_manager/utils/helpers/path_helper.dart'; 5 | import 'package:kyber_mod_manager/utils/services/frosty_service.dart'; 6 | import 'package:kyber_mod_manager/utils/types/freezed/frosty_cubic_state.dart'; 7 | 8 | class FrostyCubic extends Cubit { 9 | Timer? _timer; 10 | 11 | FrostyCubic() : super(FrostyCubicState(isOutdated: false)) { 12 | _load(); 13 | } 14 | 15 | @override 16 | Future close() { 17 | _timer?.cancel(); 18 | return super.close(); 19 | } 20 | 21 | void _load() async { 22 | var isOutdated = await FrostyService.isOutdated(); 23 | var currentVersion = await FrostyService.getFrostyVersion(); 24 | var latestVersion = (await PathHelper.getFrostyVersions()).first; 25 | emit(state.copyWith(isOutdated: isOutdated, latestVersion: latestVersion, currentVersion: currentVersion)); 26 | Timer.periodic(const Duration(minutes: 5), (_) => _load); 27 | } 28 | 29 | Future checkForUpdates() async { 30 | var isOutdated = await FrostyService.isOutdated(); 31 | var currentVersion = await FrostyService.getFrostyVersion(); 32 | var latestVersion = (await PathHelper.getFrostyVersions()).first; 33 | emit(state.copyWith(isOutdated: isOutdated, latestVersion: latestVersion, currentVersion: currentVersion)); 34 | return state.isOutdated; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/logic/status_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:kyber_mod_manager/main.dart'; 3 | 4 | class StatusCubit extends Cubit { 5 | StatusCubit() : super(ApplicationStatus(initialized: false)) { 6 | emit(ApplicationStatus(initialized: box.containsKey('setup'))); 7 | } 8 | 9 | void setInitialized(bool initialized) { 10 | emit(ApplicationStatus(initialized: initialized)); 11 | } 12 | } 13 | 14 | class ApplicationStatus { 15 | final bool initialized; 16 | 17 | ApplicationStatus({required this.initialized}); 18 | } 19 | -------------------------------------------------------------------------------- /lib/logic/widget_cubic.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | class WidgetCubit extends Cubit { 5 | WidgetCubit() : super({-1: Container()}); 6 | 7 | void toIndex(int index) { 8 | emit(index); 9 | } 10 | 11 | void navigate(int index, Widget child) { 12 | emit({index: child}); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/screens/cosmetic_mods/cosmetic_mods.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:kyber_mod_manager/constants/mod_categories.dart'; 4 | import 'package:kyber_mod_manager/main.dart'; 5 | import 'package:kyber_mod_manager/screens/mod_profiles/frosty_profile.dart'; 6 | import 'package:kyber_mod_manager/screens/mod_profiles/widgets/active_mods.dart'; 7 | import 'package:kyber_mod_manager/screens/mod_profiles/widgets/export_profile_dialog.dart'; 8 | import 'package:kyber_mod_manager/screens/mod_profiles/widgets/installed_mods.dart'; 9 | import 'package:kyber_mod_manager/utils/types/freezed/mod.dart'; 10 | import 'package:kyber_mod_manager/utils/types/freezed/mod_profile.dart'; 11 | 12 | class CosmeticMods extends StatefulWidget { 13 | const CosmeticMods({Key? key}) : super(key: key); 14 | 15 | @override 16 | _CosmeticModsState createState() => _CosmeticModsState(); 17 | } 18 | 19 | class _CosmeticModsState extends State { 20 | final String prefix = 'cosmetic_mods'; 21 | late List activeMods; 22 | 23 | @override 24 | void initState() { 25 | activeMods = List.from(box.get('cosmetics')); 26 | super.initState(); 27 | } 28 | 29 | @override 30 | void dispose() { 31 | save(); 32 | super.dispose(); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return ScaffoldPage( 38 | header: PageHeader( 39 | title: Text(translate('$prefix.title')), 40 | commandBar: CommandBar( 41 | mainAxisAlignment: MainAxisAlignment.end, 42 | primaryItems: [ 43 | if (activeMods.isNotEmpty) 44 | CommandBarButton( 45 | label: Text(translate("export_profile_dialog.export")), 46 | icon: const Icon(FluentIcons.share), 47 | onPressed: () => showDialog( 48 | context: context, 49 | builder: (_) => ExportProfileDialog( 50 | profile: ModProfile( 51 | name: 'Cosmetics', 52 | mods: activeMods, 53 | ), 54 | enableCosmetics: false, 55 | ), 56 | ), 57 | ), 58 | CommandBarButton( 59 | icon: const Icon(FluentIcons.download), 60 | label: Text(translate('edit_mod_profile.load_frosty_profile.title')), 61 | onPressed: () => showDialog( 62 | context: context, 63 | builder: (c) => FrostyProfileSelector(onSelected: (s) { 64 | setState(() => activeMods = s.where((element) => !kyber_mod_categories.contains(element.category)).toList()); 65 | save(); 66 | }), 67 | ), 68 | ), 69 | ], 70 | ), 71 | ), 72 | content: SingleChildScrollView( 73 | child: Container( 74 | padding: const EdgeInsets.symmetric(horizontal: 20), 75 | height: MediaQuery.of(context).size.height - 79, 76 | child: Row( 77 | children: [ 78 | Flexible( 79 | flex: 6, 80 | child: InstalledMods( 81 | activeMods: activeMods, 82 | excludedCategories: kyber_mod_categories, 83 | onAdd: (m) { 84 | setState(() => activeMods.add(m)); 85 | save(); 86 | }, 87 | ), 88 | ), 89 | Flexible( 90 | flex: 6, 91 | child: ActiveMods( 92 | mods: activeMods, 93 | onRemove: (m) => setState(() => activeMods.remove(m)), 94 | onReorder: (int oldIndex, int newIndex) { 95 | setState(() { 96 | if (newIndex > oldIndex) { 97 | newIndex -= 1; 98 | } 99 | final Mod movedMod = activeMods.removeAt(oldIndex); 100 | activeMods.insert(newIndex, movedMod); 101 | }); 102 | save(); 103 | }, 104 | ), 105 | ), 106 | ], 107 | ), 108 | ), 109 | ), 110 | ); 111 | } 112 | 113 | void save() => box.put('cosmetics', activeMods); 114 | } 115 | -------------------------------------------------------------------------------- /lib/screens/dialogs/battlefront_options_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:kyber_mod_manager/main.dart'; 3 | import 'package:kyber_mod_manager/utils/battlefront_options.dart'; 4 | 5 | class BattlefrontOptionsDialog extends StatefulWidget { 6 | const BattlefrontOptionsDialog({Key? key}) : super(key: key); 7 | 8 | @override 9 | State createState() => _BattlefrontOptionsDialogState(); 10 | } 11 | 12 | class _BattlefrontOptionsDialogState extends State { 13 | BattlefrontProfileOptions? options; 14 | 15 | @override 16 | void initState() { 17 | BattlefrontOptions.getOptions().then((value) => setState(() => options = value)); 18 | super.initState(); 19 | } 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return ContentDialog( 24 | constraints: const BoxConstraints(maxWidth: 700, maxHeight: 400), 25 | title: const Text("Battlefront Settings"), 26 | actions: [ 27 | Button( 28 | onPressed: () { 29 | box.put('skipOptionsCheck', true); 30 | Navigator.of(context).pop(); 31 | }, 32 | child: const Text('Skip'), 33 | ), 34 | FilledButton( 35 | onPressed: () async { 36 | await BattlefrontOptions.setConfig(); 37 | if (!mounted) return; 38 | Navigator.of(context).pop(); 39 | }, 40 | child: const Text('BF2 Settings'), 41 | ), 42 | ], 43 | content: Center( 44 | child: Column( 45 | children: [ 46 | const Text('text'), 47 | const SizedBox( 48 | height: 20, 49 | ), 50 | if (options?.enableDx12 ?? false) 51 | const Text( 52 | 'dx12', 53 | ), 54 | if (options?.fullscreenEnabled ?? false) 55 | const Text( 56 | 'fullscreen', 57 | style: TextStyle(fontSize: 14), 58 | ), 59 | ], 60 | ), 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/screens/dialogs/installed_mod_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:fluent_ui/fluent_ui.dart'; 4 | import 'package:flutter_translate/flutter_translate.dart'; 5 | 6 | class InstalledModDialog extends StatefulWidget { 7 | const InstalledModDialog({Key? key, required this.mod}) : super(key: key); 8 | 9 | final dynamic mod; 10 | 11 | @override 12 | State createState() => _InstalledModDialogState(); 13 | } 14 | 15 | class _InstalledModDialogState extends State { 16 | Uint8List? image; 17 | 18 | @override 19 | void initState() { 20 | //ModService.getModCover(widget.mod.filename).then((value) => setState(() => image = value)); 21 | super.initState(); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return ContentDialog( 27 | title: Text(widget.mod.name), 28 | constraints: const BoxConstraints(maxWidth: 700), 29 | content: ListTile( 30 | leading: SizedBox( 31 | height: 60, 32 | width: 60, 33 | child: image != null && image!.isNotEmpty ? Image.memory(image!) : const Icon(FluentIcons.blocked, size: 50), 34 | ), 35 | title: Text(widget.mod.name), 36 | subtitle: Text(widget.mod.description ?? ''), 37 | ), 38 | actions: [ 39 | Button( 40 | onPressed: () => Navigator.of(context).pop(), 41 | child: Text(translate('close')), 42 | ), 43 | ], 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/screens/dialogs/join_server_dialog/widgets/password_input.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | 4 | class PasswordInput extends StatefulWidget { 5 | const PasswordInput({Key? key, required this.onChanged, required this.checkPassword, required this.focusNode}) : super(key: key); 6 | 7 | final Function(String) onChanged; 8 | final Function(String) checkPassword; 9 | final FocusNode focusNode; 10 | 11 | @override 12 | _PasswordInputState createState() => _PasswordInputState(); 13 | } 14 | 15 | class _PasswordInputState extends State { 16 | final TextEditingController controller = TextEditingController(); 17 | 18 | @override 19 | void initState() { 20 | controller.addListener(() => widget.onChanged(controller.text)); 21 | super.initState(); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return InfoLabel( 27 | label: translate("enter_password"), 28 | child: TextBox( 29 | controller: controller, 30 | autofocus: true, 31 | placeholder: translate('password'), 32 | focusNode: widget.focusNode, 33 | onSubmitted: widget.checkPassword, 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/screens/dialogs/join_server_dialog/widgets/required_mods.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:kyber_mod_manager/utils/services/mod_service.dart'; 4 | import 'package:kyber_mod_manager/utils/types/freezed/kyber_server.dart'; 5 | 6 | class RequiredMods extends StatelessWidget { 7 | const RequiredMods({Key? key, required this.server}) : super(key: key); 8 | 9 | final KyberServer server; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | if (server.mods.isEmpty) { 14 | return Container( 15 | alignment: Alignment.topCenter, 16 | child: Text(translate('server_browser.join_dialog.required_mods.none')), 17 | ); 18 | } 19 | 20 | return SizedBox( 21 | height: 200, 22 | child: SingleChildScrollView( 23 | child: Column( 24 | children: server.mods.map((mod) { 25 | final installed = ModService.isInstalled(mod.name); 26 | return Container( 27 | margin: const EdgeInsets.symmetric(vertical: 2), 28 | child: Row( 29 | mainAxisAlignment: MainAxisAlignment.start, 30 | children: [ 31 | Tooltip( 32 | message: installed ? translate('installed') : translate('not_installed'), 33 | child: Icon( 34 | installed ? FluentIcons.check_mark : FluentIcons.error_badge, 35 | color: installed ? Colors.green : Colors.red, 36 | ), 37 | ), 38 | const SizedBox(width: 8), 39 | Expanded( 40 | child: Text(mod.name, style: const TextStyle(fontSize: 15), overflow: TextOverflow.ellipsis), 41 | ) 42 | ], 43 | ), 44 | ); 45 | }).toList(), 46 | ), 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/screens/dialogs/join_server_dialog/widgets/team_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:kyber_mod_manager/utils/types/freezed/kyber_server.dart'; 4 | 5 | const String _prefix = 'server_browser.join_dialog.team_selector'; 6 | 7 | class TeamSelector extends StatelessWidget { 8 | const TeamSelector({Key? key, required this.server, required this.value, required this.onChange}) : super(key: key); 9 | 10 | final String value; 11 | final KyberServer server; 12 | final Function(String? preferredTeam) onChange; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | if (server.autoBalanceTeams) { 17 | return Center( 18 | child: Text( 19 | translate('$_prefix.auto_balancing'), 20 | ), 21 | ); 22 | } 23 | 24 | return SizedBox( 25 | child: Column( 26 | children: [ 27 | Text(translate('$_prefix.select_team')), 28 | const SizedBox(height: 8), 29 | ComboBox( 30 | isExpanded: true, 31 | items: [ 32 | ComboBoxItem( 33 | value: '0', 34 | child: Text(translate('$_prefix.light_side')), 35 | ), 36 | ComboBoxItem( 37 | value: '1', 38 | child: Text(translate('$_prefix.dark_side')), 39 | ), 40 | ], 41 | value: value, 42 | onChanged: (value) => onChange(value), 43 | ) 44 | ], 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/screens/dialogs/kyber_release_channel_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:fluent_ui/fluent_ui.dart'; 3 | import 'package:flutter_translate/flutter_translate.dart'; 4 | import 'package:kyber_mod_manager/main.dart'; 5 | import 'package:kyber_mod_manager/utils/dll_injector.dart'; 6 | import 'package:kyber_mod_manager/utils/services/notification_service.dart'; 7 | 8 | class KyberReleaseChannelDialog extends StatefulWidget { 9 | const KyberReleaseChannelDialog({Key? key}) : super(key: key); 10 | 11 | @override 12 | State createState() => _KyberReleaseChannelDialogState(); 13 | } 14 | 15 | class _KyberReleaseChannelDialogState extends State { 16 | bool _loading = false; 17 | final String prefix = "settings.kyber_release_channel"; 18 | late TextEditingController controller; 19 | 20 | @override 21 | void initState() { 22 | controller = TextEditingController( 23 | text: box.get('releaseChannel', defaultValue: 'stable'), 24 | ); 25 | super.initState(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return ContentDialog( 31 | constraints: const BoxConstraints(maxWidth: 700, maxHeight: 400), 32 | title: Text(translate("$prefix.title")), 33 | actions: [ 34 | Button( 35 | onPressed: () { 36 | Navigator.of(context).pop(); 37 | }, 38 | child: Text(translate("cancel")), 39 | ), 40 | FilledButton( 41 | onPressed: _loading 42 | ? null 43 | : () async { 44 | if (controller.text.isEmpty) { 45 | NotificationService.showNotification( 46 | message: translate("$prefix.error.no_channel_selected"), 47 | severity: InfoBarSeverity.error, 48 | ); 49 | return; 50 | } 51 | try { 52 | setState(() => _loading = true); 53 | await box.put('releaseChannel', controller.text); 54 | await DllInjector.downloadDll(); 55 | if (!mounted) return; 56 | Navigator.of(context).pop(); 57 | } catch (e) { 58 | await box.put('releaseChannel', 'stable'); 59 | if (e is DioError) { 60 | NotificationService.showNotification( 61 | message: translate("$prefix.errors.channel_not_found"), 62 | severity: InfoBarSeverity.error, 63 | ); 64 | } else { 65 | NotificationService.showNotification( 66 | message: translate("$prefix.errors.failed_to_download"), 67 | severity: InfoBarSeverity.error, 68 | ); 69 | } 70 | await DllInjector.downloadDll(); 71 | Navigator.of(context).pop(); 72 | } 73 | }, 74 | child: Text(translate("save")), 75 | ), 76 | ], 77 | content: Center( 78 | child: Column( 79 | children: [ 80 | TextBox( 81 | controller: controller, 82 | placeholder: 'Release Channel', 83 | ), 84 | Expanded( 85 | child: _loading 86 | ? Container( 87 | alignment: Alignment.center, 88 | child: Row( 89 | mainAxisAlignment: MainAxisAlignment.center, 90 | children: [ 91 | Container( 92 | width: 25, 93 | height: 25, 94 | child: const ProgressRing(), 95 | ), 96 | SizedBox(width: 20), 97 | Text( 98 | translate("server_browser.join_dialog.buttons.downloading"), 99 | style: TextStyle(fontSize: 17), 100 | ), 101 | ], 102 | ), 103 | ) 104 | : Container(), 105 | ), 106 | ], 107 | ), 108 | ), 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/screens/dialogs/update_dialog/update_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_markdown/flutter_markdown.dart'; 3 | import 'package:flutter_translate/flutter_translate.dart'; 4 | import 'package:kyber_mod_manager/utils/auto_updater.dart'; 5 | import 'package:url_launcher/url_launcher_string.dart'; 6 | 7 | class UpdateDialog extends StatefulWidget { 8 | const UpdateDialog({Key? key, required this.versionInfo}) : super(key: key); 9 | 10 | final VersionInfo versionInfo; 11 | 12 | @override 13 | State createState() => _UpdateDialogState(); 14 | } 15 | 16 | class _UpdateDialogState extends State { 17 | bool installing = false; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return ContentDialog( 22 | title: const Text('Update available!'), 23 | constraints: const BoxConstraints( 24 | maxWidth: 800, 25 | ), 26 | actions: [ 27 | Button( 28 | onPressed: installing ? null : () => Navigator.pop(context), 29 | child: Text(translate('ignore')), 30 | ), 31 | FilledButton( 32 | onPressed: !installing 33 | ? () { 34 | setState(() => installing = true); 35 | AutoUpdater().update(); 36 | } 37 | : null, 38 | child: Text(translate('install')), 39 | ), 40 | ], 41 | content: SizedBox( 42 | height: 400, 43 | width: 700, 44 | child: SingleChildScrollView( 45 | child: Column( 46 | crossAxisAlignment: CrossAxisAlignment.start, 47 | children: [ 48 | Text( 49 | 'Version ${widget.versionInfo.version} is available.', 50 | style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), 51 | ), 52 | const Text( 53 | 'Changelog:', 54 | style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), 55 | ), 56 | const SizedBox(height: 16), 57 | MarkdownBody( 58 | data: widget.versionInfo.body, 59 | onTapLink: (String text, String? href, String title) { 60 | if (href == null) { 61 | return; 62 | } 63 | 64 | launchUrlString(href); 65 | }, 66 | ), 67 | ], 68 | ), 69 | ), 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/screens/dialogs/walk_through/widgets/frosty_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | 4 | class FrostySelector extends StatelessWidget { 5 | const FrostySelector({Key? key, required this.supportedVersions}) : super(key: key); 6 | 7 | final String prefix = 'walk_through.select_frosty_path'; 8 | final List supportedVersions; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Column( 13 | crossAxisAlignment: CrossAxisAlignment.center, 14 | children: [ 15 | Text( 16 | translate('$prefix.description'), 17 | style: const TextStyle( 18 | fontSize: 16, 19 | ), 20 | ), 21 | const SizedBox(height: 25), 22 | Text(translate('$prefix.notice'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), 23 | Text( 24 | translate('$prefix.notice_text'), 25 | style: const TextStyle( 26 | fontSize: 16, 27 | ), 28 | ), 29 | const SizedBox(height: 25), 30 | Text(translate('$prefix.important'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), 31 | Text( 32 | translate('$prefix.important_text'), 33 | style: const TextStyle( 34 | fontSize: 16, 35 | ), 36 | ), 37 | // const SizedBox(height: 15), 38 | // Text( 39 | // translate('$prefix.supported_versions'), 40 | // style: const TextStyle( 41 | // fontSize: 16, 42 | // fontWeight: FontWeight.bold, 43 | // ), 44 | // ), 45 | // UnorderedList( 46 | // supportedVersions, 47 | // mainAxisAlignment: MainAxisAlignment.center, 48 | // crossAxisAlignment: CrossAxisAlignment.center, 49 | // ) 50 | ], 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/screens/dialogs/walk_through/widgets/installed_mods.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:kyber_mod_manager/utils/services/mod_service.dart'; 3 | import 'package:kyber_mod_manager/utils/types/freezed/mod.dart'; 4 | 5 | class InstalledMods extends StatefulWidget { 6 | const InstalledMods({Key? key, required this.onLoaded}) : super(key: key); 7 | 8 | final VoidCallback onLoaded; 9 | 10 | @override 11 | _InstalledModsState createState() => _InstalledModsState(); 12 | } 13 | 14 | class _InstalledModsState extends State { 15 | bool loaded = false; 16 | 17 | @override 18 | void initState() { 19 | ModService.loadMods().then((value) { 20 | setState(() => loaded = true); 21 | widget.onLoaded(); 22 | }); 23 | super.initState(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | if (loaded) { 29 | return ListView.builder( 30 | shrinkWrap: true, 31 | itemCount: ModService.mods.length, 32 | itemBuilder: (context, index) { 33 | final Mod mod = ModService.mods[index]; 34 | 35 | return ListTile( 36 | title: Text(mod.name), 37 | subtitle: Text(' - ${mod.version}'), 38 | ); 39 | }, 40 | ); 41 | } 42 | 43 | return const Center( 44 | child: ProgressRing(), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/screens/errors/battlefront_not_installed.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:file_selector/file_selector.dart'; 4 | import 'package:fluent_ui/fluent_ui.dart'; 5 | import 'package:kyber_mod_manager/main.dart'; 6 | import 'package:kyber_mod_manager/utils/helpers/origin_helper.dart'; 7 | import 'package:kyber_mod_manager/utils/services/notification_service.dart'; 8 | 9 | class BattlefrontNotFound extends StatefulWidget { 10 | const BattlefrontNotFound({Key? key}) : super(key: key); 11 | 12 | @override 13 | State createState() => _BattlefrontNotFoundState(); 14 | } 15 | 16 | class _BattlefrontNotFoundState extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | return Center( 20 | child: Column( 21 | mainAxisAlignment: MainAxisAlignment.center, 22 | crossAxisAlignment: CrossAxisAlignment.center, 23 | children: [ 24 | const Text('Please install Star Wars: Battlefront II to continue.'), 25 | const SizedBox( 26 | height: 16, 27 | ), 28 | Row( 29 | children: [ 30 | FilledButton( 31 | child: const Text('Select Battlefront 2 path manually.'), 32 | onPressed: () async { 33 | String? dir = await getDirectoryPath(); 34 | if (dir == null) { 35 | return; 36 | } 37 | Directory path = Directory(dir); 38 | if (path.listSync().whereType().where((element) => element.path.endsWith('starwarsbattlefrontii.exe')).isEmpty) { 39 | NotificationService.showNotification(message: 'Could not find Battlefront 2 executable.', severity: InfoBarSeverity.error); 40 | return; 41 | } 42 | 43 | await box.put('battlefrontPath', dir); 44 | if (!mounted) return; 45 | Navigator.of(context).pop(); 46 | }, 47 | ), 48 | const SizedBox( 49 | width: 15, 50 | ), 51 | Button( 52 | child: const Text('Try again'), 53 | onPressed: () { 54 | var path = OriginHelper.getBattlefrontPath(); 55 | if (path.isEmpty) { 56 | NotificationService.showNotification(message: 'No executable found.', severity: InfoBarSeverity.error); 57 | return; 58 | } 59 | 60 | Navigator.of(context).pop(); 61 | }, 62 | ), 63 | ], 64 | ) 65 | ], 66 | ), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/screens/errors/chromium_not_found.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | 3 | class ChromiumNotFound extends StatelessWidget { 4 | const ChromiumNotFound({Key? key}) : super(key: key); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return const Center( 9 | child: Text('Chromium was not found. To fix this, you need to reinstall Kyber Mod Manager.'), 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/screens/errors/missing_permissions.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | 3 | class MissingPermissions extends StatefulWidget { 4 | const MissingPermissions({Key? key}) : super(key: key); 5 | 6 | @override 7 | State createState() => _MissingPermissionsState(); 8 | } 9 | 10 | class _MissingPermissionsState extends State { 11 | @override 12 | Widget build(BuildContext context) { 13 | return const Center( 14 | child: Text('Missing permissions. Please start Kyber Mod Manager as admin.'), 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/screens/errors/no_executable.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:kyber_mod_manager/utils/helpers/unzip_helper.dart'; 3 | import 'package:kyber_mod_manager/utils/services/notification_service.dart'; 4 | 5 | class NoExecutable extends StatefulWidget { 6 | const NoExecutable({Key? key}) : super(key: key); 7 | 8 | @override 9 | State createState() => _NoExecutableState(); 10 | } 11 | 12 | class _NoExecutableState extends State { 13 | @override 14 | Widget build(BuildContext context) { 15 | return Center( 16 | child: Column( 17 | mainAxisAlignment: MainAxisAlignment.center, 18 | crossAxisAlignment: CrossAxisAlignment.center, 19 | children: [ 20 | const Text('Please install WinRAR or 7-Zip to continue.'), 21 | const SizedBox( 22 | height: 16, 23 | ), 24 | FilledButton( 25 | child: const Text('Try again'), 26 | onPressed: () { 27 | var executable = UnzipHelper.getExecutable(); 28 | if (executable == null) { 29 | NotificationService.showNotification(message: 'No executable found.', severity: InfoBarSeverity.error); 30 | return; 31 | } 32 | 33 | Navigator.of(context).pop(); 34 | }, 35 | ) 36 | ], 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/screens/map_rotation_creator/map_rotation_creator.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:kyber_mod_manager/constants/modes.dart'; 3 | import 'package:kyber_mod_manager/screens/map_rotation_creator/map_rotation_export_dialog.dart'; 4 | import 'package:kyber_mod_manager/screens/map_rotation_creator/widgets/map_rotation_active_map.dart'; 5 | import 'package:kyber_mod_manager/screens/map_rotation_creator/widgets/map_rotation_mode_maps.dart'; 6 | 7 | class MapRotationCreator extends StatefulWidget { 8 | const MapRotationCreator({Key? key}) : super(key: key); 9 | 10 | @override 11 | State createState() => _MapRotationCreatorState(); 12 | } 13 | 14 | class _MapRotationCreatorState extends State { 15 | List selectedMaps = []; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return ScaffoldPage( 20 | header: PageHeader( 21 | title: const Text("Map Rotation Creator"), 22 | commandBar: CommandBar( 23 | mainAxisAlignment: MainAxisAlignment.end, 24 | primaryItems: [ 25 | CommandBarButton( 26 | icon: const Icon(FluentIcons.save), 27 | onPressed: () { 28 | showDialog(context: context, builder: (_) => MapRotationExportDialog(selectedMaps: selectedMaps)); 29 | }, 30 | ), 31 | ], 32 | ), 33 | ), 34 | content: Container( 35 | padding: const EdgeInsets.symmetric(horizontal: 16), 36 | child: SingleChildScrollView( 37 | child: SizedBox( 38 | height: MediaQuery.of(context).size.height - 79, 39 | child: Row( 40 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 41 | crossAxisAlignment: CrossAxisAlignment.start, 42 | children: [ 43 | buildModesList(), 44 | buildActiveList(), 45 | ], 46 | ), 47 | ), 48 | ), 49 | ), 50 | ); 51 | } 52 | 53 | Widget buildModesList() { 54 | return Expanded( 55 | flex: 7, 56 | child: Column( 57 | children: [ 58 | InfoLabel(label: "Available Maps"), 59 | Expanded( 60 | child: ListView.builder( 61 | itemBuilder: (context, index) { 62 | return MapRotationModeMaps( 63 | mode: modes[index], 64 | onAdd: (map) { 65 | selectedMaps.add(MapRotationMap(map: map, mode: modes[index].mode)); 66 | setState(() => null); 67 | }, 68 | onAddAll: () { 69 | selectedMaps.addAll(modes[index].maps.map((e) => MapRotationMap(map: e, mode: modes[index].mode))); 70 | setState(() => null); 71 | }, 72 | ); 73 | }, 74 | shrinkWrap: true, 75 | itemCount: modes.length, 76 | ), 77 | ), 78 | ], 79 | ), 80 | ); 81 | } 82 | 83 | Widget buildActiveList() { 84 | return Expanded( 85 | flex: 5, 86 | child: Column( 87 | children: [ 88 | InfoLabel(label: "Active Maps"), 89 | Expanded( 90 | child: ReorderableListView( 91 | onReorder: (oldIndex, newIndex) { 92 | setState(() { 93 | if (newIndex > oldIndex) { 94 | newIndex -= 1; 95 | } 96 | 97 | final dynamic map = selectedMaps.removeAt(oldIndex); 98 | selectedMaps = selectedMaps..insert(newIndex, map); 99 | }); 100 | }, 101 | buildDefaultDragHandles: false, 102 | shrinkWrap: true, 103 | children: [ 104 | for (final e in selectedMaps) 105 | MapRotationActiveMap( 106 | map: e, 107 | key: Key(e.hashCode.toString()), 108 | index: selectedMaps.indexOf(e), 109 | onDelete: () => setState(() => selectedMaps.remove(e)), 110 | ), 111 | ], 112 | ), 113 | ), 114 | ], 115 | ), 116 | ); 117 | } 118 | } 119 | 120 | class MapRotationMap { 121 | final String map; 122 | final String mode; 123 | 124 | MapRotationMap({required this.map, required this.mode}); 125 | } 126 | -------------------------------------------------------------------------------- /lib/screens/map_rotation_creator/map_rotation_export_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:file_selector/file_selector.dart'; 4 | import 'package:fluent_ui/fluent_ui.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:flutter_translate/flutter_translate.dart'; 7 | import 'package:kyber_mod_manager/screens/map_rotation_creator/map_rotation_creator.dart'; 8 | 9 | class MapRotationExportDialog extends StatefulWidget { 10 | const MapRotationExportDialog({Key? key, required this.selectedMaps}) : super(key: key); 11 | 12 | final List selectedMaps; 13 | 14 | @override 15 | State createState() => _MapRotationExportDialogState(); 16 | } 17 | 18 | class _MapRotationExportDialogState extends State { 19 | bool shuffle = false; 20 | 21 | String generateMapRotationString() { 22 | String mapRotationString = ""; 23 | 24 | var localList = widget.selectedMaps; 25 | if (shuffle) { 26 | localList.shuffle(); 27 | } 28 | 29 | for (MapRotationMap map in widget.selectedMaps) { 30 | mapRotationString += "${map.map} ${map.mode}\n"; 31 | } 32 | 33 | return mapRotationString; 34 | } 35 | 36 | void export() async { 37 | String mapRotationString = generateMapRotationString(); 38 | final destination = await getSaveLocation( 39 | suggestedName: "map_rotation.txt", 40 | acceptedTypeGroups: [ 41 | const XTypeGroup( 42 | label: 'Text', 43 | extensions: ['txt'], 44 | ), 45 | ], 46 | ); 47 | await File(destination!.path).writeAsString(mapRotationString); 48 | Navigator.of(context).pop(); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return ContentDialog( 54 | title: const Text("Export Map Rotation"), 55 | actions: [ 56 | Button( 57 | child: Text(translate('close')), 58 | onPressed: () { 59 | Navigator.of(context).pop(); 60 | }, 61 | ), 62 | Button( 63 | child: const Text("Copy to Clipboard"), 64 | onPressed: () { 65 | Clipboard.setData(ClipboardData(text: generateMapRotationString())); 66 | Navigator.of(context).pop(); 67 | }, 68 | ), 69 | FilledButton(onPressed: export, child: Text(translate("Export"))), 70 | ], 71 | constraints: const BoxConstraints(maxWidth: 500, maxHeight: 325), 72 | content: Column( 73 | crossAxisAlignment: CrossAxisAlignment.start, 74 | children: [ 75 | Checkbox( 76 | checked: shuffle, 77 | onChanged: (value) => setState(() => shuffle = value ?? false), 78 | content: const Text("Shuffle"), 79 | ), 80 | ], 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/screens/map_rotation_creator/widgets/map_rotation_active_map.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:fluent_ui/fluent_ui.dart'; 4 | import 'package:kyber_mod_manager/constants/modes.dart'; 5 | import 'package:kyber_mod_manager/main.dart'; 6 | import 'package:kyber_mod_manager/screens/map_rotation_creator/map_rotation_creator.dart'; 7 | import 'package:kyber_mod_manager/utils/helpers/map_helper.dart'; 8 | import 'package:kyber_mod_manager/utils/types/mode.dart'; 9 | 10 | class MapRotationActiveMap extends StatefulWidget { 11 | const MapRotationActiveMap({Key? key, required this.map, required this.index, required this.onDelete}) : super(key: key); 12 | 13 | final MapRotationMap map; 14 | final int index; 15 | final void Function() onDelete; 16 | 17 | @override 18 | State createState() => _MapRotationActiveMapState(); 19 | } 20 | 21 | class _MapRotationActiveMapState extends State { 22 | late String mapName; 23 | late Mode mode; 24 | 25 | @override 26 | void initState() { 27 | mapName = MapHelper.getMapName(modes.firstWhere((element) => element.mode == widget.map.mode), widget.map.map); 28 | mode = modes.firstWhere((element) => element.mode == widget.map.mode); 29 | super.initState(); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return ReorderableDragStartListener( 35 | index: widget.index, 36 | key: Key(widget.map.hashCode.toString()), 37 | child: Card( 38 | padding: EdgeInsets.zero, 39 | child: Stack( 40 | children: [ 41 | SizedBox( 42 | height: 53, 43 | width: MediaQuery.of(context).size.width * 0.2, 44 | child: ShaderMask( 45 | shaderCallback: (rect) { 46 | return LinearGradient( 47 | begin: Alignment.centerLeft, 48 | end: Alignment.centerRight, 49 | colors: [Colors.black.withOpacity(.3), Colors.transparent], 50 | ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); 51 | }, 52 | blendMode: BlendMode.dstIn, 53 | child: Image.file( 54 | File("$applicationDocumentsDirectory/maps/${(widget.map.map).replaceAll("/", "-")}.jpg"), 55 | fit: BoxFit.fitWidth, 56 | alignment: Alignment.centerLeft, 57 | ), 58 | ), 59 | ), 60 | ListTile( 61 | leading: FluentTheme( 62 | data: FluentTheme.of(context), 63 | child: IconButton(icon: const Icon(FluentIcons.delete), onPressed: widget.onDelete), 64 | ), 65 | title: Text( 66 | mapName, 67 | overflow: TextOverflow.ellipsis, 68 | maxLines: 1, 69 | ), 70 | subtitle: Text( 71 | mode.name, 72 | ), 73 | trailing: ReorderableDragStartListener( 74 | index: widget.index, 75 | key: Key(widget.map.hashCode.toString()), 76 | child: FluentTheme( 77 | data: FluentTheme.of(context), 78 | child: IconButton( 79 | icon: const Icon(FluentIcons.drag_object), 80 | onPressed: () => null, 81 | ), 82 | ), 83 | ), 84 | ), 85 | ], 86 | ), 87 | ), 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/screens/map_rotation_creator/widgets/map_rotation_mode_maps.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:kyber_mod_manager/utils/helpers/map_helper.dart'; 3 | import 'package:kyber_mod_manager/utils/types/mode.dart'; 4 | 5 | class MapRotationModeMaps extends StatefulWidget { 6 | const MapRotationModeMaps({Key? key, required this.mode, required this.onAdd, required this.onAddAll}) : super(key: key); 7 | 8 | final void Function(String map) onAdd; 9 | final void Function() onAddAll; 10 | final Mode mode; 11 | 12 | @override 13 | State createState() => _MapRotationModeMapsState(); 14 | } 15 | 16 | class _MapRotationModeMapsState extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | return Expander( 20 | header: Text(widget.mode.name), 21 | trailing: IconButton( 22 | icon: const Icon(FluentIcons.add), 23 | onPressed: widget.onAddAll, 24 | ), 25 | content: ListView.builder( 26 | shrinkWrap: true, 27 | padding: EdgeInsets.zero, 28 | physics: const NeverScrollableScrollPhysics(), 29 | itemBuilder: (context, index) { 30 | return _ModeMap(mode: widget.mode, map: widget.mode.maps[index], onAdd: widget.onAdd); 31 | }, 32 | itemCount: widget.mode.maps.length, 33 | ), 34 | ); 35 | } 36 | } 37 | 38 | class _ModeMap extends StatefulWidget { 39 | const _ModeMap({Key? key, required this.mode, required this.map, required this.onAdd}) : super(key: key); 40 | 41 | final void Function(String map) onAdd; 42 | final Mode mode; 43 | final String map; 44 | 45 | @override 46 | State<_ModeMap> createState() => _ModeMapState(); 47 | } 48 | 49 | class _ModeMapState extends State<_ModeMap> { 50 | late String mapName; 51 | 52 | @override 53 | void initState() { 54 | mapName = MapHelper.getMapName(widget.mode, widget.map); 55 | super.initState(); 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return Padding( 61 | padding: const EdgeInsets.only(bottom: 8), 62 | child: Row( 63 | children: [ 64 | IconButton( 65 | icon: const Icon(FluentIcons.add, size: 12), 66 | onPressed: () => widget.onAdd(widget.map), 67 | ), 68 | const SizedBox(width: 8), 69 | Text(mapName, style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14)), 70 | ], 71 | ), 72 | ); 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /lib/screens/mod_browser.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:fluent_ui/fluent_ui.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:kyber_mod_manager/main.dart'; 7 | import 'package:kyber_mod_manager/utils/helpers/puppeteer_helper.dart'; 8 | import 'package:kyber_mod_manager/utils/services/download_service.dart'; 9 | import 'package:kyber_mod_manager/utils/services/mod_service.dart'; 10 | import 'package:logging/logging.dart'; 11 | import 'package:path/path.dart'; 12 | import 'package:puppeteer/puppeteer.dart' hide Key; 13 | 14 | class ModBrowser extends StatefulWidget { 15 | const ModBrowser({Key? key}) : super(key: key); 16 | 17 | @override 18 | State createState() => _ModBrowserState(); 19 | } 20 | 21 | class _ModBrowserState extends State { 22 | Browser? browser; 23 | StreamSubscription? fileStream; 24 | bool browserOpen = false; 25 | bool disabled = false; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | } 31 | 32 | @override 33 | void dispose() { 34 | closeBrowser(true); 35 | super.dispose(); 36 | } 37 | 38 | void closeBrowser([dispose = false]) async { 39 | if (!mounted) return; 40 | browser?.close(); 41 | fileStream?.cancel(); 42 | if (dispose) return; 43 | setState(() { 44 | browserOpen = false; 45 | disabled = false; 46 | }); 47 | } 48 | 49 | void openBrowser() async { 50 | setState(() => disabled = true); 51 | browser = await PuppeteerHelper.startBrowser(headless: false, asApp: false, onClose: () => closeBrowser()); 52 | var page = (await browser!.pages).first; 53 | // await page.goto('https://www.nexusmods.com/starwarsbattlefront22017/', wait: Until.networkIdle); 54 | await PuppeteerHelper.initializePage(page); 55 | 56 | final String path = '${box.get('frostyPath')}\\mods\\starwarsbattlefrontii\\'; 57 | fileStream = Directory(path).watch().listen((event) async { 58 | if (event.type != 2 || (!event.path.endsWith('.zip') && !event.path.endsWith('.rar'))) { 59 | return; 60 | } 61 | 62 | Logger.root.info('Installing ${basename(event.path)}'); 63 | await compute(DownloadService.unpackFile, [path, basename(event.path)]); 64 | Logger.root.info('Installed'); 65 | ModService.loadMods(); 66 | }); 67 | 68 | setState(() { 69 | browserOpen = true; 70 | disabled = false; 71 | }); 72 | } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return ScaffoldPage( 77 | header: const PageHeader( 78 | title: Text('Mod Browser'), 79 | ), 80 | content: Container( 81 | alignment: Alignment.center, 82 | padding: const EdgeInsets.all(16), 83 | child: browserOpen 84 | ? FilledButton( 85 | onPressed: disabled ? null : () => closeBrowser(), 86 | child: const Text('Close Browser'), 87 | ) 88 | : Column( 89 | mainAxisAlignment: MainAxisAlignment.center, 90 | children: [ 91 | const Text('This page opens a browser for Nexusmods.com. All mods you download will be installed automatically.'), 92 | const SizedBox( 93 | height: 20, 94 | ), 95 | FilledButton( 96 | onPressed: disabled ? null : () => openBrowser(), 97 | child: const Text('Open Browser'), 98 | ), 99 | ], 100 | ), 101 | ), 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/screens/mod_profiles/frosty_profile.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:kyber_mod_manager/utils/services/frosty_profile_service.dart'; 4 | import 'package:kyber_mod_manager/utils/types/freezed/frosty_profile.dart'; 5 | 6 | class FrostyProfileSelector extends StatefulWidget { 7 | const FrostyProfileSelector({Key? key, this.onSelected}) : super(key: key); 8 | 9 | final Function(List mods)? onSelected; 10 | 11 | @override 12 | _FrostyProfileSelectorState createState() => _FrostyProfileSelectorState(); 13 | } 14 | 15 | class _FrostyProfileSelectorState extends State { 16 | late List profiles; 17 | String? value; 18 | 19 | @override 20 | void initState() { 21 | profiles = FrostyProfileService.getProfilesWithMods(); 22 | value = profiles.first.name; 23 | 24 | super.initState(); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return ContentDialog( 30 | constraints: const BoxConstraints(maxWidth: 500), 31 | title: Text(translate('select_frosty_profile.title')), 32 | actions: [ 33 | Button( 34 | child: Text(translate('close')), 35 | onPressed: () => Navigator.of(context).pop(), 36 | ), 37 | FilledButton( 38 | child: Text(translate('load')), 39 | onPressed: () { 40 | if (widget.onSelected != null) { 41 | widget.onSelected!(profiles.firstWhere((p) => p.name == value).mods); 42 | } 43 | Navigator.of(context).pop(profiles.firstWhere((p) => p.name == value)); 44 | }, 45 | ), 46 | ], 47 | content: InfoLabel( 48 | label: translate('select_frosty_profile.label'), 49 | child: ComboBox( 50 | value: value, 51 | onChanged: (v) => setState(() => value = v), 52 | isExpanded: true, 53 | items: profiles.map((e) { 54 | return ComboBoxItem( 55 | value: e.name, 56 | child: Text(e.name), 57 | ); 58 | }).toList(), 59 | ), 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/screens/mod_profiles/mod_profiles.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:go_router/go_router.dart'; 4 | import 'package:kyber_mod_manager/main.dart'; 5 | import 'package:kyber_mod_manager/screens/mod_profiles/widgets/export_profile_dialog.dart'; 6 | import 'package:kyber_mod_manager/utils/types/freezed/mod_profile.dart'; 7 | import 'package:kyber_mod_manager/widgets/button_text.dart'; 8 | 9 | class ModProfiles extends StatefulWidget { 10 | const ModProfiles({Key? key}) : super(key: key); 11 | 12 | @override 13 | _ModProfilesState createState() => _ModProfilesState(); 14 | } 15 | 16 | class _ModProfilesState extends State { 17 | late List profiles; 18 | 19 | @override 20 | void initState() { 21 | profiles = List.from(box.get('profiles', defaultValue: []))..sort((a, b) => a.name.compareTo(b.name)); 22 | super.initState(); 23 | } 24 | 25 | deleteProfile(ModProfile profile) async { 26 | profiles.removeWhere((p) => p.name == profile.name); 27 | await box.put('profiles', profiles); 28 | if (box.get('lastProfile', defaultValue: '') == profile.name) { 29 | box.put('lastProfile', ''); 30 | } 31 | setState(() => null); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return ScaffoldPage( 37 | header: PageHeader( 38 | title: Text(translate('mod_profiles.title')), 39 | commandBar: CommandBar( 40 | mainAxisAlignment: MainAxisAlignment.end, 41 | primaryItems: [ 42 | CommandBarButton( 43 | icon: const Icon(FluentIcons.add), 44 | label: Text(translate('mod_profiles.create_profile')), 45 | onPressed: () async { 46 | Router.neglect(context, () { 47 | context.goNamed('profile'); 48 | }); 49 | }, 50 | ), 51 | ], 52 | ), 53 | ), 54 | content: Padding( 55 | padding: const EdgeInsets.symmetric(horizontal: 20), 56 | child: ListView( 57 | children: profiles 58 | .map( 59 | (e) => ListTile( 60 | title: Text( 61 | e.name, 62 | style: const TextStyle( 63 | fontSize: 16, 64 | fontWeight: FontWeight.bold, 65 | ), 66 | ), 67 | subtitle: Text( 68 | e.description != null && e.description!.isNotEmpty ? e.description! : '', 69 | style: const TextStyle( 70 | fontSize: 14, 71 | fontWeight: FontWeight.w300, 72 | ), 73 | ), 74 | trailing: Row( 75 | children: [ 76 | Button( 77 | child: ButtonText( 78 | text: Text(translate('edit')), 79 | icon: const Icon(FluentIcons.edit), 80 | ), 81 | onPressed: () { 82 | router.goNamed("profile", queryParameters: {"profile": e.name}); 83 | }, 84 | ), 85 | const SizedBox(width: 8), 86 | DropDownButton( 87 | title: const Icon(FluentIcons.more), 88 | closeAfterClick: true, 89 | leading: const SizedBox( 90 | height: 20, 91 | ), 92 | trailing: const SizedBox(), 93 | items: [ 94 | MenuFlyoutItem( 95 | text: Text("Export"), 96 | onPressed: () => showDialog(context: context, builder: (_) => ExportProfileDialog(profile: e)), 97 | ), 98 | MenuFlyoutItem( 99 | text: Text(translate('delete')), 100 | onPressed: () => deleteProfile(e), 101 | ), 102 | ], 103 | ), 104 | ], 105 | ), 106 | ), 107 | ) 108 | .toList(), 109 | ), 110 | ), 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/screens/mod_profiles/widgets/active_mods.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:fluent_ui/fluent_ui.dart'; 4 | import 'package:flutter_translate/flutter_translate.dart'; 5 | 6 | const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; 7 | final Random _rnd = Random(); 8 | 9 | class ActiveMods extends StatelessWidget { 10 | const ActiveMods({Key? key, required this.mods, required this.onReorder, required this.onRemove}) : super(key: key); 11 | 12 | final List mods; 13 | final Function(int oldIndex, int newIndex) onReorder; 14 | final Function(dynamic mod) onRemove; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | const textStyle = TextStyle(fontSize: 14); 19 | 20 | return Column( 21 | children: [ 22 | Text( 23 | translate('edit_mod_profile.active_mods'), 24 | style: TextStyle(color: Colors.white.withOpacity(.7)), 25 | ), 26 | const SizedBox(height: 8), 27 | Expanded( 28 | child: ReorderableListView( 29 | onReorder: (oldIndex, newIndex) => onReorder(oldIndex, newIndex), 30 | buildDefaultDragHandles: false, 31 | header: mods.isEmpty 32 | ? Container( 33 | key: const Key("empty"), 34 | child: Center( 35 | child: Text( 36 | translate('edit_mod_profile.forms.mods.no_mods_selected'), 37 | style: textStyle.copyWith(fontSize: 16, fontWeight: FontWeight.bold), 38 | ), 39 | ), 40 | ) 41 | : null, 42 | children: List.of(mods.map((e) { 43 | return ReorderableDragStartListener( 44 | index: mods.indexWhere((element) => element.filename == e.filename), 45 | key: Key(e.version == 'Unknown' ? getRandomString(10) : e.filename), 46 | child: Card( 47 | padding: EdgeInsets.zero, 48 | child: ListTile( 49 | leading: IconButton( 50 | icon: const Icon(FluentIcons.delete), 51 | onPressed: () => onRemove(e), 52 | ), 53 | title: Text( 54 | e.name, 55 | style: textStyle, 56 | overflow: TextOverflow.ellipsis, 57 | maxLines: 1, 58 | ), 59 | trailing: ReorderableDragStartListener( 60 | index: mods.indexOf(e), 61 | key: Key(e.filename), 62 | child: IconButton( 63 | icon: const Icon(FluentIcons.drag_object), 64 | onPressed: () => null, 65 | ), 66 | ), 67 | ), 68 | ), 69 | ); 70 | }).toList()), 71 | ), 72 | ) 73 | ], 74 | ); 75 | } 76 | 77 | String getRandomString(int length) => String.fromCharCodes(Iterable.generate(length, (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)))); 78 | } 79 | -------------------------------------------------------------------------------- /lib/screens/mod_profiles/widgets/export_profile_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:kyber_mod_manager/main.dart'; 4 | import 'package:kyber_mod_manager/utils/services/frosty_profile_service.dart'; 5 | import 'package:kyber_mod_manager/utils/types/freezed/mod_profile.dart'; 6 | 7 | class ExportProfileDialog extends StatefulWidget { 8 | const ExportProfileDialog({Key? key, required this.profile, this.enableCosmetics = true}) : super(key: key); 9 | 10 | final ModProfile profile; 11 | final bool enableCosmetics; 12 | 13 | @override 14 | _ExportProfileDialogState createState() => _ExportProfileDialogState(); 15 | } 16 | 17 | class _ExportProfileDialogState extends State { 18 | final TextEditingController _controller = TextEditingController(); 19 | final String prefix = "export_profile_dialog"; 20 | String selectedType = "Frosty Pack"; 21 | bool cosmetics = false; 22 | 23 | @override 24 | void initState() { 25 | _controller.text = widget.profile.name; 26 | super.initState(); 27 | } 28 | 29 | void export() { 30 | if (selectedType == "Frosty Pack") { 31 | List cosmeticMods = List.from(box.get('cosmetics')); 32 | FrostyProfileService.createProfile([ 33 | ...widget.profile.mods.map((e) => e.toKyberString()).toList(), 34 | if (cosmetics) ...cosmeticMods.map((e) => e.toKyberString()).toList(), 35 | ], _controller.text); 36 | Navigator.of(context).pop(); 37 | } else { 38 | // TODO: Export to json 39 | } 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return ContentDialog( 45 | title: Text(translate('$prefix.title')), 46 | actions: [ 47 | Button( 48 | child: Text(translate('close')), 49 | onPressed: () { 50 | Navigator.of(context).pop(); 51 | }, 52 | ), 53 | FilledButton(onPressed: export, child: Text(translate('$prefix.export'))), 54 | ], 55 | constraints: const BoxConstraints(maxWidth: 500, maxHeight: 325), 56 | content: Column( 57 | crossAxisAlignment: CrossAxisAlignment.start, 58 | children: [ 59 | InfoLabel( 60 | label: translate('$prefix.export_type'), 61 | child: ComboBox( 62 | value: 'Frosty Pack', 63 | isExpanded: true, 64 | onChanged: (String? e) => setState(() => selectedType = e ?? 'Frosty Pack'), 65 | items: [ 66 | 'Frosty Pack' /*, 'File'*/ 67 | ].map((e) => ComboBoxItem(value: e, child: Text(e))).toList(), 68 | ), 69 | ), 70 | const SizedBox(height: 15), 71 | InfoLabel( 72 | label: translate('$prefix.export_type'), 73 | child: TextBox( 74 | controller: _controller, 75 | ), 76 | ), 77 | if (widget.enableCosmetics) ...[ 78 | const SizedBox(height: 15), 79 | Checkbox( 80 | checked: cosmetics, 81 | onChanged: (value) => setState(() => cosmetics = value ?? false), 82 | content: Text(translate('$prefix.include_cosmetics')), 83 | ), 84 | ], 85 | ], 86 | ), 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/screens/mod_profiles/widgets/installed_mods.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:kyber_mod_manager/screens/mod_profiles/widgets/mod_category.dart'; 4 | import 'package:kyber_mod_manager/utils/services/mod_service.dart'; 5 | 6 | class InstalledMods extends StatefulWidget { 7 | const InstalledMods({Key? key, required this.activeMods, required this.onAdd, this.excludedCategories, this.kyber = false}) : super(key: key); 8 | 9 | final List activeMods; 10 | final bool kyber; 11 | final List? excludedCategories; 12 | final Function(dynamic) onAdd; 13 | 14 | @override 15 | _InstalledModsState createState() => _InstalledModsState(); 16 | } 17 | 18 | class _InstalledModsState extends State { 19 | String search = ''; 20 | 21 | bool filterMods(String value) => widget.activeMods.where((element1) => value == element1.filename).isEmpty; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Column( 26 | children: [ 27 | Text( 28 | translate('edit_mod_profile.installed_mods'), 29 | style: TextStyle(color: Colors.white.withOpacity(.7)), 30 | ), 31 | const SizedBox(height: 8), 32 | SizedBox( 33 | child: TextBox( 34 | onChanged: (String? value) => setState(() => search = value ?? ''), 35 | placeholder: translate('search'), 36 | ), 37 | ), 38 | Expanded( 39 | flex: 5, 40 | child: SingleChildScrollView( 41 | child: ListView.builder( 42 | itemBuilder: (BuildContext context, int index) => InstalledModCategory( 43 | activeMods: widget.activeMods, 44 | onAdd: widget.onAdd, 45 | index: index, 46 | kyberCategories: widget.kyber, 47 | search: search, 48 | excludedCategories: widget.excludedCategories, 49 | ), 50 | physics: const NeverScrollableScrollPhysics(), 51 | shrinkWrap: true, 52 | itemCount: ModService.getModsByCategory(widget.kyber).length, 53 | ), 54 | ), 55 | ), 56 | ], 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/screens/mod_profiles/widgets/mod_category.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:kyber_mod_manager/utils/services/mod_service.dart'; 3 | import 'package:kyber_mod_manager/utils/types/freezed/frosty_collection.dart'; 4 | 5 | class InstalledModCategory extends StatefulWidget { 6 | const InstalledModCategory( 7 | {Key? key, required this.index, required this.onAdd, required this.kyberCategories, required this.search, required this.excludedCategories, required this.activeMods}) 8 | : super(key: key); 9 | 10 | final int index; 11 | final bool kyberCategories; 12 | final List activeMods; 13 | final String search; 14 | final Function onAdd; 15 | final List? excludedCategories; 16 | 17 | @override 18 | State createState() => _InstalledModCategoryState(); 19 | } 20 | 21 | class _InstalledModCategoryState extends State { 22 | late List mods; 23 | late String category; 24 | bool show = true; 25 | 26 | @override 27 | void initState() { 28 | loadData(); 29 | super.initState(); 30 | } 31 | 32 | @override 33 | void didUpdateWidget(covariant InstalledModCategory oldWidget) { 34 | loadData(); 35 | setState(() => null); 36 | super.didUpdateWidget(oldWidget); 37 | } 38 | 39 | void loadData() { 40 | final modsByCategory = ModService.getModsByCategory(widget.kyberCategories); 41 | category = modsByCategory.keys.toList()[widget.index]; 42 | mods = modsByCategory.values.toList()[widget.index]..sort((a, b) => a.name.compareTo(b.name)); 43 | mods = mods.where((element) => filterMods(element.filename) && (widget.search.isEmpty || element.name.toLowerCase().contains(widget.search.toLowerCase()))).toList(); 44 | show = mods.isNotEmpty && !(widget.excludedCategories != null && widget.excludedCategories!.contains(category)); 45 | } 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | if (!show) { 50 | return const SizedBox( 51 | height: 0, 52 | ); 53 | } 54 | 55 | return Card( 56 | padding: const EdgeInsets.all(10).copyWith(bottom: 0), 57 | child: Column( 58 | crossAxisAlignment: CrossAxisAlignment.start, 59 | children: [ 60 | Padding( 61 | padding: const EdgeInsets.symmetric(horizontal: 6), 62 | child: Text(category, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), 63 | ), 64 | ListView( 65 | padding: const EdgeInsets.all(0), 66 | shrinkWrap: true, 67 | children: mods.map((dynamic mod) { 68 | return ListTile( 69 | title: Text( 70 | '${mod.name} (${mod.version})${mod is FrostyCollection ? ' (Frosty Collection)' : ''}', 71 | style: const TextStyle(fontSize: 14), 72 | overflow: TextOverflow.ellipsis, 73 | ), 74 | leading: IconButton( 75 | icon: const Icon(FluentIcons.add), 76 | onPressed: () => setState(() => widget.onAdd(mod)), 77 | ), 78 | ); 79 | }).toList(), 80 | ) 81 | ], 82 | ), 83 | ); 84 | } 85 | 86 | bool filterMods(String value) => widget.activeMods.where((element1) => value == element1.filename).isEmpty; 87 | } 88 | -------------------------------------------------------------------------------- /lib/screens/run_battlefront/run_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | import 'package:kyber_mod_manager/main.dart'; 4 | import 'package:kyber_mod_manager/utils/helpers/platform_helper.dart'; 5 | import 'package:kyber_mod_manager/utils/services/frosty_profile_service.dart'; 6 | import 'package:kyber_mod_manager/utils/services/frosty_service.dart'; 7 | import 'package:kyber_mod_manager/utils/services/mod_service.dart'; 8 | import 'package:kyber_mod_manager/utils/services/profile_service.dart'; 9 | import 'package:kyber_mod_manager/utils/types/pack_type.dart'; 10 | import 'package:logging/logging.dart'; 11 | 12 | class RunDialog extends StatefulWidget { 13 | const RunDialog({Key? key, required this.profile}) : super(key: key); 14 | 15 | final String profile; 16 | 17 | @override 18 | State createState() => _RunDialogState(); 19 | } 20 | 21 | class _RunDialogState extends State { 22 | final String prefix = 'server_browser.join_dialog.joining_states'; 23 | late String selectedProfile; 24 | String content = ""; 25 | 26 | @override 27 | void initState() { 28 | selectedProfile = widget.profile; 29 | startFrosty(); 30 | super.initState(); 31 | } 32 | 33 | @override 34 | void setState(fn) { 35 | if (mounted) { 36 | super.setState(fn); 37 | } 38 | } 39 | 40 | void startFrosty() async { 41 | final String profile = selectedProfile.split(' (').first; 42 | final PackType packType = getPackType(selectedProfile); 43 | List mods = await ModService.createModPack( 44 | context, 45 | packType: packType, 46 | profileName: profile, 47 | cosmetics: false, 48 | onProgress: onProgress, 49 | setContent: (content) => setState(() => this.content = content), 50 | ); 51 | if (!mounted) return; 52 | 53 | bool startFrosty = (!dynamicEnvEnabled); 54 | if (dynamicEnvEnabled) { 55 | if (packType == PackType.FROSTY_PACK) { 56 | List appliedMods = await FrostyProfileService.getModsFromProfile(profile); 57 | if (!ProfileService.equalModlist(mods, appliedMods)) { 58 | Logger.root.info('Mod list is not equal, applying mods'); 59 | startFrosty = true; 60 | } 61 | } else if (packType == PackType.MOD_PROFILE || packType == PackType.COSMETICS) { 62 | String currentPath = PlatformHelper.getProfile(); 63 | List activeMods = await FrostyProfileService.getModsFromProfile(currentPath, isPath: true); 64 | if (!ProfileService.equalModlist(activeMods, mods) && ProfileService.getSavedProfiles().where((element) => ProfileService.equalModlist(element.mods, mods)).isEmpty) { 65 | Logger.root.info("No profile found, starting Frosty"); 66 | startFrosty = true; 67 | } 68 | } else if (packType == PackType.NO_MODS) { 69 | Logger.root.info("No mods, starting Frosty"); 70 | startFrosty = true; 71 | } 72 | } 73 | 74 | if (startFrosty) { 75 | setState(() => content = translate('$prefix.frosty')); 76 | await FrostyService.startFrosty(profile: packType == PackType.FROSTY_PACK ? profile : null); 77 | } else { 78 | PlatformHelper.startBattlefront(); 79 | } 80 | 81 | if (!mounted) return; 82 | Navigator.of(context).pop(); 83 | } 84 | 85 | void onProgress(copied, total) => setState(() => content = translate('run_battlefront.copying_profile', args: {'copied': copied, 'total': total})); 86 | 87 | @override 88 | Widget build(BuildContext context) { 89 | return ContentDialog( 90 | title: Text(translate('run_battlefront.title')), 91 | constraints: const BoxConstraints( 92 | maxWidth: 500, 93 | maxHeight: 300, 94 | ), 95 | actions: [ 96 | Button( 97 | child: Text(translate('cancel')), 98 | onPressed: () => Navigator.of(context).pop(), 99 | ), 100 | ], 101 | content: SizedBox( 102 | height: 150, 103 | width: 500, 104 | child: Row( 105 | mainAxisAlignment: MainAxisAlignment.center, 106 | children: [ 107 | const SizedBox( 108 | height: 20, 109 | width: 20, 110 | child: ProgressRing(), 111 | ), 112 | const SizedBox( 113 | width: 15, 114 | ), 115 | Text(content), 116 | ], 117 | ), 118 | ), 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/screens/settings/widgets/platform_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:flutter_translate/flutter_translate.dart'; 3 | 4 | class PlatformSelector extends StatefulWidget { 5 | const PlatformSelector({Key? key}) : super(key: key); 6 | 7 | @override 8 | _PlatformSelectorState createState() => _PlatformSelectorState(); 9 | } 10 | 11 | class _PlatformSelectorState extends State { 12 | final String prefix = 'settings.platform_selector'; 13 | String platform = 'origin'; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return ContentDialog( 18 | title: Text(translate('$prefix.title')), 19 | constraints: const BoxConstraints(maxWidth: 500), 20 | actions: [ 21 | Button( 22 | child: Text(translate('close')), 23 | onPressed: () { 24 | Navigator.of(context).pop(); 25 | }, 26 | ), 27 | FilledButton( 28 | child: Text(translate('$prefix.enable')), 29 | onPressed: () { 30 | Navigator.of(context).pop(platform.toLowerCase()); 31 | }, 32 | ), 33 | ], 34 | content: InfoLabel( 35 | label: translate('$prefix.subtitle'), 36 | child: ComboBox( 37 | value: platform, 38 | onChanged: (v) => setState(() => platform = v ?? 'Origin'), 39 | isExpanded: true, 40 | items: ['Origin', 'EA Desktop', 'Epic Games'].map((e) { 41 | return ComboBoxItem( 42 | value: e.toLowerCase(), 43 | child: Text(e), 44 | ); 45 | }).toList(), 46 | ), 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/screens/settings/widgets/settings_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | 3 | class SettingsCard extends StatelessWidget { 4 | const SettingsCard({Key? key, required this.icon, required this.title, required this.subtitle, required this.child}) : super(key: key); 5 | 6 | final IconData icon; 7 | final Widget title; 8 | final Widget subtitle; 9 | final Widget child; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Card( 14 | margin: const EdgeInsets.only(top: 4), 15 | padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), 16 | child: ListTile( 17 | title: Row( 18 | mainAxisAlignment: MainAxisAlignment.start, 19 | crossAxisAlignment: CrossAxisAlignment.center, 20 | children: [ 21 | Icon(icon), 22 | const SizedBox(width: 20), 23 | Expanded( 24 | child: Column( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | DefaultTextStyle( 28 | style: FluentTheme.of(context).typography.body!.copyWith(fontSize: 17), 29 | child: title, 30 | ), 31 | DefaultTextStyle( 32 | style: FluentTheme.of(context).typography.body!.copyWith(fontSize: 12), 33 | child: subtitle, 34 | ), 35 | ], 36 | ), 37 | ), 38 | child, 39 | ], 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/screens/settings/widgets/symlinks_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | 3 | class SymlinksDialog extends StatefulWidget { 4 | SymlinksDialog({Key? key}) : super(key: key); 5 | 6 | @override 7 | State createState() => _SymlinksDialogState(); 8 | } 9 | 10 | class _SymlinksDialogState extends State { 11 | bool _disabled = true; 12 | 13 | @override 14 | void initState() { 15 | Future.delayed(const Duration(seconds: 2), () => mounted ? setState(() => _disabled = false) : null); 16 | super.initState(); 17 | } 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return ContentDialog( 22 | constraints: const BoxConstraints(maxWidth: 600, minHeight: 400, maxHeight: 400), 23 | title: const Text("Symlinks"), 24 | actions: [ 25 | Button( 26 | child: const Text('Cancel'), 27 | onPressed: () => Navigator.of(context).pop(), 28 | ), 29 | FilledButton( 30 | onPressed: _disabled ? null : () => Navigator.of(context).pop(true), 31 | child: const Text('Activate'), 32 | ) 33 | ], 34 | content: SizedBox( 35 | height: 250, 36 | child: Container( 37 | alignment: Alignment.center, 38 | child: const Text.rich( 39 | TextSpan( 40 | text: 'If you enable this feature, you must', 41 | style: TextStyle(fontSize: 17), 42 | children: [ 43 | TextSpan(text: ' always ', style: TextStyle(fontWeight: FontWeight.w700)), 44 | TextSpan(text: 'start KMM as '), 45 | TextSpan(text: 'admin.', style: TextStyle(fontWeight: FontWeight.w700)), 46 | TextSpan(text: '\nOtherwise the saved profile generation will not work'), 47 | ], 48 | ), 49 | ), 50 | ), 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/screens/troubleshooting.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | 3 | class Troubleshooting extends StatefulWidget { 4 | Troubleshooting({Key? key}) : super(key: key); 5 | 6 | @override 7 | State createState() => _TroubleshootingState(); 8 | } 9 | 10 | class _TroubleshootingState extends State { 11 | @override 12 | Widget build(BuildContext context) { 13 | return ScaffoldPage( 14 | header: const PageHeader( 15 | title: Text('Troubleshooting'), 16 | ), 17 | content: ListView( 18 | shrinkWrap: true, 19 | padding: const EdgeInsets.symmetric(horizontal: 10), 20 | children: [ 21 | Expander( 22 | header: const Text('Kyber related errors'), 23 | initiallyExpanded: true, 24 | content: ListView( 25 | shrinkWrap: true, 26 | padding: const EdgeInsets.symmetric(horizontal: 10), 27 | children: [ 28 | Expander( 29 | header: const Text('Requires mods, but client did not send any.'), 30 | content: const Text('This is the content'), 31 | ), 32 | Expander( 33 | header: const Text("Kyber won't inject properly and BF2 just loads into the main menu."), 34 | content: const Text('This is the content'), 35 | ), 36 | ], 37 | ), 38 | ), 39 | Expander( 40 | header: const Text('Frosty related errors'), 41 | initiallyExpanded: true, 42 | content: ListView( 43 | shrinkWrap: true, 44 | padding: const EdgeInsets.symmetric(horizontal: 10), 45 | children: [ 46 | Expander( 47 | header: const Text('123123123123'), 48 | content: const Text('This is the content'), 49 | ), 50 | Expander( 51 | header: const Text("Kyber won't inject properly and BF2 just loads into the main menu."), 52 | content: const Text('This is the content'), 53 | ), 54 | ], 55 | ), 56 | ), 57 | ], 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/utils/app_locale.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:ui'; 3 | 4 | import 'package:kyber_mod_manager/main.dart'; 5 | 6 | class AppLocale { 7 | Locale getLocale() { 8 | try { 9 | return Locale.fromSubtags(languageCode: box.get('locale', defaultValue: Platform.localeName.split('_').first)); 10 | } catch (e) { 11 | return const Locale.fromSubtags(languageCode: 'en-US'); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/utils/auto_updater.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_update/auto_update.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:kyber_mod_manager/main.dart'; 6 | import 'package:kyber_mod_manager/utils/services/api_service.dart'; 7 | import 'package:kyber_mod_manager/utils/types/freezed/github_asset.dart'; 8 | import 'package:logging/logging.dart'; 9 | import 'package:version/version.dart'; 10 | 11 | class AutoUpdater { 12 | Future updateAvailable() async { 13 | List? packageInfo = await const MethodChannel('auto_update').invokeListMethod("getProductAndVersion"); 14 | Version currentVersion = Version.parse(packageInfo![1]); 15 | VersionInfo? latestVersion = await getLatestVersion(); 16 | if (latestVersion == null || latestVersion.version <= currentVersion) { 17 | Logger.root.info("No updates available"); 18 | return null; 19 | } 20 | return latestVersion; 21 | } 22 | 23 | Future update() async { 24 | VersionInfo? available = await updateAvailable(); 25 | if (available == null) { 26 | return; 27 | } 28 | 29 | await AutoUpdate.downloadAndUpdate(available.assetUrl); 30 | } 31 | 32 | Future getLatestVersion() async { 33 | var response = 34 | await ApiService.dio(cachePolicy: CachePolicy.forceCache, maxCacheStale: const Duration(hours: 1)).get('https://api.github.com/repos/7reax/kyber-mod-manager/releases'); 35 | List releases = []; 36 | response.data.forEach((release) { 37 | releases 38 | .add(GitHubAsset.fromJson({...release['assets'].where((asset) => asset['name'].toString().endsWith('.exe')).first, 'version': release['tag_name'], 'id': release['id']})); 39 | }); 40 | var version = box.get('beta') ? releases.first : releases.firstWhere((element) => !Version.parse(element.version).isPreRelease); 41 | var versionInfo = await Dio().get('https://api.github.com/repos/7reax/kyber-mod-manager/releases/${version.id}'); 42 | return VersionInfo(Version.parse(version.version), version.browser_download_url, versionInfo.data['body']); 43 | } 44 | } 45 | 46 | class VersionInfo { 47 | final Version version; 48 | final String assetUrl; 49 | final String body; 50 | 51 | VersionInfo(this.version, this.assetUrl, this.body); 52 | } 53 | -------------------------------------------------------------------------------- /lib/utils/battlefront_options.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:path_provider/path_provider.dart'; 4 | 5 | class BattlefrontOptions { 6 | static Future getOptions() async { 7 | String config = await BattlefrontOptions._getConfig(); 8 | 9 | if (config.isEmpty) { 10 | return null; 11 | } 12 | 13 | return BattlefrontProfileOptions( 14 | enableDx12: config.contains('GstRender.EnableDx12 1'), 15 | fullscreenEnabled: config.contains('GstRender.FullscreenEnabled 1'), 16 | ); 17 | } 18 | 19 | static Future crashed() async { 20 | Directory docs = await getApplicationDocumentsDirectory(); 21 | if (!Directory('${docs.path}\\STAR WARS Battlefront II\\CrashDumps').existsSync()) { 22 | return false; 23 | } 24 | 25 | List files = Directory('${docs.path}\\STAR WARS Battlefront II\\CrashDumps').listSync().whereType().toList(); 26 | if (files.isEmpty) { 27 | return false; 28 | } 29 | 30 | files.sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync())); 31 | 32 | File lastCrash = files.first; 33 | if (lastCrash.lastModifiedSync().difference(DateTime.now()).inMinutes < 2) { 34 | return false; 35 | } 36 | 37 | return true; 38 | } 39 | 40 | static Future setConfig() async { 41 | String config = await _getConfig(); 42 | String configPath = await _getConfigPath(); 43 | var values = [ 44 | ['GstRender.EnableDx12 1', 'GstRender.EnableDx12 0'], 45 | ['GstRender.FullscreenEnabled 1', 'GstRender.FullscreenEnabled 0'], 46 | ['GstRender.FullscreenMode 0', 'GstRender.FullscreenMode 1'], 47 | ['GstRender.FullscreenMode 0', 'GstRender.FullscreenMode 1'], 48 | ]; 49 | values.forEach((value) => config.replaceAll(value[0], value[1])); 50 | await File(configPath).writeAsString(config); 51 | } 52 | 53 | static Future _getConfig() async { 54 | String configPath = await _getConfigPath(); 55 | return File(configPath).readAsString(); 56 | } 57 | 58 | static Future _getConfigPath() async { 59 | Directory docs = await getApplicationDocumentsDirectory(); 60 | if (!Directory('${docs.path}/STAR WARS Battlefront II').existsSync()) { 61 | return ''; 62 | } 63 | 64 | return '${docs.path}\\STAR WARS Battlefront II\\settings\\ProfileOptions_profile'; 65 | } 66 | } 67 | 68 | class BattlefrontProfileOptions { 69 | bool enableDx12; 70 | bool fullscreenEnabled; 71 | 72 | BattlefrontProfileOptions({required this.enableDx12, required this.fullscreenEnabled}); 73 | } 74 | -------------------------------------------------------------------------------- /lib/utils/custom_logger.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:fluent_ui/fluent_ui.dart'; 4 | import 'package:kyber_mod_manager/main.dart'; 5 | import 'package:logging/logging.dart'; 6 | import 'package:sentry_flutter/sentry_flutter.dart'; 7 | 8 | class CustomLogger { 9 | static late File _logFile; 10 | 11 | static void initialize() { 12 | _logFile = File('$applicationDocumentsDirectory/log.txt'); 13 | _createLogFile(); 14 | Logger.root.level = Level.INFO; 15 | Logger.root.onRecord.listen( 16 | (record) { 17 | String message = '[${DateTime.now().toString().split('.').first}] ${record.level.name}: ${record.message}'; 18 | _logToFile(message); 19 | print(message); 20 | }, 21 | ); 22 | FlutterError.onError = (details) { 23 | Logger.root.severe('Uncaught exception: ${details.exception}\n${details.stack}'); 24 | Sentry.captureException(details.exception, stackTrace: details.stack); 25 | }; 26 | } 27 | 28 | static String getLogs() { 29 | return _logFile.readAsStringSync(); 30 | } 31 | 32 | static void _logToFile(String message) { 33 | _logFile.writeAsString('$message\n', mode: FileMode.append); 34 | } 35 | 36 | static void _createLogFile() { 37 | if (!_logFile.existsSync()) { 38 | _logFile.createSync(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/utils/helpers/map_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:kyber_mod_manager/constants/maps.dart'; 2 | import 'package:kyber_mod_manager/constants/modes.dart'; 3 | import 'package:kyber_mod_manager/utils/types/map.dart'; 4 | import 'package:kyber_mod_manager/utils/types/mode.dart'; 5 | 6 | class MapHelper { 7 | static List getMapsForMode(String x) { 8 | Mode mode = modes.firstWhere((element) => element.mode == x); 9 | return mode.maps.map((e) { 10 | MapOverride? override = mode.mapOverrides?.firstWhere((x) => x.map == e, orElse: () => MapOverride(map: '', name: '')); 11 | if (override != null && override.name != '') { 12 | return KyberMap(map: override.map, name: override.name); 13 | } 14 | return KyberMap(map: e, name: maps.firstWhere((element) => element['map'] == e)['name']); 15 | }).toList(); 16 | } 17 | 18 | static String getMapName(Mode mode, String map) { 19 | if (mode.mapOverrides != null && mode.mapOverrides!.where((x) => x.map == map).isNotEmpty) { 20 | return mode.mapOverrides!.firstWhere((x) => x.map == map).name; 21 | } 22 | 23 | return maps.firstWhere((x) => x["map"] == map)["name"]!; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/utils/helpers/origin_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:kyber_mod_manager/main.dart'; 2 | import 'package:logging/logging.dart'; 3 | import 'package:win32_registry/win32_registry.dart'; 4 | 5 | class OriginHelper { 6 | static String getBattlefrontPath({bool force = false}) { 7 | try { 8 | if (box.containsKey('battlefrontPath') &&! force) { 9 | return box.get('battlefrontPath'); 10 | } 11 | 12 | final ea = Registry.openPath(RegistryHive.localMachine, path: r'SOFTWARE\EA Games\STAR WARS Battlefront II'); 13 | String path = ea.getValueAsString('Install Dir') ?? ''; 14 | return path.substring(0, path.length - 1); 15 | } catch (e) { 16 | Logger.root.severe('Could not find Battlefront path'); 17 | return ''; 18 | } 19 | } 20 | 21 | static void updateBattlefrontPath() { 22 | box.put('battlefrontPath', getBattlefrontPath(force: true)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/utils/helpers/path_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:archive/archive_io.dart'; 5 | import 'package:dio/dio.dart'; 6 | import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; 7 | import 'package:kyber_mod_manager/utils/services/api_service.dart'; 8 | import 'package:kyber_mod_manager/utils/services/frosty_service.dart'; 9 | import 'package:kyber_mod_manager/utils/types/freezed/github_asset.dart'; 10 | import 'package:kyber_mod_manager/utils/types/frosty_config.dart'; 11 | 12 | class PathHelper { 13 | static CancelToken? _cancelToken; 14 | 15 | static Future> getFrostyVersions() async { 16 | var response = 17 | await ApiService.dio(cachePolicy: CachePolicy.forceCache, maxCacheStale: const Duration(hours: 3)).get('https://api.github.com/repos/CadeEvs/FrostyToolsuite/releases'); 18 | List releases = []; 19 | response.data.forEach((release) { 20 | releases.add(GitHubAsset.fromJson({...release['assets'].where((asset) => asset['name'] == 'FrostyModManager.zip').first, 'version': release['tag_name']})); 21 | }); 22 | return releases; 23 | } 24 | 25 | static void cancelDownload() => _cancelToken?.cancel(); 26 | 27 | static Future downloadFrosty(Directory path, GitHubAsset gitHubAsset, Function(int, int) onProgress) async { 28 | onProgress(0, gitHubAsset.size); 29 | _cancelToken = CancelToken(); 30 | await Dio().download( 31 | gitHubAsset.browser_download_url, 32 | '${path.path}.zip', 33 | cancelToken: _cancelToken, 34 | onReceiveProgress: (received, total) => onProgress(received, total), 35 | ); 36 | await Future.delayed(const Duration(seconds: 1)); 37 | final inputStream = InputFileStream('${path.path}.zip'); 38 | final archive = ZipDecoder().decodeBuffer(inputStream, verify: false); 39 | for (var file in archive.files) { 40 | String filepath = '${path.path}/${file.name.replaceFirst("FrostyModManager/", "")}'; 41 | if (!file.isFile) { 42 | continue; 43 | } 44 | 45 | if (!File(filepath).existsSync()) { 46 | File(filepath).createSync(recursive: true); 47 | } 48 | final outputStream = OutputFileStream(filepath); 49 | file.writeContent(outputStream); 50 | outputStream.close(); 51 | } 52 | archive.clear(); 53 | } 54 | 55 | static String? isValidFrostyDir(String path) { 56 | Directory directory = Directory(path); 57 | if (!directory.existsSync()) { 58 | return 'invalid_dir'; 59 | } 60 | 61 | List entities = directory.listSync().toList(); 62 | if (entities.where((element) => element.path.endsWith('FrostyModManager.exe')).isEmpty) { 63 | return 'invalid_dir'; 64 | } 65 | 66 | Directory dataDir = Directory('$path/Mods/starwarsbattlefrontii'); 67 | if (!dataDir.existsSync()) { 68 | return 'bf2_not_found'; 69 | } 70 | 71 | FrostyConfig config = FrostyService.getFrostyConfig(); 72 | if (!config.games.keys.contains('starwarsbattlefrontii')) { 73 | return 'bf2_not_found'; 74 | } 75 | 76 | return null; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/utils/helpers/puppeteer_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:kyber_mod_manager/main.dart'; 6 | import 'package:kyber_mod_manager/screens/errors/chromium_not_found.dart'; 7 | import 'package:kyber_mod_manager/utils/services/navigator_service.dart'; 8 | import 'package:puppeteer/plugins/stealth.dart'; 9 | import 'package:puppeteer/puppeteer.dart'; 10 | 11 | class PuppeteerHelper { 12 | static const String _chromiumPath = './970485/chrome-win/chrome.exe'; 13 | static Browser? _browser; 14 | 15 | static void checkFiles() { 16 | if (kDebugMode) { 17 | return; 18 | } 19 | 20 | var file = File(_chromiumPath); 21 | if (!file.existsSync()) { 22 | NavigatorService.pushErrorPage(const ChromiumNotFound()); 23 | throw Exception('Chromium not found'); 24 | } 25 | } 26 | 27 | static Future startBrowser({Function? onClose, Function? onBrowserCreated, bool headless = true, bool asApp = true}) async { 28 | if (_browser != null) { 29 | await _browser?.close().catchError((e) => null); 30 | _browser = null; 31 | } 32 | 33 | _browser = await puppeteer.launch( 34 | executablePath: kDebugMode ? null : _chromiumPath, 35 | defaultViewport: null, 36 | args: [ 37 | '--ignore-certifcate-errors', 38 | '--ignore-certifcate-errors-spki-list', 39 | '--lang=en-EN,en', 40 | '--start-maximized', 41 | '--suppress-message-center-popups', 42 | asApp ? '--app=https://nexusmods.com/starwarsbattlefront22017/' : '', 43 | ], 44 | ignoreDefaultArgs: ['--enable-automation'], 45 | headless: headless, 46 | plugins: [ 47 | StealthPlugin(), 48 | ], 49 | ); 50 | if (onBrowserCreated != null) { 51 | onBrowserCreated(_browser); 52 | } 53 | var page = (await _browser!.pages).first; 54 | _browser!.disconnected.asStream().listen((event) { 55 | onClose != null ? onClose() : null; 56 | _browser = null; 57 | }); 58 | 59 | if (box.containsKey('cookies') && box.containsKey('nexusmods_login')) { 60 | await page.setCookies(List.from(box.get('cookies').map((cookie) => CookieParam.fromJson(Map.from(cookie))).toList())); 61 | } 62 | return _browser!; 63 | } 64 | 65 | static Future initializePage(Page page) async { 66 | String downloadPath = box.get('frostyPath') + '\\Mods\\starwarsbattlefrontii'; 67 | page.browser.connection.send('Browser.setDownloadBehavior', { 68 | 'behavior': 'allow', 69 | 'downloadPath': downloadPath, 70 | }); 71 | var t = await page.evaluate("() => document.querySelector('.qc-cmp2-publisher-logo-container')"); 72 | if (t != null) await page.click('button[class\$=" css-47sehv"]'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/utils/helpers/storage_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:hive_flutter/hive_flutter.dart'; 5 | import 'package:kyber_mod_manager/main.dart'; 6 | import 'package:kyber_mod_manager/utils/types/freezed/frosty_collection.dart'; 7 | import 'package:kyber_mod_manager/utils/types/freezed/mod.dart'; 8 | import 'package:kyber_mod_manager/utils/types/freezed/mod_profile.dart'; 9 | import 'package:logging/logging.dart'; 10 | 11 | class StorageHelper { 12 | static final Map _defaultValues = { 13 | 'cosmetics': [], 14 | 'discordRPC': true, 15 | 'saveProfiles': true, 16 | 'enableCosmetics': false, 17 | 'beta': false, 18 | 'enableDynamicEnv': true, 19 | }; 20 | 21 | static Future initializeHive() async { 22 | await Hive.initFlutter(applicationDocumentsDirectory); 23 | Hive.registerAdapter(ModProfileAdapter(), override: true); 24 | Hive.registerAdapter(ModAdapter(), override: true); 25 | Hive.registerAdapter(FrostyCollectionAdapter(), override: true); 26 | box = await Hive.openBox('data').catchError((e) { 27 | if (kDebugMode) { 28 | return null; 29 | } 30 | Logger.root.severe('Error opening box: $e'); 31 | exit(1); 32 | }); 33 | _defaultValues.keys.where((key) => !box.containsKey(key)).forEach((key) => box.put(key, _defaultValues[key])); 34 | dynamicEnvEnabled = box.get('enableDynamicEnv') as bool; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/utils/helpers/system_tasks.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:process_run/process_run.dart'; 4 | 5 | bool _isWindows = Platform.isWindows; 6 | 7 | bool _trim(String line) { 8 | return line.trim() != ''; 9 | } 10 | 11 | Task _mapLine(String line) { 12 | return Task(line.trim().split(RegExp(r"\s+")), line); 13 | } 14 | 15 | class SystemTasks { 16 | static Future> tasks() async { 17 | try { 18 | var r = await Process.run(_isWindows ? "tasklist" : "ps aux", [], runInShell: false); 19 | if (r.stdout != null) { 20 | String stdout = r.stdout.toString(); 21 | List tasks = stdout.split("\n").where(_trim).map(_mapLine).toList(); 22 | tasks = tasks.where((e) => (_isWindows ? tasks.indexOf(e) > 1 : tasks.indexOf(e) > 0)).toList(); 23 | return List.from(tasks); 24 | } else { 25 | return List.generate(0, (i) => const Task([""], "")); 26 | } 27 | } catch (e) { 28 | rethrow; 29 | } 30 | } 31 | } 32 | 33 | class Task { 34 | final List p; 35 | final String line; 36 | 37 | String get pname => _isWindows ? p[0] : p[10]; 38 | 39 | String get pid => p[1]; 40 | 41 | const Task(this.p, this.line); 42 | 43 | Future kill() { 44 | String command = _isWindows ? "taskkill /PID $pid /TF" : "kill -s 9 $pid"; 45 | return runExecutableArguments(command, []); 46 | } 47 | 48 | Future killLikes() { 49 | String command = _isWindows ? "TASKKILL /F /IM $pname /T" : "pkill -9 $pname"; 50 | return runExecutableArguments(command, []); 51 | } 52 | 53 | Future start() { 54 | return runExecutableArguments(pname.replaceAll(RegExp(r"\.exe$"), ""), []); 55 | } 56 | 57 | Future reStart() async { 58 | try { 59 | await kill(); 60 | await start(); 61 | } catch (error) { 62 | rethrow; 63 | } 64 | } 65 | 66 | Future reStartLikes() async { 67 | try { 68 | await killLikes(); 69 | await start(); 70 | } catch (error) { 71 | rethrow; 72 | } 73 | } 74 | 75 | @override 76 | String toString() { 77 | return line; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/utils/helpers/unzip_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:win32_registry/win32_registry.dart'; 4 | 5 | class UnzipHelper { 6 | static Future unrar(File file, Directory to) async { 7 | String? exe = getExecutable(); 8 | if (exe == null) { 9 | throw Exception('Not executable found'); 10 | } 11 | if (exe.endsWith('7z.exe')) { 12 | await Process.run(exe, ['e', file.path, '-y'], workingDirectory: to.path); 13 | } else { 14 | await Process.run(exe, ['e', '-ibck', file.path, '*.*', to.path]); 15 | } 16 | } 17 | 18 | static String? getExecutable() { 19 | final key = Registry.openPath(RegistryHive.localMachine, path: r'SOFTWARE'); 20 | if (key.subkeyNames.contains('WinRAR')) { 21 | return Registry.openPath(RegistryHive.localMachine, path: r'SOFTWARE\WinRAR').getValueAsString('exe64'); 22 | } else if (key.subkeyNames.contains('7-Zip')) { 23 | return '${Registry.openPath(RegistryHive.localMachine, path: r'SOFTWARE\7-Zip').getValueAsString('Path64')!}\\7z.exe'; 24 | } 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/utils/helpers/window_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:fluent_ui/fluent_ui.dart'; 4 | import 'package:flutter/scheduler.dart'; 5 | import 'package:flutter_acrylic/flutter_acrylic.dart'; 6 | import 'package:kyber_mod_manager/main.dart'; 7 | import 'package:logging/logging.dart'; 8 | import 'package:window_manager/window_manager.dart'; 9 | 10 | class WindowHelper { 11 | static const Size _minimumSize = Size(900, 600); 12 | static const Size _size = Size(1400, 700); 13 | static WindowBrightness _windowBrightness = WindowBrightness.system; 14 | 15 | static Stream get windowBrightnessStream => _streamController!.stream; 16 | static StreamController? _streamController; 17 | static WindowBrightness get windowBrightness => _windowBrightness; 18 | static void set windowBrightness(WindowBrightness value) { 19 | _windowBrightness = value; 20 | _streamController?.add(value); 21 | } 22 | 23 | static Future changeWindowEffect(bool enabled) async { 24 | if (enabled) { 25 | await Window.setEffect(effect: WindowEffect.mica, dark: true); 26 | await windowManager.setBackgroundColor(Colors.transparent); 27 | } else { 28 | await Window.setEffect(effect: WindowEffect.disabled, dark: true); 29 | await windowManager.setBackgroundColor(FluentThemeData.dark().navigationPaneTheme.backgroundColor!); 30 | } 31 | } 32 | 33 | static Future changeEffect(bool dark) async { 34 | await windowManager.setBackgroundColor(Colors.transparent); 35 | 36 | if (micaSupported) { 37 | await Window.setEffect(effect: WindowEffect.mica, dark: dark); 38 | } 39 | } 40 | 41 | static Future initializeWindow() async { 42 | Logger.root.info('Initializing window'); 43 | _windowBrightness = WindowBrightness.values[box.get('brightness', defaultValue: 2)]; 44 | _streamController = StreamController.broadcast(); 45 | await Window.initialize(); 46 | 47 | if (!micaSupported) { 48 | windowManager.waitUntilReadyToShow().then((_) async { 49 | await windowManager.setTitleBarStyle(TitleBarStyle.hidden, windowButtonVisibility: false); 50 | await windowManager.setSize(_size); 51 | await windowManager.setMinimumSize(_minimumSize); 52 | await windowManager.center(); 53 | await windowManager.show(); 54 | await windowManager.setSkipTaskbar(false); 55 | }); 56 | } else { 57 | windowManager.waitUntilReadyToShow().then((_) async { 58 | bool enabled = !box.containsKey('micaEnabled') || box.get('micaEnabled'); 59 | await windowManager.setTitleBarStyle(TitleBarStyle.normal, windowButtonVisibility: false); 60 | await windowManager.setSize(_size); 61 | await windowManager.setMinimumSize(_minimumSize); 62 | await windowManager.center(); 63 | await windowManager.show(); 64 | await windowManager.setBackgroundColor(enabled ? Colors.transparent : (windowBrightness.isDark ? FluentThemeData.dark() : FluentThemeData.light()).navigationPaneTheme.backgroundColor!); 65 | 66 | if (enabled) { 67 | await Window.setEffect(effect: WindowEffect.mica, dark: _windowBrightness.isDark); 68 | } 69 | }); 70 | } 71 | } 72 | } 73 | 74 | enum WindowBrightness { 75 | light, 76 | dark, 77 | system, 78 | } 79 | 80 | extension WindowBrightnessExtension on WindowBrightness { 81 | bool get isDark { 82 | switch (this) { 83 | case WindowBrightness.light: 84 | return false; 85 | case WindowBrightness.dark: 86 | return true; 87 | case WindowBrightness.system: 88 | return SchedulerBinding.instance.window.platformBrightness == Brightness.dark; 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /lib/utils/services/api_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; 5 | import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; 6 | import 'package:http/http.dart'; 7 | import 'package:kyber_mod_manager/api/backend/download_info.dart'; 8 | import 'package:kyber_mod_manager/constants/api_constants.dart'; 9 | import 'package:kyber_mod_manager/main.dart'; 10 | import 'package:kyber_mod_manager/utils/types/freezed/discord_event.dart'; 11 | import 'package:kyber_mod_manager/utils/types/freezed/frosty_version.dart'; 12 | 13 | class ApiService { 14 | static Future isAvailable(String name) async { 15 | try { 16 | final response = await get( 17 | Uri.parse('$BACKEND_API_BASE_URL/mods?q=$name'), 18 | headers: {'Accept': 'application/json'}, 19 | ); 20 | return response.statusCode == 200; 21 | } catch (e) { 22 | return false; 23 | } 24 | } 25 | 26 | static Future getLatestFrostyVersion() async { 27 | try { 28 | final response = await get( 29 | Uri.parse('$BACKEND_API_BASE_URL/frosty/latest'), 30 | ); 31 | return response.statusCode == 200 ? jsonDecode(response.body)['version'] : ''; 32 | } catch (e) { 33 | return ''; 34 | } 35 | } 36 | 37 | static Future getFrostyVersion(String hash) async { 38 | try { 39 | final response = await get( 40 | Uri.parse('$BACKEND_API_BASE_URL/frosty/$hash'), 41 | headers: {'Accept': 'application/json'}, 42 | ); 43 | var version = json.decode(response.body); 44 | var plugins = []; 45 | version['plugins'].split(',').forEach((e) { 46 | plugins.add({ 47 | 'name': e.split(':')[0], 48 | 'hash': e.split(':')[1], 49 | }); 50 | }); 51 | version['plugins'] = plugins; 52 | return FrostyVersion.fromJson(version); 53 | } catch (e) { 54 | return const FrostyVersion(version: '', hash: ''); 55 | } 56 | } 57 | 58 | static Future> getEvents() async { 59 | final response = await Dio().get("$BACKEND_API_BASE_URL/events"); 60 | 61 | return (response.data as List).map((e) => DiscordEvent.fromJson(e)).toList(); 62 | } 63 | 64 | static Future> versionHashes() async { 65 | try { 66 | final response = await get( 67 | Uri.parse('$BACKEND_API_BASE_URL/frosty/hashes'), 68 | headers: {'Accept': 'application/json'}, 69 | ); 70 | return (json.decode(response.body) as List).map((e) { 71 | e['plugins'] = []; 72 | return FrostyVersion.fromJson(e); 73 | }).toList(); 74 | } catch (e) { 75 | return []; 76 | } 77 | } 78 | 79 | static Future> supportedFrostyVersions() async { 80 | try { 81 | final response = await get( 82 | Uri.parse('$BACKEND_API_BASE_URL/frosty/versions'), 83 | ); 84 | return List.from(json.decode(response.body)); 85 | } catch (e) { 86 | return []; 87 | } 88 | } 89 | 90 | static Future<_DownloadLinksResponse> getDownloadLinks(List mods) async { 91 | List links = []; 92 | List unavailableMods = []; 93 | await Future.wait(mods.map((e) async { 94 | var info = await getDownloadInfo(e); 95 | if (info?.fileUrl == null) { 96 | unavailableMods.add(e); 97 | return; 98 | } 99 | links.add(info!.fileUrl + '?tab=files&file_id=' + info.fileId); 100 | })); 101 | return _DownloadLinksResponse(links, unavailableMods); 102 | } 103 | 104 | static Future getDownloadInfo(String modName) async { 105 | final resp = await get(Uri.parse('$BACKEND_API_BASE_URL/mods?q=$modName')); 106 | if (resp.statusCode == 200) { 107 | return DownloadInfo.fromJson(json.decode(resp.body)); 108 | } 109 | return null; 110 | } 111 | 112 | static HiveCacheStore get cacheStore => HiveCacheStore( 113 | applicationDocumentsDirectory, 114 | hiveBoxName: 'cache', 115 | ); 116 | 117 | static Dio dio({Duration? maxCacheStale, CachePolicy? cachePolicy}) { 118 | var cacheOptions = CacheOptions( 119 | store: cacheStore, 120 | maxStale: maxCacheStale, 121 | policy: cachePolicy ?? CachePolicy.request, 122 | priority: CachePriority.high, 123 | ); 124 | 125 | return Dio()..interceptors.add(DioCacheInterceptor(options: cacheOptions)); 126 | } 127 | } 128 | 129 | class _DownloadLinksResponse { 130 | List links; 131 | List unavailable; 132 | 133 | _DownloadLinksResponse(this.links, this.unavailable); 134 | } 135 | -------------------------------------------------------------------------------- /lib/utils/services/mod_installer_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:archive/archive_io.dart'; 5 | import 'package:fluent_ui/fluent_ui.dart'; 6 | import 'package:kyber_mod_manager/main.dart'; 7 | import 'package:kyber_mod_manager/utils/helpers/unzip_helper.dart'; 8 | import 'package:kyber_mod_manager/utils/services/notification_service.dart'; 9 | import 'package:logging/logging.dart'; 10 | import 'package:windows_taskbar/windows_taskbar.dart'; 11 | 12 | class ModInstallerService { 13 | static final _allowedExtensions = ['zip', 'rar', '7z', 'fbmod']; 14 | static Directory? _installDir; 15 | 16 | static void initialize() { 17 | if (!box.containsKey('frostyPath')) { 18 | return; 19 | } 20 | 21 | _installDir = Directory(box.get('frostyPath') + '\\Mods\\starwarsbattlefrontii\\'); 22 | } 23 | 24 | static void handleDrop(List paths) async { 25 | if (paths.isEmpty || _installDir == null) { 26 | return; 27 | } 28 | 29 | await Future.forEach(paths.where((path) => _allowedExtensions.contains(path.split('.').last)), (String path) async { 30 | String extension = path.split('.').last; 31 | File file = File(path); 32 | 33 | if (extension == 'fbmod') { 34 | String basename = file.path.split('\\').last; 35 | await File(_installDir!.path + '\\' + basename).writeAsBytes(file.readAsBytesSync()); 36 | Logger.root.info('Installed mod: $basename'); 37 | NotificationService.showNotification(message: 'Installed mod: $basename'); 38 | } else if (extension == 'zip') { 39 | final inputStream = InputFileStream(file.path); 40 | final archive = ZipDecoder().decodeBuffer(inputStream, verify: false); 41 | 42 | WindowsTaskbar.setProgressMode(TaskbarProgressMode.normal); 43 | for (var file in archive.files) { 44 | WindowsTaskbar.setProgress(archive.files.indexOf(file), archive.files.length - 1); 45 | final outputStream = OutputFileStream(_installDir!.path + file.name); 46 | file.writeContent(outputStream); 47 | outputStream.close(); 48 | } 49 | await archive.clear(); 50 | Logger.root.info('Installed mod: ${file.path.split('\\').last}'); 51 | NotificationService.showNotification(message: 'Installed mod: ${file.path.split('\\').last}'); 52 | } else { 53 | await UnzipHelper.unrar(file, _installDir!).catchError((error) { 54 | NotificationService.showNotification(message: error.toString(), severity: InfoBarSeverity.error); 55 | Logger.root.severe('Could not unrar ${file.path}. $error'); 56 | }); 57 | Logger.root.info('Installed mod: ${file.path.split('\\').last}'); 58 | NotificationService.showNotification(message: 'Installed mod: ${file.path.split('\\').last}'); 59 | } 60 | WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/utils/services/navigator_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | 3 | final GlobalKey navigatorKey = GlobalKey(); 4 | 5 | class NavigatorService { 6 | static Future pushErrorPage(Widget widget) { 7 | return navigatorKey.currentState!.push(FluentPageRoute(builder: (context) => widget)); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/services/nexusmods_api_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:html/dom.dart'; 2 | import 'package:html/parser.dart'; 3 | import 'package:kyber_mod_manager/utils/services/api_service.dart'; 4 | import 'package:kyber_mod_manager/utils/types/freezed/nexus_mods_search_result.dart'; 5 | 6 | class NexusModsApiService { 7 | static const String _baseUrl = 'https://www.nexusmods.com/starwarsbattlefront22017/mods/'; 8 | 9 | static Future search(String query) async { 10 | var res = await ApiService.dio().get('https://search.nexusmods.com/mods?terms=ioi&game_id=2229&blocked_tags=&blocked_authors=&include_adult=0'); 11 | return NexusModsSearchResult.fromJson(res.data); 12 | } 13 | 14 | static Future generateDownloadUrl(String url, String version) async { 15 | if (!url.startsWith(_baseUrl) || url.replaceAll(url, _baseUrl).isEmpty) { 16 | return null; 17 | } 18 | 19 | var res = await ApiService.dio().get('$url?tab=files'); 20 | var doc = parse(res.data); 21 | List element = doc.getElementsByClassName('file-expander-header').where((element) { 22 | return element.attributes['data-version'].toString().toLowerCase() == version.toLowerCase().replaceAll('v', ''); 23 | }).toList(); 24 | if (element.length != 1) { 25 | return null; 26 | } 27 | 28 | return '$url?tab=files&file_id=${element.first.attributes['data-id']}'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/utils/services/nexusmods_login_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:kyber_mod_manager/main.dart'; 4 | import 'package:kyber_mod_manager/utils/helpers/puppeteer_helper.dart'; 5 | import 'package:puppeteer/puppeteer.dart'; 6 | 7 | class NexusmodsLoginService { 8 | static const String _mainPage = 'https://www.nexusmods.com/starwarsbattlefront22017'; 9 | static late Page _page; 10 | 11 | static Future init({required Function onClose, required Function onCreated, required Function onLoginSuccessful}) async { 12 | var browser = await PuppeteerHelper.startBrowser(onClose: onClose, headless: false); 13 | onCreated(browser); 14 | _page = (await browser.pages).first; 15 | await PuppeteerHelper.initializePage(_page); 16 | await _page.goto("https://users.nexusmods.com/auth/sign_in", wait: Until.networkAlmostIdle); 17 | _page.onFrameNavigated.listen((event) async { 18 | if (event.url == "https://users.nexusmods.com/account/profile/edit") { 19 | await Future.delayed(const Duration(milliseconds: 1000)); 20 | await _page.goto( 21 | 'https://users.nexusmods.com/oauth/authorize?client_id=nexus&redirect_uri=https://www.nexusmods.com/oauth/callback&response_type=code&referrer=$_mainPage', 22 | wait: Until.domContentLoaded, 23 | ); 24 | await box.put('cookies', (await _page.cookies()).map((e) => e.toJson()).toList()); 25 | await box.put('nexusmods_login', true); 26 | onLoginSuccessful(); 27 | } 28 | }); 29 | return browser; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/utils/services/notification_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | import 'package:kyber_mod_manager/main.dart'; 3 | 4 | class NotificationService { 5 | static void showNotification({String? title, required String message, InfoBarSeverity? severity}) { 6 | displayInfoBar(shellNavigatorKey.currentContext!, builder: (context, close) { 7 | return InfoBar( 8 | title: Text(title ?? message), 9 | content: title == null ? null : Text(message), 10 | action: IconButton( 11 | icon: const Icon(FluentIcons.clear), 12 | onPressed: close, 13 | ), 14 | 15 | style: InfoBarThemeData.standard(FluentTheme.of(context).copyWith(resources: ResourceDictionary.dark(systemFillColorAttentionBackground: FluentTheme.of(context).resources.solidBackgroundFillColorBase))), 16 | severity: severity ?? InfoBarSeverity.info, 17 | ); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/utils/services/rpc_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dart_discord_rpc/dart_discord_rpc.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | import 'package:kyber_mod_manager/constants/maps.dart'; 6 | import 'package:kyber_mod_manager/constants/modes.dart'; 7 | import 'package:kyber_mod_manager/logic/game_status_cubic.dart'; 8 | import 'package:kyber_mod_manager/main.dart'; 9 | import 'package:kyber_mod_manager/utils/dll_injector.dart'; 10 | import 'package:kyber_mod_manager/utils/services/kyber_api_service.dart'; 11 | import 'package:kyber_mod_manager/utils/services/navigator_service.dart'; 12 | import 'package:kyber_mod_manager/utils/types/freezed/game_status.dart'; 13 | import 'package:kyber_mod_manager/utils/types/mode.dart'; 14 | import 'package:logging/logging.dart'; 15 | 16 | class RPCService { 17 | static late GameStatus _gameStatus; 18 | 19 | static final DiscordRPC rpc = DiscordRPC( 20 | applicationId: '931094111694520350', 21 | ); 22 | static bool _running = false; 23 | static StreamSubscription? _subscription; 24 | 25 | static void initialize() { 26 | if (_subscription != null) { 27 | _subscription!.cancel(); 28 | } 29 | 30 | DiscordRPC.initialize(); 31 | RPCService.start(); 32 | _gameStatus = BlocProvider.of(navigatorKey.currentContext!).state; 33 | _subscription = BlocProvider.of(navigatorKey.currentContext!).stream.listen((element) { 34 | RPCService._gameStatus = element; 35 | checkStatus(); 36 | }); 37 | } 38 | 39 | static void dispose() { 40 | if (!_running) { 41 | return; 42 | } 43 | 44 | Logger.root.info('Disposing rpc-service'); 45 | 46 | rpc.clearPresence(); 47 | _subscription?.cancel(); 48 | _running = false; 49 | } 50 | 51 | static void start() { 52 | if (box.containsKey('discordRPC') && !box.get('discordRPC') || !box.containsKey('discordRPC') || _running) { 53 | return; 54 | } 55 | rpc.start(autoRegister: true); 56 | _running = true; 57 | Logger.root.info('Started rpc-service'); 58 | } 59 | 60 | static void checkStatus() async { 61 | bool isRunning = DllInjector.isInjected(); 62 | if (!box.get('discordRPC')) { 63 | return dispose(); 64 | } 65 | 66 | if (isRunning && _gameStatus.started != null) { 67 | dynamic config = await KyberApiService.getCurrentConfig(); 68 | try { 69 | if (_gameStatus.server == null) { 70 | return; 71 | } 72 | 73 | dynamic map = maps.where((element) => element['map'] == _gameStatus.server?.map).first; 74 | Mode mode = modes.where((element) => element.mode == _gameStatus.server?.mode).first; 75 | 76 | if (!_running) { 77 | return; 78 | } 79 | 80 | rpc.updatePresence(DiscordPresence( 81 | details: _gameStatus.server!.name, 82 | state: '${config['KYBER_MODE'] == 'CLIENT' ? 'Playing' : 'Hosting'} ${mode.name} on ${map['name']} (${_gameStatus.server!.users}/${_gameStatus.server!.maxPlayers})', 83 | startTimeStamp: _gameStatus.started?.millisecondsSinceEpoch, 84 | largeImageText: 'Star Wars: Battlefront II', 85 | largeImageKey: 'bf2', 86 | smallImageText: map['name'], 87 | smallImageKey: 'test', 88 | button1Label: 'View Server', 89 | button1Url: 'https://kyber.gg/servers#id=${_gameStatus.server!.id}', 90 | button2Label: 'Join Server', 91 | button2Url: 'kmm://join_server?${_gameStatus.server!.id}', 92 | )); 93 | } catch (e) { 94 | return; 95 | } 96 | } else { 97 | rpc.clearPresence(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/utils/translation/translate_preferences.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter_translate/flutter_translate.dart'; 4 | import 'package:jiffy/jiffy.dart'; 5 | import 'package:kyber_mod_manager/main.dart'; 6 | import 'package:kyber_mod_manager/utils/app_locale.dart'; 7 | 8 | class TranslatePreferences extends ITranslatePreferences { 9 | @override 10 | Future getPreferredLocale() async { 11 | return AppLocale().getLocale(); 12 | } 13 | 14 | @override 15 | Future savePreferredLocale(Locale locale) async { 16 | await Jiffy.setLocale(locale.languageCode); 17 | await box.put('locale', locale.toString()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/utils/translation/translation_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | 3 | class TranslationDelegate extends LocalizationsDelegate { 4 | @override 5 | bool isSupported(Locale locale) => true; 6 | 7 | @override 8 | Future load(Locale locale) async => _TranslationDelegate(); 9 | 10 | @override 11 | bool shouldReload(covariant LocalizationsDelegate old) => false; 12 | } 13 | 14 | class _TranslationDelegate implements FluentLocalizations { 15 | @override 16 | String get backButtonTooltip => 'Back'; 17 | 18 | @override 19 | String get closeButtonLabel => 'Close'; 20 | 21 | @override 22 | String get searchLabel => 'Search'; 23 | 24 | @override 25 | String get closeNavigationTooltip => 'Close Navigation'; 26 | 27 | @override 28 | String get openNavigationTooltip => 'Open Navigation'; 29 | 30 | @override 31 | String get clickToSearch => 'Click to search'; 32 | 33 | @override 34 | String get modalBarrierDismissLabel => 'Dismiss'; 35 | 36 | @override 37 | String get minimizeWindowTooltip => 'Minimze'; 38 | 39 | @override 40 | String get restoreWindowTooltip => 'Restore'; 41 | 42 | @override 43 | String get closeWindowTooltip => 'Close'; 44 | 45 | @override 46 | String get dialogLabel => 'Dialog'; 47 | 48 | @override 49 | String get cutActionLabel => 'Cut'; 50 | 51 | @override 52 | String get copyActionLabel => 'Copy'; 53 | 54 | @override 55 | String get pasteActionLabel => 'Paste'; 56 | 57 | @override 58 | String get selectAllActionLabel => 'Select all'; 59 | 60 | @override 61 | String get newTabLabel => 'Add new tab'; 62 | 63 | @override 64 | String get closeTabLabel => 'Close tab (Ctrl+F4)'; 65 | 66 | @override 67 | String get scrollTabBackwardLabel => 'Scroll tab list backward'; 68 | 69 | @override 70 | String get scrollTabForwardLabel => 'Scroll tab list forward'; 71 | 72 | @override 73 | String get noResultsFoundLabel => 'No results found'; 74 | 75 | String get _ctrlCmd { 76 | return 'Ctrl'; 77 | } 78 | 79 | @override 80 | String get cutShortcut => '$_ctrlCmd+X'; 81 | 82 | @override 83 | String get copyShortcut => '$_ctrlCmd+C'; 84 | 85 | @override 86 | String get pasteShortcut => '$_ctrlCmd+V'; 87 | 88 | @override 89 | String get selectAllShortcut => '$_ctrlCmd+A'; 90 | 91 | @override 92 | String get copyActionTooltip => 'Copy the selected content to the clipboard'; 93 | 94 | @override 95 | String get cutActionTooltip => 'Remove the selected content and put it in the clipboard'; 96 | 97 | @override 98 | String get pasteActionTooltip => 'Inserts the contents of the clipboard at the current location'; 99 | 100 | @override 101 | String get selectAllActionTooltip => 'Select all content'; 102 | 103 | @override 104 | String get am => "Am"; 105 | 106 | @override 107 | String get closeTabLabelSuffix => "Close"; 108 | 109 | @override 110 | String get day => "-"; 111 | 112 | @override 113 | String get hour => "-"; 114 | 115 | @override 116 | String get localeName => "-"; 117 | 118 | String get minute => "-"; 119 | 120 | @override 121 | String get month => "-"; 122 | 123 | @override 124 | String get pm => "-"; 125 | 126 | @override 127 | String get year => "-"; 128 | } 129 | -------------------------------------------------------------------------------- /lib/utils/types/freezed/discord_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'discord_event.freezed.dart'; 4 | part 'discord_event.g.dart'; 5 | 6 | @freezed 7 | class DiscordEvent with _$DiscordEvent { 8 | const factory DiscordEvent({ 9 | String? id, 10 | @JsonKey(name: 'guild_id') String? guildId, 11 | @JsonKey(name: 'channel_id') dynamic? channelId, 12 | @JsonKey(name: 'creator_id') String? creatorId, 13 | String? name, 14 | String? description, 15 | dynamic? image, 16 | @JsonKey(name: 'scheduled_start_time') DateTime? scheduledStartTime, 17 | @JsonKey(name: 'scheduled_end_time') DateTime? scheduledEndTime, 18 | @JsonKey(name: 'privacy_level') int? privacyLevel, 19 | int? status, 20 | @JsonKey(name: 'entity_type') int? entityType, 21 | @JsonKey(name: 'entity_id') dynamic? entityId, 22 | @JsonKey(name: 'pentity_metadata') EntityMetadata? entityMetadata, 23 | @JsonKey(name: 'sku_ids') List? skuIds, 24 | Creator? creator, 25 | @JsonKey(name: 'user_count') int? userCount, 26 | }) = _DiscordEvent; 27 | 28 | factory DiscordEvent.fromJson(Map json) => _$DiscordEventFromJson(json); 29 | } 30 | 31 | @freezed 32 | class Creator with _$Creator { 33 | const factory Creator({ 34 | String? id, 35 | String? username, 36 | String? avatar, 37 | @JsonKey(name: 'avatar_decoration') dynamic? avatarDecoration, 38 | String? discriminator, 39 | @JsonKey(name: 'public_flags') int? publicFlags, 40 | }) = _Creator; 41 | 42 | factory Creator.fromJson(Map json) => _$CreatorFromJson(json); 43 | } 44 | 45 | @freezed 46 | class EntityMetadata with _$EntityMetadata { 47 | const factory EntityMetadata({ 48 | String? location, 49 | }) = _EntityMetadata; 50 | 51 | factory EntityMetadata.fromJson(Map json) => _$EntityMetadataFromJson(json); 52 | } 53 | -------------------------------------------------------------------------------- /lib/utils/types/freezed/frosty_collection.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:hive_flutter/hive_flutter.dart'; 3 | import 'package:kyber_mod_manager/utils/services/mod_service.dart'; 4 | import 'package:kyber_mod_manager/utils/types/freezed/mod.dart'; 5 | 6 | part 'frosty_collection.freezed.dart'; 7 | part 'frosty_collection.g.dart'; 8 | 9 | abstract class FrostyMod { 10 | String get name; 11 | 12 | String get version; 13 | 14 | String get filename; 15 | 16 | String? get description; 17 | } 18 | 19 | @HiveType(typeId: 6) 20 | @freezed 21 | class FrostyCollection with _$FrostyCollection { 22 | const FrostyCollection._(); 23 | 24 | String toKyberString() { 25 | return '$name ($version)'; 26 | } 27 | 28 | @Implements() 29 | const factory FrostyCollection({ 30 | @HiveField(0) required String link, 31 | @HiveField(1) required String title, 32 | @HiveField(2) required String version, 33 | @HiveField(3) required String description, 34 | @HiveField(4) required String category, 35 | @HiveField(5) required String name, 36 | @HiveField(6) required String filename, 37 | @HiveField(7) required List fileNames, 38 | @HiveField(8) required List modVersions, 39 | @HiveField(9) required String author, 40 | @HiveField(10) List? mods, 41 | }) = _FrostyCollection; 42 | 43 | @Implements() 44 | factory FrostyCollection.fromFile(String filename, dynamic json) { 45 | return FrostyCollection( 46 | link: json['link'], 47 | title: json['title'], 48 | version: json['version'], 49 | description: json['description'], 50 | category: json['category'], 51 | fileNames: List.from(json['mods']), 52 | filename: filename.split(r'\').last, 53 | name: json['title'], 54 | author: json['author'], 55 | modVersions: List.from(json['modVersions']), 56 | mods: List.from( 57 | json['mods'].map( 58 | (fileName) => ModService.mods.firstWhere( 59 | (element) => element.filename.endsWith(fileName), 60 | orElse: () => Mod.fromString(''), 61 | ), 62 | ), 63 | ), 64 | ); 65 | } 66 | 67 | @Implements() 68 | factory FrostyCollection.fromJson(Map json) => _$FrostyCollectionFromJson(json); 69 | } 70 | -------------------------------------------------------------------------------- /lib/utils/types/freezed/frosty_cubic_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:kyber_mod_manager/utils/types/freezed/frosty_version.dart'; 3 | import 'package:kyber_mod_manager/utils/types/freezed/github_asset.dart'; 4 | part 'frosty_cubic_state.freezed.dart'; 5 | 6 | @freezed 7 | class FrostyCubicState with _$FrostyCubicState { 8 | const factory FrostyCubicState({ 9 | required bool isOutdated, 10 | FrostyVersion? currentVersion, 11 | GitHubAsset? latestVersion, 12 | }) = _FrostyCubicState; 13 | } 14 | -------------------------------------------------------------------------------- /lib/utils/types/freezed/frosty_profile.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'frosty_profile.freezed.dart'; 4 | part 'frosty_profile.g.dart'; 5 | 6 | @freezed 7 | class FrostyProfile with _$FrostyProfile { 8 | factory FrostyProfile({required String name, required List mods}) = _FrostyProfile; 9 | 10 | factory FrostyProfile.fromJson(Map json) => _$FrostyProfileFromJson(json); 11 | } 12 | -------------------------------------------------------------------------------- /lib/utils/types/freezed/frosty_version.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'frosty_version.freezed.dart'; 4 | part 'frosty_version.g.dart'; 5 | 6 | @freezed 7 | abstract class FrostyVersion with _$FrostyVersion { 8 | const factory FrostyVersion({ 9 | required String version, 10 | required String hash, 11 | }) = _FrostyVersion; 12 | 13 | factory FrostyVersion.fromJson(Map json) => _$FrostyVersionFromJson(json); 14 | } 15 | -------------------------------------------------------------------------------- /lib/utils/types/freezed/game_status.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:kyber_mod_manager/utils/types/freezed/kyber_server.dart'; 3 | import 'package:kyber_mod_manager/utils/types/process_details.dart'; 4 | 5 | part 'game_status.freezed.dart'; 6 | 7 | @freezed 8 | class GameStatus with _$GameStatus { 9 | factory GameStatus({required bool injected, required bool running, DateTime? started, KyberServer? server, ProcessModules? processModules}) = _GameStatus; 10 | } 11 | -------------------------------------------------------------------------------- /lib/utils/types/freezed/github_asset.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'github_asset.freezed.dart'; 4 | part 'github_asset.g.dart'; 5 | 6 | @freezed 7 | abstract class GitHubAsset with _$GitHubAsset { 8 | const factory GitHubAsset({ 9 | required String url, 10 | required int id, 11 | required String node_id, 12 | required String name, 13 | required dynamic label, 14 | required String content_type, 15 | required String state, 16 | required int size, 17 | required int download_count, 18 | required DateTime created_at, 19 | required DateTime updated_at, 20 | required String version, 21 | required String browser_download_url, 22 | }) = _GitHubAsset; 23 | 24 | factory GitHubAsset.fromJson(Map json) => _$GitHubAssetFromJson(json); 25 | } 26 | -------------------------------------------------------------------------------- /lib/utils/types/freezed/kyber_server.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'kyber_server.freezed.dart'; 4 | part 'kyber_server.g.dart'; 5 | 6 | @freezed 7 | abstract class KyberServer with _$KyberServer { 8 | const factory KyberServer({ 9 | required String id, 10 | required String name, 11 | required String description, 12 | required String map, 13 | required String mode, 14 | required List mods, 15 | required int users, 16 | required String host, 17 | required int maxPlayers, 18 | required bool autoBalanceTeams, 19 | required int startedAt, 20 | required String startedAtPretty, 21 | required bool requiresPassword, 22 | bool? official, 23 | required String region, 24 | required Proxy proxy, 25 | }) = _KyberServer; 26 | 27 | factory KyberServer.fromJson(Map json) => _$KyberServerFromJson(json); 28 | } 29 | 30 | @freezed 31 | class KyberServerMod with _$KyberServerMod { 32 | const factory KyberServerMod({ 33 | required String name, 34 | required String link, 35 | }) = _KyberServerMod; 36 | 37 | factory KyberServerMod.fromJson(Map json) => _$KyberServerModFromJson(json); 38 | } 39 | 40 | @freezed 41 | abstract class Proxy with _$Proxy { 42 | const factory Proxy({ 43 | required String ip, 44 | required String name, 45 | required String flag, 46 | }) = _Proxy; 47 | 48 | factory Proxy.fromJson(Map json) => _$ProxyFromJson(json); 49 | } 50 | -------------------------------------------------------------------------------- /lib/utils/types/freezed/mod.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:hive_flutter/hive_flutter.dart'; 5 | import 'package:kyber_mod_manager/utils/types/freezed/frosty_collection.dart'; 6 | 7 | part 'mod.freezed.dart'; 8 | part 'mod.g.dart'; 9 | 10 | @HiveType(typeId: 2) 11 | @freezed 12 | class Mod with _$Mod { 13 | const Mod._(); 14 | 15 | String toKyberString() { 16 | return '$name ($version)'; 17 | } 18 | 19 | @Implements() 20 | factory Mod({ 21 | @HiveField(0) required String name, 22 | @HiveField(1) required String filename, 23 | @HiveField(2) required String category, 24 | @HiveField(3) required String version, 25 | @HiveField(4) String? author, 26 | @HiveField(5) String? description, 27 | }) = _Mod; 28 | 29 | @Implements() 30 | factory Mod.fromJson(Map json) => _$ModFromJson(json); 31 | 32 | @Implements() 33 | factory Mod.fromString(String filename, [String? data]) { 34 | List formatted = data != null ? Uri.encodeComponent(data).split('%00') : ['Invalid', 'Unknown', 'Unknown', 'Unknown', 'Unknown']; 35 | return Mod( 36 | name: Uri.decodeComponent(formatted[0]), 37 | author: Uri.decodeComponent(formatted[1]), 38 | filename: filename.split('\\').last, 39 | category: Uri.decodeComponent(formatted[2]), 40 | version: Uri.decodeComponent(formatted[3]), 41 | description: Uri.decodeComponent(formatted[4]), 42 | ); 43 | } 44 | 45 | @Implements() 46 | factory Mod.fromBytes(String filename, [List? data]) { 47 | List modData = []; 48 | int lastIndex = 0; 49 | for (int i = 0; i != data?.length; i++) { 50 | if (modData.length > 5) break; 51 | if (data?[i] != 0x00) continue; 52 | modData.add(utf8.decode(data?.getRange(lastIndex == 0 ? 0 : lastIndex + 1, i).toList() ?? [], allowMalformed: true)); 53 | lastIndex = i; 54 | } 55 | return Mod( 56 | name: modData[0], 57 | author: modData[1], 58 | filename: filename.split('\\').last, 59 | category: modData[2], 60 | version: modData[3], 61 | description: modData.length > 4 ? modData[4] : null, 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/utils/types/freezed/mod_profile.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:hive_flutter/hive_flutter.dart'; 3 | 4 | part 'mod_profile.freezed.dart'; 5 | part 'mod_profile.g.dart'; 6 | 7 | @HiveType(typeId: 1) 8 | @freezed 9 | class ModProfile with _$ModProfile { 10 | factory ModProfile({ 11 | @HiveField(0) required String name, 12 | @HiveField(1) required List mods, 13 | @HiveField(2) String? description, 14 | }) = _ModProfile; 15 | 16 | factory ModProfile.fromJson(Map json) => _$ModProfileFromJson(json); 17 | } 18 | -------------------------------------------------------------------------------- /lib/utils/types/freezed/nexus_mods_search_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'nexus_mods_search_result.freezed.dart'; 4 | part 'nexus_mods_search_result.g.dart'; 5 | 6 | @freezed 7 | abstract class NexusModsSearchResult with _$NexusModsSearchResult { 8 | const factory NexusModsSearchResult({ 9 | required List terms, 10 | required List exclude_authors, 11 | required List exclude_tags, 12 | required bool include_adult, 13 | required int took, 14 | required int total, 15 | required List results, 16 | }) = _NexusModsSearchResult; 17 | 18 | factory NexusModsSearchResult.fromJson(Map json) => _$NexusModsSearchResultFromJson(json); 19 | } 20 | 21 | @freezed 22 | abstract class Result with _$Result { 23 | const factory Result({ 24 | required String name, 25 | required int downloads, 26 | required int endorsements, 27 | required String url, 28 | required String image, 29 | required String username, 30 | required int user_id, 31 | required String game_name, 32 | required int game_id, 33 | required int mod_id, 34 | }) = _Result; 35 | 36 | factory Result.fromJson(Map json) => _$ResultFromJson(json); 37 | } 38 | -------------------------------------------------------------------------------- /lib/utils/types/frosty_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | FrostyConfig frostyConfigFromJson(String str) => FrostyConfig.fromJson(json.decode(str)); 4 | 5 | String frostyConfigToJson(FrostyConfig data) => json.encode(data.toJson()); 6 | 7 | class FrostyConfig { 8 | FrostyConfig({ 9 | required this.games, 10 | required this.globalOptions, 11 | }); 12 | 13 | Map games; 14 | GlobalOptions globalOptions; 15 | 16 | factory FrostyConfig.fromJson(Map json) => FrostyConfig( 17 | games: Map.from(json["Games"]).map((k, v) => MapEntry(k, Game.fromJson(v))), 18 | globalOptions: GlobalOptions.fromJson(json["GlobalOptions"]), 19 | ); 20 | 21 | Map toJson() => { 22 | "Games": Map.from(games).map((k, v) => MapEntry(k, v.toJson())), 23 | "GlobalOptions": globalOptions.toJson(), 24 | }; 25 | } 26 | 27 | class Game { 28 | Game({ 29 | required this.gamePath, 30 | required this.bookmarkDb, 31 | required this.options, 32 | this.packs, 33 | }); 34 | 35 | String gamePath; 36 | String bookmarkDb; 37 | Options options; 38 | Map? packs; 39 | 40 | factory Game.fromJson(Map json) => Game( 41 | gamePath: json["GamePath"], 42 | bookmarkDb: json["BookmarkDb"], 43 | options: Options.fromJson(json["Options"]), 44 | packs: Map.from(json['Packs']), 45 | ); 46 | 47 | Map toJson() => { 48 | "GamePath": gamePath, 49 | "BookmarkDb": bookmarkDb, 50 | "Options": options.toJson(), 51 | "Packs": packs, 52 | }; 53 | } 54 | 55 | class Options { 56 | Options({ 57 | this.selectedPack, 58 | this.commandLineArgs, 59 | this.platform, 60 | this.platformLaunchingEnabled, 61 | }); 62 | 63 | String? selectedPack; 64 | String? commandLineArgs; 65 | String? platform; 66 | bool? platformLaunchingEnabled; 67 | 68 | factory Options.fromJson(Map json) => Options( 69 | selectedPack: json["SelectedPack"], 70 | commandLineArgs: json["CommandLineArgs"], 71 | platform: json["Platform"], 72 | platformLaunchingEnabled: json["PlatformLaunchingEnabled"], 73 | ); 74 | 75 | Map toJson() => { 76 | "SelectedPack": selectedPack, 77 | "CommandLineArgs": commandLineArgs, 78 | "Platform": platform, 79 | "PlatformLaunchingEnabled": platformLaunchingEnabled, 80 | }; 81 | } 82 | 83 | class GlobalOptions { 84 | GlobalOptions({ 85 | required this.useDefaultProfile, 86 | required this.defaultProfile, 87 | }); 88 | 89 | bool? useDefaultProfile; 90 | String? defaultProfile; 91 | 92 | factory GlobalOptions.fromJson(Map json) => GlobalOptions( 93 | useDefaultProfile: json["UseDefaultProfile"], 94 | defaultProfile: json["DefaultProfile"], 95 | ); 96 | 97 | Map toJson() => { 98 | "UseDefaultProfile": useDefaultProfile, 99 | "DefaultProfile": defaultProfile, 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /lib/utils/types/map.dart: -------------------------------------------------------------------------------- 1 | class KyberMap { 2 | KyberMap({ 3 | required this.map, 4 | required this.name, 5 | }); 6 | 7 | String map; 8 | String name; 9 | 10 | factory KyberMap.fromJson(Map json) => KyberMap( 11 | map: json["map"], 12 | name: json["name"], 13 | ); 14 | 15 | Map toJson() => { 16 | "map": map, 17 | "name": name, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /lib/utils/types/mod_info.dart: -------------------------------------------------------------------------------- 1 | class ModInfo { 2 | String name; 3 | String version; 4 | 5 | @override 6 | toString() => '$name ($version)'; 7 | 8 | ModInfo({required this.name, required this.version}); 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/types/mode.dart: -------------------------------------------------------------------------------- 1 | List modesFromJson(dynamic data) => List.from(data.map((x) => Mode.fromJson(x))); 2 | 3 | class Mode { 4 | Mode({ 5 | required this.mode, 6 | required this.name, 7 | required this.maps, 8 | this.mapOverrides, 9 | }); 10 | 11 | String mode; 12 | String name; 13 | List maps; 14 | List? mapOverrides; 15 | 16 | factory Mode.fromJson(Map json) => Mode( 17 | mode: json["mode"], 18 | name: json["name"], 19 | maps: List.from(json["maps"].map((x) => x)), 20 | mapOverrides: json["mapOverrides"] == null ? null : List.from(json["mapOverrides"].map((x) => MapOverride.fromJson(x))), 21 | ); 22 | 23 | Map toJson() => { 24 | "mode": mode, 25 | "name": name, 26 | "maps": List.from(maps.map((x) => x)), 27 | "mapOverrides": mapOverrides == null ? null : List.from(mapOverrides?.map((x) => x.toJson()) ?? []), 28 | }; 29 | } 30 | 31 | class MapOverride { 32 | MapOverride({ 33 | required this.map, 34 | required this.name, 35 | }); 36 | 37 | String map; 38 | String name; 39 | 40 | factory MapOverride.fromJson(Map json) => MapOverride( 41 | map: json["map"], 42 | name: json["name"], 43 | ); 44 | 45 | Map toJson() => { 46 | "map": map, 47 | "name": name, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /lib/utils/types/pack_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_translate/flutter_translate.dart'; 2 | 3 | enum PackType { 4 | NO_MODS, 5 | FROSTY_PACK, 6 | MOD_PROFILE, 7 | COSMETICS, 8 | } 9 | 10 | PackType getPackType(String packType) { 11 | if (packType.contains('Frosty Pack')) { 12 | return PackType.FROSTY_PACK; 13 | } else if (packType.contains('Mod Profile')) { 14 | return PackType.MOD_PROFILE; 15 | } else if (packType.endsWith('Cosmetics') || packType.endsWith(translate('host_server.forms.cosmetic_mods.header'))) { 16 | return PackType.COSMETICS; 17 | } else if (packType.endsWith(translate('host_server.forms.mod_profile.no_mods_profile'))) { 18 | return PackType.NO_MODS; 19 | } 20 | throw Exception('Invalid pack type: $packType'); 21 | } 22 | 23 | extension PackTypeExtesion on PackType { 24 | String get name { 25 | switch (this) { 26 | case PackType.FROSTY_PACK: 27 | return '(Frosty Pack)'; 28 | case PackType.MOD_PROFILE: 29 | return '(Mod Profile)'; 30 | case PackType.COSMETICS: 31 | return 'Cosmetics'; 32 | case PackType.NO_MODS: 33 | return translate('host_server.forms.mod_profile.no_mods_profile'); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/utils/types/process_details.dart: -------------------------------------------------------------------------------- 1 | class ProcessDetails { 2 | ProcessDetails(this.pid, this.name, String memory) { 3 | _memory = int.tryParse(memory) ?? 0; 4 | } 5 | 6 | final int pid; 7 | 8 | final String name; 9 | 10 | late final int _memory; 11 | 12 | late final String? memoryUnits; 13 | 14 | int get memory => _memory; 15 | } 16 | 17 | class ProcessModules { 18 | ProcessModules({required this.modulesLength, required this.modules}); 19 | 20 | final int modulesLength; 21 | final List modules; 22 | } 23 | -------------------------------------------------------------------------------- /lib/utils/types/saved_profile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:kyber_mod_manager/utils/types/freezed/frosty_collection.dart'; 4 | import 'package:kyber_mod_manager/utils/types/freezed/mod.dart'; 5 | 6 | SavedProfile savedProfileFromJson(String str) => SavedProfile.fromJson(json.decode(str)); 7 | 8 | String savedProfileToJson(List data) => json.encode(List.from(data.map((x) => x.toJson()))); 9 | 10 | class SavedProfile { 11 | SavedProfile({required this.mods, required this.path, required this.id, required this.size, this.lastUsed}); 12 | 13 | List mods; 14 | String path; 15 | String id; 16 | DateTime? lastUsed; 17 | int size; 18 | 19 | @override 20 | String toString() { 21 | return 'SavedProfile(mods: $mods, path: $path, id: $id, size: $size, lastUsed: $lastUsed)'; 22 | } 23 | 24 | factory SavedProfile.fromJson(Map json) => SavedProfile( 25 | mods: List.from(json["mods"].map((x) => x['fileNames'] != null ? FrostyCollection.fromJson(x) : Mod.fromJson(x))), 26 | path: json["path"], 27 | id: json["id"], 28 | size: json["size"], 29 | lastUsed: json['lastUsed'] != null ? DateTime.parse(json['lastUsed']) : null); 30 | 31 | Map toJson() => {"mods": List.from(mods.map((x) => x.toJson())), "path": path, "id": id, "size": size, "lastUsed": lastUsed?.toString()}; 32 | } 33 | -------------------------------------------------------------------------------- /lib/widgets/button_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | 3 | class ButtonText extends StatelessWidget { 4 | const ButtonText({Key? key, required this.text, required this.icon}) : super(key: key); 5 | 6 | final Icon icon; 7 | final Text text; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Row( 12 | mainAxisSize: MainAxisSize.min, 13 | children: [ 14 | icon, 15 | const SizedBox(width: 8), 16 | text, 17 | ], 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/widgets/custom_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | 3 | class CustomFilledButton extends StatelessWidget { 4 | const CustomFilledButton({Key? key, required this.child, this.color, required this.onPressed, this.disabled = false}) : super(key: key); 5 | final Widget child; 6 | final Color? color; 7 | final VoidCallback? onPressed; 8 | final bool disabled; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return FilledButton( 13 | onPressed: disabled ? null : onPressed, 14 | style: ButtonStyle( 15 | backgroundColor: color != null 16 | ? ButtonState.resolveWith((states) { 17 | var accentColor = color!.toAccentColor(); 18 | if (states.isDisabled) { 19 | return FluentTheme.of(context).inactiveColor; 20 | } else if (states.isPressing) { 21 | return accentColor.darker; 22 | } else if (states.isHovering) { 23 | return accentColor.dark; 24 | } 25 | return color; 26 | }) 27 | : null, 28 | ), 29 | child: child, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/widgets/custom_tooltip.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | 3 | class CustomTooltip extends StatelessWidget { 4 | const CustomTooltip({Key? key, required this.message}) : super(key: key); 5 | 6 | final String message; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Tooltip( 11 | style: const TooltipThemeData( 12 | padding: EdgeInsets.all(8), 13 | ), 14 | message: message, 15 | child: const Icon( 16 | FluentIcons.status_circle_question_mark, 17 | size: 22, 18 | ), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/widgets/unordered_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluent_ui/fluent_ui.dart'; 2 | 3 | class UnorderedList extends StatelessWidget { 4 | const UnorderedList(this.texts, {this.textStyle, this.mainAxisAlignment, this.crossAxisAlignment}); 5 | 6 | final List texts; 7 | final MainAxisAlignment? mainAxisAlignment; 8 | final CrossAxisAlignment? crossAxisAlignment; 9 | final TextStyle? textStyle; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | var widgetList = []; 14 | for (var text in texts) { 15 | widgetList.add(_UnorderedListItem(text, textStyle ?? const TextStyle(), crossAxisAlignment ?? CrossAxisAlignment.start, mainAxisAlignment ?? MainAxisAlignment.start)); 16 | widgetList.add(const SizedBox(height: 5.0)); 17 | } 18 | 19 | return Column( 20 | mainAxisAlignment: mainAxisAlignment ?? MainAxisAlignment.start, 21 | crossAxisAlignment: crossAxisAlignment ?? CrossAxisAlignment.start, 22 | children: widgetList, 23 | ); 24 | } 25 | } 26 | 27 | class _UnorderedListItem extends StatelessWidget { 28 | const _UnorderedListItem(this.text, this.textStyle, this.crossAxisAlignment, this.mainAxisAlignment); 29 | 30 | final String text; 31 | final CrossAxisAlignment crossAxisAlignment; 32 | final MainAxisAlignment mainAxisAlignment; 33 | final TextStyle textStyle; 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return Row( 38 | mainAxisAlignment: mainAxisAlignment, 39 | crossAxisAlignment: crossAxisAlignment, 40 | children: [ 41 | const Text("• "), 42 | Expanded( 43 | child: Text(text, style: textStyle.copyWith(fontSize: 15), overflow: TextOverflow.ellipsis), 44 | ) 45 | ], 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/dynamic_env/.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 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | -------------------------------------------------------------------------------- /packages/dynamic_env/.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. 5 | 6 | version: 7 | revision: cd41fdd495f6944ecd3506c21e94c6567b073278 8 | channel: stable 9 | 10 | project_type: plugin 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 17 | base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 18 | - platform: windows 19 | create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 20 | base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278 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 | -------------------------------------------------------------------------------- /packages/dynamic_env/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /packages/dynamic_env/lib/dynamic_env.dart: -------------------------------------------------------------------------------- 1 | import 'dynamic_env_platform_interface.dart'; 2 | 3 | class DynamicEnv { 4 | Future setEnv(int pid, String name, String value) async { 5 | return DynamicEnvPlatform.instance.setEnv(pid, name, value); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/dynamic_env/lib/dynamic_env_method_channel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | import 'dynamic_env_platform_interface.dart'; 5 | 6 | class MethodChannelDynamicEnv extends DynamicEnvPlatform { 7 | final methodChannel = const MethodChannel('dynamic_env'); 8 | 9 | @override 10 | Future setEnv(int pid, String name, String value) async { 11 | await methodChannel.invokeMethod( 12 | 'setEnv', 13 | { 14 | 'proc': pid.toString(), 15 | 'name': name, 16 | 'value': value, 17 | }, 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/dynamic_env/lib/dynamic_env_platform_interface.dart: -------------------------------------------------------------------------------- 1 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 2 | 3 | import 'dynamic_env_method_channel.dart'; 4 | 5 | abstract class DynamicEnvPlatform extends PlatformInterface { 6 | DynamicEnvPlatform() : super(token: _token); 7 | 8 | static final Object _token = Object(); 9 | 10 | static DynamicEnvPlatform _instance = MethodChannelDynamicEnv(); 11 | 12 | static DynamicEnvPlatform get instance => _instance; 13 | 14 | static set instance(DynamicEnvPlatform instance) { 15 | PlatformInterface.verifyToken(instance, _token); 16 | _instance = instance; 17 | } 18 | 19 | Future setEnv(int pid, String name, String value) { 20 | throw UnimplementedError('setEnv() has not been implemented.'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/dynamic_env/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dynamic_env 2 | description: A new Flutter plugin project. 3 | version: 0.0.1 4 | homepage: 5 | 6 | environment: 7 | sdk: ">=2.17.3 <3.0.0" 8 | flutter: ">=2.5.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | plugin_platform_interface: ^2.0.2 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | flutter_lints: ^2.0.0 19 | 20 | flutter: 21 | plugin: 22 | platforms: 23 | windows: 24 | pluginClass: DynamicEnvPluginCApi 25 | -------------------------------------------------------------------------------- /packages/dynamic_env/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ 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 | -------------------------------------------------------------------------------- /packages/dynamic_env/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # The Flutter tooling requires that developers have a version of Visual Studio 2 | # installed that includes CMake 3.14 or later. You should not increase this 3 | # version, as doing so will cause the plugin to fail to compile for some 4 | # customers of the plugin. 5 | cmake_minimum_required(VERSION 3.14) 6 | 7 | # Project-level configuration. 8 | set(PROJECT_NAME "dynamic_env") 9 | project(${PROJECT_NAME} LANGUAGES CXX) 10 | 11 | # This value is used when generating builds using this plugin, so it must 12 | # not be changed 13 | set(PLUGIN_NAME "dynamic_env_plugin") 14 | 15 | # Any new source files that you add to the plugin should be added here. 16 | list(APPEND PLUGIN_SOURCES 17 | "dynamic_env_plugin.cpp" 18 | "dynamic_env_plugin.h" 19 | ) 20 | 21 | # Define the plugin library target. Its name must not be changed (see comment 22 | # on PLUGIN_NAME above). 23 | add_library(${PLUGIN_NAME} SHARED 24 | "include/dynamic_env/dynamic_env_plugin_c_api.h" 25 | "dynamic_env_plugin_c_api.cpp" 26 | ${PLUGIN_SOURCES} 27 | ) 28 | 29 | # Apply a standard set of build settings that are configured in the 30 | # application-level CMakeLists.txt. This can be removed for plugins that want 31 | # full control over build settings. 32 | apply_standard_settings(${PLUGIN_NAME}) 33 | 34 | # Symbols are hidden by default to reduce the chance of accidental conflicts 35 | # between plugins. This should not be removed; any symbols that should be 36 | # exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. 37 | set_target_properties(${PLUGIN_NAME} PROPERTIES 38 | CXX_VISIBILITY_PRESET hidden) 39 | target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) 40 | 41 | # Source include directories and library dependencies. Add any plugin-specific 42 | # dependencies here. 43 | target_include_directories(${PLUGIN_NAME} INTERFACE 44 | "${CMAKE_CURRENT_SOURCE_DIR}/include") 45 | target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) 46 | 47 | # List of absolute paths to libraries that should be bundled with the plugin. 48 | # This list could contain prebuilt libraries, or libraries created by an 49 | # external build triggered from this build file. 50 | set(dynamic_env_bundled_libraries 51 | "" 52 | PARENT_SCOPE 53 | ) 54 | -------------------------------------------------------------------------------- /packages/dynamic_env/windows/dynamic_env_plugin.cpp: -------------------------------------------------------------------------------- 1 | #include "dynamic_env_plugin.h" 2 | 3 | // This must be included before many other Windows headers. 4 | #include 5 | 6 | // For getPlatformVersion; remove unless needed for your plugin implementation. 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace dynamic_env { 19 | 20 | // static 21 | void DynamicEnvPlugin::RegisterWithRegistrar( 22 | flutter::PluginRegistrarWindows *registrar) { 23 | auto channel = 24 | std::make_unique>( 25 | registrar->messenger(), "dynamic_env", 26 | &flutter::StandardMethodCodec::GetInstance()); 27 | 28 | auto plugin = std::make_unique(); 29 | 30 | channel->SetMethodCallHandler( 31 | [plugin_pointer = plugin.get()](const auto &call, auto result) { 32 | plugin_pointer->HandleMethodCall(call, std::move(result)); 33 | }); 34 | 35 | registrar->AddPlugin(std::move(plugin)); 36 | } 37 | 38 | DynamicEnvPlugin::DynamicEnvPlugin() {} 39 | 40 | DynamicEnvPlugin::~DynamicEnvPlugin() {} 41 | 42 | void DynamicEnvPlugin::HandleMethodCall( 43 | const flutter::MethodCall &method_call, 44 | std::unique_ptr> result) { 45 | if (method_call.method_name().compare("setEnv") == 0) { 46 | const auto *arguments = std::get_if(method_call.arguments()); 47 | auto proc = arguments->find(flutter::EncodableValue("proc")); 48 | auto name = arguments->find(flutter::EncodableValue("name")); 49 | auto value = arguments->find(flutter::EncodableValue("value")); 50 | SetEnv(std::get(proc->second).c_str(), std::get(name->second).c_str(), std::get(value->second).c_str(), std::move(result)); 51 | } else { 52 | result->NotImplemented(); 53 | } 54 | } 55 | 56 | // Credits to BattleDash 57 | void DynamicEnvPlugin::SetEnv(const char *processID, const char *name, const char *value, std::unique_ptr> result) { 58 | DWORD pid = atoi(processID); 59 | HANDLE hProcessOrigin = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); 60 | char payload[256]; 61 | snprintf(payload, sizeof(payload), "%s=%s", name, value); 62 | LPVOID lpMemory = VirtualAllocEx(hProcessOrigin, NULL, strlen(payload) + 1, 63 | MEM_COMMIT, PAGE_READWRITE); 64 | WriteProcessMemory(hProcessOrigin, lpMemory, payload, strlen(payload) + 1, 65 | NULL); 66 | HANDLE hThread = 67 | CreateRemoteThread(hProcessOrigin, NULL, 0, 68 | (LPTHREAD_START_ROUTINE)GetProcAddress( 69 | LoadLibrary(L"ucrtbase.dll"), "_putenv"), 70 | lpMemory, 0, NULL); 71 | WaitForSingleObject(hThread, INFINITE); 72 | VirtualFreeEx(hProcessOrigin, lpMemory, 0, MEM_RELEASE); 73 | CloseHandle(hThread); 74 | CloseHandle(hProcessOrigin); 75 | result->Success(); 76 | } 77 | 78 | } // namespace dynamic_env 79 | -------------------------------------------------------------------------------- /packages/dynamic_env/windows/dynamic_env_plugin.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_PLUGIN_DYNAMIC_ENV_PLUGIN_H_ 2 | #define FLUTTER_PLUGIN_DYNAMIC_ENV_PLUGIN_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace dynamic_env { 10 | 11 | class DynamicEnvPlugin : public flutter::Plugin { 12 | public: 13 | static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); 14 | 15 | DynamicEnvPlugin(); 16 | 17 | virtual ~DynamicEnvPlugin(); 18 | 19 | DynamicEnvPlugin(const DynamicEnvPlugin&) = delete; 20 | DynamicEnvPlugin& operator=(const DynamicEnvPlugin&) = delete; 21 | 22 | private: 23 | void HandleMethodCall( 24 | const flutter::MethodCall &method_call, 25 | std::unique_ptr> result); 26 | void SetEnv(const char *procesId, const char *name, const char *value, std::unique_ptr> result); 27 | }; 28 | 29 | } // namespace dynamic_env 30 | 31 | #endif // FLUTTER_PLUGIN_DYNAMIC_ENV_PLUGIN_H_ 32 | -------------------------------------------------------------------------------- /packages/dynamic_env/windows/dynamic_env_plugin_c_api.cpp: -------------------------------------------------------------------------------- 1 | #include "include/dynamic_env/dynamic_env_plugin_c_api.h" 2 | 3 | #include 4 | 5 | #include "dynamic_env_plugin.h" 6 | 7 | void DynamicEnvPluginCApiRegisterWithRegistrar( 8 | FlutterDesktopPluginRegistrarRef registrar) { 9 | dynamic_env::DynamicEnvPlugin::RegisterWithRegistrar( 10 | flutter::PluginRegistrarManager::GetInstance() 11 | ->GetRegistrar(registrar)); 12 | } 13 | -------------------------------------------------------------------------------- /packages/dynamic_env/windows/include/dynamic_env/dynamic_env_plugin_c_api.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_PLUGIN_DYNAMIC_ENV_PLUGIN_C_API_H_ 2 | #define FLUTTER_PLUGIN_DYNAMIC_ENV_PLUGIN_C_API_H_ 3 | 4 | #include 5 | 6 | #ifdef FLUTTER_PLUGIN_IMPL 7 | #define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) 8 | #else 9 | #define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) 10 | #endif 11 | 12 | #if defined(__cplusplus) 13 | extern "C" { 14 | #endif 15 | 16 | FLUTTER_PLUGIN_EXPORT void DynamicEnvPluginCApiRegisterWithRegistrar( 17 | FlutterDesktopPluginRegistrarRef registrar); 18 | 19 | #if defined(__cplusplus) 20 | } // extern "C" 21 | #endif 22 | 23 | #endif // FLUTTER_PLUGIN_DYNAMIC_ENV_PLUGIN_C_API_H_ 24 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: kyber_mod_manager 2 | description: Kyber Mod Manager 3 | publish_to: 'none' 4 | version: 1.0.11 5 | 6 | environment: 7 | sdk: '>=3.0.3 <4.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | flutter_localizations: 13 | sdk: flutter 14 | flutter_svg: ^2.0.6 15 | dio: ^5.0.2 16 | uuid: ^3.0.5 17 | process_run: ^0.13.0 18 | sentry_flutter: ^7.7.0 19 | crypto: ^3.0.2 20 | window_manager: ^0.3.1 21 | windows_taskbar: ^1.1.0 22 | flutter_fadein: ^2.0.0 23 | url_launcher: ^6.1.5 24 | html: ^0.15.0 25 | intl: ^0.18.0 26 | flutter_platform_alert: ^0.4.0 27 | dio_cache_interceptor: ^3.2.7 28 | dio_cache_interceptor_hive_store: ^3.1.1 29 | package_info_plus: ^4.0.2 30 | system_info2: ^4.0.0 31 | auto_update: ^0.0.4 32 | flutter_markdown: ^0.6.10+3 33 | logging: ^1.0.2 34 | version: ^3.0.0 35 | flutter_bloc: ^8.0.1 36 | path: ^1.8.1 37 | path_provider: ^2.0.9 38 | desktop_drop: ^0.4.1 39 | archive: ^3.3.1 40 | auto_size_text: ^3.0.0 41 | fluent_ui: ^4.0.2 42 | system_tray: ^2.0.1 43 | tinycolor2: ^3.0.1 44 | http: ^1.0.0 45 | system_theme: ^2.0.0 46 | hive: ^2.0.6 47 | hive_flutter: 48 | json_annotation: 49 | flutter_acrylic: ^1.0.0+2 50 | puppeteer: ^3.0.0 51 | jiffy: ^6.2.1 52 | win32: ^5.0.3 53 | quiver: ^3.1.0 54 | ffi: ^1.0.0 55 | freezed_annotation: 56 | syncfusion_flutter_charts: ^21.2.9 57 | influxdb_client: ^2.5.0 58 | protocol_handler: ^0.1.4 59 | dynamic_env: 60 | path: 'packages/dynamic_env' 61 | flutter_translate: 62 | git: 63 | url: https://github.com/Jesway/Flutter-Translate.git 64 | bot_toast: 65 | git: 66 | url: https://github.com/MMMzq/bot_toast.git 67 | webview_windows: 68 | git: 69 | url: https://github.com/7reax/flutter-webview-windows 70 | dart_discord_rpc: 71 | git: 72 | url: https://github.com/alexmercerind/dart_discord_rpc.git 73 | ref: harmonoid 74 | linkable: ^3.0.1 75 | go_router: ^8.0.5 76 | file_selector: ^1.0.0 77 | async: ^2.11.0 78 | 79 | dependency_overrides: 80 | win32_registry: 81 | git: 82 | url: https://github.com/dart-windows/win32_registry.git 83 | ffi: ^2.0.0 84 | 85 | dev_dependencies: 86 | hive_generator: ^2.0.0 87 | build_runner: 88 | freezed: 89 | json_serializable: 90 | flutter_lints: ^2.0.1 91 | 92 | flutter: 93 | uses-material-design: true 94 | assets: 95 | - assets/app_icon.ico 96 | - assets/flags/ 97 | - assets/i18n/ 98 | -------------------------------------------------------------------------------- /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/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(kyber_mod_manager LANGUAGES CXX) 3 | 4 | set(BINARY_NAME "kyber_mod_manager") 5 | 6 | cmake_policy(SET CMP0063 NEW) 7 | 8 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 9 | 10 | # Configure build options. 11 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 12 | if(IS_MULTICONFIG) 13 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 14 | CACHE STRING "" FORCE) 15 | else() 16 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 17 | set(CMAKE_BUILD_TYPE "Debug" CACHE 18 | STRING "Flutter build mode" FORCE) 19 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 20 | "Debug" "Profile" "Release") 21 | endif() 22 | endif() 23 | 24 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 25 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 26 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 27 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 28 | 29 | # Use Unicode for all projects. 30 | add_definitions(-DUNICODE -D_UNICODE) 31 | 32 | # Compilation settings that should be applied to most targets. 33 | function(APPLY_STANDARD_SETTINGS TARGET) 34 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 35 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 36 | target_compile_options(${TARGET} PRIVATE /EHsc) 37 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 38 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 39 | endfunction() 40 | 41 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 42 | 43 | # Flutter library and tool build rules. 44 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 45 | 46 | # Application build 47 | add_subdirectory("runner") 48 | 49 | # Generated plugin build rules, which manage building the plugins and adding 50 | # them to the application. 51 | include(flutter/generated_plugins.cmake) 52 | 53 | 54 | # === Installation === 55 | # Support files are copied into place next to the executable, so that it can 56 | # run in place. This is done instead of making a separate bundle (as on Linux) 57 | # so that building and running from within Visual Studio will work. 58 | set(BUILD_BUNDLE_DIR "$") 59 | # Make the "install" step default, as it's required to run. 60 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 61 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 62 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 63 | endif() 64 | 65 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 66 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 67 | 68 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 69 | COMPONENT Runtime) 70 | 71 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 72 | COMPONENT Runtime) 73 | 74 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 75 | COMPONENT Runtime) 76 | 77 | if(PLUGIN_BUNDLED_LIBRARIES) 78 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 79 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 80 | COMPONENT Runtime) 81 | endif() 82 | 83 | # Fully re-copy the assets directory on each build to avoid having stale files 84 | # from a previous install. 85 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 86 | install(CODE " 87 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 88 | " COMPONENT Runtime) 89 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 90 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 91 | 92 | # Install the AOT library on non-Debug builds only. 93 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 94 | CONFIGURATIONS Profile;Release 95 | COMPONENT Runtime) 96 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | 3 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 4 | 5 | # Configuration provided via flutter tool. 6 | include(${EPHEMERAL_DIR}/generated_config.cmake) 7 | 8 | # TODO: Move the rest of this into files in ephemeral. See 9 | # https://github.com/flutter/flutter/issues/57146. 10 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 11 | 12 | # === Flutter Library === 13 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 14 | 15 | # Published to parent scope for install step. 16 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 17 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 18 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 19 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 20 | 21 | list(APPEND FLUTTER_LIBRARY_HEADERS 22 | "flutter_export.h" 23 | "flutter_windows.h" 24 | "flutter_messenger.h" 25 | "flutter_plugin_registrar.h" 26 | "flutter_texture_registrar.h" 27 | ) 28 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 29 | add_library(flutter INTERFACE) 30 | target_include_directories(flutter INTERFACE 31 | "${EPHEMERAL_DIR}" 32 | ) 33 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 34 | add_dependencies(flutter flutter_assemble) 35 | 36 | # === Wrapper === 37 | list(APPEND CPP_WRAPPER_SOURCES_CORE 38 | "core_implementations.cc" 39 | "standard_codec.cc" 40 | ) 41 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 42 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 43 | "plugin_registrar.cc" 44 | ) 45 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 46 | list(APPEND CPP_WRAPPER_SOURCES_APP 47 | "flutter_engine.cc" 48 | "flutter_view_controller.cc" 49 | ) 50 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 51 | 52 | # Wrapper sources needed for a plugin. 53 | add_library(flutter_wrapper_plugin STATIC 54 | ${CPP_WRAPPER_SOURCES_CORE} 55 | ${CPP_WRAPPER_SOURCES_PLUGIN} 56 | ) 57 | apply_standard_settings(flutter_wrapper_plugin) 58 | set_target_properties(flutter_wrapper_plugin PROPERTIES 59 | POSITION_INDEPENDENT_CODE ON) 60 | set_target_properties(flutter_wrapper_plugin PROPERTIES 61 | CXX_VISIBILITY_PRESET hidden) 62 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 63 | target_include_directories(flutter_wrapper_plugin PUBLIC 64 | "${WRAPPER_ROOT}/include" 65 | ) 66 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 67 | 68 | # Wrapper sources needed for the runner. 69 | add_library(flutter_wrapper_app STATIC 70 | ${CPP_WRAPPER_SOURCES_CORE} 71 | ${CPP_WRAPPER_SOURCES_APP} 72 | ) 73 | apply_standard_settings(flutter_wrapper_app) 74 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 75 | target_include_directories(flutter_wrapper_app PUBLIC 76 | "${WRAPPER_ROOT}/include" 77 | ) 78 | add_dependencies(flutter_wrapper_app flutter_assemble) 79 | 80 | # === Flutter tool backend === 81 | # _phony_ is a non-existent file to force this command to run every time, 82 | # since currently there's no way to get a full input/output list from the 83 | # flutter tool. 84 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 85 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 86 | add_custom_command( 87 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 88 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 89 | ${CPP_WRAPPER_SOURCES_APP} 90 | ${PHONY_OUTPUT} 91 | COMMAND ${CMAKE_COMMAND} -E env 92 | ${FLUTTER_TOOL_ENVIRONMENT} 93 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 94 | windows-x64 $ 95 | VERBATIM 96 | ) 97 | add_custom_target(flutter_assemble DEPENDS 98 | "${FLUTTER_LIBRARY}" 99 | ${FLUTTER_LIBRARY_HEADERS} 100 | ${CPP_WRAPPER_SOURCES_CORE} 101 | ${CPP_WRAPPER_SOURCES_PLUGIN} 102 | ${CPP_WRAPPER_SOURCES_APP} 103 | ) 104 | -------------------------------------------------------------------------------- /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 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | void RegisterPlugins(flutter::PluginRegistry* registry) { 27 | AutoUpdatePluginRegisterWithRegistrar( 28 | registry->GetRegistrarForPlugin("AutoUpdatePlugin")); 29 | DartDiscordRpcPluginRegisterWithRegistrar( 30 | registry->GetRegistrarForPlugin("DartDiscordRpcPlugin")); 31 | DesktopDropPluginRegisterWithRegistrar( 32 | registry->GetRegistrarForPlugin("DesktopDropPlugin")); 33 | DynamicEnvPluginCApiRegisterWithRegistrar( 34 | registry->GetRegistrarForPlugin("DynamicEnvPluginCApi")); 35 | FileSelectorWindowsRegisterWithRegistrar( 36 | registry->GetRegistrarForPlugin("FileSelectorWindows")); 37 | FlutterAcrylicPluginRegisterWithRegistrar( 38 | registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); 39 | FlutterPlatformAlertPluginRegisterWithRegistrar( 40 | registry->GetRegistrarForPlugin("FlutterPlatformAlertPlugin")); 41 | ProtocolHandlerPluginRegisterWithRegistrar( 42 | registry->GetRegistrarForPlugin("ProtocolHandlerPlugin")); 43 | ScreenRetrieverPluginRegisterWithRegistrar( 44 | registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); 45 | SentryFlutterPluginRegisterWithRegistrar( 46 | registry->GetRegistrarForPlugin("SentryFlutterPlugin")); 47 | SystemThemePluginRegisterWithRegistrar( 48 | registry->GetRegistrarForPlugin("SystemThemePlugin")); 49 | SystemTrayPluginRegisterWithRegistrar( 50 | registry->GetRegistrarForPlugin("SystemTrayPlugin")); 51 | UrlLauncherWindowsRegisterWithRegistrar( 52 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 53 | WebviewWindowsPluginRegisterWithRegistrar( 54 | registry->GetRegistrarForPlugin("WebviewWindowsPlugin")); 55 | WindowManagerPluginRegisterWithRegistrar( 56 | registry->GetRegistrarForPlugin("WindowManagerPlugin")); 57 | WindowsTaskbarPluginRegisterWithRegistrar( 58 | registry->GetRegistrarForPlugin("WindowsTaskbarPlugin")); 59 | } 60 | -------------------------------------------------------------------------------- /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/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | auto_update 7 | dart_discord_rpc 8 | desktop_drop 9 | dynamic_env 10 | file_selector_windows 11 | flutter_acrylic 12 | flutter_platform_alert 13 | protocol_handler 14 | screen_retriever 15 | sentry_flutter 16 | system_theme 17 | system_tray 18 | url_launcher_windows 19 | webview_windows 20 | window_manager 21 | windows_taskbar 22 | ) 23 | 24 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 25 | ) 26 | 27 | set(PLUGIN_BUNDLED_LIBRARIES) 28 | 29 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 30 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 31 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 32 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 33 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 34 | endforeach(plugin) 35 | 36 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 37 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 38 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 39 | endforeach(ffi_plugin) 40 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | add_executable(${BINARY_NAME} WIN32 5 | "flutter_window.cpp" 6 | "main.cpp" 7 | "utils.cpp" 8 | "win32_window.cpp" 9 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 10 | "Runner.rc" 11 | "runner.exe.manifest" 12 | ) 13 | apply_standard_settings(${BINARY_NAME}) 14 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 15 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 16 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 17 | add_dependencies(${BINARY_NAME} flutter_assemble) 18 | -------------------------------------------------------------------------------- /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 | #ifdef FLUTTER_BUILD_NUMBER 64 | #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,11 67 | #endif 68 | 69 | #ifdef FLUTTER_BUILD_NAME 70 | #define VERSION_AS_STRING #FLUTTER_BUILD_NAME 71 | #else 72 | #define VERSION_AS_STRING "1.0.11" 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", "" "\0" 93 | VALUE "FileDescription", "Kyber Mod Manager" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "Kyber Mod Manager" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 Liam All rights reserved." "\0" 97 | VALUE "OriginalFilename", "Kyber Mod Manager.exe" "\0" 98 | VALUE "ProductName", "Kyber Mod 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 | -------------------------------------------------------------------------------- /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 | return true; 30 | } 31 | 32 | void FlutterWindow::OnDestroy() { 33 | if (flutter_controller_) { 34 | flutter_controller_ = nullptr; 35 | } 36 | 37 | Win32Window::OnDestroy(); 38 | } 39 | 40 | LRESULT 41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 42 | WPARAM const wparam, 43 | LPARAM const lparam) noexcept { 44 | // Give Flutter, including plugins, an opportunity to handle window messages. 45 | if (flutter_controller_) { 46 | std::optional result = 47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 48 | lparam); 49 | if (result) { 50 | return *result; 51 | } 52 | } 53 | 54 | switch (message) { 55 | case WM_FONTCHANGE: 56 | flutter_controller_->engine()->ReloadSystemFonts(); 57 | break; 58 | } 59 | 60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 61 | } 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | #include 9 | 10 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 11 | _In_ wchar_t *command_line, _In_ int show_command) { 12 | HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"Kyber Mod Manager"); 13 | if (hwnd != NULL) { 14 | DispatchToProtocolHandler(hwnd); 15 | 16 | ::ShowWindow(hwnd, SW_NORMAL); 17 | ::SetForegroundWindow(hwnd); 18 | return EXIT_FAILURE; 19 | } 20 | // Attach to console when present (e.g., 'flutter run') or create a 21 | // new console when running with a debugger. 22 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 23 | CreateAndAttachConsole(); 24 | } else { 25 | AllocConsole(); 26 | ShowWindow(GetConsoleWindow(), SW_HIDE); 27 | } 28 | 29 | // Initialize COM, so that it is available for use in the library and/or 30 | // plugins. 31 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 32 | 33 | flutter::DartProject project(L"data"); 34 | 35 | std::vector command_line_arguments = 36 | GetCommandLineArguments(); 37 | 38 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 39 | 40 | FlutterWindow window(project); 41 | Win32Window::Point origin(10, 10); 42 | Win32Window::Size size(1280, 720); 43 | if (!window.CreateAndShow(L"Kyber Mod Manager", origin, size)) { 44 | return EXIT_FAILURE; 45 | } 46 | window.SetQuitOnClose(true); 47 | 48 | ::MSG msg; 49 | while (::GetMessage(&msg, nullptr, 0, 0)) { 50 | ::TranslateMessage(&msg); 51 | ::DispatchMessage(&msg); 52 | } 53 | 54 | ::CoUninitialize(); 55 | return EXIT_SUCCESS; 56 | } 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArmchairDevelopers/KyberModManager/6a665597132f7e7cebd8664ca5853cdea7848fca/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | if (target_length == 0) { 52 | return std::string(); 53 | } 54 | std::string utf8_string; 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 and shows a win32 window with |title| and position and size 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 to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | --------------------------------------------------------------------------------