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

ParadoxGameConverters / Fronter.NET / 24615658260

18 Apr 2026 10:48PM UTC coverage: 28.393% (+0.06%) from 28.334%
24615658260

push

github

web-flow
Optimize the log grid (#971)

* Optimize the log grid

One of the changes is that the auto-scroll churn has been reduced to once per flush cycle.

* Tweak for CodeFactor

* Prevent dropping progress updates

* ClearDisplayedLogLines: clear immediately when already on UI thread

* Remove unneeded tab replacement in the log

147 of 665 branches covered (22.11%)

Branch coverage included in aggregate %.

35 of 79 new or added lines in 2 files covered. (44.3%)

6 existing lines in 2 files now uncovered.

673 of 2223 relevant lines covered (30.27%)

5.93 hits per line

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

26.28
/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; } = [
6✔
46
                new() {Id = "Default", LocKey = "THEME_SYSTEM"},
6✔
47
                new() {Id = "Light", LocKey = "THEME_LIGHT"},
6✔
48
                new() {Id = "Dark", LocKey = "THEME_DARK"},
6✔
49
        ];
6✔
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";
80

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

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

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

96
        public MainWindowViewModel(DataGrid logGrid) {
12✔
97
                Config = new Config();
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;
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;
145

146
        public bool IndeterminateProgress {
147
                get;
148
                set => this.RaiseAndSetIfChanged(ref field, value);
×
149
        } = false;
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

NEW
169
        private Task ClearLogGrid() {
×
NEW
170
                return LogGridAppender.ClearDisplayedLogLines();
×
UNCOV
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