├── .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 |
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 ](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