-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathApp.xaml.cs
More file actions
551 lines (487 loc) · 24.1 KB
/
App.xaml.cs
File metadata and controls
551 lines (487 loc) · 24.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.Toolkit.Uwp.Notifications;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using SS = global::FluentTaskScheduler.Services.SettingsService;
namespace FluentTaskScheduler
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application
{
// ── Window registry ──────────────────────────────────────────────────────
private sealed class WindowRecord
{
public string Name { get; }
public Window Win { get; }
public bool IsHidden { get; set; }
public WindowRecord(string name, Window win) { Name = name; Win = win; }
}
private static readonly List<WindowRecord> _windows = new();
private static int _windowCounter = 0;
private static System.Threading.Mutex? _instanceMutex;
private static System.Threading.EventWaitHandle? _showInstanceEvent;
/// <summary>Backward-compat alias — still valid for file pickers, icon loading, etc.</summary>
public static Window? m_window => _windows.Count > 0 ? _windows[0].Win : null;
public Window? MainWindow => m_window;
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string? lpModuleName);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr LoadImage(IntPtr hInst, IntPtr lpszName, uint uType, int cxDesired, int cyDesired, uint fuLoad);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
private const uint IMAGE_ICON = 1;
private const uint LR_DEFAULTSIZE = 0x00000040;
private const uint LR_SHARED = 0x00008000;
private const uint WM_SETICON = 0x0080;
private static readonly IntPtr ICON_SMALL = IntPtr.Zero;
private static readonly IntPtr ICON_BIG = new IntPtr(1);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AttachConsole(int dwProcessId);
private const int ATTACH_PARENT_PROCESS = -1;
public App()
{
// Force English language
try
{
Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = "en-US";
}
catch { }
// Handle toast notification activation (e.g. clicking the "minimized to tray" notification)
ToastNotificationManagerCompat.OnActivated += OnToastActivated;
this.InitializeComponent();
// Global handlers
#pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
#pragma warning restore CS8622
this.UnhandledException += App_UnhandledException;
}
private void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)
{
LogCrash(e.ExceptionObject as Exception, "AppDomain.UnhandledException");
}
private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
LogCrash(e.Exception, "TaskScheduler.UnobservedTaskException");
e.SetObserved();
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
LogCrash(e.Exception, "Xaml.UnhandledException");
e.Handled = true;
}
private void LogCrash(Exception? ex, string source)
{
string errorMessage = $"[{DateTime.Now}] [{source}] Error: {ex?.Message}\r\nStack Trace: {ex?.StackTrace ?? "No stack"}\r\n\r\n";
Services.LogService.WriteCrash(ex, source);
// Attempt to show dialog if window exists
if (m_window != null)
{
try
{
var dispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();
if (dispatcher != null)
{
// Fire and forget, we just want to see it
dispatcher.TryEnqueue(async () =>
{
try
{
var dialog = new ContentDialog
{
Title = "Unhandled Exception",
Content = errorMessage,
CloseButtonText = "Close",
XamlRoot = m_window.Content?.XamlRoot
};
await dialog.ShowAsync();
}
catch (Exception nestedEx)
{
System.Diagnostics.Debug.WriteLine($"Failed to show crash dialog: {nestedEx.Message}");
}
});
// Keep process alive briefly?
System.Threading.Thread.Sleep(5000);
}
}
catch { }
}
}
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
var args = Environment.GetCommandLineArgs();
// GUI Mode: No arguments or just the executable path
if (args.Length > 1)
{
// Attempt to attach to parent console to output text
AttachConsole(ATTACH_PARENT_PROCESS);
// CLI Mode
// usage: FluentTaskScheduler.exe --run "Path"
string command = args[1].ToLower();
string? param = args.Length > 2 ? args[2] : null;
bool jsonOutput = args.Contains("--json"); // Keep variable for potential future use or just ignore
var service = new global::FluentTaskScheduler.Services.TaskServiceWrapper();
try
{
if (command == "--list")
{
var tasks = service.GetAllTasks();
var simpleList = new System.Collections.Generic.List<object>();
foreach(var t in tasks)
{
simpleList.Add(new {
Name = t.Name,
Path = t.Path,
State = t.State,
LastRun = t.LastRunTime,
NextRun = t.NextRunTime
});
}
string json = System.Text.Json.JsonSerializer.Serialize(simpleList, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
}
else if (command == "--run" && !string.IsNullOrEmpty(param))
{
Console.WriteLine($"Running task: {param}");
service.RunTask(param);
Console.WriteLine("Task started.");
}
else if (command == "--enable" && !string.IsNullOrEmpty(param))
{
Console.WriteLine($"Enabling task: {param}");
service.EnableTask(param);
Console.WriteLine("Task enabled.");
}
else if (command == "--disable" && !string.IsNullOrEmpty(param))
{
Console.WriteLine($"Disabling task: {param}");
service.DisableTask(param);
Console.WriteLine("Task disabled.");
}
else if (command == "--export-history" && !string.IsNullOrEmpty(param))
{
string output = args.Length > 4 && args[3] == "--output" ? args[4] : "history.csv";
Console.WriteLine($"Exporting history for {param} to {output}...");
var history = service.GetTaskHistory(param);
if (history != null && history.Count > 0)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("Time,EventId,Result,User,ExitCode,Message");
foreach (var h in history)
{
sb.AppendLine($"\"{h.Time}\",{h.EventId},\"{h.Result}\",\"{h.User}\",{h.ExitCode},\"{h.Message.Replace("\"", "\"\"")}\"");
}
System.IO.File.WriteAllText(output, sb.ToString());
Console.WriteLine("Export complete.");
}
else
{
Console.WriteLine("No history found or task does not exist.");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
// Flush and Exit
Console.Out.Flush();
Environment.Exit(0);
return;
}
// ── Single-instance enforcement (GUI mode) ──────────────────────────
_instanceMutex = new System.Threading.Mutex(true, "FluentTaskScheduler_Instance", out bool isFirstInstance);
if (!isFirstInstance)
{
// Another GUI instance is already running — signal it to show itself and exit
try
{
var ev = System.Threading.EventWaitHandle.OpenExisting("FluentTaskScheduler_Show");
ev.Set();
}
catch { }
Environment.Exit(0);
return;
}
// First instance: listen for show-signals from future instances
_showInstanceEvent = new System.Threading.EventWaitHandle(
false, System.Threading.EventResetMode.AutoReset, "FluentTaskScheduler_Show");
System.Threading.Tasks.Task.Run(() =>
{
while (true)
{
_showInstanceEvent.WaitOne();
var win = _windows.Count > 0 ? _windows[0].Win : null;
win?.DispatcherQueue.TryEnqueue(() => { win.AppWindow.Show(); win.Activate(); });
}
});
// GUI Mode: create and register the first window
CreateAndRegisterWindow();
// One-time tray init (uses the first window's HWND as the message sink)
var trayHwnd = WinRT.Interop.WindowNative.GetWindowHandle(_windows[0].Win);
Services.TrayIconService.Initialize(trayHwnd);
// Callback: returns all currently hidden windows for the tray menu
Services.TrayIconService.GetHiddenWindows = () =>
{
var list = new List<(string, Action, Action)>();
foreach (var rec in _windows)
{
if (!rec.IsHidden) continue;
var r = rec; // capture
list.Add((
r.Name,
() => r.Win.DispatcherQueue.TryEnqueue(() => { r.Win.AppWindow.Show(); r.Win.Activate(); r.IsHidden = false; }),
() => r.Win.DispatcherQueue.TryEnqueue(() => { r.IsHidden = false; r.Win.Close(); })
));
}
return list;
};
Services.TrayIconService.NewWindowRequested += () =>
_windows[0].Win.DispatcherQueue.TryEnqueue(CreateAndRegisterWindow);
Services.TrayIconService.ExitRequested += () => Environment.Exit(0);
Services.TrayIconService.UpdateVisibility();
Services.LogService.Info("Application started");
Services.ReminderService.Start();
// Check for VeloPack auto-updates in the background
_ = CheckForVeloPackUpdateAsync();
// Defer smooth scrolling until visual tree is built
_windows[0].Win.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
ApplySmoothScrolling(SS.SmoothScrolling);
});
}
private void CreateAndRegisterWindow()
{
_windowCounter++;
string name = _windowCounter == 1 ? "Window 1" : $"Window {_windowCounter}";
var win = new Window();
win.Title = _windowCounter == 1 ? "FluentTaskScheduler" : $"FluentTaskScheduler — {name}";
var rec = new WindowRecord(name, win);
_windows.Add(rec);
// Icon
try
{
string iconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", "AppIcon.ico");
if (System.IO.File.Exists(iconPath))
win.AppWindow.SetIcon(iconPath);
else
{
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(win);
if (hwnd != IntPtr.Zero)
{
IntPtr hModule = GetModuleHandle(null);
IntPtr hIcon = LoadImage(hModule, new IntPtr(32512), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_SHARED);
if (hIcon != IntPtr.Zero) { SendMessage(hwnd, WM_SETICON, ICON_SMALL, hIcon); SendMessage(hwnd, WM_SETICON, ICON_BIG, hIcon); }
}
}
}
catch { }
// Size — first window restores saved size, subsequent windows use a slight offset
int offset = (_windowCounter - 1) * 30;
win.AppWindow.Resize(new Windows.Graphics.SizeInt32 { Width = SS.WindowWidth + offset, Height = SS.WindowHeight + offset });
// Save size changes for the first window only
if (_windowCounter == 1)
{
win.AppWindow.Changed += (s, e) =>
{
if (e.DidSizeChange && !rec.IsHidden)
{
SS.WindowWidth = s.Size.Width;
SS.WindowHeight = s.Size.Height;
}
};
}
// Frame & navigation
Frame rootFrame = new Frame();
rootFrame.NavigationFailed += OnNavigationFailed;
win.Content = rootFrame;
// Extend into title bar
win.ExtendsContentIntoTitleBar = true;
ApplyThemeToWindow(win);
rootFrame.Navigate(typeof(MainPage));
// Close-to-tray handler
win.AppWindow.Closing += (sender, args) =>
{
if (SS.EnableTrayIcon)
{
args.Cancel = true;
rec.IsHidden = true;
sender.Hide();
Services.NotificationService.ShowMinimizedToTray();
}
else
{
// Actually closing — remove from registry
_windows.Remove(rec);
}
};
win.Activate();
}
private void AppWindow_Closing(Microsoft.UI.Windowing.AppWindow sender, Microsoft.UI.Windowing.AppWindowClosingEventArgs args)
{
// Handled per-window inside CreateAndRegisterWindow
}
private void OnToastActivated(ToastNotificationActivatedEventArgsCompat e)
{
var args = ToastArguments.Parse(e.Argument);
if (args.TryGetValue("action", out string action) && action == "show")
{
// Restore the most-recently-hidden window, or the first window
var win = _windows.FindLast(r => r.IsHidden)?.Win ?? m_window;
win?.DispatcherQueue.TryEnqueue(() => { win.AppWindow.Show(); win.Activate(); });
}
}
public void ApplySmoothScrolling(bool enable)
{
foreach (var rec in _windows)
{
if (rec.Win?.Content == null) continue;
foreach (var sv in FindDescendants<ScrollViewer>(rec.Win.Content))
sv.IsScrollInertiaEnabled = enable;
}
}
private static IEnumerable<T> FindDescendants<T>(DependencyObject parent) where T : DependencyObject
{
if (parent == null) yield break;
int count = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is T match)
yield return match;
foreach (var descendant in FindDescendants<T>(child))
yield return descendant;
}
}
Microsoft.UI.Xaml.Media.SystemBackdrop? _backdrop;
private void ApplyThemeToWindow(Window win)
{
if (win?.Content is Control root)
{
root.RequestedTheme = SS.Theme;
win.SystemBackdrop = null;
Application.Current.Resources["TaskCardBackground"] = Application.Current.Resources["CardBackgroundFillColorDefaultBrush"];
Application.Current.Resources["TaskCardBorder"] = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Transparent);
if (SS.IsOledMode && SS.Theme == ElementTheme.Dark)
{
var black = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Black);
root.Background = black;
SetNavigationViewBackgrounds(black);
}
else if (SS.IsMicaEnabled && Microsoft.UI.Composition.SystemBackdrops.MicaController.IsSupported())
{
var transparent = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Transparent);
root.Background = transparent;
if (_backdrop == null) _backdrop = new Microsoft.UI.Xaml.Media.MicaBackdrop();
win.SystemBackdrop = _backdrop;
SetNavigationViewBackgrounds(transparent);
}
else
{
_backdrop = null;
// Use ActualTheme so the colour matches the requested theme even when the OS theme differs
bool isDark = root.ActualTheme == ElementTheme.Dark;
var bg = new Microsoft.UI.Xaml.Media.SolidColorBrush(
isDark ? Windows.UI.Color.FromArgb(255, 32, 32, 32)
: Windows.UI.Color.FromArgb(255, 243, 243, 243));
root.Background = bg;
SetNavigationViewBackgrounds(bg);
}
// Title bar customization
UpdateTitleBarTheme(win, root.ActualTheme);
}
}
private void UpdateTitleBarTheme(Window win, ElementTheme theme)
{
var appWindow = win.AppWindow;
if (appWindow == null) return;
if (Microsoft.UI.Windowing.AppWindowTitleBar.IsCustomizationSupported())
{
var titleBar = appWindow.TitleBar;
bool isDark = theme == ElementTheme.Dark;
// Set colors for the title bar buttons to match theme
if (isDark)
{
titleBar.ButtonBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonForegroundColor = Microsoft.UI.Colors.White;
titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(25, 255, 255, 255);
titleBar.ButtonHoverForegroundColor = Microsoft.UI.Colors.White;
titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(40, 255, 255, 255);
titleBar.ButtonPressedForegroundColor = Microsoft.UI.Colors.White;
titleBar.ButtonInactiveBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(100, 255, 255, 255);
}
else
{
titleBar.ButtonBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonForegroundColor = Microsoft.UI.Colors.Black;
titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(25, 0, 0, 0);
titleBar.ButtonHoverForegroundColor = Microsoft.UI.Colors.Black;
titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(40, 0, 0, 0);
titleBar.ButtonPressedForegroundColor = Microsoft.UI.Colors.Black;
titleBar.ButtonInactiveBackgroundColor = Microsoft.UI.Colors.Transparent;
titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(100, 0, 0, 0);
}
}
}
private static void SetNavigationViewBackgrounds(Microsoft.UI.Xaml.Media.Brush brush)
{
Application.Current.Resources["NavigationViewContentBackground"] = brush;
Application.Current.Resources["NavigationViewExpandedPaneBackground"] = brush;
Application.Current.Resources["NavigationViewTopPaneBackground"] = brush;
}
public void ApplyTheme(ElementTheme theme)
{
foreach (var rec in _windows)
ApplyThemeToWindow(rec.Win);
}
private async System.Threading.Tasks.Task CheckForVeloPackUpdateAsync()
{
try
{
var result = await Services.VeloPackUpdateService.CheckAndDownloadAsync();
if (result.Status != Services.VeloPackUpdateService.UpdateResultStatus.UpdateReady || result.Info == null || m_window == null) return;
m_window.DispatcherQueue.TryEnqueue(async () =>
{
try
{
var dialog = new ContentDialog
{
Title = "Update Available",
Content = $"Version {result.NewVersion} has been downloaded and is ready to install.\nRestart now to apply the update?",
PrimaryButtonText = "Restart Now",
CloseButtonText = "Later",
XamlRoot = m_window.Content?.XamlRoot,
RequestedTheme = SS.Theme
};
var dialogResult = await dialog.ShowAsync();
if (dialogResult == ContentDialogResult.Primary)
{
Services.VeloPackUpdateService.ApplyAndRestart(result.Info);
}
}
catch (Exception ex)
{
Services.LogService.Info($"[AutoUpdate] Could not show update dialog: {ex.Message}");
}
});
}
catch (Exception ex)
{
Services.LogService.Info($"[AutoUpdate] Background update check failed: {ex.Message}");
}
}
void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
}
}