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

ParadoxGameConverters / Fronter.NET / 22321902429

23 Feb 2026 07:39PM UTC coverage: 22.788% (-0.1%) from 22.889%
22321902429

push

github

web-flow
Button for cancelling a conversion in progress (#954)

* Button for cancelling a conversion in progress

* Fix warning about LaunchConverter being too long

* Fix for CodeFactor

144 of 776 branches covered (18.56%)

Branch coverage included in aggregate %.

7 of 62 new or added lines in 4 files covered. (11.29%)

2 existing lines in 2 files now uncovered.

737 of 3090 relevant lines covered (23.85%)

9.93 hits per line

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

12.17
/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;
3✔
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();
3✔
44

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

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

62
        internal Config Config { get; }
9✔
63

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

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

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

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

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

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

95
        public MainWindowViewModel(DataGrid logGrid) {
6✔
96
                Config = new Config();
3✔
97

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

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

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

115
                CancelConversionCommand = ReactiveCommand.Create(CancelConversion);
3✔
116
        }
3✔
117

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

120
        #region Reactive commands
121

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

126
        #endregion
127

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

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

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

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

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

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

165
                return true;
×
166
        }
×
167

168
        private void ClearLogGrid() {
×
169
                LogGridAppender.LogLines.Clear();
×
170
        }
×
171

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

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

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

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

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

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

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

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

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

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

NEW
244
                conversionCts?.Dispose();
×
NEW
245
                conversionCts = null;
×
UNCOV
246
        }
×
247

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

404
        private LogGridAppender LogGridAppender { get; }
3✔
405

406
        private void ScrollToLogEnd() {
×
407
                LogGridAppender.ScrollToLogEnd();
×
408
        }
×
409
}
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