commit fd2ab3e14c391db31efb1f8a03890a90bc5b2e92 Author: chrisbell Date: Fri Dec 19 18:53:21 2025 -0600 Adding final project to dev branch diff --git a/README.md b/README.md new file mode 100644 index 0000000..f588fc9 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# C971 Mobile Application Development Using C Sharp + + + +## Getting started + +To make it easy for you to get started with GitLab, here's a list of recommended next steps. + +Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! + +## Add your files + +- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files +- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: + +``` +cd existing_repo +git remote add origin https://gitlab.com/wgu-gitlab-environment/task-templates/c971-mobile-application-development-using-c-sharp.git +git branch -M main +git push -uf origin main +``` + +## Integrate with your tools + +- [ ] [Set up project integrations](https://gitlab.com/wgu-gitlab-environment/task-templates/c971-mobile-application-development-using-c-sharp/-/settings/integrations) + +## Collaborate with your team + +- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) +- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) +- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) +- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) +- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) + +## Test and Deploy + +Use the built-in continuous integration in GitLab. + +- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) +- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) +- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) +- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) +- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) + +*** + +# Editing this README + +When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. + +## Suggestions for a good README +Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. + +## Name +Choose a self-explaining name for your project. + +## Description +Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. + +## Badges +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. + +## Visuals +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. + +## Installation +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. + +## Usage +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + +## Support +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + +## Roadmap +If you have ideas for releases in the future, it is a good idea to list them in the README. + +## Contributing +State if you are open to contributions and what your requirements are for accepting them. + +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. + +## Authors and acknowledgment +Show your appreciation to those who have contributed to the project. + +## License +For open source projects, say how it is licensed. + +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/WguApp/.gitignore b/WguApp/.gitignore new file mode 100644 index 0000000..1349347 --- /dev/null +++ b/WguApp/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +.idea/ \ No newline at end of file diff --git a/WguApp/.noai b/WguApp/.noai new file mode 100644 index 0000000..e69de29 diff --git a/WguApp/App.xaml b/WguApp/App.xaml new file mode 100644 index 0000000..c9c032c --- /dev/null +++ b/WguApp/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/WguApp/App.xaml.cs b/WguApp/App.xaml.cs new file mode 100644 index 0000000..20bd91d --- /dev/null +++ b/WguApp/App.xaml.cs @@ -0,0 +1,25 @@ +using WguApp.Services; +using WguApp.Views; + +namespace WguApp; + +public partial class App : Application +{ + + private HomePage _homePage = new(); + private NavigationPage _navPage; + + public App() + { + InitializeComponent(); + + if (Current != null) Current.UserAppTheme = AppTheme.Dark; + + _navPage = new NavigationPage(_homePage); + } + + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window(_navPage); + } +} \ No newline at end of file diff --git a/WguApp/Controls/CustomButton.xaml b/WguApp/Controls/CustomButton.xaml new file mode 100644 index 0000000..faee2f7 --- /dev/null +++ b/WguApp/Controls/CustomButton.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/WguApp/Controls/CustomButton.xaml.cs b/WguApp/Controls/CustomButton.xaml.cs new file mode 100644 index 0000000..51823c2 --- /dev/null +++ b/WguApp/Controls/CustomButton.xaml.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WguApp.Controls; + +public partial class CustomButton : ContentView +{ + + public event EventHandler? Clicked; + + public static readonly BindableProperty TextProperty = + BindableProperty.Create( + propertyName: nameof(Text), + returnType: typeof(string), + declaringType: typeof(CustomButton), + defaultValue: "Text", + defaultBindingMode: BindingMode.OneWay); + + public static readonly BindableProperty DateTextProperty = + BindableProperty.Create( + propertyName: nameof(DateText), + returnType: typeof(string), + declaringType: typeof(CustomButton), + defaultValue: "//", + defaultBindingMode: BindingMode.OneWay); + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public string DateText + { + get => (string)GetValue(DateTextProperty); + set => SetValue(DateTextProperty, value); + } + + public CustomButton() + { + InitializeComponent(); + } + + private void TapGestureRecognizer_OnTapped(object? sender, TappedEventArgs e) + { + Clicked?.Invoke(this, EventArgs.Empty); + } +} \ No newline at end of file diff --git a/WguApp/GlobalXmlns.cs b/WguApp/GlobalXmlns.cs new file mode 100644 index 0000000..6e8ad19 --- /dev/null +++ b/WguApp/GlobalXmlns.cs @@ -0,0 +1,2 @@ +[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "WguApp")] +[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "WguApp.Pages")] \ No newline at end of file diff --git a/WguApp/MauiProgram.cs b/WguApp/MauiProgram.cs new file mode 100644 index 0000000..1fb6792 --- /dev/null +++ b/WguApp/MauiProgram.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Logging; +using Plugin.LocalNotification; + +namespace WguApp; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + fonts.AddFont("Phosphor.ttf", "Phosphor"); + fonts.AddFont("Phosphor-Fill.ttf", "Phosphor-Fill"); + }) + .UseLocalNotification(); + +#if DEBUG + builder.Logging.AddDebug(); +#endif + + return builder.Build(); + } +} \ No newline at end of file diff --git a/WguApp/Models/Assessment.cs b/WguApp/Models/Assessment.cs new file mode 100644 index 0000000..d56e32c --- /dev/null +++ b/WguApp/Models/Assessment.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using SQLite; + +namespace WguApp.Models; + +[Table("Assessments")] +public class Assessment +{ + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + public int CourseId { get; set; } + public string Name { get; set; } = string.Empty; + public AssessmentType Type { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public bool StartNotifCheck { get; set; } = false; + public int StartNotifId { get; set; } + public bool EndNotifCheck { get; set; } = false; + public int EndNotifId { get; set; } +} + +public enum AssessmentType +{ + Performance, Objective +} \ No newline at end of file diff --git a/WguApp/Models/Course.cs b/WguApp/Models/Course.cs new file mode 100644 index 0000000..5ea92c6 --- /dev/null +++ b/WguApp/Models/Course.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations.Schema; +using SQLite; + +namespace WguApp.Models; + +[SQLite.Table("Courses")] +public class Course +{ + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + public int TermId { get; set; } + public string Name { get; set; } = string.Empty; + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public CourseStatus Status { get; set; } + public string InstructorName { get; set; } = string.Empty; + public string InstructorPhone { get; set; } = string.Empty; + public string InstructorEmail { get; set; } = string.Empty; + public bool StartNotifCheck { get; set; } = false; + public int StartNotifId { get; set; } + public bool EndNotifCheck { get; set; } = false; + public int EndNotifId { get; set; } + public string Notes { get; set; } = string.Empty; + + public Course() { } + public Course(int termId, string name, DateTime startDate, DateTime endDate, CourseStatus status, string instructorName, string instructorPhone, string instructorEmail) + { + TermId = termId; + Name = name; + StartDate = startDate; + EndDate = endDate; + Status = status; + InstructorName = instructorName; + InstructorPhone = instructorPhone; + InstructorEmail = instructorEmail; + } +} + +public enum CourseStatus +{ + InProgress, Completed, Dropped, Planned +} \ No newline at end of file diff --git a/WguApp/Models/Term.cs b/WguApp/Models/Term.cs new file mode 100644 index 0000000..53319b8 --- /dev/null +++ b/WguApp/Models/Term.cs @@ -0,0 +1,22 @@ +using SQLite; + +namespace WguApp.Models; + +[Table("Terms")] +public class Term +{ + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public DateTime StartDate {get; set;} + public DateTime EndDate {get; set;} + + + public Term() { } + public Term(string name, DateTime startDate, DateTime endDate) + { + Name = name; + StartDate = startDate; + EndDate = endDate; + } +} \ No newline at end of file diff --git a/WguApp/Platforms/Android/AndroidManifest.xml b/WguApp/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..e036750 --- /dev/null +++ b/WguApp/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WguApp/Platforms/Android/MainActivity.cs b/WguApp/Platforms/Android/MainActivity.cs new file mode 100644 index 0000000..b9d6e5d --- /dev/null +++ b/WguApp/Platforms/Android/MainActivity.cs @@ -0,0 +1,12 @@ +using Android.App; +using Android.Content.PM; +using Android.OS; + +namespace WguApp; + +[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, + ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | + ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +public class MainActivity : MauiAppCompatActivity +{ +} \ No newline at end of file diff --git a/WguApp/Platforms/Android/MainApplication.cs b/WguApp/Platforms/Android/MainApplication.cs new file mode 100644 index 0000000..e438b78 --- /dev/null +++ b/WguApp/Platforms/Android/MainApplication.cs @@ -0,0 +1,15 @@ +using Android.App; +using Android.Runtime; + +namespace WguApp; + +[Application] +public class MainApplication : MauiApplication +{ + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/WguApp/Platforms/Android/Resources/values/colors.xml b/WguApp/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000..c04d749 --- /dev/null +++ b/WguApp/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/WguApp/Platforms/MacCatalyst/AppDelegate.cs b/WguApp/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 0000000..fd60153 --- /dev/null +++ b/WguApp/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace WguApp; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/WguApp/Platforms/MacCatalyst/Entitlements.plist b/WguApp/Platforms/MacCatalyst/Entitlements.plist new file mode 100644 index 0000000..de4adc9 --- /dev/null +++ b/WguApp/Platforms/MacCatalyst/Entitlements.plist @@ -0,0 +1,14 @@ + + + + + + + com.apple.security.app-sandbox + + + com.apple.security.network.client + + + + diff --git a/WguApp/Platforms/MacCatalyst/Info.plist b/WguApp/Platforms/MacCatalyst/Info.plist new file mode 100644 index 0000000..7268977 --- /dev/null +++ b/WguApp/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + UIDeviceFamily + + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/WguApp/Platforms/MacCatalyst/Program.cs b/WguApp/Platforms/MacCatalyst/Program.cs new file mode 100644 index 0000000..e00b3ae --- /dev/null +++ b/WguApp/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace WguApp; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} \ No newline at end of file diff --git a/WguApp/Platforms/Tizen/Main.cs b/WguApp/Platforms/Tizen/Main.cs new file mode 100644 index 0000000..19f270e --- /dev/null +++ b/WguApp/Platforms/Tizen/Main.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Maui; +using Microsoft.Maui.Hosting; + +namespace WguApp; + +class Program : MauiApplication +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + + static void Main(string[] args) + { + var app = new Program(); + app.Run(args); + } +} \ No newline at end of file diff --git a/WguApp/Platforms/Tizen/tizen-manifest.xml b/WguApp/Platforms/Tizen/tizen-manifest.xml new file mode 100644 index 0000000..27885c0 --- /dev/null +++ b/WguApp/Platforms/Tizen/tizen-manifest.xml @@ -0,0 +1,15 @@ + + + + + + maui-appicon-placeholder + + + + + http://tizen.org/privilege/internet + + + + \ No newline at end of file diff --git a/WguApp/Platforms/Windows/App.xaml b/WguApp/Platforms/Windows/App.xaml new file mode 100644 index 0000000..7f390ec --- /dev/null +++ b/WguApp/Platforms/Windows/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/WguApp/Platforms/Windows/App.xaml.cs b/WguApp/Platforms/Windows/App.xaml.cs new file mode 100644 index 0000000..bfb4ee1 --- /dev/null +++ b/WguApp/Platforms/Windows/App.xaml.cs @@ -0,0 +1,23 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace WguApp.WinUI; + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : MauiWinUIApplication +{ + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/WguApp/Platforms/Windows/Package.appxmanifest b/WguApp/Platforms/Windows/Package.appxmanifest new file mode 100644 index 0000000..1972145 --- /dev/null +++ b/WguApp/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,46 @@ + + + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WguApp/Platforms/Windows/app.manifest b/WguApp/Platforms/Windows/app.manifest new file mode 100644 index 0000000..d561f62 --- /dev/null +++ b/WguApp/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/WguApp/Platforms/iOS/AppDelegate.cs b/WguApp/Platforms/iOS/AppDelegate.cs new file mode 100644 index 0000000..fd60153 --- /dev/null +++ b/WguApp/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace WguApp; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/WguApp/Platforms/iOS/Info.plist b/WguApp/Platforms/iOS/Info.plist new file mode 100644 index 0000000..0004a4f --- /dev/null +++ b/WguApp/Platforms/iOS/Info.plist @@ -0,0 +1,32 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/WguApp/Platforms/iOS/Program.cs b/WguApp/Platforms/iOS/Program.cs new file mode 100644 index 0000000..e00b3ae --- /dev/null +++ b/WguApp/Platforms/iOS/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace WguApp; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} \ No newline at end of file diff --git a/WguApp/Platforms/iOS/Resources/PrivacyInfo.xcprivacy b/WguApp/Platforms/iOS/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..24ab3b4 --- /dev/null +++ b/WguApp/Platforms/iOS/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,51 @@ + + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + + + + + + diff --git a/WguApp/Properties/launchSettings.json b/WguApp/Properties/launchSettings.json new file mode 100644 index 0000000..4f85793 --- /dev/null +++ b/WguApp/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "Project", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/WguApp/Resources/AppIcon/appicon.svg b/WguApp/Resources/AppIcon/appicon.svg new file mode 100644 index 0000000..9d63b65 --- /dev/null +++ b/WguApp/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/WguApp/Resources/AppIcon/appiconfg.svg b/WguApp/Resources/AppIcon/appiconfg.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/WguApp/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/WguApp/Resources/Fonts/OpenSans-Regular.ttf b/WguApp/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..eefdc97 Binary files /dev/null and b/WguApp/Resources/Fonts/OpenSans-Regular.ttf differ diff --git a/WguApp/Resources/Fonts/OpenSans-Semibold.ttf b/WguApp/Resources/Fonts/OpenSans-Semibold.ttf new file mode 100644 index 0000000..7df34d8 Binary files /dev/null and b/WguApp/Resources/Fonts/OpenSans-Semibold.ttf differ diff --git a/WguApp/Resources/Fonts/Phosphor-Fill.ttf b/WguApp/Resources/Fonts/Phosphor-Fill.ttf new file mode 100644 index 0000000..b02c4cb Binary files /dev/null and b/WguApp/Resources/Fonts/Phosphor-Fill.ttf differ diff --git a/WguApp/Resources/Fonts/Phosphor.ttf b/WguApp/Resources/Fonts/Phosphor.ttf new file mode 100644 index 0000000..7c1b8a7 Binary files /dev/null and b/WguApp/Resources/Fonts/Phosphor.ttf differ diff --git a/WguApp/Resources/Images/dotnet_bot.png b/WguApp/Resources/Images/dotnet_bot.png new file mode 100644 index 0000000..1d1b981 Binary files /dev/null and b/WguApp/Resources/Images/dotnet_bot.png differ diff --git a/WguApp/Resources/Raw/AboutAssets.txt b/WguApp/Resources/Raw/AboutAssets.txt new file mode 100644 index 0000000..89dc758 --- /dev/null +++ b/WguApp/Resources/Raw/AboutAssets.txt @@ -0,0 +1,15 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories). Deployment of the asset to your application +is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. + + + +These files will be deployed with your package and will be accessible using Essentials: + + async Task LoadMauiAsset() + { + using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + } diff --git a/WguApp/Resources/Splash/splash.svg b/WguApp/Resources/Splash/splash.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/WguApp/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/WguApp/Resources/Styles/Colors.xaml b/WguApp/Resources/Styles/Colors.xaml new file mode 100644 index 0000000..30307a5 --- /dev/null +++ b/WguApp/Resources/Styles/Colors.xaml @@ -0,0 +1,45 @@ + + + + + + + #512BD4 + #ac99ea + #242424 + #DFD8F7 + #9880e5 + #2B0B98 + + White + Black + #D600AA + #190649 + #1f1f1f + + #E1E1E1 + #C8C8C8 + #ACACAC + #919191 + #6E6E6E + #404040 + #212121 + #141414 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WguApp/Resources/Styles/Styles.xaml b/WguApp/Resources/Styles/Styles.xaml new file mode 100644 index 0000000..d4dded0 --- /dev/null +++ b/WguApp/Resources/Styles/Styles.xaml @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WguApp/Services/DatabaseService.cs b/WguApp/Services/DatabaseService.cs new file mode 100644 index 0000000..9004611 --- /dev/null +++ b/WguApp/Services/DatabaseService.cs @@ -0,0 +1,230 @@ + +using SQLite; +using WguApp.Models; + +namespace WguApp.Services; + +public static class DatabaseService +{ + + private static SQLiteAsyncConnection? _db; + + public static async Task Init() + { + if (_db is not null) return; + + var databasePath = Path.Combine(FileSystem.AppDataDirectory, "WguApp.db"); + _db = new SQLiteAsyncConnection(databasePath); + + try + { + await _db.CreateTablesAsync(); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + // -- Add Methods -- // + #region Add methods + + public static async Task AddTerm(Term term) + { + await Init(); + var result = await _db?.InsertAsync(term)!; + return !(result <= 0); + } + + public static async Task AddCourse(Course course) + { + await Init(); + var result = await _db?.InsertAsync(course)!; + return !(result <= 0); + } + + public static async Task AddAssessment(Assessment assessment) + { + await Init(); + var result = await _db?.InsertAsync(assessment)!; + return !(result <= 0); + } + + #endregion + + // -- Get Methods -- // + #region Get Methods + public static async Task> GetAllTerms() + { + await Init(); + var allTerms = await _db?.Table().ToListAsync()!; + return allTerms ?? []; + } + + public static async Task> GetAllCourses() + { + await Init(); + var allCourses = await _db?.Table().ToListAsync()!; + return allCourses ?? []; + } + + public static async Task> GetAllAssessments() + { + await Init(); + var allAssessments = await _db?.Table().ToListAsync()!; + return allAssessments ?? []; + } + + public static async Task> GetAssessmentsByCourseId(int courseId) + { + await Init(); + var query = $"SELECT * FROM Assessments WHERE CourseId = {courseId}"; + var result = await _db?.QueryAsync(query)!; + return result.ToList() ?? []; + } + + public static async Task> GetCoursesByTermId(int termId) + { + await Init(); + var query = $"SELECT * FROM Courses WHERE TermId = {termId}"; + var result = await _db?.QueryAsync(query)!; + return result.ToList() ?? []; + } + + #endregion + + // -- Update Methods + #region Update Methods + + public static async Task UpdateTerm(Term term) + { + await Init(); + return await _db?.UpdateAsync(term)! != 0; + } + + public static async Task UpdateCourse(Course course) + { + await Init(); + return await _db?.UpdateAsync(course)! != 0; + } + + public static async Task UpdateAssessment(Assessment assessment) + { + await Init(); + return await _db?.UpdateAsync(assessment)! != 0; + } + + #endregion + + // -- Delete Methods + #region Delete Methods + public static async Task DeleteTerm(Term term) + { + await Init(); + return await _db?.DeleteAsync(term)! != 0; + } + + public static async Task DeleteTerm(int termId) + { + await Init(); + return await _db?.DeleteAsync(termId)! != 0; + } + + public static async Task DeleteCourse(Course course) + { + await Init(); + return await _db?.DeleteAsync(course)! != 0; + } + + public static async Task DeleteCourse(int courseId) + { + await Init(); + return await _db?.DeleteAsync(courseId)! != 0; + } + + public static async Task DeleteAssessment(Assessment assessment) + { + await Init(); + return await _db?.DeleteAsync(assessment)! != 0; + } + + public static async Task DeleteAssessment(int assessmentId) + { + await Init(); + return await _db?.DeleteAsync(assessmentId)! != 0; + } + + #endregion + + + // -- Sample Data -- // + public static async Task LoadSampleData() + { + await ClearDbData(); + + var term1 = new Term() + { + Name = "Term 1", + StartDate = DateTime.Today, + EndDate = DateTime.Today.AddDays(30) + }; + + await AddTerm(term1); + + var course1 = new Course() + { + TermId = term1.Id, + Name = "Course 1", + StartDate = DateTime.Today, + EndDate = DateTime.Today.AddDays(30), + InstructorName = "Anika Patel", + InstructorEmail = "anika.patel@strimeuniversity.edu", + InstructorPhone = "555-123-4567", + StartNotifCheck = false, + EndNotifCheck = false, + Status = CourseStatus.InProgress, + Notes = "Some notes" + }; + + await AddCourse(course1); + + var assessment1 = new Assessment() + { + CourseId = course1.Id, + Name = "Performance Assessment", + Type = AssessmentType.Performance, + StartDate = DateTime.Today, + EndDate = DateTime.Today.AddDays(30), + StartNotifCheck = false, + EndNotifCheck = false + }; + + await AddAssessment(assessment1); + + var assessment2 = new Assessment() + { + CourseId = course1.Id, + Name = "Objective Assessment", + Type = AssessmentType.Objective, + StartDate = DateTime.Today, + EndDate = DateTime.Today.AddDays(30), + StartNotifCheck = false, + EndNotifCheck = false + }; + + await AddAssessment(assessment2); + } + + public static async Task ClearDbData() + { + await Init(); + + await _db?.DeleteAllAsync()!; + await _db?.DeleteAllAsync()!; + await _db?.DeleteAllAsync()!; + + _db = null; + + } +} \ No newline at end of file diff --git a/WguApp/Services/LocalNotificationService.cs b/WguApp/Services/LocalNotificationService.cs new file mode 100644 index 0000000..5b870e6 --- /dev/null +++ b/WguApp/Services/LocalNotificationService.cs @@ -0,0 +1,114 @@ +using Plugin.LocalNotification; + +namespace WguApp.Services; + +public static class LocalNotificationService +{ + private static readonly Random Rng = new(); + + public static async Task ScheduleNotification(string title, string description, DateTime time) + { + var permCheck = await PermissionsService.CheckNotificationPermissions(); + if (!permCheck) + { + LoggerService.LogToFile("Notification Service: Notification Permission Not Granted"); + return null; + } + + var newId = Rng.Next(); + + var notif = new NotificationRequest + { + NotificationId = newId, + Title = title, + Description = description, + Schedule = + { + NotifyTime = time, + RepeatType = NotificationRepeat.No + } + }; + + try + { + await LocalNotificationCenter.Current.Show(notif); + LoggerService.LogToFile($"Notification Service: {notif.NotificationId}:{notif.Title} set"); + } + catch (Exception e) + { + LoggerService.LogToFile($"Notification Service: {e.Message}"); + throw; + } + + return notif.NotificationId; + } + + public static async Task ScheduleNotification(NotificationRequest notif) + { + try + { + await LocalNotificationCenter.Current.Show(notif); + } + catch (Exception e) + { + LoggerService.LogToFile(e.Message); + throw; + } + return notif.NotificationId; + } + + public static async Task UpdateNotification(int notificationId, string? title = null, string? description = null, DateTime? time = null) + { + var pendingNotifs = await LocalNotificationCenter.Current.GetPendingNotificationList(); + + var notif = pendingNotifs.FirstOrDefault(n => n.NotificationId == notificationId); + + if (notif is null) return false; + + await CancelNotification(notif); + + notif.Title = title ?? notif.Title; + notif.Description = description ?? notif.Description; + notif.Schedule = new NotificationRequestSchedule() + { + NotifyTime = time ?? notif.Schedule.NotifyTime, + RepeatType = NotificationRepeat.No + }; + + await ScheduleNotification(notif); + return true; + } + + public static async Task CancelNotification(int notificationId) + { + var pendingNotifs = await LocalNotificationCenter.Current.GetPendingNotificationList(); + + var match = pendingNotifs.FirstOrDefault(n => n.NotificationId == notificationId); + + if (match is null) return false; + + LocalNotificationCenter.Current.Cancel(notificationId); + + return true; + } + + public static async Task CancelNotification(NotificationRequest notif) + { + var pendingNotifs = await LocalNotificationCenter.Current.GetPendingNotificationList(); + + if (pendingNotifs.Contains(notif)) + { + notif.Cancel(); + } + + return true; + } + + public static async Task DoesNotificationAlreadyExist(int notificationId) + { + var pendingNotifs = await LocalNotificationCenter.Current.GetPendingNotificationList(); + var match = pendingNotifs.FirstOrDefault(n => n.NotificationId == notificationId); + + return match is not null; + } +} \ No newline at end of file diff --git a/WguApp/Services/LoggerService.cs b/WguApp/Services/LoggerService.cs new file mode 100644 index 0000000..daf3890 --- /dev/null +++ b/WguApp/Services/LoggerService.cs @@ -0,0 +1,11 @@ +namespace WguApp.Services; + +public class LoggerService +{ + public static void LogToFile(string text) + { + var logFilePath = Path.Combine(FileSystem.AppDataDirectory, "log.txt"); + + File.AppendAllText(logFilePath, $"{DateTime.UtcNow} : {text}\n"); + } +} \ No newline at end of file diff --git a/WguApp/Services/PermissionsService.cs b/WguApp/Services/PermissionsService.cs new file mode 100644 index 0000000..1f1f199 --- /dev/null +++ b/WguApp/Services/PermissionsService.cs @@ -0,0 +1,21 @@ +using Plugin.LocalNotification; + +namespace WguApp.Services; + +public static class PermissionsService +{ + public static async Task CheckNotificationPermissions() + { + if (await LocalNotificationCenter.Current.AreNotificationsEnabled()) return true; + + try + { + return await LocalNotificationCenter.Current.RequestNotificationPermission(); + } + catch (Exception e) + { + LoggerService.LogToFile(e.Message); + return false; + } + } +} \ No newline at end of file diff --git a/WguApp/Views/AssessmentPage.xaml b/WguApp/Views/AssessmentPage.xaml new file mode 100644 index 0000000..908ef36 --- /dev/null +++ b/WguApp/Views/AssessmentPage.xaml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WguApp/Views/AssessmentPage.xaml.cs b/WguApp/Views/AssessmentPage.xaml.cs new file mode 100644 index 0000000..4450bf0 --- /dev/null +++ b/WguApp/Views/AssessmentPage.xaml.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WguApp.Models; +using WguApp.Services; + +namespace WguApp.Views; + +public partial class AssessmentPage : ContentPage +{ + + private Assessment _assessment; + + public AssessmentPage(Assessment assessment) + { + InitializeComponent(); + + _assessment = assessment; + + Title = assessment.Name; + + TitleEntry.Text = _assessment.Name; + StartDatePicker.Date = _assessment.StartDate; + EndDatePicker.Date = _assessment.EndDate; + StartNotifCheck.IsChecked = _assessment.StartNotifCheck; + EndNotifCheck.IsChecked = _assessment.EndNotifCheck; + + StartNotifCheck.CheckedChanged += async (sender, e) => { await ToggleStartNotification(); }; + EndNotifCheck.CheckedChanged += async (sender, e) => { await ToggleEndNotification(); }; + } + + private async Task ToggleStartNotification() + { + if (StartNotifCheck.IsChecked) + { + var id = await LocalNotificationService.ScheduleNotification($"{_assessment.Name} Start Reminder", $"Your assessment '{_assessment.Id} starts in 12 hours'", _assessment.StartDate.Subtract(TimeSpan.FromHours(12))); + _assessment.StartNotifId = id ?? 0; + LoggerService.LogToFile($"{_assessment.Id}:{_assessment.Name} set start notification {_assessment.StartNotifId}"); + } + else + { + var res = await LocalNotificationService.CancelNotification(_assessment.StartNotifId); + LoggerService.LogToFile($"{_assessment.Id}:{_assessment.Name} canceled start notification {_assessment.StartNotifId}"); + _assessment.StartNotifId = 0; + } + + _assessment.StartNotifCheck = StartNotifCheck.IsChecked; + } + + private async Task ToggleEndNotification() + { + if (EndNotifCheck.IsChecked) + { + var id = await LocalNotificationService.ScheduleNotification($"{_assessment.Name} End Reminder", $"Your assessment '{_assessment.Id} ends in 12 hours'", _assessment.EndDate.Subtract(TimeSpan.FromHours(12))); + _assessment.EndNotifId = id ?? 0; + LoggerService.LogToFile($"{_assessment.Id}:{_assessment.Name} set end notification {_assessment.EndNotifId}"); + } + else + { + var res = await LocalNotificationService.CancelNotification(_assessment.EndNotifId); + LoggerService.LogToFile($"{_assessment.Id}:{_assessment.Name} canceled end notification {_assessment.EndNotifId}"); + _assessment.EndNotifId = 0; + } + + _assessment.EndNotifCheck = EndNotifCheck.IsChecked; + } + + private (string msg, bool result) ValidateFields() + { + var message = string.Empty; + + if (string.IsNullOrWhiteSpace(TitleEntry.Text)) message += "Title cannot be blank\n"; + if (StartDatePicker.Date > EndDatePicker.Date) message += "Start Date cannot be after the End Date\n"; + + return string.IsNullOrWhiteSpace(message) ? (message, true) : (message, false); + } + + private async Task Save() + { + var validationResult = ValidateFields(); + + if (!validationResult.result) + { + await DisplayAlert("Validation Error", validationResult.msg, "Ok"); + return false; + } + + _assessment.Name = TitleEntry.Text; + _assessment.StartDate = StartDatePicker.Date; + _assessment.EndDate = EndDatePicker.Date; + _assessment.StartNotifCheck = StartNotifCheck.IsChecked; + _assessment.EndNotifCheck = EndNotifCheck.IsChecked; + + var updateResult = await DatabaseService.UpdateAssessment(_assessment); + + if (updateResult) + { + await DisplayAlert("Info", $"{_assessment.Name} Saved!", "Ok"); + } + else + { + await DisplayAlert("Error", "Could not save term", "Ok"); + } + + Title = _assessment.Name; + + if (_assessment.StartNotifId != 0) + { + await LocalNotificationService.UpdateNotification(_assessment.StartNotifId, $"{_assessment.Name} Start Reminder", + $"Your course '{_assessment.Id} starts in 12 hours'", _assessment.StartDate.Subtract(TimeSpan.FromHours(12))); + } + + if (_assessment.EndNotifId != 0) + { + await LocalNotificationService.UpdateNotification(_assessment.EndNotifId, $"{_assessment.Name} End Reminder", + $"Your course '{_assessment.Id} ends in 12 hours'", _assessment.EndDate.Subtract(TimeSpan.FromHours(12))); + } + + return true; + } + + private async void SaveButton_OnClicked(object? sender, EventArgs e) + { + await Save(); + } + + private async void DeleteButton_OnClicked(object? sender, EventArgs e) + { + var result = await DisplayAlert("Delete Course", $"Do you really want to delete '{_assessment.Name}'? This action cannot be undone.", "Delete", "Cancel"); + if (!result) return; + + await DatabaseService.DeleteAssessment(_assessment); + await Navigation.PopAsync(); + } +} \ No newline at end of file diff --git a/WguApp/Views/CoursePage.xaml b/WguApp/Views/CoursePage.xaml new file mode 100644 index 0000000..36ba659 --- /dev/null +++ b/WguApp/Views/CoursePage.xaml @@ -0,0 +1,270 @@ + + + + + + + + + + + + + +