From 79bf276ab9f0b693055f18feb95ef71a52a7bd1f Mon Sep 17 00:00:00 2001 From: rjindael Date: Thu, 15 Jun 2023 00:36:05 -0700 Subject: [PATCH] Add WinUI 3 app skeleton --- .editorconfig | 202 +++++++ .vsconfig | 16 + .../Contracts/Services/IFileService.cs | 10 + .../Contracts/Services/ISampleDataService.cs | 13 + Estara.Core/Estara.Core.csproj | 17 + Estara.Core/Helpers/Json.cs | 22 + Estara.Core/Models/SampleCompany.cs | 60 ++ Estara.Core/Models/SampleOrder.cs | 81 +++ Estara.Core/Models/SampleOrderDetail.cs | 52 ++ Estara.Core/README.md | 5 + Estara.Core/Services/FileService.cs | 41 ++ Estara.Core/Services/SampleDataService.cs | 522 ++++++++++++++++++ Estara.sln | 58 +- Estara/Activation/ActivationHandler.cs | 17 + .../AppNotificationActivationHandler.cs | 51 ++ Estara/Activation/DefaultActivationHandler.cs | 29 + Estara/Activation/IActivationHandler.cs | 8 + Estara/App.xaml | 18 +- Estara/App.xaml.cs | 122 +++- Estara/AssemblyInfo.cs | 10 - Estara/Assets/LockScreenLogo.scale-200.png | Bin 0 -> 1430 bytes Estara/Assets/SplashScreen.scale-200.png | Bin 0 -> 7700 bytes Estara/Assets/Square150x150Logo.scale-200.png | Bin 0 -> 2937 bytes Estara/Assets/Square44x44Logo.scale-200.png | Bin 0 -> 1647 bytes ...x44Logo.targetsize-24_altform-unplated.png | Bin 0 -> 1255 bytes Estara/Assets/StoreLogo.png | Bin 0 -> 1451 bytes Estara/Assets/Wide310x150Logo.scale-200.png | Bin 0 -> 3204 bytes Estara/Assets/WindowIcon.ico | Bin 0 -> 66971 bytes .../Behaviors/NavigationViewHeaderBehavior.cs | 122 ++++ Estara/Behaviors/NavigationViewHeaderMode.cs | 8 + .../Contracts/Services/IActivationService.cs | 6 + .../Services/IAppNotificationService.cs | 14 + .../Services/ILocalSettingsService.cs | 8 + .../Contracts/Services/INavigationService.cs | 25 + .../Services/INavigationViewService.cs | 22 + Estara/Contracts/Services/IPageService.cs | 6 + .../Services/IThemeSelectorService.cs | 17 + .../Contracts/ViewModels/INavigationAware.cs | 8 + Estara/Estara.csproj | 58 +- Estara/Helpers/EnumToBooleanConverter.cs | 38 ++ Estara/Helpers/FrameExtensions.cs | 8 + Estara/Helpers/NavigationHelper.cs | 21 + Estara/Helpers/ResourceExtensions.cs | 10 + Estara/Helpers/RuntimeHelper.cs | 20 + Estara/Helpers/SettingsStorageExtensions.cs | 112 ++++ Estara/Helpers/TitleBarHelper.cs | 121 ++++ Estara/MainWindow.xaml | 28 +- Estara/MainWindow.xaml.cs | 53 +- Estara/Models/LocalSettingsOptions.cs | 14 + Estara/Package.appinstaller | 17 + Estara/Package.appxmanifest | 76 +++ Estara/Properties/launchsettings.json | 10 + Estara/README.md | 27 + Estara/Services/ActivationService.cs | 72 +++ Estara/Services/AppNotificationService.cs | 71 +++ Estara/Services/LocalSettingsService.cs | 88 +++ Estara/Services/NavigationService.cs | 130 +++++ Estara/Services/NavigationViewService.cs | 103 ++++ Estara/Services/PageService.cs | 60 ++ Estara/Services/ThemeSelectorService.cs | 63 +++ Estara/Strings/en-us/Resources.resw | 123 +++++ Estara/Styles/FontSizes.xaml | 9 + Estara/Styles/TextBlock.xaml | 50 ++ Estara/Styles/Thickness.xaml | 36 ++ Estara/TemplateStudio.xml | 11 + Estara/Usings.cs | 1 + .../ViewModels/ContentGridDetailViewModel.cs | 33 ++ Estara/ViewModels/ContentGridViewModel.cs | 52 ++ Estara/ViewModels/DataGridViewModel.cs | 38 ++ Estara/ViewModels/ListDetailsViewModel.cs | 46 ++ Estara/ViewModels/MainViewModel.cs | 10 + Estara/ViewModels/SettingsViewModel.cs | 65 +++ Estara/ViewModels/ShellViewModel.cs | 51 ++ Estara/Views/ContentGridDetailPage.xaml | 107 ++++ Estara/Views/ContentGridDetailPage.xaml.cs | 43 ++ Estara/Views/ContentGridPage.xaml | 43 ++ Estara/Views/ContentGridPage.xaml.cs | 19 + Estara/Views/DataGridPage.xaml | 40 ++ Estara/Views/DataGridPage.xaml.cs | 21 + Estara/Views/ListDetailsDetailControl.xaml | 73 +++ Estara/Views/ListDetailsDetailControl.xaml.cs | 30 + Estara/Views/ListDetailsPage.xaml | 100 ++++ Estara/Views/ListDetailsPage.xaml.cs | 29 + Estara/Views/MainPage.xaml | 12 + Estara/Views/MainPage.xaml.cs | 19 + Estara/Views/SettingsPage.xaml | 67 +++ Estara/Views/SettingsPage.xaml.cs | 20 + Estara/Views/ShellPage.xaml | 95 ++++ Estara/Views/ShellPage.xaml.cs | 88 +++ Estara/app.manifest | 15 + Estara/appsettings.json | 6 + README.md | 18 +- 92 files changed, 4086 insertions(+), 74 deletions(-) create mode 100644 .editorconfig create mode 100644 .vsconfig create mode 100644 Estara.Core/Contracts/Services/IFileService.cs create mode 100644 Estara.Core/Contracts/Services/ISampleDataService.cs create mode 100644 Estara.Core/Estara.Core.csproj create mode 100644 Estara.Core/Helpers/Json.cs create mode 100644 Estara.Core/Models/SampleCompany.cs create mode 100644 Estara.Core/Models/SampleOrder.cs create mode 100644 Estara.Core/Models/SampleOrderDetail.cs create mode 100644 Estara.Core/README.md create mode 100644 Estara.Core/Services/FileService.cs create mode 100644 Estara.Core/Services/SampleDataService.cs create mode 100644 Estara/Activation/ActivationHandler.cs create mode 100644 Estara/Activation/AppNotificationActivationHandler.cs create mode 100644 Estara/Activation/DefaultActivationHandler.cs create mode 100644 Estara/Activation/IActivationHandler.cs delete mode 100644 Estara/AssemblyInfo.cs create mode 100644 Estara/Assets/LockScreenLogo.scale-200.png create mode 100644 Estara/Assets/SplashScreen.scale-200.png create mode 100644 Estara/Assets/Square150x150Logo.scale-200.png create mode 100644 Estara/Assets/Square44x44Logo.scale-200.png create mode 100644 Estara/Assets/Square44x44Logo.targetsize-24_altform-unplated.png create mode 100644 Estara/Assets/StoreLogo.png create mode 100644 Estara/Assets/Wide310x150Logo.scale-200.png create mode 100644 Estara/Assets/WindowIcon.ico create mode 100644 Estara/Behaviors/NavigationViewHeaderBehavior.cs create mode 100644 Estara/Behaviors/NavigationViewHeaderMode.cs create mode 100644 Estara/Contracts/Services/IActivationService.cs create mode 100644 Estara/Contracts/Services/IAppNotificationService.cs create mode 100644 Estara/Contracts/Services/ILocalSettingsService.cs create mode 100644 Estara/Contracts/Services/INavigationService.cs create mode 100644 Estara/Contracts/Services/INavigationViewService.cs create mode 100644 Estara/Contracts/Services/IPageService.cs create mode 100644 Estara/Contracts/Services/IThemeSelectorService.cs create mode 100644 Estara/Contracts/ViewModels/INavigationAware.cs create mode 100644 Estara/Helpers/EnumToBooleanConverter.cs create mode 100644 Estara/Helpers/FrameExtensions.cs create mode 100644 Estara/Helpers/NavigationHelper.cs create mode 100644 Estara/Helpers/ResourceExtensions.cs create mode 100644 Estara/Helpers/RuntimeHelper.cs create mode 100644 Estara/Helpers/SettingsStorageExtensions.cs create mode 100644 Estara/Helpers/TitleBarHelper.cs create mode 100644 Estara/Models/LocalSettingsOptions.cs create mode 100644 Estara/Package.appinstaller create mode 100644 Estara/Package.appxmanifest create mode 100644 Estara/Properties/launchsettings.json create mode 100644 Estara/README.md create mode 100644 Estara/Services/ActivationService.cs create mode 100644 Estara/Services/AppNotificationService.cs create mode 100644 Estara/Services/LocalSettingsService.cs create mode 100644 Estara/Services/NavigationService.cs create mode 100644 Estara/Services/NavigationViewService.cs create mode 100644 Estara/Services/PageService.cs create mode 100644 Estara/Services/ThemeSelectorService.cs create mode 100644 Estara/Strings/en-us/Resources.resw create mode 100644 Estara/Styles/FontSizes.xaml create mode 100644 Estara/Styles/TextBlock.xaml create mode 100644 Estara/Styles/Thickness.xaml create mode 100644 Estara/TemplateStudio.xml create mode 100644 Estara/Usings.cs create mode 100644 Estara/ViewModels/ContentGridDetailViewModel.cs create mode 100644 Estara/ViewModels/ContentGridViewModel.cs create mode 100644 Estara/ViewModels/DataGridViewModel.cs create mode 100644 Estara/ViewModels/ListDetailsViewModel.cs create mode 100644 Estara/ViewModels/MainViewModel.cs create mode 100644 Estara/ViewModels/SettingsViewModel.cs create mode 100644 Estara/ViewModels/ShellViewModel.cs create mode 100644 Estara/Views/ContentGridDetailPage.xaml create mode 100644 Estara/Views/ContentGridDetailPage.xaml.cs create mode 100644 Estara/Views/ContentGridPage.xaml create mode 100644 Estara/Views/ContentGridPage.xaml.cs create mode 100644 Estara/Views/DataGridPage.xaml create mode 100644 Estara/Views/DataGridPage.xaml.cs create mode 100644 Estara/Views/ListDetailsDetailControl.xaml create mode 100644 Estara/Views/ListDetailsDetailControl.xaml.cs create mode 100644 Estara/Views/ListDetailsPage.xaml create mode 100644 Estara/Views/ListDetailsPage.xaml.cs create mode 100644 Estara/Views/MainPage.xaml create mode 100644 Estara/Views/MainPage.xaml.cs create mode 100644 Estara/Views/SettingsPage.xaml create mode 100644 Estara/Views/SettingsPage.xaml.cs create mode 100644 Estara/Views/ShellPage.xaml create mode 100644 Estara/Views/ShellPage.xaml.cs create mode 100644 Estara/app.manifest create mode 100644 Estara/appsettings.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fd05618 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,202 @@ +# Rules in this file were initially inferred by Visual Studio IntelliCode from the Template Studio codebase. +# You can modify the rules from these initially generated values to suit your own policies. +# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference. + +[*.cs] + +#Core editorconfig formatting - indentation + +#use soft tabs (spaces) for indentation +indent_style = space + +#Formatting - new line options + +#place else statements on a new line +csharp_new_line_before_else = true +#require braces to be on a new line for lambdas, methods, control_blocks, types, properties, and accessors (also known as "Allman" style) +csharp_new_line_before_open_brace = all + +#Formatting - organize using options + +#sort System.* using directives alphabetically, and place them before other usings +dotnet_sort_system_directives_first = true + +#Formatting - spacing options + +#require NO space between a cast and the value +csharp_space_after_cast = false +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_after_colon_in_inheritance_clause = true +#require a space after a keyword in a control flow statement such as a for loop +csharp_space_after_keywords_in_control_flow_statements = true +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_before_colon_in_inheritance_clause = true +#remove space within empty argument list parentheses +csharp_space_between_method_call_empty_parameter_list_parentheses = false +#remove space between method call name and opening parenthesis +csharp_space_between_method_call_name_and_opening_parenthesis = false +#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call +csharp_space_between_method_call_parameter_list_parentheses = false +#remove space within empty parameter list parentheses for a method declaration +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options + +#leave code block on separate lines +csharp_preserve_single_line_blocks = false + +#Style - Code block preferences + +#prefer curly braces even for one line of code +csharp_prefer_braces = true:suggestion + +#Style - expression bodied member options + +#prefer expression bodies for accessors +csharp_style_expression_bodied_accessors = true:warning +#prefer block bodies for constructors +csharp_style_expression_bodied_constructors = false:suggestion +#prefer expression bodies for methods +csharp_style_expression_bodied_methods = when_on_single_line:silent +#prefer expression-bodied members for properties +csharp_style_expression_bodied_properties = true:warning + +#Style - expression level options + +#prefer out variables to be declared before the method call +csharp_style_inlined_variable_declaration = false:suggestion +#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_member_access = true:suggestion + +#Style - Expression-level preferences + +#prefer default over default(T) +csharp_prefer_simple_default_expression = true:suggestion +#prefer objects to be initialized using object initializers when possible +dotnet_style_object_initializer = true:suggestion + +#Style - implicit and explicit types + +#prefer var over explicit type in all cases, unless overridden by another code style rule +csharp_style_var_elsewhere = true:suggestion +#prefer var is used to declare variables with built-in system types such as int +csharp_style_var_for_built_in_types = true:suggestion +#prefer var when the type is already mentioned on the right-hand side of a declaration expression +csharp_style_var_when_type_is_apparent = true:suggestion + +#Style - language keyword and framework type options + +#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion + +#Style - Language rules +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning +csharp_style_var_for_built_in_types = true:warning + +#Style - modifier options + +#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +#Style - Modifier preferences + +#when this rule is set to a list of modifiers, prefer the specified ordering. +csharp_preferred_modifier_order = public,private,protected,internal,static,async,readonly,override,sealed,abstract,virtual:warning +dotnet_style_readonly_field = true:warning + +#Style - Pattern matching + +#prefer pattern matching instead of is expression with type casts +csharp_style_pattern_matching_over_as_with_null_check = true:warning + +#Style - qualification options + +#prefer events not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_event = false:suggestion +#prefer fields not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_field = false:suggestion +#prefer methods not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_method = false:suggestion +#prefer properties not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_property = false:suggestion +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:warning +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +[*.{cs,vb}] + +#Style - Unnecessary code rules +csharp_style_unused_value_assignment_preference = discard_variable:warning + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_simplified_interpolation = true:suggestion diff --git a/.vsconfig b/.vsconfig new file mode 100644 index 0000000..65d1f73 --- /dev/null +++ b/.vsconfig @@ -0,0 +1,16 @@ +{ + "version": "1.0", + "components": [ + "Microsoft.Component.MSBuild", + "Microsoft.NetCore.Component.Runtime.7.0", + "Microsoft.NetCore.Component.SDK", + "Microsoft.VisualStudio.Component.ManagedDesktop.Core", + "Microsoft.VisualStudio.Component.ManagedDesktop.Prerequisites", + "Microsoft.VisualStudio.Component.NuGet", + "Microsoft.VisualStudio.Component.Windows10SDK.19041", + "Microsoft.VisualStudio.Component.Windows10SDK", + "Microsoft.VisualStudio.ComponentGroup.MSIX.Packaging", + "Microsoft.VisualStudio.ComponentGroup.WindowsAppSDK.Cs", + "Microsoft.VisualStudio.Workload.ManagedDesktop" + ] +} diff --git a/Estara.Core/Contracts/Services/IFileService.cs b/Estara.Core/Contracts/Services/IFileService.cs new file mode 100644 index 0000000..ff80c3b --- /dev/null +++ b/Estara.Core/Contracts/Services/IFileService.cs @@ -0,0 +1,10 @@ +namespace Estara.Core.Contracts.Services; + +public interface IFileService +{ + T Read(string folderPath, string fileName); + + void Save(string folderPath, string fileName, T content); + + void Delete(string folderPath, string fileName); +} diff --git a/Estara.Core/Contracts/Services/ISampleDataService.cs b/Estara.Core/Contracts/Services/ISampleDataService.cs new file mode 100644 index 0000000..0c13ace --- /dev/null +++ b/Estara.Core/Contracts/Services/ISampleDataService.cs @@ -0,0 +1,13 @@ +using Estara.Core.Models; + +namespace Estara.Core.Contracts.Services; + +// Remove this class once your pages/features are using your data. +public interface ISampleDataService +{ + Task> GetContentGridDataAsync(); + + Task> GetGridDataAsync(); + + Task> GetListDetailsDataAsync(); +} diff --git a/Estara.Core/Estara.Core.csproj b/Estara.Core/Estara.Core.csproj new file mode 100644 index 0000000..7041413 --- /dev/null +++ b/Estara.Core/Estara.Core.csproj @@ -0,0 +1,17 @@ + + + net7.0 + Estara.Core + AnyCPU;x64;x86 + x86;x64;arm64;AnyCPU + enable + + + + + + + + + + diff --git a/Estara.Core/Helpers/Json.cs b/Estara.Core/Helpers/Json.cs new file mode 100644 index 0000000..29e64b6 --- /dev/null +++ b/Estara.Core/Helpers/Json.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Estara.Core.Helpers; + +public static class Json +{ + public static async Task ToObjectAsync(string value) + { + return await Task.Run(() => + { + return JsonConvert.DeserializeObject(value); + }); + } + + public static async Task StringifyAsync(object value) + { + return await Task.Run(() => + { + return JsonConvert.SerializeObject(value); + }); + } +} diff --git a/Estara.Core/Models/SampleCompany.cs b/Estara.Core/Models/SampleCompany.cs new file mode 100644 index 0000000..bfa6b70 --- /dev/null +++ b/Estara.Core/Models/SampleCompany.cs @@ -0,0 +1,60 @@ +namespace Estara.Core.Models; + +// Model for the SampleDataService. Replace with your own model. +public class SampleCompany +{ + public string CompanyID + { + get; set; + } + + public string CompanyName + { + get; set; + } + + public string ContactName + { + get; set; + } + + public string ContactTitle + { + get; set; + } + + public string Address + { + get; set; + } + + public string City + { + get; set; + } + + public string PostalCode + { + get; set; + } + + public string Country + { + get; set; + } + + public string Phone + { + get; set; + } + + public string Fax + { + get; set; + } + + public ICollection Orders + { + get; set; + } +} diff --git a/Estara.Core/Models/SampleOrder.cs b/Estara.Core/Models/SampleOrder.cs new file mode 100644 index 0000000..0668e74 --- /dev/null +++ b/Estara.Core/Models/SampleOrder.cs @@ -0,0 +1,81 @@ +namespace Estara.Core.Models; + +// Model for the SampleDataService. Replace with your own model. +public class SampleOrder +{ + public long OrderID + { + get; set; + } + + public DateTime OrderDate + { + get; set; + } + + public DateTime RequiredDate + { + get; set; + } + + public DateTime ShippedDate + { + get; set; + } + + public string ShipperName + { + get; set; + } + + public string ShipperPhone + { + get; set; + } + + public double Freight + { + get; set; + } + + public string Company + { + get; set; + } + + public string ShipTo + { + get; set; + } + + public double OrderTotal + { + get; set; + } + + public string Status + { + get; set; + } + + public int SymbolCode + { + get; set; + } + + public string SymbolName + { + get; set; + } + + public char Symbol => (char)SymbolCode; + + public ICollection Details + { + get; set; + } + + public string ShortDescription => $"Order ID: {OrderID}"; + + public override string ToString() => $"{Company} {Status}"; +} diff --git a/Estara.Core/Models/SampleOrderDetail.cs b/Estara.Core/Models/SampleOrderDetail.cs new file mode 100644 index 0000000..ecefb16 --- /dev/null +++ b/Estara.Core/Models/SampleOrderDetail.cs @@ -0,0 +1,52 @@ +namespace Estara.Core.Models; + +// Model for the SampleDataService. Replace with your own model. +public class SampleOrderDetail +{ + public long ProductID + { + get; set; + } + + public string ProductName + { + get; set; + } + + public int Quantity + { + get; set; + } + + public double Discount + { + get; set; + } + + public string QuantityPerUnit + { + get; set; + } + + public double UnitPrice + { + get; set; + } + + public string CategoryName + { + get; set; + } + + public string CategoryDescription + { + get; set; + } + + public double Total + { + get; set; + } + + public string ShortDescription => $"Product ID: {ProductID} - {ProductName}"; +} diff --git a/Estara.Core/README.md b/Estara.Core/README.md new file mode 100644 index 0000000..906c066 --- /dev/null +++ b/Estara.Core/README.md @@ -0,0 +1,5 @@ +*Recommended Markdown Viewer: [Markdown Editor](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.MarkdownEditor2)* + +## Getting Started + +The Core project contains code that can be [reused across multiple application projects](https://docs.microsoft.com/dotnet/standard/net-standard#net-5-and-net-standard). diff --git a/Estara.Core/Services/FileService.cs b/Estara.Core/Services/FileService.cs new file mode 100644 index 0000000..b741474 --- /dev/null +++ b/Estara.Core/Services/FileService.cs @@ -0,0 +1,41 @@ +using System.Text; + +using Estara.Core.Contracts.Services; + +using Newtonsoft.Json; + +namespace Estara.Core.Services; + +public class FileService : IFileService +{ + public T Read(string folderPath, string fileName) + { + var path = Path.Combine(folderPath, fileName); + if (File.Exists(path)) + { + var json = File.ReadAllText(path); + return JsonConvert.DeserializeObject(json); + } + + return default; + } + + public void Save(string folderPath, string fileName, T content) + { + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + + var fileContent = JsonConvert.SerializeObject(content); + File.WriteAllText(Path.Combine(folderPath, fileName), fileContent, Encoding.UTF8); + } + + public void Delete(string folderPath, string fileName) + { + if (fileName != null && File.Exists(Path.Combine(folderPath, fileName))) + { + File.Delete(Path.Combine(folderPath, fileName)); + } + } +} diff --git a/Estara.Core/Services/SampleDataService.cs b/Estara.Core/Services/SampleDataService.cs new file mode 100644 index 0000000..a20f96b --- /dev/null +++ b/Estara.Core/Services/SampleDataService.cs @@ -0,0 +1,522 @@ +using Estara.Core.Contracts.Services; +using Estara.Core.Models; + +namespace Estara.Core.Services; + +// This class holds sample data used by some generated pages to show how they can be used. +// TODO: The following classes have been created to display sample data. Delete these files once your app is using real data. +// 1. Contracts/Services/ISampleDataService.cs +// 2. Services/SampleDataService.cs +// 3. Models/SampleCompany.cs +// 4. Models/SampleOrder.cs +// 5. Models/SampleOrderDetail.cs +public class SampleDataService : ISampleDataService +{ + private List _allOrders; + + public SampleDataService() + { + } + + private static IEnumerable AllOrders() + { + // The following is order summary data + var companies = AllCompanies(); + return companies.SelectMany(c => c.Orders); + } + + private static IEnumerable AllCompanies() + { + return new List() + { + new SampleCompany() + { + CompanyID = "ALFKI", + CompanyName = "Company A", + ContactName = "Maria Anders", + ContactTitle = "Sales Representative", + Address = "Obere Str. 57", + City = "Berlin", + PostalCode = "12209", + Country = "Germany", + Phone = "030-0074321", + Fax = "030-0076545", + Orders = new List() + { + new SampleOrder() + { + OrderID = 10643, // Symbol Globe + OrderDate = new DateTime(1997, 8, 25), + RequiredDate = new DateTime(1997, 9, 22), + ShippedDate = new DateTime(1997, 9, 22), + ShipperName = "Speedy Express", + ShipperPhone = "(503) 555-9831", + Freight = 29.46, + Company = "Company A", + ShipTo = "Company A, Obere Str. 57, Berlin, 12209, Germany", + OrderTotal = 814.50, + Status = "Shipped", + SymbolCode = 57643, + SymbolName = "Globe", + Details = new List() + { + new SampleOrderDetail() + { + ProductID = 28, + ProductName = "Rössle Sauerkraut", + Quantity = 15, + Discount = 0.25, + QuantityPerUnit = "25 - 825 g cans", + UnitPrice = 45.60, + CategoryName = "Produce", + CategoryDescription = "Dried fruit and bean curd", + Total = 513.00 + }, + new SampleOrderDetail() + { + ProductID = 39, + ProductName = "Chartreuse verte", + Quantity = 21, + Discount = 0.25, + QuantityPerUnit = "750 cc per bottle", + UnitPrice = 18.0, + CategoryName = "Beverages", + CategoryDescription = "Soft drinks, coffees, teas, beers, and ales", + Total = 283.50 + }, + new SampleOrderDetail() + { + ProductID = 46, + ProductName = "Spegesild", + Quantity = 2, + Discount = 0.25, + QuantityPerUnit = "4 - 450 g glasses", + UnitPrice = 12.0, + CategoryName = "Seafood", + CategoryDescription = "Seaweed and fish", + Total = 18.00 + } + } + }, + new SampleOrder() + { + OrderID = 10835, // Symbol Music + OrderDate = new DateTime(1998, 1, 15), + RequiredDate = new DateTime(1998, 2, 12), + ShippedDate = new DateTime(1998, 1, 21), + ShipperName = "Federal Shipping", + ShipperPhone = "(503) 555-9931", + Freight = 69.53, + Company = "Company A", + ShipTo = "Company A, Obere Str. 57, Berlin, 12209, Germany", + OrderTotal = 845.80, + Status = "Closed", + SymbolCode = 57737, + SymbolName = "Audio", + Details = new List() + { + new SampleOrderDetail() + { + ProductID = 59, + ProductName = "Raclette Courdavault", + Quantity = 15, + Discount = 0, + QuantityPerUnit = "5 kg pkg.", + UnitPrice = 55.00, + CategoryName = "Dairy Products", + CategoryDescription = "Cheeses", + Total = 825.00 + }, + new SampleOrderDetail() + { + ProductID = 77, + ProductName = "Original Frankfurter grüne Soße", + Quantity = 2, + Discount = 0.2, + QuantityPerUnit = "12 boxes", + UnitPrice = 13.0, + CategoryName = "Condiments", + CategoryDescription = "Sweet and savory sauces, relishes, spreads, and seasonings", + Total = 20.80 + } + } + }, + new SampleOrder() + { + OrderID = 10952, // Symbol Calendar + OrderDate = new DateTime(1998, 3, 16), + RequiredDate = new DateTime(1998, 4, 27), + ShippedDate = new DateTime(1998, 3, 24), + ShipperName = "Speedy Express", + ShipperPhone = "(503) 555-9831", + Freight = 40.42, + Company = "Company A", + ShipTo = "Company A, Obere Str. 57, Berlin, 12209, Germany", + OrderTotal = 471.20, + Status = "Closed", + SymbolCode = 57699, + SymbolName = "Calendar", + Details = new List() + { + new SampleOrderDetail() + { + ProductID = 6, + ProductName = "Grandma's Boysenberry Spread", + Quantity = 16, + Discount = 0.05, + QuantityPerUnit = "12 - 8 oz jars", + UnitPrice = 25.0, + CategoryName = "Condiments", + CategoryDescription = "Sweet and savory sauces, relishes, spreads, and seasonings", + Total = 380.00 + }, + new SampleOrderDetail() + { + ProductID = 28, + ProductName = "Rössle Sauerkraut", + Quantity = 2, + Discount = 0, + QuantityPerUnit = "25 - 825 g cans", + UnitPrice = 45.60, + CategoryName = "Produce", + CategoryDescription = "Dried fruit and bean curd", + Total = 91.20 + } + } + } + } + }, + new SampleCompany() + { + CompanyID = "ANATR", + CompanyName = "Company F", + ContactName = "Ana Trujillo", + ContactTitle = "Owner", + Address = "Avda. de la Constitución 2222", + City = "México D.F.", + PostalCode = "05021", + Country = "Mexico", + Phone = "(5) 555-4729", + Fax = "(5) 555-3745", + Orders = new List() + { + new SampleOrder() + { + OrderID = 10625, // Symbol Camera + OrderDate = new DateTime(1997, 8, 8), + RequiredDate = new DateTime(1997, 9, 5), + ShippedDate = new DateTime(1997, 8, 14), + ShipperName = "Speedy Express", + ShipperPhone = "(503) 555-9831", + Freight = 43.90, + Company = "Company F", + ShipTo = "Company F, Avda. de la Constitución 2222, 05021, México D.F., Mexico", + OrderTotal = 469.75, + Status = "Shipped", + SymbolCode = 57620, + SymbolName = "Camera", + Details = new List() + { + new SampleOrderDetail() + { + ProductID = 14, + ProductName = "Tofu", + Quantity = 3, + Discount = 0, + QuantityPerUnit = "40 - 100 g pkgs.", + UnitPrice = 23.25, + CategoryName = "Produce", + CategoryDescription = "Dried fruit and bean curd", + Total = 69.75 + }, + new SampleOrderDetail() + { + ProductID = 42, + ProductName = "Singaporean Hokkien Fried Mee", + Quantity = 5, + Discount = 0, + QuantityPerUnit = "32 - 1 kg pkgs.", + UnitPrice = 14.0, + CategoryName = "Grains/Cereals", + CategoryDescription = "Breads, crackers, pasta, and cereal", + Total = 70.00 + }, + new SampleOrderDetail() + { + ProductID = 60, + ProductName = "Camembert Pierrot", + Quantity = 10, + Discount = 0, + QuantityPerUnit = "15 - 300 g rounds", + UnitPrice = 34.00, + CategoryName = "Dairy Products", + CategoryDescription = "Cheeses", + Total = 340.00 + } + } + }, + new SampleOrder() + { + OrderID = 10926, // Symbol Clock + OrderDate = new DateTime(1998, 3, 4), + RequiredDate = new DateTime(1998, 4, 1), + ShippedDate = new DateTime(1998, 3, 11), + ShipperName = "Federal Shipping", + ShipperPhone = "(503) 555-9931", + Freight = 39.92, + Company = "Company F", + ShipTo = "Company F, Avda. de la Constitución 2222, 05021, México D.F., Mexico", + OrderTotal = 507.20, + Status = "Shipped", + SymbolCode = 57633, + SymbolName = "Clock", + Details = new List() + { + new SampleOrderDetail() + { + ProductID = 11, + ProductName = "Queso Cabrales", + Quantity = 2, + Discount = 0, + QuantityPerUnit = "1 kg pkg.", + UnitPrice = 21.0, + CategoryName = "Dairy Products", + CategoryDescription = "Cheeses", + Total = 42.00 + }, + new SampleOrderDetail() + { + ProductID = 13, + ProductName = "Konbu", + Quantity = 10, + Discount = 0, + QuantityPerUnit = "2 kg box", + UnitPrice = 6.0, + CategoryName = "Seafood", + CategoryDescription = "Seaweed and fish", + Total = 60.00 + }, + new SampleOrderDetail() + { + ProductID = 19, + ProductName = "Teatime Chocolate Biscuits", + Quantity = 7, + Discount = 0, + QuantityPerUnit = "10 boxes x 12 pieces", + UnitPrice = 9.20, + CategoryName = "Confections", + CategoryDescription = "Desserts, candies, and sweet breads", + Total = 64.40 + }, + new SampleOrderDetail() + { + ProductID = 72, + ProductName = "Mozzarella di Giovanni", + Quantity = 10, + Discount = 0, + QuantityPerUnit = "24 - 200 g pkgs.", + UnitPrice = 34.80, + CategoryName = "Dairy Products", + CategoryDescription = "Cheeses", + Total = 340.80 + } + } + } + } + }, + new SampleCompany() + { + CompanyID = "ANTON", + CompanyName = "Company Z", + ContactName = "Antonio Moreno", + ContactTitle = "Owner", + Address = "Mataderos 2312", + City = "México D.F.", + PostalCode = "05023", + Country = "Mexico", + Phone = "(5) 555-3932", + Fax = string.Empty, + Orders = new List() + { + new SampleOrder() + { + OrderID = 10507, // Symbol Contact + OrderDate = new DateTime(1997, 4, 15), + RequiredDate = new DateTime(1997, 5, 13), + ShippedDate = new DateTime(1997, 4, 22), + ShipperName = "Speedy Express", + ShipperPhone = "(503) 555-9831", + Freight = 47.45, + Company = "Company Z", + ShipTo = "Company Z, Mataderos 2312, 05023, México D.F., Mexico", + OrderTotal = 978.50, + Status = "Closed", + SymbolCode = 57661, + SymbolName = "Contact", + Details = new List() + { + new SampleOrderDetail() + { + ProductID = 43, + ProductName = "Ipoh Coffee", + Quantity = 15, + Discount = 0.15, + QuantityPerUnit = "16 - 500 g tins", + UnitPrice = 46.0, + CategoryName = "Beverages", + CategoryDescription = "Soft drinks, coffees, teas, beers, and ales", + Total = 816.00 + }, + new SampleOrderDetail() + { + ProductID = 48, + ProductName = "Chocolade", + Quantity = 15, + Discount = 0.15, + QuantityPerUnit = "10 pkgs.", + UnitPrice = 12.75, + CategoryName = "Confections", + CategoryDescription = "Desserts, candies, and sweet breads", + Total = 162.50 + } + } + }, + new SampleOrder() + { + OrderID = 10573, // Symbol Star + OrderDate = new DateTime(1997, 6, 19), + RequiredDate = new DateTime(1997, 7, 17), + ShippedDate = new DateTime(1997, 6, 20), + ShipperName = "Federal Shipping", + ShipperPhone = "(503) 555-9931", + Freight = 84.84, + Company = "Company Z", + ShipTo = "Company Z, Mataderos 2312, 05023, México D.F., Mexico", + OrderTotal = 2082.00, + Status = "Closed", + SymbolCode = 57619, + SymbolName = "Favorite", + Details = new List() + { + new SampleOrderDetail() + { + ProductID = 17, + ProductName = "Alice Mutton", + Quantity = 18, + Discount = 0, + QuantityPerUnit = "20 - 1 kg tins", + UnitPrice = 39.00, + CategoryName = "Meat/Poultry", + CategoryDescription = "Prepared meats", + Total = 702.00 + }, + new SampleOrderDetail() + { + ProductID = 34, + ProductName = "Sasquatch Ale", + Quantity = 40, + Discount = 0, + QuantityPerUnit = "24 - 12 oz bottles", + UnitPrice = 14.0, + CategoryName = "Beverages", + CategoryDescription = "Soft drinks, coffees, teas, beers, and ales", + Total = 560.00 + }, + new SampleOrderDetail() + { + ProductID = 53, + ProductName = "Perth Pasties", + Quantity = 25, + Discount = 0, + QuantityPerUnit = "48 pieces", + UnitPrice = 32.80, + CategoryName = "Meat/Poultry", + CategoryDescription = "Prepared meats", + Total = 820.00 + } + } + }, + new SampleOrder() + { + OrderID = 10682, // Symbol Home + OrderDate = new DateTime(1997, 9, 25), + RequiredDate = new DateTime(1997, 10, 23), + ShippedDate = new DateTime(1997, 10, 1), + ShipperName = "United Package", + ShipperPhone = "(503) 555-3199", + Freight = 36.13, + Company = "Company Z", + ShipTo = "Company Z, Mataderos 2312, 05023, México D.F., Mexico", + OrderTotal = 375.50, + Status = "Closed", + SymbolCode = 57615, + SymbolName = "Home", + Details = new List() + { + new SampleOrderDetail() + { + ProductID = 33, + ProductName = "Geitost", + Quantity = 30, + Discount = 0, + QuantityPerUnit = "500 g", + UnitPrice = 2.50, + CategoryName = "Dairy Products", + CategoryDescription = "Cheeses", + Total = 75.00 + }, + new SampleOrderDetail() + { + ProductID = 66, + ProductName = "Louisiana Hot Spiced Okra", + Quantity = 4, + Discount = 0, + QuantityPerUnit = "24 - 8 oz jars", + UnitPrice = 17.00, + CategoryName = "Condiments", + CategoryDescription = "Sweet and savory sauces, relishes, spreads, and seasonings", + Total = 68.00 + }, + new SampleOrderDetail() + { + ProductID = 75, + ProductName = "Rhönbräu Klosterbier", + Quantity = 30, + Discount = 0, + QuantityPerUnit = "24 - 0.5 l bottles", + UnitPrice = 7.75, + CategoryName = "Beverages", + CategoryDescription = "Soft drinks, coffees, teas, beers, and ales", + Total = 232.50 + } + } + } + } + } + }; + } + + public async Task> GetContentGridDataAsync() + { + _allOrders ??= new List(AllOrders()); + + await Task.CompletedTask; + return _allOrders; + } + + public async Task> GetGridDataAsync() + { + _allOrders ??= new List(AllOrders()); + + await Task.CompletedTask; + return _allOrders; + } + + public async Task> GetListDetailsDataAsync() + { + _allOrders ??= new List(AllOrders()); + + await Task.CompletedTask; + return _allOrders; + } +} diff --git a/Estara.sln b/Estara.sln index e4767f5..3588505 100644 --- a/Estara.sln +++ b/Estara.sln @@ -1,25 +1,69 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.6.33712.159 +VisualStudioVersion = 17.6.33801.468 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Estara", "Estara\Estara.csproj", "{D0431BAC-E1B7-445F-B722-6AFF8E91C9D9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Estara", "Estara\Estara.csproj", "{E8437770-A04F-4C8B-A620-4FCC9399D242}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Estara.Core", "Estara.Core\Estara.Core.csproj", "{6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|arm64 = Debug|arm64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|arm64 = Release|arm64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D0431BAC-E1B7-445F-B722-6AFF8E91C9D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D0431BAC-E1B7-445F-B722-6AFF8E91C9D9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D0431BAC-E1B7-445F-B722-6AFF8E91C9D9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D0431BAC-E1B7-445F-B722-6AFF8E91C9D9}.Release|Any CPU.Build.0 = Release|Any CPU + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Debug|Any CPU.ActiveCfg = Debug|x64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Debug|Any CPU.Build.0 = Debug|x64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Debug|Any CPU.Deploy.0 = Debug|x64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Debug|arm64.ActiveCfg = Debug|arm64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Debug|arm64.Build.0 = Debug|arm64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Debug|arm64.Deploy.0 = Debug|arm64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Debug|x64.ActiveCfg = Debug|x64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Debug|x64.Build.0 = Debug|x64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Debug|x64.Deploy.0 = Debug|x64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Debug|x86.ActiveCfg = Debug|x86 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Debug|x86.Build.0 = Debug|x86 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Debug|x86.Deploy.0 = Debug|x86 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Release|Any CPU.ActiveCfg = Release|x64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Release|Any CPU.Build.0 = Release|x64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Release|Any CPU.Deploy.0 = Release|x64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Release|arm64.ActiveCfg = Release|arm64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Release|arm64.Build.0 = Release|arm64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Release|arm64.Deploy.0 = Release|arm64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Release|x64.ActiveCfg = Release|x64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Release|x64.Build.0 = Release|x64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Release|x64.Deploy.0 = Release|x64 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Release|x86.ActiveCfg = Release|x86 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Release|x86.Build.0 = Release|x86 + {E8437770-A04F-4C8B-A620-4FCC9399D242}.Release|x86.Deploy.0 = Release|x86 + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Debug|arm64.ActiveCfg = Debug|arm64 + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Debug|arm64.Build.0 = Debug|arm64 + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Debug|x64.ActiveCfg = Debug|x64 + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Debug|x64.Build.0 = Debug|x64 + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Debug|x86.ActiveCfg = Debug|x86 + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Debug|x86.Build.0 = Debug|x86 + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Release|Any CPU.Build.0 = Release|Any CPU + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Release|arm64.ActiveCfg = Release|arm64 + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Release|arm64.Build.0 = Release|arm64 + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Release|x64.ActiveCfg = Release|x64 + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Release|x64.Build.0 = Release|x64 + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Release|x86.ActiveCfg = Release|x86 + {6FC83ED3-8060-4415-9B1C-1B7DA39D8AD4}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {7F20239B-CBBB-44CE-8DF3-ACFEBFA8D9EB} + SolutionGuid = {D817C172-AEE1-4E13-8630-4A8877D56C71} EndGlobalSection EndGlobal diff --git a/Estara/Activation/ActivationHandler.cs b/Estara/Activation/ActivationHandler.cs new file mode 100644 index 0000000..e190487 --- /dev/null +++ b/Estara/Activation/ActivationHandler.cs @@ -0,0 +1,17 @@ +namespace Estara.Activation; + +// Extend this class to implement new ActivationHandlers. See DefaultActivationHandler for an example. +// https://github.com/microsoft/TemplateStudio/blob/main/docs/WinUI/activation.md +public abstract class ActivationHandler : IActivationHandler + where T : class +{ + // Override this method to add the logic for whether to handle the activation. + protected virtual bool CanHandleInternal(T args) => true; + + // Override this method to add the logic for your activation handler. + protected abstract Task HandleInternalAsync(T args); + + public bool CanHandle(object args) => args is T && CanHandleInternal((args as T)!); + + public async Task HandleAsync(object args) => await HandleInternalAsync((args as T)!); +} diff --git a/Estara/Activation/AppNotificationActivationHandler.cs b/Estara/Activation/AppNotificationActivationHandler.cs new file mode 100644 index 0000000..88f12a5 --- /dev/null +++ b/Estara/Activation/AppNotificationActivationHandler.cs @@ -0,0 +1,51 @@ +using Estara.Contracts.Services; +using Estara.ViewModels; + +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.Windows.AppLifecycle; +using Microsoft.Windows.AppNotifications; + +namespace Estara.Activation; + +public class AppNotificationActivationHandler : ActivationHandler +{ + private readonly INavigationService _navigationService; + private readonly IAppNotificationService _notificationService; + + public AppNotificationActivationHandler(INavigationService navigationService, IAppNotificationService notificationService) + { + _navigationService = navigationService; + _notificationService = notificationService; + } + + protected override bool CanHandleInternal(LaunchActivatedEventArgs args) + { + return AppInstance.GetCurrent().GetActivatedEventArgs()?.Kind == ExtendedActivationKind.AppNotification; + } + + protected async override Task HandleInternalAsync(LaunchActivatedEventArgs args) + { + // TODO: Handle notification activations. + + //// // Access the AppNotificationActivatedEventArgs. + //// var activatedEventArgs = (AppNotificationActivatedEventArgs)AppInstance.GetCurrent().GetActivatedEventArgs().Data; + + //// // Navigate to a specific page based on the notification arguments. + //// if (_notificationService.ParseArguments(activatedEventArgs.Argument)["action"] == "Settings") + //// { + //// // Queue navigation with low priority to allow the UI to initialize. + //// App.MainWindow.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => + //// { + //// _navigationService.NavigateTo(typeof(SettingsViewModel).FullName!); + //// }); + //// } + + App.MainWindow.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => + { + App.MainWindow.ShowMessageDialogAsync("TODO: Handle notification activations.", "Notification Activation"); + }); + + await Task.CompletedTask; + } +} diff --git a/Estara/Activation/DefaultActivationHandler.cs b/Estara/Activation/DefaultActivationHandler.cs new file mode 100644 index 0000000..a3ac189 --- /dev/null +++ b/Estara/Activation/DefaultActivationHandler.cs @@ -0,0 +1,29 @@ +using Estara.Contracts.Services; +using Estara.ViewModels; + +using Microsoft.UI.Xaml; + +namespace Estara.Activation; + +public class DefaultActivationHandler : ActivationHandler +{ + private readonly INavigationService _navigationService; + + public DefaultActivationHandler(INavigationService navigationService) + { + _navigationService = navigationService; + } + + protected override bool CanHandleInternal(LaunchActivatedEventArgs args) + { + // None of the ActivationHandlers has handled the activation. + return _navigationService.Frame?.Content == null; + } + + protected async override Task HandleInternalAsync(LaunchActivatedEventArgs args) + { + _navigationService.NavigateTo(typeof(MainViewModel).FullName!, args.Arguments); + + await Task.CompletedTask; + } +} diff --git a/Estara/Activation/IActivationHandler.cs b/Estara/Activation/IActivationHandler.cs new file mode 100644 index 0000000..1030c5a --- /dev/null +++ b/Estara/Activation/IActivationHandler.cs @@ -0,0 +1,8 @@ +namespace Estara.Activation; + +public interface IActivationHandler +{ + bool CanHandle(object args); + + Task HandleAsync(object args); +} diff --git a/Estara/App.xaml b/Estara/App.xaml index 57f310e..988b7a8 100644 --- a/Estara/App.xaml +++ b/Estara/App.xaml @@ -1,9 +1,15 @@ - + - + + + + + + + + diff --git a/Estara/App.xaml.cs b/Estara/App.xaml.cs index 9d390f6..29bb90a 100644 --- a/Estara/App.xaml.cs +++ b/Estara/App.xaml.cs @@ -1,17 +1,115 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; +using Estara.Activation; +using Estara.Contracts.Services; +using Estara.Core.Contracts.Services; +using Estara.Core.Services; +using Estara.Helpers; +using Estara.Models; +using Estara.Notifications; +using Estara.Services; +using Estara.ViewModels; +using Estara.Views; -namespace Estara +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.UI.Xaml; + +namespace Estara; + +// To learn more about WinUI 3, see https://docs.microsoft.com/windows/apps/winui/winui3/. +public partial class App : Application { - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application + // The .NET Generic Host provides dependency injection, configuration, logging, and other services. + // https://docs.microsoft.com/dotnet/core/extensions/generic-host + // https://docs.microsoft.com/dotnet/core/extensions/dependency-injection + // https://docs.microsoft.com/dotnet/core/extensions/configuration + // https://docs.microsoft.com/dotnet/core/extensions/logging + public IHost Host { + get; + } + + public static T GetService() + where T : class + { + if ((App.Current as App)!.Host.Services.GetService(typeof(T)) is not T service) + { + throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs."); + } + + return service; + } + + public static WindowEx MainWindow { get; } = new MainWindow(); + + public static UIElement? AppTitlebar { get; set; } + + public App() + { + InitializeComponent(); + + Host = Microsoft.Extensions.Hosting.Host. + CreateDefaultBuilder(). + UseContentRoot(AppContext.BaseDirectory). + ConfigureServices((context, services) => + { + // Default Activation Handler + services.AddTransient, DefaultActivationHandler>(); + + // Other Activation Handlers + services.AddTransient(); + + // Services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Core Services + services.AddSingleton(); + services.AddSingleton(); + + // Views and ViewModels + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Configuration + services.Configure(context.Configuration.GetSection(nameof(LocalSettingsOptions))); + }). + Build(); + + App.GetService().Initialize(); + + UnhandledException += App_UnhandledException; + } + + private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) + { + // TODO: Log and handle exceptions as appropriate. + // https://docs.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.application.unhandledexception. + } + + protected async override void OnLaunched(LaunchActivatedEventArgs args) + { + base.OnLaunched(args); + + App.GetService().Show(string.Format("AppNotificationSamplePayload".GetLocalized(), AppContext.BaseDirectory)); + + await App.GetService().ActivateAsync(args); } } diff --git a/Estara/AssemblyInfo.cs b/Estara/AssemblyInfo.cs deleted file mode 100644 index 8b5504e..0000000 --- a/Estara/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Windows; - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] diff --git a/Estara/Assets/LockScreenLogo.scale-200.png b/Estara/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..735f57adb5dfc01886d137b4e493d7e97cf13af3 GIT binary patch literal 1430 zcmaJ>TTC2P7~aKltDttVHYH6u8Io4i*}3fO&d$gd*bA_<3j~&e7%8(eXJLfhS!M@! zKrliY>>6yT4+Kr95$!DoD(Qn-5TP|{V_KS`k~E6(LGS@#`v$hQo&^^BKsw3HIsZBT z_y6C2n`lK@apunKojRQ^(_P}Mgewt$(^BBKCTZ;*xa?J3wQ7~@S0lUvbcLeq1Bg4o zH-bvQi|wt~L7q$~a-gDFP!{&TQfc3fX*6=uHv* zT&1&U(-)L%Xp^djI2?~eBF2cxC@YOP$+9d?P&h?lPy-9M2UT9fg5jKm1t$m#iWE{M zIf%q9@;fyT?0UP>tcw-bLkz;s2LlKl2qeP0w zECS7Ate+Awk|KQ+DOk;fl}Xsy4o^CY=pwq%QAAKKl628_yNPsK>?A>%D8fQG6IgdJ ztnxttBz#NI_a@fk7SU`WtrpsfZsNs9^0(2a z@C3#YO3>k~w7?2hipBf{#b6`}Xw1hlG$yi?;1dDs7k~xDAw@jiI*+tc;t2Lflg&bM)0!Y;0_@=w%`LW^8DsYpS#-bLOklX9r?Ei}TScw|4DbpW%+7 zFgAI)f51s}{y-eWb|vrU-Ya!GuYKP)J7z#*V_k^Xo>4!1Yqj*m)x&0L^tg3GJbVAJ zJ-Pl$R=NAabouV=^z_t;^K*0AvFs!vYU>_<|I^#c?>>CR<(T?=%{;U=aI*SbZADLH z&(f2wz_Y0??Tf|g;?|1Znw6}6U43Q#qNRwv1vp9uFn1)V#*4p&%$mP9x&15^OaBiDS(XppT|z^>;B{PLVEbS3IFYV yGvCsSX*m literal 0 HcmV?d00001 diff --git a/Estara/Assets/SplashScreen.scale-200.png b/Estara/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..023e7f1feda78d5100569825acedfd213a0d84e9 GIT binary patch literal 7700 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 literal 0 HcmV?d00001 diff --git a/Estara/Assets/Square150x150Logo.scale-200.png b/Estara/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..af49fec1a5484db1d52a7f9b5ec90a27c7030186 GIT binary patch literal 2937 zcma)84OCO-8BSud5)jwMLRVKgX(S?$n?Ld|vrsm<$CF7)&zTbyy1FE5bU`Q17MRv`9ue$;R(@8kR;#vJ*IM0>cJIAOte!d7oRgdH zd%ySjdB6L9=gX^A6)VzH7p2l@v~3zJAMw|DFy#^)F@@F*`mqUn=Il>l)8_+ab;nOW{%+iPx z+s{Eu|&pIs)Z7{La9~?xKfyl z#43?gjEL15d4WbOZo#SiP%>DB^+BcnJ=7dHEe;r#G=tuw|ka z%q@}##Uh7;tc%L_64m(kHtw74ty%BJMb)_1)#S0j`)F8_1jF7vScpsnH=0V19bO8y zR`0SjIdCUo&=>JwMQF8KHA<{ODHTiQh}0^@5QRmCA?gOH6_H3K^-_sNB^RrdNuK-R zOO*vOrKCVvDwgUck`kF(E7j{I#iiN;b*ZdCt4m@HPA`EuEqGGf4%!K<;(=I=&Vyrw z%TwcWtxa}8mCZ%Cyf&ActJ6_$ox5z6-D!0-dvnRx6t7y3d+h6QYpKWO;8OdnvERo7 zuEf>ih5`wqY)~o@OeVt-wM?Q!>QzdGRj!bz6fzYrfw$hZfAKzr2-M+D+R>}~oT574c;_3zquHcElqKIsryILt3g8n3jcMb+j?i?-L3FpZJ z2WRVBRdDPc+G5aaYg#5hpE+6nQ|(VSoxT3|biF;BUq#==-27Xi=gihDPYP$7?=9cP zYKE$jeQ|3~_L0VG-(F~2ZPyD0=k{J4Q~h(t__{-mz_w8{JDY9{`1ouzz!Vr5!ECdE z6U~O1k8c}24V7~zzXWTV-Pe4)y}wQJS&q%H5`Fo_f_JvIU489aCX$;P`u#!I-=^4ijC2{&9!O&h>mi?9oYD=GC#%)6{GzN6nQYw+Fal50!#x^asjBBR50i`+mho*ttoqV)ubM2KD9S~k7+FR4>{29?6 z{!l6kDdyTN0YJ9LgkPWeXm|gyi@zM3?0@{&pXT12w|78&W-q!RRF)&iLCEZVH<|fR zN0fr2^t8H(>L?>K#>^+jWROLral(Qy-xoBq1U7A&DV||wClb)Otd9?(gZ|8znMF}D zf<1haWz^s0qgecz;RFGt0C-B4g`jNGHsFU+;{<%t65v^sjk^h$lmWn#B0#_)9ij&d z-~lc`A)YYExi^7sBuPM^Y|wA2g*5?`K?#7tzELQYNxGo$UB$4J8RJp1k(8Jj+~hMT zlN~>M@KTTh^--8y3PK_NZ@AC!{PT=CziBzGd+wTJ^@icH!Bd}%)g8V)%K?|c&WTUk zy}qv1C%(fjRoZ4ozC3{O%@5?)XzH35zHns$pgU*Q?fj4v?fp1Qbm+j;3l;9jam9Da zXVcKjPlQ73x78QPu|Ffm6x?`~e3oD=gl=4kYK?={kD5j~QCXU)`HSdduNNENzA*2$ zOm3PzF!lN5e*06-f1Uot67wY#{o-S1!KZ7E=!~7ynnk9_iJR#kFoNbAOT#^2Gd17F zMmvU6>lndZQGd|ax9kUoXXO+$N?|j@6qpsF&_j7YXvwo_C{JpmLw5&#e6k>atv%es z5)7r*Wvv_JkUpT}M!_o!nVlEk1Zbl=a*2hQ*<|%*K1Glj^FcF`6kTzGQ3lz~2tCc@ z&x|tj;aH&1&9HwcJBcT`;{?a+pnej;M1HO(6Z{#J!cZA04hnFl;NXA+&`=7bjW_^o zfC40u3LMG?NdPtwGl>Tq6u}*QG)}-y;)lu-_>ee3kibW(69n0$0Zy!}9rQz%*v1iO zT9_H>99yIrSPYVy6^);rR}7Yo=J_T@hi+qhTZXnVWyf;JDYm5#eYLTxr*?kiNn!+Y zQ+LUkBafNJ#rH#C(?d5^;gw9o#%daEI{mA*LHPIHPU`#|H$hD zwm>0&+kahQ)E#%~k>&5@&#Vg82H?s%71=)(soi@174pi9--2{w{1$}Sz4zGn3Du&x bht0Iza^2ykEt4(epJ78uh5nDlX8(TxzDYwP literal 0 HcmV?d00001 diff --git a/Estara/Assets/Square44x44Logo.scale-200.png b/Estara/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..ce342a2ec8a61291ba76c54604aea7e9d20af11b GIT binary patch literal 1647 zcmaJ?eM}Q)7(e+G1Q(|`V9JhTI2>MkceK4;p;PR&$Pi?ejk3YQ_3o`S&|W_dsOZ8# zWPTt69g`t$ab`0cj-Y0yiBSOqmd)tG7G(}M5aP0_%&9TijB#&)I{zSE^4@#z^FF`l z`8{8`o%wlL(UI|y2!cdsuVamHH~H86F!*-15em4)NqUpCQM5?aoC_eCf@lV4wvF2a zjDQn1JBL69f&@2M3rvzJcfE!eZ8FZUBlFlC5RD)it33{mF9#B82AiyQE%w)`vlwa> zv{<1sm&kSKK$&%2jSFn7$t&P%%6Ue>R=EAnG8N7fqynWG8L3p!4801a;8{+nliO(qd(jNJ_?+9W3#hLIDLoT6~3fx9=`CC-D}-AMrpEO7HK zt3$GicGPc?GmDjy7K2P@La;eu4!$zWCZ`ym{Z$b zu-O6RM&K4JT|BIZB`E-gxqG%FzanI#+2FFmqHqXG7yxWB=w55RGOM)$xMb(>kSNR z2w=1AZi%z=AmG~yea~XaXJR!v7vLn(RUnELfiB1|6D84ICOS}^Zo2AdN}<&*h}G_u z{xZ!(%>tLT3J3<5XhWy-tg+6)0nmUUENLW8TWA{R6bgVd3X;anYFZ^IRis*_P-C-r z;i>%1^eL3UI2-{w8nuFFcs0e~7J{O2k^~Ce%+Ly4U?|=!0LH=t6()xi<^I-rs+9sF z*q{E-CxZbGPeu#a;XJwE;9S1?#R&uns>^0G3p`hEUF*v`M?@h%T%J%RChmD|EVydq zmHWh*_=S%emRC*mhxaVLzT@>Z2SX0u9v*DIJ@WC^kLVdlGV6LpK$KIrlJqc zpJ921)+3JJdTx|<`G&kXpKkjGJv=76R`yYIQ{#c-`%+`#V(7}Q;&@6U8!Td1`d;?N z_9mnI#?AA}4J!r)LN4!E-@H5eXauuB7TOawS>Y|{-P?NNx-lq+z1W-+y(;39P&&LP zL{N80?&=C*qKmdA^moMZRuPcD!B<*mq$ch=0Cnlitw#txRWhb3%TQvPqjkC`F69G4b! ze7z9MZ#+;_#l?H37UqUhDFb^l&s2{oM$3I0o^Q!yx;;V)QmCMo)Tb_ui|mit8MS?U zm##6$sZZ1$@|s%?l@>4Z<*Q}sRBSKMhb4I{e5LdEhsHIHTe8Bod5c>6QtT>$XgUBz z6MK`kO$=jmt@FqggOhJ5j~e@ygRbG;<{Vu)*+nn9aQeo0;$#j;|MS=S$&L?BeV25z xs3B`@=#`5TF{^6(A1rvdY@|-RtQ|iS5{tyX+wH?;n8E)G$kykv-D^wh{{!TZT%7;_ literal 0 HcmV?d00001 diff --git a/Estara/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Estara/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000000000000000000000000000000000..f6c02ce97e0a802b85f6021e822c89f8bf57d5cd GIT binary patch literal 1255 zcmaJ>TWs4@7*5+{G#S+&C!qC#> zf>5N3P6jO*Cz>ug*(_DmW=)kea&m$gZ^+nyiF`;j%w@}y8)>p*SH}C`m?DXeieF2U zyQHecc_L%Gh!7GMt+hG06y;+|p4>m~}PjA}rKViGiEnn7G0ZO<>G|7q;2?NwGCM3s?eued6%hd$B+ z*kQJ{#~$S=DFE(%=E+UkmlEI*%3llUf~8Ja9YU1Vui0IbGBkW_gHB%Rd&!!ioX zs40O?i9I{};kle7GMvE7(rk`la=gTI)47=>%?q@^iL-nUo3}h4S}N-KHn8t5mVP8w z&bSErwp+37 zNJJ8?a|{r5Q3R0Z5s-LB1WHOwYC@7pCHWND#cL1cZ?{kJ368_*(UDWUDyb<}0y@o# zfMF016iMWPCb6obAxT$JlB6(2DrlXDTB&!0`!m??4F(qWMhjVZo?JXQmz`1*58Z=& zcDmB|S-E@j?BoFGix0flckqdS4jsPNzhfWyWIM98GxcLs89C(~dw%$_t;JjX-SD}E zfiGV;{8Q%8r}w9x>EEigW81>`kvnU@pK)4+xk9@+bNj9L!AAZ@SZ@q|)&BmY3+HZx zul~BeG4|}-;L%cHViQGQX?^zFfO0&#cHwel=d`lH9sJ-@Sl@n*(8J2>%Ac`IxyY?Q z{=GhWvC#gu-~Ia7*n{=+;qM?Ul_wy1+u7ho;=`>EwP^g~R@{unBds`!#@}tluZQpS zm)M~nYEifJWJGx?_6DcTy>#uh%>!H9=hb^(v`=m3F1{L>db=<5_tm+_&knAQ2EU$s Mu9UqpbNZeC0BbUo^Z)<= literal 0 HcmV?d00001 diff --git a/Estara/Assets/StoreLogo.png b/Estara/Assets/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..7385b56c0e4d3c6b0efe3324aa1194157d837826 GIT binary patch literal 1451 zcmaJ>eN5D57_Z|bH;{0+1#mbl)eTU3{h)Wf7EZV?;HD@XL@{B`Ui%(2aMxQ~xdXSv z5nzWi(LW)U2=Vc-cY@s7nPt{i0hc6!7xN4NNHI#EQl>YNBy8l4%x9gr_W-j zEZMQmmTIy(>;lblRfh`dIyTgc9W5d!VP$L4(kKrN1c5G~(O_#xG zAJCNTstD^5SeXFB+&$h=ToJP2H>xr$iqPs-#O*;4(!Fjw25-!gEb*)mU}=)J;Iu>w zxK(5XoD0wrPSKQ~rbL^Cw6O_03*l*}i=ydbu7adJ6y;%@tjFeXIXT+ms30pmbOP%Q zX}S;+LBh8Tea~TSkHzvX6$rYb)+n&{kSbIqh|c7hmlxmwSiq5iVhU#iEQ<>a18|O^Sln-8t&+t`*{qBWo5M?wFM(JuimAOb5!K#D}XbslM@#1ZVz_;!9U zpfEpLAOz=0g@bd6Xj_ILi-x^!M}73h^o@}hM$1jflTs|Yuj9AL@A3<-?MV4!^4q`e z)fO@A;{9K^?W?DbnesnPr6kK>$zaKo&;FhFd(GYFCIU^T+OIMb%Tqo+P%oq(IdX7S zf6+HLO?7o0m+p>~Tp5UrXWh!UH!wZ5kv!E`_w)PTpI(#Iw{AS`gH4^b(bm^ZCq^FZ zY9DD7bH}rq9mg88+KgA$Zp!iWncuU2n1AuIa@=sWvUR-s`Qb{R*kk(SPU^`$6BXz8 zn#7yaFOIK%qGxyi`dYtm#&qqox0$h=pNi#u=M8zUG@bpiZ=3sT=1}Trr}39cC)H|v zbL?W)=&s4zrh)7>L(|cc%$1#!zfL?HjpeP%T+x_a+jZ16b^iKOHxFEX$7d|8${H-* zIrOJ5w&i$>*D>AKaIoYg`;{L@jM((Kt?$N$5OnuPqVvq**Nm}(f0wwOF%iX_Pba;V z;m@wxX&NcV3?<1+u?A{y_DIj7#m3Af1rCE)o`D&Y3}0%7E;iX1yMDiS)sh0wKi!36 zL!Wmq?P^Ku&rK~HJd97KkLTRl>ScGFYZNlYytWnhmuu|)L&ND8_PmkayQb{HOY640 bno1(wj@u8DCVuFR|31B*4ek@pZJqxCDDe1x literal 0 HcmV?d00001 diff --git a/Estara/Assets/Wide310x150Logo.scale-200.png b/Estara/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..288995b397fdbef1fb7e85afd71445d5de1952c5 GIT binary patch literal 3204 zcmbVPeQXow8NYmBd90>}0NP?GhXW~VaeThm=a0tV#EwJMI!)6M3}|c4_Bl3=Kd>G0 z(GHx1wl<7(tP?FsOQkTilSo*iIvF%uArExJ73~P zSv1xEy!U(Wd4A9D`FQV@W3@F^qJ@PEF$@z`Z!*BbFsS(^?B zyiAzJ+q})bkgiQHWqEb*jJD-coHYr1^iocg)l!Qa{Xqs-l~6J}p-|##ZHYofskQ3$ zI0;xzXyhazBeXhIsg5A=%ufo@f)1yy&ScKS0;HF^!r_2UE^lpZEom(+@duma3awTv zCrCL-%D_SvYWIcdHkmI}#50(fkUi)Qgx!80ju>g1za^}ff>JI8Z@^-iCiaCgg@TgF z+vtE?Q9{VQUX&MW9SYYmGcxA14%N2@7FwBTD4N<(2{nWgV8$e3?-F=L^&FrtWn~(U_Q~~^uYiyeY6-KoTnfh9AWz@ zIKje0)u!_Lw)E}G!#kEfwKVdNt(UAf9*f>tEL_(=xco-T%jTi@7YlC3hs2ik%Le0H ztj}RTeCF(5mwvi3_56>-yB?l;J>-1%!9~=fs|QcNG3J~a@JCu`4SB460s0ZO+##4fFUSGLcj_ja^fL4&BKALfb#$6$O?>P@qx2Agl^x0i&ugt zsy5Pyu=()`7HRMG3IB7F1@`_ z+-!J%#i6e^U$e#+C%Q>_qVRzWRsG^W_n+@OcX@vzI&z;mzHNb!GQ?LWA(wtpqHqTM z1OFw_{Zn?fD)p)`c`kOgv{de=v@suGRqY{N^U7gI1VF3*F=obwaXI6ob5__Yn zVTguS!%(NI09J8x#AO_aW!9W7k*UvB;IWDFC3srwftr{kHj%g)fvnAm;&h_dnl~

