diff --git a/.gitignore b/.gitignore
index 58d4acc..ff9ce1a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -402,4 +402,7 @@ ASALocalRun/
.mfractor/
# Local History for Visual Studio
-.localhistory/
\ No newline at end of file
+.localhistory/
+
+# Customization
+Svg2Png/
\ No newline at end of file
diff --git a/Billing.Shared/Billing.Shared.projitems b/Billing.Shared/Billing.Shared.projitems
index 55ce733..2f2dce2 100644
--- a/Billing.Shared/Billing.Shared.projitems
+++ b/Billing.Shared/Billing.Shared.projitems
@@ -21,6 +21,9 @@
MainShell.xaml
+
+
+
diff --git a/Billing.Shared/Models/BaseModel.cs b/Billing.Shared/Models/BaseModel.cs
new file mode 100644
index 0000000..647af34
--- /dev/null
+++ b/Billing.Shared/Models/BaseModel.cs
@@ -0,0 +1,182 @@
+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
+{
+ 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()
+ {
+ XDocument xdoc = ToXml();
+ using MemoryStream ms = new();
+ using StreamWriter writer = new(ms, Encoding.UTF8);
+ xdoc.Save(writer, SaveOptions.DisableFormatting);
+ writer.Flush();
+ 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;
+ }
+ }
+}
diff --git a/Billing.Shared/Models/Billing.cs b/Billing.Shared/Models/Billing.cs
new file mode 100644
index 0000000..0b38869
--- /dev/null
+++ b/Billing.Shared/Models/Billing.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Xml.Linq;
+
+namespace Billing.Models;
+
+public class Billing : BaseModel
+{
+ public decimal Amount { get; set; }
+ public string Name { get; set; }
+ public int CategoryId { get; set; }
+ public string Store { get; set; }
+ public DateTime CreateTime { get; set; }
+
+ public override void OnXmlDeserialize(XElement node)
+ {
+ Amount = Read(node, nameof(Amount), 0m);
+ Name = Read(node, nameof(Name), string.Empty);
+ CategoryId = Read(node, nameof(CategoryId), -1);
+ Store = Read(node, nameof(Store), string.Empty);
+ CreateTime = Read(node, nameof(CreateTime), default(DateTime));
+ }
+
+ public override void OnXmlSerialize(XElement node)
+ {
+ Write(node, nameof(Amount), Amount);
+ Write(node, nameof(Name), Name);
+ Write(node, nameof(CategoryId), CategoryId);
+ Write(node, nameof(Store), Store);
+ Write(node, nameof(CreateTime), CreateTime);
+ }
+}
diff --git a/Billing.Shared/Models/Category.cs b/Billing.Shared/Models/Category.cs
new file mode 100644
index 0000000..213aa1b
--- /dev/null
+++ b/Billing.Shared/Models/Category.cs
@@ -0,0 +1,31 @@
+using System.Xml.Linq;
+
+namespace Billing.Models;
+
+public class Category : BaseModel
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public int? ParentId { get; set; }
+
+ public override void OnXmlDeserialize(XElement node)
+ {
+ Id = Read(node, nameof(Id), 0);
+ Name = Read(node, nameof(Name), string.Empty);
+ 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(Name), Name);
+ if (ParentId != null)
+ {
+ Write(node, nameof(ParentId), ParentId.Value);
+ }
+ }
+}
diff --git a/Billing/Billing.iOS/Billing.iOS.csproj b/Billing/Billing.iOS/Billing.iOS.csproj
index dfecd7b..5fd347b 100644
--- a/Billing/Billing.iOS/Billing.iOS.csproj
+++ b/Billing/Billing.iOS/Billing.iOS.csproj
@@ -130,6 +130,7 @@
+