From 5ec4119025eaf1e617a883d87d4841482000155f Mon Sep 17 00:00:00 2001 From: Tsanie Date: Fri, 11 Mar 2022 16:10:11 +0800 Subject: [PATCH] switch to sqlite --- Billing.Shared/App.cs | 20 +- Billing.Shared/Billing.Shared.projitems | 9 +- Billing.Shared/Helper.cs | 25 ++- Billing.Shared/MainShell.xaml | 5 + Billing.Shared/Models/Account.cs | 29 +-- Billing.Shared/Models/BaseModel.cs | 186 ----------------- Billing.Shared/Models/Bill.cs | 29 +-- Billing.Shared/Models/Category.cs | 45 +---- Billing.Shared/Models/IIdItem.cs | 7 + Billing.Shared/SplashPage.xaml | 9 + Billing.Shared/SplashPage.xaml.cs | 20 ++ Billing.Shared/Store/StoreHelper.cs | 189 ++++++++++++------ Billing.Shared/UI/Converters.cs | 2 +- Billing.Shared/UI/Definition.cs | 67 +++---- Billing.Shared/Views/AccountPage.xaml.cs | 20 +- Billing.Shared/Views/AddAccountPage.xaml.cs | 2 +- Billing.Shared/Views/AddCategoryPage.xaml.cs | 10 +- Billing.Shared/Views/BillPage.xaml.cs | 9 +- Billing.Shared/Views/CategoryPage.xaml.cs | 16 +- .../Views/CategorySelectPage.xaml.cs | 2 +- Billing.Shared/Views/IconSelectPage.xaml.cs | 5 +- Billing.Shared/Views/RankPage.xaml.cs | 3 +- .../Billing.Android/Billing.Android.csproj | 5 +- Billing/Billing.iOS/Billing.iOS.csproj | 5 +- Billing/Billing.iOS/Info.plist | 2 - 25 files changed, 286 insertions(+), 435 deletions(-) delete mode 100644 Billing.Shared/Models/BaseModel.cs create mode 100644 Billing.Shared/Models/IIdItem.cs create mode 100644 Billing.Shared/SplashPage.xaml create mode 100644 Billing.Shared/SplashPage.xaml.cs diff --git a/Billing.Shared/App.cs b/Billing.Shared/App.cs index 6442405..9282a5d 100644 --- a/Billing.Shared/App.cs +++ b/Billing.Shared/App.cs @@ -27,25 +27,21 @@ namespace Billing InitResources(); MainPage = new MainShell(); - Shell.Current.GoToAsync("//Settings"); + Shell.Current.GoToAsync("//Splash"); } - public static void WriteAccounts() => StoreHelper.WriteAccounts(accounts); - - public static void WriteBills() => StoreHelper.WriteBills(bills); - - public static void WriteCategories() => StoreHelper.WriteCategories(categories); - protected override void OnStart() { Helper.Debug($"personal folder: {StoreHelper.PersonalFolder}"); Helper.Debug($"cache folder: {StoreHelper.CacheFolder}"); - - accounts = StoreHelper.GetAccounts(); - categories = StoreHelper.GetCategories(); - bills = StoreHelper.GetBills(); + } - Shell.Current.GoToAsync("//Bills"); + internal static async Task InitilalizeData() + { + await Task.WhenAll( + Task.Run(async () => accounts = await StoreHelper.GetAccountsAsync()), + Task.Run(async () => categories = await StoreHelper.GetCategoriesAsync()), + Task.Run(async () => bills = await StoreHelper.GetBillsAsync())); } protected override void OnResume() diff --git a/Billing.Shared/Billing.Shared.projitems b/Billing.Shared/Billing.Shared.projitems index 5566fd7..0afc34f 100644 --- a/Billing.Shared/Billing.Shared.projitems +++ b/Billing.Shared/Billing.Shared.projitems @@ -11,6 +11,10 @@ + + Designer + MSBuild:UpdateDesignTimeXaml + @@ -21,7 +25,6 @@ MainShell.xaml - @@ -78,6 +81,10 @@ + + + SplashPage.xaml + diff --git a/Billing.Shared/Helper.cs b/Billing.Shared/Helper.cs index 53beef9..f0a5100 100644 --- a/Billing.Shared/Helper.cs +++ b/Billing.Shared/Helper.cs @@ -1,8 +1,11 @@ using Billing.Models; +using Billing.UI; using Billing.Views; using System; using System.Globalization; using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Xamarin.Essentials; using Xamarin.Forms; @@ -82,7 +85,7 @@ namespace Billing { return new UIBill(b) { - Icon = App.Categories.FirstOrDefault(c => c.Id == b.CategoryId)?.Icon ?? BaseModel.ICON_DEFAULT, + Icon = App.Categories.FirstOrDefault(c => c.Id == b.CategoryId)?.Icon ?? Definition.DefaultIcon, Name = b.Name, DateCreation = b.CreateTime, Amount = b.Amount, @@ -130,4 +133,24 @@ namespace Billing public delegate void PropertyValueChanged(TOwner obj, TResult old, TResult @new); } + + internal class AsyncLazy + { + private readonly Lazy> instance; + + public AsyncLazy(Func factory) + { + instance = new Lazy>(() => Task.Run(factory)); + } + + public AsyncLazy(Func> factory) + { + instance = new Lazy>(() => Task.Run(factory)); + } + + public TaskAwaiter GetAwaiter() + { + return instance.Value.GetAwaiter(); + } + } } \ No newline at end of file diff --git a/Billing.Shared/MainShell.xaml b/Billing.Shared/MainShell.xaml index 3da6c25..44b1d24 100644 --- a/Billing.Shared/MainShell.xaml +++ b/Billing.Shared/MainShell.xaml @@ -1,6 +1,7 @@ + + + + \ No newline at end of file diff --git a/Billing.Shared/Models/Account.cs b/Billing.Shared/Models/Account.cs index 3dadad5..51e6ae2 100644 --- a/Billing.Shared/Models/Account.cs +++ b/Billing.Shared/Models/Account.cs @@ -1,9 +1,12 @@ -using System.Xml.Linq; +using SQLite; namespace Billing.Models { - public class Account : BaseModel + public class Account : IIdItem { + private const string ICON_DEFAULT = "ic_default"; + + [PrimaryKey, AutoIncrement] public int Id { get; set; } public string Icon { get; set; } = ICON_DEFAULT; public AccountCategory Category { get; set; } @@ -11,28 +14,6 @@ namespace Billing.Models public decimal Initial { get; set; } public decimal Balance { get; set; } public string Memo { get; set; } - - public override void OnXmlDeserialize(XElement node) - { - Id = Read(node, nameof(Id), 0); - 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); - } - - public override void OnXmlSerialize(XElement node) - { - Write(node, nameof(Id), Id); - 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); - } } public enum AccountCategory diff --git a/Billing.Shared/Models/BaseModel.cs b/Billing.Shared/Models/BaseModel.cs deleted file mode 100644 index 0dbdb62..0000000 --- a/Billing.Shared/Models/BaseModel.cs +++ /dev/null @@ -1,186 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Xml.Linq; - -namespace Billing.Models -{ - public interface IModel - { - void OnXmlSerialize(XElement node); - - void OnXmlDeserialize(XElement node); - } - - public abstract class BaseModel : IModel, IDisposable - { - public const string ICON_DEFAULT = "ic_default"; - - private bool disposed = false; - - public static T ParseXml(string xml) where T : BaseModel, new() - { - XDocument doc = XDocument.Parse(xml); - T model = new(); - model.OnXmlDeserialize(doc.Root); - return model; - } - - protected static string ToString(IFormattable v) => v.ToString(null, CultureInfo.InvariantCulture); - protected static double ParseDouble(string s) => double.Parse(s, CultureInfo.InvariantCulture); - protected static decimal ParseDecimal(string s) => decimal.Parse(s, CultureInfo.InvariantCulture); - protected static int ParseInt(string s) => int.Parse(s, CultureInfo.InvariantCulture); - protected static long ParseLong(string s) => long.Parse(s, CultureInfo.InvariantCulture); - - protected static bool IsTrue(string s) => - string.Equals(s, "true", StringComparison.OrdinalIgnoreCase) || - string.Equals(s, "yes", StringComparison.OrdinalIgnoreCase) || - s == "1"; - - #region Private Methods - - private static XElement WriteString(XElement parent, string name, string val, Action action = null) - { - XElement ele; - if (val == null) - { - ele = new XElement(name); - } - else - { - ele = new XElement(name, val); - } - action?.Invoke(ele); - parent.Add(ele); - return ele; - } - - private static T ReadSubnode(XElement node, string subname, Func func) - { - var ele = node.Elements().FirstOrDefault(e => string.Equals(e.Name.ToString(), subname, StringComparison.OrdinalIgnoreCase)); - return func(ele); - } - - private static T ReadObject(XElement node) where T : IModel, new() - { - if (IsTrue(node.Attribute("null")?.Value)) - { - return default; - } - T value = new(); - value.OnXmlDeserialize(node); - return value; - } - - private static T[] ReadArray(XElement node) where T : IModel, new() - { - if (IsTrue(node.Attribute("null")?.Value)) - { - return default; - } - int count = ParseInt(node.Attribute("count").Value); - T[] array = new T[count]; - foreach (XElement ele in node.Elements("item")) - { - int index = ParseInt(ele.Attribute("index").Value); - array[index] = ReadObject(ele); - } - return array; - } - - #endregion - - #region Read/Write Helper - - protected static XElement Write(XElement parent, string name, string val) => WriteString(parent, name, val); - protected static XElement Write(XElement parent, string name, DateTime date) => WriteString(parent, name, ToString(date.Ticks)); - protected static XElement Write(XElement parent, string name, T value) where T : IFormattable => WriteString(parent, name, ToString(value)); - protected static XElement WriteObject(XElement parent, string name, T value) where T : IModel => WriteString(parent, name, null, - action: ele => - { - if (value == null) - { - ele.Add(new XAttribute("null", 1)); - } - else - { - value.OnXmlSerialize(ele); - } - }); - protected static XElement WriteArray(XElement parent, string name, T[] value) where T : IModel => WriteString(parent, name, null, - action: ele => - { - if (value == null) - { - ele.Add(new XAttribute("null", 1)); - } - else - { - ele.Add(new XAttribute("count", value.Length)); - for (var i = 0; i < value.Length; i++) - { - XElement item = WriteObject(ele, "item", value[i]); - item.Add(new XAttribute("index", i)); - } - } - }); - - protected static string Read(XElement node, string subname, string def) => ReadSubnode(node, subname, e => e?.Value ?? def); - protected static int Read(XElement node, string subname, int def) => ReadSubnode(node, subname, e => e == null ? def : ParseInt(e.Value)); - protected static long Read(XElement node, string subname, long def) => ReadSubnode(node, subname, e => e == null ? def : ParseLong(e.Value)); - protected static double Read(XElement node, string subname, double def) => ReadSubnode(node, subname, e => e == null ? def : ParseDouble(e.Value)); - protected static decimal Read(XElement node, string subname, decimal def) => ReadSubnode(node, subname, e => e == null ? def : ParseDecimal(e.Value)); - protected static DateTime Read(XElement node, string subname, DateTime def) => ReadSubnode(node, subname, e => e == null ? def : new DateTime(ParseLong(e.Value))); - protected static T ReadObject(XElement node, string subname, T def = default) where T : IModel, new() => ReadSubnode(node, subname, e => e == null ? def : ReadObject(e)); - protected static T[] ReadArray(XElement node, string subname, T[] def = null) where T : IModel, new() => ReadSubnode(node, subname, e => e == null ? def : ReadArray(e)); - - #endregion - - public XDocument ToXml() - { - XDocument xdoc = new(new XDeclaration("1.0", "utf-8", "yes"), new XElement("root")); - ToXml(xdoc.Root); - return xdoc; - } - - public void ToXml(XElement node) - { - OnXmlSerialize(node); - } - - public void SaveToStream(Stream stream) - { - ToXml().Save(stream, SaveOptions.DisableFormatting); - } - - public abstract void OnXmlSerialize(XElement node); - public abstract void OnXmlDeserialize(XElement node); - - - public override string ToString() - { - using MemoryStream ms = new(); - //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(); - } - - protected virtual void Dispose(bool dispose) { } - - public void Dispose() - { - if (!disposed) - { - Dispose(true); - disposed = true; - } - } - } -} \ No newline at end of file diff --git a/Billing.Shared/Models/Bill.cs b/Billing.Shared/Models/Bill.cs index 4ed804f..bd11615 100644 --- a/Billing.Shared/Models/Bill.cs +++ b/Billing.Shared/Models/Bill.cs @@ -1,10 +1,11 @@ using System; -using System.Xml.Linq; +using SQLite; namespace Billing.Models { - public class Bill : BaseModel + public class Bill : IIdItem { + [PrimaryKey, AutoIncrement] public int Id { get; set; } public decimal Amount { get; set; } public string Name { get; set; } @@ -13,29 +14,5 @@ namespace Billing.Models public string Store { get; set; } public DateTime CreateTime { get; set; } public string Note { get; set; } - - public override void OnXmlDeserialize(XElement node) - { - Id = Read(node, nameof(Id), 0); - Amount = Read(node, nameof(Amount), 0m); - Name = Read(node, nameof(Name), string.Empty); - CategoryId = Read(node, nameof(CategoryId), -1); - WalletId = Read(node, nameof(WalletId), -1); - Store = Read(node, nameof(Store), string.Empty); - CreateTime = Read(node, nameof(CreateTime), default(DateTime)); - Note = Read(node, nameof(Note), string.Empty); - } - - public override void OnXmlSerialize(XElement node) - { - Write(node, nameof(Id), Id); - Write(node, nameof(Amount), Amount); - Write(node, nameof(Name), Name); - Write(node, nameof(CategoryId), CategoryId); - Write(node, nameof(WalletId), WalletId); - Write(node, nameof(Store), Store); - Write(node, nameof(CreateTime), CreateTime); - Write(node, nameof(Note), Note); - } } } \ No newline at end of file diff --git a/Billing.Shared/Models/Category.cs b/Billing.Shared/Models/Category.cs index 8fd2e1d..9726509 100644 --- a/Billing.Shared/Models/Category.cs +++ b/Billing.Shared/Models/Category.cs @@ -1,50 +1,19 @@ -using Xamarin.Forms; -using System.Xml.Linq; +using SQLite; namespace Billing.Models { - public class Category : BaseModel + public class Category : IIdItem { + private const string ICON_DEFAULT = "ic_default"; + private const long TRANSPARENT_COLOR = 0x00ffffffL; + + [PrimaryKey, AutoIncrement] public int Id { get; set; } public CategoryType Type { get; set; } public string Icon { get; set; } = ICON_DEFAULT; public string Name { get; set; } - public Color TintColor { get; set; } = Color.Transparent; + public long TintColor { get; set; } = TRANSPARENT_COLOR; public int? ParentId { get; set; } - - 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 color = Read(node, nameof(TintColor), string.Empty); - if (!string.IsNullOrEmpty(color)) - { - TintColor = Color.FromHex(color); - } - var parentId = Read(node, nameof(ParentId), -1); - if (parentId >= 0) - { - ParentId = parentId; - } - } - - 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 (TintColor != Color.Transparent) - { - Write(node, nameof(TintColor), TintColor.ToHex()); - } - if (ParentId != null) - { - Write(node, nameof(ParentId), ParentId.Value); - } - } } public enum CategoryType diff --git a/Billing.Shared/Models/IIdItem.cs b/Billing.Shared/Models/IIdItem.cs new file mode 100644 index 0000000..1b1b124 --- /dev/null +++ b/Billing.Shared/Models/IIdItem.cs @@ -0,0 +1,7 @@ +namespace Billing.Models +{ + public interface IIdItem + { + public int Id { get; } + } +} diff --git a/Billing.Shared/SplashPage.xaml b/Billing.Shared/SplashPage.xaml new file mode 100644 index 0000000..5c15884 --- /dev/null +++ b/Billing.Shared/SplashPage.xaml @@ -0,0 +1,9 @@ + + + + diff --git a/Billing.Shared/SplashPage.xaml.cs b/Billing.Shared/SplashPage.xaml.cs new file mode 100644 index 0000000..5b3c88b --- /dev/null +++ b/Billing.Shared/SplashPage.xaml.cs @@ -0,0 +1,20 @@ +using Billing.UI; +using Xamarin.Forms; + +namespace Billing +{ + public partial class SplashPage : BillingPage + { + public SplashPage() + { + InitializeComponent(); + } + + public override async void OnLoaded() + { + await App.InitilalizeData(); + + await Shell.Current.GoToAsync("//Bills"); + } + } +} diff --git a/Billing.Shared/Store/StoreHelper.cs b/Billing.Shared/Store/StoreHelper.cs index 98c94fd..86454ed 100644 --- a/Billing.Shared/Store/StoreHelper.cs +++ b/Billing.Shared/Store/StoreHelper.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Billing.Models; -using Billing.UI; +using SQLite; using Xamarin.Essentials; using Resource = Billing.Languages.Resource; @@ -14,50 +15,20 @@ namespace Billing.Store 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"; + #region Sqlite3 + private const string dbfile = "data.db3"; + private static SQLiteAsyncConnection database; + #endregion - 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() + private static readonly AsyncLazy Instance = new(async () => { - 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() - { - var list = GetList(Path.Combine(PersonalFolder, categoryFile)); - if (list == null || list.Count == 0) + var instance = new StoreHelper(); + await database.CreateTablesAsync(); + var count = await database.ExecuteScalarAsync("SELECT COUNT(Id) FROM [Category]"); + if (count <= 0) { - list = new List + // init categories + await database.InsertAllAsync(new List { // sample categories new() { Id = 1, Name = Resource.Clothing, Icon = "clothes" }, @@ -89,53 +60,145 @@ namespace Billing.Store new() { Id = 113, ParentId = 6, Name = Resource.Party, Icon = "party" }, new() { Id = 200, ParentId = 10, Type = CategoryType.Income, Name = Resource.Salary, Icon = "#brand#buffer" }, new() { Id = 201, ParentId = 10, Type = CategoryType.Income, Name = Resource.Bonus, Icon = "dollar" }, - }; - Task.Run(() => WriteCategoriesInternal(list)); + }); } - return list; + return instance; + }); + + public static async Task> GetAccountsAsync() + { + var instance = await Instance; + return await instance.GetListAsync(); + } + public static async Task SaveAccountItemAsync(Account account) + { + var instance = await Instance; + return await instance.SaveItemAsync(account); + } + public static async Task DeleteAccountItemAsync(Account account) + { + var instance = await Instance; + return await instance.DeleteItemAsync(account); } - private void WriteCategoriesInternal(IEnumerable categories) + public static async Task> GetBillsAsync() { - var filename = Path.Combine(PersonalFolder, categoryFile); - WriteList(filename, categories); + var instance = await Instance; + return await instance.GetListAsync(); + } + public static async Task SaveBillItemAsync(Bill bill) + { + var instance = await Instance; + return await instance.SaveItemAsync(bill); + } + public static async Task DeleteBillItemAsync(Bill bill) + { + var instance = await Instance; + return await instance.DeleteItemAsync(bill); + } + + public static async Task> GetCategoriesAsync() + { + var instance = await Instance; + return await instance.GetListAsync(); + } + public static async Task SaveCategoryItemAsync(Category category) + { + var instance = await Instance; + return await instance.SaveItemAsync(category); + } + public static async Task DeleteCategoryItemAsync(Category category) + { + var instance = await Instance; + return await instance.DeleteItemAsync(category); + } + + private StoreHelper() + { + database = new SQLiteAsyncConnection(Path.Combine(PersonalFolder, dbfile), + SQLiteOpenFlags.ReadWrite | + SQLiteOpenFlags.Create | + SQLiteOpenFlags.SharedCache); + } + + public Task> GetListAsync(string query, params object[] args) where T : new() + { + try + { + return database.QueryAsync(query, args); + } + catch (Exception ex) + { + Helper.Error("db.read", $"failed to read db, query: {string.Format(query, args)}, error: {ex.Message}"); + } + return default; + } + + public Task GetItemAsync(int id) where T : IIdItem, new() + { + try + { + var source = new TaskCompletionSource(); + Task.Run(async () => + { + var list = await database.QueryAsync($"SELECT * FROM [{typeof(T).Name}] WHERE [Id] = ?", id); + source.SetResult(list.FirstOrDefault()); + }); + return source.Task; + } + catch (Exception ex) + { + Helper.Error("db.read", $"failed to get item, table: {typeof(T)}, id: {id}, error: {ex.Message}"); + } + return default; } #region Helper - private void WriteList(string filename, IEnumerable list) where T : IModel, new() + private Task> GetListAsync() where T : new() { - if (list == null) - { - return; - } try { - using var stream = File.Open(filename, FileMode.Create); - list.ToStream(stream); + return database.Table().ToListAsync(); } catch (Exception ex) { - Helper.Error("file.write", $"failed to write file: {filename}, error: {ex.Message}"); + Helper.Error("db.read", $"failed to read db, error: {ex.Message}"); } + return default; } - private List GetList(string file) where T : IModel, new() + private Task SaveItemAsync(T item) where T : IIdItem { try { - if (File.Exists(file)) + if (item.Id < 0) { - using var stream = File.OpenRead(file); - var list = ModelExtensionHelper.FromStream(stream); - return list; + return database.InsertAsync(item); + } + else + { + return database.UpdateAsync(item); } } catch (Exception ex) { - Helper.Error("file.read", $"failed to read file: {file}, error: {ex.Message}"); + Helper.Error("db.write", $"failed to insert/update item, table: {typeof(T)}, id: {item.Id}, item: {item}, error: {ex.Message}"); } - return default; + return Task.FromResult(-1); + } + + private Task DeleteItemAsync(T item) where T : IIdItem + { + try + { + return database.DeleteAsync(item); + } + catch (Exception ex) + { + Helper.Error("db.delete", $"failed to delete item, table: {typeof(T)}, id: {item.Id}, item: {item}, error: {ex.Message}"); + } + return Task.FromResult(-1); } #endregion diff --git a/Billing.Shared/UI/Converters.cs b/Billing.Shared/UI/Converters.cs index 7bd695b..58eeee1 100644 --- a/Billing.Shared/UI/Converters.cs +++ b/Billing.Shared/UI/Converters.cs @@ -205,7 +205,7 @@ namespace Billing.UI { if (!int.TryParse(key, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int i)) { - return ImageSource.FromFile(BaseModel.ICON_DEFAULT); + return ImageSource.FromFile(Definition.DefaultIcon); } glyph = char.ConvertFromUtf32(i); } diff --git a/Billing.Shared/UI/Definition.cs b/Billing.Shared/UI/Definition.cs index e3928c6..c132632 100644 --- a/Billing.Shared/UI/Definition.cs +++ b/Billing.Shared/UI/Definition.cs @@ -1,17 +1,15 @@ using System; -using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; -using System.Xml.Linq; using Billing.Languages; -using Billing.Models; using Xamarin.Forms; namespace Billing.UI { public static partial class Definition { - public static string PrimaryColorKey = "PrimaryColor"; + public const string PrimaryColorKey = "PrimaryColor"; + public const string DefaultIcon = "ic_default"; + public static partial (string main, long build) GetVersion(); public static partial string GetRegularFontFamily(); public static partial string GetSemiBoldFontFamily(); @@ -98,50 +96,33 @@ namespace Billing.UI // add 23:59:59.999... return date.AddTicks(863999999999); } - } - public static class ModelExtensionHelper - { - public static List FromStream(Stream stream) where T : IModel, new() + public static bool IsTransparent(this long color) { - 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; + return (color & 0xff000000L) == 0x00000000L; } - public static void ToStream(this IEnumerable list, Stream stream) where T : IModel + public static Color ToColor(this long color) { - 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); - } + ulong c = (ulong)color; + int r = (int)(c & 0xff); + c >>= 8; + int g = (int)(c & 0xff); + c >>= 8; + int b = (int)(c & 0xff); + c >>= 8; + int a = (int)(c & 0xff); + return Color.FromRgba(r, g, b, a); + } - XDocument doc = new(new XDeclaration("1.0", "utf-8", "yes"), root); - doc.Save(stream, SaveOptions.DisableFormatting); + public static long ToLong(this Color color) + { + long l = + (uint)(color.A * 255) << 24 | + (uint)(color.B * 255) << 16 | + (uint)(color.G * 255) << 8 | + (uint)(color.R * 255); + return l; } } diff --git a/Billing.Shared/Views/AccountPage.xaml.cs b/Billing.Shared/Views/AccountPage.xaml.cs index 3604e71..77a136d 100644 --- a/Billing.Shared/Views/AccountPage.xaml.cs +++ b/Billing.Shared/Views/AccountPage.xaml.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Billing.Languages; using Billing.Models; +using Billing.Store; using Billing.UI; using Xamarin.Forms; @@ -120,25 +120,27 @@ namespace Billing.Views if (group == null) { Helper.Error("account.delete", "unexpected deleting account, cannot find the current category"); - return; - } - group.Remove(account); - if (group.Count == 0) + } + else { - accounts.Remove(group); + group.Remove(account); + if (group.Count == 0) + { + accounts.Remove(group); + } } RefreshBalance(); groupLayout.Refresh(accounts); RankPage.Instance?.SetNeedRefresh(); - _ = Task.Run(App.WriteAccounts); + await StoreHelper.DeleteAccountItemAsync(account); } } } } - private void AccountChecked(object sender, AccountEventArgs e) + private async void AccountChecked(object sender, AccountEventArgs e) { var add = e.Account.Id < 0; if (add) @@ -151,7 +153,7 @@ namespace Billing.Views RankPage.Instance?.SetNeedRefresh(); - Task.Run(App.WriteAccounts); + await StoreHelper.SaveAccountItemAsync(e.Account); } } diff --git a/Billing.Shared/Views/AddAccountPage.xaml.cs b/Billing.Shared/Views/AddAccountPage.xaml.cs index ddae6cd..8b04253 100644 --- a/Billing.Shared/Views/AddAccountPage.xaml.cs +++ b/Billing.Shared/Views/AddAccountPage.xaml.cs @@ -59,7 +59,7 @@ namespace Billing.Views this.account = account; if (account == null) { - AccountIcon = BaseModel.ICON_DEFAULT; + AccountIcon = Definition.DefaultIcon; Category = AccountCategory.Cash; } else diff --git a/Billing.Shared/Views/AddCategoryPage.xaml.cs b/Billing.Shared/Views/AddCategoryPage.xaml.cs index f9d3430..a9cb66f 100644 --- a/Billing.Shared/Views/AddCategoryPage.xaml.cs +++ b/Billing.Shared/Views/AddCategoryPage.xaml.cs @@ -56,14 +56,13 @@ namespace Billing.Views { CategoryName = category.Name; CategoryIcon = category.Icon; - if (category.TintColor == Color.Transparent || - category.TintColor == default) + if (category.TintColor.IsTransparent()) { TintColor = BaseTheme.CurrentPrimaryColor; } else { - TintColor = category.TintColor; + TintColor = category.TintColor.ToColor(); } } else @@ -103,6 +102,7 @@ namespace Billing.Views { var currentColor = BaseTheme.CurrentPrimaryColor; var tintColor = TintColor; + var color = (tintColor == currentColor ? Color.Transparent : tintColor).ToLong(); var category = App.Categories.FirstOrDefault(c => c.Id == categoryId); if (category == null) { @@ -111,7 +111,7 @@ namespace Billing.Views Id = -1, Name = CategoryName, Icon = CategoryIcon, - TintColor = tintColor == currentColor ? Color.Transparent : tintColor, + TintColor = color, ParentId = parent?.Id, Type = parent?.Type ?? CategoryType.Spending }); @@ -120,7 +120,7 @@ namespace Billing.Views { category.Name = CategoryName; category.Icon = CategoryIcon; - category.TintColor = tintColor == currentColor ? Color.Transparent : tintColor; + category.TintColor = color; CategoryChecked?.Invoke(this, category); } await Navigation.PopAsync(); diff --git a/Billing.Shared/Views/BillPage.xaml.cs b/Billing.Shared/Views/BillPage.xaml.cs index e5f9d1b..a6ef24f 100644 --- a/Billing.Shared/Views/BillPage.xaml.cs +++ b/Billing.Shared/Views/BillPage.xaml.cs @@ -1,4 +1,5 @@ using Billing.Models; +using Billing.Store; using Billing.UI; using System; using System.Collections.Generic; @@ -84,7 +85,7 @@ namespace Billing.Views private void UpdateBill(UIBill bill) { - bill.Icon = App.Categories.FirstOrDefault(c => c.Id == bill.Bill.CategoryId)?.Icon ?? BaseModel.ICON_DEFAULT; + bill.Icon = App.Categories.FirstOrDefault(c => c.Id == bill.Bill.CategoryId)?.Icon ?? Definition.DefaultIcon; bill.Name = bill.Bill.Name; bill.DateCreation = bill.Bill.CreateTime; bill.Amount = bill.Bill.Amount; @@ -158,13 +159,13 @@ namespace Billing.Views RankPage.Instance?.SetNeedRefresh(); - _ = Task.Run(App.WriteBills); + await StoreHelper.DeleteBillItemAsync(bill.Bill); } } } } - private void OnBillChecked(object sender, Bill e) + private async void OnBillChecked(object sender, Bill e) { if (e.Id < 0) { @@ -195,7 +196,7 @@ namespace Billing.Views RankPage.Instance?.SetNeedRefresh(); - Task.Run(App.WriteBills); + await StoreHelper.SaveBillItemAsync(e); } } diff --git a/Billing.Shared/Views/CategoryPage.xaml.cs b/Billing.Shared/Views/CategoryPage.xaml.cs index b71a978..d5f15bb 100644 --- a/Billing.Shared/Views/CategoryPage.xaml.cs +++ b/Billing.Shared/Views/CategoryPage.xaml.cs @@ -1,11 +1,11 @@ using Billing.Languages; using Billing.Models; +using Billing.Store; using Billing.Themes; using Billing.UI; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Xamarin.Forms; namespace Billing.Views @@ -68,9 +68,9 @@ namespace Billing.Views Icon = category.Icon, Name = category.Name, IsTopCategory = IsTopCategory, - TintColor = category.TintColor == Color.Transparent || category.TintColor == default ? + TintColor = category.TintColor.IsTransparent() ? BaseTheme.CurrentPrimaryColor : - category.TintColor + category.TintColor.ToColor() }; } @@ -112,7 +112,7 @@ namespace Billing.Views Categories.Remove(c); groupLayout.Refresh(Categories); App.Categories.Remove(c.Category); - _ = Task.Run(App.WriteCategories); + await StoreHelper.DeleteCategoryItemAsync(c.Category); } } } @@ -144,7 +144,7 @@ namespace Billing.Views } } - private void OnCategoryChecked(object sender, Category category) + private async void OnCategoryChecked(object sender, Category category) { if (category.Id < 0) { @@ -183,16 +183,16 @@ namespace Billing.Views } groupLayout.Refresh(Categories); - Task.Run(App.WriteCategories); + await StoreHelper.SaveCategoryItemAsync(category); } private void UpdateCategory(UICategory c) { c.Name = c.Category.Name; c.Icon = c.Category.Icon; - c.TintColor = c.Category.TintColor == Color.Transparent || c.Category.TintColor == default ? + c.TintColor = c.Category.TintColor.IsTransparent() ? BaseTheme.CurrentPrimaryColor : - c.Category.TintColor; + c.Category.TintColor.ToColor(); } } diff --git a/Billing.Shared/Views/CategorySelectPage.xaml.cs b/Billing.Shared/Views/CategorySelectPage.xaml.cs index 2e6f9e5..21eebf0 100644 --- a/Billing.Shared/Views/CategorySelectPage.xaml.cs +++ b/Billing.Shared/Views/CategorySelectPage.xaml.cs @@ -72,7 +72,7 @@ namespace Billing.Views IsChecked = c.Id == categoryId, Icon = c.Icon, Name = c.Name, - TintColor = c.TintColor == Color.Transparent || c.TintColor == default ? defaultColor : c.TintColor + TintColor = c.TintColor.IsTransparent() ? defaultColor : c.TintColor.ToColor() }; } diff --git a/Billing.Shared/Views/IconSelectPage.xaml.cs b/Billing.Shared/Views/IconSelectPage.xaml.cs index 9347e7f..cbc395d 100644 --- a/Billing.Shared/Views/IconSelectPage.xaml.cs +++ b/Billing.Shared/Views/IconSelectPage.xaml.cs @@ -1,5 +1,4 @@ -using Billing.Models; -using Billing.UI; +using Billing.UI; using System; using System.Collections.Generic; using System.Linq; @@ -37,7 +36,7 @@ namespace Billing.Views { var source = new List { - new() { Icon = BaseModel.ICON_DEFAULT }, + new() { Icon = Definition.DefaultIcon }, new() { Icon = "wallet" }, new() { Icon = "dollar" }, new() { Icon = "creditcard" }, diff --git a/Billing.Shared/Views/RankPage.xaml.cs b/Billing.Shared/Views/RankPage.xaml.cs index 0dd3ebc..0852d22 100644 --- a/Billing.Shared/Views/RankPage.xaml.cs +++ b/Billing.Shared/Views/RankPage.xaml.cs @@ -1,4 +1,5 @@ using Billing.Models; +using Billing.Store; using Billing.Themes; using Billing.UI; using Microcharts; @@ -473,7 +474,7 @@ namespace Billing.Views private async void OnBillChecked(object sender, Bill e) { - await Task.Run(App.WriteBills); + await StoreHelper.SaveBillItemAsync(e); LoadData(); } diff --git a/Billing/Billing.Android/Billing.Android.csproj b/Billing/Billing.Android/Billing.Android.csproj index 9c3a3be..83f151f 100644 --- a/Billing/Billing.Android/Billing.Android.csproj +++ b/Billing/Billing.Android/Billing.Android.csproj @@ -65,12 +65,11 @@ - - 0.9.5.9 - + + diff --git a/Billing/Billing.iOS/Billing.iOS.csproj b/Billing/Billing.iOS/Billing.iOS.csproj index 9188daf..4097576 100644 --- a/Billing/Billing.iOS/Billing.iOS.csproj +++ b/Billing/Billing.iOS/Billing.iOS.csproj @@ -169,12 +169,11 @@ - - 0.9.5.9 - + + diff --git a/Billing/Billing.iOS/Info.plist b/Billing/Billing.iOS/Info.plist index 13847c2..0a4fe63 100644 --- a/Billing/Billing.iOS/Info.plist +++ b/Billing/Billing.iOS/Info.plist @@ -41,8 +41,6 @@ OpenSans-Regular.ttf OpenSans-SemiBold.ttf - UIFileSharingEnabled - CFBundleVersion 9 CFBundleShortVersionString