Add WinUI 3 app skeleton

This commit is contained in:
rjindael 2023-06-15 00:36:05 -07:00
parent da627ea20b
commit 79bf276ab9
No known key found for this signature in database
GPG Key ID: D069369C906CCF31
92 changed files with 4086 additions and 74 deletions

202
.editorconfig Normal file
View File

@ -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

16
.vsconfig Normal file
View File

@ -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"
]
}

View File

@ -0,0 +1,10 @@
namespace Estara.Core.Contracts.Services;
public interface IFileService
{
T Read<T>(string folderPath, string fileName);
void Save<T>(string folderPath, string fileName, T content);
void Delete(string folderPath, string fileName);
}

View File

@ -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<IEnumerable<SampleOrder>> GetContentGridDataAsync();
Task<IEnumerable<SampleOrder>> GetGridDataAsync();
Task<IEnumerable<SampleOrder>> GetListDetailsDataAsync();
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>Estara.Core</RootNamespace>
<Platforms>AnyCPU;x64;x86</Platforms>
<Platforms>x86;x64;arm64;AnyCPU</Platforms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,22 @@
using Newtonsoft.Json;
namespace Estara.Core.Helpers;
public static class Json
{
public static async Task<T> ToObjectAsync<T>(string value)
{
return await Task.Run<T>(() =>
{
return JsonConvert.DeserializeObject<T>(value);
});
}
public static async Task<string> StringifyAsync(object value)
{
return await Task.Run<string>(() =>
{
return JsonConvert.SerializeObject(value);
});
}
}

View File

@ -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<SampleOrder> Orders
{
get; set;
}
}

View File

@ -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<SampleOrderDetail> Details
{
get; set;
}
public string ShortDescription => $"Order ID: {OrderID}";
public override string ToString() => $"{Company} {Status}";
}

View File

@ -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}";
}

5
Estara.Core/README.md Normal file
View File

@ -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).

View File

@ -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<T>(string folderPath, string fileName)
{
var path = Path.Combine(folderPath, fileName);
if (File.Exists(path))
{
var json = File.ReadAllText(path);
return JsonConvert.DeserializeObject<T>(json);
}
return default;
}
public void Save<T>(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));
}
}
}

View File

@ -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<SampleOrder> _allOrders;
public SampleDataService()
{
}
private static IEnumerable<SampleOrder> AllOrders()
{
// The following is order summary data
var companies = AllCompanies();
return companies.SelectMany(c => c.Orders);
}
private static IEnumerable<SampleCompany> AllCompanies()
{
return new List<SampleCompany>()
{
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<SampleOrder>()
{
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<SampleOrderDetail>()
{
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<SampleOrderDetail>()
{
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<SampleOrderDetail>()
{
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<SampleOrder>()
{
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<SampleOrderDetail>()
{
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<SampleOrderDetail>()
{
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<SampleOrder>()
{
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<SampleOrderDetail>()
{
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<SampleOrderDetail>()
{
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<SampleOrderDetail>()
{
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<IEnumerable<SampleOrder>> GetContentGridDataAsync()
{
_allOrders ??= new List<SampleOrder>(AllOrders());
await Task.CompletedTask;
return _allOrders;
}
public async Task<IEnumerable<SampleOrder>> GetGridDataAsync()
{
_allOrders ??= new List<SampleOrder>(AllOrders());
await Task.CompletedTask;
return _allOrders;
}
public async Task<IEnumerable<SampleOrder>> GetListDetailsDataAsync()
{
_allOrders ??= new List<SampleOrder>(AllOrders());
await Task.CompletedTask;
return _allOrders;
}
}

View File

@ -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

View File

@ -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<T> : 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)!);
}

View File

@ -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<LaunchActivatedEventArgs>
{
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;
}
}

View File

@ -0,0 +1,29 @@
using Estara.Contracts.Services;
using Estara.ViewModels;
using Microsoft.UI.Xaml;
namespace Estara.Activation;
public class DefaultActivationHandler : ActivationHandler<LaunchActivatedEventArgs>
{
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;
}
}

View File

@ -0,0 +1,8 @@
namespace Estara.Activation;
public interface IActivationHandler
{
bool CanHandle(object args);
Task HandleAsync(object args);
}

View File

@ -1,9 +1,15 @@
<Application x:Class="Estara.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Estara"
StartupUri="MainWindow.xaml">
<Application
x:Class="Estara.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="/Styles/FontSizes.xaml" />
<ResourceDictionary Source="/Styles/Thickness.xaml" />
<ResourceDictionary Source="/Styles/TextBlock.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -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
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
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<T>()
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<ActivationHandler<LaunchActivatedEventArgs>, DefaultActivationHandler>();
// Other Activation Handlers
services.AddTransient<IActivationHandler, AppNotificationActivationHandler>();
// Services
services.AddSingleton<IAppNotificationService, AppNotificationService>();
services.AddSingleton<ILocalSettingsService, LocalSettingsService>();
services.AddSingleton<IThemeSelectorService, ThemeSelectorService>();
services.AddTransient<INavigationViewService, NavigationViewService>();
services.AddSingleton<IActivationService, ActivationService>();
services.AddSingleton<IPageService, PageService>();
services.AddSingleton<INavigationService, NavigationService>();
// Core Services
services.AddSingleton<ISampleDataService, SampleDataService>();
services.AddSingleton<IFileService, FileService>();
// Views and ViewModels
services.AddTransient<SettingsViewModel>();
services.AddTransient<SettingsPage>();
services.AddTransient<DataGridViewModel>();
services.AddTransient<DataGridPage>();
services.AddTransient<ContentGridDetailViewModel>();
services.AddTransient<ContentGridDetailPage>();
services.AddTransient<ContentGridViewModel>();
services.AddTransient<ContentGridPage>();
services.AddTransient<ListDetailsViewModel>();
services.AddTransient<ListDetailsPage>();
services.AddTransient<MainViewModel>();
services.AddTransient<MainPage>();
services.AddTransient<ShellPage>();
services.AddTransient<ShellViewModel>();
// Configuration
services.Configure<LocalSettingsOptions>(context.Configuration.GetSection(nameof(LocalSettingsOptions)));
}).
Build();
App.GetService<IAppNotificationService>().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<IAppNotificationService>().Show(string.Format("AppNotificationSamplePayload".GetLocalized(), AppContext.BaseDirectory));
await App.GetService<IActivationService>().ActivateAsync(args);
}
}

View File

@ -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)
)]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
Estara/Assets/StoreLogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -0,0 +1,122 @@
using Estara.Contracts.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.Xaml.Interactivity;
namespace Estara.Behaviors;
public class NavigationViewHeaderBehavior : Behavior<NavigationView>
{
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<INavigationService>();
navigationService.Navigated += OnNavigated;
_current = this;
}
protected override void OnDetaching()
{
base.OnDetaching();
var navigationService = App.GetService<INavigationService>();
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;
}
}
}

