complete account page

This commit is contained in:
Tsanie 2022-03-02 17:31:49 +08:00
parent aa38e186c7
commit 51f8e4f2e8
26 changed files with 547 additions and 76 deletions

View File

@ -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 Billing.Themes;
using Xamarin.Essentials; using Xamarin.Essentials;
using Xamarin.Forms; using Xamarin.Forms;
@ -9,6 +13,13 @@ namespace Billing
{ {
public static AppTheme CurrentTheme { get; private set; } public static AppTheme CurrentTheme { get; private set; }
public static PlatformCulture CurrentCulture { 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() public App()
{ {
@ -19,8 +30,21 @@ namespace Billing
Shell.Current.GoToAsync("//Bills"); Shell.Current.GoToAsync("//Bills");
} }
public static void WriteAccounts() => StoreHelper.WriteAccounts(accounts);
public static void WriteBills() => StoreHelper.WriteBills(bills);
protected override void OnStart() 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() protected override void OnResume()

View File

@ -22,7 +22,6 @@
<DependentUpon>MainShell.xaml</DependentUpon> <DependentUpon>MainShell.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="$(MSBuildThisFileDirectory)Models\BaseModel.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Models\BaseModel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\Billing.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\Category.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Models\Category.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\Account.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Models\Account.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Themes\BaseTheme.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Themes\BaseTheme.cs" />
@ -59,6 +58,8 @@
<DependentUpon>SettingPage.xaml</DependentUpon> <DependentUpon>SettingPage.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="$(MSBuildThisFileDirectory)UI\OptionsCells.cs" /> <Compile Include="$(MSBuildThisFileDirectory)UI\OptionsCells.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Store\StoreHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\Bill.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)MainShell.xaml"> <EmbeddedResource Include="$(MSBuildThisFileDirectory)MainShell.xaml">
@ -95,4 +96,7 @@
<Generator>MSBuild:UpdateDesignTimeXaml</Generator> <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="$(MSBuildThisFileDirectory)Store\" />
</ItemGroup>
</Project> </Project>

View File

@ -1,9 +1,11 @@
using System; using System;
using Xamarin.Essentials;
namespace Billing namespace Billing
{ {
internal static class Helper internal static class Helper
{ {
#if DEBUG
public static void Debug(string message) public static void Debug(string message)
{ {
var time = DateTime.Now.ToString("HH:mm:ss.fff"); var time = DateTime.Now.ToString("HH:mm:ss.fff");
@ -20,5 +22,36 @@ namespace Billing
var time = DateTime.Now.ToString("HH:mm:ss.fff"); var time = DateTime.Now.ToString("HH:mm:ss.fff");
System.Diagnostics.Debug.Fail($"[{time}] - {category}", message); 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;
}
}
}
} }
} }

View File

@ -14,6 +14,8 @@ namespace Billing.Languages
public static string CreditCard => Text(nameof(CreditCard)); public static string CreditCard => Text(nameof(CreditCard));
public static string DebitCard => Text(nameof(DebitCard)); public static string DebitCard => Text(nameof(DebitCard));
public static string ElecAccount => Text(nameof(ElecAccount)); 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(); static readonly Dictionary<string, LanguageResource> dict = new();

View File

@ -32,4 +32,8 @@
<DebitCard>Debit Card</DebitCard> <DebitCard>Debit Card</DebitCard>
<ElecAccount>Electronic Account</ElecAccount> <ElecAccount>Electronic Account</ElecAccount>
<IconSelector>Icon Selection</IconSelector> <IconSelector>Icon Selection</IconSelector>
<AddBill>Add Billing</AddBill>
<EditBill>Edit Billing</EditBill>
<Account>Account</Account>
<CreatedTime>Created Time</CreatedTime>
</root> </root>

View File

@ -32,4 +32,8 @@
<DebitCard>储蓄卡</DebitCard> <DebitCard>储蓄卡</DebitCard>
<ElecAccount>电子账户</ElecAccount> <ElecAccount>电子账户</ElecAccount>
<IconSelector>图标选择</IconSelector> <IconSelector>图标选择</IconSelector>
<AddBill>增加账单</AddBill>
<EditBill>编辑账单</EditBill>
<Account>账户</Account>
<CreatedTime>创建时间</CreatedTime>
</root> </root>

View File

@ -161,11 +161,12 @@ namespace Billing.Models
public override string ToString() public override string ToString()
{ {
XDocument xdoc = ToXml();
using MemoryStream ms = new(); using MemoryStream ms = new();
using StreamWriter writer = new(ms, Encoding.UTF8); //using StreamWriter writer = new(ms, Encoding.UTF8);
xdoc.Save(writer, SaveOptions.DisableFormatting); //XDocument xdoc = ToXml();
writer.Flush(); //xdoc.Save(writer, SaveOptions.DisableFormatting);
//writer.Flush();
SaveToStream(ms);
ms.Seek(0, SeekOrigin.Begin); ms.Seek(0, SeekOrigin.Begin);
using StreamReader reader = new(ms, Encoding.UTF8); using StreamReader reader = new(ms, Encoding.UTF8);
return reader.ReadToEnd(); return reader.ReadToEnd();

View File

@ -3,7 +3,7 @@ using System.Xml.Linq;
namespace Billing.Models namespace Billing.Models
{ {
public class Billing : BaseModel public class Bill : BaseModel
{ {
public decimal Amount { get; set; } public decimal Amount { get; set; }
public string Name { get; set; } public string Name { get; set; }

View File

@ -5,6 +5,7 @@ namespace Billing.Models
public class Category : BaseModel public class Category : BaseModel
{ {
public int Id { get; set; } public int Id { get; set; }
public CategoryType Type { get; set; }
public string Icon { get; set; } = ICON_DEFAULT; public string Icon { get; set; } = ICON_DEFAULT;
public string Name { get; set; } public string Name { get; set; }
public int? ParentId { get; set; } public int? ParentId { get; set; }
@ -12,6 +13,7 @@ namespace Billing.Models
public override void OnXmlDeserialize(XElement node) public override void OnXmlDeserialize(XElement node)
{ {
Id = Read(node, nameof(Id), 0); Id = Read(node, nameof(Id), 0);
Type = (CategoryType)Read(node, nameof(Type), 0);
Icon = Read(node, nameof(Icon), ICON_DEFAULT); Icon = Read(node, nameof(Icon), ICON_DEFAULT);
Name = Read(node, nameof(Name), string.Empty); Name = Read(node, nameof(Name), string.Empty);
var parentId = Read(node, nameof(ParentId), -1); var parentId = Read(node, nameof(ParentId), -1);
@ -24,6 +26,7 @@ namespace Billing.Models
public override void OnXmlSerialize(XElement node) public override void OnXmlSerialize(XElement node)
{ {
Write(node, nameof(Id), Id); Write(node, nameof(Id), Id);
Write(node, nameof(Type), (int)Type);
Write(node, nameof(Icon), Icon); Write(node, nameof(Icon), Icon);
Write(node, nameof(Name), Name); Write(node, nameof(Name), Name);
if (ParentId != null) if (ParentId != null)
@ -32,4 +35,10 @@ namespace Billing.Models
} }
} }
} }
public enum CategoryType
{
Spending,
Income
}
} }

View File

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

View File

@ -23,6 +23,7 @@ namespace Billing.Themes
public const string TextColor = nameof(TextColor); public const string TextColor = nameof(TextColor);
public const string SecondaryTextColor = nameof(SecondaryTextColor); public const string SecondaryTextColor = nameof(SecondaryTextColor);
public const string RedColor = nameof(RedColor); public const string RedColor = nameof(RedColor);
public const string GreenColor = nameof(GreenColor);
protected abstract Color PrimaryMauiColor { get; } protected abstract Color PrimaryMauiColor { get; }
protected abstract Color SecondaryMauiColor { get; } protected abstract Color SecondaryMauiColor { get; }

View File

@ -28,6 +28,7 @@ namespace Billing.Themes
Add(TextColor, Color.FromRgb(0xcc, 0xcc, 0xcc)); Add(TextColor, Color.FromRgb(0xcc, 0xcc, 0xcc));
Add(SecondaryTextColor, Color.LightGray); Add(SecondaryTextColor, Color.LightGray);
Add(RedColor, Color.FromRgb(211, 5, 5)); Add(RedColor, Color.FromRgb(211, 5, 5));
Add(GreenColor, Color.FromRgb(5, 211, 5));
Add(new Style(typeof(TabBar)) Add(new Style(typeof(TabBar))
{ {

View File

@ -28,6 +28,7 @@ namespace Billing.Themes
Add(TextColor, Color.FromRgb(0x33, 0x33, 0x33)); Add(TextColor, Color.FromRgb(0x33, 0x33, 0x33));
Add(SecondaryTextColor, Color.DimGray); Add(SecondaryTextColor, Color.DimGray);
Add(RedColor, Color.FromRgb(211, 64, 85)); Add(RedColor, Color.FromRgb(211, 64, 85));
Add(RedColor, Color.FromRgb(64, 211, 85));
Add(new Style(typeof(TabBar)) Add(new Style(typeof(TabBar))
{ {

View File

@ -1,5 +1,7 @@
using Billing.Languages; using Billing.Languages;
using Billing.Models; using Billing.Models;
using Billing.Themes;
using Billing.Views;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
@ -20,18 +22,23 @@ namespace Billing.UI
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{ {
return value; throw new NotImplementedException();
} }
} }
public class MoneyConverter : IValueConverter public class MoneyConverter : IValueConverter
{ {
public bool MarkVisible { get; set; } = true; public bool MarkVisible { get; set; } = true;
public bool Absolute { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
if (value is decimal d) if (value is decimal d)
{ {
if (Absolute)
{
d = Math.Abs(d);
}
var number = d.ToString("n2"); var number = d.ToString("n2");
if (MarkVisible) 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 class NotNullConverter : IValueConverter
{ {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 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) 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) 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) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{ {
return value; throw new NotImplementedException();
} }
} }
} }

View File

@ -1,4 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Linq;
using Billing.Models;
using Xamarin.Forms; using Xamarin.Forms;
namespace Billing.UI 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 public class Tap : IDisposable
{ {
private readonly static object sync = new(); private readonly static object sync = new();

View File

@ -1,7 +1,4 @@
using System.Collections; using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Xamarin.Forms; using Xamarin.Forms;
namespace Billing.UI namespace Billing.UI

View File

@ -14,6 +14,7 @@
<ContentPage.Resources> <ContentPage.Resources>
<ui:MoneyConverter x:Key="moneyConverter"/> <ui:MoneyConverter x:Key="moneyConverter"/>
<ui:MoneyConverter x:Key="money2Converter" MarkVisible="False"/> <ui:MoneyConverter x:Key="money2Converter" MarkVisible="False"/>
<ui:AccountCategoryConverter x:Key="categoryConverter"/>
<ui:IconConverter x:Key="iconConverter"/> <ui:IconConverter x:Key="iconConverter"/>
</ContentPage.Resources> </ContentPage.Resources>
@ -43,11 +44,12 @@
Text="{Binding Liability, Converter={StaticResource moneyConverter}}"/> Text="{Binding Liability, Converter={StaticResource moneyConverter}}"/>
</Grid> </Grid>
</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> <ui:GroupStackLayout.GroupHeaderTemplate>
<DataTemplate x:DataType="v:AccountGrouping"> <DataTemplate x:DataType="v:AccountGrouping">
<StackLayout Orientation="Horizontal" Padding="10, 0"> <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}}" <Label Text="{Binding Balance, Converter={StaticResource money2Converter}}"
Margin="10, 0" TextColor="{DynamicResource SecondaryTextColor}"/> Margin="10, 0" TextColor="{DynamicResource SecondaryTextColor}"/>
</StackLayout> </StackLayout>
@ -55,7 +57,7 @@
</ui:GroupStackLayout.GroupHeaderTemplate> </ui:GroupStackLayout.GroupHeaderTemplate>
<ui:GroupStackLayout.ItemTemplate> <ui:GroupStackLayout.ItemTemplate>
<DataTemplate x:DataType="m:Account"> <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}}" <ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}"
WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/> WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/>
<Label Text="{Binding Name}" TextColor="{DynamicResource TextColor}" <Label Text="{Binding Name}" TextColor="{DynamicResource TextColor}"

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Billing.Models; using Billing.Models;
using Billing.UI; using Billing.UI;
using Xamarin.Forms; using Xamarin.Forms;
@ -20,17 +21,57 @@ namespace Billing.Views
public Command AddAccount { get; } public Command AddAccount { get; }
private readonly List<AccountGrouping> accounts; private readonly List<AccountGrouping> accounts = new();
private bool initialized;
public AccountPage() public AccountPage()
{ {
AddAccount = new Command(OnAddAccount); AddAccount = new Command(OnAddAccount);
accounts = new List<AccountGrouping>();
SetValue(AccountsProperty, accounts); SetValue(AccountsProperty, accounts);
InitializeComponent(); 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() private async void OnAddAccount()
{ {
if (Tap.IsBusy) if (Tap.IsBusy)
@ -47,28 +88,23 @@ namespace Billing.Views
private void AccountChecked(object sender, AccountEventArgs e) private void AccountChecked(object sender, AccountEventArgs e)
{ {
Helper.Debug(e.Account.ToString()); App.Accounts.Add(e.Account);
var group = accounts.FirstOrDefault(g => g.Key == e.Account.Category); AddToAccountGroup(e.Account);
if (group == null)
{
group = new AccountGrouping(e.Account.Category)
{
e.Account
};
accounts.Add(group);
}
else
{
group.Add(e.Account);
}
groupLayout.Refresh(accounts); groupLayout.Refresh(accounts);
Task.Run(App.WriteAccounts);
} }
} }
public class AccountGrouping : List<Account>, IGrouping<AccountCategory, Account> public class AccountGrouping : List<Account>, IGrouping<AccountCategory, Account>
{ {
public AccountGrouping(AccountCategory key) : base() => Key = key;
public AccountCategory Key { get; } public AccountCategory Key { get; }
public decimal Balance { get; set; } public decimal Balance { get; set; }
public AccountGrouping(AccountCategory key, decimal balance) : base()
{
Key = key;
Balance = balance;
}
} }
} }

View File

@ -1,15 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<ui:BillingPage xmlns="http://xamarin.com/schemas/2014/forms" <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:r="clr-namespace:Billing.Languages"
xmlns:ui="clr-namespace:Billing.UI" xmlns:ui="clr-namespace:Billing.UI"
xmlns:v="clr-namespace:Billing.Views" xmlns:v="clr-namespace:Billing.Views"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Billing.Views.AddAccountPage" x:Class="Billing.Views.AddAccountPage"
x:Name="addAccountPage" x:Name="addAccountPage"
x:DataType="v:AddAccountPage" x:DataType="v:AddAccountPage"
Title="{r:Text AddAccount}" Title="{r:Text AddAccount}"
BindingContext="{x:Reference addAccountPage}" BindingContext="{x:Reference addAccountPage}">
NavigationPage.BackButtonTitle="">
<ContentPage.Resources> <ContentPage.Resources>
<ui:AccountCategoryConverter x:Key="categoryConverter"/> <ui:AccountCategoryConverter x:Key="categoryConverter"/>
@ -42,7 +41,7 @@
</TableSection.Title> </TableSection.Title>
<ui:OptionEntryCell Height="44" Icon="sackdollar.png" Keyboard="Numeric" <ui:OptionEntryCell Height="44" Icon="sackdollar.png" Keyboard="Numeric"
Title="{r:Text Balance}" Title="{r:Text Balance}"
Text="{Binding Balance, Mode=TwoWay}" Text="{Binding Initial, Mode=TwoWay}"
Placeholder="{r:Text BalancePlaceholder}"/> Placeholder="{r:Text BalancePlaceholder}"/>
<ui:OptionTextCell Height="44" Icon="yuan.png" <ui:OptionTextCell Height="44" Icon="yuan.png"
Title="{r:Text Currency}" Title="{r:Text Currency}"

View File

@ -12,7 +12,7 @@ namespace Billing.Views
private static readonly BindableProperty AccountNameProperty = BindableProperty.Create(nameof(AccountName), typeof(string), typeof(AddAccountPage)); 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 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 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)); private static readonly BindableProperty MemoProperty = BindableProperty.Create(nameof(Memo), typeof(string), typeof(AddAccountPage));
public string AccountName public string AccountName
@ -30,10 +30,10 @@ namespace Billing.Views
get => (AccountCategory)GetValue(CategoryProperty); get => (AccountCategory)GetValue(CategoryProperty);
set => SetValue(CategoryProperty, value); set => SetValue(CategoryProperty, value);
} }
public decimal Balance public string Initial
{ {
get => (decimal)GetValue(BalanceProperty); get => (string)GetValue(InitialProperty);
set => SetValue(BalanceProperty, value); set => SetValue(InitialProperty, value);
} }
public string Memo public string Memo
{ {
@ -54,6 +54,7 @@ namespace Billing.Views
CheckAccount = new Command(OnCheckAccount); CheckAccount = new Command(OnCheckAccount);
SelectIcon = new Command(OnSelectIcon); SelectIcon = new Command(OnSelectIcon);
SelectCategory = new Command(OnSelectCategory); SelectCategory = new Command(OnSelectCategory);
AccountIcon = BaseModel.ICON_DEFAULT; AccountIcon = BaseModel.ICON_DEFAULT;
Category = AccountCategory.Cash; Category = AccountCategory.Cash;
InitializeComponent(); InitializeComponent();
@ -61,13 +62,16 @@ namespace Billing.Views
public AddAccountPage(Account account) public AddAccountPage(Account account)
{ {
CheckAccount = new Command(OnCheckAccount);
SelectIcon = new Command(OnSelectIcon);
SelectCategory = new Command(OnSelectCategory);
this.account = account; this.account = account;
AccountName = account.Name; AccountName = account.Name;
AccountIcon = account.Icon; AccountIcon = account.Icon;
Category = account.Category; Category = account.Category;
Balance = account.Balance; Initial = account.Initial.ToString();
Memo = account.Memo; Memo = account.Memo;
CheckAccount = new Command(OnCheckAccount);
InitializeComponent(); InitializeComponent();
} }
@ -80,6 +84,7 @@ namespace Billing.Views
using (Tap.Start()) using (Tap.Start())
{ {
await Navigation.PopAsync(); await Navigation.PopAsync();
_ = decimal.TryParse(Initial, out decimal initial);
AccountChecked?.Invoke(this, new AccountEventArgs AccountChecked?.Invoke(this, new AccountEventArgs
{ {
Account = new Account Account = new Account
@ -88,7 +93,8 @@ namespace Billing.Views
Name = AccountName, Name = AccountName,
Icon = AccountIcon, Icon = AccountIcon,
Category = Category, Category = Category,
Balance = Balance, Initial = initial,
Balance = initial,
Memo = Memo Memo = Memo
} }
}); });

View File

@ -1,15 +1,52 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<ui:BillingPage xmlns="http://xamarin.com/schemas/2014/forms" <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:ui="clr-namespace:Billing.UI"
xmlns:v="clr-namespace:Billing.Views" xmlns:v="clr-namespace:Billing.Views"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Billing.Views.AddBillPage" x:Class="Billing.Views.AddBillPage"
x:Name="billPage"
x:DataType="v:AddBillPage" x:DataType="v:AddBillPage"
Title="Add Billing"> BindingContext="{x:Reference billPage}">
<StackLayout> <ContentPage.ToolbarItems>
<Label Text="Add a billing here..." <ToolbarItem Order="Primary" IconImageSource="check.png" Command="{Binding CheckBill}"/>
VerticalOptions="CenterAndExpand" </ContentPage.ToolbarItems>
HorizontalOptions="CenterAndExpand" />
</StackLayout> <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> </ui:BillingPage>

View File

@ -1,12 +1,37 @@
using System;
using Billing.Languages;
using Billing.Models;
using Billing.UI; using Billing.UI;
using Xamarin.Forms;
namespace Billing.Views namespace Billing.Views
{ {
public partial class AddBillPage : BillingPage 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(); InitializeComponent();
Title = Resource.AddBill;
}
public AddBillPage(Bill bill)
{
this.bill = bill;
CheckBill = new Command(OnCheckBill);
InitializeComponent();
Title = Resource.EditBill;
}
private void OnCheckBill()
{
} }
} }
} }

View File

@ -12,6 +12,10 @@
<ContentPage.Resources> <ContentPage.Resources>
<ui:TitleDateConverter x:Key="titleDateConverter"/> <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> </ContentPage.Resources>
<Shell.TitleView> <Shell.TitleView>
@ -26,27 +30,44 @@
</Grid> </Grid>
</Shell.TitleView> </Shell.TitleView>
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto, Auto, *">
<ui:BillingDate x:Name="billingDate" SelectedDate="{Binding SelectedDate}" DateSelected="OnDateSelected"/> <ui:BillingDate x:Name="billingDate" SelectedDate="{Binding SelectedDate}" DateSelected="OnDateSelected"/>
<ScrollView Grid.Row="1"> <Grid Grid.Row="1" Padding="8" ColumnSpacing="8" ColumnDefinitions="Auto, *, Auto"
<Grid Padding="8" ColumnSpacing="8" ColumnDefinitions="Auto, *, Auto" BackgroundColor="{DynamicResource PromptBackgroundColor}">
BackgroundColor="{DynamicResource PromptBackgroundColor}" <ui:TintImage Source="bars.png" WidthRequest="23" HeightRequest="23"/>
VerticalOptions="Start"> <Label Grid.Column="1" Text="{r:Text NoRecords}" TextColor="{DynamicResource TextColor}"
<ui:TintImage Source="bars.png" WidthRequest="23" HeightRequest="23"/> VerticalOptions="Center"/>
<Label Grid.Column="1" Text="{r:Text NoRecords}" TextColor="{DynamicResource TextColor}" <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"/> VerticalOptions="Center"/>
<StackLayout Grid.Column="2" Orientation="Horizontal" Spacing="6"> <ui:TintImage Source="right.png" WidthRequest="24" HeightRequest="24"/>
<StackLayout.GestureRecognizers> </StackLayout>
<TapGestureRecognizer Command="{Binding AddBilling}"/> </Grid>
</StackLayout.GestureRecognizers> <!-- bill list -->
<Label Text="{r:Text TapToMemo}" TextColor="{DynamicResource PrimaryColor}" <ScrollView Grid.Row="2">
VerticalOptions="Center"/> <ui:GroupStackLayout x:Name="billsLayout" ItemsSource="{Binding Bills}" Margin="0, 10, 0, 0">
<!--<Label Style="{DynamicResource IconLightStyle}" <ui:GroupStackLayout.ItemTemplate>
Text="{x:Static local:Definition.IconRight}" <DataTemplate x:DataType="v:UIBill">
TextColor="{DynamicResource TabBarUnselectedColor}"/>--> <Grid Padding="20, 0, 10, 0" ColumnSpacing="10"
<ui:TintImage Source="right.png" WidthRequest="24" HeightRequest="24"/> ColumnDefinitions="Auto, *, Auto" RowDefinitions="Auto, Auto">
</StackLayout> <ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}"
</Grid> 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> </ScrollView>
<!--<ui:CircleButton Grid.Row="1" VerticalOptions="End" HorizontalOptions="End" <!--<ui:CircleButton Grid.Row="1" VerticalOptions="End" HorizontalOptions="End"
Margin="20" Padding="0" Margin="20" Padding="0"

View File

@ -1,5 +1,8 @@
using Billing.Models;
using Billing.UI; using Billing.UI;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms; using Xamarin.Forms;
namespace Billing.Views namespace Billing.Views
@ -7,12 +10,18 @@ namespace Billing.Views
public partial class BillPage : BillingPage public partial class BillPage : BillingPage
{ {
private static readonly BindableProperty SelectedDateProperty = BindableProperty.Create(nameof(SelectedDate), typeof(DateTime), typeof(BillPage)); 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 public DateTime SelectedDate
{ {
get => (DateTime)GetValue(SelectedDateProperty); get => (DateTime)GetValue(SelectedDateProperty);
set => SetValue(SelectedDateProperty, value); set => SetValue(SelectedDateProperty, value);
} }
public List<UIBill> Bills
{
get => (List<UIBill>)GetValue(BillsProperty);
set => SetValue(BillsProperty, value);
}
public Command AddBilling { get; } public Command AddBilling { get; }
@ -29,7 +38,18 @@ namespace Billing.Views
{ {
SelectedDate = e.Date; 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) private void OnTitleDateLongPressed(object sender, EventArgs e)
@ -45,8 +65,51 @@ namespace Billing.Views
} }
using (Tap.Start()) 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;
}
}
} }

View File

@ -38,7 +38,9 @@ namespace Billing.Views
var source = new List<BillingIcon> var source = new List<BillingIcon>
{ {
new() { Icon = BaseModel.ICON_DEFAULT }, 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}" })); source.AddRange(IconConverter.IconPreset.Select(icon => new BillingIcon { Icon = $"#brand#{icon.Key}" }));
foreach (var icon in source) foreach (var icon in source)

View File

@ -35,7 +35,7 @@ namespace Billing.iOS.Renderers
private void OnLongPressed(UILongPressGestureRecognizer e) private void OnLongPressed(UILongPressGestureRecognizer e)
{ {
if (Element is LongPressButton button) if (e.State == UIGestureRecognizerState.Began && Element is LongPressButton button)
{ {
button.TriggerLongPress(); button.TriggerLongPress();
} }