MY- 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 uP%O@+*Bs76h}X~NDhso;l*+SeDGRfulm&$ +{ + private static NavigationViewHeaderBehavior? _current; + + private Page? _currentPage; + + public DataTemplate? DefaultHeaderTemplate + { + get; set; + } + + public object DefaultHeader + { + get => GetValue(DefaultHeaderProperty); + set => SetValue(DefaultHeaderProperty, value); + } + + public static readonly DependencyProperty DefaultHeaderProperty = + DependencyProperty.Register("DefaultHeader", typeof(object), typeof(NavigationViewHeaderBehavior), new PropertyMetadata(null, (d, e) => _current!.UpdateHeader())); + + public static NavigationViewHeaderMode GetHeaderMode(Page item) => (NavigationViewHeaderMode)item.GetValue(HeaderModeProperty); + + public static void SetHeaderMode(Page item, NavigationViewHeaderMode value) => item.SetValue(HeaderModeProperty, value); + + public static readonly DependencyProperty HeaderModeProperty = + DependencyProperty.RegisterAttached("HeaderMode", typeof(bool), typeof(NavigationViewHeaderBehavior), new PropertyMetadata(NavigationViewHeaderMode.Always, (d, e) => _current!.UpdateHeader())); + + public static object GetHeaderContext(Page item) => item.GetValue(HeaderContextProperty); + + public static void SetHeaderContext(Page item, object value) => item.SetValue(HeaderContextProperty, value); + + public static readonly DependencyProperty HeaderContextProperty = + DependencyProperty.RegisterAttached("HeaderContext", typeof(object), typeof(NavigationViewHeaderBehavior), new PropertyMetadata(null, (d, e) => _current!.UpdateHeader())); + + public static DataTemplate GetHeaderTemplate(Page item) => (DataTemplate)item.GetValue(HeaderTemplateProperty); + + public static void SetHeaderTemplate(Page item, DataTemplate value) => item.SetValue(HeaderTemplateProperty, value); + + public static readonly DependencyProperty HeaderTemplateProperty = + DependencyProperty.RegisterAttached("HeaderTemplate", typeof(DataTemplate), typeof(NavigationViewHeaderBehavior), new PropertyMetadata(null, (d, e) => _current!.UpdateHeaderTemplate())); + + protected override void OnAttached() + { + base.OnAttached(); + + var navigationService = App.GetService(); + navigationService.Navigated += OnNavigated; + + _current = this; + } + + protected override void OnDetaching() + { + base.OnDetaching(); + + var navigationService = App.GetService(); + navigationService.Navigated -= OnNavigated; + } + + private void OnNavigated(object sender, NavigationEventArgs e) + { + if (sender is Frame frame && frame.Content is Page page) + { + _currentPage = page; + + UpdateHeader(); + UpdateHeaderTemplate(); + } + } + + private void UpdateHeader() + { + if (_currentPage != null) + { + var headerMode = GetHeaderMode(_currentPage); + if (headerMode == NavigationViewHeaderMode.Never) + { + AssociatedObject.Header = null; + AssociatedObject.AlwaysShowHeader = false; + } + else + { + var headerFromPage = GetHeaderContext(_currentPage); + if (headerFromPage != null) + { + AssociatedObject.Header = headerFromPage; + } + else + { + AssociatedObject.Header = DefaultHeader; + } + + if (headerMode == NavigationViewHeaderMode.Always) + { + AssociatedObject.AlwaysShowHeader = true; + } + else + { + AssociatedObject.AlwaysShowHeader = false; + } + } + } + } + + private void UpdateHeaderTemplate() + { + if (_currentPage != null) + { + var headerTemplate = GetHeaderTemplate(_currentPage); + AssociatedObject.HeaderTemplate = headerTemplate ?? DefaultHeaderTemplate; + } + } +} diff --git a/Estara/Behaviors/NavigationViewHeaderMode.cs b/Estara/Behaviors/NavigationViewHeaderMode.cs new file mode 100644 index 0000000..8a05706 --- /dev/null +++ b/Estara/Behaviors/NavigationViewHeaderMode.cs @@ -0,0 +1,8 @@ +namespace Estara.Behaviors; + +public enum NavigationViewHeaderMode +{ + Always, + Never, + Minimal +} diff --git a/Estara/Contracts/Services/IActivationService.cs b/Estara/Contracts/Services/IActivationService.cs new file mode 100644 index 0000000..f73c9af --- /dev/null +++ b/Estara/Contracts/Services/IActivationService.cs @@ -0,0 +1,6 @@ +namespace Estara.Contracts.Services; + +public interface IActivationService +{ + Task ActivateAsync(object activationArgs); +} diff --git a/Estara/Contracts/Services/IAppNotificationService.cs b/Estara/Contracts/Services/IAppNotificationService.cs new file mode 100644 index 0000000..d4d3921 --- /dev/null +++ b/Estara/Contracts/Services/IAppNotificationService.cs @@ -0,0 +1,14 @@ +using System.Collections.Specialized; + +namespace Estara.Contracts.Services; + +public interface IAppNotificationService +{ + void Initialize(); + + bool Show(string payload); + + NameValueCollection ParseArguments(string arguments); + + void Unregister(); +} diff --git a/Estara/Contracts/Services/ILocalSettingsService.cs b/Estara/Contracts/Services/ILocalSettingsService.cs new file mode 100644 index 0000000..441b9df --- /dev/null +++ b/Estara/Contracts/Services/ILocalSettingsService.cs @@ -0,0 +1,8 @@ +namespace Estara.Contracts.Services; + +public interface ILocalSettingsService +{ + Task ReadSettingAsync(string key); + + Task SaveSettingAsync(string key, T value); +} diff --git a/Estara/Contracts/Services/INavigationService.cs b/Estara/Contracts/Services/INavigationService.cs new file mode 100644 index 0000000..3b10bbb --- /dev/null +++ b/Estara/Contracts/Services/INavigationService.cs @@ -0,0 +1,25 @@ +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace Estara.Contracts.Services; + +public interface INavigationService +{ + event NavigatedEventHandler Navigated; + + bool CanGoBack + { + get; + } + + Frame? Frame + { + get; set; + } + + bool NavigateTo(string pageKey, object? parameter = null, bool clearNavigation = false); + + bool GoBack(); + + void SetListDataItemForNextConnectedAnimation(object item); +} diff --git a/Estara/Contracts/Services/INavigationViewService.cs b/Estara/Contracts/Services/INavigationViewService.cs new file mode 100644 index 0000000..e8ae2c3 --- /dev/null +++ b/Estara/Contracts/Services/INavigationViewService.cs @@ -0,0 +1,22 @@ +using Microsoft.UI.Xaml.Controls; + +namespace Estara.Contracts.Services; + +public interface INavigationViewService +{ + IList? MenuItems + { + get; + } + + object? SettingsItem + { + get; + } + + void Initialize(NavigationView navigationView); + + void UnregisterEvents(); + + NavigationViewItem? GetSelectedItem(Type pageType); +} diff --git a/Estara/Contracts/Services/IPageService.cs b/Estara/Contracts/Services/IPageService.cs new file mode 100644 index 0000000..48c7774 --- /dev/null +++ b/Estara/Contracts/Services/IPageService.cs @@ -0,0 +1,6 @@ +namespace Estara.Contracts.Services; + +public interface IPageService +{ + Type GetPageType(string key); +} diff --git a/Estara/Contracts/Services/IThemeSelectorService.cs b/Estara/Contracts/Services/IThemeSelectorService.cs new file mode 100644 index 0000000..a9cddad --- /dev/null +++ b/Estara/Contracts/Services/IThemeSelectorService.cs @@ -0,0 +1,17 @@ +using Microsoft.UI.Xaml; + +namespace Estara.Contracts.Services; + +public interface IThemeSelectorService +{ + ElementTheme Theme + { + get; + } + + Task InitializeAsync(); + + Task SetThemeAsync(ElementTheme theme); + + Task SetRequestedThemeAsync(); +} diff --git a/Estara/Contracts/ViewModels/INavigationAware.cs b/Estara/Contracts/ViewModels/INavigationAware.cs new file mode 100644 index 0000000..be87c2d --- /dev/null +++ b/Estara/Contracts/ViewModels/INavigationAware.cs @@ -0,0 +1,8 @@ +namespace Estara.Contracts.ViewModels; + +public interface INavigationAware +{ + void OnNavigatedTo(object parameter); + + void OnNavigatedFrom(); +} diff --git a/Estara/Estara.csproj b/Estara/Estara.csproj index 4106cb0..040424c 100644 --- a/Estara/Estara.csproj +++ b/Estara/Estara.csproj @@ -1,10 +1,60 @@  - WinExe - net6.0-windows - enable - true + net7.0-windows10.0.19041.0 + 10.0.17763.0 + Estara + Assets/WindowIcon.ico + app.manifest + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + Properties\PublishProfiles\win10-$(Platform).pubxml + enable + enable + true + true + true + True + True + 12A33CEAC330575CECF628E17569ECC1AA31D9A5 + http://timestamp.digicert.com + SHA256 + True + True + True + Never + https://kiseki.lol/estara + + + + + + + + + + + + + + + + + + + + + Always + + + + + + + + + true + diff --git a/Estara/Helpers/EnumToBooleanConverter.cs b/Estara/Helpers/EnumToBooleanConverter.cs new file mode 100644 index 0000000..319b76e --- /dev/null +++ b/Estara/Helpers/EnumToBooleanConverter.cs @@ -0,0 +1,38 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; + +namespace Estara.Helpers; + +public class EnumToBooleanConverter : IValueConverter +{ + public EnumToBooleanConverter() + { + } + + public object Convert(object value, Type targetType, object parameter, string language) + { + if (parameter is string enumString) + { + if (!Enum.IsDefined(typeof(ElementTheme), value)) + { + throw new ArgumentException("ExceptionEnumToBooleanConverterValueMustBeAnEnum"); + } + + var enumValue = Enum.Parse(typeof(ElementTheme), enumString); + + return enumValue.Equals(value); + } + + throw new ArgumentException("ExceptionEnumToBooleanConverterParameterMustBeAnEnumName"); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (parameter is string enumString) + { + return Enum.Parse(typeof(ElementTheme), enumString); + } + + throw new ArgumentException("ExceptionEnumToBooleanConverterParameterMustBeAnEnumName"); + } +} diff --git a/Estara/Helpers/FrameExtensions.cs b/Estara/Helpers/FrameExtensions.cs new file mode 100644 index 0000000..48e88ef --- /dev/null +++ b/Estara/Helpers/FrameExtensions.cs @@ -0,0 +1,8 @@ +using Microsoft.UI.Xaml.Controls; + +namespace Estara.Helpers; + +public static class FrameExtensions +{ + public static object? GetPageViewModel(this Frame frame) => frame?.Content?.GetType().GetProperty("ViewModel")?.GetValue(frame.Content, null); +} diff --git a/Estara/Helpers/NavigationHelper.cs b/Estara/Helpers/NavigationHelper.cs new file mode 100644 index 0000000..8f1c7f5 --- /dev/null +++ b/Estara/Helpers/NavigationHelper.cs @@ -0,0 +1,21 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Estara.Helpers; + +// Helper class to set the navigation target for a NavigationViewItem. +// +// Usage in XAML: +// +// +// Usage in code: +// NavigationHelper.SetNavigateTo(navigationViewItem, typeof(MainViewModel).FullName); +public class NavigationHelper +{ + public static string GetNavigateTo(NavigationViewItem item) => (string)item.GetValue(NavigateToProperty); + + public static void SetNavigateTo(NavigationViewItem item, string value) => item.SetValue(NavigateToProperty, value); + + public static readonly DependencyProperty NavigateToProperty = + DependencyProperty.RegisterAttached("NavigateTo", typeof(string), typeof(NavigationHelper), new PropertyMetadata(null)); +} diff --git a/Estara/Helpers/ResourceExtensions.cs b/Estara/Helpers/ResourceExtensions.cs new file mode 100644 index 0000000..1e04634 --- /dev/null +++ b/Estara/Helpers/ResourceExtensions.cs @@ -0,0 +1,10 @@ +using Microsoft.Windows.ApplicationModel.Resources; + +namespace Estara.Helpers; + +public static class ResourceExtensions +{ + private static readonly ResourceLoader _resourceLoader = new(); + + public static string GetLocalized(this string resourceKey) => _resourceLoader.GetString(resourceKey); +} diff --git a/Estara/Helpers/RuntimeHelper.cs b/Estara/Helpers/RuntimeHelper.cs new file mode 100644 index 0000000..676dc7b --- /dev/null +++ b/Estara/Helpers/RuntimeHelper.cs @@ -0,0 +1,20 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace Estara.Helpers; + +public class RuntimeHelper +{ + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder? packageFullName); + + public static bool IsMSIX + { + get + { + var length = 0; + + return GetCurrentPackageFullName(ref length, null) != 15700L; + } + } +} diff --git a/Estara/Helpers/SettingsStorageExtensions.cs b/Estara/Helpers/SettingsStorageExtensions.cs new file mode 100644 index 0000000..da2b3b7 --- /dev/null +++ b/Estara/Helpers/SettingsStorageExtensions.cs @@ -0,0 +1,112 @@ +using Estara.Core.Helpers; + +using Windows.Storage; +using Windows.Storage.Streams; + +namespace Estara.Helpers; + +// Use these extension methods to store and retrieve local and roaming app data +// More details regarding storing and retrieving app data at https://docs.microsoft.com/windows/apps/design/app-settings/store-and-retrieve-app-data +public static class SettingsStorageExtensions +{ + private const string FileExtension = ".json"; + + public static bool IsRoamingStorageAvailable(this ApplicationData appData) + { + return appData.RoamingStorageQuota == 0; + } + + public static async Task SaveAsync(this StorageFolder folder, string name, T content) + { + var file = await folder.CreateFileAsync(GetFileName(name), CreationCollisionOption.ReplaceExisting); + var fileContent = await Json.StringifyAsync(content); + + await FileIO.WriteTextAsync(file, fileContent); + } + + public static async Task ReadAsync(this StorageFolder folder, string name) + { + if (!File.Exists(Path.Combine(folder.Path, GetFileName(name)))) + { + return default; + } + + var file = await folder.GetFileAsync($"{name}.json"); + var fileContent = await FileIO.ReadTextAsync(file); + + return await Json.ToObjectAsync(fileContent); + } + + public static async Task SaveAsync(this ApplicationDataContainer settings, string key, T value) + { + settings.SaveString(key, await Json.StringifyAsync(value)); + } + + public static void SaveString(this ApplicationDataContainer settings, string key, string value) + { + settings.Values[key] = value; + } + + public static async Task ReadAsync(this ApplicationDataContainer settings, string key) + { + object? obj; + + if (settings.Values.TryGetValue(key, out obj)) + { + return await Json.ToObjectAsync((string)obj); + } + + return default; + } + + public static async Task SaveFileAsync(this StorageFolder folder, byte[] content, string fileName, CreationCollisionOption options = CreationCollisionOption.ReplaceExisting) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (string.IsNullOrEmpty(fileName)) + { + throw new ArgumentException("File name is null or empty. Specify a valid file name", nameof(fileName)); + } + + var storageFile = await folder.CreateFileAsync(fileName, options); + await FileIO.WriteBytesAsync(storageFile, content); + return storageFile; + } + + public static async Task ReadFileAsync(this StorageFolder folder, string fileName) + { + var item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false); + + if ((item != null) && item.IsOfType(StorageItemTypes.File)) + { + var storageFile = await folder.GetFileAsync(fileName); + var content = await storageFile.ReadBytesAsync(); + return content; + } + + return null; + } + + public static async Task ReadBytesAsync(this StorageFile file) + { + if (file != null) + { + using IRandomAccessStream stream = await file.OpenReadAsync(); + using var reader = new DataReader(stream.GetInputStreamAt(0)); + await reader.LoadAsync((uint)stream.Size); + var bytes = new byte[stream.Size]; + reader.ReadBytes(bytes); + return bytes; + } + + return null; + } + + private static string GetFileName(string name) + { + return string.Concat(name, FileExtension); + } +} diff --git a/Estara/Helpers/TitleBarHelper.cs b/Estara/Helpers/TitleBarHelper.cs new file mode 100644 index 0000000..a55047f --- /dev/null +++ b/Estara/Helpers/TitleBarHelper.cs @@ -0,0 +1,121 @@ +using System.Runtime.InteropServices; + +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; + +using Windows.UI; +using Windows.UI.ViewManagement; + +namespace Estara.Helpers; + +// Helper class to workaround custom title bar bugs. +// DISCLAIMER: The resource key names and color values used below are subject to change. Do not depend on them. +// https://github.com/microsoft/TemplateStudio/issues/4516 +internal class TitleBarHelper +{ + private const int WAINACTIVE = 0x00; + private const int WAACTIVE = 0x01; + private const int WMACTIVATE = 0x0006; + + [DllImport("user32.dll")] + private static extern IntPtr GetActiveWindow(); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam); + + public static void UpdateTitleBar(ElementTheme theme) + { + if (App.MainWindow.ExtendsContentIntoTitleBar) + { + if (theme == ElementTheme.Default) + { + var uiSettings = new UISettings(); + var background = uiSettings.GetColorValue(UIColorType.Background); + + theme = background == Colors.White ? ElementTheme.Light : ElementTheme.Dark; + } + + if (theme == ElementTheme.Default) + { + theme = Application.Current.RequestedTheme == ApplicationTheme.Light ? ElementTheme.Light : ElementTheme.Dark; + } + + Application.Current.Resources["WindowCaptionForeground"] = theme switch + { + ElementTheme.Dark => new SolidColorBrush(Colors.White), + ElementTheme.Light => new SolidColorBrush(Colors.Black), + _ => new SolidColorBrush(Colors.Transparent) + }; + + Application.Current.Resources["WindowCaptionForegroundDisabled"] = theme switch + { + ElementTheme.Dark => new SolidColorBrush(Color.FromArgb(0x66, 0xFF, 0xFF, 0xFF)), + ElementTheme.Light => new SolidColorBrush(Color.FromArgb(0x66, 0x00, 0x00, 0x00)), + _ => new SolidColorBrush(Colors.Transparent) + }; + + Application.Current.Resources["WindowCaptionButtonBackgroundPointerOver"] = theme switch + { + ElementTheme.Dark => new SolidColorBrush(Color.FromArgb(0x33, 0xFF, 0xFF, 0xFF)), + ElementTheme.Light => new SolidColorBrush(Color.FromArgb(0x33, 0x00, 0x00, 0x00)), + _ => new SolidColorBrush(Colors.Transparent) + }; + + Application.Current.Resources["WindowCaptionButtonBackgroundPressed"] = theme switch + { + ElementTheme.Dark => new SolidColorBrush(Color.FromArgb(0x66, 0xFF, 0xFF, 0xFF)), + ElementTheme.Light => new SolidColorBrush(Color.FromArgb(0x66, 0x00, 0x00, 0x00)), + _ => new SolidColorBrush(Colors.Transparent) + }; + + Application.Current.Resources["WindowCaptionButtonStrokePointerOver"] = theme switch + { + ElementTheme.Dark => new SolidColorBrush(Colors.White), + ElementTheme.Light => new SolidColorBrush(Colors.Black), + _ => new SolidColorBrush(Colors.Transparent) + }; + + Application.Current.Resources["WindowCaptionButtonStrokePressed"] = theme switch + { + ElementTheme.Dark => new SolidColorBrush(Colors.White), + ElementTheme.Light => new SolidColorBrush(Colors.Black), + _ => new SolidColorBrush(Colors.Transparent) + }; + + Application.Current.Resources["WindowCaptionBackground"] = new SolidColorBrush(Colors.Transparent); + Application.Current.Resources["WindowCaptionBackgroundDisabled"] = new SolidColorBrush(Colors.Transparent); + + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow); + if (hwnd == GetActiveWindow()) + { + SendMessage(hwnd, WMACTIVATE, WAINACTIVE, IntPtr.Zero); + SendMessage(hwnd, WMACTIVATE, WAACTIVE, IntPtr.Zero); + } + else + { + SendMessage(hwnd, WMACTIVATE, WAACTIVE, IntPtr.Zero); + SendMessage(hwnd, WMACTIVATE, WAINACTIVE, IntPtr.Zero); + } + } + } + + public static void ApplySystemThemeToCaptionButtons() + { + var res = Application.Current.Resources; + var frame = App.AppTitlebar as FrameworkElement; + if (frame != null) + { + if (frame.ActualTheme == ElementTheme.Dark) + { + res["WindowCaptionForeground"] = Colors.White; + } + else + { + res["WindowCaptionForeground"] = Colors.Black; + } + + UpdateTitleBar(frame.ActualTheme); + } + } +} diff --git a/Estara/MainWindow.xaml b/Estara/MainWindow.xaml index 4ebd003..d58e547 100644 --- a/Estara/MainWindow.xaml +++ b/Estara/MainWindow.xaml @@ -1,12 +1,16 @@ - - - - - + + + + + diff --git a/Estara/MainWindow.xaml.cs b/Estara/MainWindow.xaml.cs index 157c8ed..ca5b470 100644 --- a/Estara/MainWindow.xaml.cs +++ b/Estara/MainWindow.xaml.cs @@ -1,28 +1,37 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; +using Estara.Helpers; -namespace Estara +using Windows.UI.ViewManagement; + +namespace Estara; + +public sealed partial class MainWindow : WindowEx { - /// - /// Interaction logic for MainWindow.xaml - /// - public partial class MainWindow : Window + private Microsoft.UI.Dispatching.DispatcherQueue dispatcherQueue; + + private UISettings settings; + + public MainWindow() { - public MainWindow() + InitializeComponent(); + + AppWindow.SetIcon(Path.Combine(AppContext.BaseDirectory, "Assets/WindowIcon.ico")); + Content = null; + Title = "AppDisplayName".GetLocalized(); + + // Theme change code picked from https://github.com/microsoft/WinUI-Gallery/pull/1239 + dispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); + settings = new UISettings(); + settings.ColorValuesChanged += Settings_ColorValuesChanged; // cannot use FrameworkElement.ActualThemeChanged event + } + + // this handles updating the caption button colors correctly when indows system theme is changed + // while the app is open + private void Settings_ColorValuesChanged(UISettings sender, object args) + { + // This calls comes off-thread, hence we will need to dispatch it to current app's thread + dispatcherQueue.TryEnqueue(() => { - InitializeComponent(); - } + TitleBarHelper.ApplySystemThemeToCaptionButtons(); + }); } } diff --git a/Estara/Models/LocalSettingsOptions.cs b/Estara/Models/LocalSettingsOptions.cs new file mode 100644 index 0000000..5c73a70 --- /dev/null +++ b/Estara/Models/LocalSettingsOptions.cs @@ -0,0 +1,14 @@ +namespace Estara.Models; + +public class LocalSettingsOptions +{ + public string? ApplicationDataFolder + { + get; set; + } + + public string? LocalSettingsFile + { + get; set; + } +} diff --git a/Estara/Package.appinstaller b/Estara/Package.appinstaller new file mode 100644 index 0000000..c619457 --- /dev/null +++ b/Estara/Package.appinstaller @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/Estara/Package.appxmanifest b/Estara/Package.appxmanifest new file mode 100644 index 0000000..0016d7c --- /dev/null +++ b/Estara/Package.appxmanifest @@ -0,0 +1,76 @@ + + + + + + + + + + Estara + Kiseki + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Estara/Properties/launchsettings.json b/Estara/Properties/launchsettings.json new file mode 100644 index 0000000..4c3e54a --- /dev/null +++ b/Estara/Properties/launchsettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Estara (Package)": { + "commandName": "MsixPackage" + }, + "Estara (Unpackaged)": { + "commandName": "Project" + } + } +} diff --git a/Estara/README.md b/Estara/README.md new file mode 100644 index 0000000..e24cced --- /dev/null +++ b/Estara/README.md @@ -0,0 +1,27 @@ +*Recommended Markdown Viewer: [Markdown Editor](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.MarkdownEditor2)* + +## Getting Started + +Browse and address `TODO:` comments in `View -> Task List` to learn the codebase and understand next steps for turning the generated code into production code. + +Explore the [WinUI Gallery](https://www.microsoft.com/store/productId/9P3JFPWWDZRC) to learn about available controls and design patterns. + +Relaunch Template Studio to modify the project by right-clicking on the project in `View -> Solution Explorer` then selecting `Add -> New Item (Template Studio)`. + +## Publishing + +For projects with MSIX packaging, right-click on the application project and select `Package and Publish -> Create App Packages...` to create an MSIX package. + +For projects without MSIX packaging, follow the [deployment guide](https://docs.microsoft.com/windows/apps/windows-app-sdk/deploy-unpackaged-apps) or add the `Self-Contained` Feature to enable xcopy deployment. + +## CI Pipelines + +See [README.md](https://github.com/microsoft/TemplateStudio/blob/main/docs/WinUI/pipelines/README.md) for guidance on building and testing projects in CI pipelines. + +## Changelog + +See [releases](https://github.com/microsoft/TemplateStudio/releases) and [milestones](https://github.com/microsoft/TemplateStudio/milestones). + +## Feedback + +Bugs and feature requests should be filed at https://aka.ms/templatestudio. diff --git a/Estara/Services/ActivationService.cs b/Estara/Services/ActivationService.cs new file mode 100644 index 0000000..9171c17 --- /dev/null +++ b/Estara/Services/ActivationService.cs @@ -0,0 +1,72 @@ +using Estara.Activation; +using Estara.Contracts.Services; +using Estara.Views; + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Estara.Services; + +public class ActivationService : IActivationService +{ + private readonly ActivationHandler _defaultHandler; + private readonly IEnumerable _activationHandlers; + private readonly IThemeSelectorService _themeSelectorService; + private UIElement? _shell = null; + + public ActivationService(ActivationHandler defaultHandler, IEnumerable activationHandlers, IThemeSelectorService themeSelectorService) + { + _defaultHandler = defaultHandler; + _activationHandlers = activationHandlers; + _themeSelectorService = themeSelectorService; + } + + public async Task ActivateAsync(object activationArgs) + { + // Execute tasks before activation. + await InitializeAsync(); + + // Set the MainWindow Content. + if (App.MainWindow.Content == null) + { + _shell = App.GetService(); + App.MainWindow.Content = _shell ?? new Frame(); + } + + // Handle activation via ActivationHandlers. + await HandleActivationAsync(activationArgs); + + // Activate the MainWindow. + App.MainWindow.Activate(); + + // Execute tasks after activation. + await StartupAsync(); + } + + private async Task HandleActivationAsync(object activationArgs) + { + var activationHandler = _activationHandlers.FirstOrDefault(h => h.CanHandle(activationArgs)); + + if (activationHandler != null) + { + await activationHandler.HandleAsync(activationArgs); + } + + if (_defaultHandler.CanHandle(activationArgs)) + { + await _defaultHandler.HandleAsync(activationArgs); + } + } + + private async Task InitializeAsync() + { + await _themeSelectorService.InitializeAsync().ConfigureAwait(false); + await Task.CompletedTask; + } + + private async Task StartupAsync() + { + await _themeSelectorService.SetRequestedThemeAsync(); + await Task.CompletedTask; + } +} diff --git a/Estara/Services/AppNotificationService.cs b/Estara/Services/AppNotificationService.cs new file mode 100644 index 0000000..8401c0a --- /dev/null +++ b/Estara/Services/AppNotificationService.cs @@ -0,0 +1,71 @@ +using System.Collections.Specialized; +using System.Web; + +using Estara.Contracts.Services; +using Estara.ViewModels; + +using Microsoft.Windows.AppNotifications; + +namespace Estara.Notifications; + +public class AppNotificationService : IAppNotificationService +{ + private readonly INavigationService _navigationService; + + public AppNotificationService(INavigationService navigationService) + { + _navigationService = navigationService; + } + + ~AppNotificationService() + { + Unregister(); + } + + public void Initialize() + { + AppNotificationManager.Default.NotificationInvoked += OnNotificationInvoked; + + AppNotificationManager.Default.Register(); + } + + public void OnNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args) + { + // TODO: Handle notification invocations when your app is already running. + + //// // Navigate to a specific page based on the notification arguments. + //// if (ParseArguments(args.Argument)["action"] == "Settings") + //// { + //// App.MainWindow.DispatcherQueue.TryEnqueue(() => + //// { + //// _navigationService.NavigateTo(typeof(SettingsViewModel).FullName!); + //// }); + //// } + + App.MainWindow.DispatcherQueue.TryEnqueue(() => + { + App.MainWindow.ShowMessageDialogAsync("TODO: Handle notification invocations when your app is already running.", "Notification Invoked"); + + App.MainWindow.BringToFront(); + }); + } + + public bool Show(string payload) + { + var appNotification = new AppNotification(payload); + + AppNotificationManager.Default.Show(appNotification); + + return appNotification.Id != 0; + } + + public NameValueCollection ParseArguments(string arguments) + { + return HttpUtility.ParseQueryString(arguments); + } + + public void Unregister() + { + AppNotificationManager.Default.Unregister(); + } +} diff --git a/Estara/Services/LocalSettingsService.cs b/Estara/Services/LocalSettingsService.cs new file mode 100644 index 0000000..b49c2b6 --- /dev/null +++ b/Estara/Services/LocalSettingsService.cs @@ -0,0 +1,88 @@ +using Estara.Contracts.Services; +using Estara.Core.Contracts.Services; +using Estara.Core.Helpers; +using Estara.Helpers; +using Estara.Models; + +using Microsoft.Extensions.Options; + +using Windows.ApplicationModel; +using Windows.Storage; + +namespace Estara.Services; + +public class LocalSettingsService : ILocalSettingsService +{ + private const string _defaultApplicationDataFolder = "Estara/ApplicationData"; + private const string _defaultLocalSettingsFile = "LocalSettings.json"; + + private readonly IFileService _fileService; + private readonly LocalSettingsOptions _options; + + private readonly string _localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + private readonly string _applicationDataFolder; + private readonly string _localsettingsFile; + + private IDictionary _settings; + + private bool _isInitialized; + + public LocalSettingsService(IFileService fileService, IOptions options) + { + _fileService = fileService; + _options = options.Value; + + _applicationDataFolder = Path.Combine(_localApplicationData, _options.ApplicationDataFolder ?? _defaultApplicationDataFolder); + _localsettingsFile = _options.LocalSettingsFile ?? _defaultLocalSettingsFile; + + _settings = new Dictionary(); + } + + private async Task InitializeAsync() + { + if (!_isInitialized) + { + _settings = await Task.Run(() => _fileService.Read>(_applicationDataFolder, _localsettingsFile)) ?? new Dictionary(); + + _isInitialized = true; + } + } + + public async Task ReadSettingAsync(string key) + { + if (RuntimeHelper.IsMSIX) + { + if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out var obj)) + { + return await Json.ToObjectAsync((string)obj); + } + } + else + { + await InitializeAsync(); + + if (_settings != null && _settings.TryGetValue(key, out var obj)) + { + return await Json.ToObjectAsync((string)obj); + } + } + + return default; + } + + public async Task SaveSettingAsync(string key, T value) + { + if (RuntimeHelper.IsMSIX) + { + ApplicationData.Current.LocalSettings.Values[key] = await Json.StringifyAsync(value); + } + else + { + await InitializeAsync(); + + _settings[key] = await Json.StringifyAsync(value); + + await Task.Run(() => _fileService.Save(_applicationDataFolder, _localsettingsFile, _settings)); + } + } +} diff --git a/Estara/Services/NavigationService.cs b/Estara/Services/NavigationService.cs new file mode 100644 index 0000000..cfed7d5 --- /dev/null +++ b/Estara/Services/NavigationService.cs @@ -0,0 +1,130 @@ +using System.Diagnostics.CodeAnalysis; + +using CommunityToolkit.WinUI.UI.Animations; + +using Estara.Contracts.Services; +using Estara.Contracts.ViewModels; +using Estara.Helpers; + +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace Estara.Services; + +// For more information on navigation between pages see +// https://github.com/microsoft/TemplateStudio/blob/main/docs/WinUI/navigation.md +public class NavigationService : INavigationService +{ + private readonly IPageService _pageService; + private object? _lastParameterUsed; + private Frame? _frame; + + public event NavigatedEventHandler? Navigated; + + public Frame? Frame + { + get + { + if (_frame == null) + { + _frame = App.MainWindow.Content as Frame; + RegisterFrameEvents(); + } + + return _frame; + } + + set + { + UnregisterFrameEvents(); + _frame = value; + RegisterFrameEvents(); + } + } + + [MemberNotNullWhen(true, nameof(Frame), nameof(_frame))] + public bool CanGoBack => Frame != null && Frame.CanGoBack; + + public NavigationService(IPageService pageService) + { + _pageService = pageService; + } + + private void RegisterFrameEvents() + { + if (_frame != null) + { + _frame.Navigated += OnNavigated; + } + } + + private void UnregisterFrameEvents() + { + if (_frame != null) + { + _frame.Navigated -= OnNavigated; + } + } + + public bool GoBack() + { + if (CanGoBack) + { + var vmBeforeNavigation = _frame.GetPageViewModel(); + _frame.GoBack(); + if (vmBeforeNavigation is INavigationAware navigationAware) + { + navigationAware.OnNavigatedFrom(); + } + + return true; + } + + return false; + } + + public bool NavigateTo(string pageKey, object? parameter = null, bool clearNavigation = false) + { + var pageType = _pageService.GetPageType(pageKey); + + if (_frame != null && (_frame.Content?.GetType() != pageType || (parameter != null && !parameter.Equals(_lastParameterUsed)))) + { + _frame.Tag = clearNavigation; + var vmBeforeNavigation = _frame.GetPageViewModel(); + var navigated = _frame.Navigate(pageType, parameter); + if (navigated) + { + _lastParameterUsed = parameter; + if (vmBeforeNavigation is INavigationAware navigationAware) + { + navigationAware.OnNavigatedFrom(); + } + } + + return navigated; + } + + return false; + } + + private void OnNavigated(object sender, NavigationEventArgs e) + { + if (sender is Frame frame) + { + var clearNavigation = (bool)frame.Tag; + if (clearNavigation) + { + frame.BackStack.Clear(); + } + + if (frame.GetPageViewModel() is INavigationAware navigationAware) + { + navigationAware.OnNavigatedTo(e.Parameter); + } + + Navigated?.Invoke(sender, e); + } + } + + public void SetListDataItemForNextConnectedAnimation(object item) => Frame.SetListDataItemForNextConnectedAnimation(item); +} diff --git a/Estara/Services/NavigationViewService.cs b/Estara/Services/NavigationViewService.cs new file mode 100644 index 0000000..b56af09 --- /dev/null +++ b/Estara/Services/NavigationViewService.cs @@ -0,0 +1,103 @@ +using System.Diagnostics.CodeAnalysis; + +using Estara.Contracts.Services; +using Estara.Helpers; +using Estara.ViewModels; + +using Microsoft.UI.Xaml.Controls; + +namespace Estara.Services; + +public class NavigationViewService : INavigationViewService +{ + private readonly INavigationService _navigationService; + + private readonly IPageService _pageService; + + private NavigationView? _navigationView; + + public IList? MenuItems => _navigationView?.MenuItems; + + public object? SettingsItem => _navigationView?.SettingsItem; + + public NavigationViewService(INavigationService navigationService, IPageService pageService) + { + _navigationService = navigationService; + _pageService = pageService; + } + + [MemberNotNull(nameof(_navigationView))] + public void Initialize(NavigationView navigationView) + { + _navigationView = navigationView; + _navigationView.BackRequested += OnBackRequested; + _navigationView.ItemInvoked += OnItemInvoked; + } + + public void UnregisterEvents() + { + if (_navigationView != null) + { + _navigationView.BackRequested -= OnBackRequested; + _navigationView.ItemInvoked -= OnItemInvoked; + } + } + + public NavigationViewItem? GetSelectedItem(Type pageType) + { + if (_navigationView != null) + { + return GetSelectedItem(_navigationView.MenuItems, pageType) ?? GetSelectedItem(_navigationView.FooterMenuItems, pageType); + } + + return null; + } + + private void OnBackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args) => _navigationService.GoBack(); + + private void OnItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args) + { + if (args.IsSettingsInvoked) + { + _navigationService.NavigateTo(typeof(SettingsViewModel).FullName!); + } + else + { + var selectedItem = args.InvokedItemContainer as NavigationViewItem; + + if (selectedItem?.GetValue(NavigationHelper.NavigateToProperty) is string pageKey) + { + _navigationService.NavigateTo(pageKey); + } + } + } + + private NavigationViewItem? GetSelectedItem(IEnumerable menuItems, Type pageType) + { + foreach (var item in menuItems.OfType()) + { + if (IsMenuItemForPageType(item, pageType)) + { + return item; + } + + var selectedChild = GetSelectedItem(item.MenuItems, pageType); + if (selectedChild != null) + { + return selectedChild; + } + } + + return null; + } + + private bool IsMenuItemForPageType(NavigationViewItem menuItem, Type sourcePageType) + { + if (menuItem.GetValue(NavigationHelper.NavigateToProperty) is string pageKey) + { + return _pageService.GetPageType(pageKey) == sourcePageType; + } + + return false; + } +} diff --git a/Estara/Services/PageService.cs b/Estara/Services/PageService.cs new file mode 100644 index 0000000..feff383 --- /dev/null +++ b/Estara/Services/PageService.cs @@ -0,0 +1,60 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +using Estara.Contracts.Services; +using Estara.ViewModels; +using Estara.Views; + +using Microsoft.UI.Xaml.Controls; + +namespace Estara.Services; + +public class PageService : IPageService +{ + private readonly Dictionary _pages = new(); + + public PageService() + { + Configure(); + Configure(); + Configure(); + Configure(); + Configure(); + Configure(); + } + + public Type GetPageType(string key) + { + Type? pageType; + lock (_pages) + { + if (!_pages.TryGetValue(key, out pageType)) + { + throw new ArgumentException($"Page not found: {key}. Did you forget to call PageService.Configure?"); + } + } + + return pageType; + } + + private void Configure() + where VM : ObservableObject + where V : Page + { + lock (_pages) + { + var key = typeof(VM).FullName!; + if (_pages.ContainsKey(key)) + { + throw new ArgumentException($"The key {key} is already configured in PageService"); + } + + var type = typeof(V); + if (_pages.ContainsValue(type)) + { + throw new ArgumentException($"This type is already configured with key {_pages.First(p => p.Value == type).Key}"); + } + + _pages.Add(key, type); + } + } +} diff --git a/Estara/Services/ThemeSelectorService.cs b/Estara/Services/ThemeSelectorService.cs new file mode 100644 index 0000000..bee0b53 --- /dev/null +++ b/Estara/Services/ThemeSelectorService.cs @@ -0,0 +1,63 @@ +using Estara.Contracts.Services; +using Estara.Helpers; + +using Microsoft.UI.Xaml; + +namespace Estara.Services; + +public class ThemeSelectorService : IThemeSelectorService +{ + private const string SettingsKey = "AppBackgroundRequestedTheme"; + + public ElementTheme Theme { get; set; } = ElementTheme.Default; + + private readonly ILocalSettingsService _localSettingsService; + + public ThemeSelectorService(ILocalSettingsService localSettingsService) + { + _localSettingsService = localSettingsService; + } + + public async Task InitializeAsync() + { + Theme = await LoadThemeFromSettingsAsync(); + await Task.CompletedTask; + } + + public async Task SetThemeAsync(ElementTheme theme) + { + Theme = theme; + + await SetRequestedThemeAsync(); + await SaveThemeInSettingsAsync(Theme); + } + + public async Task SetRequestedThemeAsync() + { + if (App.MainWindow.Content is FrameworkElement rootElement) + { + rootElement.RequestedTheme = Theme; + + TitleBarHelper.UpdateTitleBar(Theme); + } + + await Task.CompletedTask; + } + + private async Task LoadThemeFromSettingsAsync() + { + var themeName = await _localSettingsService.ReadSettingAsync(SettingsKey); + + if (Enum.TryParse(themeName, out ElementTheme cacheTheme)) + { + return cacheTheme; + } + + return ElementTheme.Default; + } + + private async Task SaveThemeInSettingsAsync(ElementTheme theme) + { + await _localSettingsService.SaveSettingAsync(SettingsKey, theme.ToString()); + } +} diff --git a/Estara/Strings/en-us/Resources.resw b/Estara/Strings/en-us/Resources.resw new file mode 100644 index 0000000..4c1ddee --- /dev/null +++ b/Estara/Strings/en-us/Resources.resw @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Estara + + + Estara + + + Main + + + Select an item from the list. + + + ListDetails + + + ContentGrid + + + DataGrid + + + Personalization + + + Theme + + + Light + + + Dark + + + Default + + + About this application + + + TODO: Replace with your app description. + + + Privacy Statement + + + https://YourPrivacyUrlGoesHere/ + + + <toast launch="action=ToastClick"> + <visual> + <binding template="ToastGeneric"> + <text>App Notification</text> + <text></text> + <image placement="appLogoOverride" hint-crop="circle" src="{0}Assets/WindowIcon.ico"/> + </binding> + </visual> + <actions> + <action content="Settings" arguments="action=Settings"/> + </actions> +</toast> + + diff --git a/Estara/Styles/FontSizes.xaml b/Estara/Styles/FontSizes.xaml new file mode 100644 index 0000000..44904b1 --- /dev/null +++ b/Estara/Styles/FontSizes.xaml @@ -0,0 +1,9 @@ + + + 24 + + 16 + + diff --git a/Estara/Styles/TextBlock.xaml b/Estara/Styles/TextBlock.xaml new file mode 100644 index 0000000..8626187 --- /dev/null +++ b/Estara/Styles/TextBlock.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + diff --git a/Estara/Styles/Thickness.xaml b/Estara/Styles/Thickness.xaml new file mode 100644 index 0000000..96ef0c9 --- /dev/null +++ b/Estara/Styles/Thickness.xaml @@ -0,0 +1,36 @@ + + + 0,36,0,0 + 0,36,0,36 + + 0,24,0,0 + 0,24,0,24 + 24,0,24,0 + 0,0,0,24 + + 12,0,0,0 + 12,0,12,0 + 0,12,0,0 + 0,0,12,0 + 0,12,0,12 + + 8,0,0,0 + 0,8,0,0 + 8,8,8,8 + + 0,4,0,0 + 4,4,4,4 + + 1,1,0,0 + 8,0,0,0 + 0,48,0,0 + 56,34,0,0 + 56,24,56,0 + + 36,24,36,0 + + -12,4,0,0 + + diff --git a/Estara/TemplateStudio.xml b/Estara/TemplateStudio.xml new file mode 100644 index 0000000..03eb2eb --- /dev/null +++ b/Estara/TemplateStudio.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/Estara/Usings.cs b/Estara/Usings.cs new file mode 100644 index 0000000..4cc487e --- /dev/null +++ b/Estara/Usings.cs @@ -0,0 +1 @@ +global using WinUIEx; diff --git a/Estara/ViewModels/ContentGridDetailViewModel.cs b/Estara/ViewModels/ContentGridDetailViewModel.cs new file mode 100644 index 0000000..640bf20 --- /dev/null +++ b/Estara/ViewModels/ContentGridDetailViewModel.cs @@ -0,0 +1,33 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +using Estara.Contracts.ViewModels; +using Estara.Core.Contracts.Services; +using Estara.Core.Models; + +namespace Estara.ViewModels; + +public partial class ContentGridDetailViewModel : ObservableRecipient, INavigationAware +{ + private readonly ISampleDataService _sampleDataService; + + [ObservableProperty] + private SampleOrder? item; + + public ContentGridDetailViewModel(ISampleDataService sampleDataService) + { + _sampleDataService = sampleDataService; + } + + public async void OnNavigatedTo(object parameter) + { + if (parameter is long orderID) + { + var data = await _sampleDataService.GetContentGridDataAsync(); + Item = data.First(i => i.OrderID == orderID); + } + } + + public void OnNavigatedFrom() + { + } +} diff --git a/Estara/ViewModels/ContentGridViewModel.cs b/Estara/ViewModels/ContentGridViewModel.cs new file mode 100644 index 0000000..52ab710 --- /dev/null +++ b/Estara/ViewModels/ContentGridViewModel.cs @@ -0,0 +1,52 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +using Estara.Contracts.Services; +using Estara.Contracts.ViewModels; +using Estara.Core.Contracts.Services; +using Estara.Core.Models; + +namespace Estara.ViewModels; + +public partial class ContentGridViewModel : ObservableRecipient, INavigationAware +{ + private readonly INavigationService _navigationService; + private readonly ISampleDataService _sampleDataService; + + public ObservableCollection Source { get; } = new ObservableCollection(); + + public ContentGridViewModel(INavigationService navigationService, ISampleDataService sampleDataService) + { + _navigationService = navigationService; + _sampleDataService = sampleDataService; + } + + public async void OnNavigatedTo(object parameter) + { + Source.Clear(); + + // TODO: Replace with real data. + var data = await _sampleDataService.GetContentGridDataAsync(); + foreach (var item in data) + { + Source.Add(item); + } + } + + public void OnNavigatedFrom() + { + } + + [RelayCommand] + private void OnItemClick(SampleOrder? clickedItem) + { + if (clickedItem != null) + { + _navigationService.SetListDataItemForNextConnectedAnimation(clickedItem); + _navigationService.NavigateTo(typeof(ContentGridDetailViewModel).FullName!, clickedItem.OrderID); + } + } +} diff --git a/Estara/ViewModels/DataGridViewModel.cs b/Estara/ViewModels/DataGridViewModel.cs new file mode 100644 index 0000000..81ca609 --- /dev/null +++ b/Estara/ViewModels/DataGridViewModel.cs @@ -0,0 +1,38 @@ +using System.Collections.ObjectModel; + +using CommunityToolkit.Mvvm.ComponentModel; + +using Estara.Contracts.ViewModels; +using Estara.Core.Contracts.Services; +using Estara.Core.Models; + +namespace Estara.ViewModels; + +public partial class DataGridViewModel : ObservableRecipient, INavigationAware +{ + private readonly ISampleDataService _sampleDataService; + + public ObservableCollection Source { get; } = new ObservableCollection(); + + public DataGridViewModel(ISampleDataService sampleDataService) + { + _sampleDataService = sampleDataService; + } + + public async void OnNavigatedTo(object parameter) + { + Source.Clear(); + + // TODO: Replace with real data. + var data = await _sampleDataService.GetGridDataAsync(); + + foreach (var item in data) + { + Source.Add(item); + } + } + + public void OnNavigatedFrom() + { + } +} diff --git a/Estara/ViewModels/ListDetailsViewModel.cs b/Estara/ViewModels/ListDetailsViewModel.cs new file mode 100644 index 0000000..aab73f4 --- /dev/null +++ b/Estara/ViewModels/ListDetailsViewModel.cs @@ -0,0 +1,46 @@ +using System.Collections.ObjectModel; + +using CommunityToolkit.Mvvm.ComponentModel; + +using Estara.Contracts.ViewModels; +using Estara.Core.Contracts.Services; +using Estara.Core.Models; + +namespace Estara.ViewModels; + +public partial class ListDetailsViewModel : ObservableRecipient, INavigationAware +{ + private readonly ISampleDataService _sampleDataService; + + [ObservableProperty] + private SampleOrder? selected; + + public ObservableCollection SampleItems { get; private set; } = new ObservableCollection(); + + public ListDetailsViewModel(ISampleDataService sampleDataService) + { + _sampleDataService = sampleDataService; + } + + public async void OnNavigatedTo(object parameter) + { + SampleItems.Clear(); + + // TODO: Replace with real data. + var data = await _sampleDataService.GetListDetailsDataAsync(); + + foreach (var item in data) + { + SampleItems.Add(item); + } + } + + public void OnNavigatedFrom() + { + } + + public void EnsureItemSelected() + { + Selected ??= SampleItems.First(); + } +} diff --git a/Estara/ViewModels/MainViewModel.cs b/Estara/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..ed0feb3 --- /dev/null +++ b/Estara/ViewModels/MainViewModel.cs @@ -0,0 +1,10 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Estara.ViewModels; + +public partial class MainViewModel : ObservableRecipient +{ + public MainViewModel() + { + } +} diff --git a/Estara/ViewModels/SettingsViewModel.cs b/Estara/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..2521f4a --- /dev/null +++ b/Estara/ViewModels/SettingsViewModel.cs @@ -0,0 +1,65 @@ +using System.Reflection; +using System.Windows.Input; + +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +using Estara.Contracts.Services; +using Estara.Helpers; + +using Microsoft.UI.Xaml; + +using Windows.ApplicationModel; + +namespace Estara.ViewModels; + +public partial class SettingsViewModel : ObservableRecipient +{ + private readonly IThemeSelectorService _themeSelectorService; + + [ObservableProperty] + private ElementTheme _elementTheme; + + [ObservableProperty] + private string _versionDescription; + + public ICommand SwitchThemeCommand + { + get; + } + + public SettingsViewModel(IThemeSelectorService themeSelectorService) + { + _themeSelectorService = themeSelectorService; + _elementTheme = _themeSelectorService.Theme; + _versionDescription = GetVersionDescription(); + + SwitchThemeCommand = new RelayCommand( + async (param) => + { + if (ElementTheme != param) + { + ElementTheme = param; + await _themeSelectorService.SetThemeAsync(param); + } + }); + } + + private static string GetVersionDescription() + { + Version version; + + if (RuntimeHelper.IsMSIX) + { + var packageVersion = Package.Current.Id.Version; + + version = new(packageVersion.Major, packageVersion.Minor, packageVersion.Build, packageVersion.Revision); + } + else + { + version = Assembly.GetExecutingAssembly().GetName().Version!; + } + + return $"{"AppDisplayName".GetLocalized()} - {version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; + } +} diff --git a/Estara/ViewModels/ShellViewModel.cs b/Estara/ViewModels/ShellViewModel.cs new file mode 100644 index 0000000..9f5c1d2 --- /dev/null +++ b/Estara/ViewModels/ShellViewModel.cs @@ -0,0 +1,51 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +using Estara.Contracts.Services; +using Estara.Views; + +using Microsoft.UI.Xaml.Navigation; + +namespace Estara.ViewModels; + +public partial class ShellViewModel : ObservableRecipient +{ + [ObservableProperty] + private bool isBackEnabled; + + [ObservableProperty] + private object? selected; + + public INavigationService NavigationService + { + get; + } + + public INavigationViewService NavigationViewService + { + get; + } + + public ShellViewModel(INavigationService navigationService, INavigationViewService navigationViewService) + { + NavigationService = navigationService; + NavigationService.Navigated += OnNavigated; + NavigationViewService = navigationViewService; + } + + private void OnNavigated(object sender, NavigationEventArgs e) + { + IsBackEnabled = NavigationService.CanGoBack; + + if (e.SourcePageType == typeof(SettingsPage)) + { + Selected = NavigationViewService.SettingsItem; + return; + } + + var selectedItem = NavigationViewService.GetSelectedItem(e.SourcePageType); + if (selectedItem != null) + { + Selected = selectedItem; + } + } +} diff --git a/Estara/Views/ContentGridDetailPage.xaml b/Estara/Views/ContentGridDetailPage.xaml new file mode 100644 index 0000000..49d29cf --- /dev/null +++ b/Estara/Views/ContentGridDetailPage.xaml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Estara/Views/ContentGridDetailPage.xaml.cs b/Estara/Views/ContentGridDetailPage.xaml.cs new file mode 100644 index 0000000..d853468 --- /dev/null +++ b/Estara/Views/ContentGridDetailPage.xaml.cs @@ -0,0 +1,43 @@ +using CommunityToolkit.WinUI.UI.Animations; + +using Estara.Contracts.Services; +using Estara.ViewModels; + +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace Estara.Views; + +public sealed partial class ContentGridDetailPage : Page +{ + public ContentGridDetailViewModel ViewModel + { + get; + } + + public ContentGridDetailPage() + { + ViewModel = App.GetService(); + InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + this.RegisterElementForConnectedAnimation("animationKeyContentGrid", itemHero); + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + base.OnNavigatingFrom(e); + if (e.NavigationMode == NavigationMode.Back) + { + var navigationService = App.GetService(); + + if (ViewModel.Item != null) + { + navigationService.SetListDataItemForNextConnectedAnimation(ViewModel.Item); + } + } + } +} diff --git a/Estara/Views/ContentGridPage.xaml b/Estara/Views/ContentGridPage.xaml new file mode 100644 index 0000000..8c0223a --- /dev/null +++ b/Estara/Views/ContentGridPage.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/Estara/Views/ContentGridPage.xaml.cs b/Estara/Views/ContentGridPage.xaml.cs new file mode 100644 index 0000000..215e097 --- /dev/null +++ b/Estara/Views/ContentGridPage.xaml.cs @@ -0,0 +1,19 @@ +using Estara.ViewModels; + +using Microsoft.UI.Xaml.Controls; + +namespace Estara.Views; + +public sealed partial class ContentGridPage : Page +{ + public ContentGridViewModel ViewModel + { + get; + } + + public ContentGridPage() + { + ViewModel = App.GetService(); + InitializeComponent(); + } +} diff --git a/Estara/Views/DataGridPage.xaml b/Estara/Views/DataGridPage.xaml new file mode 100644 index 0000000..955b486 --- /dev/null +++ b/Estara/Views/DataGridPage.xaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Estara/Views/DataGridPage.xaml.cs b/Estara/Views/DataGridPage.xaml.cs new file mode 100644 index 0000000..795dfa0 --- /dev/null +++ b/Estara/Views/DataGridPage.xaml.cs @@ -0,0 +1,21 @@ +using Estara.ViewModels; + +using Microsoft.UI.Xaml.Controls; + +namespace Estara.Views; + +// TODO: Change the grid as appropriate for your app. Adjust the column definitions on DataGridPage.xaml. +// For more details, see the documentation at https://docs.microsoft.com/windows/communitytoolkit/controls/datagrid. +public sealed partial class DataGridPage : Page +{ + public DataGridViewModel ViewModel + { + get; + } + + public DataGridPage() + { + ViewModel = App.GetService(); + InitializeComponent(); + } +} diff --git a/Estara/Views/ListDetailsDetailControl.xaml b/Estara/Views/ListDetailsDetailControl.xaml new file mode 100644 index 0000000..cf22d1a --- /dev/null +++ b/Estara/Views/ListDetailsDetailControl.xaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Estara/Views/ListDetailsDetailControl.xaml.cs b/Estara/Views/ListDetailsDetailControl.xaml.cs new file mode 100644 index 0000000..b94eb95 --- /dev/null +++ b/Estara/Views/ListDetailsDetailControl.xaml.cs @@ -0,0 +1,30 @@ +using Estara.Core.Models; + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Estara.Views; + +public sealed partial class ListDetailsDetailControl : UserControl +{ + public SampleOrder? ListDetailsMenuItem + { + get => GetValue(ListDetailsMenuItemProperty) as SampleOrder; + set => SetValue(ListDetailsMenuItemProperty, value); + } + + public static readonly DependencyProperty ListDetailsMenuItemProperty = DependencyProperty.Register("ListDetailsMenuItem", typeof(SampleOrder), typeof(ListDetailsDetailControl), new PropertyMetadata(null, OnListDetailsMenuItemPropertyChanged)); + + public ListDetailsDetailControl() + { + InitializeComponent(); + } + + private static void OnListDetailsMenuItemPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ListDetailsDetailControl control) + { + control.ForegroundElement.ChangeView(0, 0, 1); + } + } +} diff --git a/Estara/Views/ListDetailsPage.xaml b/Estara/Views/ListDetailsPage.xaml new file mode 100644 index 0000000..8ea7af7 --- /dev/null +++ b/Estara/Views/ListDetailsPage.xaml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Estara/Views/ListDetailsPage.xaml.cs b/Estara/Views/ListDetailsPage.xaml.cs new file mode 100644 index 0000000..3afc196 --- /dev/null +++ b/Estara/Views/ListDetailsPage.xaml.cs @@ -0,0 +1,29 @@ +using CommunityToolkit.WinUI.UI.Controls; + +using Estara.ViewModels; + +using Microsoft.UI.Xaml.Controls; + +namespace Estara.Views; + +public sealed partial class ListDetailsPage : Page +{ + public ListDetailsViewModel ViewModel + { + get; + } + + public ListDetailsPage() + { + ViewModel = App.GetService(); + InitializeComponent(); + } + + private void OnViewStateChanged(object sender, ListDetailsViewState e) + { + if (e == ListDetailsViewState.Both) + { + ViewModel.EnsureItemSelected(); + } + } +} diff --git a/Estara/Views/MainPage.xaml b/Estara/Views/MainPage.xaml new file mode 100644 index 0000000..40b9352 --- /dev/null +++ b/Estara/Views/MainPage.xaml @@ -0,0 +1,12 @@ + + + + + + diff --git a/Estara/Views/MainPage.xaml.cs b/Estara/Views/MainPage.xaml.cs new file mode 100644 index 0000000..2ed7dea --- /dev/null +++ b/Estara/Views/MainPage.xaml.cs @@ -0,0 +1,19 @@ +using Estara.ViewModels; + +using Microsoft.UI.Xaml.Controls; + +namespace Estara.Views; + +public sealed partial class MainPage : Page +{ + public MainViewModel ViewModel + { + get; + } + + public MainPage() + { + ViewModel = App.GetService(); + InitializeComponent(); + } +} diff --git a/Estara/Views/SettingsPage.xaml b/Estara/Views/SettingsPage.xaml new file mode 100644 index 0000000..29488a5 --- /dev/null +++ b/Estara/Views/SettingsPage.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + Light + + + + + Dark + + + + + Default + + + + + + + + + + + + + + + + diff --git a/Estara/Views/SettingsPage.xaml.cs b/Estara/Views/SettingsPage.xaml.cs new file mode 100644 index 0000000..4f30919 --- /dev/null +++ b/Estara/Views/SettingsPage.xaml.cs @@ -0,0 +1,20 @@ +using Estara.ViewModels; + +using Microsoft.UI.Xaml.Controls; + +namespace Estara.Views; + +// TODO: Set the URL for your privacy policy by updating SettingsPage_PrivacyTermsLink.NavigateUri in Resources.resw. +public sealed partial class SettingsPage : Page +{ + public SettingsViewModel ViewModel + { + get; + } + + public SettingsPage() + { + ViewModel = App.GetService(); + InitializeComponent(); + } +} diff --git a/Estara/Views/ShellPage.xaml b/Estara/Views/ShellPage.xaml new file mode 100644 index 0000000..6f20015 --- /dev/null +++ b/Estara/Views/ShellPage.xaml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Estara/Views/ShellPage.xaml.cs b/Estara/Views/ShellPage.xaml.cs new file mode 100644 index 0000000..f732fa8 --- /dev/null +++ b/Estara/Views/ShellPage.xaml.cs @@ -0,0 +1,88 @@ +using Estara.Contracts.Services; +using Estara.Helpers; +using Estara.ViewModels; + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; + +using Windows.System; + +namespace Estara.Views; + +// TODO: Update NavigationViewItem titles and icons in ShellPage.xaml. +public sealed partial class ShellPage : Page +{ + public ShellViewModel ViewModel + { + get; + } + + public ShellPage(ShellViewModel viewModel) + { + ViewModel = viewModel; + InitializeComponent(); + + ViewModel.NavigationService.Frame = NavigationFrame; + ViewModel.NavigationViewService.Initialize(NavigationViewControl); + + // TODO: Set the title bar icon by updating /Assets/WindowIcon.ico. + // A custom title bar is required for full window theme and Mica support. + // https://docs.microsoft.com/windows/apps/develop/title-bar?tabs=winui3#full-customization + App.MainWindow.ExtendsContentIntoTitleBar = true; + App.MainWindow.SetTitleBar(AppTitleBar); + App.MainWindow.Activated += MainWindow_Activated; + AppTitleBarText.Text = "AppDisplayName".GetLocalized(); + } + + private void OnLoaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + TitleBarHelper.UpdateTitleBar(RequestedTheme); + + KeyboardAccelerators.Add(BuildKeyboardAccelerator(VirtualKey.Left, VirtualKeyModifiers.Menu)); + KeyboardAccelerators.Add(BuildKeyboardAccelerator(VirtualKey.GoBack)); + } + + private void MainWindow_Activated(object sender, WindowActivatedEventArgs args) + { + var resource = args.WindowActivationState == WindowActivationState.Deactivated ? "WindowCaptionForegroundDisabled" : "WindowCaptionForeground"; + + AppTitleBarText.Foreground = (SolidColorBrush)App.Current.Resources[resource]; + App.AppTitlebar = AppTitleBarText as UIElement; + } + + private void NavigationViewControl_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args) + { + AppTitleBar.Margin = new Thickness() + { + Left = sender.CompactPaneLength * (sender.DisplayMode == NavigationViewDisplayMode.Minimal ? 2 : 1), + Top = AppTitleBar.Margin.Top, + Right = AppTitleBar.Margin.Right, + Bottom = AppTitleBar.Margin.Bottom + }; + } + + private static KeyboardAccelerator BuildKeyboardAccelerator(VirtualKey key, VirtualKeyModifiers? modifiers = null) + { + var keyboardAccelerator = new KeyboardAccelerator() { Key = key }; + + if (modifiers.HasValue) + { + keyboardAccelerator.Modifiers = modifiers.Value; + } + + keyboardAccelerator.Invoked += OnKeyboardAcceleratorInvoked; + + return keyboardAccelerator; + } + + private static void OnKeyboardAcceleratorInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) + { + var navigationService = App.GetService(); + + var result = navigationService.GoBack(); + + args.Handled = result; + } +} diff --git a/Estara/app.manifest b/Estara/app.manifest new file mode 100644 index 0000000..68dd172 --- /dev/null +++ b/Estara/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/Estara/appsettings.json b/Estara/appsettings.json new file mode 100644 index 0000000..e7de7ea --- /dev/null +++ b/Estara/appsettings.json @@ -0,0 +1,6 @@ +{ + "LocalSettingsOptions": { + "ApplicationDataFolder": "Estara/ApplicationData", + "LocalSettingsFile": "LocalSettings.json" + } +} diff --git a/README.md b/README.md index 23e183c..2fc2ead 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,20 @@ A Kiseki experiment # License -Licensed under the GNU Affero General Public License v3.0. A copy of it [has been included](https://github.com/kiseki-lol/estara/blob/trunk/LICENSE). \ No newline at end of file +``` +Estara - A Kiseki experiment +Copyright (C) 2023 Kiseki + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +``` \ No newline at end of file