diff --git a/Billing.Shared/App.cs b/Billing.Shared/App.cs index 6fb0e23..5bb2751 100644 --- a/Billing.Shared/App.cs +++ b/Billing.Shared/App.cs @@ -1,4 +1,8 @@ -using Billing.Languages; +using System.Collections.Generic; +using System.Threading.Tasks; +using Billing.Languages; +using Billing.Models; +using Billing.Store; using Billing.Themes; using Xamarin.Essentials; using Xamarin.Forms; @@ -9,6 +13,13 @@ namespace Billing { public static AppTheme CurrentTheme { get; private set; } public static PlatformCulture CurrentCulture { get; private set; } + public static List<Bill> Bills => bills ??= new List<Bill>(); + public static List<Account> Accounts => accounts ??= new List<Account>(); + public static List<Category> Categories => categories ??= new List<Category>(); + + private static List<Bill> bills; + private static List<Account> accounts; + private static List<Category> categories; public App() { @@ -19,8 +30,21 @@ namespace Billing Shell.Current.GoToAsync("//Bills"); } + public static void WriteAccounts() => StoreHelper.WriteAccounts(accounts); + + public static void WriteBills() => StoreHelper.WriteBills(bills); + protected override void OnStart() { + Helper.Debug($"personal folder: {StoreHelper.PersonalFolder}"); + Helper.Debug($"cache folder: {StoreHelper.CacheFolder}"); + + Task.Run(() => + { + accounts = StoreHelper.GetAccounts(); + categories = StoreHelper.GetCategories(); + bills = StoreHelper.GetBills(); + }); } protected override void OnResume() diff --git a/Billing.Shared/Billing.Shared.projitems b/Billing.Shared/Billing.Shared.projitems index ddb5bab..c3b350f 100644 --- a/Billing.Shared/Billing.Shared.projitems +++ b/Billing.Shared/Billing.Shared.projitems @@ -22,7 +22,6 @@ <DependentUpon>MainShell.xaml</DependentUpon> </Compile> <Compile Include="$(MSBuildThisFileDirectory)Models\BaseModel.cs" /> - <Compile Include="$(MSBuildThisFileDirectory)Models\Billing.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Models\Category.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Models\Account.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Themes\BaseTheme.cs" /> @@ -59,6 +58,8 @@ <DependentUpon>SettingPage.xaml</DependentUpon> </Compile> <Compile Include="$(MSBuildThisFileDirectory)UI\OptionsCells.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)Store\StoreHelper.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)Models\Bill.cs" /> </ItemGroup> <ItemGroup> <EmbeddedResource Include="$(MSBuildThisFileDirectory)MainShell.xaml"> @@ -95,4 +96,7 @@ <Generator>MSBuild:UpdateDesignTimeXaml</Generator> </EmbeddedResource> </ItemGroup> + <ItemGroup> + <Folder Include="$(MSBuildThisFileDirectory)Store\" /> + </ItemGroup> </Project> \ No newline at end of file diff --git a/Billing.Shared/Helper.cs b/Billing.Shared/Helper.cs index b8a20ee..5b68076 100644 --- a/Billing.Shared/Helper.cs +++ b/Billing.Shared/Helper.cs @@ -1,9 +1,11 @@ using System; +using Xamarin.Essentials; namespace Billing { internal static class Helper { +#if DEBUG public static void Debug(string message) { var time = DateTime.Now.ToString("HH:mm:ss.fff"); @@ -20,5 +22,36 @@ namespace Billing var time = DateTime.Now.ToString("HH:mm:ss.fff"); System.Diagnostics.Debug.Fail($"[{time}] - {category}", message); } +#else +#pragma warning disable IDE0060 // Remove unused parameter + public static void Debug(string message) + { + } + + public static void Error(string category, Exception ex) + { + } + + public static void Error(string category, string message) + { + } +#pragma warning restore IDE0060 // Remove unused parameter +#endif + + public static bool NetworkAvailable + { + get + { + try + { + return Connectivity.NetworkAccess == NetworkAccess.Internet + || Connectivity.NetworkAccess == NetworkAccess.ConstrainedInternet; + } + catch + { + return false; + } + } + } } } \ No newline at end of file diff --git a/Billing.Shared/Languages/Resource.cs b/Billing.Shared/Languages/Resource.cs index 98fb69d..261f260 100644 --- a/Billing.Shared/Languages/Resource.cs +++ b/Billing.Shared/Languages/Resource.cs @@ -14,6 +14,8 @@ namespace Billing.Languages public static string CreditCard => Text(nameof(CreditCard)); public static string DebitCard => Text(nameof(DebitCard)); public static string ElecAccount => Text(nameof(ElecAccount)); + public static string AddBill => Text(nameof(AddBill)); + public static string EditBill => Text(nameof(EditBill)); static readonly Dictionary<string, LanguageResource> dict = new(); diff --git a/Billing.Shared/Languages/en.xml b/Billing.Shared/Languages/en.xml index bbabe70..dd6340f 100644 --- a/Billing.Shared/Languages/en.xml +++ b/Billing.Shared/Languages/en.xml @@ -32,4 +32,8 @@ <DebitCard>Debit Card</DebitCard> <ElecAccount>Electronic Account</ElecAccount> <IconSelector>Icon Selection</IconSelector> + <AddBill>Add Billing</AddBill> + <EditBill>Edit Billing</EditBill> + <Account>Account</Account> + <CreatedTime>Created Time</CreatedTime> </root> \ No newline at end of file diff --git a/Billing.Shared/Languages/zh-CN.xml b/Billing.Shared/Languages/zh-CN.xml index 5441ff3..1933fd2 100644 --- a/Billing.Shared/Languages/zh-CN.xml +++ b/Billing.Shared/Languages/zh-CN.xml @@ -32,4 +32,8 @@ <DebitCard>储蓄卡</DebitCard> <ElecAccount>电子账户</ElecAccount> <IconSelector>图标选择</IconSelector> + <AddBill>增加账单</AddBill> + <EditBill>编辑账单</EditBill> + <Account>账户</Account> + <CreatedTime>创建时间</CreatedTime> </root> \ No newline at end of file diff --git a/Billing.Shared/Models/BaseModel.cs b/Billing.Shared/Models/BaseModel.cs index c5a2d6c..0dbdb62 100644 --- a/Billing.Shared/Models/BaseModel.cs +++ b/Billing.Shared/Models/BaseModel.cs @@ -161,11 +161,12 @@ namespace Billing.Models public override string ToString() { - XDocument xdoc = ToXml(); using MemoryStream ms = new(); - using StreamWriter writer = new(ms, Encoding.UTF8); - xdoc.Save(writer, SaveOptions.DisableFormatting); - writer.Flush(); + //using StreamWriter writer = new(ms, Encoding.UTF8); + //XDocument xdoc = ToXml(); + //xdoc.Save(writer, SaveOptions.DisableFormatting); + //writer.Flush(); + SaveToStream(ms); ms.Seek(0, SeekOrigin.Begin); using StreamReader reader = new(ms, Encoding.UTF8); return reader.ReadToEnd(); diff --git a/Billing.Shared/Models/Billing.cs b/Billing.Shared/Models/Bill.cs similarity index 96% rename from Billing.Shared/Models/Billing.cs rename to Billing.Shared/Models/Bill.cs index bc17f1d..5e5242e 100644 --- a/Billing.Shared/Models/Billing.cs +++ b/Billing.Shared/Models/Bill.cs @@ -3,7 +3,7 @@ using System.Xml.Linq; namespace Billing.Models { - public class Billing : BaseModel + public class Bill : BaseModel { public decimal Amount { get; set; } public string Name { get; set; } diff --git a/Billing.Shared/Models/Category.cs b/Billing.Shared/Models/Category.cs index 4be6366..ef06c7f 100644 --- a/Billing.Shared/Models/Category.cs +++ b/Billing.Shared/Models/Category.cs @@ -5,6 +5,7 @@ namespace Billing.Models public class Category : BaseModel { public int Id { get; set; } + public CategoryType Type { get; set; } public string Icon { get; set; } = ICON_DEFAULT; public string Name { get; set; } public int? ParentId { get; set; } @@ -12,6 +13,7 @@ namespace Billing.Models public override void OnXmlDeserialize(XElement node) { Id = Read(node, nameof(Id), 0); + Type = (CategoryType)Read(node, nameof(Type), 0); Icon = Read(node, nameof(Icon), ICON_DEFAULT); Name = Read(node, nameof(Name), string.Empty); var parentId = Read(node, nameof(ParentId), -1); @@ -24,6 +26,7 @@ namespace Billing.Models public override void OnXmlSerialize(XElement node) { Write(node, nameof(Id), Id); + Write(node, nameof(Type), (int)Type); Write(node, nameof(Icon), Icon); Write(node, nameof(Name), Name); if (ParentId != null) @@ -32,4 +35,10 @@ namespace Billing.Models } } } + + public enum CategoryType + { + Spending, + Income + } } \ No newline at end of file diff --git a/Billing.Shared/Store/StoreHelper.cs b/Billing.Shared/Store/StoreHelper.cs new file mode 100644 index 0000000..a533a92 --- /dev/null +++ b/Billing.Shared/Store/StoreHelper.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Billing.Models; +using Billing.UI; +using Xamarin.Essentials; + +namespace Billing.Store +{ + public class StoreHelper + { + public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal); + public static readonly string CacheFolder = FileSystem.CacheDirectory; + + private const string accountFile = "accounts.xml"; + private const string billFile = "bills.xml"; + private const string categoryFile = "categories.xml"; + + private static StoreHelper instance; + private static StoreHelper Instance => instance ??= new StoreHelper(); + + public static List<Account> GetAccounts() => Instance.GetAccountsInternal(); + public static void WriteAccounts(IEnumerable<Account> accounts) => Instance.WriteAccountsInternal(accounts); + public static List<Bill> GetBills() => Instance.GetBillsInternal(); + public static void WriteBills(IEnumerable<Bill> bills) => Instance.WriteBillsInternal(bills); + public static List<Category> GetCategories() => Instance.GetCategoriesInternal(); + public static void WriteCategories(IEnumerable<Category> categories) => Instance.WriteCategoriesInternal(categories); + + private StoreHelper() { } + + private List<Account> GetAccountsInternal() + { + return GetList<Account>(Path.Combine(PersonalFolder, accountFile)); + } + + private void WriteAccountsInternal(IEnumerable<Account> accounts) + { + var filename = Path.Combine(PersonalFolder, accountFile); + WriteList(filename, accounts); + } + + private List<Bill> GetBillsInternal() + { + return GetList<Bill>(Path.Combine(PersonalFolder, billFile)); + } + + private void WriteBillsInternal(IEnumerable<Bill> bills) + { + var filename = Path.Combine(PersonalFolder, billFile); + WriteList(filename, bills); + } + + private List<Category> GetCategoriesInternal() + { + return GetList<Category>(Path.Combine(PersonalFolder, categoryFile)); + } + + private void WriteCategoriesInternal(IEnumerable<Category> categories) + { + var filename = Path.Combine(PersonalFolder, categoryFile); + WriteList(filename, categories); + } + + #region Helper + + private void WriteList<T>(string filename, IEnumerable<T> list) where T : IModel, new() + { + if (list == null) + { + return; + } + try + { + using var stream = File.OpenWrite(filename); + list.ToStream(stream); + } + catch (Exception ex) + { + Helper.Error("file.write", $"failed to write file: {filename}, error: {ex.Message}"); + } + } + + private List<T> GetList<T>(string file) where T : IModel, new() + { + try + { + if (File.Exists(file)) + { + using var stream = File.OpenRead(file); + var list = ModelExtensionHelper.FromStream<T>(stream); + return list; + } + } + catch (Exception ex) + { + Helper.Error("file.read", $"failed to read file: {file}, error: {ex.Message}"); + } + return default; + } + + #endregion + } +} diff --git a/Billing.Shared/Themes/BaseTheme.cs b/Billing.Shared/Themes/BaseTheme.cs index f19568f..e13bb04 100644 --- a/Billing.Shared/Themes/BaseTheme.cs +++ b/Billing.Shared/Themes/BaseTheme.cs @@ -23,6 +23,7 @@ namespace Billing.Themes public const string TextColor = nameof(TextColor); public const string SecondaryTextColor = nameof(SecondaryTextColor); public const string RedColor = nameof(RedColor); + public const string GreenColor = nameof(GreenColor); protected abstract Color PrimaryMauiColor { get; } protected abstract Color SecondaryMauiColor { get; } diff --git a/Billing.Shared/Themes/Dark.cs b/Billing.Shared/Themes/Dark.cs index 736470c..b67a811 100644 --- a/Billing.Shared/Themes/Dark.cs +++ b/Billing.Shared/Themes/Dark.cs @@ -28,6 +28,7 @@ namespace Billing.Themes Add(TextColor, Color.FromRgb(0xcc, 0xcc, 0xcc)); Add(SecondaryTextColor, Color.LightGray); Add(RedColor, Color.FromRgb(211, 5, 5)); + Add(GreenColor, Color.FromRgb(5, 211, 5)); Add(new Style(typeof(TabBar)) { diff --git a/Billing.Shared/Themes/Light.cs b/Billing.Shared/Themes/Light.cs index b3ad5ba..5511bd2 100644 --- a/Billing.Shared/Themes/Light.cs +++ b/Billing.Shared/Themes/Light.cs @@ -28,6 +28,7 @@ namespace Billing.Themes Add(TextColor, Color.FromRgb(0x33, 0x33, 0x33)); Add(SecondaryTextColor, Color.DimGray); Add(RedColor, Color.FromRgb(211, 64, 85)); + Add(RedColor, Color.FromRgb(64, 211, 85)); Add(new Style(typeof(TabBar)) { diff --git a/Billing.Shared/UI/Converters.cs b/Billing.Shared/UI/Converters.cs index 6202e8b..775cfde 100644 --- a/Billing.Shared/UI/Converters.cs +++ b/Billing.Shared/UI/Converters.cs @@ -1,5 +1,7 @@ using Billing.Languages; using Billing.Models; +using Billing.Themes; +using Billing.Views; using System; using System.Collections.Generic; using System.Globalization; @@ -20,18 +22,23 @@ namespace Billing.UI public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - return value; + throw new NotImplementedException(); } } public class MoneyConverter : IValueConverter { public bool MarkVisible { get; set; } = true; + public bool Absolute { get; set; } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is decimal d) { + if (Absolute) + { + d = Math.Abs(d); + } var number = d.ToString("n2"); if (MarkVisible) { @@ -60,6 +67,46 @@ namespace Billing.UI } } + public class BalanceColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var resource = Application.Current.Resources; + if (value is decimal d) + { + if (d >= 0) + { + return resource[BaseTheme.GreenColor]; + } + return resource[BaseTheme.RedColor]; + } + return resource[BaseTheme.TextColor]; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + public class UIBillConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is UIBill bill) + { + var time = bill.DateCreation.ToString("HH:mm"); + return $"{time} ({bill.Wallet})"; + } + return string.Empty; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + public class NotNullConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) @@ -69,7 +116,7 @@ namespace Billing.UI public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - return value; + throw new NotImplementedException(); } } @@ -93,7 +140,7 @@ namespace Billing.UI public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - return value; + throw new NotImplementedException(); } } @@ -151,7 +198,7 @@ namespace Billing.UI public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - return value; + throw new NotImplementedException(); } } } \ No newline at end of file diff --git a/Billing.Shared/UI/Definition.cs b/Billing.Shared/UI/Definition.cs index 2c5695b..e63c46f 100644 --- a/Billing.Shared/UI/Definition.cs +++ b/Billing.Shared/UI/Definition.cs @@ -1,4 +1,8 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Xml.Linq; +using Billing.Models; using Xamarin.Forms; namespace Billing.UI @@ -75,6 +79,51 @@ namespace Billing.UI } } + public static class ModelExtensionHelper + { + public static List<T> FromStream<T>(Stream stream) where T : IModel, new() + { + XDocument doc = XDocument.Load(stream); + var root = doc.Root; + var list = new List<T>(); + foreach (XElement ele in root.Elements("item")) + { + if (ele.Attribute("null")?.Value == "1") + { + list.Add(default); + } + else + { + T value = new(); + value.OnXmlDeserialize(ele); + list.Add(value); + } + } + return list; + } + + public static void ToStream<T>(this IEnumerable<T> list, Stream stream) where T : IModel + { + XElement root = new("root"); + foreach (var t in list) + { + XElement item = new("item"); + if (t == null) + { + item.Add(new XAttribute("null", 1)); + } + else + { + t.OnXmlSerialize(item); + } + root.Add(item); + } + + XDocument doc = new(new XDeclaration("1.0", "utf-8", "yes"), root); + doc.Save(stream, SaveOptions.DisableFormatting); + } + } + public class Tap : IDisposable { private readonly static object sync = new(); diff --git a/Billing.Shared/UI/GroupStackLayout.cs b/Billing.Shared/UI/GroupStackLayout.cs index 90e4eaf..053d57e 100644 --- a/Billing.Shared/UI/GroupStackLayout.cs +++ b/Billing.Shared/UI/GroupStackLayout.cs @@ -1,7 +1,4 @@ using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; using Xamarin.Forms; namespace Billing.UI diff --git a/Billing.Shared/Views/AccountPage.xaml b/Billing.Shared/Views/AccountPage.xaml index c615b84..be2bcd5 100644 --- a/Billing.Shared/Views/AccountPage.xaml +++ b/Billing.Shared/Views/AccountPage.xaml @@ -14,6 +14,7 @@ <ContentPage.Resources> <ui:MoneyConverter x:Key="moneyConverter"/> <ui:MoneyConverter x:Key="money2Converter" MarkVisible="False"/> + <ui:AccountCategoryConverter x:Key="categoryConverter"/> <ui:IconConverter x:Key="iconConverter"/> </ContentPage.Resources> @@ -43,11 +44,12 @@ Text="{Binding Liability, Converter={StaticResource moneyConverter}}"/> </Grid> </Grid> - <ui:GroupStackLayout x:Name="groupLayout" ItemsSource="{Binding Accounts}"> + <ui:GroupStackLayout x:Name="groupLayout" ItemsSource="{Binding Accounts}" Margin="0, 10, 0, 0"> <ui:GroupStackLayout.GroupHeaderTemplate> <DataTemplate x:DataType="v:AccountGrouping"> <StackLayout Orientation="Horizontal" Padding="10, 0"> - <Label Text="{Binding Key}" TextColor="{DynamicResource SecondaryTextColor}"/> + <Label Text="{Binding Key, Converter={StaticResource categoryConverter}}" + TextColor="{DynamicResource SecondaryTextColor}"/> <Label Text="{Binding Balance, Converter={StaticResource money2Converter}}" Margin="10, 0" TextColor="{DynamicResource SecondaryTextColor}"/> </StackLayout> @@ -55,7 +57,7 @@ </ui:GroupStackLayout.GroupHeaderTemplate> <ui:GroupStackLayout.ItemTemplate> <DataTemplate x:DataType="m:Account"> - <StackLayout Orientation="Horizontal" Padding="20, 0, 10, 0" HeightRequest="44" Spacing="10"> + <StackLayout Orientation="Horizontal" Padding="20, 0, 10, 0" Spacing="10"> <ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}" WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/> <Label Text="{Binding Name}" TextColor="{DynamicResource TextColor}" diff --git a/Billing.Shared/Views/AccountPage.xaml.cs b/Billing.Shared/Views/AccountPage.xaml.cs index 7d130fd..c089ff8 100644 --- a/Billing.Shared/Views/AccountPage.xaml.cs +++ b/Billing.Shared/Views/AccountPage.xaml.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Billing.Models; using Billing.UI; using Xamarin.Forms; @@ -20,17 +21,57 @@ namespace Billing.Views public Command AddAccount { get; } - private readonly List<AccountGrouping> accounts; + private readonly List<AccountGrouping> accounts = new(); + private bool initialized; public AccountPage() { AddAccount = new Command(OnAddAccount); - accounts = new List<AccountGrouping>(); SetValue(AccountsProperty, accounts); InitializeComponent(); } + protected override void OnAppearing() + { + if (!initialized) + { + initialized = true; + accounts.Clear(); + foreach (var account in App.Accounts) + { + AddToAccountGroup(account); + } + } + groupLayout.Refresh(accounts); + } + + private void AddToAccountGroup(Account account) + { + int maxId; + if (accounts.Count > 0) + { + maxId = accounts.Max(g => g.Max(a => a.Id)); + } + else + { + maxId = -1; + } + account.Id = maxId + 1; + + var group = accounts.FirstOrDefault(g => g.Key == account.Category); + if (group == null) + { + group = new AccountGrouping(account.Category, account.Balance) { account }; + accounts.Add(group); + } + else + { + group.Add(account); + group.Balance += account.Balance; + } + } + private async void OnAddAccount() { if (Tap.IsBusy) @@ -47,28 +88,23 @@ namespace Billing.Views private void AccountChecked(object sender, AccountEventArgs e) { - Helper.Debug(e.Account.ToString()); - 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); - } + App.Accounts.Add(e.Account); + AddToAccountGroup(e.Account); groupLayout.Refresh(accounts); + + Task.Run(App.WriteAccounts); } } public class AccountGrouping : List<Account>, IGrouping<AccountCategory, Account> { - public AccountGrouping(AccountCategory key) : base() => Key = key; public AccountCategory Key { get; } public decimal Balance { get; set; } + + public AccountGrouping(AccountCategory key, decimal balance) : base() + { + Key = key; + Balance = balance; + } } } \ No newline at end of file diff --git a/Billing.Shared/Views/AddAccountPage.xaml b/Billing.Shared/Views/AddAccountPage.xaml index b77cc25..72c92fb 100644 --- a/Billing.Shared/Views/AddAccountPage.xaml +++ b/Billing.Shared/Views/AddAccountPage.xaml @@ -1,15 +1,14 @@ <?xml version="1.0" encoding="utf-8" ?> <ui:BillingPage xmlns="http://xamarin.com/schemas/2014/forms" + xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:r="clr-namespace:Billing.Languages" xmlns:ui="clr-namespace:Billing.UI" xmlns:v="clr-namespace:Billing.Views" - xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Billing.Views.AddAccountPage" x:Name="addAccountPage" x:DataType="v:AddAccountPage" Title="{r:Text AddAccount}" - BindingContext="{x:Reference addAccountPage}" - NavigationPage.BackButtonTitle=""> + BindingContext="{x:Reference addAccountPage}"> <ContentPage.Resources> <ui:AccountCategoryConverter x:Key="categoryConverter"/> @@ -42,7 +41,7 @@ </TableSection.Title> <ui:OptionEntryCell Height="44" Icon="sackdollar.png" Keyboard="Numeric" Title="{r:Text Balance}" - Text="{Binding Balance, Mode=TwoWay}" + Text="{Binding Initial, Mode=TwoWay}" Placeholder="{r:Text BalancePlaceholder}"/> <ui:OptionTextCell Height="44" Icon="yuan.png" Title="{r:Text Currency}" diff --git a/Billing.Shared/Views/AddAccountPage.xaml.cs b/Billing.Shared/Views/AddAccountPage.xaml.cs index cbd7e69..edd15c3 100644 --- a/Billing.Shared/Views/AddAccountPage.xaml.cs +++ b/Billing.Shared/Views/AddAccountPage.xaml.cs @@ -12,7 +12,7 @@ namespace Billing.Views private static readonly BindableProperty AccountNameProperty = BindableProperty.Create(nameof(AccountName), typeof(string), typeof(AddAccountPage)); private static readonly BindableProperty AccountIconProperty = BindableProperty.Create(nameof(AccountIcon), typeof(string), typeof(AddAccountPage)); private static readonly BindableProperty CategoryProperty = BindableProperty.Create(nameof(Category), typeof(AccountCategory), typeof(AddAccountPage)); - private static readonly BindableProperty BalanceProperty = BindableProperty.Create(nameof(Balance), typeof(decimal), typeof(AddAccountPage)); + private static readonly BindableProperty InitialProperty = BindableProperty.Create(nameof(Initial), typeof(string), typeof(AddAccountPage)); private static readonly BindableProperty MemoProperty = BindableProperty.Create(nameof(Memo), typeof(string), typeof(AddAccountPage)); public string AccountName @@ -30,10 +30,10 @@ namespace Billing.Views get => (AccountCategory)GetValue(CategoryProperty); set => SetValue(CategoryProperty, value); } - public decimal Balance + public string Initial { - get => (decimal)GetValue(BalanceProperty); - set => SetValue(BalanceProperty, value); + get => (string)GetValue(InitialProperty); + set => SetValue(InitialProperty, value); } public string Memo { @@ -54,6 +54,7 @@ namespace Billing.Views CheckAccount = new Command(OnCheckAccount); SelectIcon = new Command(OnSelectIcon); SelectCategory = new Command(OnSelectCategory); + AccountIcon = BaseModel.ICON_DEFAULT; Category = AccountCategory.Cash; InitializeComponent(); @@ -61,13 +62,16 @@ namespace Billing.Views public AddAccountPage(Account account) { + CheckAccount = new Command(OnCheckAccount); + SelectIcon = new Command(OnSelectIcon); + SelectCategory = new Command(OnSelectCategory); + this.account = account; AccountName = account.Name; AccountIcon = account.Icon; Category = account.Category; - Balance = account.Balance; + Initial = account.Initial.ToString(); Memo = account.Memo; - CheckAccount = new Command(OnCheckAccount); InitializeComponent(); } @@ -80,6 +84,7 @@ namespace Billing.Views using (Tap.Start()) { await Navigation.PopAsync(); + _ = decimal.TryParse(Initial, out decimal initial); AccountChecked?.Invoke(this, new AccountEventArgs { Account = new Account @@ -88,7 +93,8 @@ namespace Billing.Views Name = AccountName, Icon = AccountIcon, Category = Category, - Balance = Balance, + Initial = initial, + Balance = initial, Memo = Memo } }); diff --git a/Billing.Shared/Views/AddBillPage.xaml b/Billing.Shared/Views/AddBillPage.xaml index cfee05a..3bb68f4 100644 --- a/Billing.Shared/Views/AddBillPage.xaml +++ b/Billing.Shared/Views/AddBillPage.xaml @@ -1,15 +1,52 @@ <?xml version="1.0" encoding="utf-8" ?> <ui:BillingPage xmlns="http://xamarin.com/schemas/2014/forms" + xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" + xmlns:r="clr-namespace:Billing.Languages" xmlns:ui="clr-namespace:Billing.UI" xmlns:v="clr-namespace:Billing.Views" - xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Billing.Views.AddBillPage" + x:Name="billPage" x:DataType="v:AddBillPage" - Title="Add Billing"> + BindingContext="{x:Reference billPage}"> - <StackLayout> - <Label Text="Add a billing here..." - VerticalOptions="CenterAndExpand" - HorizontalOptions="CenterAndExpand" /> - </StackLayout> + <ContentPage.ToolbarItems> + <ToolbarItem Order="Primary" IconImageSource="check.png" Command="{Binding CheckBill}"/> + </ContentPage.ToolbarItems> + + <ContentPage.Content> + <TableView Intent="Settings" HasUnevenRows="True"> + <TableSection Title=" "> + <ui:OptionEditorCell Height="120" Icon="pencil.png" FontSize="20" Keyboard="Text" + Title="{r:Text Account}" + Text="{Binding AccountName, Mode=TwoWay}" + Placeholder="0.00"/> + </TableSection> + <TableSection> + <TableSection.Title> + <OnPlatform x:TypeArguments="x:String" Android=" "/> + </TableSection.Title> + <ui:OptionSelectCell Height="44" Icon="project.png" + Title="{r:Text Category}" + Detail="{Binding Category, Converter={StaticResource categoryConverter}}" + Command="{Binding SelectCategory}"/> + <ui:OptionSelectCell Height="44" Icon="project.png" + Title="{r:Text Account}" + Detail="{Binding Category, Converter={StaticResource categoryConverter}}" + Command="{Binding SelectCategory}"/> + <ui:OptionSelectCell Height="44" Icon="project.png" + Title="{r:Text CreatedTime}" + Detail="{Binding Category, Converter={StaticResource categoryConverter}}" + Command="{Binding SelectCategory}"/> + </TableSection> + <TableSection> + <TableSection.Title> + <OnPlatform x:TypeArguments="x:String" Android=" "/> + </TableSection.Title> + <ui:OptionEditorCell Height="120" Icon="note.png" Keyboard="Plain" + Title="{r:Text Memo}" + Text="{Binding Memo, Mode=TwoWay}" + Placeholder="{r:Text MemoPlaceholder}"/> + </TableSection> + </TableView> + </ContentPage.Content> </ui:BillingPage> \ No newline at end of file diff --git a/Billing.Shared/Views/AddBillPage.xaml.cs b/Billing.Shared/Views/AddBillPage.xaml.cs index 1cdab90..ca2e72b 100644 --- a/Billing.Shared/Views/AddBillPage.xaml.cs +++ b/Billing.Shared/Views/AddBillPage.xaml.cs @@ -1,12 +1,37 @@ +using System; +using Billing.Languages; +using Billing.Models; using Billing.UI; +using Xamarin.Forms; namespace Billing.Views { public partial class AddBillPage : BillingPage { - public AddBillPage() + public Command CheckBill { get; } + + private readonly Bill bill; + private readonly DateTime createDate; + + public AddBillPage(DateTime date) { + createDate = date; + CheckBill = new Command(OnCheckBill); InitializeComponent(); + Title = Resource.AddBill; + } + + public AddBillPage(Bill bill) + { + this.bill = bill; + CheckBill = new Command(OnCheckBill); + InitializeComponent(); + Title = Resource.EditBill; + } + + private void OnCheckBill() + { + } } } \ No newline at end of file diff --git a/Billing.Shared/Views/BillPage.xaml b/Billing.Shared/Views/BillPage.xaml index 96672b9..354c4f5 100644 --- a/Billing.Shared/Views/BillPage.xaml +++ b/Billing.Shared/Views/BillPage.xaml @@ -12,6 +12,10 @@ <ContentPage.Resources> <ui:TitleDateConverter x:Key="titleDateConverter"/> + <ui:MoneyConverter x:Key="moneyConverter" MarkVisible="False" Absolute="True"/> + <ui:BalanceColorConverter x:Key="colorConverter"/> + <ui:UIBillConverter x:Key="billConverter"/> + <ui:IconConverter x:Key="iconConverter"/> </ContentPage.Resources> <Shell.TitleView> @@ -26,27 +30,44 @@ </Grid> </Shell.TitleView> - <Grid RowDefinitions="Auto,*"> + <Grid RowDefinitions="Auto, Auto, *"> <ui:BillingDate x:Name="billingDate" SelectedDate="{Binding SelectedDate}" DateSelected="OnDateSelected"/> - <ScrollView Grid.Row="1"> - <Grid Padding="8" ColumnSpacing="8" ColumnDefinitions="Auto, *, Auto" - BackgroundColor="{DynamicResource PromptBackgroundColor}" - VerticalOptions="Start"> - <ui:TintImage Source="bars.png" WidthRequest="23" HeightRequest="23"/> - <Label Grid.Column="1" Text="{r:Text NoRecords}" TextColor="{DynamicResource TextColor}" + <Grid Grid.Row="1" Padding="8" ColumnSpacing="8" ColumnDefinitions="Auto, *, Auto" + BackgroundColor="{DynamicResource PromptBackgroundColor}"> + <ui:TintImage Source="bars.png" WidthRequest="23" HeightRequest="23"/> + <Label Grid.Column="1" Text="{r:Text NoRecords}" TextColor="{DynamicResource TextColor}" + VerticalOptions="Center"/> + <StackLayout Grid.Column="2" Orientation="Horizontal" Spacing="6"> + <StackLayout.GestureRecognizers> + <TapGestureRecognizer Command="{Binding AddBilling}"/> + </StackLayout.GestureRecognizers> + <Label Text="{r:Text TapToMemo}" TextColor="{DynamicResource PrimaryColor}" VerticalOptions="Center"/> - <StackLayout Grid.Column="2" Orientation="Horizontal" Spacing="6"> - <StackLayout.GestureRecognizers> - <TapGestureRecognizer Command="{Binding AddBilling}"/> - </StackLayout.GestureRecognizers> - <Label Text="{r:Text TapToMemo}" TextColor="{DynamicResource PrimaryColor}" - VerticalOptions="Center"/> - <!--<Label Style="{DynamicResource IconLightStyle}" - Text="{x:Static local:Definition.IconRight}" - TextColor="{DynamicResource TabBarUnselectedColor}"/>--> - <ui:TintImage Source="right.png" WidthRequest="24" HeightRequest="24"/> - </StackLayout> - </Grid> + <ui:TintImage Source="right.png" WidthRequest="24" HeightRequest="24"/> + </StackLayout> + </Grid> + <!-- bill list --> + <ScrollView Grid.Row="2"> + <ui:GroupStackLayout x:Name="billsLayout" ItemsSource="{Binding Bills}" Margin="0, 10, 0, 0"> + <ui:GroupStackLayout.ItemTemplate> + <DataTemplate x:DataType="v:UIBill"> + <Grid Padding="20, 0, 10, 0" ColumnSpacing="10" + ColumnDefinitions="Auto, *, Auto" RowDefinitions="Auto, Auto"> + <ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}" + WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/> + <Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}" + VerticalOptions="Center" + FontSize="Default" FontAttributes="Bold"/> + <Label Grid.Column="2" Text="{Binding Amount, Converter={StaticResource moneyConverter}}" + TextColor="{Binding Amount, Converter={StaticResource colorConverter}}" + VerticalOptions="Center"/> + <Label Grid.Row="1" Grid.Column="1" Text="{Binding ., Converter={StaticResource billConverter}}" + FontSize="Small" + TextColor="{DynamicResource SecondaryTextColor}"/> + </Grid> + </DataTemplate> + </ui:GroupStackLayout.ItemTemplate> + </ui:GroupStackLayout> </ScrollView> <!--<ui:CircleButton Grid.Row="1" VerticalOptions="End" HorizontalOptions="End" Margin="20" Padding="0" diff --git a/Billing.Shared/Views/BillPage.xaml.cs b/Billing.Shared/Views/BillPage.xaml.cs index 2e55c96..c8cb0b0 100644 --- a/Billing.Shared/Views/BillPage.xaml.cs +++ b/Billing.Shared/Views/BillPage.xaml.cs @@ -1,5 +1,8 @@ +using Billing.Models; using Billing.UI; using System; +using System.Collections.Generic; +using System.Linq; using Xamarin.Forms; namespace Billing.Views @@ -7,12 +10,18 @@ namespace Billing.Views public partial class BillPage : BillingPage { private static readonly BindableProperty SelectedDateProperty = BindableProperty.Create(nameof(SelectedDate), typeof(DateTime), typeof(BillPage)); + private static readonly BindableProperty BillsProperty = BindableProperty.Create(nameof(Bills), typeof(List<UIBill>), typeof(BillPage)); public DateTime SelectedDate { get => (DateTime)GetValue(SelectedDateProperty); set => SetValue(SelectedDateProperty, value); } + public List<UIBill> Bills + { + get => (List<UIBill>)GetValue(BillsProperty); + set => SetValue(BillsProperty, value); + } public Command AddBilling { get; } @@ -29,7 +38,18 @@ namespace Billing.Views { SelectedDate = e.Date; - // TODO: while selecting date + var bills = App.Bills.Where(b => + b.CreateTime.Year == e.Date.Year && + b.CreateTime.Month == e.Date.Month && + b.CreateTime.Day == e.Date.Day); + Bills = new List<UIBill>(bills.Select(b => new UIBill(b) + { + Icon = App.Categories.FirstOrDefault(c => c.Id == b.CategoryId)?.Icon ?? BaseModel.ICON_DEFAULT, + Name = b.Name, + DateCreation = b.CreateTime, + Amount = b.Amount, + Wallet = App.Accounts.FirstOrDefault(a => a.Id == b.WalletId)?.Name + })); } private void OnTitleDateLongPressed(object sender, EventArgs e) @@ -45,8 +65,51 @@ namespace Billing.Views } using (Tap.Start()) { - await Navigation.PushAsync(new AddBillPage()); + var page = new AddBillPage(SelectedDate); + await Navigation.PushAsync(page); } } } + + public class UIBill : BindableObject + { + public static readonly BindableProperty IconProperty = BindableProperty.Create(nameof(Icon), typeof(string), typeof(UIBill)); + public static readonly BindableProperty NameProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(UIBill)); + public static readonly BindableProperty DateCreationProperty = BindableProperty.Create(nameof(DateCreation), typeof(DateTime), typeof(UIBill)); + public static readonly BindableProperty AmountProperty = BindableProperty.Create(nameof(Amount), typeof(decimal), typeof(UIBill)); + public static readonly BindableProperty WalletProperty = BindableProperty.Create(nameof(Wallet), typeof(string), typeof(UIBill)); + + public string Icon + { + get => (string)GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + public string Name + { + get => (string)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + public DateTime DateCreation + { + get => (DateTime)GetValue(DateCreationProperty); + set => SetValue(DateCreationProperty, value); + } + public decimal Amount + { + get => (decimal)GetValue(AmountProperty); + set => SetValue(AmountProperty, value); + } + public string Wallet + { + get => (string)GetValue(WalletProperty); + set => SetValue(WalletProperty, value); + } + + public Bill Bill { get; } + + public UIBill(Bill bill) + { + Bill = bill; + } + } } \ No newline at end of file diff --git a/Billing.Shared/Views/IconSelectPage.xaml.cs b/Billing.Shared/Views/IconSelectPage.xaml.cs index 0a2440a..33df746 100644 --- a/Billing.Shared/Views/IconSelectPage.xaml.cs +++ b/Billing.Shared/Views/IconSelectPage.xaml.cs @@ -38,7 +38,9 @@ namespace Billing.Views var source = new List<BillingIcon> { new() { Icon = BaseModel.ICON_DEFAULT }, - new() { Icon = "wallet" } + new() { Icon = "wallet" }, + new() { Icon = "creditcard" }, + new() { Icon = "debitcard" } }; source.AddRange(IconConverter.IconPreset.Select(icon => new BillingIcon { Icon = $"#brand#{icon.Key}" })); foreach (var icon in source) diff --git a/Billing/Billing.iOS/Renderers/LongPressButtonRenderer.cs b/Billing/Billing.iOS/Renderers/LongPressButtonRenderer.cs index 8397006..b556b1d 100644 --- a/Billing/Billing.iOS/Renderers/LongPressButtonRenderer.cs +++ b/Billing/Billing.iOS/Renderers/LongPressButtonRenderer.cs @@ -35,7 +35,7 @@ namespace Billing.iOS.Renderers private void OnLongPressed(UILongPressGestureRecognizer e) { - if (Element is LongPressButton button) + if (e.State == UIGestureRecognizerState.Began && Element is LongPressButton button) { button.TriggerLongPress(); }