? FontList { get; private set; }
13 | #endregion Properties
14 |
15 | #region Constructor
16 | public SettingsViewModel()
17 | {
18 | FontList ??= [.. Fonts.SystemFontFamilies.OrderBy(x => x.Source)];
19 | }
20 | #endregion Constructor
21 |
22 | #region Open App folder
23 | [RelayCommand]
24 | private static void OpenAppFolder()
25 | {
26 | string filePath = string.Empty;
27 | try
28 | {
29 | filePath = Path.Combine(AppInfo.AppDirectory, "Strings.test.xaml");
30 | if (File.Exists(filePath))
31 | {
32 | _ = Process.Start("explorer.exe", $"/select,\"{filePath}\"");
33 | }
34 | else
35 | {
36 | using Process p = new();
37 | p.StartInfo.FileName = AppInfo.AppDirectory;
38 | p.StartInfo.UseShellExecute = true;
39 | p.StartInfo.ErrorDialog = false;
40 | _ = p.Start();
41 | }
42 | }
43 | catch (Exception ex)
44 | {
45 | _log.Error(ex, $"Error trying to open {filePath}: {ex.Message}");
46 | _ = new MDCustMsgBox(GetStringResource("MsgText_Error_FileExplorer"),
47 | "My Scheduled Tasks ERROR",
48 | ButtonType.Ok,
49 | false,
50 | true,
51 | _mainWindow,
52 | true).ShowDialog();
53 | }
54 | }
55 | #endregion Open App folder
56 |
57 | #region Open settings
58 | [RelayCommand]
59 | private static void OpenSettings()
60 | {
61 | ConfigHelpers.SaveSettings();
62 | TextFileViewer.ViewTextFile(ConfigHelpers.SettingsFileName!);
63 | }
64 | #endregion Open settings
65 |
66 | #region Export settings
67 | [RelayCommand]
68 | private static void ExportSettings()
69 | {
70 | ConfigHelpers.ExportSettings();
71 | }
72 | #endregion Export settings
73 |
74 | #region Import settings
75 | [RelayCommand]
76 | private static void ImportSettings()
77 | {
78 | ConfigHelpers.ImportSettings();
79 | }
80 | #endregion Import settings
81 |
82 | #region List (dump) settings to log file
83 | [RelayCommand]
84 | private static void DumpSettings()
85 | {
86 | ConfigHelpers.DumpSettings();
87 | NavigationViewModel.ViewLogFile();
88 | }
89 | #endregion List (dump) settings to log file
90 |
91 | #region Compare languages
92 | [RelayCommand]
93 | private static void CompareLanguageKeys()
94 | {
95 | CompareLanguageDictionaries();
96 | TextFileViewer.ViewTextFile(GetLogfileName());
97 | }
98 | #endregion Compare languages
99 | }
100 |
--------------------------------------------------------------------------------
/MyScheduledTasks/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
54 |
62 |
63 |
64 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Styles/NavigationStyles.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
22 |
23 |
24 |
25 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
52 |
55 |
56 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Styles/ButtonStyles.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
29 |
30 |
31 |
32 |
49 |
50 |
51 |
52 |
69 |
70 |
--------------------------------------------------------------------------------
/MyScheduledTasks/CommonCompletionCodes.txt:
--------------------------------------------------------------------------------
1 | Common completion (Last Result) codes for scheduled tasks are:
2 |
3 | 0 or 0x0: The operation completed successfully.
4 | 1 or 0x1: Incorrect function called or unknown function called.
5 | 2 or 0x2: File not found.
6 | 10 or 0xa: The environment is incorrect.
7 | 0x00041300: Task is ready to run at its next scheduled time.
8 | 0x00041301: The task is currently running.
9 | 0x00041302: The task has been disabled.
10 | 0x00041303: The task has not yet run.
11 | 0x00041304: There are no more runs scheduled for this task.
12 | 0x00041305: One or more of the properties that are needed to run this task have not been set.
13 | 0x00041306: The last run of the task was terminated by the user.
14 | 0x00041307: Either the task has no triggers or the existing triggers are disabled or not set.
15 | 0x00041308: Event triggers do not have set run times.
16 | 0x80010002: Call was canceled by the message filter
17 | 0x80041309: A task's trigger is not found.
18 | 0x8004130A: One or more of the properties required to run this task have not been set.
19 | 0x8004130B: There is no running instance of the task.
20 | 0x8004130C: The Task Scheduler service is not installed on this computer.
21 | 0x8004130D: The task object could not be opened.
22 | 0x8004130E: The object is either an invalid task object or is not a task object.
23 | 0x8004130F: No account information could be found in the Task Scheduler security database for the task indicated.
24 | 0x80041310: Unable to establish existence of the account specified.
25 | 0x80041311: Corruption was detected in the Task Scheduler security database
26 | 0x80041312: Task Scheduler security services are available only on Windows NT.
27 | 0x80041313: The task object version is either unsupported or invalid.
28 | 0x80041314: The task has been configured with an unsupported combination of account settings and run time options.
29 | 0x80041315: The Task Scheduler Service is not running.
30 | 0x80041316: The task XML contains an unexpected node.
31 | 0x80041317: The task XML contains an element or attribute from an unexpected namespace.
32 | 0x80041318: The task XML contains a value which is incorrectly formatted or out of range.
33 | 0x80041319: The task XML is missing a required element or attribute.
34 | 0x8004131A: The task XML is malformed.
35 | 0x0004131B: The task is registered, but not all specified triggers will start the task.
36 | 0x0004131C: The task is registered, but may fail to start. Batch logon privilege needs to be enabled for the task principal.
37 | 0x8004131D: The task XML contains too many nodes of the same type.
38 | 0x8004131E: The task cannot be started after the trigger end boundary.
39 | 0x8004131F: An instance of this task is already running.
40 | 0x80041320: The task will not run because the user is not logged on.
41 | 0x80041321: The task image is corrupt or has been tampered with.
42 | 0x80041322: The Task Scheduler service is not available.
43 | 0x80041323: The Task Scheduler service is too busy to handle your request. Please try again later.
44 | 0x80041324: The Task Scheduler service attempted to run the task, but the task did not run due to one of the constraints in the task definition.
45 | 0x00041325: The Task Scheduler service has asked the task to run.
46 | 0x80041326: The task is disabled.
47 | 0x80041327: The task has properties that are not compatible with earlier versions of Windows.
48 | 0x80041328: The task settings do not allow the task to start on demand.
49 | 0x80070002: The Task Scheduler cannot find the file.
50 | 0x800710E0: The operator or administrator has refused the request.
51 | 0xC000013A: The application terminated as a result of a CTRL+C.
52 | 0xC0000142: The application failed to initialize properly.
53 |
54 | Source: Wikipedia contributors. (2023b, December 22). Windows Task Scheduler. Wikipedia. https://en.wikipedia.org/wiki/Windows_Task_Scheduler#Column_'Last_Result'
--------------------------------------------------------------------------------
/MyScheduledTasks/Helpers/LocalizationHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace MyScheduledTasks.Helpers;
4 |
5 | ///
6 | /// Class for localization and culture helper methods.
7 | ///
8 | internal static class LocalizationHelpers
9 | {
10 | #region Properties
11 | ///
12 | /// Uri of the resource dictionary
13 | ///
14 | private static string? LanguageFile { get; set; }
15 |
16 | ///
17 | /// Number of language strings in a resource dictionary
18 | ///
19 | public static int LanguageStrings { get; set; }
20 | #endregion Properties
21 |
22 | #region Return the current culture (language)
23 | ///
24 | /// Gets the current culture.
25 | ///
26 | /// Current culture name
27 | public static string GetCurrentCulture()
28 | {
29 | return CultureInfo.CurrentCulture.Name;
30 | }
31 | #endregion Return the current culture (language)
32 |
33 | #region Return the current UI culture
34 | ///
35 | /// Gets the current UI culture.
36 | ///
37 | /// Current UI culture name
38 | public static string GetCurrentUICulture()
39 | {
40 | return CultureInfo.CurrentUICulture.Name;
41 | }
42 | #endregion Return the current UI culture
43 |
44 | #region Apply language settings
45 | ///
46 | /// Apply language settings.
47 | ///
48 | /// The resource dictionary corresponding to the selected language.
49 | public static void ApplyLanguageSettings(ResourceDictionary LanguageDictionary)
50 | {
51 | LanguageStrings = LanguageDictionary.Count;
52 | LanguageFile = LanguageDictionary.Source.OriginalString;
53 | if (LanguageStrings == 0)
54 | {
55 | _log.Warn($"No strings loaded from {LanguageFile}");
56 | }
57 | _log.Debug($"Current culture: {GetCurrentCulture()} UI: {GetCurrentUICulture()}");
58 | _log.Debug($"{LanguageStrings} strings loaded from {LanguageFile}");
59 | }
60 | #endregion Apply language settings
61 |
62 | #region Check if Use OS Language is set
63 | ///
64 | /// Check if the option to use the OS language is set and if the language is defined.
65 | ///
66 | /// The language code to check.
67 | /// True if the language is defined and the language exists. Otherwise return false.
68 | public static bool CheckUseOsLanguage(string language)
69 | {
70 | if (UserSettings.Setting!.UseOSLanguage)
71 | {
72 | if (UILanguage.DefinedLanguages.Exists(x => x.LanguageCode == language))
73 | {
74 | return true;
75 | }
76 | _log.Warn($"Language \"{language}\" has not been defined in this application. Defaulting to en-US and setting \"Use OS Language\" to false.");
77 | UserSettings.Setting.UseOSLanguage = false;
78 | ConfigHelpers.SaveSettings();
79 | }
80 | return false;
81 | }
82 | #endregion Check if Use OS Language is set
83 |
84 | #region Save settings and restart (after language change)
85 | ///
86 | /// Saves settings and restarts the application. Invoked when language is changed.
87 | ///
88 | public static void SaveAndRestart()
89 | {
90 | ConfigHelpers.SaveSettings();
91 | using Process p = new();
92 | p.StartInfo.FileName = AppInfo.AppPath;
93 | p.StartInfo.UseShellExecute = true;
94 | _ = p.Start();
95 | _log.Debug("Restarting for language change.");
96 | Application.Current.Shutdown();
97 | }
98 | #endregion Save settings and restart (after language change)
99 | }
100 |
--------------------------------------------------------------------------------
/MyScheduledTasks/MyScheduledTasks.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | MyScheduledTasks
5 | WinExe
6 | net8.0-windows
7 | 10.0.19041.41
8 | enable
9 | true
10 | My Scheduled Tasks
11 | © 2020-$([System.DateTime]::UtcNow.Year) Tim Kennedy
12 | Tim Kennedy
13 | T_K
14 | My Scheduled Tasks
15 | en-US
16 | Images\MST.ico
17 | AnyCPU;x64
18 |
19 |
20 |
21 |
22 | app.manifest
23 |
24 |
25 |
26 |
27 | Recommended
28 | 8.0
29 |
30 |
31 |
32 |
33 | en
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | all
53 | 3.9.50
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | PreserveNewest
63 |
64 |
65 | PreserveNewest
66 |
67 |
68 | PreserveNewest
69 |
70 |
71 | PreserveNewest
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | -File "$(ProjectDir)PowerShell\GenBuildInfo.ps1"
80 | -assemblyName $(AssemblyName)
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | My Scheduled Tasks
8 |
9 |
10 |
11 | [](https://github.com/Timthreetwelve/MyScheduledTasks/blob/main/LICENSE)
12 | [](https://dotnet.microsoft.com/en-us/download)
13 | [](https://github.com/Timthreetwelve/MyScheduledTasks/releases/latest)
14 | [](https://github.com/Timthreetwelve/MyScheduledTasks/releases/latest)
15 | [](https://github.com/Timthreetwelve/MyScheduledTasks/commits/main)
16 | [](https://github.com/Timthreetwelve/MyScheduledTasks/commits/main)
17 | [](https://github.com/Timthreetwelve/MyScheduledTasks/commits/main)
18 | [](https://docs.github.com/en/get-started/exploring-projects-on-github/saving-repositories-with-stars)
19 | [](https://github.com/Timthreetwelve/MyScheduledTasks/releases)
20 | [](https://github.com/Timthreetwelve/MyScheduledTasks/releases/latest)
21 | [](https://github.com/Timthreetwelve/MyScheduledTasks/issues)
22 | [](https://github.com/Timthreetwelve/MyScheduledTasks/issues)
23 |
24 |
25 |
26 | ### My Scheduled Tasks is an application that lets you keep track of the tasks in Windows Task Scheduler that you care about.
27 |
28 | #### My Scheduled Task is now Multilingual!
29 | Please see [Contribute a Translation](https://github.com/Timthreetwelve/MyScheduledTasks/wiki/Contribute-a-Translation) topic in the Wiki if you would like to contribute a translation.
30 | You will also find a topic on [Testing a Language File](https://github.com/Timthreetwelve/MyScheduledTasks/wiki/Testing-a-Language-File) in the wiki.
31 |
32 | #### My Scheduled Tasks runs on .NET 8
33 | Self-contained versions are available if .NET 8 isn't installed. Portable versions are also available. See the [releases page](https://github.com/Timthreetwelve/MyScheduledTasks/releases).
34 |
35 | #### Features
36 |
37 | * See the tasks that you have chosen in one window, no digging around in folders.
38 | * Easily add scheduled tasks to your list.
39 | * Optionally hide Microsoft tasks from the Add Tasks list.
40 | * Easily check the last result of the tasks in your list.
41 | * Run, Disable, Enable, or Delete individual tasks.
42 | * Export tasks to XML file.
43 | * Import tasks from XML file.
44 | * Add a note to individual tasks.
45 | * Select any task to see more details.
46 | * Optionally hide the details pane.
47 | * Optionally run My Scheduled Tasks as a scheduled task and get notification of tasks that have a non-zero result code.
48 | * Choose which columns are visible.
49 |
50 | #### Screenshot
51 | 
52 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Helpers/NLogHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace MyScheduledTasks.Helpers;
4 |
5 | ///
6 | /// Class for NLog helper methods
7 | ///
8 | internal static class NLogHelpers
9 | {
10 | ///
11 | /// Static instance for NLog Logger.
12 | ///
13 | ///
14 | /// Used with a "static using" in GlobalUsings.cs to avoid creating an instance in every class.
15 | ///
16 | internal static readonly Logger _log = LogManager.GetLogger("logTemp");
17 |
18 | #region Create the NLog configuration
19 | ///
20 | /// Configure NLog
21 | ///
22 | /// True to start with new log file. False to append to current file.
23 | public static void NLogConfig(bool newFile)
24 | {
25 | LoggingConfiguration config = new();
26 |
27 | // create log file Target for NLog
28 | FileTarget logfile = new("logfile")
29 | {
30 | // new file on startup
31 | DeleteOldFileOnStartup = newFile,
32 |
33 | // create the file if needed
34 | FileName = CreateFilename(),
35 |
36 | // message and footer layouts
37 | Footer = "${date:format=yyyy/MM/dd HH\\:mm\\:ss}",
38 | Layout = "${date:format=yyyy/MM/dd HH\\:mm\\:ss} " +
39 | "${pad:padding=-5:inner=${level:uppercase=true}} " +
40 | "${message}${onexception:${newline}${exception:format=tostring}}"
41 | };
42 |
43 | // add the log file target
44 | config.AddTarget(logfile);
45 |
46 | // add the rule for the log file
47 | LoggingRule file = new("*", LogLevel.Debug, logfile)
48 | {
49 | RuleName = "LogToFile"
50 | };
51 | config.LoggingRules.Add(file);
52 |
53 | // create debugger target
54 | DebuggerTarget debugger = new("debugger")
55 | {
56 | Layout = "${processtime} >>> ${message} "
57 | };
58 |
59 | // add the target
60 | config.AddTarget(debugger);
61 |
62 | // add the rule
63 | LoggingRule bug = new("*", LogLevel.Trace, debugger);
64 | config.LoggingRules.Add(bug);
65 |
66 | // add the configuration to NLog
67 | LogManager.Configuration = config;
68 |
69 | // Lastly, set the logging level based on setting
70 | SetLogLevel(UserSettings.Setting!.IncludeDebug);
71 | }
72 | #endregion Create the NLog configuration
73 |
74 | #region Create a filename in the temp folder
75 | private static string CreateFilename()
76 | {
77 | // create filename string
78 | string myName = AppInfo.AppName;
79 | string today = DateTime.Now.ToString("yyyyMMdd", CultureInfo.InvariantCulture);
80 | string filename = Debugger.IsAttached ? $"{myName}.{today}.debug.log" : $"{myName}.{today}.log";
81 |
82 | // combine temp folder with filename
83 | string tempDir = Path.GetTempPath();
84 | return Path.Combine(tempDir, "T_K", filename);
85 | }
86 | #endregion Create a filename in the temp folder
87 |
88 | #region Set NLog logging level
89 | ///
90 | /// Set the NLog logging level to Debug or Info
91 | ///
92 | /// If true set level to Debug, otherwise set to Info
93 | public static void SetLogLevel(bool debug)
94 | {
95 | LoggingConfiguration config = LogManager.Configuration;
96 |
97 | LoggingRule rule = config.FindRuleByName("LogToFile");
98 | if (rule != null)
99 | {
100 | LogLevel level = debug ? LogLevel.Debug : LogLevel.Info;
101 | rule.SetLoggingLevels(level, LogLevel.Fatal);
102 | LogManager.ReconfigExistingLoggers();
103 | }
104 | }
105 | #endregion Set NLog logging level
106 |
107 | #region Get the log file name
108 | ///
109 | /// Gets the filename for the NLog log fie
110 | ///
111 | ///
112 | public static string GetLogfileName()
113 | {
114 | LoggingConfiguration config = LogManager.Configuration;
115 | return (config.FindTargetByName("logfile")
116 | as FileTarget)?.FileName.Render(new LogEventInfo { TimeStamp = DateTime.Now })!;
117 | }
118 | #endregion Get the log file name
119 | }
120 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Models/Enums.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace MyScheduledTasks.Models;
4 |
5 | ///
6 | /// Navigation Page
7 | ///
8 | public enum NavPage
9 | {
10 | [Description("The Main Page")]
11 | Main = 0,
12 | [LocalizedDescription("SettingsEnum_Navigation_AddTasks")]
13 | AddTasks,
14 | [LocalizedDescription("SettingsEnum_Navigation_Settings")]
15 | Settings,
16 | [LocalizedDescription("SettingsEnum_Navigation_About")]
17 | About
18 | }
19 |
20 | #region Theme
21 | ///
22 | /// Theme type
23 | ///
24 | [TypeConverter(typeof(EnumDescriptionTypeConverter))]
25 | public enum ThemeType
26 | {
27 | [LocalizedDescription("SettingsEnum_Theme_Light")]
28 | Light = 0,
29 | [LocalizedDescription("SettingsEnum_Theme_Dark")]
30 | Dark = 1,
31 | [LocalizedDescription("SettingsEnum_Theme_Darker")]
32 | Darker = 2,
33 | [LocalizedDescription("SettingsEnum_Theme_System")]
34 | System = 3,
35 | [LocalizedDescription("SettingsEnum_Theme_DarkBlue")]
36 | DarkBlue = 4
37 | }
38 | #endregion Theme
39 |
40 | #region UI size
41 | ///
42 | /// Size of the UI
43 | ///
44 | [TypeConverter(typeof(EnumDescriptionTypeConverter))]
45 | public enum MySize
46 | {
47 | [LocalizedDescription("SettingsEnum_Size_Smallest")]
48 | Smallest = 0,
49 | [LocalizedDescription("SettingsEnum_Size_Smaller")]
50 | Smaller = 1,
51 | [LocalizedDescription("SettingsEnum_Size_Small")]
52 | Small = 2,
53 | [LocalizedDescription("SettingsEnum_Size_Default")]
54 | Default = 3,
55 | [LocalizedDescription("SettingsEnum_Size_Large")]
56 | Large = 4,
57 | [LocalizedDescription("SettingsEnum_Size_Larger")]
58 | Larger = 5,
59 | [LocalizedDescription("SettingsEnum_Size_Largest")]
60 | Largest = 6
61 | }
62 | #endregion UI size
63 |
64 | #region Accent color
65 | ///
66 | /// One of the 19 predefined Material Design in XAML colors plus Black & White
67 | ///
68 | [TypeConverter(typeof(EnumDescriptionTypeConverter))]
69 | public enum AccentColor
70 | {
71 | [LocalizedDescription("SettingsEnum_AccentColor_Red")]
72 | Red = 0,
73 | [LocalizedDescription("SettingsEnum_AccentColor_Pink")]
74 | Pink = 1,
75 | [LocalizedDescription("SettingsEnum_AccentColor_Purple")]
76 | Purple = 2,
77 | [LocalizedDescription("SettingsEnum_AccentColor_DeepPurple")]
78 | DeepPurple = 3,
79 | [LocalizedDescription("SettingsEnum_AccentColor_Indigo")]
80 | Indigo = 4,
81 | [LocalizedDescription("SettingsEnum_AccentColor_Blue")]
82 | Blue = 5,
83 | [LocalizedDescription("SettingsEnum_AccentColor_LightBlue")]
84 | LightBlue = 6,
85 | [LocalizedDescription("SettingsEnum_AccentColor_Cyan")]
86 | Cyan = 7,
87 | [LocalizedDescription("SettingsEnum_AccentColor_Teal")]
88 | Teal = 8,
89 | [LocalizedDescription("SettingsEnum_AccentColor_Green")]
90 | Green = 9,
91 | [LocalizedDescription("SettingsEnum_AccentColor_LightGreen")]
92 | LightGreen = 10,
93 | [LocalizedDescription("SettingsEnum_AccentColor_Lime")]
94 | Lime = 11,
95 | [LocalizedDescription("SettingsEnum_AccentColor_Yellow")]
96 | Yellow = 12,
97 | [LocalizedDescription("SettingsEnum_AccentColor_Amber")]
98 | Amber = 13,
99 | [LocalizedDescription("SettingsEnum_AccentColor_Orange")]
100 | Orange = 14,
101 | [LocalizedDescription("SettingsEnum_AccentColor_DeepOrange")]
102 | DeepOrange = 15,
103 | [LocalizedDescription("SettingsEnum_AccentColor_Brown")]
104 | Brown = 16,
105 | [LocalizedDescription("SettingsEnum_AccentColor_Gray")]
106 | Gray = 17,
107 | [LocalizedDescription("SettingsEnum_AccentColor_BlueGray")]
108 | BlueGray = 18,
109 | [LocalizedDescription("SettingsEnum_AccentColor_Black")]
110 | Black = 19,
111 | [LocalizedDescription("SettingsEnum_AccentColor_White")]
112 | White = 20,
113 | }
114 | #endregion Accent color
115 |
116 | #region Spacing
117 | ///
118 | /// Space between rows in the data grids
119 | ///
120 | [TypeConverter(typeof(EnumDescriptionTypeConverter))]
121 | public enum Spacing
122 | {
123 | [LocalizedDescription("SettingsEnum_Spacing_Compact")]
124 | Compact = 0,
125 | [LocalizedDescription("SettingsEnum_Spacing_Comfortable")]
126 | Comfortable = 1,
127 | [LocalizedDescription("SettingsEnum_Spacing_Wide")]
128 | Wide = 2
129 | }
130 | #endregion Spacing
131 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Styles/ExpanderStyles.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
20 |
21 |
22 |
23 |
39 |
40 |
41 |
42 |
58 |
59 |
60 |
61 |
80 |
81 |
82 |
83 |
84 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/MyScheduledTasks/ViewModels/AddTasksViewModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace MyScheduledTasks.ViewModels;
4 |
5 | internal sealed partial class AddTasksViewModel
6 | {
7 | #region Private field
8 | private static int _itemsAdded;
9 | #endregion Private field
10 |
11 | #region Add selected items to TaskList
12 | ///
13 | /// Add all selected items
14 | ///
15 | /// Name of the DataGrid
16 | private static void AddSelectedItems(DataGrid grid)
17 | {
18 | if (grid.SelectedItems.Count > 0)
19 | {
20 | _itemsAdded = 0;
21 | foreach (AllTasks item in grid.SelectedItems)
22 | {
23 | AddToMyTasks(item);
24 | }
25 | if (_itemsAdded > 0)
26 | {
27 | _log.Info($"{_itemsAdded} task(s) added");
28 | string msg = string.Format(CultureInfo.CurrentCulture, AddTasksTasksAdded, _itemsAdded);
29 | SnackbarMsg.QueueMessage(msg, 3000);
30 | TaskFileHelpers.WriteTasksToFile();
31 | }
32 |
33 | grid.UnselectAll();
34 | }
35 | }
36 | #endregion Add selected items to TaskList
37 |
38 | #region Add a single item
39 | ///
40 | /// Adds a single item to the tasks list
41 | ///
42 | /// Path of the item to be added
43 | /// True if the task was added. Otherwise returns false.
44 | internal static bool AddToMyTasks(AllTasks item)
45 | {
46 | Task? task = GetTaskInfo(item.TaskPath!);
47 | if (task == null)
48 | {
49 | string msg = string.Format(CultureInfo.InvariantCulture, AddTasksNotFound, item.TaskName);
50 | _log.Error($"The Scheduled Task \"{item.TaskPath}\" was not found.");
51 | _ = new MDCustMsgBox(msg,
52 | GetStringResource("AddTasks_Error"),
53 | ButtonType.Ok,
54 | false,
55 | true,
56 | null,
57 | true).ShowDialog();
58 | return false;
59 | }
60 |
61 | if (ScheduledTask.TaskList.Any(p => p.TaskPath == task.Path))
62 | {
63 | int pos = ScheduledTask.TaskList.IndexOf(ScheduledTask.TaskList.FirstOrDefault(x => x.TaskPath == task.Path)!);
64 | _log.Warn($"{task.Path} is already present in the list in position {pos + 1}");
65 | string msg = string.Format(CultureInfo.InvariantCulture, AddTasksTaskAlreadyAdded, task.Path);
66 | SnackbarMsg.QueueMessage(msg, 3000);
67 | return false;
68 | }
69 | ScheduledTask schedTask = ScheduledTask.BuildScheduledTask(task, null);
70 | ScheduledTask.TaskList.Add(schedTask);
71 |
72 | MyTasks newTask = new(task.Path, false, string.Empty);
73 | MyTasks.MyTasksCollection!.Add(newTask);
74 |
75 | _log.Info($"Added: \"{task.Path}\"");
76 | _itemsAdded++;
77 | return true;
78 | }
79 | #endregion Add a single item
80 |
81 | #region Include or exclude Microsoft tasks
82 | ///
83 | /// Determine source of add tasks list
84 | ///
85 | /// Name of the DataGrid
86 | private static void DetermineSource(DataGrid grid)
87 | {
88 | grid.ItemsSource = UserSettings.Setting!.HideMicrosoftFolder ? AllTasks.Non_MS_TasksCollection : AllTasks.All_TasksCollection;
89 | }
90 | #endregion Include or exclude Microsoft tasks
91 |
92 | #region Get info for a task
93 |
94 | private static Task? GetTaskInfo(string name)
95 | {
96 | using TaskService ts = new();
97 | return ts.GetTask(name);
98 | }
99 | #endregion Get info for a task
100 |
101 | #region Relay commands
102 | [RelayCommand]
103 | public static void HideTasks(DataGrid grid)
104 | {
105 | DetermineSource(grid);
106 | }
107 |
108 | [RelayCommand]
109 | private static void RefreshTasks()
110 | {
111 | TaskHelpers.GetAllTasks();
112 | }
113 |
114 | [RelayCommand]
115 | private static void AddTasks(DataGrid grid)
116 | {
117 | AddSelectedItems(grid);
118 | }
119 | #endregion Relay commands
120 |
121 | #region Static property and change event handler for filter text
122 | public static event EventHandler? StaticPropertyChanged;
123 |
124 | private static string? _filterText;
125 | public static string FilterText
126 | {
127 | get => _filterText!;
128 | set
129 | {
130 | if (_filterText != value)
131 | {
132 | _filterText = value;
133 | StaticPropertyChanged?.Invoke(null, new PropertyChangedEventArgs(FilterText));
134 | }
135 | }
136 | }
137 | #endregion Static property and change event handler for filter text
138 | }
139 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Dialogs/MDCustMsgBox.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | // Inspired by https://stackoverflow.com/a/60302166
4 |
5 | namespace MyScheduledTasks.Dialogs;
6 |
7 | ///
8 | /// Custom message box that works well with Material Design in XAML.
9 | ///
10 | public partial class MDCustMsgBox : Window
11 | {
12 | #region Public Property
13 | public static CustResultType CustResult { get; private set; }
14 | #endregion
15 |
16 | ///
17 | /// Custom message box for MDIX
18 | ///
19 | /// Text of the message
20 | /// Text that goes in the title bar
21 | /// OK, OKCancel, YesNoCancel or YesNo
22 | /// True to hide close button
23 | /// True to make window topmost
24 | /// Owner of the window
25 | /// True will set accent color to OrangeRed
26 | public MDCustMsgBox(string Message,
27 | string Title,
28 | ButtonType Buttons,
29 | bool HideClose = false,
30 | bool OnTop = true,
31 | Window? MsgBoxOwner = null,
32 | bool IsError = false)
33 | {
34 | InitializeComponent();
35 |
36 | DataContext = this;
37 |
38 | #region Topmost
39 | if (OnTop)
40 | {
41 | Topmost = true;
42 | }
43 | #endregion
44 |
45 | #region Message text
46 | TxtMessage.Text = Message;
47 | #endregion Message text
48 |
49 | #region Message box title
50 | TxtTitle.Text = string.IsNullOrEmpty(Title) ? Application.Current!.MainWindow!.Title : Title;
51 | #endregion Message box title
52 |
53 | #region Button visibility
54 | switch (Buttons)
55 | {
56 | case ButtonType.Ok:
57 | BtnCancel.Visibility = Visibility.Collapsed;
58 | BtnYes.Visibility = Visibility.Collapsed;
59 | BtnNo.Visibility = Visibility.Collapsed;
60 | _ = BtnOk.Focus();
61 | break;
62 |
63 | case ButtonType.OkCancel:
64 | BtnYes.Visibility = Visibility.Collapsed;
65 | BtnNo.Visibility = Visibility.Collapsed;
66 | _ = BtnOk.Focus();
67 | break;
68 |
69 | case ButtonType.YesNo:
70 | BtnOk.Visibility = Visibility.Collapsed;
71 | BtnCancel.Visibility = Visibility.Collapsed;
72 | _ = BtnYes.Focus();
73 | break;
74 |
75 | case ButtonType.YesNoCancel:
76 | BtnOk.Visibility = Visibility.Collapsed;
77 | _ = BtnYes.Focus();
78 | break;
79 | }
80 | if (HideClose)
81 | {
82 | BtnClose.Visibility = Visibility.Collapsed;
83 | }
84 | #endregion Button visibility
85 |
86 | #region Window position
87 | if (MsgBoxOwner != null)
88 | {
89 | Owner = MsgBoxOwner;
90 | WindowStartupLocation = Owner.IsVisible ? WindowStartupLocation.CenterOwner : WindowStartupLocation.CenterScreen;
91 | }
92 | else
93 | {
94 | WindowStartupLocation = WindowStartupLocation.CenterScreen;
95 | }
96 | #endregion Window position
97 |
98 | #region Error message
99 | if (IsError)
100 | {
101 | BorderBrush = Brushes.OrangeRed;
102 | BorderThickness = new Thickness(2);
103 | CardHeader.Background = BorderBrush;
104 | CardHeader.FontWeight = FontWeights.Bold;
105 | }
106 | #endregion Error message
107 | }
108 |
109 | #region Mouse event
110 | private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
111 | {
112 | DragMove();
113 | }
114 | #endregion Mouse event
115 |
116 | #region Button commands
117 | [RelayCommand]
118 | private void CancelButton()
119 | {
120 | Close();
121 | CustResult = CustResultType.Cancel;
122 | }
123 |
124 | [RelayCommand]
125 | private void OKButton()
126 | {
127 | Close();
128 | CustResult = CustResultType.Ok;
129 | }
130 |
131 | [RelayCommand]
132 | private void YesButton()
133 | {
134 | Close();
135 | CustResult = CustResultType.Yes;
136 | }
137 |
138 | [RelayCommand]
139 | private void NoButton()
140 | {
141 | Close();
142 | CustResult = CustResultType.No;
143 | }
144 | #endregion Button commands
145 | }
146 |
147 | #region Button type enumeration
148 | public enum ButtonType
149 | {
150 | OkCancel,
151 | YesNo,
152 | YesNoCancel,
153 | Ok,
154 | }
155 | #endregion Button type enumeration
156 |
157 | #region Result type enumeration
158 | public enum CustResultType
159 | {
160 | Ok,
161 | Yes,
162 | No,
163 | Cancel
164 | }
165 | #endregion Result type enumeration
166 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | timthreetwelve@outlook.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Helpers/AppInfo.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace MyScheduledTasks.Helpers;
4 |
5 | ///
6 | /// Class to return information about the current application
7 | ///
8 | public static class AppInfo
9 | {
10 | ///
11 | /// Returns the operating system description e.g. Microsoft Windows 10.0.19044
12 | ///
13 | public static string OsPlatform => RuntimeInformation.OSDescription;
14 |
15 | ///
16 | /// Returns the framework name
17 | ///
18 | public static string? Framework => Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName;
19 |
20 | ///
21 | /// Returns the framework description
22 | ///
23 | public static string RuntimeVersion => RuntimeInformation.FrameworkDescription;
24 |
25 | ///
26 | /// Returns the version number in Major.Minor.Build format
27 | ///
28 | private static string TitleVersion => Assembly.GetEntryAssembly()!.GetName().Version!.ToString().Remove(Assembly.GetEntryAssembly()!.GetName().Version!.ToString().LastIndexOf('.'));
29 |
30 | ///
31 | /// Returns the file version
32 | ///
33 | public static string AppFileVersion => Assembly.GetEntryAssembly()!.GetCustomAttribute()?.Version ?? "missing";
34 |
35 | ///
36 | /// Returns the full version number as String
37 | ///
38 | public static string AppVersion => Assembly.GetEntryAssembly()!.GetName().Version!.ToString();
39 |
40 | ///
41 | /// Returns the full version number as Version
42 | ///
43 | public static Version AppVersionVer => Assembly.GetEntryAssembly()!.GetName().Version!;
44 |
45 | ///
46 | /// Returns the app's full path including the EXE name
47 | ///
48 | public static string AppPath => Environment.ProcessPath!;
49 |
50 | ///
51 | /// Returns the app's full path excluding the EXE name
52 | ///
53 | public static string AppDirectory => Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) ?? "missing";
54 |
55 | ///
56 | /// Returns the app's name without the extension
57 | ///
58 | public static string AppName => Assembly.GetEntryAssembly()!.GetName().Name ?? "missing";
59 |
60 | ///
61 | /// Returns the app's name with the extension
62 | ///
63 | public static string AppExeName => Path.GetFileName(AppPath);
64 |
65 | ///
66 | /// Returns the app's full name (name, version, culture, etc.)
67 | ///
68 | public static string AppFullName => Assembly.GetEntryAssembly()!.GetName().FullName;
69 |
70 | ///
71 | /// Returns the Company Name from the Assembly info
72 | ///
73 | public static string AppCompany => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).CompanyName ?? "missing";
74 |
75 | ///
76 | /// Returns the Author from the Assembly info
77 | ///
78 | public static string AppDescription => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).FileDescription ?? "missing";
79 |
80 | ///
81 | /// Returns the product version from the Assembly info
82 | ///
83 | public static string AppProductVersion => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).ProductVersion ?? "missing";
84 |
85 | ///
86 | /// Returns the Copyright info from the Assembly info
87 | ///
88 | public static string AppCopyright => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).LegalCopyright ?? "missing";
89 |
90 | ///
91 | /// Returns the Product Name from the Assembly info
92 | ///
93 | public static string AppProduct => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).ProductName ?? "missing";
94 |
95 | ///
96 | /// Returns the File Name from the Assembly info
97 | ///
98 | public static string AppFileName => FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).FileName;
99 |
100 | ///
101 | /// Combines the product name with the title version.
102 | ///
103 | ///
104 | /// String in the format: AppName - 0.0.1
105 | ///
106 | public static string ToolTipVersion => $"{AppProduct} - {TitleVersion}";
107 |
108 | ///
109 | /// Returns the Process Name
110 | ///
111 | public static string AppProcessName => Process.GetCurrentProcess().ProcessName;
112 |
113 | ///
114 | /// Returns the Process ID as Int
115 | ///
116 | public static int AppProcessID => Environment.ProcessId;
117 |
118 | ///
119 | /// Returns the Process Start Time as DateTime
120 | ///
121 | public static DateTime AppProcessStart => Process.GetCurrentProcess().StartTime;
122 |
123 | ///
124 | /// Returns the Process MainModule
125 | ///
126 | public static string AppProcessMainModule => Process.GetCurrentProcess().MainModule!.ModuleName;
127 |
128 | ///
129 | /// The CLR version
130 | ///
131 | public static string CLRVersion => Environment.Version.ToString();
132 |
133 | ///
134 | /// True if running as administrator
135 | ///
136 | public static bool IsAdmin => new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
137 | }
138 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Configuration/UserSettings.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace MyScheduledTasks.Configuration;
4 |
5 | [INotifyPropertyChanged]
6 | public partial class UserSettings : ConfigManager
7 | {
8 | #region Properties
9 | ///
10 | /// Add text to root folder.
11 | ///
12 | [ObservableProperty]
13 | private bool _annotateRoot = true;
14 |
15 | ///
16 | /// Check for updates automatically when About page is opened.
17 | ///
18 | [ObservableProperty]
19 | private bool _autoCheckForUpdates = true;
20 |
21 | ///
22 | /// Height of the details pane.
23 | ///
24 | [ObservableProperty]
25 | private double _detailsHeight = 520;
26 |
27 | ///
28 | /// Used to determine used to determine scaling of dialogs.
29 | ///
30 | [ObservableProperty]
31 | private static double _dialogScale = 1;
32 |
33 | ///
34 | /// Don't display Microsoft folder.
35 | ///
36 | [ObservableProperty]
37 | private bool _hideMicrosoftFolder;
38 |
39 | ///
40 | /// Include debug level messages in the log file.
41 | ///
42 | [ObservableProperty]
43 | private bool _includeDebug = true;
44 |
45 | ///
46 | /// Keep window topmost.
47 | ///
48 | [ObservableProperty]
49 | private bool _keepOnTop;
50 |
51 | ///
52 | /// Enable language testing.
53 | ///
54 | [ObservableProperty]
55 | private bool _languageTesting;
56 |
57 | ///
58 | /// Accent color.
59 | ///
60 | [ObservableProperty]
61 | private AccentColor _primaryColor = AccentColor.Blue;
62 |
63 | ///
64 | /// Vertical spacing in the data grids.
65 | ///
66 | [ObservableProperty]
67 | private Spacing _rowSpacing = Spacing.Comfortable;
68 |
69 | ///
70 | /// Font used in datagrids.
71 | ///
72 | [ObservableProperty]
73 | private string? _selectedFont = "Segoe UI";
74 |
75 | ///
76 | /// Font size used throughout the application.
77 | /// Defaults to 14 which was the original size.
78 | ///
79 | [ObservableProperty]
80 | private double _selectedFontSize = 14;
81 |
82 | ///
83 | /// Show the advanced menu.
84 | ///
85 | [ObservableProperty]
86 | private bool _showAdvancedMenu;
87 |
88 | ///
89 | /// Show the alert column in the grid.
90 | ///
91 | [ObservableProperty]
92 | private bool _showAlertCol = true;
93 |
94 | ///
95 | /// Show the details pane at the bottom.
96 | ///
97 | [ObservableProperty]
98 | private bool _showDetails = true;
99 |
100 | ///
101 | /// Show Exit in the navigation menu.
102 | ///
103 | [ObservableProperty]
104 | private bool _showExitInNav = true;
105 |
106 | ///
107 | /// Show the folder column.
108 | ///
109 | [ObservableProperty]
110 | private bool _showFolderCol = true;
111 |
112 | ///
113 | /// Show the last run column.
114 | ///
115 | [ObservableProperty]
116 | private bool _showLastRunCol = true;
117 |
118 | ///
119 | /// Show the next run column.
120 | ///
121 | [ObservableProperty]
122 | private bool _showNextRunCol;
123 |
124 | ///
125 | /// Show the note column.
126 | ///
127 | [ObservableProperty]
128 | private bool _showNoteCol = true;
129 |
130 | ///
131 | /// Show the Result column.
132 | ///
133 | [ObservableProperty]
134 | private bool _showResultCol = true;
135 |
136 | ///
137 | /// Show the Status column.
138 | ///
139 | [ObservableProperty]
140 | private bool _showStatusCol = true;
141 |
142 | ///
143 | /// Use sound.
144 | ///
145 | [ObservableProperty]
146 | private bool _sound = true;
147 |
148 | ///
149 | /// Option start with window centered on screen.
150 | ///
151 | [ObservableProperty]
152 | private bool _startCentered = true;
153 |
154 | ///
155 | /// Defined language to use in the UI.
156 | ///
157 | [ObservableProperty]
158 | private string _uILanguage = "en-US";
159 |
160 | ///
161 | /// Amount of UI zoom.
162 | ///
163 | [ObservableProperty]
164 | private MySize _uISize = MySize.Default;
165 |
166 | ///
167 | /// Theme type.
168 | ///
169 | [ObservableProperty]
170 | private ThemeType _uITheme = ThemeType.System;
171 |
172 | ///
173 | /// Use the operating system language (if one has been provided).
174 | ///
175 | [ObservableProperty]
176 | private bool _useOSLanguage;
177 |
178 | ///
179 | /// Height of the window.
180 | ///
181 | [ObservableProperty]
182 | private double _windowHeight = 800;
183 |
184 | ///
185 | /// Position of left side of the window.
186 | ///
187 | [ObservableProperty]
188 | private double _windowLeft = 100;
189 |
190 | ///
191 | /// Position of the top side of the window.
192 | ///
193 | [ObservableProperty]
194 | private double _windowTop = 100;
195 |
196 | ///
197 | /// Width of the window.
198 | ///
199 | [ObservableProperty]
200 | private double _windowWidth = 1400;
201 | #endregion Properties
202 | }
203 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Helpers/TaskFileHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace MyScheduledTasks.Helpers;
4 |
5 | internal static class TaskFileHelpers
6 | {
7 | #region Tasks filename
8 | ///
9 | /// Name of the tasks file.
10 | ///
11 | public static string TasksFile { get; } = Path.Combine(AppInfo.AppDirectory, "MyTasks.json");
12 | #endregion Tasks filename
13 |
14 | #region JSON serializer options
15 | ///
16 | /// JSON serializer options
17 | ///
18 | private static readonly JsonSerializerOptions _options = new()
19 | {
20 | WriteIndented = true
21 | };
22 | #endregion JSON serializer options
23 |
24 | #region MainWindow Instance
25 | private static readonly MainWindow? _mainWindow = Application.Current.MainWindow as MainWindow;
26 | #endregion MainWindow Instance
27 |
28 | #region Read the tasks JSON file
29 | ///
30 | /// Read the tasks file.
31 | ///
32 | public static void ReadMyTasks()
33 | {
34 | // If the file doesn't exist, create a minimal JSON file
35 | if (!File.Exists(TasksFile))
36 | {
37 | CreateEmptyFile();
38 | }
39 | // Read the JSON file
40 | try
41 | {
42 | string json = File.ReadAllText(TasksFile);
43 | MyTasks.MyTasksCollection = JsonSerializer.Deserialize>(json)!;
44 | _log.Info($"Read {MyTasks.MyTasksCollection.Count} items from {TasksFile} ");
45 | }
46 | // Can't really do much if the file is not readable
47 | catch (Exception ex)
48 | {
49 | _log.Fatal(ex, $"Error reading {TasksFile}");
50 | string msg = string.Format(CultureInfo.InvariantCulture, MsgTextErrorReadingFile, TasksFile);
51 | msg += $"\n\n{ex.Message}\n\n{GetStringResource("MsgText_ErrorFatal")}";
52 | _ = new MDCustMsgBox(msg,
53 | GetStringResource("MsgText_ErrorCaption"),
54 | ButtonType.Ok,
55 | true,
56 | true,
57 | null!,
58 | true).ShowDialog();
59 |
60 | // Quit via Environment.Exit so that normal shutdown processing doesn't run
61 | Environment.Exit(1);
62 | }
63 | }
64 |
65 | ///
66 | /// Create an empty JSON file if one doesn't exist.
67 | ///
68 | private static void CreateEmptyFile()
69 | {
70 | const string x = "[]";
71 | try
72 | {
73 | File.WriteAllText(TasksFile, x);
74 | }
75 | catch (Exception ex)
76 | {
77 | _log.Fatal(ex, $"Error creating {TasksFile}");
78 | string msg = string.Format(CultureInfo.InvariantCulture, MsgTextErrorCreatingFile, TasksFile);
79 | msg += $"\n\n{ex.Message}\n\n{GetStringResource("MsgText_ErrorFatal")}";
80 | _ = new MDCustMsgBox(msg,
81 | GetStringResource("MsgText_ErrorCaption"),
82 | ButtonType.Ok,
83 | true,
84 | true,
85 | null!,
86 | true).ShowDialog();
87 |
88 | // Quit via Environment.Exit so that normal shutdown processing doesn't run
89 | Environment.Exit(1);
90 | }
91 | }
92 | #endregion Read the tasks JSON file
93 |
94 | #region Write the tasks JSON file
95 | ///
96 | /// Convert MyTasksCollection to JSON and save it to a file
97 | ///
98 | /// Setting quiet to true will not display snack bar message
99 | public static void WriteTasksToFile(bool quiet = true)
100 | {
101 | try
102 | {
103 | string tasks = JsonSerializer.Serialize(MyTasks.MyTasksCollection, _options);
104 | File.WriteAllText(TasksFile, tasks);
105 | _log.Info($"Saving {MyTasks.MyTasksCollection!.Count} tasks to {TasksFile} ");
106 | if (!quiet)
107 | {
108 | SnackbarMsg.QueueMessage(GetStringResource("MsgText_FileSaved"), 3000);
109 | }
110 | MyTasks.IsDirty = false;
111 | }
112 | catch (Exception ex)
113 | {
114 | _log.Error(ex, $"Error saving {TasksFile}");
115 | string msg = string.Format(CultureInfo.InvariantCulture, MsgTextErrorSavingFile, TasksFile);
116 | msg += $"\n\n{ex.Message}";
117 | _ = new MDCustMsgBox(msg,
118 | GetStringResource("MsgText_ErrorCaption"),
119 | ButtonType.Ok,
120 | true,
121 | true,
122 | _mainWindow!,
123 | true).ShowDialog();
124 | }
125 | }
126 | #endregion Write the tasks JSON file
127 |
128 | #region Check for empty task list
129 | ///
130 | /// Display a message box if the tasks list is empty.
131 | ///
132 | public static void CheckEmptyList()
133 | {
134 | if (ScheduledTask.TaskList.Count == 0)
135 | {
136 | _ = new MDCustMsgBox(GetStringResource("MsgText_ErrorEmptyTaskList"),
137 | GetStringResource("MsgText_ErrorEmptyTaskListCaption"),
138 | ButtonType.YesNo,
139 | true,
140 | true,
141 | _mainWindow!)
142 | .ShowDialog();
143 |
144 | if (MDCustMsgBox.CustResult == CustResultType.Yes)
145 | {
146 | _mainWindow!.NavigationListBox.SelectedValue = NavigationViewModel.FindNavPage(NavPage.AddTasks);
147 | }
148 | }
149 | }
150 | #endregion Check for empty task list
151 | }
152 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Inno_Setup/MyScheduledTasksLocalization.iss:
--------------------------------------------------------------------------------
1 | [LangOptions]
2 | DialogFontSize=10
3 | DialogFontName="Segoe UI"
4 | WelcomeFontSize=12
5 | WelcomeFontName="Verdana"
6 |
7 | [Languages]
8 | Name: "en"; MessagesFile: "compiler:Default.isl"
9 | Name: "ko"; MessagesFile: "compiler:Languages\Korean.isl"
10 | Name: "es"; MessagesFile: "compiler:Languages\Spanish.isl"
11 | Name: "it"; MessagesFile: "compiler:Languages\Italian.isl"
12 | Name: "nl"; MessagesFile: "compiler:Languages\Dutch.isl"
13 | Name: "sk"; MessagesFile: "compiler:Languages\Slovak.isl"
14 |
15 | [Messages]
16 | en.SetupWindowTitle = Setup - {#MyAppName} {#MyAppVersion}
17 | ko.SetupWindowTitle = 설치 - {#MyAppName} {#MyAppVersion}
18 | es.SetupWindowTitle = Instalar - {#MyAppName} {#MyAppVersion}
19 | it.SetupWindowTitle = Installazione di {#MyAppName} {#MyAppVersion}
20 | nl.SetupWindowTitle = Setup - {#MyAppName} {#MyAppVersion}
21 | sk.SetupWindowTitle = Sprievodca inštaláciou - {#MyAppName} {#MyAppVersion}
22 |
23 | [CustomMessages]
24 | en.NotSelfContained=This will install the standard version of %1 version %2.%n%nThis version requires an existing installation of .NET 8 Desktop Runtime and is compatible with both x64 and x86 systems.%n%nIt Is recommended that you close all other applications before continuing.%n%nClick 'Next' to continue, or 'Cancel' to exit Setup.
25 | en.SelfContainedx86=This will install the self-contained x86 (32-bit) version of %1 version %2.%n%nIt Is recommended that you close all other applications before continuing.%n%nClick 'Next' to continue, or 'Cancel' to exit Setup.
26 | en.SelfContainedx64=This will install the self-contained x64 (64-bit) version of %1 version %2.%n%nIt Is recommended that you close all other applications before continuing.%n%nClick 'Next' to continue, or 'Cancel' to exit Setup.
27 | en.ViewReadme=View the ReadMe file
28 | en.AppIsRunning=is running, please close it to continue with the installation.
29 | en.ClearSettings=Do you want to remove the settings files and registry entries?%n%nSelect 'No' if you plan to reinstall the program.
30 |
31 | ko.NotSelfContained=이렇게 하면 %1 버전 %2.%n%n이 버전의 표준 버전이 설치됩니다. 이 버전은 .NET 8 데스크톱 런타임을 기존에 설치해야 하며 x64 및 x86 시스템과 모두 호환됩니다.%n%n계속하기 전에 다른 모든 응용 프로그램을 닫는 것이 좋습니다.%n%n계속하려면 '다음'을 클릭하거나 '취소'를 클릭하여 설치를 종료하는 것이 좋습니다.
32 | ko.SelfContainedx86=이렇게 하면 %1 버전의 자체 포함 x86 (32비트) 버전 %2.%n%n계속하기 전에 다른 모든 응용 프로그램을 닫는 것이 좋습니다.%n%n계속하려면 '다음'을 클릭하거나 '취소'를 클릭하여 설치를 종료합니다.
33 | ko.SelfContainedx64=이렇게 하면 %1 버전의 자체 포함 x64 (64비트) 버전 %2.%n%n계속하기 전에 다른 모든 응용 프로그램을 닫는 것이 좋습니다.%n%n계속하려면 '다음'을 클릭하거나 '취소'를 클릭하여 설치를 종료합니다.
34 | ko.ViewReadme=ReadMe 파일 보기
35 | ko.AppIsRunning=가 실행 중입니다. 설치를 계속하려면 닫으세요.
36 | ko.ClearSettings=설정 파일과 레지스트리 항목을 제거하시겠습니까? %n%n프로그램을 다시 설치하려면 '아니오'를 선택합니다.
37 |
38 | es.NotSelfContained=Esto instalará la versión estándar de la versión %2 de %1.%n%nEsta versión requiere una instalación existente de .NET 8 Desktop Runtime y es compatible con los sistemas x64 y x86.%n%nSe recomienda cerrar todas las demás aplicaciones antes de continuar.%n%nHaga clic en 'Siguiente' para continuar o en 'Cancelar' para salir de la configuración.
39 | es.SelfContainedx86=Esto instalará la versión x86 (32 bits) independiente de la versión %2 de %1.%n%nSe recomienda cerrar todas las demás aplicaciones antes de continuar.%n%nHaga clic en 'Siguiente' para continuar o en 'Cancelar' para salir de la configuración.
40 | es.SelfContainedx64=Esto instalará la versión x64 (64 bits) independiente de la versión %2 de %1.%n%nSe recomienda cerrar todas las demás aplicaciones antes de continuar.%n%nHaga clic en 'Siguiente' para continuar o en 'Cancelar' para salir de la configuración.
41 | es.ViewReadme=Abrir el archivo Léame
42 | es.AppIsRunning=se está ejecutando, por favor ciérrelo para continuar con la instalación.
43 | es.ClearSettings=¿Desea eliminar la configuración y los archivos de datos?%n%nSeleccione 'No' si planea reinstalar.
44 |
45 | it.NotSelfContained=Verrà installata la versione standard di %1 versione %2.%n%nQuesta versione richiede che sia già installato .NET 8 Desktop Runtime ed è compatibile con i sistemi 32bit e 64bit.%n%nPrima di continuare l'instalalzione ti consigliamo di chiudere tutte le altre applicazioni.%n%nSeleziona 'Avanti' per continuare o 'Annulla' per uscire dall'installazione.
46 | it.SelfContainedx86=Verrà installata la versione standalone di %1 versione %2 32 bit.%n%nPrima di continuare l'instalalzione ti consigliamo di chiudere tutte le altre applicazioni.%n%nSelziona 'Avanti' per continuare o 'Annulla' per uscire dall'installazione.
47 | it.SelfContainedx64=Verrà installata la versione standalone di %1 versione %2 64 bit.%n%nPrima di continuare l'instalalzione ti consigliamo di chiudere tutte le altre applicazioni.%n%nSeleziona 'Avanti' per continuare o 'Annulla' per uscire dall'installazione.
48 | it.ViewReadme=Visualizza file ReadMe
49 | it.AppIsRunning=è in esecuzione.%nChiudilo per poter continuare l'installazione.
50 | it.ClearSettings=Vuoi rimuovere le impostazioni, i file cronologia e le voci del registro?%n%nSeleziona "No" se hai intenzione di reinstallare il programma.
51 |
52 | nl.NotSelfContained=Hiermee wordt de standaardversie van %1 versie %2 geïnstalleerd.%n%n%nDeze versie vereist een bestaande installatie van .NET 8 Desktop Runtime en is compatibel met zowel x64- als x86-systemen.%n%nHet wordt aanbevolen om alle andere toepassingen te sluiten voordat u doorgaat.%n%nKlik op 'volgende' om door te gaan of op 'annuleren' om de installatie af te sluiten.
53 | nl.SelfContainedx86=Hiermee wordt de portable x86 (32-bits) versie van %1 versie %2 geïnstalleerd.%n%n%nHet wordt aanbevolen om alle andere toepassingen te sluiten voordat u doorgaat.%n%nKlik op 'volgende' om door te gaan of op 'annuleren' om de installatie af te sluiten.
54 | nl.SelfContainedx64= Hiermee wordt de portable x64 (64-bits) versie van %1 versie %2 geïnstalleerd.%n%n%nHet wordt aanbevolen om alle andere toepassingen te sluiten voordat u doorgaat.%n%nKlik op 'volgende' om door te gaan of op 'annuleren' om de installatie af te sluiten.
55 | nl.ViewReadme=Open de ReadMe file
56 | nl.AppIsRunning=wordt uitgevoerd, sluit deze dan af om door te gaan met de installatie.
57 | nl.ClearSettings=Wilt u de instellingen en gegevensbestanden verwijderen?%n%nKies 'NEE' als u het programma opnieuw wilt installeren.
58 |
59 | sk.ViewReadme=Zobraziť súbor ReadMe
60 | sk.AppIsRunning=beží, zatvorte ho, aby ste mohli pokračovať v inštalácii.
61 | sk.ClearSettings=Chcete odstrániť súbory nastavení?%n%nAk plánujete preinštalovať, vyberte 'Nie'.
62 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Dialogs/EditNote.xaml:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
47 |
55 |
56 |
57 |
58 |
59 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
97 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Helpers/GitHubHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | // Leave the Octokit using statement here. It's a problem in GlobalUsings.cs
4 | using Octokit;
5 | using System.Threading.Tasks;
6 |
7 | namespace MyScheduledTasks.Helpers;
8 |
9 | ///
10 | /// Class for methods that check GitHub for releases
11 | ///
12 | internal static class GitHubHelpers
13 | {
14 | #region MainWindow Instance
15 | private static readonly MainWindow? _mainWindow = System.Windows.Application.Current.MainWindow as MainWindow;
16 | #endregion MainWindow Instance
17 |
18 | ///
19 | /// The application version from GitHub.
20 | ///
21 | public static Version? GitHubVersion { get; private set; }
22 |
23 | #region Check for newer release
24 | ///
25 | /// Checks to see if a newer release is available.
26 | ///
27 | ///
28 | /// If the release version is greater than the current version
29 | /// a message box will be shown asking to go to the releases page.
30 | ///
31 | public static async System.Threading.Tasks.Task CheckRelease()
32 | {
33 | try
34 | {
35 | SnackbarMsg.ClearAndQueueMessage(GetStringResource("MsgText_AppUpdateChecking"));
36 | Release release = await GetLatestReleaseAsync(AppConstString.RepoOwner, AppConstString.RepoName);
37 | if (release.TagName == null)
38 | {
39 | CheckFailed();
40 | return;
41 | }
42 | string tag = release.TagName;
43 |
44 | if (tag.StartsWith("v", StringComparison.InvariantCultureIgnoreCase))
45 | {
46 | tag = tag.ToLower(CultureInfo.InvariantCulture).TrimStart('v');
47 | }
48 |
49 | GitHubVersion = new(tag);
50 |
51 | _log.Debug($"Latest version is {GitHubVersion} released on {release.PublishedAt!.Value.UtcDateTime} UTC");
52 |
53 | if (GitHubVersion <= AppInfo.AppVersionVer)
54 | {
55 | string msg = GetStringResource("MsgText_AppUpdateNoneFound");
56 | _log.Debug("No newer releases were found.");
57 | _ = new MDCustMsgBox(msg,
58 | "My Scheduled Tasks",
59 | ButtonType.Ok,
60 | false,
61 | true,
62 | _mainWindow).ShowDialog();
63 | }
64 | else
65 | {
66 | _log.Debug($"A newer release ({GitHubVersion}) has been found.");
67 | string msg = string.Format(CultureInfo.InvariantCulture, MsgTextAppUpdateNewerFound, GitHubVersion);
68 | _ = new MDCustMsgBox($"{msg}\n\n" +
69 | $"{GetStringResource("MsgText_AppUpdateGoToRelease")}\n\n" +
70 | $"{GetStringResource("MsgText_AppUpdateCloseApp")}",
71 | "My Scheduled Tasks",
72 | ButtonType.YesNo,
73 | false,
74 | true,
75 | _mainWindow).ShowDialog();
76 |
77 | if (MDCustMsgBox.CustResult == CustResultType.Yes)
78 | {
79 | _log.Debug($"Opening {release.HtmlUrl}");
80 | string url = release.HtmlUrl;
81 | Process p = new();
82 | p.StartInfo.FileName = url;
83 | p.StartInfo.UseShellExecute = true;
84 | p.Start();
85 | System.Windows.Application.Current.Shutdown();
86 | }
87 | }
88 | }
89 | catch (Exception ex)
90 | {
91 | _log.Error(ex, "Error encountered while checking version");
92 | CheckFailed();
93 | }
94 | }
95 | #endregion Check for newer release
96 |
97 | #region Check for new release async
98 | ///
99 | /// Checks to see if a newer release is available asynchronously.
100 | ///
101 | /// True if a newer release is available, otherwise false.
102 | public static async Task CheckForNewReleaseAsync()
103 | {
104 | try
105 | {
106 | Release release = await GetLatestReleaseAsync(AppConstString.RepoOwner, AppConstString.RepoName);
107 | if (release.TagName == null)
108 | {
109 | return false;
110 | }
111 |
112 | string tag = release.TagName.Trim();
113 | if (string.IsNullOrEmpty(tag))
114 | {
115 | return false;
116 | }
117 |
118 | if (tag.StartsWith("v", StringComparison.InvariantCultureIgnoreCase))
119 | {
120 | tag = tag[1..]; // Remove the leading 'v'
121 | }
122 |
123 | if (!Version.TryParse(tag, out var version))
124 | {
125 | _log.Error($"Failed to parse version tag: {tag}");
126 | return false;
127 | }
128 |
129 | GitHubVersion = version;
130 | _log.Debug($"Latest version is {GitHubVersion} released on {release.PublishedAt!.Value.UtcDateTime} UTC");
131 | return GitHubVersion > AppInfo.AppVersionVer;
132 | }
133 | catch (Exception ex)
134 | {
135 | _log.Error(ex, "Error encountered while checking GitHub for latest release.");
136 | return false;
137 | }
138 | }
139 | #endregion Check for new release async
140 |
141 | #region Get latest release
142 | ///
143 | /// Gets the latest release.
144 | ///
145 | /// The repository owner.
146 | /// Name of the repository.
147 | /// Release object
148 | private static async Task GetLatestReleaseAsync(string repoOwner, string repoName)
149 | {
150 | try
151 | {
152 | GitHubClient client = new(new ProductHeaderValue(repoName));
153 | _log.Debug("Checking GitHub for latest release.");
154 | return await client.Repository.Release.GetLatest(repoOwner, repoName);
155 | }
156 | catch (Exception ex)
157 | {
158 | _log.Error(ex, "Get latest release from GitHub failed.");
159 | return new();
160 | }
161 | }
162 | #endregion Get latest release
163 |
164 | #region Check failed message
165 | ///
166 | /// Display a message box stating that the release check failed.
167 | ///
168 | private static void CheckFailed()
169 | {
170 | _ = new MDCustMsgBox(GetStringResource("MsgText_AppUpdateCheckFailed"),
171 | "My Scheduled Tasks",
172 | ButtonType.Ok,
173 | false,
174 | true,
175 | _mainWindow,
176 | true).ShowDialog();
177 | }
178 | #endregion Check failed message
179 | }
180 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Configuration/ConfigHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace MyScheduledTasks.Configuration;
4 |
5 | ///
6 | /// Class for methods used for creating, reading and saving settings.
7 | ///
8 | internal static class ConfigHelpers
9 | {
10 | #region Properties & fields
11 | public static string? SettingsFileName { get; private set; }
12 |
13 | private static readonly JsonSerializerOptions _options = new()
14 | {
15 | WriteIndented = true
16 | };
17 | #endregion Properties & fields
18 |
19 | #region MainWindow Instance
20 | private static readonly MainWindow? _mainWindow = Application.Current.MainWindow as MainWindow;
21 | #endregion MainWindow Instance
22 |
23 | #region Initialize settings
24 | ///
25 | /// Initialization method. Gets the file name for settings file and creates it if it
26 | /// doesn't exist.
27 | ///
28 | /// Option name of settings file
29 | public static void InitializeSettings(string settingsFile = "usersettings.json")
30 | {
31 | string? settingsDir = Path.GetDirectoryName(AppContext.BaseDirectory);
32 | SettingsFileName = Path.Combine(settingsDir!, settingsFile);
33 |
34 | if (!File.Exists(SettingsFileName))
35 | {
36 | UserSettings.Setting = new UserSettings();
37 | SaveSettings();
38 | }
39 | ConfigManager.Setting = ReadConfiguration();
40 |
41 | ConfigManager.Setting = new TempSettings();
42 | }
43 | #endregion Initialize settings
44 |
45 | #region Read setting from file
46 | ///
47 | /// Read settings from JSON file.
48 | ///
49 | /// UserSettings
50 | private static UserSettings ReadConfiguration()
51 | {
52 | try
53 | {
54 | return JsonSerializer.Deserialize(File.ReadAllText(SettingsFileName!))!;
55 | }
56 | catch (Exception ex)
57 | {
58 | _ = MessageBox.Show($"{GetStringResource("MsgText_ErrorReadingSettings")}\n{ex.Message}",
59 | GetStringResource("MsgText_ErrorCaption"),
60 | MessageBoxButton.OK,
61 | MessageBoxImage.Error);
62 | return new UserSettings();
63 | }
64 | }
65 | #endregion Read setting from file
66 |
67 | #region Save settings to JSON file
68 | ///
69 | /// Write settings to JSON file.
70 | ///
71 | public static void SaveSettings()
72 | {
73 | try
74 | {
75 | string json = JsonSerializer.Serialize(UserSettings.Setting, _options);
76 | File.WriteAllText(SettingsFileName!, json);
77 | }
78 | catch (Exception ex)
79 | {
80 | _ = MessageBox.Show($"{GetStringResource("MsgText_ErrorSavingSettings")}\n{ex.Message}",
81 | GetStringResource("MsgText_ErrorCaption"),
82 | MessageBoxButton.OK,
83 | MessageBoxImage.Error);
84 | }
85 | }
86 | #endregion Save settings to JSON file
87 |
88 | #region Export settings
89 | ///
90 | /// Exports the current settings to a JSON file.
91 | ///
92 | public static void ExportSettings()
93 | {
94 | try
95 | {
96 | string appPart = AppInfo.AppProduct.Replace(" ", "");
97 | string settingsPart = GetStringResource("NavItem_Settings");
98 | string datePart = DateTime.Now.ToString("yyyyMMdd", CultureInfo.CurrentCulture);
99 | SaveFileDialog saveFile = new()
100 | {
101 | CheckPathExists = true,
102 | Filter = "JSON File|*.json|All Files|*.*",
103 | FileName = $"{appPart}_{settingsPart}_{datePart}.json"
104 | };
105 |
106 | if (saveFile.ShowDialog() == true)
107 | {
108 | _log.Debug($"Exporting settings file to {saveFile.FileName}.");
109 | string json = JsonSerializer.Serialize(UserSettings.Setting, _options);
110 | File.WriteAllText(saveFile.FileName, json);
111 | }
112 | }
113 | catch (Exception ex)
114 | {
115 | _log.Debug(ex, "Error exporting settings file.");
116 | _ = MessageBox.Show($"{GetStringResource("MsgText_ErrorExportingSettings")}\n{ex.Message}",
117 | GetStringResource("MsgText_ErrorCaption"),
118 | MessageBoxButton.OK,
119 | MessageBoxImage.Error);
120 | }
121 | }
122 | #endregion Export settings
123 |
124 | #region Import settings
125 | ///
126 | /// Imports settings from a previously exported file.
127 | ///
128 | public static void ImportSettings()
129 | {
130 | try
131 | {
132 | OpenFileDialog importFile = new()
133 | {
134 | CheckPathExists = true,
135 | CheckFileExists = true,
136 | Filter = "JSON File|*.json",
137 | InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
138 | };
139 |
140 | if (importFile.ShowDialog() == true)
141 | {
142 | _log.Debug($"Importing settings file from {importFile.FileName}.");
143 | ConfigManager.Setting = JsonSerializer.Deserialize(File.ReadAllText(importFile.FileName))!;
144 | SaveSettings();
145 |
146 | _ = new MDCustMsgBox($"{GetStringResource("MsgText_ImportSettingsRestart")}",
147 | "My Scheduled Tasks",
148 | ButtonType.Ok,
149 | false,
150 | true,
151 | _mainWindow).ShowDialog();
152 | }
153 | }
154 | catch (Exception ex)
155 | {
156 | _log.Debug(ex, "Error importing settings file.");
157 | _ = MessageBox.Show($"{GetStringResource("MsgText_ErrorImportingSettings")}\n{ex.Message}",
158 | GetStringResource("MsgText_ErrorCaption"),
159 | MessageBoxButton.OK,
160 | MessageBoxImage.Error);
161 | }
162 | }
163 | #endregion Import settings
164 |
165 | #region Dump settings into the log
166 | ///
167 | /// Dumps (writes) current settings to the log file.
168 | ///
169 | public static void DumpSettings()
170 | {
171 | string dashes = new('-', 25);
172 | string header = $"{dashes} Begin Settings {dashes}";
173 | string trailer = $"{dashes} End Settings {dashes}";
174 | _log.Debug(header);
175 | PropertyInfo[] properties = typeof(UserSettings).GetProperties();
176 | int maxLength = properties.Max(s => s.Name.Length);
177 | foreach (PropertyInfo property in properties)
178 | {
179 | string? value = property.GetValue(UserSettings.Setting, [])!.ToString();
180 | _log.Debug($"{property.Name.PadRight(maxLength)} : {value}");
181 | }
182 | _log.Debug(trailer);
183 | }
184 | #endregion Dump settings into the log
185 | }
186 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | [Aa][Rr][Mm]/
24 | [Aa][Rr][Mm]64/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 |
30 | # Visual Studio 2015/2017 cache/options directory
31 | .vs/
32 | # Uncomment if you have tasks that create the project's static files in wwwroot
33 | #wwwroot/
34 |
35 | # Visual Studio 2017 auto generated files
36 | Generated\ Files/
37 |
38 | # MSTest test Results
39 | [Tt]est[Rr]esult*/
40 | [Bb]uild[Ll]og.*
41 |
42 | # NUNIT
43 | *.VisualState.xml
44 | TestResult.xml
45 |
46 | # Build Results of an ATL Project
47 | [Dd]ebugPS/
48 | [Rr]eleasePS/
49 | dlldata.c
50 |
51 | # Benchmark Results
52 | BenchmarkDotNet.Artifacts/
53 |
54 | # .NET Core
55 | project.lock.json
56 | project.fragment.lock.json
57 | artifacts/
58 |
59 | # StyleCop
60 | StyleCopReport.xml
61 |
62 | # Files built by Visual Studio
63 | *_i.c
64 | *_p.c
65 | *_h.h
66 | *.ilk
67 | *.meta
68 | *.obj
69 | *.iobj
70 | *.pch
71 | *.pdb
72 | *.ipdb
73 | *.pgc
74 | *.pgd
75 | *.rsp
76 | *.sbr
77 | *.tlb
78 | *.tli
79 | *.tlh
80 | *.tmp
81 | *.tmp_proj
82 | *_wpftmp.csproj
83 | *.log
84 | *.vspscc
85 | *.vssscc
86 | .builds
87 | *.pidb
88 | *.svclog
89 | *.scc
90 |
91 | # Chutzpah Test files
92 | _Chutzpah*
93 |
94 | # Visual C++ cache files
95 | ipch/
96 | *.aps
97 | *.ncb
98 | *.opendb
99 | *.opensdf
100 | *.sdf
101 | *.cachefile
102 | *.VC.db
103 | *.VC.VC.opendb
104 |
105 | # Visual Studio profiler
106 | *.psess
107 | *.vsp
108 | *.vspx
109 | *.sap
110 |
111 | # Visual Studio Trace Files
112 | *.e2e
113 |
114 | # TFS 2012 Local Workspace
115 | $tf/
116 |
117 | # Guidance Automation Toolkit
118 | *.gpState
119 |
120 | # ReSharper is a .NET coding add-in
121 | _ReSharper*/
122 | *.[Rr]e[Ss]harper
123 | *.DotSettings.user
124 |
125 | # JustCode is a .NET coding add-in
126 | .JustCode
127 |
128 | # TeamCity is a build add-in
129 | _TeamCity*
130 |
131 | # DotCover is a Code Coverage Tool
132 | *.dotCover
133 |
134 | # AxoCover is a Code Coverage Tool
135 | .axoCover/*
136 | !.axoCover/settings.json
137 |
138 | # Visual Studio code coverage results
139 | *.coverage
140 | *.coveragexml
141 |
142 | # NCrunch
143 | _NCrunch_*
144 | .*crunch*.local.xml
145 | nCrunchTemp_*
146 |
147 | # MightyMoose
148 | *.mm.*
149 | AutoTest.Net/
150 |
151 | # Web workbench (sass)
152 | .sass-cache/
153 |
154 | # Installshield output folder
155 | [Ee]xpress/
156 |
157 | # DocProject is a documentation generator add-in
158 | DocProject/buildhelp/
159 | DocProject/Help/*.HxT
160 | DocProject/Help/*.HxC
161 | DocProject/Help/*.hhc
162 | DocProject/Help/*.hhk
163 | DocProject/Help/*.hhp
164 | DocProject/Help/Html2
165 | DocProject/Help/html
166 |
167 | # Click-Once directory
168 | publish/
169 |
170 | # Publish Web Output
171 | *.[Pp]ublish.xml
172 | *.azurePubxml
173 | # Note: Comment the next line if you want to checkin your web deploy settings,
174 | # but database connection strings (with potential passwords) will be unencrypted
175 | *.pubxml
176 | *.publishproj
177 |
178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
179 | # checkin your Azure Web App publish settings, but sensitive information contained
180 | # in these scripts will be unencrypted
181 | PublishScripts/
182 |
183 | # NuGet Packages
184 | *.nupkg
185 | # The packages folder can be ignored because of Package Restore
186 | **/[Pp]ackages/*
187 | # except build/, which is used as an MSBuild target.
188 | !**/[Pp]ackages/build/
189 | # Uncomment if necessary however generally it will be regenerated when needed
190 | #!**/[Pp]ackages/repositories.config
191 | # NuGet v3's project.json files produces more ignorable files
192 | *.nuget.props
193 | *.nuget.targets
194 |
195 | # Microsoft Azure Build Output
196 | csx/
197 | *.build.csdef
198 |
199 | # Microsoft Azure Emulator
200 | ecf/
201 | rcf/
202 |
203 | # Windows Store app package directories and files
204 | AppPackages/
205 | BundleArtifacts/
206 | Package.StoreAssociation.xml
207 | _pkginfo.txt
208 | *.appx
209 |
210 | # Visual Studio cache files
211 | # files ending in .cache can be ignored
212 | *.[Cc]ache
213 | # but keep track of directories ending in .cache
214 | !?*.[Cc]ache/
215 |
216 | # Others
217 | ClientBin/
218 | ~$*
219 | *~
220 | *.dbmdl
221 | *.dbproj.schemaview
222 | *.jfm
223 | *.pfx
224 | *.publishsettings
225 | orleans.codegen.cs
226 |
227 | # Including strong name files can present a security risk
228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
229 | #*.snk
230 |
231 | # Since there are multiple workflows, uncomment next line to ignore bower_components
232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
233 | #bower_components/
234 |
235 | # RIA/Silverlight projects
236 | Generated_Code/
237 |
238 | # Backup & report files from converting an old project file
239 | # to a newer Visual Studio version. Backup files are not needed,
240 | # because we have git ;-)
241 | _UpgradeReport_Files/
242 | Backup*/
243 | UpgradeLog*.XML
244 | UpgradeLog*.htm
245 | ServiceFabricBackup/
246 | *.rptproj.bak
247 |
248 | # SQL Server files
249 | *.mdf
250 | *.ldf
251 | *.ndf
252 |
253 | # Business Intelligence projects
254 | *.rdl.data
255 | *.bim.layout
256 | *.bim_*.settings
257 | *.rptproj.rsuser
258 | *- Backup*.rdl
259 |
260 | # Microsoft Fakes
261 | FakesAssemblies/
262 |
263 | # GhostDoc plugin setting file
264 | *.GhostDoc.xml
265 |
266 | # Node.js Tools for Visual Studio
267 | .ntvs_analysis.dat
268 | node_modules/
269 |
270 | # Visual Studio 6 build log
271 | *.plg
272 |
273 | # Visual Studio 6 workspace options file
274 | *.opt
275 |
276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
277 | *.vbw
278 |
279 | # Visual Studio LightSwitch build output
280 | **/*.HTMLClient/GeneratedArtifacts
281 | **/*.DesktopClient/GeneratedArtifacts
282 | **/*.DesktopClient/ModelManifest.xml
283 | **/*.Server/GeneratedArtifacts
284 | **/*.Server/ModelManifest.xml
285 | _Pvt_Extensions
286 |
287 | # Paket dependency manager
288 | .paket/paket.exe
289 | paket-files/
290 |
291 | # FAKE - F# Make
292 | .fake/
293 |
294 | # JetBrains Rider
295 | .idea/
296 | *.sln.iml
297 |
298 | # CodeRush personal settings
299 | .cr/personal
300 |
301 | # Python Tools for Visual Studio (PTVS)
302 | __pycache__/
303 | *.pyc
304 |
305 | # Cake - Uncomment if you are using it
306 | # tools/**
307 | # !tools/packages.config
308 |
309 | # Tabs Studio
310 | *.tss
311 |
312 | # Telerik's JustMock configuration file
313 | *.jmconfig
314 |
315 | # BizTalk build output
316 | *.btp.cs
317 | *.btm.cs
318 | *.odx.cs
319 | *.xsd.cs
320 |
321 | # OpenCover UI analysis results
322 | OpenCover/
323 |
324 | # Azure Stream Analytics local run output
325 | ASALocalRun/
326 |
327 | # MSBuild Binary and Structured Log
328 | *.binlog
329 |
330 | # NVidia Nsight GPU debugger configuration file
331 | *.nvuser
332 |
333 | # MFractors (Xamarin productivity tool) working folder
334 | .mfractor/
335 |
336 | # Local History for Visual Studio
337 | .localhistory/
338 |
339 | # BeatPulse healthcheck temp database
340 | healthchecksdb
341 | MyScheduledTasks/Properties/launchSettings.json
342 |
343 | MyScheduledTasks/.editorconfig
344 | /MyScheduledTasks/BuildInfo.cs
345 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Models/ScheduledTask.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace MyScheduledTasks.Models;
4 |
5 | public partial class ScheduledTask : ObservableObject
6 | {
7 | #region Build a ScheduledTask object
8 | ///
9 | /// Creates a ScheduledTask object
10 | ///
11 | /// TaskScheduler task
12 | /// Task from list of tasks to check
13 | /// ScheduledTask object
14 | public static ScheduledTask BuildScheduledTask(Task? task, MyTasks? myTask)
15 | {
16 | if (task != null)
17 | {
18 | // Get folder name
19 | int pos = task.Path.LastIndexOf('\\');
20 | string folder = pos == 0 ? "\\" : task.Path[..pos];
21 |
22 | ScheduledTask scheduledTask = new()
23 | {
24 | AllowDemandStart = task.Definition.Settings.AllowDemandStart,
25 | Enabled = task.Definition.Settings.Enabled,
26 | IsChecked = myTask?.Alert == true,
27 | LastRunRaw = task.LastRunTime,
28 | NextRunRaw = task.NextRunTime,
29 | Priority = task.Definition.Settings.Priority.ToString(),
30 | StartASAP = task.Definition.Settings.StartWhenAvailable,
31 | StartOnAC = task.Definition.Settings.DisallowStartIfOnBatteries,
32 | TaskAccount = task.Definition.Principal.Account,
33 | TaskActions = task.Definition.Actions.ToString(),
34 | TaskAuthor = task.Definition.RegistrationInfo.Author,
35 | TaskCreatedRaw = task.Definition.RegistrationInfo.Date,
36 | TaskDescription = task.Definition.RegistrationInfo.Description,
37 | TaskFolder = folder,
38 | TaskMissedRuns = task.NumberOfMissedRuns,
39 | TaskName = task.Name,
40 | TaskNote = myTask != null ? myTask.TaskNote : string.Empty,
41 | TaskPath = task.Path,
42 | TaskResult = (uint?)task.LastTaskResult,
43 | TaskRunLevel = (int)task.Definition.Principal.RunLevel,
44 | TaskRunLoggedOn = task.Definition.Settings.RunOnlyIfLoggedOn,
45 | TaskStatus = task.State.ToString(),
46 | TaskTriggers = task.Definition.Triggers.ToString(),
47 | TimeLimit = task.Definition.Settings.ExecutionTimeLimit.ToString(),
48 | WakeToRun = task.Definition.Settings.WakeToRun,
49 | };
50 |
51 | if (task.Definition.Triggers.Count > 1)
52 | {
53 | IEnumerable triggers = task.Definition.Triggers.Select(t => t.ToString(CultureInfo.InvariantCulture));
54 | scheduledTask.TaskTriggers = string.Join(Environment.NewLine, triggers).TrimEnd(Environment.NewLine.ToCharArray());
55 | }
56 | if (task.Definition.Actions.Count > 1)
57 | {
58 | IEnumerable actions = task.Definition.Actions.Select(a => a.ToString(CultureInfo.InvariantCulture));
59 | scheduledTask.TaskActions = string.Join(Environment.NewLine, actions).TrimEnd(Environment.NewLine.ToCharArray());
60 | }
61 |
62 | return scheduledTask;
63 | }
64 | return null!;
65 | }
66 | #endregion Build a ScheduledTask object
67 |
68 | #region Observable collection
69 | public static ObservableCollection TaskList { get; set; } = [];
70 | #endregion Observable collection
71 |
72 | #region Properties
73 | [ObservableProperty]
74 | private bool _allowDemandStart;
75 |
76 | [ObservableProperty]
77 | private bool _enabled;
78 |
79 | [ObservableProperty]
80 | private DateTime? _lastRunRaw;
81 |
82 | [ObservableProperty]
83 | private DateTime? _nextRunRaw;
84 |
85 | [ObservableProperty]
86 | private string? _priority;
87 |
88 | [ObservableProperty]
89 | private bool _startASAP;
90 |
91 | [ObservableProperty]
92 | private bool _startOnAC;
93 |
94 | [ObservableProperty]
95 | private string? _taskAccount;
96 |
97 | [ObservableProperty]
98 | private string? _taskActions;
99 |
100 | [ObservableProperty]
101 | private string? _taskAuthor;
102 |
103 | [ObservableProperty]
104 | private DateTime? _taskCreatedRaw;
105 |
106 | [ObservableProperty]
107 | private string? _taskDescription;
108 |
109 | [ObservableProperty]
110 | private string? _taskFolder;
111 |
112 | [ObservableProperty]
113 | private string? _timeLimit;
114 |
115 | [ObservableProperty]
116 | private int _taskMissedRuns;
117 |
118 | [ObservableProperty]
119 | private string? _taskName;
120 |
121 | [ObservableProperty]
122 | private string? _taskPath;
123 |
124 | [ObservableProperty]
125 | private uint? _taskResult;
126 |
127 | [ObservableProperty]
128 | private int _taskRunLevel;
129 |
130 | [ObservableProperty]
131 | private bool _taskRunLoggedOn;
132 |
133 | [ObservableProperty]
134 | private string? _taskStatus;
135 |
136 | [ObservableProperty]
137 | private string? _taskTriggers;
138 |
139 | [ObservableProperty]
140 | private bool _wakeToRun;
141 |
142 | public bool HighestPrivileges => TaskRunLevel != 0;
143 |
144 | public DateTime? LastRun => LastRunRaw == null || LastRunRaw == DateTime.MinValue || LastRunRaw == new DateTime(1999, 11, 30, 0, 0, 0, DateTimeKind.Local) ? null : LastRunRaw;
145 |
146 | public DateTime? NextRun => NextRunRaw == DateTime.MinValue ? null : NextRunRaw;
147 |
148 | public DateTime? TaskCreated => TaskCreatedRaw == DateTime.MinValue ? null : TaskCreatedRaw;
149 |
150 | public string TaskResultHex => TaskResult == null ? string.Empty : $"0x{TaskResult:X8}";
151 |
152 | public string TaskResultShort
153 | {
154 | get
155 | {
156 | return TaskResult switch
157 | {
158 | null => GetStringResource("TaskResult_Null"), //Null
159 | 0 => GetStringResource("TaskResult_OK"), //The operation completed successfully
160 | 0x41300 => GetStringResource("TaskResult_ReadyToRun"), //Task is ready to run at its next scheduled time
161 | 0x41301 => GetStringResource("TaskResult_Running"), //The task is currently running
162 | 0x41302 => GetStringResource("TaskResult_Disabled"), //The task has been disabled
163 | 0x41303 => GetStringResource("TaskResult_NotYetRun"), //The task has not yet run
164 | 0x41306 => GetStringResource("TaskResult_Terminated"), //The last run of the task was terminated by the user
165 | 0x80070002 => GetStringResource("TaskResult_FileNotFound"), //File not found
166 | _ => GetStringResource("TaskResult_NonZero"), //Other non-zero
167 | };
168 | }
169 | }
170 |
171 | private string? _taskNote;
172 | public string TaskNote
173 | {
174 | get => _taskNote!;
175 | set
176 | {
177 | _taskNote = value;
178 | TaskHelpers.TaskNoteChanged();
179 | }
180 | }
181 |
182 | private bool _isChecked;
183 | public bool IsChecked
184 | {
185 | get => _isChecked;
186 | set
187 | {
188 | if (value != _isChecked)
189 | {
190 | _isChecked = value;
191 | TaskHelpers.TaskAlertChanged();
192 | }
193 | }
194 | }
195 | #endregion Properties
196 | }
197 |
--------------------------------------------------------------------------------
/MyScheduledTasks/Styles/DataGridStyles.xaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
20 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
41 |
42 |
43 |
44 |
57 |
58 |
59 |
60 |
66 |
67 |
68 |
69 |
110 |
111 |
112 |
113 |
138 |
139 |
140 |
141 |
146 |
147 |
148 |
149 |
163 |
164 |
--------------------------------------------------------------------------------
/MyScheduledTasks/App.xaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Tim Kennedy. All Rights Reserved. Licensed under the MIT License.
2 |
3 | namespace MyScheduledTasks;
4 |
5 | public partial class App : Application
6 | {
7 | #region Properties
8 | ///
9 | /// Number of language strings in the test resource dictionary
10 | ///
11 | private static int TestLanguageStrings { get; set; }
12 |
13 | ///
14 | /// Uri of the test resource dictionary
15 | ///
16 | private static string? TestLanguageFile { get; set; }
17 |
18 | ///
19 | /// Number of language strings in the default resource dictionary
20 | ///
21 | public static int DefaultLanguageStrings { get; private set; }
22 |
23 | ///
24 | /// Command line arguments
25 | ///
26 | internal static string[] Args { get; private set; } = [];
27 | #endregion Properties
28 |
29 | #region On Startup event
30 | ///
31 | /// Override the Startup Event.
32 | ///
33 | protected override void OnStartup(StartupEventArgs e)
34 | {
35 | base.OnStartup(e);
36 |
37 | // Unhandled exception handler
38 | AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
39 |
40 | // Command line arguments
41 | Args = e.Args;
42 |
43 | // Initialize settings here so that saved language can be accessed below.
44 | ConfigHelpers.InitializeSettings();
45 |
46 | // Configure and start NLog
47 | NLogConfig(false);
48 |
49 | // Log startup messages
50 | MainWindowHelpers.LogStartup();
51 |
52 | // Set the UI language
53 | SetLanguage();
54 |
55 | // Enable language testing if requested.
56 | CheckLanguageTesting();
57 | }
58 | #endregion On Startup event
59 |
60 | #region Set the UI language
61 | ///
62 | /// Set the UI language.
63 | ///
64 | ///
65 | /// Strings.en-US.xaml is loaded in App.xaml as the fallback language.
66 | /// Consequently there is no need to explicitly load it in case of an error.
67 | ///
68 | private void SetLanguage()
69 | {
70 | // Get the number of strings in the default language file
71 | DefaultLanguageStrings = GetTotalDefaultLanguageCount();
72 |
73 | // Resource dictionary for language
74 | ResourceDictionary LanguageDictionary = [];
75 |
76 | // Log culture info at startup
77 | _log.Debug($"Startup culture: {LocalizationHelpers.GetCurrentCulture()} UI: {LocalizationHelpers.GetCurrentUICulture()}");
78 |
79 | // Get the current UI language
80 | string currentLanguage = Thread.CurrentThread.CurrentUICulture.Name;
81 |
82 | // Check the UseOSLanguage setting. If true try to use the language. Do not change current culture.
83 | if (LocalizationHelpers.CheckUseOsLanguage(currentLanguage))
84 | {
85 | if (currentLanguage == "en-US")
86 | {
87 | LocalizationHelpers.LanguageStrings = DefaultLanguageStrings;
88 | _log.Debug("Use OS Language option is true. Language is en-US. No need to load language file.");
89 | return;
90 | }
91 | try
92 | {
93 | LanguageDictionary.Source = new Uri($"Languages/Strings.{currentLanguage}.xaml", UriKind.RelativeOrAbsolute);
94 | Resources.MergedDictionaries.Add(LanguageDictionary);
95 | _log.Debug($"Use OS Language option is true. Language {currentLanguage} loaded.");
96 | }
97 | catch (Exception ex)
98 | {
99 | LanguageDictionary.Source = new Uri("Languages/Strings.en-US.xaml", UriKind.RelativeOrAbsolute);
100 | _log.Warn(ex, $"Language {currentLanguage} could not be located. Defaulting to en-US");
101 | }
102 | LocalizationHelpers.ApplyLanguageSettings(LanguageDictionary);
103 | return;
104 | }
105 |
106 | // If a language is defined in settings, and it exists in the list of defined languages, set the current culture and language to it.
107 | if (!string.IsNullOrEmpty(UserSettings.Setting!.UILanguage) &&
108 | UILanguage.DefinedLanguages.Exists(x => x.LanguageCode == UserSettings.Setting.UILanguage))
109 | {
110 | try
111 | {
112 | LanguageDictionary.Source = new Uri($"Languages/Strings.{UserSettings.Setting.UILanguage}.xaml", UriKind.RelativeOrAbsolute);
113 | Thread.CurrentThread.CurrentCulture = new CultureInfo(UserSettings.Setting.UILanguage);
114 | Thread.CurrentThread.CurrentUICulture = new CultureInfo(UserSettings.Setting.UILanguage);
115 | Resources.MergedDictionaries.Add(LanguageDictionary);
116 | }
117 | catch (Exception ex)
118 | {
119 | LanguageDictionary.Source = new Uri("Languages/Strings.en-US.xaml", UriKind.RelativeOrAbsolute);
120 | _log.Warn(ex, $"Error using language \"{UserSettings.Setting.UILanguage}\". Defaulting to en-US");
121 | }
122 | LocalizationHelpers.ApplyLanguageSettings(LanguageDictionary);
123 | return;
124 | }
125 |
126 | // If language is not found in settings, or the language is not defined in UILanguage.DefinedLanguages, use en-US.
127 | // Strings.en-US.xaml is loaded in App.xaml therefore there is no need to explicitly load it here.
128 | LanguageDictionary.Source = new Uri("Languages/Strings.en-US.xaml", UriKind.RelativeOrAbsolute);
129 | UserSettings.Setting.UILanguage = "en-US";
130 | ConfigHelpers.SaveSettings();
131 | _log.Warn("Language defaulting to en-US");
132 | LocalizationHelpers.ApplyLanguageSettings(LanguageDictionary);
133 | }
134 | #endregion Set the UI language
135 |
136 | #region Language testing
137 | private void CheckLanguageTesting()
138 | {
139 | if (UserSettings.Setting!.LanguageTesting)
140 | {
141 | _log.Info("Language testing enabled");
142 | ResourceDictionary testDict = [];
143 | string testLanguageFile = Path.Combine(AppInfo.AppDirectory, "Strings.test.xaml");
144 | if (File.Exists(testLanguageFile))
145 | {
146 | try
147 | {
148 | testDict.Source = new Uri(testLanguageFile, UriKind.RelativeOrAbsolute);
149 | if (testDict.Source != null)
150 | {
151 | Resources.MergedDictionaries.Add(testDict);
152 | TestLanguageStrings = testDict.Count;
153 | TestLanguageFile = testDict.Source.OriginalString;
154 | _log.Debug($"{TestLanguageStrings} strings loaded from {TestLanguageFile}");
155 | }
156 | }
157 | catch (Exception ex)
158 | {
159 | _log.Error(ex, $"Error loading test language file {TestLanguageFile}");
160 | string msg = string.Format(CultureInfo.CurrentCulture,
161 | $"{GetStringResource("MsgText_Error_TestLanguage")}\n\n{ex.Message}\n\n{ex.InnerException}");
162 | _ = MessageBox.Show(msg,
163 | GetStringResource("MsgText_ErrorCaption"),
164 | MessageBoxButton.OK,
165 | MessageBoxImage.Error);
166 | }
167 | }
168 | else
169 | {
170 | _log.Error($"Error loading test language file {TestLanguageFile}. File not found.");
171 | }
172 | }
173 | }
174 | #endregion Language testing
175 |
176 | #region Unhandled Exception Handler
177 | ///
178 | /// Handles any exceptions that weren't caught by a try-catch statement.
179 | ///
180 | ///
181 | /// This uses default message box.
182 | ///
183 | private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs args)
184 | {
185 | _log.Error("Unhandled Exception");
186 | Exception e = (Exception)args.ExceptionObject;
187 | _log.Error(e.Message);
188 | if (e.InnerException != null)
189 | {
190 | _log.Error(e.InnerException.ToString());
191 | }
192 | _log.Error(e.StackTrace);
193 |
194 | string msg = string.Format(CultureInfo.CurrentCulture,
195 | $"{GetStringResource("MsgText_ErrorGeneral")}\n{e.Message}\n{GetStringResource("MsgText_SeeLogFile")}");
196 | _ = MessageBox.Show(msg,
197 | GetStringResource("MsgText_ErrorCaption"),
198 | MessageBoxButton.OK,
199 | MessageBoxImage.Error);
200 | }
201 | #endregion Unhandled Exception Handler
202 | }
203 |
--------------------------------------------------------------------------------