View File

@ -0,0 +1,8 @@
namespace Estara.Behaviors;
public enum NavigationViewHeaderMode
{
Always,
Never,
Minimal
}

View File

@ -0,0 +1,6 @@
namespace Estara.Contracts.Services;
public interface IActivationService
{
Task ActivateAsync(object activationArgs);
}

View File

@ -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();
}

View File

@ -0,0 +1,8 @@
namespace Estara.Contracts.Services;
public interface ILocalSettingsService
{
Task<T?> ReadSettingAsync<T>(string key);
Task SaveSettingAsync<T>(string key, T value);
}

View File

@ -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);
}

View File

@ -0,0 +1,22 @@
using Microsoft.UI.Xaml.Controls;
namespace Estara.Contracts.Services;
public interface INavigationViewService
{
IList<object>? MenuItems
{
get;
}
object? SettingsItem
{
get;
}
void Initialize(NavigationView navigationView);
void UnregisterEvents();
NavigationViewItem? GetSelectedItem(Type pageType);
}

View File

@ -0,0 +1,6 @@
namespace Estara.Contracts.Services;
public interface IPageService
{
Type GetPageType(string key);
}

View File

@ -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();
}

View File

@ -0,0 +1,8 @@
namespace Estara.Contracts.ViewModels;
public interface INavigationAware
{
void OnNavigatedTo(object parameter);
void OnNavigatedFrom();
}

View File

@ -1,10 +1,60 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Estara</RootNamespace>
<ApplicationIcon>Assets/WindowIcon.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;arm64</Platforms>
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
<PublishProfile>Properties\PublishProfiles\win10-$(Platform).pubxml</PublishProfile>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<GenerateAppInstallerFile>True</GenerateAppInstallerFile>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<PackageCertificateThumbprint>12A33CEAC330575CECF628E17569ECC1AA31D9A5</PackageCertificateThumbprint>
<AppxPackageSigningTimestampServerUrl>http://timestamp.digicert.com</AppxPackageSigningTimestampServerUrl>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<AppxSymbolPackageEnabled>True</AppxSymbolPackageEnabled>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundle>Never</AppxBundle>
<AppInstallerUri>https://kiseki.lol/estara</AppInstallerUri>
</PropertyGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Animations" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.3.230502000" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageReference Include="WinUIEx" Version="2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Estara.Core\Estara.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
</Project>

View File

@ -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");
}
}

View File

@ -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);
}

View File

@ -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:
// <NavigationViewItem x:Uid="Shell_Main" Icon="Document" helpers:NavigationHelper.NavigateTo="AppName.ViewModels.MainViewModel" />
//
// 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));
}

View File

@ -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);
}

View File

@ -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;
}
}
}

View File

@ -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<T>(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<T?> ReadAsync<T>(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<T>(fileContent);
}
public static async Task SaveAsync<T>(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<T?> ReadAsync<T>(this ApplicationDataContainer settings, string key)
{
object? obj;
if (settings.Values.TryGetValue(key, out obj))
{
return await Json.ToObjectAsync<T>((string)obj);
}
return default;
}
public static async Task<StorageFile> 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<byte[]?> 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<byte[]?> 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);
}
}

View File

@ -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);
}
}
}

View File

@ -1,12 +1,16 @@
<Window x:Class="Estara.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Estara"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
</Grid>
</Window>
<windowex:WindowEx
x:Class="Estara.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Estara"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:windowex="using:WinUIEx"
MinWidth="500"
MinHeight="500"
PersistenceId="MainWindow"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop/>
</Window.SystemBackdrop>
</windowex:WindowEx>

View File

@ -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
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
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();
});
}
}

View File

@ -0,0 +1,14 @@
namespace Estara.Models;
public class LocalSettingsOptions
{
public string? ApplicationDataFolder
{
get; set;
}
public string? LocalSettingsFile
{
get; set;
}
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<AppInstaller Uri="{AppInstallerUri}"
Version="{Version}"
xmlns="http://schemas.microsoft.com/appx/appinstaller/2017/2">
<!-- Read more about updating settings with Package.appinstaller at https://docs.microsoft.com/windows/msix/app-installer/update-settings -->
<MainBundle Name="{Name}"
Version="{Version}"
Publisher="{Publisher}"
Uri="{MainPackageUri}"/>
<UpdateSettings>
<OnLaunch HoursBetweenUpdateChecks="0" />
</UpdateSettings>
</AppInstaller>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:genTemplate="http://schemas.microsoft.com/appx/developer/templatestudio"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
IgnorableNamespaces="uap rescap genTemplate">
<Identity
Name="3ab72387-a593-4ca3-8c3e-69ab6395152d"
Publisher="CN=Kiseki, O=Kiseki, C=US"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="3ab72387-a593-4ca3-8c3e-69ab6395152d" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>Estara</DisplayName>
<PublisherDisplayName>Kiseki</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="ms-resource:AppDisplayName"
Description="ms-resource:AppDescription"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="1e430715-9903-4ede-9933-b4d5a731c7b2" />
</desktop:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Estara.exe" Arguments="----AppNotificationActivated:" DisplayName="Toast activator">
<com:Class Id="1e430715-9903-4ede-9933-b4d5a731c7b2" DisplayName="Toast activator"/>
</com:ExeServer>
</com:ComServer>
</com:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
<genTemplate:Metadata>
<genTemplate:Item Name="generator" Value="Template Studio"/>
<genTemplate:Item Name="wizardVersion" Version="v5.4" />
<genTemplate:Item Name="projectType" Value="NavView" />
<genTemplate:Item Name="framework" Value="MVVMToolkit" />
<genTemplate:Item Name="platform" Value="WinUI" />
<genTemplate:Item Name="appmodel" Value="Desktop" />
</genTemplate:Metadata>
</Package>

View File

@ -0,0 +1,10 @@
{
"profiles": {
"Estara (Package)": {
"commandName": "MsixPackage"
},
"Estara (Unpackaged)": {
"commandName": "Project"
}
}
}

27
Estara/README.md Normal file
View File

@ -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.

View File

@ -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<LaunchActivatedEventArgs> _defaultHandler;
private readonly IEnumerable<IActivationHandler> _activationHandlers;
private readonly IThemeSelectorService _themeSelectorService;
private UIElement? _shell = null;
public ActivationService(ActivationHandler<LaunchActivatedEventArgs> defaultHandler, IEnumerable<IActivationHandler> 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<ShellPage>();
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;
}
}

View File

@ -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();
}
}

View File

@ -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<string, object> _settings;
private bool _isInitialized;
public LocalSettingsService(IFileService fileService, IOptions<LocalSettingsOptions> options)
{
_fileService = fileService;
_options = options.Value;
_applicationDataFolder = Path.Combine(_localApplicationData, _options.ApplicationDataFolder ?? _defaultApplicationDataFolder);
_localsettingsFile = _options.LocalSettingsFile ?? _defaultLocalSettingsFile;
_settings = new Dictionary<string, object>();
}
private async Task InitializeAsync()
{
if (!_isInitialized)
{
_settings = await Task.Run(() => _fileService.Read<IDictionary<string, object>>(_applicationDataFolder, _localsettingsFile)) ?? new Dictionary<string, object>();
_isInitialized = true;
}
}
public async Task<T?> ReadSettingAsync<T>(string key)
{
if (RuntimeHelper.IsMSIX)
{
if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out var obj))
{
return await Json.ToObjectAsync<T>((string)obj);
}
}
else
{
await InitializeAsync();
if (_settings != null && _settings.TryGetValue(key, out var obj))
{
return await Json.ToObjectAsync<T>((string)obj);
}
}
return default;
}
public async Task SaveSettingAsync<T>(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));
}
}
}

View File

@ -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);
}

View File

@ -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<object>? 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<object> menuItems, Type pageType)
{
foreach (var item in menuItems.OfType<NavigationViewItem>())
{
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;
}
}

View File

@ -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<string, Type> _pages = new();
public PageService()
{
Configure<MainViewModel, MainPage>();
Configure<ListDetailsViewModel, ListDetailsPage>();
Configure<ContentGridViewModel, ContentGridPage>();
Configure<ContentGridDetailViewModel, ContentGridDetailPage>();
Configure<DataGridViewModel, DataGridPage>();
Configure<SettingsViewModel, SettingsPage>();
}
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<VM, V>()
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);
}
}
}

View File

@ -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<ElementTheme> LoadThemeFromSettingsAsync()
{
var themeName = await _localSettingsService.ReadSettingAsync<string>(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());
}
}

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AppDisplayName" xml:space="preserve">
<value>Estara</value>
</data>
<data name="AppDescription" xml:space="preserve">
<value>Estara</value>
</data>
<data name="Shell_Main.Content" xml:space="preserve">
<value>Main</value>
</data>
<data name="ListDetails_NoSelection.Text" xml:space="preserve">
<value>Select an item from the list.</value>
</data>
<data name="Shell_ListDetails.Content" xml:space="preserve">
<value>ListDetails</value>
</data>
<data name="Shell_ContentGrid.Content" xml:space="preserve">
<value>ContentGrid</value>
</data>
<data name="Shell_DataGrid.Content" xml:space="preserve">
<value>DataGrid</value>
</data>
<data name="Settings_Personalization.Text" xml:space="preserve">
<value>Personalization</value>
</data>
<data name="Settings_Theme.Text" xml:space="preserve">
<value>Theme</value>
</data>
<data name="Settings_Theme_Light.Content" xml:space="preserve">
<value>Light</value>
</data>
<data name="Settings_Theme_Dark.Content" xml:space="preserve">
<value>Dark</value>
</data>
<data name="Settings_Theme_Default.Content" xml:space="preserve">
<value>Default</value>
</data>
<data name="Settings_About.Text" xml:space="preserve">
<value>About this application</value>
</data>
<data name="Settings_AboutDescription.Text" xml:space="preserve">
<value>TODO: Replace with your app description.</value>
</data>
<data name="SettingsPage_PrivacyTermsLink.Content" xml:space="preserve">
<value>Privacy Statement</value>
</data>
<data name="SettingsPage_PrivacyTermsLink.NavigateUri" xml:space="preserve">
<value>https://YourPrivacyUrlGoesHere/</value>
</data>
<data name="AppNotificationSamplePayload" xml:space="preserve">
<value>&lt;toast launch="action=ToastClick"&gt;
&lt;visual&gt;
&lt;binding template="ToastGeneric"&gt;
&lt;text&gt;App Notification&lt;/text&gt;
&lt;text&gt;&lt;/text&gt;
&lt;image placement="appLogoOverride" hint-crop="circle" src="{0}Assets/WindowIcon.ico"/&gt;
&lt;/binding&gt;
&lt;/visual&gt;
&lt;actions&gt;
&lt;action content="Settings" arguments="action=Settings"/&gt;
&lt;/actions&gt;
&lt;/toast&gt;</value>
</data>
</root>

View File

@ -0,0 +1,9 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:Double x:Key="LargeFontSize">24</x:Double>
<x:Double x:Key="MediumFontSize">16</x:Double>
</ResourceDictionary>

View File

@ -0,0 +1,50 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="PageTitleStyle" TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontWeight" Value="SemiLight" />
<Setter Property="FontSize" Value="{StaticResource LargeFontSize}" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
<Setter Property="TextWrapping" Value="NoWrap" />
</Style>
<Style x:Key="BodyTextStyle" TargetType="TextBlock">
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontSize" Value="{StaticResource MediumFontSize}" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<Style x:Key="ListTitleStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}">
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="MaxLines" Value="1" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
</Style>
<Style x:Key="ListSubTitleStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}">
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="MaxLines" Value="1" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
</Style>
<Style x:Key="DetailSubTitleStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}">
<Setter Property="FontSize" Value="18" />
<Setter Property="FontWeight" Value="SemiLight" />
</Style>
<Style x:Key="DetailBodyBaseMediumStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}">
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontSize" Value="15" />
<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseMediumBrush}" />
</Style>
<Style x:Key="DetailBodyStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}">
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontSize" Value="15" />
</Style>
</ResourceDictionary>

View File

@ -0,0 +1,36 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Thickness x:Key="LargeTopMargin">0,36,0,0</Thickness>
<Thickness x:Key="LargeTopBottomMargin">0,36,0,36</Thickness>
<Thickness x:Key="MediumTopMargin">0,24,0,0</Thickness>
<Thickness x:Key="MediumTopBottomMargin">0,24,0,24</Thickness>
<Thickness x:Key="MediumLeftRightMargin">24,0,24,0</Thickness>
<Thickness x:Key="MediumBottomMargin">0,0,0,24</Thickness>
<Thickness x:Key="SmallLeftMargin">12,0,0,0</Thickness>
<Thickness x:Key="SmallLeftRightMargin">12,0,12,0</Thickness>
<Thickness x:Key="SmallTopMargin">0,12,0,0</Thickness>
<Thickness x:Key="SmallRightMargin">0,0,12,0</Thickness>
<Thickness x:Key="SmallTopBottomMargin">0,12,0,12</Thickness>
<Thickness x:Key="XSmallLeftMargin">8,0,0,0</Thickness>
<Thickness x:Key="XSmallTopMargin">0,8,0,0</Thickness>
<Thickness x:Key="XSmallLeftTopRightBottomMargin">8,8,8,8</Thickness>
<Thickness x:Key="XXSmallTopMargin">0,4,0,0</Thickness>
<Thickness x:Key="XXSmallLeftTopRightBottomMargin">4,4,4,4</Thickness>
<Thickness x:Key="NavigationViewContentGridBorderThickness">1,1,0,0</Thickness>
<CornerRadius x:Key="NavigationViewContentGridCornerRadius">8,0,0,0</CornerRadius>
<Thickness x:Key="NavigationViewContentMargin">0,48,0,0</Thickness>
<Thickness x:Key="NavigationViewHeaderMargin">56,34,0,0</Thickness>
<Thickness x:Key="NavigationViewPageContentMargin">56,24,56,0</Thickness>
<Thickness x:Key="MenuBarContentMargin">36,24,36,0</Thickness>
<Thickness x:Key="SettingsPageHyperlinkButtonMargin">-12,4,0,0</Thickness>
</ResourceDictionary>

11
Estara/TemplateStudio.xml Normal file
View File

@ -0,0 +1,11 @@
<xml
xmlns:genTemplate="http://schemas.microsoft.com/appx/developer/templatestudio">
<genTemplate:Metadata>
<genTemplate:Item Name="generator" Value="Template Studio"/>
<genTemplate:Item Name="wizardVersion" Version="v5.4" />
<genTemplate:Item Name="projectType" Value="NavView" />
<genTemplate:Item Name="framework" Value="MVVMToolkit" />
<genTemplate:Item Name="platform" Value="WinUI" />
<genTemplate:Item Name="appmodel" Value="Desktop" />
</genTemplate:Metadata>
</xml>

1
Estara/Usings.cs Normal file
View File

@ -0,0 +1 @@
global using WinUIEx;

View File

@ -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()
{
}
}

View File

@ -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<SampleOrder> Source { get; } = new ObservableCollection<SampleOrder>();
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);
}
}
}

View File

@ -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<SampleOrder> Source { get; } = new ObservableCollection<SampleOrder>();
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()
{
}
}

View File

@ -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<SampleOrder> SampleItems { get; private set; } = new ObservableCollection<SampleOrder>();
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();
}
}

View File

@ -0,0 +1,10 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Estara.ViewModels;
public partial class MainViewModel : ObservableRecipient
{
public MainViewModel()
{
}
}

View File

@ -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<ElementTheme>(
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}";
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,107 @@
<Page
x:Class="Estara.Views.ContentGridDetailPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:models="using:Estara.Core.Models"
mc:Ignorable="d">
<Grid x:Name="ContentArea">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState>
<VisualState.StateTriggers>
<!--641 is the default CompactModeThresholdWidth in NavigationView -->
<AdaptiveTrigger MinWindowWidth="641" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="propertiesGroup1.(RelativePanel.RightOf)" Value="itemHero" />
<Setter Target="propertiesGroup1.(RelativePanel.Below)" Value="title" />
<Setter Target="propertiesGroup2.(RelativePanel.RightOf)" Value="propertiesGroup1" />
<Setter Target="propertiesGroup2.(RelativePanel.Below)" Value="title" />
<Setter Target="propertiesGroup1.Width" Value="200" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ScrollViewer
IsTabStop="True">
<StackPanel
x:Name="contentPanel">
<RelativePanel>
<Grid
x:Name="itemHero"
Width="200"
Height="200"
Margin="{StaticResource SmallRightMargin}"
Padding="{StaticResource XSmallLeftTopRightBottomMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
RelativePanel.AlignTopWithPanel="True"
RelativePanel.AlignLeftWithPanel="True">
<FontIcon
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="70"
Glyph="{x:Bind ViewModel.Item.Symbol}"
AutomationProperties.Name="{x:Bind ViewModel.Item.SymbolName}" />
</Grid>
<TextBlock
x:Name="title"
Margin="{StaticResource XXSmallTopMargin}"
RelativePanel.AlignTopWithPanel="True"
RelativePanel.RightOf="itemHero"
Style="{ThemeResource TitleTextBlockStyle}"
Text="{x:Bind ViewModel.Item.Company, Mode=OneWay}" />
<StackPanel x:Name="propertiesGroup1" RelativePanel.Below="itemHero">
<StackPanel x:Name="statusGroup" Margin="{StaticResource SmallTopMargin}">
<TextBlock Style="{ThemeResource SubtitleTextBlockStyle}" Text="Status" />
<TextBlock Style="{ThemeResource BodyTextBlockStyle}" Text="{x:Bind ViewModel.Item.Status}" />
</StackPanel>
<StackPanel x:Name="orderDateGroup" Margin="{StaticResource SmallTopMargin}">
<TextBlock Style="{ThemeResource SubtitleTextBlockStyle}" Text="Order date" />
<TextBlock Style="{ThemeResource BodyTextBlockStyle}" Text="{x:Bind ViewModel.Item.OrderDate}" />
</StackPanel>
</StackPanel>
<StackPanel x:Name="propertiesGroup2" RelativePanel.Below="propertiesGroup1">
<StackPanel x:Name="shipToGroup" Margin="{StaticResource SmallTopMargin}">
<TextBlock Style="{ThemeResource SubtitleTextBlockStyle}" Text="Ship to" />
<TextBlock Style="{ThemeResource BodyTextBlockStyle}" Text="{x:Bind ViewModel.Item.ShipTo}" />
</StackPanel>
<StackPanel x:Name="orderTotalGroup" Margin="{StaticResource SmallTopMargin}">
<TextBlock Style="{ThemeResource SubtitleTextBlockStyle}" Text="Order total" />
<TextBlock Style="{ThemeResource BodyTextBlockStyle}" Text="{x:Bind ViewModel.Item.OrderTotal}" />
</StackPanel>
</StackPanel>
</RelativePanel>
<TextBlock
Margin="{StaticResource SmallTopMargin}"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="Note 1" />
<TextBlock
Style="{ThemeResource BodyTextBlockStyle}"
Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis hendrerit nulla, vel molestie libero. In nec ultricies magna, ultricies molestie ipsum. Mauris non dignissim velit. Etiam malesuada blandit mauris eu maximus. Quisque ornare, felis nec scelerisque mollis, risus dolor posuere magna, in gravida quam mi id nisi. Nullam mattis consequat ex. Cras nulla neque, dictum ac urna et, vestibulum feugiat ex. Pellentesque malesuada accumsan ligula, vel fringilla lacus facilisis sit amet. Proin convallis tempor arcu, ac placerat libero pretium ut. Praesent hendrerit nisl at lobortis viverra. Fusce vitae velit odio. Nam ut tortor sed purus finibus sollicitudin quis at ante. Ut sodales dolor vel eros mollis suscipit. Donec eu nulla id urna ultricies consequat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;" />
<TextBlock
Margin="{StaticResource SmallTopMargin}"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="Note 2" />
<TextBlock
Margin="{StaticResource MediumBottomMargin}"
Style="{ThemeResource BodyTextBlockStyle}"
Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis hendrerit nulla, vel molestie libero. In nec ultricies magna, ultricies molestie ipsum. Mauris non dignissim velit. Etiam malesuada blandit mauris eu maximus. Quisque ornare, felis nec scelerisque mollis, risus dolor posuere magna, in gravida quam mi id nisi. Nullam mattis consequat ex. Cras nulla neque, dictum ac urna et, vestibulum feugiat ex. Pellentesque malesuada accumsan ligula, vel fringilla lacus facilisis sit amet. Proin convallis tempor arcu, ac placerat libero pretium ut. Praesent hendrerit nisl at lobortis viverra. Fusce vitae velit odio. Nam ut tortor sed purus finibus sollicitudin quis at ante. Ut sodales dolor vel eros mollis suscipit. Donec eu nulla id urna ultricies consequat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;" />
</StackPanel>
</ScrollViewer>
</Grid>
</Page>

View File

@ -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<ContentGridDetailViewModel>();
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<INavigationService>();
if (ViewModel.Item != null)
{
navigationService.SetListDataItemForNextConnectedAnimation(ViewModel.Item);
}
}
}
}

View File

@ -0,0 +1,43 @@
<Page
x:Class="Estara.Views.ContentGridPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:animations="using:CommunityToolkit.WinUI.UI.Animations"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:models="using:Estara.Core.Models"
mc:Ignorable="d">
<Grid x:Name="ContentArea">
<controls:AdaptiveGridView
animations:Connected.ListItemElementName="itemThumbnail"
animations:Connected.ListItemKey="animationKeyContentGrid"
DesiredWidth="180"
ItemHeight="160"
IsItemClickEnabled="True"
ItemClickCommand="{x:Bind ViewModel.ItemClickCommand}"
ItemsSource="{x:Bind ViewModel.Source,Mode=OneWay}"
SelectionMode="None"
StretchContentForSingleRow="False">
<controls:AdaptiveGridView.ItemTemplate>
<DataTemplate x:DataType="models:SampleOrder">
<Grid
x:Name="itemThumbnail"
Padding="{StaticResource XSmallLeftTopRightBottomMargin}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<FontIcon
Glyph="{x:Bind Symbol}"
AutomationProperties.Name="{x:Bind SymbolName}" />
<TextBlock
Margin="{StaticResource XXSmallTopMargin}"
HorizontalAlignment="Center"
Style="{ThemeResource BodyTextStyle}"
Text="{x:Bind Company}" />
</StackPanel>
</Grid>
</DataTemplate>
</controls:AdaptiveGridView.ItemTemplate>
</controls:AdaptiveGridView>
</Grid>
</Page>

View File

@ -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<ContentGridViewModel>();
InitializeComponent();
}
}

View File

@ -0,0 +1,40 @@
<Page
x:Class="Estara.Views.DataGridPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
mc:Ignorable="d">
<Grid x:Name="ContentArea">
<controls:DataGrid
AutoGenerateColumns="False"
GridLinesVisibility="Horizontal"
ItemsSource="{x:Bind ViewModel.Source, Mode=OneWay}">
<controls:DataGrid.Resources>
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundColor" Color="Transparent" />
</controls:DataGrid.Resources>
<controls:DataGrid.Columns>
<!-- TODO: Replace column definitions to match real data. Consider adding Header values to Resources.resw. -->
<controls:DataGridTextColumn Binding="{Binding OrderID}" Header="OrderID" />
<controls:DataGridTextColumn Binding="{Binding OrderDate}" Header="OrderDate" />
<controls:DataGridTextColumn Binding="{Binding Company}" Header="Company" />
<controls:DataGridTextColumn Binding="{Binding ShipTo}" Header="ShipTo" />
<controls:DataGridTextColumn Binding="{Binding OrderTotal}" Header="OrderTotal" />
<controls:DataGridTextColumn Binding="{Binding Status}" Header="Status" />
<controls:DataGridTemplateColumn Header="Symbol">
<controls:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<FontIcon
HorizontalAlignment="Left"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Glyph="{Binding Symbol}"
AutomationProperties.Name="{Binding SymbolName}" />
</DataTemplate>
</controls:DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumn>
</controls:DataGrid.Columns>
</controls:DataGrid>
</Grid>
</Page>

View File

@ -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<DataGridViewModel>();
InitializeComponent();
}
}

View File

@ -0,0 +1,73 @@
<UserControl
x:Class="Estara.Views.ListDetailsDetailControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ScrollViewer
Name="ForegroundElement"
HorizontalAlignment="Stretch"
VerticalScrollMode="Enabled"
IsTabStop="True">
<StackPanel HorizontalAlignment="Left">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
<FontIcon
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="48"
Glyph="{x:Bind ListDetailsMenuItem.Symbol, Mode=OneWay}"
AutomationProperties.Name="{x:Bind ListDetailsMenuItem.SymbolName, Mode=OneWay}" />
<TextBlock
Margin="{StaticResource SmallLeftMargin}"
VerticalAlignment="Center"
Text="{x:Bind ListDetailsMenuItem.Company, Mode=OneWay}"
Style="{ThemeResource TitleTextBlockStyle}" />
</StackPanel>
<StackPanel Padding="0,15,0,0">
<TextBlock Text="Status" Style="{StaticResource DetailSubTitleStyle}" />
<TextBlock Text="{x:Bind ListDetailsMenuItem.Status, Mode=OneWay}" Style="{StaticResource DetailBodyBaseMediumStyle}" />
<TextBlock
Margin="{StaticResource SmallTopMargin}"
Text="Order date"
Style="{StaticResource DetailSubTitleStyle}" />
<TextBlock Text="{x:Bind ListDetailsMenuItem.OrderDate, Mode=OneWay}" Style="{StaticResource DetailBodyBaseMediumStyle}" />
<TextBlock
Margin="{StaticResource SmallTopMargin}"
Text="Company"
Style="{StaticResource DetailSubTitleStyle}" />
<TextBlock Text="{x:Bind ListDetailsMenuItem.Company, Mode=OneWay}" Style="{StaticResource DetailBodyBaseMediumStyle}" />
<TextBlock
Margin="{StaticResource SmallTopMargin}"
Text="Ship to"
Style="{StaticResource DetailSubTitleStyle}" />
<TextBlock Text="{x:Bind ListDetailsMenuItem.ShipTo, Mode=OneWay}" Style="{StaticResource DetailBodyBaseMediumStyle}" />
<TextBlock
Margin="{StaticResource SmallTopMargin}"
Text="Order total"
Style="{StaticResource DetailSubTitleStyle}" />
<TextBlock Text="{x:Bind ListDetailsMenuItem.OrderTotal, Mode=OneWay}" Style="{StaticResource DetailBodyBaseMediumStyle}" />
<TextBlock
Margin="{StaticResource MediumTopMargin}"
Text="Note 1"
Style="{StaticResource DetailSubTitleStyle}" />
<TextBlock
Style="{StaticResource DetailBodyStyle}"
Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis hendrerit nulla, vel molestie libero. In nec ultricies magna, ultricies molestie ipsum. Mauris non dignissim velit. Etiam malesuada blandit mauris eu maximus. Quisque ornare, felis nec scelerisque mollis, risus dolor posuere magna, in gravida quam mi id nisi. Nullam mattis consequat ex. Cras nulla neque, dictum ac urna et, vestibulum feugiat ex. Pellentesque malesuada accumsan ligula, vel fringilla lacus facilisis sit amet. Proin convallis tempor arcu, ac placerat libero pretium ut. Praesent hendrerit nisl at lobortis viverra. Fusce vitae velit odio. Nam ut tortor sed purus finibus sollicitudin quis at ante. Ut sodales dolor vel eros mollis suscipit. Donec eu nulla id urna ultricies consequat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;" />
<TextBlock
Margin="{StaticResource SmallTopMargin}"
Text="Note 2"
Style="{StaticResource DetailSubTitleStyle}" />
<TextBlock
Margin="{StaticResource MediumBottomMargin}"
Style="{StaticResource DetailBodyStyle}"
Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis hendrerit nulla, vel molestie libero. In nec ultricies magna, ultricies molestie ipsum. Mauris non dignissim velit. Etiam malesuada blandit mauris eu maximus. Quisque ornare, felis nec scelerisque mollis, risus dolor posuere magna, in gravida quam mi id nisi. Nullam mattis consequat ex. Cras nulla neque, dictum ac urna et, vestibulum feugiat ex. Pellentesque malesuada accumsan ligula, vel fringilla lacus facilisis sit amet. Proin convallis tempor arcu, ac placerat libero pretium ut. Praesent hendrerit nisl at lobortis viverra. Fusce vitae velit odio. Nam ut tortor sed purus finibus sollicitudin quis at ante. Ut sodales dolor vel eros mollis suscipit. Donec eu nulla id urna ultricies consequat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@ -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);
}
}
}

View File

@ -0,0 +1,100 @@
<Page
x:Class="Estara.Views.ListDetailsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:models="using:Estara.Core.Models"
xmlns:views="using:Estara.Views"
xmlns:behaviors="using:Estara.Behaviors"
behaviors:NavigationViewHeaderBehavior.HeaderMode="Never"
mc:Ignorable="d">
<Page.Resources>
<DataTemplate x:Key="ItemTemplate" x:DataType="models:SampleOrder">
<Grid Height="60">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
Grid.Column="0"
VerticalAlignment="Center"
FontSize="32"
Glyph="{x:Bind Symbol}"
AutomationProperties.Name="{x:Bind SymbolName}" />
<StackPanel
Grid.Column="1"
Margin="{StaticResource SmallLeftMargin}"
VerticalAlignment="Center">
<TextBlock Text="{x:Bind Company}" Style="{StaticResource ListTitleStyle}" />
<TextBlock Text="{x:Bind Status}" Style="{StaticResource ListSubTitleStyle}" />
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="DetailsTemplate">
<Grid>
<views:ListDetailsDetailControl ListDetailsMenuItem="{Binding}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="NoSelectionContentTemplate">
<Grid>
<TextBlock
x:Uid="ListDetails_NoSelection"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextAlignment="Center"
Style="{ThemeResource SubtitleTextBlockStyle}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="ListHeaderTemplate">
<Grid Height="40">
<TextBlock
VerticalAlignment="Center"
Style="{StaticResource ListTitleStyle}"
Text="{Binding}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="MinimalListHeaderTemplate">
<Grid Height="40">
<TextBlock
Margin="96,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource ListTitleStyle}"
Text="{Binding}" />
</Grid>
</DataTemplate>
</Page.Resources>
<Grid x:Name="ContentArea">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState>
<VisualState.StateTriggers>
<!--641 is the default CompactModeThresholdWidth in NavigationView -->
<AdaptiveTrigger MinWindowWidth="641" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ListDetailsViewControl.ListHeaderTemplate" Value="{StaticResource ListHeaderTemplate}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<controls:ListDetailsView
x:Uid="ListDetails"
x:Name="ListDetailsViewControl"
BackButtonBehavior="Manual"
Background="Transparent"
BorderBrush="Transparent"
DetailsTemplate="{StaticResource DetailsTemplate}"
ItemsSource="{x:Bind ViewModel.SampleItems}"
ItemTemplate="{StaticResource ItemTemplate}"
ListHeaderTemplate="{StaticResource MinimalListHeaderTemplate}"
NoSelectionContentTemplate="{StaticResource NoSelectionContentTemplate}"
SelectedItem="{x:Bind ViewModel.Selected, Mode=TwoWay}"
ViewStateChanged="OnViewStateChanged"/>
</Grid>
</Page>

View File

@ -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<ListDetailsViewModel>();
InitializeComponent();
}
private void OnViewStateChanged(object sender, ListDetailsViewState e)
{
if (e == ListDetailsViewState.Both)
{
ViewModel.EnsureItemSelected();
}
}
}

View File

@ -0,0 +1,12 @@
<Page
x:Class="Estara.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid x:Name="ContentArea">
</Grid>
</Page>

View File

@ -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<MainViewModel>();
InitializeComponent();
}
}

View File

@ -0,0 +1,67 @@
<Page
x:Class="Estara.Views.SettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="using:Estara.Helpers"
xmlns:xaml="using:Microsoft.UI.Xaml"
mc:Ignorable="d">
<Page.Resources>
<helpers:EnumToBooleanConverter x:Key="EnumToBooleanConverter" />
</Page.Resources>
<Grid>
<StackPanel
x:Name="ContentArea">
<TextBlock x:Uid="Settings_Personalization" Style="{ThemeResource SubtitleTextBlockStyle}" />
<StackPanel Margin="{StaticResource SmallTopBottomMargin}">
<TextBlock x:Uid="Settings_Theme" />
<StackPanel Margin="{StaticResource XSmallTopMargin}">
<RadioButton
x:Uid="Settings_Theme_Light"
Command="{x:Bind ViewModel.SwitchThemeCommand}"
IsChecked="{x:Bind ViewModel.ElementTheme, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter=Light, Mode=OneWay}"
FontSize="15"
GroupName="AppTheme">
<RadioButton.CommandParameter>
<xaml:ElementTheme>Light</xaml:ElementTheme>
</RadioButton.CommandParameter>
</RadioButton>
<RadioButton
x:Uid="Settings_Theme_Dark"
Command="{x:Bind ViewModel.SwitchThemeCommand}"
IsChecked="{x:Bind ViewModel.ElementTheme, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter=Dark, Mode=OneWay}"
FontSize="15"
GroupName="AppTheme">
<RadioButton.CommandParameter>
<xaml:ElementTheme>Dark</xaml:ElementTheme>
</RadioButton.CommandParameter>
</RadioButton>
<RadioButton
x:Uid="Settings_Theme_Default"
Command="{x:Bind ViewModel.SwitchThemeCommand}"
IsChecked="{x:Bind ViewModel.ElementTheme, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter=Default, Mode=OneWay}"
FontSize="15"
GroupName="AppTheme">
<RadioButton.CommandParameter>
<xaml:ElementTheme>Default</xaml:ElementTheme>
</RadioButton.CommandParameter>
</RadioButton>
</StackPanel>
</StackPanel>
<TextBlock x:Uid="Settings_About" Style="{ThemeResource SubtitleTextBlockStyle}" />
<StackPanel Margin="{StaticResource XSmallTopMargin}">
<TextBlock Text="{x:Bind ViewModel.VersionDescription, Mode=OneWay}" Style="{ThemeResource BodyTextBlockStyle}" />
<TextBlock
x:Uid="Settings_AboutDescription"
Margin="{StaticResource XSmallTopMargin}"
Style="{ThemeResource BodyTextBlockStyle}" />
<HyperlinkButton x:Uid="SettingsPage_PrivacyTermsLink" Margin="{StaticResource SettingsPageHyperlinkButtonMargin}" />
</StackPanel>
</StackPanel>
</Grid>
</Page>

View File

@ -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<SettingsViewModel>();
InitializeComponent();
}
}

View File

@ -0,0 +1,95 @@
<Page
x:Class="Estara.Views.ShellPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="using:Estara.Helpers"
xmlns:behaviors="using:Estara.Behaviors"
xmlns:i="using:Microsoft.Xaml.Interactivity"
Loaded="OnLoaded">
<Grid>
<Grid x:Name="AppTitleBar"
Canvas.ZIndex="1"
Height="{Binding ElementName=NavigationViewControl, Path=CompactPaneLength}"
IsHitTestVisible="True"
VerticalAlignment="Top">
<Image Source="/Assets/WindowIcon.ico"
HorizontalAlignment="Left"
Width="16"
Height="16" />
<TextBlock x:Name="AppTitleBarText"
VerticalAlignment="Center"
TextWrapping="NoWrap"
Style="{StaticResource CaptionTextBlockStyle}"
Margin="28,0,0,0"/>
</Grid>
<NavigationView
x:Name="NavigationViewControl"
Canvas.ZIndex="0"
IsBackButtonVisible="Visible"
IsBackEnabled="{x:Bind ViewModel.IsBackEnabled, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.Selected, Mode=OneWay}"
IsSettingsVisible="True"
ExpandedModeThresholdWidth="1280"
DisplayModeChanged="NavigationViewControl_DisplayModeChanged"
Header="{x:Bind ((ContentControl)ViewModel.Selected).Content, Mode=OneWay}">
<NavigationView.MenuItems>
<!--
TODO: Update item titles by updating <x:Uid>.Content entries in Strings/en-us/Resources.resw.
https://docs.microsoft.com/windows/uwp/app-resources/localize-strings-ui-manifest#refer-to-a-string-resource-identifier-from-xaml
TODO: Update item icons by updating FontIcon.Glyph properties.
https://docs.microsoft.com/windows/apps/design/style/segoe-fluent-icons-font#icon-list
-->
<NavigationViewItem x:Uid="Shell_Main" helpers:NavigationHelper.NavigateTo="Estara.ViewModels.MainViewModel">
<NavigationViewItem.Icon>
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="&#xe7c3;"/>
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem x:Uid="Shell_ListDetails" helpers:NavigationHelper.NavigateTo="Estara.ViewModels.ListDetailsViewModel">
<NavigationViewItem.Icon>
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="&#xea37;"/>
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem x:Uid="Shell_ContentGrid" helpers:NavigationHelper.NavigateTo="Estara.ViewModels.ContentGridViewModel">
<NavigationViewItem.Icon>
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="&#xf0e2;"/>
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem x:Uid="Shell_DataGrid" helpers:NavigationHelper.NavigateTo="Estara.ViewModels.DataGridViewModel">
<NavigationViewItem.Icon>
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="&#xe80a;"/>
</NavigationViewItem.Icon>
</NavigationViewItem>
</NavigationView.MenuItems>
<NavigationView.HeaderTemplate>
<DataTemplate>
<Grid>
<TextBlock
Text="{Binding}"
Style="{ThemeResource TitleTextBlockStyle}" />
</Grid>
</DataTemplate>
</NavigationView.HeaderTemplate>
<i:Interaction.Behaviors>
<behaviors:NavigationViewHeaderBehavior
DefaultHeader="{x:Bind ((ContentControl)ViewModel.Selected).Content, Mode=OneWay}">
<behaviors:NavigationViewHeaderBehavior.DefaultHeaderTemplate>
<DataTemplate>
<Grid>
<TextBlock
Text="{Binding}"
Style="{ThemeResource TitleTextBlockStyle}" />
</Grid>
</DataTemplate>
</behaviors:NavigationViewHeaderBehavior.DefaultHeaderTemplate>
</behaviors:NavigationViewHeaderBehavior>
</i:Interaction.Behaviors>
<Grid Margin="{StaticResource NavigationViewPageContentMargin}">
<Frame x:Name="NavigationFrame" />
</Grid>
</NavigationView>
</Grid>
</Page>

View File

@ -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<INavigationService>();
var result = navigationService.GoBack();
args.Handled = result;
}
}

15
Estara/app.manifest Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Estara.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

6
Estara/appsettings.json Normal file
View File

@ -0,0 +1,6 @@
{
"LocalSettingsOptions": {
"ApplicationDataFolder": "Estara/ApplicationData",
"LocalSettingsFile": "LocalSettings.json"
}
}

View File

@ -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).
```
Estara - A Kiseki experiment
Copyright (C) 2023 Kiseki <inbox@kiseki.lol>
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 <https://www.gnu.org/licenses/>.
```