├── .gitignore ├── Assets └── Images │ ├── Dark │ ├── Calculator-Dark.png │ ├── Date-Dark.png │ ├── Grid-Dark-Filled.png │ ├── Grid-Dark.png │ ├── List-Dark-Filled.png │ ├── List-Dark.png │ └── Weighting-Dark.png │ ├── Light │ ├── Calculator-Light.png │ ├── Date-Light.png │ ├── Grid-Light-Filled.png │ ├── Grid-Light.png │ ├── List-Light-Filled.png │ ├── List-Light.png │ └── Weighting-Light.png │ └── Logo.png ├── GradeManagement.csproj ├── GradeManagement.sln ├── NOTICE.md ├── README.md └── src ├── App.axaml ├── App.axaml.cs ├── Converters ├── FloatStringConverter.cs ├── IntStringConverter.cs └── MonthConverter.cs ├── CustomControls ├── CustomDragButton.cs └── PanelTemplateSelector.cs ├── Enums ├── AddPageAction.cs ├── DateType.cs ├── ElementType.cs ├── SelectedButtonStyle.cs └── ThemeMode.cs ├── ExtensionCollection ├── CollectionExtensions.cs ├── ColorExtensions.cs ├── EnumExtensions.cs ├── FloatExtensions.cs └── StringExtensions.cs ├── Interfaces ├── IAddViewModel.cs ├── IElement.cs ├── IGradable.cs ├── IGradesContainer.cs ├── IListViewModel.cs └── ITargetGrade.cs ├── Models ├── ColorRepresentation.cs ├── DataManager.cs ├── DragControl.cs ├── Elements │ ├── BaseClasses │ │ └── ColorableElement.cs │ ├── Grade.cs │ ├── GradeGroup.cs │ ├── SchoolYear.cs │ └── Subject.cs ├── MonthRepresentation.cs └── Settings │ ├── Preferences.cs │ └── SettingsManager.cs ├── Program.cs ├── Resources ├── BaseResources.axaml ├── DarkTheme.axaml ├── DarkTheme.cs ├── LightTheme.axaml ├── LightTheme.cs ├── Resources.cs └── Theme.cs ├── Styles ├── AddPagesStyle.axaml ├── CalendarStyle.axaml ├── ContextMenuStyle.axaml ├── DialogStyle.axaml ├── ListViewStyle.axaml ├── MainStyle.axaml ├── MainWindowStyle.axaml └── TargetGradeStyle.axaml ├── UtilityCollection ├── ColorUtilities.cs ├── DateUtilities.cs ├── UniversalUtilities.cs └── WindowUtilities.cs ├── ViewLocator.cs ├── ViewModels ├── AddPages │ ├── AddGradeViewModel.cs │ ├── AddSubjectViewModel.cs │ └── AddYearViewModel.cs ├── BaseClasses │ ├── AddViewModelBase.cs │ ├── DialogBase.cs │ ├── ListViewModelBase.cs │ └── ViewModelBase.cs ├── Dialogs │ └── ConfirmationDialogViewModel.cs ├── Lists │ ├── GradeListViewModel.cs │ ├── SubjectListViewModel.cs │ └── YearListViewModel.cs ├── MainWindowViewModel.cs └── TargetGrade │ ├── AverageGradeViewModel.cs │ ├── TargetGradeViewModel.cs │ └── TargetGradeWindowModel.cs └── Views ├── AddPages ├── AddGradeWindow.axaml ├── AddGradeWindow.axaml.cs ├── AddSubjectWindow.axaml ├── AddSubjectWindow.axaml.cs ├── AddYearWindow.axaml └── AddYearWindow.axaml.cs ├── Dialogs ├── ConfirmationDialogView.axaml └── ConfirmationDialogView.axaml.cs ├── Lists ├── ElementButtonControls │ ├── ButtonStyleBase.cs │ ├── GridButton.cs │ └── ListButton.cs ├── GradeListView.axaml ├── GradeListView.axaml.cs ├── SubjectListView.axaml ├── SubjectListView.axaml.cs ├── YearListView.axaml └── YearListView.axaml.cs ├── MainWindow.axaml ├── MainWindow.axaml.cs └── TargetGrade ├── AverageGradeView.axaml ├── AverageGradeView.axaml.cs ├── TargetGradeView.axaml ├── TargetGradeView.axaml.cs ├── TargetGradeWindow.axaml └── TargetGradeWindow.axaml.cs /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | .vs/ 4 | 5 | bin/ 6 | obj/ 7 | 8 | *.user -------------------------------------------------------------------------------- /Assets/Images/Dark/Calculator-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Dark/Calculator-Dark.png -------------------------------------------------------------------------------- /Assets/Images/Dark/Date-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Dark/Date-Dark.png -------------------------------------------------------------------------------- /Assets/Images/Dark/Grid-Dark-Filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Dark/Grid-Dark-Filled.png -------------------------------------------------------------------------------- /Assets/Images/Dark/Grid-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Dark/Grid-Dark.png -------------------------------------------------------------------------------- /Assets/Images/Dark/List-Dark-Filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Dark/List-Dark-Filled.png -------------------------------------------------------------------------------- /Assets/Images/Dark/List-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Dark/List-Dark.png -------------------------------------------------------------------------------- /Assets/Images/Dark/Weighting-Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Dark/Weighting-Dark.png -------------------------------------------------------------------------------- /Assets/Images/Light/Calculator-Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Light/Calculator-Light.png -------------------------------------------------------------------------------- /Assets/Images/Light/Date-Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Light/Date-Light.png -------------------------------------------------------------------------------- /Assets/Images/Light/Grid-Light-Filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Light/Grid-Light-Filled.png -------------------------------------------------------------------------------- /Assets/Images/Light/Grid-Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Light/Grid-Light.png -------------------------------------------------------------------------------- /Assets/Images/Light/List-Light-Filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Light/List-Light-Filled.png -------------------------------------------------------------------------------- /Assets/Images/Light/List-Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Light/List-Light.png -------------------------------------------------------------------------------- /Assets/Images/Light/Weighting-Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Light/Weighting-Light.png -------------------------------------------------------------------------------- /Assets/Images/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duck-dev/Grade-Management/24e6ceaedfa9b63c0a5544d716556277d22e774a/Assets/Images/Logo.png -------------------------------------------------------------------------------- /GradeManagement.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | net7.0 5 | enable 6 | 11 7 | true 8 | osx-x64;win-x64;win-x86;linux-x64 9 | 10 | 11 | true 12 | 13 | 14 | GradeManagement 15 | GradeManagement 16 | com.duck-dev.grade-management 17 | 1.0.1 18 | 1.0.1 19 | APPL 20 | ???? 21 | GradeManagement 22 | GradeManagementIcon.icns 23 | NSApplication 24 | true 25 | 26 | 27 | none 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | App.axaml 42 | Code 43 | 44 | 45 | GradeListView.axaml 46 | Code 47 | 48 | 49 | SubjectListView.axaml 50 | Code 51 | 52 | 53 | YearListView.axaml 54 | Code 55 | 56 | 57 | AddSubjectWindow.axaml 58 | Code 59 | 60 | 61 | AddGradeWindow.axaml 62 | Code 63 | 64 | 65 | WishGradeWindow.axaml 66 | Code 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /GradeManagement.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GradeManagement", "GradeManagement.csproj", "{24F8B115-AEED-452C-BE37-87FD8B1ABD59}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {24F8B115-AEED-452C-BE37-87FD8B1ABD59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {24F8B115-AEED-452C-BE37-87FD8B1ABD59}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {24F8B115-AEED-452C-BE37-87FD8B1ABD59}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {24F8B115-AEED-452C-BE37-87FD8B1ABD59}.Release|Any CPU.Build.0 = Release|Any CPU 15 | {24F8B115-AEED-452C-BE37-87FD8B1ABD59}.Release|Any CPU.Deploy.0 = Release|Any CPU 16 | EndGlobalSection 17 | EndGlobal 18 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | # Avalonia 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) .NET Foundation and Contributors All Rights Reserved 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grade Management 2 | 3 | 4 | A modern, highly customizable and extraordinarily feature-rich solution for storing, organizing and managing your school grades. 5 | This distinguishes it enormously from equivalent products. And... it's 100% **free** and **open-source**! 6 | 7 | Keep track of all your grades, divided into different semesters and subjects, as well as averages and more information about your grades. 8 | Organize your grades however you prefer it and let them get displayed according to your preferences. 9 | Play around with different views, colors and plenty of settings. 10 | 11 | ## A non-exhaustive list of the most important features 12 | 13 | Despite a very high freedom in terms of customization and functions, simplicity hasn't been omitted in the development of **Grade Management**. 14 | If you like to keep it simple, this tool offers you everything you need. 15 | If you need more than that, make use of the freedoms **Grade Management** offers you... 16 | 17 | ### Functions 18 | - **Organize and split up:** Divide your grades into subjects and these in turn into semesters. 19 | - **Much more than just a grade-value:** If only the grade and the name of a grade aren't enough for you, 20 | how about specifying the **Date** of an exam as well as the **Weighting** (for grades and subjects) 21 | and actually considering it in the average calculation? 22 | - **Calculate Target-Grade:** 23 | You want to know how much you're supposed to learn for the next exam in order to reach a certain average? 24 | Sure, let **Grade Management** calculate it for you. 25 | - **Calculate Average:** 26 | The same goes for calculating the hypothetical average with your next assumed grade. 27 | - **Many convenient features:** Have you made yourself comfortable? 28 | Duplicate, copy, paste, create and delete semesters, subjects or grades with a few mouse clicks within a fraction of a second. 29 | - **Import/Export Data:** Are you afraid of losing your data? 30 | Don't fear anymore, export your semesters or let them be automatically exported at a certain specified time. (Coming soon) 31 | - **Many settings:** Customize your **Grade Management** with a wide range of settings. (Coming soon) 32 | - **Order elements:** Order the elements according to many different parameters 33 | or just drag your own preferred order whenever you feel like it. (Coming soon) 34 | - **Multi-Lingual:** Do you speak English... oder Deutsch... ou Français? 35 | We offer you freedom in the choice of your favored language. (Coming soon) 36 | 37 | ### Look and Design 38 | - **Sooo 39 | colorful:** 40 | Assign a **color** to each semester and subject to recognize them at first glance. 41 | - **Intuitive and simple, yet so powerful:** Incredibly **intuitive** layout and navigation. 42 | - **Different views:** 43 | Do you prefer to view your semesters, subjects or grades in a **grid**- or **list**-pattern? Whatever, you can decide for yourself! 44 | - **Themes:** Light Theme or Dark Theme? We say **both** and even **more**. (Coming soon) 45 | 46 | ### Privacy 47 | Grade Management collects no personal data and requires no internet connection for any of the features. It is a 100% "local" 48 | application without any hidden or visible data transfers. Solely your semesters/subjects/grades and settings are being saved 49 | **locally**, but are not visible to anyone except the user of the local device. Your grades are incognito, hooray! 50 | 51 | ## Development 52 | This tool is being developed with the open-source .NET XAML UI-framework [Avalonia](https://avaloniaui.net/) and C#. 53 | 54 | ## Gallery 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/App.axaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Avalonia; 5 | using Avalonia.Controls.ApplicationLifetimes; 6 | using Avalonia.Markup.Xaml; 7 | using Avalonia.Markup.Xaml.Styling; 8 | using GradeManagement.Enums; 9 | using GradeManagement.ViewModels; 10 | using GradeManagement.Views; 11 | 12 | namespace GradeManagement 13 | { 14 | public class App : Application 15 | { 16 | private const string ResourcesPath = "avares://GradeManagement/src/Resources/"; 17 | 18 | private ThemeMode _theme = ThemeMode.None; 19 | private StyleInclude? _currentThemeStyle; 20 | private readonly Dictionary _themeSourcesCollection = new() 21 | { 22 | { ThemeMode.Light, new Uri(Path.Combine(ResourcesPath, "LightTheme.axaml")) }, 23 | { ThemeMode.Dark, new Uri(Path.Combine(ResourcesPath, "DarkTheme.axaml")) } 24 | }; 25 | 26 | public App() => SetTheme(ThemeMode.Light); // TODO: Get from saved settings 27 | 28 | public override void Initialize() 29 | { 30 | AvaloniaXamlLoader.Load(this); 31 | } 32 | 33 | public override void OnFrameworkInitializationCompleted() 34 | { 35 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 36 | { 37 | desktop.MainWindow = new MainWindow 38 | { 39 | DataContext = new MainWindowViewModel { AppInstance = this } 40 | }; 41 | } 42 | 43 | base.OnFrameworkInitializationCompleted(); 44 | } 45 | 46 | internal void SetTheme(ThemeMode theme) 47 | { 48 | if (theme == _theme) 49 | return; 50 | _theme = theme; 51 | 52 | if (_currentThemeStyle is not null) 53 | this.Styles.Remove(_currentThemeStyle); 54 | _currentThemeStyle = new StyleInclude(_themeSourcesCollection[theme]) 55 | { 56 | Source = _themeSourcesCollection[theme] 57 | }; 58 | this.Styles.Add(_currentThemeStyle); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/Converters/FloatStringConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Globalization; 4 | using Avalonia.Data.Converters; 5 | 6 | namespace GradeManagement.Converters 7 | { 8 | public class FloatStringConverter : IValueConverter 9 | { 10 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 11 | => value?.ToString(); 12 | 13 | [SuppressMessage("ReSharper", "HeapView.BoxingAllocation")] // Unfortunately, I can't change it 14 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 15 | { 16 | if (value is string stringValue && float.TryParse(stringValue, out float floatValue)) 17 | return floatValue; 18 | 19 | return 0; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Converters/IntStringConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Globalization; 4 | using Avalonia.Data.Converters; 5 | 6 | namespace GradeManagement.Converters 7 | { 8 | public class IntStringConverter : IValueConverter 9 | { 10 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 11 | => value?.ToString(); 12 | 13 | [SuppressMessage("ReSharper", "HeapView.BoxingAllocation")] // Unfortunately, I can't change it 14 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 15 | { 16 | if (value is string stringValue && int.TryParse(stringValue, out int intValue)) 17 | return intValue; 18 | 19 | return -1; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Converters/MonthConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Avalonia.Data.Converters; 4 | using GradeManagement.Enums; 5 | using GradeManagement.ExtensionCollection; 6 | using GradeManagement.Models; 7 | using GradeManagement.UtilityCollection; 8 | using GradeManagement.ViewModels.AddPages; 9 | 10 | namespace GradeManagement.Converters 11 | { 12 | public class MonthConverter : IValueConverter 13 | { 14 | public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 15 | { 16 | var instance = AddGradeViewModel.Instance; 17 | return instance is null ? string.Empty : instance.SelectedMonth.MonthName; 18 | } 19 | 20 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 21 | { 22 | if (value is not string monthName) 23 | return ConversionFailed(); 24 | 25 | // ReSharper disable once InlineOutVariableDeclaration 26 | int monthNumber; 27 | if (int.TryParse(monthName, out monthNumber)) 28 | { 29 | if (!Utilities.ValidateDate(1, monthNumber, Utilities.TodaysYear, out var protocol) // Day and year aren't important 30 | && protocol.CustomHasFlag(DateType.Month)) 31 | { 32 | return ConversionFailed(); 33 | } 34 | return new MonthRepresentation(monthNumber); 35 | } 36 | else if (TryParseMonth(monthName, out monthNumber) && 37 | Utilities.ValidateDate(1, monthNumber, Utilities.TodaysYear, out _)) // Day and year aren't important 38 | return new MonthRepresentation(monthName); 39 | 40 | return ConversionFailed(); 41 | } 42 | 43 | internal static string ConvertMonth(int month) 44 | { 45 | var date = new DateTime(Utilities.TodaysYear, month, 1); // Day and year aren't important 46 | // TODO: Use currently selected language as culture once it has been implemented 47 | return date.ToString("MMMM", CultureInfo.CurrentCulture); 48 | } 49 | 50 | internal static bool TryParseMonth(string month, out int monthNumber) 51 | { 52 | monthNumber = 0; 53 | // TODO: Use currently selected language as culture once it has been implemented 54 | if (!DateTime.TryParseExact(month, "MMMM", CultureInfo.CurrentCulture, 55 | DateTimeStyles.None, out var dateTime)) 56 | return false; 57 | 58 | monthNumber = dateTime.Month; 59 | return true; 60 | } 61 | 62 | private static MonthRepresentation ConversionFailed() => new(string.Empty); 63 | } 64 | } -------------------------------------------------------------------------------- /src/CustomControls/CustomDragButton.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia.Controls; 3 | using Avalonia.Input; 4 | using Avalonia.Styling; 5 | 6 | namespace GradeManagement.CustomControls 7 | { 8 | public sealed class CustomDragButton : Button, IStyleable 9 | { 10 | public event EventHandler PointerPressedPreview = delegate { }; 11 | 12 | Type IStyleable.StyleKey => typeof(Button); 13 | 14 | protected override void OnPointerPressed(PointerPressedEventArgs args) 15 | { 16 | PointerPressedPreview(this, args); 17 | base.OnPointerPressed(args); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/CustomControls/PanelTemplateSelector.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Templates; 4 | using Avalonia.Metadata; 5 | 6 | namespace GradeManagement.CustomControls 7 | { 8 | public class PanelTemplateSelector : IDataTemplate 9 | { 10 | [Content] 11 | public Dictionary Templates { get; } = new(); 12 | public IControl Build(object data) => Templates[(bool)data].Build(data); 13 | public bool Match(object data) => data is bool; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Enums/AddPageAction.cs: -------------------------------------------------------------------------------- 1 | namespace GradeManagement.Enums 2 | { 3 | public enum AddPageAction 4 | { 5 | Create, 6 | Edit 7 | } 8 | } -------------------------------------------------------------------------------- /src/Enums/DateType.cs: -------------------------------------------------------------------------------- 1 | namespace GradeManagement.Enums 2 | { 3 | [System.Flags] 4 | public enum DateType 5 | { 6 | None = 0, 7 | Day = 1, 8 | Month = 2, 9 | Year = 4 10 | } 11 | } -------------------------------------------------------------------------------- /src/Enums/ElementType.cs: -------------------------------------------------------------------------------- 1 | namespace GradeManagement.Enums 2 | { 3 | public enum ElementType 4 | { 5 | SchoolYear, 6 | Subject, 7 | Grade, 8 | } 9 | } -------------------------------------------------------------------------------- /src/Enums/SelectedButtonStyle.cs: -------------------------------------------------------------------------------- 1 | namespace GradeManagement.Enums 2 | { 3 | public enum SelectedButtonStyle 4 | { 5 | Grid = 0, 6 | List = 1 7 | } 8 | } -------------------------------------------------------------------------------- /src/Enums/ThemeMode.cs: -------------------------------------------------------------------------------- 1 | namespace GradeManagement.Enums 2 | { 3 | public enum ThemeMode 4 | { 5 | None, 6 | Light, 7 | Dark 8 | } 9 | } -------------------------------------------------------------------------------- /src/ExtensionCollection/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace GradeManagement.ExtensionCollection 6 | { 7 | public static partial class Extensions 8 | { 9 | /// 10 | /// Add the element to the collection only if it doesn't contain it already. 11 | /// 12 | /// The collection to which the element should be added. 13 | /// The element to be added. 14 | public static void SafeAdd(this ICollection list, T element) 15 | { 16 | if(!list.Contains(element)) 17 | list.Add(element); 18 | } 19 | 20 | /// 21 | /// Remove the element from the collection only if it already contains it. 22 | /// 23 | /// The collection, the element should be removed from. 24 | /// The element to be removed. 25 | public static void SafeRemove(this ICollection list, T element) 26 | { 27 | if (list.Contains(element)) 28 | list.Remove(element); 29 | } 30 | 31 | /// 32 | /// Create a deep copy of a collection. 33 | /// 34 | /// The list to be cloned. 35 | /// The type of the elements inside the collection. All of them ought to inherit 36 | /// 37 | /// The cloned collection. 38 | public static IEnumerable Clone(this IEnumerable list) where T : ICloneable 39 | => list.Select(x => (T)x.Clone()); 40 | } 41 | } -------------------------------------------------------------------------------- /src/ExtensionCollection/ColorExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Avalonia.Media; 4 | 5 | namespace GradeManagement.ExtensionCollection 6 | { 7 | [SuppressMessage("ReSharper", "InconsistentNaming")] 8 | public static partial class Extensions 9 | { 10 | /// 11 | /// Linear interpolation between two colors. 12 | /// 13 | /// Start value (a) 14 | /// End value (b) 15 | /// Amount (t) 16 | /// Linearly interpolated value. 17 | public static Color Lerp(this Color color, Color to, float amount) 18 | { 19 | byte a = (byte) ((float) color.A).Lerp(to.A, amount), 20 | r = (byte) ((float) color.R).Lerp(to.R, amount), 21 | g = (byte) ((float) color.G).Lerp(to.G, amount), 22 | b = (byte) ((float) color.B).Lerp(to.B, amount); 23 | return Color.FromArgb(a, r, g, b); 24 | } 25 | 26 | /// 27 | /// Chooses the specified dark or light tint, based on the background's brightness. 28 | /// Dark background => Light tint and vice-versa. 29 | /// 30 | /// The background color, whose brightness determines the foreground tint. 31 | /// The dark tint. 32 | /// The light tint. 33 | /// The threshold, at which the color becomes dark upwards and light downwards. 34 | /// Default value: 110 35 | /// The adjusted foreground color. 36 | public static Color AdjustForegroundBrightness(this Color backgroundColor, Color darkColor, Color lightColor, int threshold = 110) 37 | => ((PerceivedBrightness(backgroundColor) > threshold) ? darkColor : lightColor); 38 | 39 | /// 40 | /// Calculate the brightness of a color. 41 | /// 42 | /// The passed . 43 | /// The brightness represented as an integer. 44 | [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] 45 | public static int PerceivedBrightness(this Color color) 46 | { 47 | return (int)Math.Sqrt( 48 | color.R * color.R * .299 + 49 | color.G * color.G * .587 + 50 | color.B * color.B * .114); 51 | } 52 | 53 | /// 54 | /// Darkens the specified color by 55 | /// 56 | /// The color to be darkened. 57 | /// How much darker should the color become? 58 | /// The adjusted color. 59 | public static Color DarkenColor(this Color color, float amount) => AdjustTint(color, Colors.Black, amount); 60 | 61 | /// 62 | /// Brightens the specified color by 63 | /// 64 | /// The color to be brightened. 65 | /// How much brighter should the color become? 66 | /// The adjusted color. 67 | public static Color BrightenColor(this Color color, float amount) => AdjustTint(color, Colors.White, amount); 68 | 69 | /// 70 | /// Adjust the specified color by to be closer to the color. 71 | /// 72 | /// The color to be adjusted. 73 | /// The goal color, to which the tends to go. 74 | /// How close should the be to the color? 75 | /// The adjusted color. 76 | public static Color AdjustTint(this Color color, Color goal, float amount) => color.Lerp(goal, amount); 77 | 78 | /// 79 | /// Return the hexadecimal representation of a . 80 | /// 81 | /// The color to use for the hexadecimal representation. 82 | /// The hexadecimal representation of this 83 | public static string ToHexString(this Color color) => $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}"; 84 | 85 | public static Color WithR(this Color color, byte r) => 86 | new (color.A, r, color.G, color.B); 87 | 88 | public static Color WithRG(this Color color, byte r, byte g) 89 | => new(color.A, r, g, color.B); 90 | 91 | public static Color WithRB(this Color color, byte r, byte b) 92 | => new(color.A, r, color.G, b); 93 | 94 | public static Color WithRA(this Color color, byte r, byte a) 95 | => new(a, r, color.G, color.B); 96 | 97 | public static Color WithRGB(this Color color, byte r, byte g, byte b) 98 | => new(color.A, r, g, b); 99 | 100 | public static Color WithRGA(this Color color, byte r, byte g, byte a) 101 | => new(a, r, g, color.B); 102 | 103 | public static Color WithRBA(this Color color, byte r, byte b, byte a) 104 | => new(a, r, color.G, b); 105 | 106 | public static Color WithG(this Color color, byte g) 107 | => new(color.A, color.R, g, color.B); 108 | 109 | public static Color WithGB(this Color color, byte g, byte b) 110 | => new(color.A, color.R, g, b); 111 | 112 | public static Color WithGA(this Color color, byte g, byte a) 113 | => new(a, color.R, g, color.B); 114 | 115 | public static Color WithGBA(this Color color, byte g, byte b, byte a) 116 | => new(a, color.R, g, b); 117 | 118 | public static Color WithB(this Color color, byte b) 119 | => new(color.A, color.R, color.G, b); 120 | 121 | public static Color WithBA(this Color color, byte b, byte a) 122 | => new(a, color.R, color.G, b); 123 | 124 | public static Color WithA(this Color color, byte a) 125 | => new(a, color.R, color.G, color.B); 126 | 127 | public static Color With(this Color color, byte r, byte g, byte b, byte a) 128 | => new(a, r, g, b); 129 | } 130 | } -------------------------------------------------------------------------------- /src/ExtensionCollection/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | using GradeManagement.Enums; 2 | 3 | namespace GradeManagement.ExtensionCollection 4 | { 5 | public static partial class Extensions 6 | { 7 | /// 8 | /// Faster version of HasFlag 9 | /// 10 | /// The extended enum. 11 | /// The flag value. 12 | /// Has flag? 13 | public static bool CustomHasFlag(this DateType enumeration, DateType value) 14 | => (enumeration & value) == value; 15 | } 16 | } -------------------------------------------------------------------------------- /src/ExtensionCollection/FloatExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GradeManagement.ExtensionCollection 4 | { 5 | public static partial class Extensions 6 | { 7 | /// 8 | /// Linear interpolation between two float's. 9 | /// 10 | /// Start value (a) 11 | /// End value (b) 12 | /// Amount (t) 13 | /// Linearly interpolated value. 14 | public static float Lerp(this float start, float end, float amount) 15 | { 16 | amount = Math.Clamp(amount, 0, 1); 17 | return start + (end - start) * amount; 18 | } 19 | 20 | /// 21 | /// Test equality between two nullable floats. 22 | /// 23 | /// Nullable float A 24 | /// Nullable float B 25 | /// The tolerance for deviation. 26 | /// Returns `true` if the absolute difference is smaller than the tolerance, otherwise it returns `false`. 27 | public static bool AlmostEquals(this float? a, float? b, float tolerance) 28 | { 29 | // ReSharper disable once CompareOfFloatsByEqualityOperator 30 | if (a is not { } aFloat || b is not { } bFloat) 31 | return a == b; 32 | 33 | return AlmostEquals(aFloat, bFloat, tolerance); 34 | } 35 | 36 | /// 37 | /// Test equality between two floats. 38 | /// 39 | /// Float a 40 | /// Float b 41 | /// The tolerance for deviation. 42 | /// Returns `true` if the absolute difference is smaller than the tolerance, otherwise it returns `false`. 43 | public static bool AlmostEquals(this float a, float b, float tolerance) 44 | => Math.Abs(a - b) < tolerance; 45 | } 46 | } -------------------------------------------------------------------------------- /src/ExtensionCollection/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace GradeManagement.ExtensionCollection 4 | { 5 | public static partial class Extensions 6 | { 7 | /// 8 | /// Split a string with a camelCase (or PascalCase) pattern into each word and join them with a space together. 9 | /// 10 | /// The input string that's supposed to be split. 11 | /// The separator, which will be used to separate each word in the final string. 12 | /// Default: Single Space. 13 | /// An array of all separate words. 14 | public static string SplitCamelCase(this string input, string separator = " ") 15 | { 16 | var words = Regex.Split(input, "([A-Z][a-z]+)"); 17 | return string.Join(separator, words).Trim(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Interfaces/IAddViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace GradeManagement.Interfaces 2 | { 3 | public interface IAddViewModel where T : IElement 4 | { 5 | void EditElement(T element); 6 | } 7 | } -------------------------------------------------------------------------------- /src/Interfaces/IElement.cs: -------------------------------------------------------------------------------- 1 | using GradeManagement.Views.Lists.ElementButtonControls; 2 | 3 | namespace GradeManagement.Interfaces 4 | { 5 | public interface IElement : IGradable 6 | { 7 | string Name { get; } 8 | ButtonStyleBase? ButtonStyle { get; set; } 9 | 10 | T? Duplicate(bool save = true) where T : class, IElement; 11 | void Save(T? element = null) where T : class, IElement; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Interfaces/IGradable.cs: -------------------------------------------------------------------------------- 1 | namespace GradeManagement.Interfaces 2 | { 3 | public interface IGradable 4 | { 5 | float GradeValue { get; } 6 | float Weighting { get; } 7 | bool Counts { get; } 8 | int ElementCount { get; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Interfaces/IGradesContainer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using GradeManagement.Models.Elements; 3 | 4 | namespace GradeManagement.Interfaces; 5 | 6 | public interface IGradesContainer 7 | { 8 | IGradesContainer? ParentContainer { get; } 9 | List Grades { get; } 10 | } -------------------------------------------------------------------------------- /src/Interfaces/IListViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace GradeManagement.Interfaces 4 | { 5 | public interface IListViewModel where T : class, IElement 6 | { 7 | ObservableCollection? Items { get; set; } 8 | bool EmptyCollection { get; } 9 | 10 | void Duplicate(IElement element); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Interfaces/ITargetGrade.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using GradeManagement.Models.Elements; 3 | 4 | namespace GradeManagement.Interfaces 5 | { 6 | public interface ITargetGrade 7 | { 8 | IEnumerable? Grades { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Models/ColorRepresentation.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Media; 2 | using ReactiveUI; 3 | 4 | namespace GradeManagement.Models 5 | { 6 | public class ColorRepresentation : ReactiveObject 7 | { 8 | private bool _selected; 9 | 10 | public ColorRepresentation(Color color) 11 | { 12 | ElementColor = color; 13 | ElementColorBrush = new SolidColorBrush(color); 14 | } 15 | 16 | internal Color ElementColor { get; } 17 | internal SolidColorBrush ElementColorBrush { get; } 18 | internal bool Selected 19 | { 20 | get => _selected; 21 | set => this.RaiseAndSetIfChanged(ref _selected, value); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Models/DataManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text.Json; 4 | using GradeManagement.Models.Elements; 5 | using GradeManagement.UtilityCollection; 6 | 7 | namespace GradeManagement.Models 8 | { 9 | public static class DataManager 10 | { 11 | internal static List SchoolYears { get; private set; } = new(); 12 | 13 | private static string FilePath => Path.Combine(Utilities.FilesParentPath, "SchoolYears.json"); 14 | 15 | internal static void LoadData() 16 | { 17 | if (!File.Exists(FilePath)) 18 | return; 19 | 20 | string content = File.ReadAllText(FilePath); 21 | var deserializedList = JsonSerializer.Deserialize>(content); 22 | if(deserializedList is not null) 23 | SchoolYears = deserializedList; 24 | } 25 | 26 | internal static void SaveData() 27 | { 28 | var options = new JsonSerializerOptions { WriteIndented = true }; 29 | string jsonString = JsonSerializer.Serialize(SchoolYears, options); 30 | File.WriteAllText(FilePath, jsonString); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Models/DragControl.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Input; 3 | using Avalonia.Interactivity; 4 | 5 | namespace GradeManagement.Models 6 | { 7 | public abstract class DragControl : UserControl 8 | { 9 | protected DragControl() 10 | { 11 | AddHandler(DragDrop.DropEvent, Drop); 12 | AddHandler(DragDrop.DragEnterEvent, DragEnter); 13 | AddHandler(DragDrop.DragLeaveEvent, DragLeave); 14 | } 15 | 16 | internal async void BeginDrag(object? sender, PointerPressedEventArgs args) 17 | { 18 | if (sender is not Button button) 19 | return; 20 | 21 | DataObject data = new(); 22 | data.Set("Button", button); 23 | await DragDrop.DoDragDrop(args, data, DragDropEffects.Copy); 24 | } 25 | 26 | private void Drop(object? sender, DragEventArgs args) 27 | { 28 | if (sender is not Button button) 29 | return; 30 | } 31 | 32 | private void DragEnter(object? sender, DragEventArgs args) 33 | { 34 | if (sender is not Button button) 35 | return; 36 | } 37 | 38 | private void DragLeave(object? sender, RoutedEventArgs args) 39 | { 40 | if (sender is not Button button) 41 | return; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Models/Elements/BaseClasses/ColorableElement.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using Avalonia; 3 | using Avalonia.Media; 4 | using GradeManagement.Enums; 5 | using GradeManagement.ExtensionCollection; 6 | using GradeManagement.UtilityCollection; 7 | using ReactiveUI; 8 | 9 | // ReSharper disable once CheckNamespace 10 | namespace GradeManagement.Models.Elements 11 | { 12 | public class ColorableElement : ReactiveObject 13 | { 14 | private const float TitleDarkenFactor = 0.3f; 15 | private const float TitleBrightenFactor = 0.5f; 16 | private const float AdditionalInfoDarkenFactor = 0.25f; 17 | private const float AdditionalInfoBrightenFactor = 0.3f; 18 | 19 | private const int GridThresholdTitle = 110; 20 | private const int ListThresholdTitle = 135; 21 | 22 | private readonly Color _additionalInfoBaseColor = Color.Parse("#999999"); 23 | private readonly Color _lightBackground = Color.Parse("#c7cad1"); 24 | 25 | private SolidColorBrush? _additionalInfoGrid; 26 | private SolidColorBrush? _additionalInfoList; 27 | private SolidColorBrush? _titleBrushGrid; 28 | private SolidColorBrush? _titleBrushList; 29 | private SelectedButtonStyle _buttonStyle; 30 | 31 | private string _elementColorHex = "#c7cad1"; 32 | private SolidColorBrush? _additionalInfoBrush; 33 | 34 | private bool _darkSymbols; 35 | 36 | protected ColorableElement(string elementColorHex) => this.ElementColorHex = elementColorHex; 37 | 38 | [JsonInclude] 39 | public string ElementColorHex 40 | { 41 | get => _elementColorHex; 42 | protected set 43 | { 44 | if (_elementColorHex.Equals(value)) 45 | return; 46 | _elementColorHex = value; 47 | ApplyChangedColor(); 48 | 49 | this.RaisePropertyChanged(nameof(ElementColor)); 50 | this.RaisePropertyChanged(nameof(TitleBrush)); 51 | this.RaisePropertyChanged(nameof(BackgroundBrush)); 52 | this.RaisePropertyChanged(nameof(BackgroundBrushHover)); 53 | this.RaisePropertyChanged(nameof(AdditionalInfoColor)); 54 | } 55 | } 56 | 57 | protected virtual int GridThresholdAdditionalInfo { get; } 58 | protected virtual int ListThresholdAdditionalInfo { get; } 59 | 60 | internal Color ElementColor { get; private set; } 61 | 62 | internal SolidColorBrush? TitleBrush { get; private set; } 63 | 64 | internal LinearGradientBrush? BackgroundBrush { get; private set; } 65 | 66 | internal LinearGradientBrush? BackgroundBrushHover { get; private set; } 67 | 68 | internal SolidColorBrush? AdditionalInfoColor 69 | { 70 | get => _additionalInfoBrush; 71 | private set => this.RaiseAndSetIfChanged(ref _additionalInfoBrush, value); 72 | } 73 | 74 | protected bool DarkSymbols 75 | { 76 | get => _darkSymbols; 77 | set => this.RaiseAndSetIfChanged(ref _darkSymbols, value); 78 | } 79 | 80 | private Color DarkTitleTint => ElementColor.DarkenColor(TitleDarkenFactor); 81 | private Color LightTitleTint => ElementColor.BrightenColor(TitleBrightenFactor); 82 | 83 | private Color AdditionalInfoDark => _additionalInfoBaseColor.DarkenColor(AdditionalInfoDarkenFactor); 84 | private Color AdditionalInfoLight => _additionalInfoBaseColor.BrightenColor(AdditionalInfoBrightenFactor); 85 | 86 | protected virtual void ApplyChangedColor() 87 | { 88 | ElementColor = Color.Parse(_elementColorHex); 89 | 90 | var backgroundGradient = Utilities.CreateLinearGradientBrush( 91 | new RelativePoint(0, 1, RelativeUnit.Relative), 92 | new RelativePoint(1, 0, RelativeUnit.Relative), 93 | new[] { ElementColor, _lightBackground }, 94 | new[] { 0.2, 1.0 }); 95 | BackgroundBrush = backgroundGradient; 96 | 97 | var backgroundGradientHover = Utilities.CreateLinearGradientBrush( 98 | new RelativePoint(0, 1, RelativeUnit.Relative), 99 | new RelativePoint(1, 0, RelativeUnit.Relative), 100 | new[] { ElementColor.DarkenColor(0.075f), _lightBackground.DarkenColor(0.075f) }, 101 | new[] { 0.2, 1.0 }); 102 | BackgroundBrushHover = backgroundGradientHover; 103 | 104 | SetAdditionalInfoColor(); 105 | } 106 | 107 | internal void AdjustTextColors(bool isGrid, bool changeButtonStyle = true) 108 | { 109 | if(changeButtonStyle) 110 | _buttonStyle = isGrid ? SelectedButtonStyle.Grid : SelectedButtonStyle.List; 111 | AdditionalInfoColor = isGrid ? _additionalInfoGrid : _additionalInfoList; 112 | TitleBrush = isGrid ? _titleBrushGrid : _titleBrushList; 113 | 114 | int threshold = isGrid ? GridThresholdAdditionalInfo : ListThresholdAdditionalInfo; 115 | DarkSymbols = ElementColor.PerceivedBrightness() > threshold; 116 | } 117 | 118 | private void SetAdditionalInfoColor() 119 | { 120 | var gridColor = ElementColor.AdjustForegroundBrightness(AdditionalInfoDark, AdditionalInfoLight, GridThresholdAdditionalInfo); 121 | _additionalInfoGrid = new SolidColorBrush(gridColor); 122 | var listColor = ElementColor.AdjustForegroundBrightness(AdditionalInfoDark, AdditionalInfoLight, ListThresholdAdditionalInfo); 123 | _additionalInfoList = new SolidColorBrush(listColor); 124 | 125 | gridColor = ElementColor.AdjustForegroundBrightness(DarkTitleTint, LightTitleTint, GridThresholdTitle); 126 | _titleBrushGrid = new SolidColorBrush(gridColor); 127 | listColor = ElementColor.AdjustForegroundBrightness(DarkTitleTint, LightTitleTint, ListThresholdTitle); 128 | _titleBrushList = new SolidColorBrush(listColor); 129 | 130 | bool isGrid = _buttonStyle == SelectedButtonStyle.Grid; 131 | AdjustTextColors(isGrid, false); 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /src/Models/Elements/Grade.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text.Json.Serialization; 4 | using GradeManagement.Enums; 5 | using GradeManagement.Interfaces; 6 | using GradeManagement.Models.Settings; 7 | using GradeManagement.ViewModels; 8 | using GradeManagement.ViewModels.Lists; 9 | using GradeManagement.Views.Lists.ElementButtonControls; 10 | using ReactiveUI; 11 | 12 | namespace GradeManagement.Models.Elements 13 | { 14 | [JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)] 15 | [JsonDerivedType(typeof(Grade), "grade")] 16 | [JsonDerivedType(typeof(GradeGroup), "gradeGroup")] 17 | public class Grade : ReactiveObject, IElement, ICloneable 18 | { 19 | private const int MaxNameLength = 64; 20 | private const double EnabledOpacity = 1.0; 21 | private const double DisabledOpacity = 0.4; 22 | private const double DisabledOpacityGrade = 0.4; 23 | 24 | private string _name = string.Empty; 25 | private float _gradeValue = float.NaN; 26 | private float? _scoredPoints; 27 | private float? _maxPoints; 28 | private float _weighting; 29 | private DateTime _date; 30 | private bool _counts; 31 | private ButtonStyleBase? _buttonStyle; 32 | 33 | [JsonConstructor] 34 | public Grade(string name, float gradeValue, float? scoredPoints, float? maxPoints, float weighting, DateTime date, bool counts) 35 | { 36 | if (name.Length > MaxNameLength) 37 | name = name.Substring(0, MaxNameLength); 38 | this.Name = name; 39 | this.GradeValue = gradeValue; 40 | this.ScoredPoints = scoredPoints; 41 | this.MaxPoints = maxPoints; 42 | this.Weighting = weighting; 43 | this.Date = date; 44 | this.Counts = counts; 45 | 46 | var isGrid = SettingsManager.Settings?.GradeButtonStyle == SelectedButtonStyle.Grid; 47 | this.ButtonStyle = isGrid ? new GridButton(this) : new ListButton(this); 48 | } 49 | 50 | [JsonInclude] 51 | public string Name 52 | { 53 | get => _name; 54 | private set => this.RaiseAndSetIfChanged(ref _name, value); 55 | } 56 | 57 | [JsonInclude] 58 | public virtual float GradeValue 59 | { 60 | get => _gradeValue; 61 | private set 62 | { 63 | if (value.Equals(_gradeValue)) 64 | return; 65 | _gradeValue = value; 66 | this.RaisePropertyChanged(nameof(RoundedGrade)); 67 | } 68 | } 69 | 70 | [JsonInclude] 71 | public float? ScoredPoints 72 | { 73 | get => _scoredPoints; 74 | private set => this.RaiseAndSetIfChanged(ref _scoredPoints, value); 75 | } 76 | 77 | [JsonInclude] 78 | public float? MaxPoints 79 | { 80 | get => _maxPoints; 81 | private set => this.RaiseAndSetIfChanged(ref _maxPoints, value); 82 | } 83 | 84 | [JsonInclude] 85 | public float Weighting 86 | { 87 | get => _weighting; 88 | private set => this.RaiseAndSetIfChanged(ref _weighting, value); 89 | } 90 | 91 | [JsonInclude] 92 | public DateTime Date 93 | { 94 | get => _date; 95 | private set 96 | { 97 | if(value.Equals(_date)) 98 | return; 99 | _date = value; 100 | this.RaisePropertyChanged(nameof(DateString)); 101 | } 102 | } 103 | 104 | [JsonInclude] 105 | public bool Counts 106 | { 107 | get => _counts; 108 | private set 109 | { 110 | this.RaiseAndSetIfChanged(ref _counts, value); 111 | this.RaisePropertyChanged(nameof(ElementsOpacity)); 112 | this.RaisePropertyChanged(nameof(GradeTextOpacity)); 113 | } 114 | } 115 | 116 | [JsonIgnore] 117 | public virtual int ElementCount => 1; 118 | 119 | [JsonIgnore] 120 | public ButtonStyleBase? ButtonStyle 121 | { 122 | get => _buttonStyle; 123 | set => this.RaiseAndSetIfChanged(ref _buttonStyle, value); 124 | } 125 | 126 | internal float RoundedGrade => (float)Math.Round(GradeValue, 2); 127 | internal string DateString => Date.ToString("dd.MM.yyyy", CultureInfo.CurrentCulture); 128 | internal double ElementsOpacity => Counts ? EnabledOpacity : DisabledOpacity; 129 | internal double GradeTextOpacity => Counts ? EnabledOpacity : DisabledOpacityGrade; 130 | 131 | internal bool IsMultiGrade => this is GradeGroup; 132 | 133 | internal bool PointsSpecified => !IsMultiGrade && ScoredPoints != null && MaxPoints != null; 134 | 135 | public T? Duplicate(bool save = true) where T : class, IElement 136 | { 137 | if (this.Clone() is not Grade duplicate) 138 | return null; 139 | 140 | if(save) 141 | Save(duplicate); 142 | 143 | return duplicate as T; 144 | } 145 | 146 | public void Save(T? element = null) where T : class, IElement 147 | { 148 | Grade grade = element as Grade ?? this; 149 | IGradesContainer? container = null; 150 | if (MainWindowViewModel.Instance?.Content is GradeListViewModel viewModel) 151 | container = viewModel.GradesContainer; 152 | container?.Grades.Add(grade); 153 | } 154 | 155 | public object Clone() => new Grade(_name, _gradeValue, _scoredPoints, _maxPoints, _weighting, _date, Counts); 156 | 157 | internal void Edit(string newName, float newGrade, float? newScoredPoints, float? newMaxPoints, float newWeighting, 158 | DateTime newDate, bool counts) 159 | { 160 | var oldGrade = this.GradeValue; 161 | var oldCounts = this.Counts; 162 | var oldWeighting = this.Weighting; 163 | 164 | this.Name = newName; 165 | this.GradeValue = newGrade; 166 | this.ScoredPoints = newScoredPoints; 167 | this.MaxPoints = newMaxPoints; 168 | this.Weighting = newWeighting; 169 | this.Date = newDate; 170 | this.Counts = counts; 171 | this.RaisePropertyChanged(nameof(PointsSpecified)); 172 | 173 | if (oldCounts == counts && Math.Abs(oldWeighting - newWeighting) < 0.001f && Math.Abs(oldGrade - newGrade) < 0.001f) 174 | return; 175 | var mainInstance = MainWindowViewModel.Instance; 176 | mainInstance?.UpdateAverage(); 177 | } 178 | } 179 | } -------------------------------------------------------------------------------- /src/Models/Elements/GradeGroup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json.Serialization; 5 | using GradeManagement.ExtensionCollection; 6 | using GradeManagement.Interfaces; 7 | using GradeManagement.UtilityCollection; 8 | using GradeManagement.ViewModels; 9 | using GradeManagement.ViewModels.Lists; 10 | 11 | namespace GradeManagement.Models.Elements 12 | { 13 | public class GradeGroup : Grade, IElement, IGradesContainer, ICloneable 14 | { 15 | [JsonConstructor] 16 | public GradeGroup(string name, List grades, float weighting, DateTime date, bool counts) 17 | : base(name, Utilities.GetAverage(grades, false), null, null, weighting, date, counts) 18 | { 19 | this.Grades = grades; 20 | } 21 | 22 | public GradeGroup(string name, ICollection grades, float weighting, DateTime date, bool counts) 23 | : base(name, Utilities.GetAverage(grades, false), null, null, weighting, date, counts) 24 | { 25 | this.Grades = grades.ToList(); 26 | } 27 | 28 | [JsonIgnore] 29 | public IGradesContainer? ParentContainer { get; set; } 30 | 31 | [JsonInclude] 32 | public List Grades { get; set; } 33 | 34 | [JsonIgnore] 35 | public override float GradeValue => Utilities.GetAverage(Grades, true); 36 | 37 | [JsonIgnore] 38 | public override int ElementCount => Grades.Count; 39 | 40 | public new object Clone() => new GradeGroup(Name, Grades.Clone().ToList(), Weighting, Date, Counts); 41 | 42 | public new T? Duplicate(bool save = true) where T : class, IElement 43 | { 44 | if (this.Clone() is not GradeGroup duplicate) 45 | return null; 46 | 47 | if(save) 48 | Save(duplicate); 49 | return duplicate as T; 50 | } 51 | 52 | public new void Save(T? element = null) where T : class, IElement 53 | { 54 | GradeGroup grade = element as GradeGroup ?? this; 55 | IGradesContainer? container = null; 56 | if (MainWindowViewModel.Instance?.Content is GradeListViewModel viewModel) 57 | container = viewModel.GradesContainer; 58 | container?.Grades.Add(grade); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/Models/Elements/SchoolYear.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.Json.Serialization; 4 | using GradeManagement.Enums; 5 | using GradeManagement.ExtensionCollection; 6 | using GradeManagement.Interfaces; 7 | using GradeManagement.Models.Settings; 8 | using GradeManagement.UtilityCollection; 9 | using GradeManagement.Views.Lists.ElementButtonControls; 10 | using ReactiveUI; 11 | 12 | namespace GradeManagement.Models.Elements 13 | { 14 | public class SchoolYear : ColorableElement, IElement 15 | { 16 | private const int MaxNameLength = 64; 17 | 18 | private string _name = string.Empty; 19 | private List _subjects = new(); 20 | private ButtonStyleBase? _buttonStyle; 21 | 22 | public SchoolYear(string name, string elementColorHex) : base(elementColorHex) 23 | { 24 | if (name.Length > MaxNameLength) 25 | name = name.Substring(0, MaxNameLength); 26 | this.Name = name; 27 | 28 | var isGrid = SettingsManager.Settings?.YearButtonStyle == SelectedButtonStyle.Grid; 29 | this.ButtonStyle = isGrid ? new GridButton(this) : new ListButton(this); 30 | AdjustTextColors(isGrid); 31 | } 32 | 33 | [JsonConstructor] 34 | public SchoolYear(string name, string elementColorHex, List subjects) : this(name, elementColorHex) 35 | { 36 | this.Subjects = subjects; 37 | } 38 | 39 | [JsonInclude] 40 | public string Name 41 | { 42 | get => _name; 43 | private set => this.RaiseAndSetIfChanged(ref _name, value); 44 | } 45 | 46 | [JsonInclude] 47 | public List Subjects 48 | { 49 | get => _subjects; 50 | private set 51 | { 52 | if (value.SequenceEqual(_subjects)) 53 | return; 54 | this.RaiseAndSetIfChanged(ref _subjects, value); 55 | this.RaisePropertyChanged(nameof(GradeValue)); 56 | } 57 | } 58 | 59 | [JsonIgnore] 60 | public ButtonStyleBase? ButtonStyle 61 | { 62 | get => _buttonStyle; 63 | set => this.RaiseAndSetIfChanged(ref _buttonStyle, value); 64 | } 65 | 66 | [JsonIgnore] 67 | public float GradeValue => Utilities.GetAverage(Subjects, true); 68 | 69 | [JsonIgnore] 70 | public float Weighting => 1; 71 | 72 | [JsonIgnore] 73 | public bool Counts => true; 74 | 75 | [JsonIgnore] 76 | public int ElementCount => Subjects.Count; 77 | 78 | protected override int GridThresholdAdditionalInfo => 120; 79 | protected override int ListThresholdAdditionalInfo => 135; 80 | 81 | public T? Duplicate(bool save = true) where T : class, IElement 82 | { 83 | var duplicate = this.Clone(); 84 | if(save) 85 | Save(duplicate); 86 | 87 | return duplicate as T; 88 | } 89 | 90 | public void Save(T? element = null) where T : class, IElement 91 | { 92 | SchoolYear year = element as SchoolYear ?? this; 93 | DataManager.SchoolYears.Add(year); 94 | } 95 | 96 | internal void Edit(string newName, string colorHex) 97 | { 98 | this.Name = newName; 99 | this.ElementColorHex = colorHex; 100 | } 101 | 102 | private SchoolYear Clone() => new(_name, ElementColorHex, _subjects.Clone().ToList()); 103 | } 104 | } -------------------------------------------------------------------------------- /src/Models/Elements/Subject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json.Serialization; 5 | using GradeManagement.Enums; 6 | using GradeManagement.ExtensionCollection; 7 | using GradeManagement.Interfaces; 8 | using GradeManagement.Models.Settings; 9 | using GradeManagement.UtilityCollection; 10 | using GradeManagement.ViewModels; 11 | using GradeManagement.Views.Lists.ElementButtonControls; 12 | using ReactiveUI; 13 | 14 | namespace GradeManagement.Models.Elements 15 | { 16 | public class Subject : ColorableElement, IElement, ICloneable, IGradesContainer 17 | { 18 | private const int MaxNameLength = 64; 19 | private const double EnabledOpacity = 1.0; 20 | private const double DisabledOpacity = 0.6; 21 | private const double DisabledOpacityGrade = 0.4; 22 | 23 | private string _name = string.Empty; 24 | private List _grades = new(); 25 | private float _weighting; 26 | private bool _counts; 27 | private ButtonStyleBase? _buttonStyle; 28 | 29 | public Subject(string name, float weighting, string elementColorHex, bool counts) : base(elementColorHex) 30 | { 31 | if (name.Length > MaxNameLength) 32 | name = name.Substring(0, MaxNameLength); 33 | this.Name = name; 34 | this.Weighting = weighting; 35 | this.Counts = counts; 36 | 37 | var isGrid = SettingsManager.Settings?.SubjectButtonStyle == SelectedButtonStyle.Grid; 38 | this.ButtonStyle = isGrid ? new GridButton(this) : new ListButton(this); 39 | AdjustTextColors(isGrid); 40 | } 41 | 42 | [JsonConstructor] 43 | public Subject(string name, float weighting, string elementColorHex, List grades, bool counts) 44 | : this(name, weighting, elementColorHex, counts) 45 | { 46 | this.Grades = grades; 47 | } 48 | 49 | [JsonInclude] 50 | public string Name 51 | { 52 | get => _name; 53 | private set => this.RaiseAndSetIfChanged(ref _name, value); 54 | } 55 | 56 | [JsonIgnore] 57 | public IGradesContainer? ParentContainer { get; } 58 | 59 | [JsonInclude] 60 | public List Grades 61 | { 62 | get => _grades; 63 | private set 64 | { 65 | if (value.SequenceEqual(_grades)) 66 | return; 67 | this.RaiseAndSetIfChanged(ref _grades, value); 68 | this.RaisePropertyChanged(nameof(ElementCount)); 69 | this.RaisePropertyChanged(nameof(RoundedAverage)); 70 | } 71 | } 72 | 73 | [JsonInclude] 74 | public float Weighting 75 | { 76 | get => _weighting; 77 | private set => this.RaiseAndSetIfChanged(ref _weighting, value); 78 | } 79 | 80 | [JsonInclude] 81 | public bool Counts 82 | { 83 | get => _counts; 84 | private set 85 | { 86 | this.RaiseAndSetIfChanged(ref _counts, value); 87 | this.RaisePropertyChanged(nameof(ElementsOpacity)); 88 | this.RaisePropertyChanged(nameof(GradeTextOpacity)); 89 | } 90 | } 91 | 92 | [JsonIgnore] 93 | public float GradeValue => Utilities.GetAverage(Grades, false); 94 | 95 | [JsonIgnore] 96 | public int ElementCount => Grades.Count; 97 | 98 | [JsonIgnore] 99 | public ButtonStyleBase? ButtonStyle 100 | { 101 | get => _buttonStyle; 102 | set => this.RaiseAndSetIfChanged(ref _buttonStyle, value); 103 | } 104 | 105 | protected override int GridThresholdAdditionalInfo => 120; 106 | protected override int ListThresholdAdditionalInfo => 135; 107 | 108 | internal float RoundedAverage => Utilities.GetAverage(Grades, true); 109 | internal double ElementsOpacity => Counts ? EnabledOpacity : DisabledOpacity; 110 | internal double GradeTextOpacity => Counts ? EnabledOpacity : DisabledOpacityGrade; 111 | 112 | public T? Duplicate(bool save = true) where T : class, IElement 113 | { 114 | if (Clone() is not Subject duplicate) 115 | return null; 116 | 117 | if (save) 118 | Save(duplicate); 119 | 120 | return duplicate as T; 121 | } 122 | 123 | public void Save(T? element = null) where T : class, IElement 124 | { 125 | Subject subject = element as Subject ?? this; 126 | var currentYear = MainWindowViewModel.CurrentYear; 127 | currentYear?.Subjects.Add(subject); 128 | } 129 | 130 | public object Clone() => new Subject(_name, _weighting, ElementColorHex, _grades.Clone().ToList(), Counts); 131 | 132 | internal void Edit(string newName, float newWeighting, string newSubjectColorHex, bool counts) 133 | { 134 | var oldCounts = this.Counts; 135 | var oldWeighting = this.Weighting; 136 | 137 | this.Name = newName; 138 | this.Weighting = newWeighting; 139 | this.Counts = counts; 140 | this.ElementColorHex = newSubjectColorHex; 141 | 142 | if (oldCounts == counts && Math.Abs(oldWeighting - newWeighting) < 0.001f) 143 | return; 144 | var mainInstance = MainWindowViewModel.Instance; 145 | mainInstance?.UpdateAverage(); 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /src/Models/MonthRepresentation.cs: -------------------------------------------------------------------------------- 1 | using GradeManagement.Converters; 2 | 3 | namespace GradeManagement.Models 4 | { 5 | public class MonthRepresentation 6 | { 7 | private int _month; 8 | private string? _monthName; 9 | 10 | public MonthRepresentation(int month) => this.Month = month; 11 | public MonthRepresentation(string monthName) => this.MonthName = monthName; 12 | 13 | internal int Month 14 | { 15 | get => _month; 16 | private init 17 | { 18 | if (value is <= 0 or > 12) 19 | return; 20 | _month = value; 21 | _monthName = MonthConverter.ConvertMonth(value); 22 | } 23 | } 24 | 25 | internal string MonthName 26 | { 27 | get => _monthName!; 28 | private init 29 | { 30 | if (!MonthConverter.TryParseMonth(value, out int monthNumber)) 31 | { 32 | _monthName = string.Empty; 33 | _month = 0; 34 | return; 35 | } 36 | 37 | _monthName = value; 38 | _month = monthNumber; 39 | } 40 | } 41 | 42 | public override string? ToString() => _monthName; 43 | internal void Set(string monthName) => _monthName = monthName; 44 | internal void Set(int monthNum) => _month = monthNum; 45 | } 46 | } -------------------------------------------------------------------------------- /src/Models/Settings/Preferences.cs: -------------------------------------------------------------------------------- 1 | using GradeManagement.Enums; 2 | 3 | namespace GradeManagement.Models.Settings 4 | { 5 | public class Preferences 6 | { 7 | public SelectedButtonStyle YearButtonStyle { get; set; } = SelectedButtonStyle.Grid; 8 | public SelectedButtonStyle SubjectButtonStyle { get; set; } = SelectedButtonStyle.Grid; 9 | public SelectedButtonStyle GradeButtonStyle { get; set; } = SelectedButtonStyle.Grid; 10 | 11 | public bool ShowRemoveConfirmation { get; set; } = true; 12 | } 13 | } -------------------------------------------------------------------------------- /src/Models/Settings/SettingsManager.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using GradeManagement.UtilityCollection; 5 | 6 | namespace GradeManagement.Models.Settings 7 | { 8 | public static class SettingsManager 9 | { 10 | internal static Preferences? Settings { get; private set; } = new(); 11 | private static string FilePath => Path.Combine(Utilities.FilesParentPath, "Settings.json"); 12 | 13 | internal static void LoadSettings() 14 | { 15 | if (!File.Exists(FilePath)) 16 | { 17 | SaveSettings(); 18 | return; 19 | } 20 | 21 | string content = File.ReadAllText(FilePath); 22 | var options = new JsonSerializerOptions 23 | { 24 | Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } 25 | }; 26 | Settings = JsonSerializer.Deserialize(content, options); 27 | 28 | DataManager.LoadData(); 29 | } 30 | 31 | internal static void SaveSettings() 32 | { 33 | var options = new JsonSerializerOptions 34 | { 35 | WriteIndented = true, 36 | Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } 37 | }; 38 | string jsonString = JsonSerializer.Serialize(Settings, options); 39 | File.WriteAllText(FilePath, jsonString); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.ReactiveUI; 3 | 4 | namespace GradeManagement 5 | { 6 | internal static class Program 7 | { 8 | // Initialization code. Don't use any Avalonia, third-party APIs or any 9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 10 | // yet and stuff might break. 11 | public static void Main(string[] args) => BuildAvaloniaApp() 12 | .StartWithClassicDesktopLifetime(args); 13 | 14 | // Avalonia configuration, don't remove; also used by visual designer. 15 | private static AppBuilder BuildAvaloniaApp() 16 | => AppBuilder.Configure() 17 | .UsePlatformDetect() 18 | .LogToTrace() 19 | .UseReactiveUI(); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Resources/BaseResources.axaml: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /src/Resources/DarkTheme.axaml: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/Resources/DarkTheme.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Media; 3 | using GradeManagement.UtilityCollection; 4 | 5 | namespace GradeManagement.ResourcesNamespace 6 | { 7 | public class DarkTheme : Theme 8 | { 9 | public DarkTheme() 10 | { 11 | SetResources(); 12 | } 13 | 14 | protected internal override void SetResources() 15 | { 16 | OppositeAccentBrush = 17 | Utilities.GetResourceFromStyle(Application.Current, "OppositeAccent", StyleIndex) 18 | ?? new SolidColorBrush(Color.Parse("#FFFFFF")); 19 | SameAccentBrush = 20 | Utilities.GetResourceFromStyle(Application.Current, "SameAccent", StyleIndex) 21 | ?? new SolidColorBrush(Color.Parse("#0A0A0A")); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Resources/LightTheme.axaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Resources/LightTheme.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Media; 3 | using GradeManagement.UtilityCollection; 4 | 5 | namespace GradeManagement.ResourcesNamespace 6 | { 7 | public class LightTheme : Theme 8 | { 9 | public LightTheme() 10 | { 11 | SetResources(); 12 | } 13 | 14 | protected internal override void SetResources() 15 | { 16 | OppositeAccentBrush = 17 | Utilities.GetResourceFromStyle(Application.Current, "OppositeAccent", StyleIndex) 18 | ?? new SolidColorBrush(Color.Parse("#0A0A0A")); 19 | SameAccentBrush = 20 | Utilities.GetResourceFromStyle(Application.Current, "SameAccent", StyleIndex) 21 | ?? new SolidColorBrush(Color.Parse("#FFFFFF")); 22 | 23 | MainBackgroundBrush = 24 | Utilities.GetResourceFromStyle(Application.Current, "MainBackground", StyleIndex) 25 | ?? new SolidColorBrush(Color.Parse("#D8DDE6")); 26 | ElementBackgroundBrush = 27 | Utilities.GetResourceFromStyle(Application.Current, "ElementBackground", StyleIndex) 28 | ?? new SolidColorBrush(Color.Parse("#C7CAD1")); 29 | VariantElementBackgroundBrush = 30 | Utilities.GetResourceFromStyle(Application.Current, "VariantElementBackground", StyleIndex) 31 | ?? new SolidColorBrush(Color.Parse("#B8BABF")); 32 | 33 | StandardGreyBrush = 34 | Utilities.GetResourceFromStyle(Application.Current, "StandardGrey", StyleIndex) 35 | ?? new SolidColorBrush(Color.Parse("#808080")); 36 | VariantStandardGreyBrush = 37 | Utilities.GetResourceFromStyle(Application.Current, "VariantStandardGrey", StyleIndex) 38 | ?? new SolidColorBrush(Color.Parse("#919191")); 39 | DarkGreyBrush = 40 | Utilities.GetResourceFromStyle(Application.Current, "DarkGrey", StyleIndex) 41 | ?? new SolidColorBrush(Color.Parse("#666666")); 42 | AlmostAccentBrush = 43 | Utilities.GetResourceFromStyle(Application.Current, "AlmostAccent", StyleIndex) 44 | ?? new SolidColorBrush(Color.Parse("#E6E6E6")); 45 | DarkerAlmostAccentBrush = 46 | Utilities.GetResourceFromStyle(Application.Current, "DarkerAlmostAccent", StyleIndex) 47 | ?? new SolidColorBrush(Color.Parse("#D4D4D4")); 48 | DarkenedDialogBackgroundBrush = 49 | Utilities.GetResourceFromStyle(Application.Current, "DarkenedDialogBackground", StyleIndex) 50 | ?? new SolidColorBrush(Color.Parse("#66000000")); 51 | HighlyDarkenedBackgroundBrush = 52 | Utilities.GetResourceFromStyle(Application.Current, "HighlyDarkenedBackground", StyleIndex) 53 | ?? new SolidColorBrush(Color.Parse("#99000000")); 54 | InfoButtonGreyBrush = 55 | Utilities.GetResourceFromStyle(Application.Current, "InfoButtonGrey", StyleIndex) 56 | ?? new SolidColorBrush(Color.Parse("#8B8D92")); 57 | 58 | CalendarDayButtonHoverBrush = 59 | Utilities.GetResourceFromStyle(Application.Current, "CalendarDayButtonHover", StyleIndex) 60 | ?? new SolidColorBrush(Color.Parse("#DCDCDC")); 61 | CalendarDayButtonInactiveBrush = 62 | Utilities.GetResourceFromStyle(Application.Current, "CalendarDayButtonInactive", StyleIndex) 63 | ?? new SolidColorBrush(Color.Parse("#C9C9C9")); 64 | CalendarDayButtonInactiveHoverBrush = 65 | Utilities.GetResourceFromStyle(Application.Current, "CalendarDayButtonInactiveHover", StyleIndex) 66 | ?? new SolidColorBrush(Color.Parse("#D1D1D1")); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/Resources/Resources.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Avalonia; 3 | using Avalonia.Media; 4 | using GradeManagement.Enums; 5 | using GradeManagement.UtilityCollection; 6 | 7 | namespace GradeManagement.ResourcesNamespace 8 | { 9 | public static partial class Resources 10 | { 11 | private const int StyleIndex = 2; 12 | 13 | private static readonly LightTheme _lightThemeInstance = new(); 14 | private static readonly DarkTheme _darkThemeInstance = new(); 15 | private static readonly Dictionary _themesLookup = new() 16 | { 17 | { ThemeMode.Light, _lightThemeInstance }, { ThemeMode.Dark, _darkThemeInstance } 18 | }; 19 | 20 | public static Theme CurrentTheme { get; private set; } = _lightThemeInstance; 21 | 22 | // SolidColorBrush 23 | 24 | public static readonly SolidColorBrush FullyTransparentBrush = 25 | Utilities.GetResourceFromStyle(Application.Current, "FullyTransparent", StyleIndex) 26 | ?? new SolidColorBrush(Color.Parse("#0000FFFF")); 27 | public static readonly SolidColorBrush AppGreenBrush = 28 | Utilities.GetResourceFromStyle(Application.Current, "AppGreen", StyleIndex) 29 | ?? new SolidColorBrush(Color.Parse("#009B72")); 30 | public static readonly SolidColorBrush VariantAppGreenBrush = 31 | Utilities.GetResourceFromStyle(Application.Current, "VariantAppGreen", StyleIndex) 32 | ?? new SolidColorBrush(Color.Parse("#00AD7F")); 33 | public static readonly SolidColorBrush LightPurpleBrush = 34 | Utilities.GetResourceFromStyle(Application.Current, "LightPurple", StyleIndex) 35 | ?? new SolidColorBrush(Color.Parse("#A5B1CC")); 36 | public static readonly SolidColorBrush InvalidColorBrush = 37 | Utilities.GetResourceFromStyle(Application.Current, "InvalidColor", StyleIndex) 38 | ?? new SolidColorBrush(Color.Parse("#D64045")); 39 | 40 | public static readonly SolidColorBrush LightGreenContextMenuBrush = 41 | Utilities.GetResourceFromStyle(Application.Current, "LightGreenContextMenu", StyleIndex) 42 | ?? new SolidColorBrush(Color.Parse("#58C4A8")); 43 | public static readonly SolidColorBrush DarkerLightGreenContextMenuBrush = 44 | Utilities.GetResourceFromStyle(Application.Current, "DarkerLightGreenContextMenu", StyleIndex) 45 | ?? new SolidColorBrush(Color.Parse("#0EB085")); 46 | public static readonly SolidColorBrush LightRedContextMenuBrush = 47 | Utilities.GetResourceFromStyle(Application.Current, "LightRedContextMenu", StyleIndex) 48 | ?? new SolidColorBrush(Color.Parse("#D4735B")); 49 | public static readonly SolidColorBrush DarkerLightRedContextMenuBrush = 50 | Utilities.GetResourceFromStyle(Application.Current, "DarkerLightRedContextMenu", StyleIndex) 51 | ?? new SolidColorBrush(Color.Parse("#C96047")); 52 | 53 | // Colors 54 | public static readonly Color FullyTransparent = FullyTransparentBrush.Color; 55 | public static readonly Color AppGreen = AppGreenBrush.Color; 56 | public static readonly Color VariantAppGreen = VariantAppGreenBrush.Color; 57 | public static readonly Color LightPurple = LightPurpleBrush.Color; 58 | public static readonly Color InvalidColor = InvalidColorBrush.Color; 59 | 60 | public static readonly Color LightGreenContextMenu = LightGreenContextMenuBrush.Color; 61 | public static readonly Color DarkerLightGreenContextMenu = DarkerLightGreenContextMenuBrush.Color; 62 | public static readonly Color LightRedContextMenu = LightRedContextMenuBrush.Color; 63 | public static readonly Color DarkerLightRedContextMenu = DarkerLightRedContextMenuBrush.Color; 64 | 65 | internal static void SetTheme(ThemeMode themeMode) 66 | { 67 | if (_themesLookup.ContainsKey(themeMode)) 68 | CurrentTheme = _themesLookup[themeMode]; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/Resources/Theme.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Media; 2 | 3 | namespace GradeManagement.ResourcesNamespace 4 | { 5 | public abstract class Theme 6 | { 7 | protected const int StyleIndex = 2; 8 | 9 | // SolidColorBrush 10 | 11 | protected internal SolidColorBrush OppositeAccentBrush { get; protected set; } = null!; 12 | protected internal SolidColorBrush SameAccentBrush { get; protected set; } = null!; 13 | 14 | protected internal SolidColorBrush MainBackgroundBrush { get; protected set; } = null!; 15 | protected internal SolidColorBrush ElementBackgroundBrush { get; protected set; } = null!; 16 | protected internal SolidColorBrush VariantElementBackgroundBrush { get; protected set; } = null!; 17 | 18 | protected internal SolidColorBrush StandardGreyBrush { get; protected set; } = null!; 19 | protected internal SolidColorBrush VariantStandardGreyBrush { get; protected set; } = null!; 20 | protected internal SolidColorBrush DarkGreyBrush { get; protected set; } = null!; 21 | protected internal SolidColorBrush AlmostAccentBrush { get; protected set; } = null!; 22 | protected internal SolidColorBrush DarkerAlmostAccentBrush { get; protected set; } = null!; 23 | protected internal SolidColorBrush DarkenedDialogBackgroundBrush { get; protected set; } = null!; 24 | protected internal SolidColorBrush HighlyDarkenedBackgroundBrush { get; protected set; } = null!; 25 | protected internal SolidColorBrush InfoButtonGreyBrush { get; protected set; } = null!; 26 | 27 | protected internal SolidColorBrush CalendarDayButtonHoverBrush { get; protected set; } = null!; 28 | protected internal SolidColorBrush CalendarDayButtonInactiveBrush { get; protected set; } = null!; 29 | protected internal SolidColorBrush CalendarDayButtonInactiveHoverBrush { get; protected set; } = null!; 30 | 31 | // Color 32 | 33 | protected internal Color OppositeAccent => OppositeAccentBrush.Color; 34 | protected internal Color SameAccent => SameAccentBrush.Color; 35 | 36 | protected internal Color MainBackground => MainBackgroundBrush.Color; 37 | protected internal Color ElementBackground => ElementBackgroundBrush.Color; 38 | protected internal Color VariantElementBackground => VariantElementBackgroundBrush.Color; 39 | 40 | protected internal Color StandardGrey => StandardGreyBrush.Color; 41 | protected internal Color VariantStandardGrey => VariantStandardGreyBrush.Color; 42 | protected internal Color DarkGrey => DarkGreyBrush.Color; 43 | protected internal Color AlmostAccent => AlmostAccentBrush.Color; 44 | protected internal Color DarkerAlmostAccent => DarkerAlmostAccentBrush.Color; 45 | protected internal Color DarkenedDialogBackground => DarkenedDialogBackgroundBrush.Color; 46 | protected internal Color HighlyDarkenedBackground => HighlyDarkenedBackgroundBrush.Color; 47 | protected internal Color InfoButtonGrey => InfoButtonGreyBrush.Color; 48 | 49 | protected internal Color CalendarDayButtonHover => CalendarDayButtonHoverBrush.Color; 50 | protected internal Color CalendarDayButtonInactive => CalendarDayButtonInactiveBrush.Color; 51 | protected internal Color CalendarDayButtonInactiveHover => CalendarDayButtonInactiveHoverBrush.Color; 52 | 53 | protected internal abstract void SetResources(); 54 | } 55 | } -------------------------------------------------------------------------------- /src/Styles/AddPagesStyle.axaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 42 | 43 | 44 | 45 | 59 | 62 | 65 | 66 | 69 | 70 | 71 | 72 | 78 | 81 | 85 | 89 | 90 | 91 | 92 | 103 | 106 | 107 | 108 | 109 | 117 | 120 | 123 | 124 | 127 | -------------------------------------------------------------------------------- /src/Styles/CalendarStyle.axaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 11 | 12 | 40 | 43 | 44 | 50 | 51 | 55 | 56 | 61 | 64 | 65 | 69 | 72 | 73 | 74 | 100 | 101 | 107 | 108 | 113 | 116 | -------------------------------------------------------------------------------- /src/Styles/ContextMenuStyle.axaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 10 | 13 | -------------------------------------------------------------------------------- /src/Styles/DialogStyle.axaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 17 | 20 | 21 | 30 | 31 | 37 | 38 | 44 | 48 | 51 | 55 | -------------------------------------------------------------------------------- /src/Styles/ListViewStyle.axaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 14 | 17 | 18 | 21 | 22 | 27 | 30 | 33 | 34 | 41 | 42 | 43 | 44 | 50 | 51 | 56 | 57 | 63 | -------------------------------------------------------------------------------- /src/Styles/MainStyle.axaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 15 | 16 | 24 | 28 | 31 | -------------------------------------------------------------------------------- /src/Styles/MainWindowStyle.axaml: -------------------------------------------------------------------------------- 1 |  3 | 4 | 5 | 6 | 10 | 11 | 17 | 18 | 22 | 23 | 26 | 27 | 28 | 29 | 34 | 37 | 40 | 41 | 45 | 46 | 52 | 53 | 59 | 60 | 61 | 62 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/Styles/TargetGradeStyle.axaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 19 | 20 | 28 | 29 | 37 | -------------------------------------------------------------------------------- /src/UtilityCollection/ColorUtilities.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Avalonia; 3 | using Avalonia.Media; 4 | 5 | namespace GradeManagement.UtilityCollection 6 | { 7 | public static partial class Utilities 8 | { 9 | /// 10 | /// Create a with variable start-, endpoints and gradient stops. 11 | /// 12 | /// The start point of the gradient as a 13 | /// The end point of the gradient as a 14 | /// A collection of , each of them containing 15 | /// the and Offset of the it's intended for. 16 | /// LinearGradientBrush 17 | public static LinearGradientBrush CreateLinearGradientBrush(RelativePoint startPoint, RelativePoint endPoint, 18 | IEnumerable> gradientStopInfos) 19 | { 20 | var gradientStops = new GradientStops(); 21 | foreach (var (key, value) in gradientStopInfos) 22 | gradientStops.Add(new GradientStop(key, value)); 23 | 24 | return CreateLinearGradientBrush(startPoint, endPoint, gradientStops); 25 | } 26 | 27 | /// 28 | /// Create a with variable start-, endpoints and gradient stops. 29 | /// 30 | /// The start point of the gradient as a 31 | /// The end point of the gradient as a 32 | /// The color of each 33 | /// The offset of each 34 | /// LinearGradientBrush 35 | public static LinearGradientBrush CreateLinearGradientBrush(RelativePoint startPoint, RelativePoint endPoint, 36 | Color[] colors, double[] offsets) 37 | { 38 | var gradientStops = new GradientStops(); 39 | for (int i = 0; i < colors.Length; i++) 40 | { 41 | if(i < offsets.Length) 42 | gradientStops.Add(new GradientStop(colors[i], offsets[i])); 43 | } 44 | 45 | return CreateLinearGradientBrush(startPoint, endPoint, gradientStops); 46 | } 47 | 48 | /// 49 | /// Create a with variable start-, endpoints and gradient stops. 50 | /// 51 | /// The start point of the gradient as a 52 | /// The end point of the gradient as a 53 | /// The Gradient Stops used for the gradient. 54 | /// LinearGradientBrush 55 | public static LinearGradientBrush CreateLinearGradientBrush(RelativePoint startPoint, RelativePoint endPoint, 56 | GradientStops gradientStops) 57 | { 58 | return new LinearGradientBrush 59 | { 60 | StartPoint = startPoint, 61 | EndPoint = endPoint, 62 | GradientStops = gradientStops 63 | }; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/UtilityCollection/DateUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using GradeManagement.Enums; 4 | 5 | namespace GradeManagement.UtilityCollection 6 | { 7 | public static partial class Utilities 8 | { 9 | public static int TodaysDay => DateTime.Today.Day; 10 | public static string TodaysMonth => DateTime.Today.ToString("MMMM"); 11 | public static int TodaysYear => DateTime.Today.Year; 12 | 13 | /// 14 | /// Check whether a specific date (day, month, year) is valid. 15 | /// 16 | /// Returns an enum with a flag of the invalid parameters. 17 | /// Whether the date is valid or not. 18 | [SuppressMessage("ReSharper", "InvalidXmlDocComment")] 19 | public static bool ValidateDate(int day, int month, int year, out DateType validationProtocol) 20 | { 21 | validationProtocol = DateType.None; 22 | 23 | bool monthValid = month is > 0 and <= 12; 24 | bool yearValid = year is >= 1 and <= 9999; 25 | 26 | if (!monthValid) 27 | validationProtocol += 2; 28 | if (!yearValid) 29 | validationProtocol += 4; 30 | 31 | if (!monthValid) 32 | { 33 | if(day is < 1 or > 31) 34 | validationProtocol += 1; 35 | return false; 36 | } 37 | 38 | if (!yearValid) 39 | { 40 | if (month == 2) 41 | { 42 | if(day is < 1 or > 29) 43 | validationProtocol += 1; 44 | } 45 | else 46 | { 47 | if ((day <= 0) || (day > DateTime.DaysInMonth(2021, month))) 48 | validationProtocol += 1; 49 | } 50 | return false; 51 | } 52 | 53 | bool dayValid = (day > 0) && (day <= DateTime.DaysInMonth(year, month)); 54 | if (!dayValid) 55 | validationProtocol = DateType.Day; 56 | 57 | return dayValid; 58 | } 59 | 60 | /// 61 | /// 62 | /// Check whether a specific date (DateTime) is valid. 63 | /// 64 | public static bool ValidateDate(DateTime? date, out DateType validationProtocol) 65 | { 66 | validationProtocol = DateType.None; 67 | 68 | if (date is null) 69 | return false; 70 | 71 | var newDate = date.Value; 72 | return ValidateDate(newDate.Day, newDate.Month, newDate.Year, out validationProtocol); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/UtilityCollection/UniversalUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.IO; 5 | using System.Linq; 6 | using Avalonia.Controls; 7 | using Avalonia.Markup.Xaml.Styling; 8 | using Avalonia.Styling; 9 | using GradeManagement.Interfaces; 10 | 11 | namespace GradeManagement.UtilityCollection 12 | { 13 | public static partial class Utilities 14 | { 15 | /// 16 | /// The parent path of all settings- and data-files 17 | /// 18 | public static string FilesParentPath 19 | { 20 | get 21 | { 22 | var directory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); 23 | return directory ?? throw new Exception("Directory name of the currently executing assembly is null."); 24 | } 25 | } 26 | 27 | public const float EqualityTolerance = 0.000001f; 28 | 29 | /// 30 | /// Calculate the average of several grades with a weighting factor for each grade. 31 | /// 32 | /// A collection of IGradables, 33 | /// whose grades will be used for the average. 34 | /// Determines whether the returned average should be rounded to 2 decimal digits or not. 35 | /// The calculated average, either rounded or exact, based on the passed bool > 36 | [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] 37 | public static float GetAverage(IEnumerable gradables, bool round) 38 | { 39 | var enumerable = gradables.Where(x => x.Counts && x.ElementCount > 0 && !float.IsNaN(x.GradeValue) && x.GradeValue != 0); 40 | if (!enumerable.Any()) 41 | return 0; 42 | float result = enumerable.Sum(x => x.GradeValue * x.Weighting) / enumerable.Sum(x => x.Weighting); 43 | return round ? (float)Math.Round(result, 2) : result; 44 | } 45 | 46 | /// 47 | /// Log a message to the console (for debugging purposes). 48 | /// 49 | /// The message to be logged as a string. 50 | public static void Log(string? message) 51 | { 52 | Console.WriteLine(message); 53 | System.Diagnostics.Trace.WriteLine(message); 54 | } 55 | 56 | /// 57 | /// Retrieves a resource from a specified and tries to cast it to the specified type. 58 | /// 59 | /// The element to retrieve the resource from. 60 | /// The name of the resource you want to retrieve (key). 61 | /// The actual type of the resource you want to retrieve. 62 | /// The resource as it's actual type. 63 | public static T? GetResource(IResourceNode element, string resourceName) 64 | where T : class 65 | { 66 | element.TryGetResource(resourceName, out object? resource); 67 | return resource as T; 68 | } 69 | 70 | /// 71 | /// Retrieves a resource from a specified , which is in turns contained 72 | /// in the of an , and tries to cast it to the specified type. 73 | /// 74 | /// The element to retrieve the resource from. 75 | /// The name of the resource you want to retrieve (key). 76 | /// The index of the inside the collection 77 | /// of the . 78 | /// The actual type of the resource you want to retrieve. 79 | /// The type of the element to retrieve the resource from. 80 | /// This type must implement . 81 | /// The resource as it's actual type. 82 | public static TResource? GetResourceFromStyle(TElement? element, string resourceName, int styleIndex) 83 | where TResource : class 84 | where TElement : IStyleHost, IResourceNode 85 | { 86 | var styleInclude = element?.Styles[styleIndex] as StyleInclude; 87 | return (styleInclude?.Loaded is Style style ? GetResource(style, resourceName) : null); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/UtilityCollection/WindowUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using Avalonia.Controls; 4 | using GradeManagement.ViewModels.BaseClasses; 5 | 6 | namespace GradeManagement.UtilityCollection 7 | { 8 | public static partial class Utilities 9 | { 10 | public static TWindow? ShowAddPage(out TViewModel? viewModel, Window? parentWindow) 11 | where TWindow : Window, new() 12 | where TViewModel : AddViewModelBase, new() 13 | { 14 | var window = new TWindow(); 15 | viewModel = new TViewModel(); 16 | 17 | window.DataContext = viewModel; 18 | viewModel.CurrentAddWindow = window; 19 | 20 | return ShowDialog(window, parentWindow); 21 | } 22 | 23 | public static Window? ShowAddPage(Type? windowType, Type? viewModelType, Window? parentWindow, out AddViewModelBase? addViewModel) 24 | { 25 | addViewModel = null; 26 | if (windowType is null || viewModelType is null || Activator.CreateInstance(windowType) is not Window window) 27 | return null; 28 | 29 | var viewModel = (AddViewModelBase)Activator.CreateInstance(viewModelType)!; 30 | addViewModel = viewModel; 31 | window.DataContext = viewModel; 32 | viewModel.CurrentAddWindow = window; 33 | 34 | return ShowDialog(window, parentWindow); 35 | } 36 | 37 | public static T? ShowDialog(T window, Window? parentWindow) 38 | where T : Window 39 | { 40 | if (parentWindow is null) 41 | return null; 42 | window.ShowDialog(parentWindow); 43 | CatchClosingWindow(window); 44 | return window; 45 | } 46 | 47 | public static void CatchClosingWindow(Window window) 48 | { 49 | EventHandler? closingDel = null; 50 | closingDel = delegate 51 | { 52 | window.Closing -= closingDel; 53 | if (window.DataContext is not ViewModelBase viewModel) 54 | return; 55 | 56 | viewModel.EraseData(); 57 | }; 58 | window.Closing += closingDel; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Templates; 4 | using GradeManagement.ViewModels.BaseClasses; 5 | 6 | namespace GradeManagement 7 | { 8 | public class ViewLocator : IDataTemplate 9 | { 10 | public bool SupportsRecycling => false; 11 | 12 | public IControl Build(object data) => BuildStatic(data); 13 | public bool Match(object data) => data is ViewModelBase; 14 | 15 | internal static IControl BuildStatic(object data) => BuildStatic(data.GetType()); 16 | 17 | internal static IControl BuildStatic(Type type) 18 | { 19 | var newType = ReplaceType(type, out string name); 20 | return BuildControl(newType, name); 21 | } 22 | 23 | private static Type? ReplaceType(Type type, out string name) 24 | { 25 | name = type.FullName!.Replace("ViewModel", "View"); 26 | var newType = Type.GetType(name); 27 | 28 | return newType; 29 | } 30 | 31 | private static IControl BuildControl(Type? type, string name) 32 | { 33 | if (type != null) 34 | return (Control)Activator.CreateInstance(type)!; 35 | 36 | return new TextBlock { Text = "Not Found: " + name }; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/ViewModels/AddPages/AddSubjectViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Linq; 3 | using Avalonia.Media; 4 | using GradeManagement.Enums; 5 | using GradeManagement.ExtensionCollection; 6 | using GradeManagement.Interfaces; 7 | using GradeManagement.Models.Elements; 8 | using GradeManagement.ViewModels.BaseClasses; 9 | using GradeManagement.ViewModels.Lists; 10 | 11 | namespace GradeManagement.ViewModels.AddPages 12 | { 13 | public class AddSubjectViewModel : AddViewModelBase, IAddViewModel 14 | { 15 | public AddSubjectViewModel() 16 | { 17 | BorderBrushes = new SolidColorBrush[] { new(IncompleteColor), new(IncompleteColor) }; 18 | WeightingIndex = 1; 19 | EditPageText(AddPageAction.Create, "Subject"); 20 | } 21 | 22 | protected override bool DataComplete => !string.IsNullOrEmpty(ElementName) && !string.IsNullOrWhiteSpace(ElementName) 23 | && !float.IsNaN(ElementWeighting) 24 | && DataChanged(); 25 | 26 | private Subject? EditedSubject { get; set; } 27 | 28 | public void EditElement(Subject subject) 29 | { 30 | EditedSubject = subject; 31 | EditPageText(AddPageAction.Edit, "Subject", subject.Name); 32 | 33 | ElementName = subject.Name; 34 | ElementWeightingString = subject.Weighting.ToString(CultureInfo.CurrentCulture); 35 | ElementCounts = subject.Counts; 36 | 37 | var colorRepresentation = ElementColorsCollection.FirstOrDefault(x => x.ElementColor.Equals(subject.ElementColor)); 38 | if(colorRepresentation is not null) 39 | ChangeColor(colorRepresentation); 40 | } 41 | 42 | protected internal override void EraseData() 43 | { 44 | base.EraseData(); 45 | // TODO: Reset color selection 46 | EditedSubject = null; 47 | } 48 | 49 | private void CreateElement() 50 | { 51 | var currentYear = MainWindowViewModel.CurrentYear; 52 | if (ElementName is null || currentYear is null) 53 | return; 54 | 55 | var colorHex = SelectedColor.ElementColor.ToHexString(); 56 | if (EditedSubject is null) 57 | { 58 | var viewModel = SubjectListViewModel.Instance; 59 | var subject = new Subject(ElementName, ElementWeighting, colorHex, ElementCounts); 60 | 61 | currentYear.Subjects.SafeAdd(subject); 62 | viewModel?.Items?.Add(subject); 63 | } 64 | else 65 | EditedSubject.Edit(ElementName, ElementWeighting, colorHex, ElementCounts); 66 | 67 | UpdateVisualOnChange(); 68 | EditedSubject = null; 69 | } 70 | 71 | private bool DataChanged() 72 | { 73 | if (EditedSubject is null) 74 | return true; 75 | 76 | return ElementName is not null && (!ElementName.Trim().Equals(EditedSubject.Name.Trim()) 77 | || !ElementWeighting.Equals(EditedSubject.Weighting) 78 | || ElementCounts != EditedSubject.Counts 79 | || !SelectedColor.ElementColor.Equals(EditedSubject.ElementColor)); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/ViewModels/AddPages/AddYearViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Avalonia.Media; 3 | using GradeManagement.Enums; 4 | using GradeManagement.ExtensionCollection; 5 | using GradeManagement.Interfaces; 6 | using GradeManagement.Models; 7 | using GradeManagement.Models.Elements; 8 | using GradeManagement.ViewModels.BaseClasses; 9 | using GradeManagement.ViewModels.Lists; 10 | 11 | namespace GradeManagement.ViewModels.AddPages 12 | { 13 | public class AddYearViewModel : AddViewModelBase, IAddViewModel 14 | { 15 | public AddYearViewModel() 16 | { 17 | BorderBrushes = new SolidColorBrush[] { new(IncompleteColor) }; 18 | EditPageText(AddPageAction.Create, "School Year"); 19 | } 20 | 21 | protected override bool DataComplete => !string.IsNullOrEmpty(ElementName) && !string.IsNullOrWhiteSpace(ElementName) 22 | && DataChanged(); 23 | 24 | private SchoolYear? EditedYear { get; set; } 25 | 26 | public void EditElement(SchoolYear year) 27 | { 28 | EditedYear = year; 29 | EditPageText(AddPageAction.Edit, "School Year", year.Name); 30 | ElementName = year.Name; 31 | 32 | var colorRepresentation = ElementColorsCollection.FirstOrDefault(x => x.ElementColor.Equals(year.ElementColor)); 33 | if(colorRepresentation is not null) 34 | ChangeColor(colorRepresentation); 35 | } 36 | 37 | protected internal override void EraseData() 38 | { 39 | base.EraseData(); 40 | EditedYear = null; 41 | } 42 | 43 | private void CreateElement() 44 | { 45 | if(ElementName is null) 46 | return; 47 | 48 | var colorHex = SelectedColor.ElementColor.ToHexString(); 49 | if (EditedYear is null) 50 | { 51 | var viewModel = YearListViewModel.Instance; 52 | var year = new SchoolYear(ElementName, colorHex); 53 | 54 | DataManager.SchoolYears.Add(year); 55 | viewModel?.Items?.Add(year); 56 | } 57 | else 58 | EditedYear.Edit(ElementName, colorHex); 59 | 60 | UpdateVisualOnChange(); 61 | EditedYear = null; 62 | } 63 | 64 | private bool DataChanged() 65 | { 66 | if (EditedYear is null) 67 | return true; 68 | 69 | return ElementName is not null && (!ElementName.Trim().Equals(EditedYear.Name.Trim()) 70 | || !SelectedColor.ElementColor.Equals(EditedYear.ElementColor)); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/ViewModels/BaseClasses/AddViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using Avalonia.Media; 4 | using GradeManagement.Enums; 5 | using GradeManagement.Models; 6 | using GradeManagement.ResourcesNamespace; 7 | using GradeManagement.ViewModels.AddPages; 8 | using ReactiveUI; 9 | 10 | namespace GradeManagement.ViewModels.BaseClasses 11 | { 12 | public abstract class AddViewModelBase : ViewModelBase 13 | { 14 | private string? _elementName; 15 | private string? _elementWeightingStr; 16 | private string? _buttonText; 17 | private string? _title; 18 | private bool _elementCounts = true; 19 | private ColorRepresentation _selectedColor; 20 | 21 | private readonly Color[] _elementColors = 22 | { 23 | DefaultElementColor, Color.Parse("#FFAE03"), Color.Parse("#EB8934"), Color.Parse("#D64045"), 24 | Color.Parse("#FF85FB"), Color.Parse("#A326C9"), Color.Parse("#5F8BB0"), Color.Parse("#6FB3BF"), 25 | Color.Parse("#A5B1CC"), Color.Parse("#009B72"), Color.Parse("#74CC31"), Color.Parse("#A8744F") 26 | }; 27 | 28 | protected AddViewModelBase() 29 | { 30 | foreach (var color in _elementColors) 31 | { 32 | var colorRepresentation = new ColorRepresentation(color); 33 | ElementColorsCollection.Add(colorRepresentation); 34 | } 35 | 36 | _selectedColor = SelectedColor = ElementColorsCollection[0]; 37 | SelectedColor.Selected = true; 38 | } 39 | 40 | // Colors for border (incomplete/complete selection) 41 | protected static Color IncompleteColor => Resources.InvalidColor; 42 | 43 | protected static Color NormalColor => Resources.AppGreen; 44 | 45 | protected static Color InactiveColor => Resources.CurrentTheme.StandardGrey; 46 | 47 | protected virtual bool DataComplete { get; } 48 | protected SolidColorBrush[]? BorderBrushes { get; init; } 49 | 50 | // Indexes of the elements in the UI 51 | protected int WeightingIndex { get; init; } 52 | protected int NameIndex { get; init; } 53 | 54 | protected string? Title 55 | { 56 | get => _title; 57 | set => this.RaiseAndSetIfChanged(ref _title, value); 58 | } 59 | 60 | protected string? ButtonText 61 | { 62 | get => _buttonText; 63 | set => this.RaiseAndSetIfChanged(ref _buttonText, value); 64 | } 65 | 66 | protected string? ElementName 67 | { 68 | get => _elementName; 69 | set 70 | { 71 | this.RaiseAndSetIfChanged(ref _elementName, value); 72 | BorderBrushes![NameIndex].Color = NormalColor; 73 | if (string.IsNullOrEmpty(value) || string.IsNullOrWhiteSpace(value)) 74 | BorderBrushes[NameIndex].Color = IncompleteColor; 75 | 76 | this.RaisePropertyChanged(nameof(BorderBrushes)); 77 | this.RaisePropertyChanged(nameof(DataComplete)); 78 | } 79 | } 80 | 81 | protected float ElementWeighting { get; private set; } = float.NaN; 82 | protected string? ElementWeightingString 83 | { 84 | get => _elementWeightingStr; 85 | set 86 | { 87 | this.RaiseAndSetIfChanged(ref _elementWeightingStr, value); 88 | ElementWeighting = float.NaN; 89 | BorderBrushes![WeightingIndex].Color = NormalColor; 90 | 91 | if (float.TryParse(value, out float weighting)) 92 | ElementWeighting = weighting; 93 | else 94 | BorderBrushes![WeightingIndex].Color = IncompleteColor; 95 | 96 | this.RaisePropertyChanged(nameof(BorderBrushes)); 97 | this.RaisePropertyChanged(nameof(DataComplete)); 98 | } 99 | } 100 | 101 | protected bool ElementCounts 102 | { 103 | get => _elementCounts; 104 | set 105 | { 106 | this.RaiseAndSetIfChanged(ref _elementCounts, value); 107 | this.RaisePropertyChanged(nameof(DataComplete)); 108 | } 109 | } 110 | 111 | protected ObservableCollection ElementColorsCollection { get; } = new(); 112 | 113 | protected ColorRepresentation SelectedColor 114 | { 115 | get => _selectedColor; 116 | private set 117 | { 118 | _selectedColor = value; 119 | this.RaisePropertyChanged(nameof(DataComplete)); 120 | } 121 | } 122 | 123 | private static Color DefaultElementColor => Resources.CurrentTheme.ElementBackground; 124 | 125 | protected internal override void EraseData() 126 | { 127 | ElementName = string.Empty; 128 | ElementWeightingString = string.Empty; 129 | ElementCounts = true; 130 | SelectedColor = ElementColorsCollection[0]; 131 | } 132 | 133 | protected void ChangeColor(ColorRepresentation colorRepresentation) 134 | { 135 | SelectedColor.Selected = false; 136 | colorRepresentation.Selected = true; 137 | SelectedColor = colorRepresentation; 138 | } 139 | 140 | internal void EditPageText(AddPageAction action, Type pageType, string suffix = "") 141 | { 142 | string type = pageType switch 143 | { 144 | { } yearType when yearType == typeof(AddYearViewModel) => "School Year", 145 | { } subjectType when subjectType == typeof(AddSubjectViewModel) => "Subject", 146 | { } gradeType when gradeType == typeof(AddGradeViewModel) => "Grade", 147 | _ => throw new ArgumentException($"Value of parameter \"{nameof(pageType)}\" should be an `AddViewModelBase` inheritor.", 148 | nameof(pageType)) 149 | }; 150 | EditPageText(action, type, suffix); 151 | } 152 | 153 | internal void EditPageText(AddPageAction action, string type, string suffix = "") 154 | { 155 | string prefix = action.ToString(); 156 | Title = $"{prefix} {(string.IsNullOrEmpty(suffix) ? type : ($"\"{suffix}\""))}:"; 157 | ButtonText = $"{prefix} {type}"; 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /src/ViewModels/BaseClasses/DialogBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Avalonia.Media; 4 | using GradeManagement.ExtensionCollection; 5 | using GradeManagement.Models.Settings; 6 | 7 | namespace GradeManagement.ViewModels.BaseClasses 8 | { 9 | public abstract class DialogBase : ViewModelBase 10 | { 11 | private static readonly Color _darkBorder = Color.Parse("#696969"); 12 | private static readonly Color _lightBorder = Color.Parse("#9C9C9C"); 13 | 14 | protected DialogBase(string title, 15 | IEnumerable buttonColors, 16 | IEnumerable buttonTextColors, 17 | IEnumerable buttonTexts) 18 | { 19 | this.Title = title; 20 | this.ButtonColors = buttonColors.ToArray(); 21 | this.ButtonTextColors = buttonTextColors.ToArray(); 22 | this.ButtonTexts = buttonTexts.ToArray(); 23 | } 24 | 25 | protected string Title { get; init; } 26 | 27 | protected SolidColorBrush[] ButtonColors { get; init; } 28 | protected SolidColorBrush[] ButtonColorsHover 29 | => ButtonColors.Select(x => new SolidColorBrush(x.Color.DarkenColor(0.1f))).ToArray(); 30 | protected SolidColorBrush[] ButtonTextColors { get; init; } 31 | 32 | protected string[] ButtonTexts { get; init; } 33 | 34 | protected bool IgnoreDialog { get; set; } 35 | 36 | protected static void CloseDialog() 37 | { 38 | if (MainWindowViewModel.Instance is not { } mainInstance) 39 | return; 40 | mainInstance.Content.CurrentDialog = null; 41 | } 42 | 43 | protected void CheckIgnoreDialog() 44 | { 45 | if (!IgnoreDialog || SettingsManager.Settings is not { } settings) 46 | return; 47 | settings.ShowRemoveConfirmation = false; 48 | SettingsManager.SaveSettings(); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/ViewModels/BaseClasses/ListViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia.Media; 3 | using GradeManagement.Enums; 4 | using GradeManagement.ExtensionCollection; 5 | using GradeManagement.Interfaces; 6 | using GradeManagement.Models.Elements; 7 | using GradeManagement.ViewModels.Dialogs; 8 | using ReactiveUI; 9 | 10 | namespace GradeManagement.ViewModels.BaseClasses 11 | { 12 | public abstract class ListViewModelBase : ViewModelBase 13 | { 14 | private bool _isViewGrid; 15 | private DialogBase? _currentDialog; 16 | 17 | protected internal Type? AddPageType { get; protected init; } 18 | protected internal Type? AddViewModelType { get; protected init; } 19 | protected internal Type? ElementType { get; protected init; } 20 | 21 | protected bool IsViewGrid 22 | { 23 | get => _isViewGrid; 24 | set => this.RaiseAndSetIfChanged(ref _isViewGrid, value); 25 | } 26 | 27 | internal DialogBase? CurrentDialog 28 | { 29 | get => _currentDialog; 30 | set => this.RaiseAndSetIfChanged(ref _currentDialog, value); 31 | } 32 | 33 | protected void DuplicateElement(IElement element) where T : class, IElement 34 | { 35 | var instance = MainWindowViewModel.Instance; 36 | if (instance is null) 37 | return; 38 | 39 | var duplicate = element.Duplicate(); 40 | UpdateVisualOnChange(); 41 | 42 | if (duplicate is null) 43 | return; 44 | 45 | if (typeof(T) == typeof(GradeGroup)) 46 | { 47 | (this as IListViewModel)?.Items?.Add((duplicate as Grade)!); 48 | return; 49 | } 50 | var viewModel = this as IListViewModel; 51 | viewModel?.Items?.Add(duplicate); 52 | } 53 | 54 | protected void RemoveElement(IElement element, ElementType elementType, Action confirmAction) 55 | { 56 | if (SettingsRef is not null && !SettingsRef.ShowRemoveConfirmation) 57 | { 58 | confirmAction.Invoke(); 59 | return; 60 | } 61 | 62 | string elementTypeName = elementType.ToString().SplitCamelCase(); 63 | string dialogTitle = $"Do you really want to remove the {elementTypeName} \"{element.Name}\"?"; 64 | CurrentDialog = new ConfirmationDialogViewModel(dialogTitle, 65 | new [] { Color.Parse("#D64045"), Color.Parse("#808080") }, 66 | new[] { Colors.White, Colors.White }, 67 | new[] { "Remove", "Cancel" }, 68 | confirmAction); 69 | } 70 | 71 | protected internal virtual void ChangeTopbar() 72 | { 73 | if (TopbarTexts is null) 74 | throw new ArgumentNullException(); 75 | } 76 | 77 | protected internal void ChangeButtonView(bool isGrid) 78 | { 79 | IsViewGrid = isGrid; 80 | AdjustTextColors(isGrid); 81 | } 82 | 83 | private void AdjustTextColors(bool isGrid) 84 | { 85 | if (this is IListViewModel {Items: { }} subjectViewModel) 86 | { 87 | foreach (var item in subjectViewModel.Items) 88 | item.AdjustTextColors(isGrid); 89 | return; 90 | } 91 | 92 | if (this is IListViewModel {Items: { }} yearViewModel) 93 | { 94 | foreach (var item in yearViewModel.Items) 95 | item.AdjustTextColors(isGrid); 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/ViewModels/BaseClasses/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using GradeManagement.Models; 3 | using GradeManagement.Models.Settings; 4 | using GradeManagement.Views; 5 | using ReactiveUI; 6 | 7 | namespace GradeManagement.ViewModels.BaseClasses 8 | { 9 | public abstract class ViewModelBase : ReactiveObject 10 | { 11 | protected MainWindow? MainWindowInstance { get; private set; } 12 | protected Preferences? SettingsRef { get; } 13 | 14 | protected Controls? TopbarTexts { get; private set; } 15 | protected internal Window? CurrentAddWindow { get; internal set; } 16 | 17 | protected ViewModelBase() 18 | => this.SettingsRef = SettingsManager.Settings; 19 | 20 | protected void InitializeTopbarElements() 21 | { 22 | if (MainWindow.Instance is null) 23 | return; 24 | 25 | this.MainWindowInstance = MainWindow.Instance; 26 | this.TopbarTexts = this.MainWindowInstance.Get("Topbar-Grid").Children; 27 | } 28 | 29 | protected void UpdateVisualOnChange() 30 | { 31 | CloseAddWindow(); 32 | DataManager.SaveData(); 33 | } 34 | 35 | protected internal virtual void EraseData() { } 36 | 37 | private void CloseAddWindow() 38 | { 39 | CurrentAddWindow?.Close(); 40 | CurrentAddWindow = null; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/ViewModels/Dialogs/ConfirmationDialogViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Avalonia.Media; 5 | using GradeManagement.ViewModels.BaseClasses; 6 | 7 | namespace GradeManagement.ViewModels.Dialogs 8 | { 9 | public class ConfirmationDialogViewModel : DialogBase 10 | { 11 | private enum ActionType 12 | { 13 | Confirm, 14 | Cancel 15 | } 16 | 17 | private readonly Action? _confirmAction; 18 | private readonly Action? _cancelAction; 19 | 20 | public ConfirmationDialogViewModel(string title, 21 | IEnumerable buttonColors, 22 | IEnumerable buttonTextColors, 23 | IEnumerable buttonTexts, 24 | Action? confirmAction, 25 | Action? cancelAction = null) : base(title, buttonColors, buttonTextColors, buttonTexts) 26 | { 27 | _confirmAction = confirmAction; 28 | _cancelAction = cancelAction; 29 | } 30 | 31 | public ConfirmationDialogViewModel(string title, 32 | IEnumerable buttonColors, 33 | IEnumerable buttonTextColors, 34 | IEnumerable buttonTexts, 35 | Action? confirmAction, 36 | Action? cancelAction = null) 37 | : this(title, 38 | buttonColors.Select(x => new SolidColorBrush(x)), 39 | buttonTextColors.Select(x => new SolidColorBrush(x)), 40 | buttonTexts, 41 | confirmAction, 42 | cancelAction) 43 | { } 44 | 45 | private void Command(ActionType actionType) 46 | { 47 | CloseDialog(); 48 | switch (actionType) 49 | { 50 | case ActionType.Confirm: 51 | CheckIgnoreDialog(); 52 | _confirmAction?.Invoke(); 53 | break; 54 | case ActionType.Cancel: 55 | _cancelAction?.Invoke(); 56 | break; 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/ViewModels/Lists/GradeListViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using Avalonia.Controls; 6 | using GradeManagement.Enums; 7 | using GradeManagement.Interfaces; 8 | using GradeManagement.Models.Elements; 9 | using GradeManagement.Models.Settings; 10 | using GradeManagement.UtilityCollection; 11 | using GradeManagement.ViewModels.AddPages; 12 | using GradeManagement.ViewModels.BaseClasses; 13 | using GradeManagement.ViewModels.TargetGrade; 14 | using GradeManagement.Views.AddPages; 15 | using GradeManagement.Views.TargetGrade; 16 | using ReactiveUI; 17 | 18 | namespace GradeManagement.ViewModels.Lists 19 | { 20 | public class GradeListViewModel : ListViewModelBase, IListViewModel 21 | { 22 | private ObservableCollection? _items; 23 | private TargetGradeWindowModel? _targetGradeWindowModel; 24 | 25 | public GradeListViewModel(IEnumerable items, IGradesContainer gradesContainer) 26 | { 27 | Instance = this; 28 | AddPageType = typeof(AddGradeWindow); 29 | AddViewModelType = typeof(AddGradeViewModel); 30 | ElementType = typeof(Grade); 31 | GradesContainer = gradesContainer; 32 | 33 | bool isGrid = SettingsManager.Settings?.GradeButtonStyle == SelectedButtonStyle.Grid; 34 | ChangeButtonView(isGrid); 35 | 36 | var mainInstance = MainWindowViewModel.Instance; 37 | Items = new ObservableCollection(items); 38 | Items.CollectionChanged += (sender, args) => 39 | { 40 | this.RaisePropertyChanged(nameof(EmptyCollection)); 41 | mainInstance?.UpdateAverage(); 42 | }; 43 | InitializeTopbarElements(); 44 | } 45 | 46 | public ObservableCollection? Items 47 | { 48 | get => _items; 49 | set 50 | { 51 | this.RaiseAndSetIfChanged(ref _items, value); 52 | this.RaisePropertyChanged(nameof(EmptyCollection)); 53 | } 54 | } 55 | 56 | public bool EmptyCollection => Items?.Count == 0; 57 | 58 | internal IGradesContainer? GradesContainer { get; set; } 59 | 60 | internal static GradeListViewModel? Instance { get; private set; } 61 | 62 | public void Duplicate(IElement element) 63 | { 64 | if(element is GradeGroup) 65 | DuplicateElement(element); 66 | else 67 | DuplicateElement(element); 68 | } 69 | 70 | protected internal override void ChangeTopbar() 71 | { 72 | base.ChangeTopbar(); 73 | 74 | int[] additionalGradesIndices = {3, 4}; 75 | const int gradeBorder = 3; 76 | bool parentIsGrade = GradesContainer?.ParentContainer is GradeGroup; 77 | bool containerIsGrade = GradesContainer is GradeGroup; 78 | for (int i = 0; i < TopbarTexts!.Count; i++) 79 | { 80 | IControl control = TopbarTexts[i]; 81 | control.IsVisible = true; 82 | if (i >= gradeBorder) 83 | control.IsVisible = containerIsGrade; 84 | if (additionalGradesIndices.Contains(i)) 85 | control.IsVisible = parentIsGrade; 86 | } 87 | } 88 | 89 | private void RemoveElement(Grade grade) 90 | { 91 | Action action = () => 92 | { 93 | GradesContainer?.Grades.Remove(grade); 94 | Items?.Remove(grade); 95 | UpdateVisualOnChange(); 96 | }; 97 | base.RemoveElement(grade, Enums.ElementType.Grade, action); 98 | } 99 | 100 | private void OpenTargetGradeCalc() 101 | { 102 | if (Items is null) 103 | return; 104 | var window = new TargetGradeWindow(); 105 | _targetGradeWindowModel ??= new TargetGradeWindowModel(Items); 106 | _targetGradeWindowModel.ClearData(); 107 | _targetGradeWindowModel.ConfigureViewModels(Items); 108 | window.DataContext = _targetGradeWindowModel; 109 | 110 | Utilities.ShowDialog(window, MainWindowInstance); 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /src/ViewModels/Lists/SubjectListViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using GradeManagement.Enums; 5 | using GradeManagement.Interfaces; 6 | using GradeManagement.Models.Elements; 7 | using GradeManagement.Models.Settings; 8 | using GradeManagement.ViewModels.AddPages; 9 | using GradeManagement.ViewModels.BaseClasses; 10 | using GradeManagement.Views.AddPages; 11 | using ReactiveUI; 12 | 13 | namespace GradeManagement.ViewModels.Lists 14 | { 15 | public class SubjectListViewModel : ListViewModelBase, IListViewModel 16 | { 17 | private readonly bool[] _elementsVisibilities = { true, false, false, false, false, false, false }; 18 | private ObservableCollection? _items; 19 | 20 | public SubjectListViewModel(IEnumerable items) 21 | { 22 | Instance = this; 23 | AddPageType = typeof(AddSubjectWindow); 24 | AddViewModelType = typeof(AddSubjectViewModel); 25 | ElementType = typeof(Subject); 26 | 27 | bool isGrid = SettingsManager.Settings?.SubjectButtonStyle == SelectedButtonStyle.Grid; 28 | ChangeButtonView(isGrid); 29 | 30 | var mainInstance = MainWindowViewModel.Instance; 31 | Items = new ObservableCollection(items); 32 | Items.CollectionChanged += (sender, args) => 33 | { 34 | this.RaisePropertyChanged(nameof(EmptyCollection)); 35 | mainInstance?.UpdateAverage(); 36 | }; 37 | InitializeTopbarElements(); 38 | } 39 | 40 | public ObservableCollection? Items 41 | { 42 | get => _items; 43 | set 44 | { 45 | this.RaiseAndSetIfChanged(ref _items, value); 46 | this.RaisePropertyChanged(nameof(EmptyCollection)); 47 | } 48 | } 49 | 50 | public bool EmptyCollection => Items?.Count == 0; 51 | 52 | internal static SubjectListViewModel? Instance { get; private set; } 53 | 54 | public void Duplicate(IElement element) 55 | => DuplicateElement(element); 56 | 57 | protected internal override void ChangeTopbar() 58 | { 59 | base.ChangeTopbar(); 60 | for (int i = 0; i < TopbarTexts!.Count; i++) 61 | TopbarTexts[i].IsVisible = _elementsVisibilities[i]; 62 | } 63 | 64 | private void RemoveElement(Subject subject) 65 | { 66 | var currentYear = MainWindowViewModel.CurrentYear; 67 | Action action = () => 68 | { 69 | currentYear?.Subjects.Remove(subject); 70 | Items?.Remove(subject); 71 | UpdateVisualOnChange(); 72 | }; 73 | base.RemoveElement(subject, Enums.ElementType.Subject, action); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/ViewModels/Lists/YearListViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using GradeManagement.Enums; 5 | using GradeManagement.Interfaces; 6 | using GradeManagement.Models; 7 | using GradeManagement.Models.Elements; 8 | using GradeManagement.Models.Settings; 9 | using GradeManagement.ViewModels.AddPages; 10 | using GradeManagement.ViewModels.BaseClasses; 11 | using GradeManagement.Views.AddPages; 12 | using ReactiveUI; 13 | 14 | namespace GradeManagement.ViewModels.Lists 15 | { 16 | public class YearListViewModel : ListViewModelBase, IListViewModel 17 | { 18 | private ObservableCollection? _items; 19 | 20 | public YearListViewModel(IEnumerable years) 21 | { 22 | Instance = this; 23 | AddPageType = typeof(AddYearWindow); 24 | AddViewModelType = typeof(AddYearViewModel); 25 | ElementType = typeof(SchoolYear); 26 | 27 | bool isGrid = SettingsManager.Settings?.YearButtonStyle == SelectedButtonStyle.Grid; 28 | ChangeButtonView(isGrid); 29 | 30 | var mainInstance = MainWindowViewModel.Instance; 31 | Items = new ObservableCollection(years); 32 | Items.CollectionChanged += (sender, args) => 33 | { 34 | this.RaisePropertyChanged(nameof(EmptyCollection)); 35 | mainInstance?.UpdateAverage(); 36 | }; 37 | InitializeTopbarElements(); 38 | } 39 | 40 | public ObservableCollection? Items 41 | { 42 | get => _items; 43 | set 44 | { 45 | this.RaiseAndSetIfChanged(ref _items, value); 46 | this.RaisePropertyChanged(nameof(EmptyCollection)); 47 | } 48 | } 49 | 50 | public bool EmptyCollection => Items?.Count == 0; 51 | 52 | internal static YearListViewModel? Instance { get; private set; } 53 | 54 | public void Duplicate(IElement element) 55 | => DuplicateElement(element); 56 | 57 | protected internal override void ChangeTopbar() 58 | { 59 | base.ChangeTopbar(); 60 | foreach (var control in TopbarTexts!) 61 | control.IsVisible = false; 62 | } 63 | 64 | private void RemoveElement(SchoolYear year) 65 | { 66 | Action action = () => 67 | { 68 | DataManager.SchoolYears.Remove(year); 69 | Items?.Remove(year); 70 | UpdateVisualOnChange(); 71 | }; 72 | base.RemoveElement(year, Enums.ElementType.SchoolYear, action); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/ViewModels/TargetGrade/AverageGradeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using GradeManagement.Interfaces; 5 | using GradeManagement.Models.Elements; 6 | using GradeManagement.UtilityCollection; 7 | using GradeManagement.ViewModels.BaseClasses; 8 | using ReactiveUI; 9 | 10 | namespace GradeManagement.ViewModels.TargetGrade 11 | { 12 | public class AverageGradeViewModel : ViewModelBase, ITargetGrade 13 | { 14 | private string _gradeString = string.Empty; 15 | private float _grade = float.NaN; 16 | private string _weightingString = string.Empty; 17 | private float _weighting = float.NaN; 18 | 19 | public AverageGradeViewModel(IEnumerable grades) 20 | => this.Grades = grades; 21 | 22 | public IEnumerable? Grades { get; set; } 23 | 24 | private string GradeString 25 | { 26 | get => _gradeString; 27 | set 28 | { 29 | _grade = float.TryParse(value, out float grade) ? grade : float.NaN; 30 | this.RaiseAndSetIfChanged(ref _gradeString, value); 31 | this.RaisePropertyChanged(nameof(CalculatedAverage)); 32 | } 33 | } 34 | 35 | private string WeightingString 36 | { 37 | get => _weightingString; 38 | set 39 | { 40 | _weighting = float.TryParse(value, out float weighting) ? weighting : float.NaN; 41 | this.RaiseAndSetIfChanged(ref _weightingString, value); 42 | this.RaisePropertyChanged(nameof(CalculatedAverage)); 43 | } 44 | } 45 | 46 | private string CalculatedAverage 47 | { 48 | get 49 | { 50 | if (float.IsNaN(_grade) || float.IsNaN(_weighting) || this.Grades is null) 51 | return "-"; 52 | 53 | var newGrades = new List(Grades) 54 | { 55 | new Grade("[TempGrade]", _grade, null, null, _weighting, DateTime.Today, true) 56 | }; 57 | float result = Utilities.GetAverage(newGrades, true); 58 | return float.IsNaN(result) ? "-" : result.ToString(CultureInfo.InvariantCulture); 59 | } 60 | } 61 | 62 | protected internal override void EraseData() 63 | { 64 | Grades = null; 65 | GradeString = string.Empty; 66 | WeightingString = string.Empty; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/ViewModels/TargetGrade/TargetGradeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using GradeManagement.Interfaces; 5 | using GradeManagement.Models.Elements; 6 | using GradeManagement.ViewModels.BaseClasses; 7 | using ReactiveUI; 8 | 9 | namespace GradeManagement.ViewModels.TargetGrade 10 | { 11 | public class TargetGradeViewModel : ViewModelBase, ITargetGrade 12 | { 13 | private string _targetAverageString = string.Empty; 14 | private float _targetAverage = float.NaN; 15 | private string _weightingString = string.Empty; 16 | private float _weighting = float.NaN; 17 | 18 | public IEnumerable? Grades { get; set; } 19 | 20 | public TargetGradeViewModel(IEnumerable grades) 21 | => this.Grades = grades; 22 | 23 | private string TargetAverageString 24 | { 25 | get => _targetAverageString; 26 | set 27 | { 28 | _targetAverage = float.TryParse(value, out float average) ? average : float.NaN; 29 | this.RaiseAndSetIfChanged(ref _targetAverageString, value); 30 | this.RaisePropertyChanged(nameof(NeededGrade)); 31 | } 32 | } 33 | 34 | private string WeightingString 35 | { 36 | get => _weightingString; 37 | set 38 | { 39 | _weighting = float.TryParse(value, out float weighting) ? weighting : float.NaN; 40 | this.RaiseAndSetIfChanged(ref _weightingString, value); 41 | this.RaisePropertyChanged(nameof(NeededGrade)); 42 | } 43 | } 44 | 45 | private string NeededGrade 46 | { 47 | get 48 | { 49 | if (float.IsNaN(_targetAverage) || float.IsNaN(_weighting) || this.Grades is null) 50 | return "-"; 51 | 52 | var previousGrades = Grades.Sum(x => x.GradeValue * x.Weighting); 53 | var weightingSum = Grades.Sum(x => x.Weighting); 54 | var result = (_targetAverage * (weightingSum + _weighting) - previousGrades) / _weighting; 55 | return float.IsNaN(result) ? "-" : result.ToString("F2"); 56 | } 57 | } 58 | 59 | protected internal override void EraseData() 60 | { 61 | Grades = null; 62 | TargetAverageString = string.Empty; 63 | WeightingString = string.Empty; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/ViewModels/TargetGrade/TargetGradeWindowModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using Avalonia.Media; 6 | using GradeManagement.ExtensionCollection; 7 | using GradeManagement.Interfaces; 8 | using GradeManagement.Models.Elements; 9 | using GradeManagement.ViewModels.BaseClasses; 10 | using ReactiveUI; 11 | 12 | namespace GradeManagement.ViewModels.TargetGrade 13 | { 14 | public class TargetGradeWindowModel : ViewModelBase 15 | { 16 | private const string TargetGradeTitle = "Calculate Target Grade"; 17 | private const string AverageTitle = "Calculate Average"; 18 | 19 | private static readonly Color _greenColor = new(255,0,155,114); 20 | private static readonly Color _whiteColor = new(255, 255, 255, 255); 21 | private static readonly Color _blackColor = new(255, 0, 0, 0); 22 | private static readonly Color _darkerGreenColor = _greenColor.DarkenColor(0.075f); 23 | private static readonly Color _darkerDefaultColor = Color.Parse("#D8DDE6").DarkenColor(0.075f); 24 | 25 | private readonly List _viewModels = new(); 26 | private ViewModelBase? _content; 27 | private string _windowTitle = TargetGradeTitle; 28 | private int _currentButton; 29 | 30 | public TargetGradeWindowModel(IEnumerable grades) 31 | { 32 | var enumerable = grades as Grade[] ?? grades.ToArray(); 33 | var viewModel = new TargetGradeViewModel(enumerable); 34 | _content = Content = viewModel; 35 | _viewModels.SafeAdd(_content); 36 | 37 | ClearData(); 38 | 39 | Grades = enumerable; 40 | viewModel.Grades = this.Grades; 41 | } 42 | 43 | internal IEnumerable? Grades { get; set; } 44 | 45 | private string WindowTitle 46 | { 47 | get => _windowTitle; 48 | set => this.RaiseAndSetIfChanged(ref _windowTitle, value); 49 | } 50 | 51 | private ObservableCollection ButtonColors { get; } 52 | = new() { new SolidColorBrush(_greenColor), new SolidColorBrush(_greenColor,0) }; 53 | 54 | private ObservableCollection ButtonColorsHover { get; } 55 | = new() 56 | { 57 | new SolidColorBrush(_greenColor.DarkenColor(0.075f)), 58 | new SolidColorBrush(_darkerDefaultColor) 59 | }; 60 | 61 | private ObservableCollection ButtonTextColors { get; } 62 | = new() { new SolidColorBrush(_whiteColor), new SolidColorBrush(_blackColor) }; 63 | 64 | // ReSharper disable once CollectionNeverQueried.Local 65 | private ObservableCollection FontWeights { get; } 66 | = new() { FontWeight.Bold, FontWeight.Normal }; 67 | 68 | private ViewModelBase? Content 69 | { 70 | get => _content; 71 | set => this.RaiseAndSetIfChanged(ref _content, value); 72 | } 73 | 74 | internal void ConfigureViewModels(IEnumerable grades) 75 | { 76 | this.Grades = grades; 77 | if(GetViewModel(typeof(TargetGradeViewModel)) is TargetGradeViewModel targetGrade) 78 | targetGrade.Grades = grades; 79 | if(GetViewModel(typeof(AverageGradeViewModel)) is AverageGradeViewModel averageGrade) 80 | averageGrade.Grades = grades; 81 | } 82 | 83 | internal void ClearData() 84 | { 85 | if(GetViewModel(typeof(TargetGradeViewModel)) is TargetGradeViewModel targetGrade) 86 | targetGrade.EraseData(); 87 | if(GetViewModel(typeof(AverageGradeViewModel)) is AverageGradeViewModel averageGrade) 88 | averageGrade.EraseData(); 89 | } 90 | 91 | private void SwitchCalculator(int selectedButton) 92 | { 93 | if (_currentButton == selectedButton) 94 | return; 95 | _currentButton = selectedButton; 96 | 97 | WindowTitle = _windowTitle.Equals(AverageTitle) ? TargetGradeTitle : AverageTitle; 98 | 99 | ChangeButtons(selectedButton); 100 | ToggleControl(selectedButton); 101 | } 102 | 103 | private void ChangeButtons(int selectedButton) 104 | { 105 | var otherButton = Math.Abs(selectedButton - 1); 106 | 107 | ButtonColors[selectedButton].Opacity = 1; 108 | ButtonColors[otherButton].Opacity = 0; 109 | 110 | ButtonColorsHover[selectedButton].Color = _darkerGreenColor; 111 | ButtonColorsHover[otherButton].Color = _darkerDefaultColor; 112 | 113 | ButtonTextColors[selectedButton].Color = _whiteColor; 114 | ButtonTextColors[otherButton].Color = _blackColor; 115 | 116 | FontWeights[selectedButton] = FontWeight.Bold; 117 | FontWeights[otherButton] = FontWeight.Normal; 118 | } 119 | 120 | private void ToggleControl(int selected) 121 | { 122 | var type = selected == 0 ? typeof(TargetGradeViewModel) : typeof(AverageGradeViewModel); 123 | ViewModelBase viewModel; 124 | if (_viewModels.Any(x => x.GetType() == type)) 125 | viewModel = _viewModels.Find(x => x.GetType() == type)!; 126 | else 127 | { 128 | viewModel = (ViewModelBase)Activator.CreateInstance(type, this.Grades)!; 129 | _viewModels.Add(viewModel); 130 | } 131 | 132 | Content = viewModel; 133 | _content?.EraseData(); 134 | 135 | if (viewModel is ITargetGrade viewModelInterface) 136 | viewModelInterface.Grades = this.Grades; 137 | } 138 | 139 | private ViewModelBase? GetViewModel(Type type) 140 | => _viewModels.FirstOrDefault(x => x.GetType() == type); 141 | } 142 | } -------------------------------------------------------------------------------- /src/Views/AddPages/AddGradeWindow.axaml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 33 | 34 | 35 | 36 | 39 | 42 | 45 | 46 | 47 | 55 | 56 | 59 | 62 | 64 | 66 | 69 | 71 | 74 | 75 | 77 | 79 | 55 | 56 | 57 | 58 | 59 | 61 | 52 | 53 | 54 | 55 | 56 | 31 | 44 | 46 | 47 | 51 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/Views/Dialogs/ConfirmationDialogView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Markup.Xaml; 3 | 4 | namespace GradeManagement.Views.Dialogs 5 | { 6 | public class ConfirmationDialogView : UserControl 7 | { 8 | public ConfirmationDialogView() 9 | { 10 | InitializeComponent(); 11 | } 12 | 13 | private void InitializeComponent() 14 | { 15 | AvaloniaXamlLoader.Load(this); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Views/Lists/ElementButtonControls/ButtonStyleBase.cs: -------------------------------------------------------------------------------- 1 | using GradeManagement.Interfaces; 2 | using GradeManagement.Models.Elements; 3 | 4 | namespace GradeManagement.Views.Lists.ElementButtonControls 5 | { 6 | public abstract class ButtonStyleBase 7 | { 8 | protected ButtonStyleBase(IElement element) 9 | { 10 | switch (element) 11 | { 12 | case SchoolYear year: 13 | YearRef = year; 14 | break; 15 | case Subject subject: 16 | SubjectRef = subject; 17 | break; 18 | case Grade grade: 19 | GradeRef = grade; 20 | break; 21 | } 22 | } 23 | 24 | protected SchoolYear? YearRef { get; } 25 | protected Subject? SubjectRef { get; } 26 | protected Grade? GradeRef { get; } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Views/Lists/ElementButtonControls/GridButton.cs: -------------------------------------------------------------------------------- 1 | using GradeManagement.Interfaces; 2 | 3 | namespace GradeManagement.Views.Lists.ElementButtonControls 4 | { 5 | public class GridButton : ButtonStyleBase 6 | { 7 | public GridButton(IElement element) : base(element) { } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Views/Lists/ElementButtonControls/ListButton.cs: -------------------------------------------------------------------------------- 1 | using GradeManagement.Interfaces; 2 | 3 | namespace GradeManagement.Views.Lists.ElementButtonControls 4 | { 5 | public class ListButton : ButtonStyleBase 6 | { 7 | public ListButton(IElement element) : base(element) { } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Views/Lists/GradeListView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Markup.Xaml; 2 | using GradeManagement.Models; 3 | 4 | namespace GradeManagement.Views.Lists 5 | { 6 | public class GradeListView : DragControl 7 | { 8 | public GradeListView() 9 | { 10 | InitializeComponent(); 11 | } 12 | 13 | private void InitializeComponent() 14 | { 15 | AvaloniaXamlLoader.Load(this); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Views/Lists/SubjectListView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Markup.Xaml; 2 | using GradeManagement.Models; 3 | 4 | namespace GradeManagement.Views.Lists 5 | { 6 | public class SubjectListView : DragControl 7 | { 8 | public SubjectListView() 9 | { 10 | InitializeComponent(); 11 | } 12 | 13 | private void InitializeComponent() 14 | { 15 | AvaloniaXamlLoader.Load(this); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Views/Lists/YearListView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Markup.Xaml; 2 | using GradeManagement.Models; 3 | 4 | namespace GradeManagement.Views.Lists 5 | { 6 | public class YearListView : DragControl 7 | { 8 | public YearListView() 9 | { 10 | InitializeComponent(); 11 | } 12 | 13 | private void InitializeComponent() 14 | { 15 | AvaloniaXamlLoader.Load(this); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 29 | 30 | 32 | 33 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 56 | 57 | 58 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 89 | 51 | 66 | 67 | 68 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/Views/TargetGrade/TargetGradeWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace GradeManagement.Views.TargetGrade 6 | { 7 | public class TargetGradeWindow : Window 8 | { 9 | public TargetGradeWindow() 10 | { 11 | InitializeComponent(); 12 | #if DEBUG 13 | this.AttachDevTools(); 14 | #endif 15 | } 16 | 17 | private void InitializeComponent() 18 | { 19 | AvaloniaXamlLoader.Load(this); 20 | } 21 | } 22 | } --------------------------------------------------------------------------------