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 Bills => bills ??= new List(); + public static List Accounts => accounts ??= new List(); + public static List Categories => categories ??= new List(); + + private static List bills; + private static List accounts; + private static List 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 @@ MainShell.xaml - @@ -59,6 +58,8 @@ SettingPage.xaml + + @@ -95,4 +96,7 @@ MSBuild:UpdateDesignTimeXaml + + + \ 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 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 @@ Debit Card Electronic Account Icon Selection + Add Billing + Edit Billing + Account + Created Time \ 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 @@ 储蓄卡 电子账户 图标选择 + 增加账单 + 编辑账单 + 账户 + 创建时间 \ 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 GetAccounts() => Instance.GetAccountsInternal(); + public static void WriteAccounts(IEnumerable accounts) => Instance.WriteAccountsInternal(accounts); + public static List GetBills() => Instance.GetBillsInternal(); + public static void WriteBills(IEnumerable bills) => Instance.WriteBillsInternal(bills); + public static List GetCategories() => Instance.GetCategoriesInternal(); + public static void WriteCategories(IEnumerable categories) => Instance.WriteCategoriesInternal(categories); + + private StoreHelper() { } + + private List GetAccountsInternal() + { + return GetList(Path.Combine(PersonalFolder, accountFile)); + } + + private void WriteAccountsInternal(IEnumerable accounts) + { + var filename = Path.Combine(PersonalFolder, accountFile); + WriteList(filename, accounts); + } + + private List GetBillsInternal() + { + return GetList(Path.Combine(PersonalFolder, billFile)); + } + + private void WriteBillsInternal(IEnumerable bills) + { + var filename = Path.Combine(PersonalFolder, billFile); + WriteList(filename, bills); + } + + private List GetCategoriesInternal() + { + return GetList(Path.Combine(PersonalFolder, categoryFile)); + } + + private void WriteCategoriesInternal(IEnumerable categories) + { + var filename = Path.Combine(PersonalFolder, categoryFile); + WriteList(filename, categories); + } + + #region Helper + + private void WriteList(string filename, IEnumerable 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 GetList(string file) where T : IModel, new() + { + try + { + if (File.Exists(file)) + { + using var stream = File.OpenRead(file); + var list = ModelExtensionHelper.FromStream(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 FromStream(Stream stream) where T : IModel, new() + { + XDocument doc = XDocument.Load(stream); + var root = doc.Root; + var list = new List(); + 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(this IEnumerable 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 @@ + @@ -43,11 +44,12 @@ Text="{Binding Liability, Converter={StaticResource moneyConverter}}"/> - + - @@ -55,7 +57,7 @@ - + + + + + + + + + + + + +