• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ParadoxGameConverters / Fronter.NET / 24615805603

18 Apr 2026 10:58PM UTC coverage: 25.99% (-2.4%) from 28.393%
24615805603

push

github

web-flow
Bump dependencies (#970)

* Bump dependencies

* Fix coverage not being uploaded

166 of 770 branches covered (21.56%)

Branch coverage included in aggregate %.

740 of 2716 relevant lines covered (27.25%)

5.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

12.7
/Fronter.NET/ViewModels/MainWindowViewModel.cs
1
using Avalonia;
2
using Avalonia.Controls;
3
using Avalonia.Controls.ApplicationLifetimes;
4
using Avalonia.Notification;
5
using Avalonia.Threading;
6
using commonItems.Collections;
7
using Fronter.Extensions;
8
using Fronter.LogAppenders;
9
using Fronter.Models;
10
using Fronter.Models.Configuration;
11
using Fronter.Services;
12
using Fronter.Views;
13
using log4net;
14
using log4net.Core;
15
using MsBox.Avalonia;
16
using MsBox.Avalonia.Dto;
17
using MsBox.Avalonia.Enums;
18
using ReactiveUI;
19
using System;
20
using System.Collections.Generic;
21
using System.Collections.ObjectModel;
22
using System.Diagnostics.CodeAnalysis;
23
using System.IO;
24
using System.Linq;
25
using System.Reactive;
26
using System.Threading;
27
using System.Threading.Tasks;
28

29
namespace Fronter.ViewModels;
30

31
[SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Global")]
32
internal sealed class MainWindowViewModel : ViewModelBase {
33
        private static readonly ILog logger = LogManager.GetLogger("Frontend");
×
34
        private readonly TranslationSource loc = TranslationSource.Instance;
6✔
35
        public IEnumerable<MenuItemViewModel> LanguageMenuItems => loc.LoadedLanguages
×
36
                .Select(l => new MenuItemViewModel {
×
37
                        Command = SetLanguageCommand,
×
38
                        CommandParameter = l,
×
39
                        Header = loc.TranslateLanguage(l),
×
40
                        Items = Array.Empty<MenuItemViewModel>(),
×
41
                });
×
42

43
        public INotificationMessageManager NotificationManager { get; } = new NotificationMessageManager();
44

45
        private IdObjectCollection<string, FrontendTheme> Themes { get; } = [
46
                new() {Id = "Default", LocKey = "THEME_SYSTEM"},
47
                new() {Id = "Light", LocKey = "THEME_LIGHT"},
48
                new() {Id = "Dark", LocKey = "THEME_DARK"},
49
        ];
50
        public IEnumerable<MenuItemViewModel> ThemeMenuItems => Themes
3✔
51
                .Select(theme => new MenuItemViewModel {
3✔
52
                        Command = SetThemeCommand,
3✔
53
                        CommandParameter = theme.Id,
3✔
54
                        Header = loc.Translate(theme.LocKey),
3✔
55
                        Items = Array.Empty<MenuItemViewModel>(),
3✔
56
                });
3✔
57

58
        // Conversion control
59
        private CancellationTokenSource? conversionCts;
60
        
61
        public ReactiveCommand<Unit, Unit> CancelConversionCommand { get; }
62

63
        internal Config Config { get; }
64

65
        internal PathPickerViewModel PathPicker { get; }
66
        internal TargetPlaysetPickerViewModel TargetPlaysetPicker { get; }
67
        public bool TargetPlaysetPickerTabVisible => Config.TargetPlaysetSelectionEnabled;
×
68
        public OptionsViewModel Options { get; }
69
        public bool OptionsTabVisible => Options.Items.Any();
2✔
70

71
        public Level LogFilterLevel {
72
                get => LogGridAppender.LogFilterLevel;
×
73
                private set => this.RaiseAndSetIfChanged(ref LogGridAppender.LogFilterLevel, value);
×
74
        }
75

76
        public string SaveStatus {
77
                get;
78
                set => this.RaiseAndSetIfChanged(ref field, value);
×
79
        } = "CONVERTSTATUSPRE";
6✔
80

81
        public string ConvertStatus {
82
                get;
83
                set => this.RaiseAndSetIfChanged(ref field, value);
×
84
        } = "CONVERTSTATUSPRE";
6✔
85

86
        public string CopyStatus {
87
                get;
88
                set => this.RaiseAndSetIfChanged(ref field, value);
×
89
        } = "CONVERTSTATUSPRE";
6✔
90

91
        public bool ConvertButtonEnabled {
92
                get;
93
                set => this.RaiseAndSetIfChanged(ref field, value);
2✔
94
        } = true;
6✔
95

96
        public MainWindowViewModel(DataGrid logGrid) {
12✔
97
                Config = new Config();
6✔
98

99
                var appenders = LogManager.GetRepository().GetAppenders();
6✔
100
                var gridAppender = appenders.First(a => a.Name.Equals("grid"));
6✔
101
                if (gridAppender is not LogGridAppender logGridAppender) {
6!
102
                        throw new LogException($"Log appender \"{gridAppender.Name}\" is not a {typeof(LogGridAppender)}");
×
103
                }
104
                LogGridAppender = logGridAppender;
6✔
105
                LogGridAppender.LogGrid = logGrid;
6✔
106

107
                PathPicker = new PathPickerViewModel(Config);
6✔
108
                TargetPlaysetPicker = new TargetPlaysetPickerViewModel(Config);
6✔
109
                Options = new OptionsViewModel(Config.Options);
6✔
110

111
                // Create reactive commands.
112
                ToggleLogFilterLevelCommand = ReactiveCommand.Create<string>(ToggleLogFilterLevel);
6✔
113
                SetLanguageCommand = ReactiveCommand.Create<string>(SetLanguage);
6✔
114
                SetThemeCommand = ReactiveCommand.Create<string>(SetTheme);
6✔
115

116
                CancelConversionCommand = ReactiveCommand.Create(CancelConversion);
6✔
117
        }
6✔
118

119
        public ReadOnlyObservableCollection<LogLine> FilteredLogLines => LogGridAppender.FilteredLogLines;
×
120

121
        #region Reactive commands
122

123
        public ReactiveCommand<string, Unit> ToggleLogFilterLevelCommand { get; }
124
        public ReactiveCommand<string, Unit> SetLanguageCommand { get; }
125
        public ReactiveCommand<string, Unit> SetThemeCommand { get; }
126

127
        #endregion
128

129
        public void ToggleLogFilterLevel(string value) {
×
130
                var level = LogManager.GetRepository().LevelMap[value];
×
131
                if (level is null) {
×
132
                        logger.Error($"Unknown log level: {value}");
×
133
                } else {
×
134
                        LogFilterLevel = level;
×
135
                }
×
136
                LogGridAppender.ToggleLogFilterLevel();
×
137
                this.RaisePropertyChanged(nameof(FilteredLogLines));
×
138
                Dispatcher.UIThread.Post(ScrollToLogEnd, DispatcherPriority.Normal);
×
139
        }
×
140

141
        public ushort Progress {
142
                get;
143
                set => this.RaiseAndSetIfChanged(ref field, value);
×
144
        } = 0;
6✔
145

146
        public bool IndeterminateProgress {
147
                get;
148
                set => this.RaiseAndSetIfChanged(ref field, value);
×
149
        } = false;
6✔
150

151
        private bool VerifyMandatoryPaths() {
×
152
                foreach (var folder in Config.RequiredFolders) {
×
153
                        if (!folder.Mandatory || Directory.Exists(folder.Value)) {
×
154
                                continue;
×
155
                        }
156

157
                        logger.Error($"Mandatory folder {folder.Name} at {folder.Value} not found.");
×
158
                        return false;
×
159
                }
160

161
                foreach (var file in Config.RequiredFiles.Where(file => file.Mandatory && !File.Exists(file.Value))) {
×
162
                        logger.Error($"Mandatory file {file.Name} at {file.Value} not found.");
×
163
                        return false;
×
164
                }
165

166
                return true;
×
167
        }
×
168

169
        private Task ClearLogGrid() {
×
170
                return LogGridAppender.ClearDisplayedLogLines();
×
171
        }
×
172

173
        private void CopyToTargetGameModDirectory() {
×
174
                var modCopier = new ModCopier(Config);
×
175
                bool copySuccess;
176
                var copyThread = new Thread(() => {
×
177
                        IndeterminateProgress = true;
×
178
                        CopyStatus = "CONVERTSTATUSIN";
×
179

×
180
                        copySuccess = modCopier.CopyMod();
×
181
                        CopyStatus = copySuccess ? "CONVERTSTATUSPOSTSUCCESS" : "CONVERTSTATUSPOSTFAIL";
×
182
                        Progress = Config.ProgressOnCopyingComplete;
×
183
                        IndeterminateProgress = false;
×
184

×
185
                        ConvertButtonEnabled = true;
×
186
                });
×
187
                copyThread.Start();
×
188
        }
×
189
        public async Task LaunchConverter() {
×
190
                ConvertButtonEnabled = false;
×
191
                await ClearLogGrid();
×
192

193
                Progress = 0;
×
194
                SaveStatus = "CONVERTSTATUSPRE";
×
195
                ConvertStatus = "CONVERTSTATUSPRE";
×
196
                CopyStatus = "CONVERTSTATUSPRE";
×
197

198
                if (!VerifyMandatoryPaths()) {
×
199
                        ConvertButtonEnabled = true;
×
200
                        return;
×
201
                }
202
                Config.ExportConfiguration();
×
203

204
                var converterLauncher = new ConverterLauncher(Config);
×
205
                bool success;
206
                conversionCts = new CancellationTokenSource();
×
207
                var token = conversionCts.Token;
×
208

209
                bool wasCancelled = false;
×
210
                await Task.Run(async () => {
×
211
                        ConvertStatus = "CONVERTSTATUSIN";
×
212

×
213
                        try {
×
214
                                success = await converterLauncher.LaunchConverter(token);
×
215
                        } catch (TaskCanceledException e) {
×
216
                                logger.Debug($"Converter backend task was cancelled: {e.Message}");
×
217
                                success = false;
×
218
                                wasCancelled = true;
×
219
                        } catch (Exception e) {
×
220
                                logger.Error($"Failed to start converter backend: {e.Message}");
×
221
                                await ShowFailedToStartConverterMsBox(e.Message);
×
222
                                success = false;
×
223
                        }
×
224

×
225
                        if (success) {
×
226
                                ConvertStatus = "CONVERTSTATUSPOSTSUCCESS";
×
227

×
228
                                if (Config.CopyToTargetGameModDirectory) {
×
229
                                        CopyToTargetGameModDirectory();
×
230
                                } else {
×
231
                                        ConvertButtonEnabled = true;
×
232
                                }
×
233
                        } else {
×
234
                                if (wasCancelled) {
×
235
                                        ConvertStatus = "CONVERTSTATUSPOSTCANCEL";
×
236
                                        // don't pop up error dialog on user cancellation
×
237
                                } else {
×
238
                                        ConvertStatus = "CONVERTSTATUSPOSTFAIL";
×
239
                                        await Dispatcher.UIThread.InvokeAsync(ShowErrorMessageBox);
×
240
                                }
×
241
                                ConvertButtonEnabled = true;
×
242
                        }
×
243
                });
×
244

245
                conversionCts?.Dispose();
×
246
                conversionCts = null;
×
247
        }
×
248

249
        private async Task ShowFailedToStartConverterMsBox(string errorMessage) {
×
250
                var messageText = $"{loc.Translate("FAILED_TO_START_CONVERTER_BACKEND")}: {errorMessage}";
×
251
                if (!ElevatedPrivilegesDetector.IsAdministrator) {
×
252
                        messageText += "\n\n" + loc.Translate("ELEVATED_PRIVILEGES_REQUIRED");
×
253
                        if (OperatingSystem.IsWindows()) {
×
254
                                messageText += "\n\n" + loc.Translate("RUN_AS_ADMIN");
×
255
                        } else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD()) {
×
256
                                messageText += "\n\n" + loc.Translate("RUN_WITH_SUDO");
×
257
                        }
×
258
                } else {
×
259
                        messageText += "\n\n" + loc.Translate("FAILED_TO_START_CONVERTER_POSSIBLE_BUG");
×
260
                }
×
261

262
                await Dispatcher.UIThread.InvokeAsync(async () => {
×
263
                        await MessageBoxManager.GetMessageBoxStandard(
×
264
                                title: loc.Translate("FAILED_TO_START_CONVERTER"),
×
265
                                text: messageText,
×
266
                                ButtonEnum.Ok,
×
267
                                Icon.Error
×
268
                        ).ShowWindowDialogAsync(MainWindow.Instance);
×
269
                }, DispatcherPriority.Normal);
×
270
        }
×
271

272
        private async Task ShowErrorMessageBox() {
×
273
                var messageBoxWindow = MessageBoxManager
×
274
                        .GetMessageBoxStandard(new MessageBoxStandardParams {
×
275
                                Icon = Icon.Error,
×
276
                                ContentTitle = loc.Translate("CONVERSION_FAILED"),
×
277
                                ContentMessage = loc.Translate("CONVERSION_FAILED_MESSAGE"),
×
278
                                ButtonDefinitions = ButtonEnum.OkCancel,
×
279
                        });
×
280
                var result = await messageBoxWindow.ShowWindowDialogAsync(MainWindow.Instance);
×
281
                if (result == ButtonResult.Ok) {
×
282
                        BrowserLauncher.Open(Config.ConverterReleaseForumThread);
×
283
                }
×
284
        }
×
285

286
        public async Task CheckForUpdates() {
×
287
                if (!Config.UpdateCheckerEnabled) {
×
288
                        return;
×
289
                }
290

291
                bool isUpdateAvailable = await UpdateChecker.IsUpdateAvailable("commit_id.txt", Config.PagesCommitIdUrl);
×
292
                if (!isUpdateAvailable) {
×
293
                        return;
×
294
                }
295

296
                var info = await UpdateChecker.GetLatestReleaseInfo(Config.Name);
×
297
                if (info.AssetUrl is null) {
×
298
                        return;
×
299
                }
300

301
                var updateNowStr = loc.Translate("UPDATE_NOW");
×
302
                var maybeLaterStr = loc.Translate("MAYBE_LATER");
×
303
                var msgBody = UpdateChecker.GetUpdateMessageBody(loc.Translate("NEW_VERSION_BODY"), info);
×
304
                var messageBoxWindow = MessageBoxManager
×
305
                        .GetMessageBoxCustom(new MessageBoxCustomParams {
×
306
                                Icon = Icon.Info,
×
307
                                ContentTitle = loc.Translate("NEW_VERSION_TITLE"),
×
308
                                ContentHeader = loc.Translate("NEW_VERSION_HEADER"),
×
309
                                ContentMessage = MarkdownPlainTextRenderer.Render(msgBody), // We need to render markdown to plain text until `Markdown = true` is re-enabled.
×
310
                                // Markdown = true, // disabled until this PR is merged and Markdown.Avalonia is updated: https://github.com/whistyun/Markdown.Avalonia/pull/154
×
311
                                ButtonDefinitions = [
×
312
                                        new() {Name = updateNowStr, IsDefault = true},
×
313
                                        new() {Name = maybeLaterStr, IsCancel = true},
×
314
                                ],
×
315
                                MaxWidth = 1280,
×
316
                                MaxHeight = 720,
×
317
                        });
×
318

319
                bool performUpdate = false;
×
320
                await Dispatcher.UIThread.InvokeAsync(async () => {
×
321
                        string result = await messageBoxWindow.ShowWindowDialogAsync(MainWindow.Instance);
×
322
                        performUpdate = result.Equals(updateNowStr);
×
323
                }, DispatcherPriority.Normal);
×
324

325
                if (!performUpdate) {
×
326
                        logger.Info($"Update to version {info.Version} postponed.");
×
327
                        return;
×
328
                }
329

330
                // If we can use an installer, download it, run it, and exit.
331
                if (info.UseInstaller) {
×
332
                        await UpdateChecker.RunInstallerAndDie(info.AssetUrl, Config, NotificationManager);
×
333
                } else {
×
334
                        UpdateChecker.StartUpdaterAndDie(info.AssetUrl, Config.ConverterFolder);
×
335
                }
×
336
        }
×
337

338
        public async Task CheckForUpdatesOnStartup() {
×
339
                if (!Config.CheckForUpdatesOnStartup) {
×
340
                        return;
×
341
                }
342
                await CheckForUpdates();
×
343
        }
×
344

345
#pragma warning disable CA1822
346
        public void Exit() {
×
347
#pragma warning restore CA1822
348
                if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
×
349
                        desktop.Shutdown(exitCode: 0);
×
350
                }
×
351
        }
×
352

353
        public void CancelConversion() {
1✔
354
                // User requested cancellation.  Signal the running launch and reset UI state.
355
                conversionCts?.Cancel();
1!
356
                conversionCts?.Dispose();
1!
357
                conversionCts = null;
1✔
358
                ConvertButtonEnabled = true;
1✔
359
        }
1✔
360

361
#pragma warning disable CA1822
362
        public async Task OpenAboutDialog() {
×
363
#pragma warning restore CA1822
364
                var messageBoxWindow = MessageBoxManager
×
365
                        .GetMessageBoxStandard(new MessageBoxStandardParams {
×
366
                                ContentTitle = TranslationSource.Instance["ABOUT_TITLE"],
×
367
                                Icon = Icon.Info,
×
368
                                ContentHeader = TranslationSource.Instance["ABOUT_HEADER"],
×
369
                                ContentMessage = TranslationSource.Instance["ABOUT_BODY"],
×
370
                                ButtonDefinitions = ButtonEnum.Ok,
×
371
                                SizeToContent = SizeToContent.WidthAndHeight,
×
372
                                MinHeight = 250,
×
373
                                ShowInCenter = true,
×
374
                                WindowStartupLocation = WindowStartupLocation.CenterOwner,
×
375
                        });
×
376
                await messageBoxWindow.ShowWindowDialogAsync(MainWindow.Instance);
×
377
        }
×
378

379
#pragma warning disable CA1822
380
        public void OpenPatreonPage() {
×
381
#pragma warning restore CA1822
382
                BrowserLauncher.Open("https://www.patreon.com/ParadoxGameConverters");
×
383
        }
×
384

385
        public void SetLanguage(string languageKey) {
×
386
                loc.SaveLanguage(languageKey);
×
387
        }
×
388

389
#pragma warning disable CA1822
390
        public void SetTheme(string themeName) {
×
391
#pragma warning restore CA1822
392
                _ = App.SaveTheme(themeName);
×
393
        }
×
394

395
        public string WindowTitle {
396
                get {
×
397
                        var displayName = loc.Translate(Config.DisplayName);
×
398
                        if (string.IsNullOrWhiteSpace(displayName)) {
×
399
                                displayName = "Converter";
×
400
                        }
×
401
                        return $"{displayName} Frontend";
×
402
                }
×
403
        }
404

405
        private LogGridAppender LogGridAppender { get; }
406

407
        private void ScrollToLogEnd() {
×
408
                LogGridAppender.ScrollToLogEnd();
×
409
        }
×
410
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc