diff --git a/Billing.Shared/Billing.Shared.projitems b/Billing.Shared/Billing.Shared.projitems index cab1ba6..a25db1d 100644 --- a/Billing.Shared/Billing.Shared.projitems +++ b/Billing.Shared/Billing.Shared.projitems @@ -36,12 +36,13 @@ + + AccountPage.xaml AddAccountPage.xaml - Code AddBillPage.xaml @@ -67,6 +68,10 @@ Designer MSBuild:UpdateDesignTimeXaml + + Designer + MSBuild:UpdateDesignTimeXaml + Designer MSBuild:UpdateDesignTimeXaml @@ -80,9 +85,4 @@ MSBuild:UpdateDesignTimeXaml - - - MSBuild:UpdateDesignTimeXaml - - \ No newline at end of file diff --git a/Billing.Shared/Models/Account.cs b/Billing.Shared/Models/Account.cs index bc2a069..3dadad5 100644 --- a/Billing.Shared/Models/Account.cs +++ b/Billing.Shared/Models/Account.cs @@ -8,6 +8,7 @@ namespace Billing.Models public string Icon { get; set; } = ICON_DEFAULT; public AccountCategory Category { get; set; } public string Name { get; set; } + public decimal Initial { get; set; } public decimal Balance { get; set; } public string Memo { get; set; } @@ -17,6 +18,7 @@ namespace Billing.Models Icon = Read(node, nameof(Icon), ICON_DEFAULT); Category = (AccountCategory)Read(node, nameof(Category), 0); Name = Read(node, nameof(Name), string.Empty); + Initial = Read(node, nameof(Initial), 0m); Balance = Read(node, nameof(Balance), 0m); Memo = Read(node, nameof(Memo), null); } @@ -27,6 +29,7 @@ namespace Billing.Models Write(node, nameof(Icon), Icon); Write(node, nameof(Category), (int)Category); Write(node, nameof(Name), Name); + Write(node, nameof(Initial), Initial); Write(node, nameof(Balance), Balance); Write(node, nameof(Memo), Memo); } diff --git a/Billing.Shared/UI/BillingDate.xaml.cs b/Billing.Shared/UI/BillingDate.xaml.cs index e205c79..a87ed25 100644 --- a/Billing.Shared/UI/BillingDate.xaml.cs +++ b/Billing.Shared/UI/BillingDate.xaml.cs @@ -7,13 +7,13 @@ namespace Billing.UI { #region UI Properties - private static readonly BindableProperty SundayProperty = BindableProperty.Create(nameof(Sunday), typeof(BillingDay), typeof(BillingDate)); - private static readonly BindableProperty MondayProperty = BindableProperty.Create(nameof(Monday), typeof(BillingDay), typeof(BillingDate)); - private static readonly BindableProperty TuesdayProperty = BindableProperty.Create(nameof(Tuesday), typeof(BillingDay), typeof(BillingDate)); - private static readonly BindableProperty WednesdayProperty = BindableProperty.Create(nameof(Wednesday), typeof(BillingDay), typeof(BillingDate)); - private static readonly BindableProperty ThursdayProperty = BindableProperty.Create(nameof(Thursday), typeof(BillingDay), typeof(BillingDate)); - private static readonly BindableProperty FridayProperty = BindableProperty.Create(nameof(Friday), typeof(BillingDay), typeof(BillingDate)); - private static readonly BindableProperty SaturdayProperty = BindableProperty.Create(nameof(Saturday), typeof(BillingDay), typeof(BillingDate)); + public static readonly BindableProperty SundayProperty = BindableProperty.Create(nameof(Sunday), typeof(BillingDay), typeof(BillingDate)); + public static readonly BindableProperty MondayProperty = BindableProperty.Create(nameof(Monday), typeof(BillingDay), typeof(BillingDate)); + public static readonly BindableProperty TuesdayProperty = BindableProperty.Create(nameof(Tuesday), typeof(BillingDay), typeof(BillingDate)); + public static readonly BindableProperty WednesdayProperty = BindableProperty.Create(nameof(Wednesday), typeof(BillingDay), typeof(BillingDate)); + public static readonly BindableProperty ThursdayProperty = BindableProperty.Create(nameof(Thursday), typeof(BillingDay), typeof(BillingDate)); + public static readonly BindableProperty FridayProperty = BindableProperty.Create(nameof(Friday), typeof(BillingDay), typeof(BillingDate)); + public static readonly BindableProperty SaturdayProperty = BindableProperty.Create(nameof(Saturday), typeof(BillingDay), typeof(BillingDate)); public BillingDay Sunday => (BillingDay)GetValue(SundayProperty); public BillingDay Monday => (BillingDay)GetValue(MondayProperty); @@ -219,12 +219,12 @@ namespace Billing.UI public class BillingDay : BindableObject { - private static readonly BindableProperty DateProperty = BindableProperty.Create(nameof(Date), typeof(DateTime), typeof(BillingDay), propertyChanged: OnDatePropertyChanged); - private static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(BillingDay)); - private static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(BillingDay), defaultValue: Definition.GetCascadiaRegularFontFamily()); - private static readonly BindableProperty IsSelectedProperty = BindableProperty.Create(nameof(IsSelected), typeof(bool), typeof(BillingDay)); - private static readonly BindableProperty OpacityProperty = BindableProperty.Create(nameof(Opacity), typeof(double), typeof(BillingDay), defaultValue: 1.0); - private static readonly BindableProperty TextOpacityProperty = BindableProperty.Create(nameof(TextOpacity), typeof(double), typeof(BillingDay), defaultValue: 1.0); + public static readonly BindableProperty DateProperty = BindableProperty.Create(nameof(Date), typeof(DateTime), typeof(BillingDay), propertyChanged: OnDatePropertyChanged); + public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(BillingDay)); + public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(BillingDay), defaultValue: Definition.GetCascadiaRegularFontFamily()); + public static readonly BindableProperty IsSelectedProperty = BindableProperty.Create(nameof(IsSelected), typeof(bool), typeof(BillingDay)); + public static readonly BindableProperty OpacityProperty = BindableProperty.Create(nameof(Opacity), typeof(double), typeof(BillingDay), defaultValue: 1.0); + public static readonly BindableProperty TextOpacityProperty = BindableProperty.Create(nameof(TextOpacity), typeof(double), typeof(BillingDay), defaultValue: 1.0); private static void OnDatePropertyChanged(BindableObject obj, object old, object @new) { diff --git a/Billing.Shared/UI/GroupStackLayout.cs b/Billing.Shared/UI/GroupStackLayout.cs new file mode 100644 index 0000000..0ae2020 --- /dev/null +++ b/Billing.Shared/UI/GroupStackLayout.cs @@ -0,0 +1,159 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using Xamarin.Forms; + +namespace Billing.UI +{ + public class GroupStackLayout : Layout + { + public static readonly BindableProperty GroupHeaderTemplateProperty = BindableProperty.Create(nameof(GroupHeaderTemplate), typeof(DataTemplate), typeof(GroupStackLayout)); + public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(GroupStackLayout)); + public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IList), typeof(GroupStackLayout), propertyChanged: OnItemsSourcePropertyChanged); + public static readonly BindableProperty SpacingProperty = BindableProperty.Create(nameof(Spacing), typeof(double), typeof(GroupStackLayout), defaultValue: 4d); + + public DataTemplate GroupHeaderTemplate + { + get => (DataTemplate)GetValue(GroupHeaderTemplateProperty); + set => SetValue(GroupHeaderTemplateProperty, value); + } + public DataTemplate ItemTemplate + { + get => (DataTemplate)GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + public IList ItemsSource + { + get => (IList)GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + public double Spacing + { + get => (double)GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + private static void OnItemsSourcePropertyChanged(BindableObject obj, object old, object @new) + { + var stack = (GroupStackLayout)obj; + stack.lastWidth = -1; + if (@new == null) + { + stack.cachedLayout.Clear(); + stack.Children.Clear(); + stack.InvalidateLayout(); + } + else if (@new is IList list) + { + stack.freezed = true; + stack.cachedLayout.Clear(); + stack.Children.Clear(); + var groupTemplate = stack.GroupHeaderTemplate; + var itemTemplate = stack.ItemTemplate; + for (var i = 0; i < list.Count; i++) + { + var item = list[i]; + if (item is IList sublist) + { + if (groupTemplate != null) + { + var child = groupTemplate.CreateContent(); + if (child is View view) + { + view.BindingContext = item; + stack.Children.Add(view); + } + } + foreach (var it in sublist) + { + var child = itemTemplate.CreateContent(); + if (child is View view) + { + view.BindingContext = it; + stack.Children.Add(view); + } + } + } + else + { + var child = itemTemplate.CreateContent(); + if (child is View view) + { + view.BindingContext = list[i]; + stack.Children.Add(view); + } + } + } + stack.freezed = false; + stack.UpdateChildrenLayout(); + stack.InvalidateLayout(); + } + } + + private readonly Dictionary cachedLayout = new(); + + private bool freezed; + private double lastWidth = -1; + private SizeRequest lastSizeRequest; + + public void Refresh(IList list) + { + OnItemsSourcePropertyChanged(this, null, list); + } + + protected override void LayoutChildren(double x, double y, double width, double height) + { + if (freezed) + { + return; + } + var source = ItemsSource; + if (source == null || source.Count <= 0) + { + return; + } + var spacing = Spacing; + var lastHeight = 0.0; + foreach (var item in Children) + { + var measured = item.Measure(width, height, MeasureFlags.IncludeMargins); + var rect = new Rectangle( + 0, lastHeight, width, + measured.Request.Height); + if (cachedLayout.TryGetValue(item, out var v)) + { + if (v != rect) + { + cachedLayout[item] = rect; + item.Layout(rect); + } + } + else + { + cachedLayout.Add(item, rect); + item.Layout(rect); + } + lastHeight += rect.Height + spacing; + } + } + + protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) + { + if (lastWidth == widthConstraint) + { + return lastSizeRequest; + } + lastWidth = widthConstraint; + var spacing = Spacing; + var lastHeight = 0.0; + foreach (var item in Children) + { + var measured = item.Measure(widthConstraint, heightConstraint, MeasureFlags.IncludeMargins); + lastHeight += measured.Request.Height + spacing; + } + lastSizeRequest = new SizeRequest(new Size(widthConstraint, lastHeight)); + return lastSizeRequest; + } + } +} diff --git a/Billing.Shared/UI/ItemSelectPage.cs b/Billing.Shared/UI/ItemSelectPage.cs new file mode 100644 index 0000000..12c7446 --- /dev/null +++ b/Billing.Shared/UI/ItemSelectPage.cs @@ -0,0 +1,51 @@ +using Billing.Themes; +using System.Collections; + +using Xamarin.Forms; + +namespace Billing.UI +{ + public class ItemSelectPage : ContentPage + { + public ItemSelectPage(IList source) + { + Content = new ListView + { + ItemsSource = source, + ItemTemplate = new DataTemplate(() => new StackLayout + { + Orientation = StackOrientation.Horizontal, + Padding = new Thickness(20, 0), + Spacing = 10, + Children = + { + new Image + { + WidthRequest = 22, + HeightRequest = 22, + Aspect = Aspect.AspectFit, + VerticalOptions = LayoutOptions.Center + } + .Binding(Image.SourceProperty, "Icon"), + + new Label + { + VerticalOptions = LayoutOptions.Center, + LineBreakMode = LineBreakMode.TailTruncation + } + .Binding(Label.TextProperty, "Name") + .DynamicResource(Label.TextColorProperty, BaseTheme.TextColor) + } + }) + } + .DynamicResource(BackgroundColorProperty, BaseTheme.WindowBackgroundColor); + } + } + + public class SelectItem + { + public string Icon { get; set; } + public T Value { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/Billing.Shared/UI/OptionsCells.cs b/Billing.Shared/UI/OptionsCells.cs index f84dd11..a71167c 100644 --- a/Billing.Shared/UI/OptionsCells.cs +++ b/Billing.Shared/UI/OptionsCells.cs @@ -4,7 +4,6 @@ using Xamarin.Forms; namespace Billing.UI { - public class OptionEntry : Entry { } public class OptionEditor : Editor { } diff --git a/Billing.Shared/Views/AccountPage.xaml b/Billing.Shared/Views/AccountPage.xaml index 598a4f7..86fb32c 100644 --- a/Billing.Shared/Views/AccountPage.xaml +++ b/Billing.Shared/Views/AccountPage.xaml @@ -13,7 +13,7 @@ - + @@ -42,8 +42,17 @@ Text="{Binding Liability, Converter={StaticResource moneyConverter}}"/> - - + + + + + + + + @@ -56,8 +65,8 @@ - - + + \ No newline at end of file diff --git a/Billing.Shared/Views/AccountPage.xaml.cs b/Billing.Shared/Views/AccountPage.xaml.cs index d965a13..1c843dd 100644 --- a/Billing.Shared/Views/AccountPage.xaml.cs +++ b/Billing.Shared/Views/AccountPage.xaml.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using Billing.Models; using Billing.UI; using Xamarin.Forms; @@ -10,21 +12,21 @@ namespace Billing.Views private static readonly BindableProperty BalanceProperty = BindableProperty.Create(nameof(Balance), typeof(decimal), typeof(AccountPage)); private static readonly BindableProperty AssetProperty = BindableProperty.Create(nameof(Asset), typeof(decimal), typeof(AccountPage)); private static readonly BindableProperty LiabilityProperty = BindableProperty.Create(nameof(Liability), typeof(decimal), typeof(AccountPage)); - private static readonly BindableProperty AccountsProperty = BindableProperty.Create(nameof(Accounts), typeof(ObservableCollection), typeof(AccountPage)); + private static readonly BindableProperty AccountsProperty = BindableProperty.Create(nameof(Accounts), typeof(List), typeof(AccountPage)); public decimal Balance => (decimal)GetValue(BalanceProperty); public decimal Asset => (decimal)GetValue(AssetProperty); public decimal Liability => (decimal)GetValue(LiabilityProperty); - public ObservableCollection Accounts => (ObservableCollection)GetValue(AccountsProperty); + public List Accounts => (List)GetValue(AccountsProperty); public Command AddAccount { get; } - private readonly ObservableCollection accounts; + private readonly List accounts; public AccountPage() { AddAccount = new Command(OnAddAccount); - accounts = new ObservableCollection(); + accounts = new List(); SetValue(AccountsProperty, accounts); InitializeComponent(); @@ -47,7 +49,27 @@ namespace Billing.Views private void AccountChecked(object sender, AccountEventArgs e) { Helper.Debug(e.Account.ToString()); - accounts.Add(e.Account); + var group = accounts.FirstOrDefault(g => g.Key == e.Account.Category); + if (group == null) + { + group = new AccountGrouping(e.Account.Category) + { + e.Account + }; + accounts.Add(group); + } + else + { + group.Add(e.Account); + } + groupLayout.Refresh(accounts); } } + + public class AccountGrouping : List, IGrouping + { + public AccountGrouping(AccountCategory key) : base() => Key = key; + public AccountCategory Key { get; } + public decimal Balance { get; set; } + } } \ No newline at end of file diff --git a/Billing.Shared/Views/AddAccountPage.xaml b/Billing.Shared/Views/AddAccountPage.xaml index 37761d7..07e1dae 100644 --- a/Billing.Shared/Views/AddAccountPage.xaml +++ b/Billing.Shared/Views/AddAccountPage.xaml @@ -13,7 +13,6 @@ - @@ -23,14 +22,16 @@ - - + - @@ -38,20 +39,22 @@ - - + - + Placeholder="{r:Text MemoPlaceholder}"/> diff --git a/Billing.Shared/Views/AddAccountPage.xaml.cs b/Billing.Shared/Views/AddAccountPage.xaml.cs index 6b365ab..87e93a6 100644 --- a/Billing.Shared/Views/AddAccountPage.xaml.cs +++ b/Billing.Shared/Views/AddAccountPage.xaml.cs @@ -1,6 +1,8 @@ -using Billing.Models; +using Billing.Languages; +using Billing.Models; using Billing.UI; using System; +using System.Collections.Generic; using Xamarin.Forms; namespace Billing.Views @@ -69,16 +71,6 @@ namespace Billing.Views InitializeComponent(); } - private void Balance_Unfocused(object sender, FocusEventArgs e) - { - if (sender is OptionEntry entry && decimal.TryParse(entry.Text, out decimal d)) - { - var converter = (MoneyConverter)Resources["moneyConverter"]; - entry.Text = converter.Convert(d, null, null, null)?.ToString(); - //Balance = d; - } - } - private async void OnCheckAccount() { if (Tap.IsBusy) @@ -108,9 +100,22 @@ namespace Billing.Views } - private void OnSelectCategory() + private async void OnSelectCategory() { - + if (Tap.IsBusy) + { + return; + } + using (Tap.Start()) + { + await Navigation.PushAsync(new ItemSelectPage(new List> + { + new() { Icon = "sackdollar", Value = AccountCategory.Cash, Name = Resource.Cash }, + new() { Icon = "creditcard", Value = AccountCategory.CreditCard, Name = Resource.CreditCard }, + new() { Icon = "", Value = AccountCategory.DebitCard, Name = Resource.DebitCard }, + new() { Icon = "", Value = AccountCategory.ElecAccount, Name = Resource.ElecAccount } + })); + } } } diff --git a/Billing/Billing.Android/Resources/drawable-mdpi/creditcard.png b/Billing/Billing.Android/Resources/drawable-mdpi/creditcard.png new file mode 100644 index 0000000..948d197 Binary files /dev/null and b/Billing/Billing.Android/Resources/drawable-mdpi/creditcard.png differ diff --git a/Billing/Billing.Android/Resources/drawable-xhdpi/creditcard.png b/Billing/Billing.Android/Resources/drawable-xhdpi/creditcard.png new file mode 100644 index 0000000..8bf47e4 Binary files /dev/null and b/Billing/Billing.Android/Resources/drawable-xhdpi/creditcard.png differ diff --git a/Billing/Billing.Android/Resources/drawable-xxhdpi/creditcard.png b/Billing/Billing.Android/Resources/drawable-xxhdpi/creditcard.png new file mode 100644 index 0000000..d8f2b29 Binary files /dev/null and b/Billing/Billing.Android/Resources/drawable-xxhdpi/creditcard.png differ diff --git a/Billing/Billing.Android/Resources/drawable/creditcard.png b/Billing/Billing.Android/Resources/drawable/creditcard.png new file mode 100644 index 0000000..e40b05d Binary files /dev/null and b/Billing/Billing.Android/Resources/drawable/creditcard.png differ diff --git a/Billing/Billing.iOS/Resources/creditcard.png b/Billing/Billing.iOS/Resources/creditcard.png new file mode 100644 index 0000000..948d197 Binary files /dev/null and b/Billing/Billing.iOS/Resources/creditcard.png differ diff --git a/Billing/Billing.iOS/Resources/creditcard@2x.png b/Billing/Billing.iOS/Resources/creditcard@2x.png new file mode 100644 index 0000000..8bf47e4 Binary files /dev/null and b/Billing/Billing.iOS/Resources/creditcard@2x.png differ diff --git a/Billing/Billing.iOS/Resources/creditcard@3x.png b/Billing/Billing.iOS/Resources/creditcard@3x.png new file mode 100644 index 0000000..d8f2b29 Binary files /dev/null and b/Billing/Billing.iOS/Resources/creditcard@3x.png differ