Compare commits

...

29 Commits

Author SHA1 Message Date
776cc7da49 location adjustment 2022-04-11 15:12:34 +08:00
1072a1a15c fix 2022-03-28 16:46:22 +08:00
469b1a8627 tiny fix 2022-03-28 14:46:16 +08:00
ba289b6087 location 2022-03-21 11:04:37 +08:00
b7affae8ab separate location extension 2022-03-18 18:44:30 +08:00
7ca377b8c2 wgs84 to gcj02 2022-03-18 14:08:17 +08:00
f27b0a2564 android map initialize 2022-03-18 00:56:42 +08:00
a214110c8c ios map fix 2022-03-18 00:55:42 +08:00
4067bc2768 add map view page 2022-03-18 00:17:40 +08:00
ba7b3e7389 fix android release configuration 2022-03-17 23:13:59 +08:00
5cbcfbcd56 release new version 2022-03-17 21:34:26 +08:00
ef5e91aad1 feature: shortcut 2022-03-17 20:29:27 +08:00
60f7824cb5 feature: save location 2022-03-17 16:19:18 +08:00
b46b150f6a tiny fix 2022-03-17 13:19:45 +08:00
cac4735bc4 haptic feedback 2022-03-16 16:45:10 +08:00
d3af69b31e feature: support db import on Android 2022-03-15 22:45:21 +08:00
8ba6f4bf85 fix issue 2022-03-15 20:12:40 +08:00
5b209cc19c optimized and add diagnostic feature 2022-03-15 15:17:02 +08:00
77b4e54734 add: last used category 2022-03-15 07:47:44 +08:00
9a8f1289ed version up 2022-03-12 01:41:24 +08:00
c43bfb51be fix crash of sqlite in release mode 2022-03-12 01:08:17 +08:00
51ac42b9fc share db 2022-03-11 22:25:31 +08:00
6d2e0624ab version up 2022-03-11 17:17:33 +08:00
28897c96ea issue fix 2022-03-11 17:16:49 +08:00
5ec4119025 switch to sqlite 2022-03-11 16:10:11 +08:00
f5f16d43f4 format BindableProperty & fix a tiny issue about rank page refreshing 2022-03-11 13:17:00 +08:00
71c1a7f0f1 fix date selection range issue 2022-03-11 00:07:50 +08:00
74053a328e filter conditions 2022-03-10 17:27:49 +08:00
84ec2df987 android segmented control renderer 2022-03-10 00:02:11 +08:00
114 changed files with 12090 additions and 4505 deletions

View File

@ -1,9 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Billing.Languages; using Billing.Languages;
using Billing.Models; using Billing.Models;
using Billing.Store; using Billing.Store;
using Billing.Themes; using Billing.Themes;
using Billing.UI;
using Xamarin.Essentials; using Xamarin.Essentials;
using Xamarin.Forms; using Xamarin.Forms;
@ -11,41 +13,55 @@ namespace Billing
{ {
public class App : Application public class App : Application
{ {
internal const string NewBillAction = "/newbill";
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<Bill> Bills => bills ??= new List<Bill>();
public static List<Account> Accounts => accounts ??= new List<Account>(); public static List<Account> Accounts => accounts ??= new List<Account>();
public static List<Category> Categories => categories ??= new List<Category>(); public static List<Category> Categories => categories ??= new List<Category>();
public static bool SaveLocation => saveLocation;
public static string MainRoute => mainRoute;
private static List<Bill> bills; private static List<Bill> bills;
private static List<Account> accounts; private static List<Account> accounts;
private static List<Category> categories; private static List<Category> categories;
private static bool saveLocation;
private static string mainRoute;
public App() private string initialUrl;
public App(string url = null)
{ {
if (url == NewBillAction)
{
#if __ANDROID__
mainRoute = "//Bills/Details";
#endif
}
else
{
mainRoute = "//Bills";
initialUrl = url;
}
CurrentCulture = new PlatformCulture(); CurrentCulture = new PlatformCulture();
saveLocation = Preferences.Get(Definition.SaveLocationKey, false);
InitResources(); InitResources();
MainPage = new MainShell(); MainPage = new MainShell();
Shell.Current.GoToAsync("//Settings");
} }
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() protected override void OnStart()
{ {
Helper.Debug($"personal folder: {StoreHelper.PersonalFolder}"); Helper.Debug($"personal folder: {StoreHelper.PersonalFolder}");
Helper.Debug($"cache folder: {StoreHelper.CacheFolder}"); Helper.Debug($"cache folder: {StoreHelper.CacheFolder}");
accounts = StoreHelper.GetAccounts();
categories = StoreHelper.GetCategories();
bills = StoreHelper.GetBills();
Shell.Current.GoToAsync("//Bills"); if (initialUrl != null)
{
var url = initialUrl;
initialUrl = null;
_ = OpenUrl(url);
}
} }
protected override void OnResume() protected override void OnResume()
@ -83,5 +99,56 @@ namespace Billing
// TODO: status bar // TODO: status bar
Resources = instance; Resources = instance;
} }
public static void SetSaveLocation(bool flag)
{
saveLocation = flag;
}
public static async Task InitializeData()
{
var instance = await StoreHelper.Instance;
await Task.WhenAll(
Task.Run(async () => accounts = await instance.GetListAsync<Account>()),
Task.Run(async () => categories = await instance.GetListAsync<Category>()),
Task.Run(async () => bills = await instance.GetListAsync<Bill>()));
}
#if __ANDROID__
public static async Task<bool> OpenUrl(string url)
#elif __IOS__
public static bool OpenUrl(string url)
#endif
{
if (string.IsNullOrEmpty(url))
{
return false;
}
if (File.Exists(url))
{
#if __ANDROID__
var status = await Helper.CheckAndRequestPermissionAsync<Permissions.StorageRead>();
if (status != PermissionStatus.Granted)
{
return false;
}
#endif
_ = Task.Run(async () =>
{
var result = await StoreHelper.ReloadDatabase(url);
if (result)
{
await InitializeData();
var current = Shell.Current.CurrentPage;
if (current is BillingPage page)
{
MainThread.BeginInvokeOnMainThread(() => page.TriggerRefresh());
}
}
});
}
return true;
}
} }
} }

View File

@ -11,6 +11,10 @@
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Languages\en.xml" /> <EmbeddedResource Include="$(MSBuildThisFileDirectory)Languages\en.xml" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Languages\zh-CN.xml" /> <EmbeddedResource Include="$(MSBuildThisFileDirectory)Languages\zh-CN.xml" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)SplashPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)App.cs" /> <Compile Include="$(MSBuildThisFileDirectory)App.cs" />
@ -18,10 +22,11 @@
<Compile Include="$(MSBuildThisFileDirectory)Helper.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Helper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Languages\PlatformCulture.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Languages\PlatformCulture.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Languages\Resource.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Languages\Resource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)LocationExtension.cs" />
<Compile Include="$(MSBuildThisFileDirectory)MainShell.xaml.cs"> <Compile Include="$(MSBuildThisFileDirectory)MainShell.xaml.cs">
<DependentUpon>MainShell.xaml</DependentUpon> <DependentUpon>MainShell.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile> </Compile>
<Compile Include="$(MSBuildThisFileDirectory)Models\BaseModel.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" />
@ -29,6 +34,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Themes\Light.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Themes\Light.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\BillingDate.xaml.cs"> <Compile Include="$(MSBuildThisFileDirectory)UI\BillingDate.xaml.cs">
<DependentUpon>BillingDate.xaml</DependentUpon> <DependentUpon>BillingDate.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile> </Compile>
<Compile Include="$(MSBuildThisFileDirectory)UI\BillingPage.cs" /> <Compile Include="$(MSBuildThisFileDirectory)UI\BillingPage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\ColorPicker.cs" /> <Compile Include="$(MSBuildThisFileDirectory)UI\ColorPicker.cs" />
@ -41,12 +47,15 @@
<Compile Include="$(MSBuildThisFileDirectory)UI\WrapLayout.cs" /> <Compile Include="$(MSBuildThisFileDirectory)UI\WrapLayout.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Views\AccountPage.xaml.cs"> <Compile Include="$(MSBuildThisFileDirectory)Views\AccountPage.xaml.cs">
<DependentUpon>AccountPage.xaml</DependentUpon> <DependentUpon>AccountPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile> </Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\AddAccountPage.xaml.cs"> <Compile Include="$(MSBuildThisFileDirectory)Views\AddAccountPage.xaml.cs">
<DependentUpon>AddAccountPage.xaml</DependentUpon> <DependentUpon>AddAccountPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile> </Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\AddBillPage.xaml.cs"> <Compile Include="$(MSBuildThisFileDirectory)Views\AddBillPage.xaml.cs">
<DependentUpon>AddBillPage.xaml</DependentUpon> <DependentUpon>AddBillPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile> </Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\AddCategoryPage.xaml.cs"> <Compile Include="$(MSBuildThisFileDirectory)Views\AddCategoryPage.xaml.cs">
<DependentUpon>AddCategoryPage.xaml</DependentUpon> <DependentUpon>AddCategoryPage.xaml</DependentUpon>
@ -54,6 +63,7 @@
</Compile> </Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\BillPage.xaml.cs"> <Compile Include="$(MSBuildThisFileDirectory)Views\BillPage.xaml.cs">
<DependentUpon>BillPage.xaml</DependentUpon> <DependentUpon>BillPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile> </Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\CategoryPage.xaml.cs"> <Compile Include="$(MSBuildThisFileDirectory)Views\CategoryPage.xaml.cs">
<DependentUpon>CategoryPage.xaml</DependentUpon> <DependentUpon>CategoryPage.xaml</DependentUpon>
@ -73,11 +83,19 @@
</Compile> </Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\SettingPage.xaml.cs"> <Compile Include="$(MSBuildThisFileDirectory)Views\SettingPage.xaml.cs">
<DependentUpon>SettingPage.xaml</DependentUpon> <DependentUpon>SettingPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile> </Compile>
<Compile Include="$(MSBuildThisFileDirectory)UI\OptionsCells.cs" /> <Compile Include="$(MSBuildThisFileDirectory)UI\OptionsCells.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Store\StoreHelper.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Store\StoreHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\Bill.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Models\Bill.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\SegmentedControl.cs" /> <Compile Include="$(MSBuildThisFileDirectory)UI\SegmentedControl.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\IIdItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SplashPage.xaml.cs">
<DependentUpon>SplashPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Models\Logs.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Views\ViewLocationPage.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)MainShell.xaml"> <EmbeddedResource Include="$(MSBuildThisFileDirectory)MainShell.xaml">
@ -111,6 +129,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\IconSelectPage.xaml"> <EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\IconSelectPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator> <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
@ -119,21 +138,25 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\CategoryPage.xaml"> <EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\CategoryPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator> <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\AddCategoryPage.xaml"> <EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\AddCategoryPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator> <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\CategorySelectPage.xaml"> <EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\CategorySelectPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator> <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\RankPage.xaml"> <EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\RankPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator> <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>

View File

@ -1,8 +1,11 @@
using Billing.Models; using Billing.Models;
using Billing.UI;
using Billing.Views; using Billing.Views;
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Xamarin.Essentials; using Xamarin.Essentials;
using Xamarin.Forms; using Xamarin.Forms;
@ -16,33 +19,35 @@ namespace Billing
var time = DateTime.Now.ToString("HH:mm:ss.fff"); var time = DateTime.Now.ToString("HH:mm:ss.fff");
System.Diagnostics.Debug.WriteLine($"[{time}] - {message}"); System.Diagnostics.Debug.WriteLine($"[{time}] - {message}");
} }
public static void Error(string category, Exception ex)
{
Error(category, ex?.Message ?? "unknown error");
}
public static void Error(string category, string message)
{
var time = DateTime.Now.ToString("HH:mm:ss.fff");
System.Diagnostics.Debug.Fail($"[{time}] - {category}", message);
}
#else #else
#pragma warning disable IDE0060 // Remove unused parameter #pragma warning disable IDE0060 // Remove unused parameter
public static void Debug(string message) public static void Debug(string message)
{ {
} }
#pragma warning restore IDE0060 // Remove unused parameter
#endif
public static void Error(string category, Exception ex) public static void Error(string category, Exception ex)
{ {
Error(category, ex?.ToString() ?? "unknown error");
} }
public static void Error(string category, string message) public static void Error(string category, string message)
{ {
} #if DEBUG
#pragma warning restore IDE0060 // Remove unused parameter var time = DateTime.Now.ToString("HH:mm:ss.fff");
System.Diagnostics.Debug.WriteLine($"[{time}] - {category}", message);
MainThread.BeginInvokeOnMainThread(async () => await Shell.Current.DisplayAlert(category, message, "Ok"));
#endif #endif
_ = Store.StoreHelper.SaveLogItemAsync(new Logs()
{
LogTime = DateTime.Now,
Category = category,
Detail = message
});
}
public static bool NetworkAvailable public static bool NetworkAvailable
{ {
get get
@ -80,9 +85,11 @@ namespace Billing
public static UIBill WrapBill(Bill b) public static UIBill WrapBill(Bill b)
{ {
var category = App.Categories.FirstOrDefault(c => c.Id == b.CategoryId);
return new UIBill(b) return new UIBill(b)
{ {
Icon = App.Categories.FirstOrDefault(c => c.Id == b.CategoryId)?.Icon ?? BaseModel.ICON_DEFAULT, Icon = category?.Icon ?? Definition.DefaultIcon,
TintColor = category?.TintColor ?? Definition.TransparentColor,
Name = b.Name, Name = b.Name,
DateCreation = b.CreateTime, DateCreation = b.CreateTime,
Amount = b.Amount, Amount = b.Amount,
@ -129,5 +136,26 @@ namespace Billing
} }
public delegate void PropertyValueChanged<TResult, TOwner>(TOwner obj, TResult old, TResult @new); public delegate void PropertyValueChanged<TResult, TOwner>(TOwner obj, TResult old, TResult @new);
public static async Task<PermissionStatus> CheckAndRequestPermissionAsync<T>() where T : Permissions.BasePermission, new()
{
var status = await Permissions.CheckStatusAsync<T>();
if (status != PermissionStatus.Disabled &&
status != PermissionStatus.Granted)
{
status = await Permissions.RequestAsync<T>();
}
return status;
}
}
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<Task<T>> factory) : base(() => Task.Run(factory)) { }
public TaskAwaiter<T> GetAwaiter()
{
return Value.GetAwaiter();
}
} }
} }

View File

@ -10,12 +10,22 @@ namespace Billing.Languages
internal class Resource internal class Resource
{ {
public static string Ok => Text(nameof(Ok)); public static string Ok => Text(nameof(Ok));
public static string Cancel => Text(nameof(Cancel));
public static string Yes => Text(nameof(Yes)); public static string Yes => Text(nameof(Yes));
public static string No => Text(nameof(No)); public static string No => Text(nameof(No));
public static string ConfirmDeleteAccount => Text(nameof(ConfirmDeleteAccount)); public static string ConfirmDeleteAccount => Text(nameof(ConfirmDeleteAccount));
public static string ConfirmDeleteBill => Text(nameof(ConfirmDeleteBill)); public static string ConfirmDeleteBill => Text(nameof(ConfirmDeleteBill));
public static string TitleDateFormat => Text(nameof(TitleDateFormat)); public static string TitleDateFormat => Text(nameof(TitleDateFormat));
public static string TitleShortDateFormat => Text(nameof(TitleShortDateFormat));
public static string DateRangeFormat => Text(nameof(DateRangeFormat)); public static string DateRangeFormat => Text(nameof(DateRangeFormat));
public static string Custom => Text(nameof(Custom));
public static string Monthly => Text(nameof(Monthly));
public static string Today => Text(nameof(Today));
public static string PastMonth => Text(nameof(PastMonth));
public static string PastQuarter => Text(nameof(PastQuarter));
public static string PastSixMonths => Text(nameof(PastSixMonths));
public static string PastYear => Text(nameof(PastYear));
public static string Total => Text(nameof(Total));
public static string Cash => Text(nameof(Cash)); public static string Cash => Text(nameof(Cash));
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));
@ -29,9 +39,15 @@ namespace Billing.Languages
public static string AmountRequired => Text(nameof(AmountRequired)); public static string AmountRequired => Text(nameof(AmountRequired));
public static string Income => Text(nameof(Income)); public static string Income => Text(nameof(Income));
public static string Spending => Text(nameof(Spending)); public static string Spending => Text(nameof(Spending));
public static string LastSelected => Text(nameof(LastSelected));
public static string Recent => Text(nameof(Recent));
public static string CategoryManage => Text(nameof(CategoryManage)); public static string CategoryManage => Text(nameof(CategoryManage));
public static string AddCategory => Text(nameof(AddCategory)); public static string AddCategory => Text(nameof(AddCategory));
public static string ConfirmDeleteCategory => Text(nameof(ConfirmDeleteCategory)); public static string ConfirmDeleteCategory => Text(nameof(ConfirmDeleteCategory));
public static string ShareLogs => Text(nameof(ShareLogs));
public static string ManyRecords => Text(nameof(ManyRecords));
public static string SendEmail => Text(nameof(SendEmail));
public static string HowToShareDiagnostic => Text(nameof(HowToShareDiagnostic));
#region Categories #region Categories
public static string Clothing => Text(nameof(Clothing)); public static string Clothing => Text(nameof(Clothing));

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<Ok>OK</Ok> <Ok>OK</Ok>
<Cancel>Cancel</Cancel>
<About>About</About> <About>About</About>
<Version>Version</Version> <Version>Version</Version>
<Preference>Preference</Preference> <Preference>Preference</Preference>
@ -19,7 +20,19 @@
<NoRecords>Bills not yet generated</NoRecords> <NoRecords>Bills not yet generated</NoRecords>
<TapToMemo>Click here to record</TapToMemo> <TapToMemo>Click here to record</TapToMemo>
<TitleDateFormat>MM/dd/yyyy</TitleDateFormat> <TitleDateFormat>MM/dd/yyyy</TitleDateFormat>
<TitleShortDateFormat>MM/dd/yyyy</TitleShortDateFormat>
<DateRangeFormat>MM/dd</DateRangeFormat> <DateRangeFormat>MM/dd</DateRangeFormat>
<To>To</To>
<Type>Type</Type>
<Preset>Preset</Preset>
<Custom>Custom</Custom>
<Monthly>Monthly</Monthly>
<Today>Today</Today>
<PastMonth>Past Month</PastMonth>
<PastQuarter>Past Quarter</PastQuarter>
<PastSixMonths>Past Six Months</PastSixMonths>
<PastYear>Past Year</PastYear>
<Total>Total</Total>
<Balance>Balance</Balance> <Balance>Balance</Balance>
<Assets>Assets</Assets> <Assets>Assets</Assets>
<Liability>Liability</Liability> <Liability>Liability</Liability>
@ -52,6 +65,8 @@
<AmountRequired>Please enter the amount.</AmountRequired> <AmountRequired>Please enter the amount.</AmountRequired>
<Income>Income</Income> <Income>Income</Income>
<Spending>Spending</Spending> <Spending>Spending</Spending>
<LastSelected>Last Selected</LastSelected>
<Recent>Recent</Recent>
<Clothing>Clothing</Clothing> <Clothing>Clothing</Clothing>
<Food>Food</Food> <Food>Food</Food>
<Drinks>Drinks</Drinks> <Drinks>Drinks</Drinks>
@ -86,6 +101,7 @@
<Feature>Feature</Feature> <Feature>Feature</Feature>
<CategoryManage>Category Management</CategoryManage> <CategoryManage>Category Management</CategoryManage>
<Detail>Detail</Detail> <Detail>Detail</Detail>
<SaveLocation>Save Location</SaveLocation>
<AddCategory>Add Category</AddCategory> <AddCategory>Add Category</AddCategory>
<ConfirmDeleteCategory>Are you sure you want to delete the category: {0}?</ConfirmDeleteCategory> <ConfirmDeleteCategory>Are you sure you want to delete the category: {0}?</ConfirmDeleteCategory>
<SelectCategory>Select Category</SelectCategory> <SelectCategory>Select Category</SelectCategory>
@ -93,4 +109,10 @@
<NoResult>(no results)</NoResult> <NoResult>(no results)</NoResult>
<Top10>Top 10</Top10> <Top10>Top 10</Top10>
<CategoryRank>Category Ranking</CategoryRank> <CategoryRank>Category Ranking</CategoryRank>
<Diagnostic>Diagnostic</Diagnostic>
<ShareLogs>Share Logs</ShareLogs>
<ManyRecords>{0} record(s)</ManyRecords>
<SendEmail>Send Eamil</SendEmail>
<HowToShareDiagnostic>How would you like to share diagnostic logs?</HowToShareDiagnostic>
<ViewLocation>View Location</ViewLocation>
</root> </root>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<Ok>确定</Ok> <Ok>确定</Ok>
<Cancel>取消</Cancel>
<About>关于</About> <About>关于</About>
<Version>版本号</Version> <Version>版本号</Version>
<Preference>偏好</Preference> <Preference>偏好</Preference>
@ -19,7 +20,19 @@
<NoRecords>还未产生账单</NoRecords> <NoRecords>还未产生账单</NoRecords>
<TapToMemo>点此记录</TapToMemo> <TapToMemo>点此记录</TapToMemo>
<TitleDateFormat>yyyy年MM月dd日</TitleDateFormat> <TitleDateFormat>yyyy年MM月dd日</TitleDateFormat>
<TitleShortDateFormat>yyyy/MM/dd</TitleShortDateFormat>
<DateRangeFormat>MM月dd日</DateRangeFormat> <DateRangeFormat>MM月dd日</DateRangeFormat>
<To></To>
<Type>类型</Type>
<Preset>预设</Preset>
<Custom>自定义</Custom>
<Monthly>当月</Monthly>
<Today>今日</Today>
<PastMonth>一个月</PastMonth>
<PastQuarter>一个季度</PastQuarter>
<PastSixMonths>六个月</PastSixMonths>
<PastYear>一年</PastYear>
<Total>全部</Total>
<Balance>余额</Balance> <Balance>余额</Balance>
<Assets>资产</Assets> <Assets>资产</Assets>
<Liability>负债</Liability> <Liability>负债</Liability>
@ -52,6 +65,8 @@
<AmountRequired>请输入金额。</AmountRequired> <AmountRequired>请输入金额。</AmountRequired>
<Income>收入</Income> <Income>收入</Income>
<Spending>支出</Spending> <Spending>支出</Spending>
<LastSelected>最后选择</LastSelected>
<Recent>最近</Recent>
<Clothing>衣物</Clothing> <Clothing>衣物</Clothing>
<Food>食品</Food> <Food>食品</Food>
<Drinks>饮料</Drinks> <Drinks>饮料</Drinks>
@ -86,6 +101,7 @@
<Feature>功能</Feature> <Feature>功能</Feature>
<CategoryManage>分类管理</CategoryManage> <CategoryManage>分类管理</CategoryManage>
<Detail>详细</Detail> <Detail>详细</Detail>
<SaveLocation>保存位置</SaveLocation>
<AddCategory>新建分类</AddCategory> <AddCategory>新建分类</AddCategory>
<ConfirmDeleteCategory>是否确认删除该分类:{0}</ConfirmDeleteCategory> <ConfirmDeleteCategory>是否确认删除该分类:{0}</ConfirmDeleteCategory>
<SelectCategory>选择类别</SelectCategory> <SelectCategory>选择类别</SelectCategory>
@ -93,4 +109,10 @@
<NoResult>(无记录)</NoResult> <NoResult>(无记录)</NoResult>
<Top10>Top 10</Top10> <Top10>Top 10</Top10>
<CategoryRank>分类排行</CategoryRank> <CategoryRank>分类排行</CategoryRank>
<Diagnostic>诊断</Diagnostic>
<ShareLogs>发送日志</ShareLogs>
<ManyRecords>{0} 条记录</ManyRecords>
<SendEmail>发送邮件</SendEmail>
<HowToShareDiagnostic>您想以哪种方式分享诊断日志?</HowToShareDiagnostic>
<ViewLocation>查看位置</ViewLocation>
</root> </root>

View File

@ -0,0 +1,88 @@
using System;
namespace Billing
{
public static class LocationExtension
{
private const double PI = 3.1415926535897931;
private const double A = 6378245d;
private const double EE = 0.00669342162296594323;
public static (double longitude, double latitude) Wgs84ToGcj02(this (double lon, double lat) position)
{
return Transform(position);
}
public static (double longitude, double latitude) Gcj02ToWgs84(this (double lon, double lat) position)
{
(double longitude, double latitude) = Transform(position);
longitude = position.lon * 2d - longitude;
latitude = position.lat * 2d - latitude;
return (longitude, latitude);
}
public static (double longitude, double latitude) Gcj02ToBd09(this (double lon, double lat) position)
{
double x = position.lon;
double y = position.lat;
double z = Math.Sqrt(x * x + y * y) + 0.00002 * Math.Sin(y * PI);
double theta = Math.Atan2(y, x) + 0.000003 * Math.Cos(x * PI);
double longitude = z * Math.Cos(theta) + 0.0065;
double latitude = z * Math.Sin(theta) + 0.006;
return (longitude, latitude);
}
public static (double longitude, double latitude) Bd09ToGcj02(this (double lon, double lat) position)
{
double x = position.lon - 0.0065;
double y = position.lat - 0.006;
double z = Math.Sqrt(x * x + y * y) - 0.00002 * Math.Sin(y * PI);
double theta = Math.Atan2(y, x) - 0.000003 * Math.Cos(x * PI);
double longitude = z * Math.Cos(theta);
double latitude = z * Math.Sin(theta);
return (longitude, latitude);
}
private static (double longitude, double latitude) Transform(this (double lon, double lat) position)
{
if (IsOutOfChina(position.lon, position.lat))
{
return position;
}
var offsetLatitude = TransformLatitude(position.lon - 105d, position.lat - 35d);
var offsetLongitude = TransformLongitude(position.lon - 105d, position.lat - 35d);
var radiusLatitude = position.lat / 180d * PI;
var magic = 1d - EE * Math.Pow(Math.Sin(radiusLatitude), 2);
var sqrtMagic = Math.Sqrt(magic);
offsetLatitude = offsetLatitude * 180d / (A * (1d - EE) / (magic * sqrtMagic) * PI);
offsetLongitude = offsetLongitude * 180d / (A / sqrtMagic * Math.Cos(radiusLatitude) * PI);
return (position.lon + offsetLongitude, position.lat + offsetLatitude);
}
private static bool IsOutOfChina(double lon, double lat)
{
return lon < 72.004 || lon > 137.8347
|| lat < 0.8293 || lat > 55.8171;
}
private static double TransformLatitude(double x, double y)
{
var ret = -100d + 2d * x + 3d * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.Sqrt(Math.Abs(x));
ret += (20d * Math.Sin(6d * x * PI) + 20d * Math.Sin(2d * x * PI)) * 2d / 3d;
ret += (20d * Math.Sin(y * PI) + 40d * Math.Sin(y / 3d * PI)) * 2d / 3d;
ret += (160d * Math.Sin(y / 12d * PI) + 320d * Math.Sin(y * PI / 30d)) * 2d / 3d;
return ret;
}
private static double TransformLongitude(double x, double y)
{
var ret = 300d + x + 2d * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.Sqrt(Math.Abs(x));
ret += (20d * Math.Sin(6d * x * PI) + 20d * Math.Sin(2d * x * PI)) * 2d / 3d;
ret += (20d * Math.Sin(x * PI) + 40d * Math.Sin(x / 3d * PI)) * 2d / 3d;
ret += (150d * Math.Sin(x / 12d * PI) + 300d * Math.Sin(x / 30d * PI)) * 2d / 3d;
return ret;
}
}
}

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<Shell xmlns="http://xamarin.com/schemas/2014/forms" <Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Billing"
xmlns:v="clr-namespace:Billing.Views" xmlns:v="clr-namespace:Billing.Views"
xmlns:r="clr-namespace:Billing.Languages" xmlns:r="clr-namespace:Billing.Languages"
x:Class="Billing.MainShell" x:Class="Billing.MainShell"
@ -9,11 +10,14 @@
TitleColor="{DynamicResource PrimaryColor}" TitleColor="{DynamicResource PrimaryColor}"
Shell.NavBarHasShadow="True"> Shell.NavBarHasShadow="True">
<FlyoutItem>
<ShellContent ContentTemplate="{DataTemplate local:SplashPage}" Route="Splash"/>
</FlyoutItem>
<TabBar> <TabBar>
<ShellContent ContentTemplate="{DataTemplate v:AccountPage}" Route="Accounts" Title="{r:Text Accounts}" Icon="wallet.png"/> <ShellContent ContentTemplate="{DataTemplate v:AccountPage}" Route="Accounts" Title="{r:Text Accounts}" Icon="wallet.png"/>
<ShellContent ContentTemplate="{DataTemplate v:BillPage}" Route="Bills" Title="{r:Text Bills}" Icon="bill.png"/> <ShellContent ContentTemplate="{DataTemplate v:BillPage}" Route="Bills" Title="{r:Text Bills}" Icon="bill.png"/>
<ShellContent ContentTemplate="{DataTemplate v:RankPage}" Route="Ranks" Title="{r:Text Report}" Icon="rank.png"/> <ShellContent ContentTemplate="{DataTemplate v:RankPage}" Route="Ranks" Title="{r:Text Report}" Icon="rank.png"/>
<ShellContent ContentTemplate="{DataTemplate v:SettingPage}" Route="Settings" Title="{r:Text Settings}" Icon="settings.png"/> <ShellContent ContentTemplate="{DataTemplate v:SettingPage}" Route="Settings" Title="{r:Text Settings}" Icon="settings.png"/>
</TabBar> </TabBar>
</Shell> </Shell>

View File

@ -1,3 +1,4 @@
using Billing.Views;
using Xamarin.Forms; using Xamarin.Forms;
namespace Billing namespace Billing
@ -6,6 +7,8 @@ namespace Billing
{ {
public MainShell() public MainShell()
{ {
Routing.RegisterRoute("Bills/Details", typeof(AddBillPage));
InitializeComponent(); InitializeComponent();
} }
} }

View File

@ -1,9 +1,15 @@
using System.Xml.Linq; using SQLite;
namespace Billing.Models namespace Billing.Models
{ {
public class Account : BaseModel public class Account : IIdItem
{ {
private const string ICON_DEFAULT = "ic_default";
private static Account empty;
public static Account Empty => empty ??= new() { Id = -1 };
[PrimaryKey, AutoIncrement]
public int Id { get; set; } public int Id { get; set; }
public string Icon { get; set; } = ICON_DEFAULT; public string Icon { get; set; } = ICON_DEFAULT;
public AccountCategory Category { get; set; } public AccountCategory Category { get; set; }
@ -11,28 +17,6 @@ namespace Billing.Models
public decimal Initial { get; set; } public decimal Initial { get; set; }
public decimal Balance { get; set; } public decimal Balance { get; set; }
public string Memo { 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 public enum AccountCategory

View File

@ -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<T>(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<XElement> 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<T>(XElement node, string subname, Func<XElement, T> func)
{
var ele = node.Elements().FirstOrDefault(e => string.Equals(e.Name.ToString(), subname, StringComparison.OrdinalIgnoreCase));
return func(ele);
}
private static T ReadObject<T>(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<T>(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<T>(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<T>(XElement parent, string name, T value) where T : IFormattable => WriteString(parent, name, ToString(value));
protected static XElement WriteObject<T>(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<T>(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<T>(XElement node, string subname, T def = default) where T : IModel, new() => ReadSubnode(node, subname, e => e == null ? def : ReadObject<T>(e));
protected static T[] ReadArray<T>(XElement node, string subname, T[] def = null) where T : IModel, new() => ReadSubnode(node, subname, e => e == null ? def : ReadArray<T>(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;
}
}
}
}

View File

@ -1,10 +1,11 @@
using System; using System;
using System.Xml.Linq; using SQLite;
namespace Billing.Models namespace Billing.Models
{ {
public class Bill : BaseModel public class Bill : IIdItem
{ {
[PrimaryKey, AutoIncrement]
public int Id { get; set; } public int Id { get; set; }
public decimal Amount { get; set; } public decimal Amount { get; set; }
public string Name { get; set; } public string Name { get; set; }
@ -13,29 +14,10 @@ namespace Billing.Models
public string Store { get; set; } public string Store { get; set; }
public DateTime CreateTime { get; set; } public DateTime CreateTime { get; set; }
public string Note { get; set; } public string Note { get; set; }
public double? Latitude { get; set; }
public override void OnXmlDeserialize(XElement node) public double? Longitude { get; set; }
{ public double? Altitude { get; set; }
Id = Read(node, nameof(Id), 0); public double? Accuracy { get; set; }
Amount = Read(node, nameof(Amount), 0m); public bool? IsGps { get; set; }
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);
}
} }
} }

View File

@ -1,50 +1,25 @@
using Xamarin.Forms; using SQLite;
using System.Xml.Linq; using System;
namespace Billing.Models namespace Billing.Models
{ {
public class Category : BaseModel public class Category : IIdItem
{ {
private const string ICON_DEFAULT = "ic_default";
private const long TRANSPARENT_COLOR = 0x00ffffffL;
private static Category empty;
public static Category Empty => empty ??= new() { Id = -1 };
[PrimaryKey, AutoIncrement]
public int Id { get; set; } public int Id { get; set; }
public CategoryType Type { 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 Color TintColor { get; set; } = Color.Transparent; public long TintColor { get; set; } = TRANSPARENT_COLOR;
public int? ParentId { get; set; } public int? ParentId { get; set; }
public DateTime? LastUsed { get; set; }
public override void OnXmlDeserialize(XElement node) public int? LastAccountId { get; set; }
{
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 public enum CategoryType

View File

@ -0,0 +1,7 @@
namespace Billing.Models
{
public interface IIdItem
{
public int Id { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using SQLite;
namespace Billing.Models
{
public class Logs : IIdItem
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public DateTime LogTime { get; set; }
public string Category { get; set; }
public string Detail { get; set; }
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ui:BillingPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:ui="clr-namespace:Billing.UI"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Billing.SplashPage"
Shell.NavBarIsVisible="False"
Shell.TabBarIsVisible="False">
<!--<Label Text="Loading..." HorizontalOptions="Center" VerticalOptions="Center"/>-->
</ui:BillingPage>

View File

@ -0,0 +1,23 @@
using Billing.UI;
using Xamarin.Forms;
namespace Billing
{
public partial class SplashPage : BillingPage
{
public SplashPage()
{
InitializeComponent();
}
protected override async void OnLoaded()
{
if (!string.IsNullOrEmpty(App.MainRoute))
{
await App.InitializeData();
await Shell.Current.GoToAsync(App.MainRoute);
}
}
}
}

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Billing.Models; using Billing.Models;
using Billing.UI; using SQLite;
using Xamarin.Essentials; using Xamarin.Essentials;
using Resource = Billing.Languages.Resource; using Resource = Billing.Languages.Resource;
@ -14,130 +14,309 @@ namespace Billing.Store
public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal); public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
public static readonly string CacheFolder = FileSystem.CacheDirectory; public static readonly string CacheFolder = FileSystem.CacheDirectory;
private const string accountFile = "accounts.xml"; public static string DatabasePath => Path.Combine(PersonalFolder, dbfile);
private const string billFile = "bills.xml";
private const string categoryFile = "categories.xml";
private static StoreHelper instance; #region Sqlite3
private static StoreHelper Instance => instance ??= new StoreHelper(); private const string dbfile = ".master.db3";
private static SQLiteAsyncConnection database;
#endregion
public static List<Account> GetAccounts() => Instance.GetAccountsInternal(); public static async Task<bool> ReloadDatabase(string file)
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)); var path = DatabasePath;
} if (string.Equals(file, path, StringComparison.OrdinalIgnoreCase))
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()
{
var list = GetList<Category>(Path.Combine(PersonalFolder, categoryFile));
if (list == null || list.Count == 0)
{ {
list = new List<Category> return false;
{
// sample categories
new() { Id = 1, Name = Resource.Clothing, Icon = "clothes" },
new() { Id = 2, Name = Resource.Food, Icon = "food" },
new() { Id = 4, Name = Resource.Daily, Icon = "daily" },
new() { Id = 5, Name = Resource.Trans, Icon = "trans" },
new() { Id = 6, Name = Resource.Entertainment, Icon = "face" },
new() { Id = 7, Name = Resource.Learn, Icon = "learn" },
new() { Id = 8, Name = Resource.Medical, Icon = "medical" },
new() { Id = 9, Name = Resource.OtherSpending, Icon = "plus" },
new() { Id = 10, Type = CategoryType.Income, Name = Resource.Earnings, Icon = "#brand#btc" },
new() { Id = 20, Type = CategoryType.Income, Name = Resource.OtherIncome, Icon = "plus" },
// sub-categories
new() { Id = 100, ParentId = 1, Name = Resource.Jewellery, Icon = "gem" },
new() { Id = 101, ParentId = 1, Name = Resource.Cosmetics, Icon = "makeup" },
new() { Id = 102, ParentId = 2, Name = Resource.Brunch, Icon = "brunch" },
new() { Id = 103, ParentId = 2, Name = Resource.Dinner, Icon = "dinner" },
new() { Id = 104, ParentId = 2, Name = Resource.Drinks, Icon = "drink" },
new() { Id = 105, ParentId = 2, Name = Resource.Fruit, Icon = "fruit" },
new() { Id = 106, ParentId = 4, Name = Resource.UtilityBill, Icon = "bill" },
new() { Id = 107, ParentId = 4, Name = Resource.PropertyFee, Icon = "fee" },
new() { Id = 108, ParentId = 4, Name = Resource.Rent, Icon = "rent" },
new() { Id = 109, ParentId = 4, Name = Resource.Maintenance, Icon = "maintenance" },
new() { Id = 110, ParentId = 5, Name = Resource.LightRail, Icon = "rail" },
new() { Id = 111, ParentId = 5, Name = Resource.Taxi, Icon = "taxi" },
new() { Id = 112, ParentId = 6, Name = Resource.Fitness, Icon = "fitness" },
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;
}
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 try
{ {
using var stream = File.Open(filename, FileMode.Create); if (database != null)
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); await database.CloseAsync();
var list = ModelExtensionHelper.FromStream<T>(stream);
return list;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Helper.Error("file.read", $"failed to read file: {file}, error: {ex.Message}"); Helper.Error("database.close", ex);
return false;
}
try
{
File.Copy(file, path, true);
File.Delete(file);
}
catch (Exception ex)
{
Helper.Error("file.import", ex);
}
try
{
database = new SQLiteAsyncConnection(path,
SQLiteOpenFlags.ReadWrite |
SQLiteOpenFlags.Create |
SQLiteOpenFlags.SharedCache);
return true;
}
catch (Exception ex)
{
Helper.Error("database.connect", ex);
return false;
}
}
public static readonly AsyncLazy<StoreHelper> Instance = new(async () =>
{
var instance = new StoreHelper();
try
{
await database.CreateTablesAsync<Category, Account, Bill, Logs>();
} catch (Exception ex)
{
Helper.Error("database.init.table", ex);
}
try
{
var count = await database.ExecuteScalarAsync<int>("SELECT COUNT(Id) FROM [Account]");
if (count <= 0)
{
await database.InsertAsync(new Account { Name = Resource.Cash, Icon = "wallet" });
}
}
catch (Exception ex)
{
Helper.Error("database.init.account", ex);
}
try
{
var count = await database.ExecuteScalarAsync<int>("SELECT COUNT(Id) FROM [Category]");
if (count <= 0)
{
// init categories
await database.InsertAllAsync(new List<Category>
{
// sample categories
new() { Name = Resource.Clothing, Icon = "clothes" },
new() { Name = Resource.Food, Icon = "food" },
new() { Name = Resource.Daily, Icon = "daily" },
new() { Name = Resource.Trans, Icon = "trans" },
new() { Name = Resource.Entertainment, Icon = "face" },
new() { Name = Resource.Learn, Icon = "learn" },
new() { Name = Resource.Medical, Icon = "medical" },
new() { Name = Resource.OtherSpending, Icon = "plus" },
new() { Type = CategoryType.Income, Name = Resource.Earnings, Icon = "#brand#btc" },
new() { Type = CategoryType.Income, Name = Resource.OtherIncome, Icon = "plus" },
// sub-categories
new() { ParentId = 1, Name = Resource.Jewellery, Icon = "gem" },
new() { ParentId = 1, Name = Resource.Cosmetics, Icon = "makeup" },
new() { ParentId = 2, Name = Resource.Brunch, Icon = "brunch" },
new() { ParentId = 2, Name = Resource.Dinner, Icon = "dinner" },
new() { ParentId = 2, Name = Resource.Drinks, Icon = "drink" },
new() { ParentId = 2, Name = Resource.Fruit, Icon = "fruit" },
new() { ParentId = 3, Name = Resource.UtilityBill, Icon = "bill" },
new() { ParentId = 3, Name = Resource.PropertyFee, Icon = "fee" },
new() { ParentId = 3, Name = Resource.Rent, Icon = "rent" },
new() { ParentId = 3, Name = Resource.Maintenance, Icon = "maintenance" },
new() { ParentId = 4, Name = Resource.LightRail, Icon = "rail" },
new() { ParentId = 4, Name = Resource.Taxi, Icon = "taxi" },
new() { ParentId = 5, Name = Resource.Fitness, Icon = "fitness" },
new() { ParentId = 5, Name = Resource.Party, Icon = "party" },
new() { ParentId = 9, Type = CategoryType.Income, Name = Resource.Salary, Icon = "#brand#buffer" },
new() { ParentId = 9, Type = CategoryType.Income, Name = Resource.Bonus, Icon = "dollar" },
});
}
}
catch (Exception ex)
{
Helper.Error("database.init.category", ex);
}
return instance;
});
public static async Task<int> SaveAccountItemAsync(Account account)
{
var instance = await Instance;
return await instance.SaveItemAsync(account);
}
public static async Task<int> DeleteAccountItemAsync(Account account)
{
var instance = await Instance;
return await instance.DeleteItemAsync(account);
}
public static async Task<int> SaveBillItemAsync(Bill bill)
{
var instance = await Instance;
return await instance.SaveItemAsync(bill);
}
public static async Task<int> DeleteBillItemAsync(Bill bill)
{
var instance = await Instance;
return await instance.DeleteItemAsync(bill);
}
public static async Task<int> SaveCategoryItemAsync(Category category)
{
var instance = await Instance;
return await instance.SaveItemAsync(category);
}
public static async Task<int> DeleteCategoryItemAsync(Category category)
{
var instance = await Instance;
return await instance.DeleteItemAsync(category);
}
public static async Task<int> GetLogsCount()
{
await Instance;
try
{
return await database.ExecuteScalarAsync<int>("SELECT COUNT(Id) FROM [Logs]");
}
catch (SQLiteException)
{
await database.CreateTableAsync<Logs>();
return 0;
}
}
public static async Task<int> SaveLogItemAsync(Logs log)
{
var instance = await Instance;
return await instance.SaveItemAsync(log);
}
public static string GetLogFile()
{
return Path.Combine(CacheFolder, "logs.csv");
}
public static async Task<string> ExportLogs()
{
try
{
var instance = await Instance;
var logs = await instance.GetListAsync<Logs>();
var file = GetLogFile();
using var writer = new StreamWriter(File.Open(file, FileMode.Create, FileAccess.Write));
writer.WriteLine("Id,DateTime,Category,Detail");
foreach (var log in logs)
{
var category = log.Category?.Replace("\n", " \\n ");
var detail = log.Detail?.Replace("\n", " \\n ");
writer.WriteLine($"{log.Id},{log.LogTime},{category},{detail}");
}
writer.Flush();
await database.RunInTransactionAsync(conn =>
{
conn.Execute("DELETE FROM [Logs]");
conn.Execute("DELETE FROM [sqlite_sequence] WHERE [name] = 'Logs'");
});
return file;
}
catch
{
return null;
}
}
private StoreHelper()
{
if (database == null)
{
try
{
database = new SQLiteAsyncConnection(DatabasePath,
SQLiteOpenFlags.ReadWrite |
SQLiteOpenFlags.Create |
SQLiteOpenFlags.SharedCache);
}
catch (Exception ex)
{
Helper.Error("database.ctor.connect", ex);
}
}
}
public Task<List<T>> GetListAsync<T>(string query, params object[] args) where T : new()
{
try
{
return database.QueryAsync<T>(query, args);
}
catch (Exception ex)
{
Helper.Error("db.read", $"failed to read db, query: {string.Format(query, args)}, error: {ex.Message}");
} }
return default; return default;
} }
public Task<T> GetItemAsync<T>(int id) where T : IIdItem, new()
{
try
{
var source = new TaskCompletionSource<T>();
Task.Run(async () =>
{
var item = await database.FindWithQueryAsync<T>($"SELECT * FROM [{typeof(T).Name}] WHERE [Id] = ? LIMIT 1", id);
source.SetResult(item);
});
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
public Task<List<T>> GetListAsync<T>() where T : new()
{
try
{
return database.Table<T>().ToListAsync();
}
catch (Exception ex)
{
Helper.Error("db.read", $"failed to read db, error: {ex.Message}");
}
return default;
}
public Task<int> SaveItemAsync<T>(T item) where T : IIdItem
{
try
{
if (item.Id > 0)
{
return database.UpdateAsync(item);
}
else
{
return database.InsertAsync(item);
}
}
catch (Exception ex)
{
Helper.Error("db.write", $"failed to insert/update item, table: {typeof(T)}, id: {item.Id}, item: {item}, error: {ex.Message}");
}
return Task.FromResult(0);
}
public Task<int> DeleteItemAsync<T>(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(0);
}
#endregion #endregion
} }
} }

View File

@ -5,6 +5,8 @@ namespace Billing.Themes
{ {
public abstract class BaseTheme : ResourceDictionary public abstract class BaseTheme : ResourceDictionary
{ {
public const double DefaultFontSize = 15.0;
public static Color CurrentPrimaryColor => (Color)Application.Current.Resources[PrimaryColor]; public static Color CurrentPrimaryColor => (Color)Application.Current.Resources[PrimaryColor];
public static Color CurrentTextColor => (Color)Application.Current.Resources[TextColor]; public static Color CurrentTextColor => (Color)Application.Current.Resources[TextColor];
public static Color CurrentSecondaryTextColor => (Color)Application.Current.Resources[SecondaryTextColor]; public static Color CurrentSecondaryTextColor => (Color)Application.Current.Resources[SecondaryTextColor];
@ -31,9 +33,8 @@ namespace Billing.Themes
protected void InitResources() protected void InitResources()
{ {
var regularFontFamily = Definition.GetRegularFontFamily(); Add(FontSemiBold, Definition.SemiBoldFontFamily);
Add(FontSemiBold, Definition.GetSemiBoldFontFamily()); Add(FontBold, Definition.BoldFontFamily);
Add(FontBold, Definition.GetBoldFontFamily());
Add(PrimaryColor, PrimaryMauiColor); Add(PrimaryColor, PrimaryMauiColor);
Add(SecondaryColor, SecondaryMauiColor); Add(SecondaryColor, SecondaryMauiColor);
@ -43,41 +44,49 @@ namespace Billing.Themes
{ {
Setters = Setters =
{ {
new Setter { Property = Label.FontSizeProperty, Value = Device.GetNamedSize(NamedSize.Small, typeof(Label)) }, new Setter { Property = Label.FontSizeProperty, Value = DefaultFontSize },
new Setter { Property = Label.TextColorProperty, Value = PrimaryMauiColor }, new Setter { Property = Label.TextColorProperty, Value = PrimaryMauiColor },
new Setter { Property = Label.FontFamilyProperty, Value = regularFontFamily } new Setter { Property = Label.FontFamilyProperty, Value = Definition.RegularFontFamily }
} }
}); });
Add(new Style(typeof(OptionEntry)) Add(new Style(typeof(OptionEntry))
{ {
Setters = Setters =
{ {
new Setter { Property = Entry.FontSizeProperty, Value = Device.GetNamedSize(NamedSize.Small, typeof(Entry)) }, new Setter { Property = Entry.FontSizeProperty, Value = DefaultFontSize },
new Setter { Property = Entry.FontFamilyProperty, Value = regularFontFamily } new Setter { Property = Entry.FontFamilyProperty, Value = Definition.RegularFontFamily }
} }
}); });
Add(new Style(typeof(OptionEditor)) Add(new Style(typeof(OptionEditor))
{ {
Setters = Setters =
{ {
new Setter { Property = Editor.FontSizeProperty, Value = Device.GetNamedSize(NamedSize.Small, typeof(Editor)) }, new Setter { Property = Editor.FontSizeProperty, Value = DefaultFontSize },
new Setter { Property = Editor.FontFamilyProperty, Value = regularFontFamily } new Setter { Property = Editor.FontFamilyProperty, Value = Definition.RegularFontFamily }
} }
}); });
Add(new Style(typeof(OptionDatePicker)) Add(new Style(typeof(OptionDatePicker))
{ {
Setters = Setters =
{ {
new Setter { Property = DatePicker.FontSizeProperty, Value = Device.GetNamedSize(NamedSize.Small, typeof(DatePicker)) }, new Setter { Property = DatePicker.FontSizeProperty, Value = DefaultFontSize },
new Setter { Property = DatePicker.FontFamilyProperty, Value = regularFontFamily } new Setter { Property = DatePicker.FontFamilyProperty, Value = Definition.RegularFontFamily }
} }
}); });
Add(new Style(typeof(OptionTimePicker)) Add(new Style(typeof(OptionTimePicker))
{ {
Setters = Setters =
{ {
new Setter { Property = TimePicker.FontSizeProperty, Value = Device.GetNamedSize(NamedSize.Small, typeof(TimePicker)) }, new Setter { Property = TimePicker.FontSizeProperty, Value = DefaultFontSize },
new Setter { Property = TimePicker.FontFamilyProperty, Value = regularFontFamily } new Setter { Property = TimePicker.FontFamilyProperty, Value = Definition.RegularFontFamily }
}
});
Add(new Style(typeof(OptionPicker))
{
Setters =
{
new Setter { Property = Picker.FontSizeProperty, Value = DefaultFontSize },
new Setter { Property = Picker.FontFamilyProperty, Value = Definition.RegularFontFamily }
} }
}); });
Add(new Style(typeof(TintImage)) Add(new Style(typeof(TintImage))

View File

@ -230,7 +230,7 @@ namespace Billing.UI
{ {
public static readonly BindableProperty DateProperty = Helper.Create<DateTime, BillingDay>(nameof(Date), propertyChanged: OnDatePropertyChanged); public static readonly BindableProperty DateProperty = Helper.Create<DateTime, BillingDay>(nameof(Date), propertyChanged: OnDatePropertyChanged);
public static readonly BindableProperty TextProperty = Helper.Create<string, BillingDay>(nameof(Text)); public static readonly BindableProperty TextProperty = Helper.Create<string, BillingDay>(nameof(Text));
public static readonly BindableProperty FontFamilyProperty = Helper.Create<string, BillingDay>(nameof(FontFamily), defaultValue: Definition.GetRegularFontFamily()); public static readonly BindableProperty FontFamilyProperty = Helper.Create<string, BillingDay>(nameof(FontFamily), defaultValue: Definition.RegularFontFamily);
public static readonly BindableProperty IsSelectedProperty = Helper.Create<bool, BillingDay>(nameof(IsSelected), defaultValue: false); public static readonly BindableProperty IsSelectedProperty = Helper.Create<bool, BillingDay>(nameof(IsSelected), defaultValue: false);
public static readonly BindableProperty OpacityProperty = Helper.Create<double, BillingDay>(nameof(Opacity), defaultValue: 1.0); public static readonly BindableProperty OpacityProperty = Helper.Create<double, BillingDay>(nameof(Opacity), defaultValue: 1.0);
public static readonly BindableProperty TextOpacityProperty = Helper.Create<double, BillingDay>(nameof(TextOpacity), defaultValue: 1.0); public static readonly BindableProperty TextOpacityProperty = Helper.Create<double, BillingDay>(nameof(TextOpacity), defaultValue: 1.0);
@ -273,11 +273,11 @@ namespace Billing.UI
if (Helper.IsSameDay(date, selected)) if (Helper.IsSameDay(date, selected))
{ {
IsSelected = true; IsSelected = true;
SetValue(FontFamilyProperty, Definition.GetBoldFontFamily()); SetValue(FontFamilyProperty, Definition.BoldFontFamily);
} }
else else
{ {
SetValue(FontFamilyProperty, Definition.GetRegularFontFamily()); SetValue(FontFamilyProperty, Definition.RegularFontFamily);
} }
if (date.Year == selected.Year && date.Month == selected.Month) if (date.Year == selected.Year && date.Month == selected.Month)
{ {

View File

@ -7,6 +7,9 @@ namespace Billing.UI
public abstract class BillingPage : ContentPage public abstract class BillingPage : ContentPage
{ {
public event EventHandler Loaded; public event EventHandler Loaded;
public event EventHandler Refreshed;
private bool loaded;
public BillingPage() public BillingPage()
{ {
@ -14,9 +17,28 @@ namespace Billing.UI
Shell.SetTabBarIsVisible(this, false); Shell.SetTabBarIsVisible(this, false);
} }
public virtual void OnLoaded() protected virtual void OnLoaded()
{ {
Loaded?.Invoke(this, EventArgs.Empty); Loaded?.Invoke(this, EventArgs.Empty);
} }
protected virtual void OnRefresh()
{
Refreshed?.Invoke(this, EventArgs.Empty);
}
public void TriggerLoad()
{
if (!loaded)
{
loaded = true;
OnLoaded();
}
}
public void TriggerRefresh()
{
OnRefresh();
}
} }
} }

View File

@ -8,15 +8,19 @@ namespace Billing.UI
{ {
public class ColorPicker : SKCanvasView public class ColorPicker : SKCanvasView
{ {
public static readonly BindableProperty ColorProperty = BindableProperty.Create(nameof(Color), typeof(Color), typeof(ColorPicker)); public static readonly BindableProperty ColorProperty = Helper.Create<Color, ColorPicker>(nameof(Color));
public static readonly BindableProperty CommandProperty = Helper.Create<Command, ColorPicker>(nameof(Command));
public Color Color public Color Color
{ {
get => (Color)GetValue(ColorProperty); get => (Color)GetValue(ColorProperty);
set => SetValue(ColorProperty, value); set => SetValue(ColorProperty, value);
} }
public Command Command
public event EventHandler<Color> ColorChanged; {
get => (Command)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
private SKPoint? lastTouch; private SKPoint? lastTouch;
@ -116,7 +120,7 @@ namespace Billing.UI
var color = touchColor.ToFormsColor(); var color = touchColor.ToFormsColor();
Color = color; Color = color;
ColorChanged?.Invoke(this, color); Command?.Execute(color);
} }
} }
@ -126,8 +130,8 @@ namespace Billing.UI
lastTouch = e.Location; lastTouch = e.Location;
var size = CanvasSize; var size = CanvasSize;
if ((e.Location.X > 0 && e.Location.X < size.Width) && if (e.Location.X > 0 && e.Location.X < size.Width &&
(e.Location.Y > 0 && e.Location.Y < size.Height)) e.Location.Y > 0 && e.Location.Y < size.Height)
{ {
e.Handled = true; e.Handled = true;
InvalidateSurface(); InvalidateSurface();

View File

@ -205,13 +205,13 @@ namespace Billing.UI
{ {
if (!int.TryParse(key, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int i)) 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); glyph = char.ConvertFromUtf32(i);
} }
return new FontImageSource return new FontImageSource
{ {
FontFamily = Definition.GetBrandsFontFamily(), FontFamily = Definition.BrandsFontFamily,
Size = 20, Size = 20,
Glyph = glyph, Glyph = glyph,
Color = Color.Black Color = Color.Black
@ -244,4 +244,23 @@ namespace Billing.UI
throw new NotImplementedException(); throw new NotImplementedException();
} }
} }
public class TintColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is long l)
{
return l.IsTransparent() ?
BaseTheme.CurrentPrimaryColor :
l.ToColor();
}
return Color.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
} }

View File

@ -34,8 +34,8 @@ namespace Billing.UI
public class LongPressGrid : Grid public class LongPressGrid : Grid
{ {
public static readonly BindableProperty LongCommandProperty = BindableProperty.Create(nameof(LongCommand), typeof(Command), typeof(LongPressGrid)); public static readonly BindableProperty LongCommandProperty = Helper.Create<Command, LongPressGrid>(nameof(LongCommand));
public static readonly BindableProperty LongCommandParameterProperty = BindableProperty.Create(nameof(LongCommandParameter), typeof(object), typeof(LongPressGrid)); public static readonly BindableProperty LongCommandParameterProperty = Helper.Create<object, LongPressGrid>(nameof(LongCommandParameter));
public Command LongCommand public Command LongCommand
{ {

View File

@ -1,22 +1,16 @@
using System; using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Linq;
using Billing.Languages; using Billing.Languages;
using Billing.Models;
using Xamarin.Forms; using Xamarin.Forms;
namespace Billing.UI namespace Billing.UI
{ {
public static partial class Definition public static partial class Definition
{ {
public static string PrimaryColorKey = "PrimaryColor"; public const string SaveLocationKey = "SaveLocationKey";
public static partial (string main, long build) GetVersion(); public const string PrimaryColorKey = "PrimaryColor";
public static partial string GetRegularFontFamily(); public const string DefaultIcon = "ic_default";
public static partial string GetSemiBoldFontFamily(); public const long TransparentColor = 0x00ffffffL;
public static partial string GetBoldFontFamily();
public static partial string GetBrandsFontFamily();
} }
public static class ExtensionHelper public static class ExtensionHelper
@ -92,50 +86,39 @@ namespace Billing.UI
var result = await page.DisplayActionSheet(message, Resource.No, yes); var result = await page.DisplayActionSheet(message, Resource.No, yes);
return result == yes; return result == yes;
} }
}
public static class ModelExtensionHelper public static DateTime LastMoment(this DateTime date)
{
public static List<T> FromStream<T>(Stream stream) where T : IModel, new()
{ {
XDocument doc = XDocument.Load(stream); // add 23:59:59.999...
var root = doc.Root; return date.AddTicks(863999999999);
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 public static bool IsTransparent(this long color)
{ {
XElement root = new("root"); return (color & 0xff000000L) == 0x00000000L;
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); public static Color ToColor(this long color)
doc.Save(stream, SaveOptions.DisableFormatting); {
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);
}
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;
} }
} }

View File

@ -5,12 +5,12 @@ namespace Billing.UI
{ {
public class GroupStackLayout : Layout<View> public class GroupStackLayout : Layout<View>
{ {
public static readonly BindableProperty GroupHeaderTemplateProperty = BindableProperty.Create(nameof(GroupHeaderTemplate), typeof(DataTemplate), typeof(GroupStackLayout)); public static readonly BindableProperty GroupHeaderTemplateProperty = Helper.Create<DataTemplate, GroupStackLayout>(nameof(GroupHeaderTemplate));
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(GroupStackLayout)); public static readonly BindableProperty ItemTemplateProperty = Helper.Create<DataTemplate, GroupStackLayout>(nameof(ItemTemplate));
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IList), typeof(GroupStackLayout), propertyChanged: OnItemsSourcePropertyChanged); public static readonly BindableProperty ItemsSourceProperty = Helper.Create<IList, GroupStackLayout>(nameof(ItemsSource), propertyChanged: OnItemsSourcePropertyChanged);
public static readonly BindableProperty SpacingProperty = BindableProperty.Create(nameof(Spacing), typeof(double), typeof(GroupStackLayout), defaultValue: 4d); public static readonly BindableProperty SpacingProperty = Helper.Create<double, GroupStackLayout>(nameof(Spacing), defaultValue: 4d);
public static readonly BindableProperty RowHeightProperty = BindableProperty.Create(nameof(RowHeight), typeof(double), typeof(GroupStackLayout), defaultValue: 32d); public static readonly BindableProperty RowHeightProperty = Helper.Create<double, GroupStackLayout>(nameof(RowHeight), defaultValue: 32d);
public static readonly BindableProperty GroupHeightProperty = BindableProperty.Create(nameof(GroupHeight), typeof(double), typeof(GroupStackLayout), defaultValue: 24d); public static readonly BindableProperty GroupHeightProperty = Helper.Create<double, GroupStackLayout>(nameof(GroupHeight), defaultValue: 24d);
public DataTemplate GroupHeaderTemplate public DataTemplate GroupHeaderTemplate
{ {
@ -43,17 +43,16 @@ namespace Billing.UI
set => SetValue(GroupHeightProperty, value); set => SetValue(GroupHeightProperty, value);
} }
private static void OnItemsSourcePropertyChanged(BindableObject obj, object old, object @new) private static void OnItemsSourcePropertyChanged(GroupStackLayout stack, IList old, IList list)
{ {
var stack = (GroupStackLayout)obj;
stack.lastWidth = -1; stack.lastWidth = -1;
if (@new == null) if (list == null)
{ {
//stack.cachedLayout.Clear(); //stack.cachedLayout.Clear();
stack.Children.Clear(); stack.Children.Clear();
stack.InvalidateLayout(); stack.InvalidateLayout();
} }
else if (@new is IList list) else
{ {
stack.freezed = true; stack.freezed = true;
//stack.cachedLayout.Clear(); //stack.cachedLayout.Clear();

View File

@ -4,16 +4,32 @@ using Xamarin.Forms;
namespace Billing.UI namespace Billing.UI
{ {
public enum BorderStyle
{
None = 0,
RoundedRect
}
public class OptionEntry : Entry { } public class OptionEntry : Entry { }
public class OptionEditor : Editor { } public class OptionEditor : Editor { }
public class OptionPicker : Picker
{
public static readonly BindableProperty BorderStyleProperty = Helper.Create<BorderStyle, OptionPicker>(nameof(BorderStyle));
public BorderStyle BorderStyle
{
get => (BorderStyle)GetValue(BorderStyleProperty);
set => SetValue(BorderStyleProperty, value);
}
}
public class OptionDatePicker : DatePicker { } public class OptionDatePicker : DatePicker { }
public class OptionTimePicker : TimePicker { } public class OptionTimePicker : TimePicker { }
public abstract class OptionCell : ViewCell public abstract class OptionCell : ViewCell
{ {
public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(OptionCell)); public static readonly BindableProperty TitleProperty = Helper.Create<string, OptionCell>(nameof(Title));
public static readonly BindableProperty BackgroundColorProperty = BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(OptionCell)); public static readonly BindableProperty BackgroundColorProperty = Helper.Create<Color, OptionCell>(nameof(BackgroundColor));
public static readonly BindableProperty IconProperty = BindableProperty.Create(nameof(Icon), typeof(ImageSource), typeof(OptionCell)); public static readonly BindableProperty IconProperty = Helper.Create<ImageSource, OptionCell>(nameof(Icon));
public string Title public string Title
{ {
@ -124,7 +140,7 @@ namespace Billing.UI
public class OptionTextCell : OptionCell public class OptionTextCell : OptionCell
{ {
public static readonly BindableProperty DetailProperty = BindableProperty.Create(nameof(Detail), typeof(string), typeof(OptionTextCell)); public static readonly BindableProperty DetailProperty = Helper.Create<string, OptionTextCell>(nameof(Detail));
public string Detail public string Detail
{ {
@ -143,8 +159,8 @@ namespace Billing.UI
public class OptionSelectCell : OptionTextCell public class OptionSelectCell : OptionTextCell
{ {
public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(Command), typeof(OptionSelectCell)); public static readonly BindableProperty CommandProperty = Helper.Create<Command, OptionSelectCell>(nameof(Command));
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(OptionSelectCell)); public static readonly BindableProperty CommandParameterProperty = Helper.Create<object, OptionSelectCell>(nameof(CommandParameter));
public Command Command public Command Command
{ {
@ -189,8 +205,8 @@ namespace Billing.UI
public class OptionImageCell : OptionSelectCell public class OptionImageCell : OptionSelectCell
{ {
public static readonly BindableProperty ImageSourceProperty = BindableProperty.Create(nameof(ImageSource), typeof(ImageSource), typeof(OptionImageCell)); public static readonly BindableProperty ImageSourceProperty = Helper.Create<ImageSource, OptionImageCell>(nameof(ImageSource));
public static readonly BindableProperty TintColorProperty = BindableProperty.Create(nameof(TintColor), typeof(Color?), typeof(OptionImageCell)); public static readonly BindableProperty TintColorProperty = Helper.Create<Color?, OptionImageCell>(nameof(TintColor));
[TypeConverter(typeof(ImageSourceConverter))] [TypeConverter(typeof(ImageSourceConverter))]
public ImageSource ImageSource public ImageSource ImageSource
@ -221,6 +237,13 @@ namespace Billing.UI
.Binding(Image.SourceProperty, nameof(ImageSource)) .Binding(Image.SourceProperty, nameof(ImageSource))
.Binding(TintHelper.TintColorProperty, nameof(TintColor)), .Binding(TintHelper.TintColorProperty, nameof(TintColor)),
new Label
{
VerticalOptions = LayoutOptions.Center
}
.Binding(Label.TextProperty, nameof(Detail))
.DynamicResource(Label.TextColorProperty, BaseTheme.SecondaryTextColor),
new TintImage new TintImage
{ {
HeightRequest = 20, HeightRequest = 20,
@ -240,7 +263,7 @@ namespace Billing.UI
public class OptionSwitchCell : OptionCell public class OptionSwitchCell : OptionCell
{ {
public static readonly BindableProperty IsToggledProperty = BindableProperty.Create(nameof(IsToggled), typeof(bool), typeof(OptionSwitchCell)); public static readonly BindableProperty IsToggledProperty = Helper.Create<bool, OptionSwitchCell>(nameof(IsToggled));
public bool IsToggled public bool IsToggled
{ {
@ -258,7 +281,7 @@ namespace Billing.UI
public class OptionDatePickerCell : OptionCell public class OptionDatePickerCell : OptionCell
{ {
public static readonly BindableProperty DateProperty = BindableProperty.Create(nameof(Date), typeof(DateTime), typeof(OptionDatePickerCell)); public static readonly BindableProperty DateProperty = Helper.Create<DateTime, OptionDatePickerCell>(nameof(Date));
public DateTime Date public DateTime Date
{ {
@ -278,7 +301,7 @@ namespace Billing.UI
public class OptionTimePickerCell : OptionCell public class OptionTimePickerCell : OptionCell
{ {
public static readonly BindableProperty TimeProperty = BindableProperty.Create(nameof(Time), typeof(TimeSpan), typeof(OptionTimePickerCell)); public static readonly BindableProperty TimeProperty = Helper.Create<TimeSpan, OptionTimePickerCell>(nameof(Time));
public TimeSpan Time public TimeSpan Time
{ {
@ -298,9 +321,9 @@ namespace Billing.UI
public class OptionEntryCell : OptionCell public class OptionEntryCell : OptionCell
{ {
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(OptionEntryCell)); public static readonly BindableProperty TextProperty = Helper.Create<string, OptionEntryCell>(nameof(Text));
public static readonly BindableProperty KeyboardProperty = BindableProperty.Create(nameof(Keyboard), typeof(Keyboard), typeof(OptionEntryCell)); public static readonly BindableProperty KeyboardProperty = Helper.Create<Keyboard, OptionEntryCell>(nameof(Keyboard), defaultValue: Keyboard.Default);
public static readonly BindableProperty PlaceholderProperty = BindableProperty.Create(nameof(Placeholder), typeof(string), typeof(OptionEntryCell)); public static readonly BindableProperty PlaceholderProperty = Helper.Create<string, OptionEntryCell>(nameof(Placeholder));
public string Text public string Text
{ {
@ -351,10 +374,10 @@ namespace Billing.UI
public class OptionEditorCell : OptionVerticalCell public class OptionEditorCell : OptionVerticalCell
{ {
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(OptionEditorCell)); public static readonly BindableProperty TextProperty = Helper.Create<string, OptionEditorCell>(nameof(Text));
public static readonly BindableProperty FontSizeProperty = BindableProperty.Create(nameof(FontSize), typeof(double), typeof(OptionEditorCell), defaultValue: Device.GetNamedSize(NamedSize.Default, typeof(Editor))); public static readonly BindableProperty FontSizeProperty = Helper.Create<double, OptionEditorCell>(nameof(FontSize), defaultValue: Device.GetNamedSize(NamedSize.Default, typeof(Editor)));
public static readonly BindableProperty KeyboardProperty = BindableProperty.Create(nameof(Keyboard), typeof(Keyboard), typeof(OptionEditorCell), defaultValue: Keyboard.Default); public static readonly BindableProperty KeyboardProperty = Helper.Create<Keyboard, OptionEditorCell>(nameof(Keyboard), defaultValue: Keyboard.Default);
public static readonly BindableProperty PlaceholderProperty = BindableProperty.Create(nameof(Placeholder), typeof(string), typeof(OptionEditorCell)); public static readonly BindableProperty PlaceholderProperty = Helper.Create<string, OptionEditorCell>(nameof(Placeholder));
public string Text public string Text
{ {

View File

@ -7,24 +7,23 @@ namespace Billing.UI
{ {
public class WrapLayout : Layout<View> public class WrapLayout : Layout<View>
{ {
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IList), typeof(WrapLayout), propertyChanged: OnItemsSourcePropertyChanged); public static readonly BindableProperty ItemsSourceProperty = Helper.Create<IList, WrapLayout>(nameof(ItemsSource), propertyChanged: OnItemsSourcePropertyChanged);
public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create(nameof(ColumnSpacing), typeof(double), typeof(WrapLayout), defaultValue: 4d, propertyChanged: (obj, _, _) => ((WrapLayout)obj).InvalidateLayout()); public static readonly BindableProperty ColumnSpacingProperty = Helper.Create<double, WrapLayout>(nameof(ColumnSpacing), defaultValue: 4d, propertyChanged: (layout, _, _) => layout.InvalidateLayout());
public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create(nameof(RowSpacing), typeof(double), typeof(WrapLayout), defaultValue: 4d, propertyChanged: (obj, _, _) => ((WrapLayout)obj).InvalidateLayout()); public static readonly BindableProperty RowSpacingProperty = Helper.Create<double, WrapLayout>(nameof(RowSpacing), defaultValue: 4d, propertyChanged: (layout, _, _) => layout.InvalidateLayout());
private static void OnItemsSourcePropertyChanged(BindableObject obj, object old, object @new) private static void OnItemsSourcePropertyChanged(WrapLayout layout, IList old, IList list)
{ {
var itemTemplate = BindableLayout.GetItemTemplate(obj); var itemTemplate = BindableLayout.GetItemTemplate(layout);
if (itemTemplate == null) if (itemTemplate == null)
{ {
return; return;
} }
var layout = (WrapLayout)obj; if (list == null)
if (@new == null)
{ {
layout.Children.Clear(); layout.Children.Clear();
layout.InvalidateLayout(); layout.InvalidateLayout();
} }
else if (@new is IList list) else
{ {
layout.freezed = true; layout.freezed = true;
layout.Children.Clear(); layout.Children.Clear();

View File

@ -1,8 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Billing.Languages; using Billing.Languages;
using Billing.Models; using Billing.Models;
using Billing.Store;
using Billing.UI; using Billing.UI;
using Xamarin.Forms; using Xamarin.Forms;
@ -10,10 +10,10 @@ namespace Billing.Views
{ {
public partial class AccountPage : BillingPage public partial class AccountPage : BillingPage
{ {
private static readonly BindableProperty BalanceProperty = BindableProperty.Create(nameof(Balance), typeof(decimal), typeof(AccountPage)); private static readonly BindableProperty BalanceProperty = Helper.Create<decimal, AccountPage>(nameof(Balance));
private static readonly BindableProperty AssetProperty = BindableProperty.Create(nameof(Asset), typeof(decimal), typeof(AccountPage)); private static readonly BindableProperty AssetProperty = Helper.Create<decimal, AccountPage>(nameof(Asset));
private static readonly BindableProperty LiabilityProperty = BindableProperty.Create(nameof(Liability), typeof(decimal), typeof(AccountPage)); private static readonly BindableProperty LiabilityProperty = Helper.Create<decimal, AccountPage>(nameof(Liability));
private static readonly BindableProperty AccountsProperty = BindableProperty.Create(nameof(Accounts), typeof(List<AccountGrouping>), typeof(AccountPage)); private static readonly BindableProperty AccountsProperty = Helper.Create<List<AccountGrouping>, AccountPage>(nameof(Accounts));
public decimal Balance => (decimal)GetValue(BalanceProperty); public decimal Balance => (decimal)GetValue(BalanceProperty);
public decimal Asset => (decimal)GetValue(AssetProperty); public decimal Asset => (decimal)GetValue(AssetProperty);
@ -50,6 +50,17 @@ namespace Billing.Views
groupLayout.Refresh(accounts); groupLayout.Refresh(accounts);
} }
protected override void OnRefresh()
{
accounts.Clear();
foreach (var account in App.Accounts)
{
AddToAccountGroup(account);
}
RefreshBalance(true);
groupLayout.Refresh(accounts);
}
private void RefreshBalance(bool calc = false) private void RefreshBalance(bool calc = false)
{ {
if (calc) if (calc)
@ -64,17 +75,6 @@ namespace Billing.Views
private void AddToAccountGroup(Account account) 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); var group = accounts.FirstOrDefault(g => g.Key == account.Category);
if (group == null) if (group == null)
{ {
@ -120,25 +120,29 @@ namespace Billing.Views
if (group == null) if (group == null)
{ {
Helper.Error("account.delete", "unexpected deleting account, cannot find the current category"); Helper.Error("account.delete", "unexpected deleting account, cannot find the current category");
return; }
} else
group.Remove(account);
if (group.Count == 0)
{ {
accounts.Remove(group); group.Remove(account);
if (group.Count == 0)
{
accounts.Remove(group);
}
} }
RefreshBalance(); RefreshBalance();
groupLayout.Refresh(accounts); 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; var add = e.Account.Id <= 0;
if (add) if (add)
{ {
App.Accounts.Add(e.Account); App.Accounts.Add(e.Account);
@ -147,7 +151,9 @@ namespace Billing.Views
RefreshBalance(!add); RefreshBalance(!add);
groupLayout.Refresh(accounts); groupLayout.Refresh(accounts);
Task.Run(App.WriteAccounts); RankPage.Instance?.SetNeedRefresh();
await StoreHelper.SaveAccountItemAsync(e.Account);
} }
} }

View File

@ -9,11 +9,11 @@ namespace Billing.Views
{ {
public partial class AddAccountPage : BillingPage public partial class AddAccountPage : BillingPage
{ {
private static readonly BindableProperty AccountNameProperty = BindableProperty.Create(nameof(AccountName), typeof(string), typeof(AddAccountPage)); private static readonly BindableProperty AccountNameProperty = Helper.Create<string, AddAccountPage>(nameof(AccountName));
private static readonly BindableProperty AccountIconProperty = BindableProperty.Create(nameof(AccountIcon), typeof(string), typeof(AddAccountPage)); private static readonly BindableProperty AccountIconProperty = Helper.Create<string, AddAccountPage>(nameof(AccountIcon));
private static readonly BindableProperty CategoryProperty = BindableProperty.Create(nameof(Category), typeof(AccountCategory), typeof(AddAccountPage)); private static readonly BindableProperty CategoryProperty = Helper.Create<AccountCategory, AddAccountPage>(nameof(Category));
private static readonly BindableProperty InitialProperty = BindableProperty.Create(nameof(Initial), typeof(string), typeof(AddAccountPage)); private static readonly BindableProperty InitialProperty = Helper.Create<string, AddAccountPage>(nameof(Initial));
private static readonly BindableProperty MemoProperty = BindableProperty.Create(nameof(Memo), typeof(string), typeof(AddAccountPage)); private static readonly BindableProperty MemoProperty = Helper.Create<string, AddAccountPage>(nameof(Memo));
public string AccountName public string AccountName
{ {
@ -59,7 +59,7 @@ namespace Billing.Views
this.account = account; this.account = account;
if (account == null) if (account == null)
{ {
AccountIcon = BaseModel.ICON_DEFAULT; AccountIcon = Definition.DefaultIcon;
Category = AccountCategory.Cash; Category = AccountCategory.Cash;
} }
else else
@ -73,15 +73,9 @@ namespace Billing.Views
InitializeComponent(); InitializeComponent();
} }
private bool focused; protected override void OnLoaded()
public override void OnLoaded()
{ {
if (!focused) editorName.SetFocus();
{
focused = true;
editorName.SetFocus();
}
} }
private async void OnCheckAccount() private async void OnCheckAccount()
@ -111,7 +105,6 @@ namespace Billing.Views
{ {
Account = account ?? new Account Account = account ?? new Account
{ {
Id = -1,
Name = AccountName, Name = AccountName,
Icon = AccountIcon, Icon = AccountIcon,
Category = Category, Category = Category,

View File

@ -10,9 +10,15 @@
BindingContext="{x:Reference billPage}"> BindingContext="{x:Reference billPage}">
<ContentPage.ToolbarItems> <ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" IconImageSource="pin.png" Command="{Binding ViewLocation}"/>
<ToolbarItem Order="Primary" IconImageSource="check.png" Command="{Binding CheckBill}"/> <ToolbarItem Order="Primary" IconImageSource="check.png" Command="{Binding CheckBill}"/>
</ContentPage.ToolbarItems> </ContentPage.ToolbarItems>
<ContentPage.Resources>
<ui:IconConverter x:Key="iconConverter"/>
<ui:TintColorConverter x:Key="tintColorConverter"/>
</ContentPage.Resources>
<ContentPage.Content> <ContentPage.Content>
<TableView Intent="Settings" HasUnevenRows="True"> <TableView Intent="Settings" HasUnevenRows="True">
<TableSection Title=" "> <TableSection Title=" ">
@ -29,14 +35,18 @@
Title="{r:Text Name}" Title="{r:Text Name}"
Text="{Binding Name, Mode=TwoWay}" Text="{Binding Name, Mode=TwoWay}"
Placeholder="{r:Text NamePlaceholder}"/> Placeholder="{r:Text NamePlaceholder}"/>
<ui:OptionSelectCell Height="44" Icon="project.png" <ui:OptionImageCell Height="44" Icon="project.png"
Title="{r:Text Category}" Title="{r:Text Category}"
Detail="{Binding CategoryName}" Detail="{Binding Category.Name}"
Command="{Binding SelectCategory}"/> ImageSource="{Binding Category.Icon, Converter={StaticResource iconConverter}}"
<ui:OptionSelectCell Height="44" Icon="wallet.png" TintColor="{Binding Category.TintColor, Converter={StaticResource tintColorConverter}}"
Title="{r:Text Account}" Command="{Binding SelectCategory}"/>
Detail="{Binding WalletName}" <ui:OptionImageCell Height="44" Icon="wallet.png"
Command="{Binding SelectWallet}"/> Title="{r:Text Account}"
Detail="{Binding Wallet.Name}"
ImageSource="{Binding Wallet.Icon, Converter={StaticResource iconConverter}}"
TintColor="{DynamicResource PrimaryColor}"
Command="{Binding SelectWallet}"/>
<ui:OptionEntryCell Height="44" Icon="online.png" <ui:OptionEntryCell Height="44" Icon="online.png"
Title="{r:Text Store}" Title="{r:Text Store}"
Text="{Binding Store, Mode=TwoWay}"/> Text="{Binding Store, Mode=TwoWay}"/>

View File

@ -1,24 +1,31 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Billing.Languages; using System.Threading;
using System.Threading.Tasks;
using Billing.Models; using Billing.Models;
using Billing.Store;
using Billing.UI; using Billing.UI;
using Xamarin.Essentials;
using Xamarin.Forms; using Xamarin.Forms;
using Resource = Billing.Languages.Resource;
namespace Billing.Views namespace Billing.Views
{ {
public partial class AddBillPage : BillingPage public partial class AddBillPage : BillingPage
{ {
private static readonly BindableProperty AmountProperty = BindableProperty.Create(nameof(Amount), typeof(string), typeof(AddBillPage)); private static readonly BindableProperty CheckBillProperty = Helper.Create<Command, AddBillPage>(nameof(CheckBill), defaultValue: new Command(() => { }, () => false));
private static readonly BindableProperty NameProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(AddBillPage)); private static readonly BindableProperty AmountProperty = Helper.Create<string, AddBillPage>(nameof(Amount));
private static readonly BindableProperty CategoryNameProperty = BindableProperty.Create(nameof(CategoryName), typeof(string), typeof(AddBillPage)); private static readonly BindableProperty NameProperty = Helper.Create<string, AddBillPage>(nameof(Name), defaultValue: string.Empty);
private static readonly BindableProperty WalletNameProperty = BindableProperty.Create(nameof(WalletName), typeof(string), typeof(AddBillPage)); private static readonly BindableProperty CategoryProperty = Helper.Create<Category, AddBillPage>(nameof(Category));
private static readonly BindableProperty StoreProperty = BindableProperty.Create(nameof(Store), typeof(string), typeof(AddBillPage)); private static readonly BindableProperty WalletProperty = Helper.Create<Account, AddBillPage>(nameof(Wallet));
private static readonly BindableProperty CreatedDateProperty = BindableProperty.Create(nameof(CreatedDate), typeof(DateTime), typeof(AddBillPage)); private static readonly BindableProperty StoreProperty = Helper.Create<string, AddBillPage>(nameof(Store));
private static readonly BindableProperty CreatedTimeProperty = BindableProperty.Create(nameof(CreatedTime), typeof(TimeSpan), typeof(AddBillPage)); private static readonly BindableProperty CreatedDateProperty = Helper.Create<DateTime, AddBillPage>(nameof(CreatedDate));
private static readonly BindableProperty NoteProperty = BindableProperty.Create(nameof(Note), typeof(string), typeof(AddBillPage)); private static readonly BindableProperty CreatedTimeProperty = Helper.Create<TimeSpan, AddBillPage>(nameof(CreatedTime));
private static readonly BindableProperty NoteProperty = Helper.Create<string, AddBillPage>(nameof(Note));
private static readonly BindableProperty ViewLocationProperty = Helper.Create<Command, AddBillPage>(nameof(ViewLocation), defaultValue: new Command(() => { }, () => false));
public Command CheckBill => (Command)GetValue(CheckBillProperty);
public string Amount public string Amount
{ {
get => (string)GetValue(AmountProperty); get => (string)GetValue(AmountProperty);
@ -29,8 +36,8 @@ namespace Billing.Views
get => (string)GetValue(NameProperty); get => (string)GetValue(NameProperty);
set => SetValue(NameProperty, value); set => SetValue(NameProperty, value);
} }
public string CategoryName => (string)GetValue(CategoryNameProperty); public Category Category => (Category)GetValue(CategoryProperty);
public string WalletName => (string)GetValue(WalletNameProperty); public Account Wallet => (Account)GetValue(WalletProperty);
public string Store public string Store
{ {
get => (string)GetValue(StoreProperty); get => (string)GetValue(StoreProperty);
@ -52,22 +59,23 @@ namespace Billing.Views
set => SetValue(NoteProperty, value); set => SetValue(NoteProperty, value);
} }
public Command CheckBill { get; }
public Command SelectCategory { get; } public Command SelectCategory { get; }
public Command SelectWallet { get; } public Command SelectWallet { get; }
public Command ViewLocation => (Command)GetValue(ViewLocationProperty);
public event EventHandler<Bill> BillChecked; public event EventHandler<Bill> BillChecked;
private readonly Bill bill; private readonly Bill bill;
private readonly DateTime createDate; private readonly DateTime createDate;
private int walletId; private bool categoryChanged;
private int categoryId; private CancellationTokenSource tokenSource;
private Location location;
public AddBillPage() : this(DateTime.Today) { }
public AddBillPage(DateTime date) public AddBillPage(DateTime date)
{ {
createDate = date; createDate = date;
CheckBill = new Command(OnCheckBill);
SelectCategory = new Command(OnSelectCategory); SelectCategory = new Command(OnSelectCategory);
SelectWallet = new Command(OnSelectWallet); SelectWallet = new Command(OnSelectWallet);
InitializeComponent(); InitializeComponent();
@ -79,25 +87,38 @@ namespace Billing.Views
public AddBillPage(Bill bill) public AddBillPage(Bill bill)
{ {
this.bill = bill; this.bill = bill;
CheckBill = new Command(OnCheckBill);
SelectCategory = new Command(OnSelectCategory); SelectCategory = new Command(OnSelectCategory);
SelectWallet = new Command(OnSelectWallet); SelectWallet = new Command(OnSelectWallet);
#if __IOS__
if (bill != null && bill.Latitude != null && bill.Longitude != null)
{
SetValue(ViewLocationProperty, new Command(OnViewLocation));
}
#endif
InitializeComponent(); InitializeComponent();
Title = Resource.EditBill; Title = Resource.EditBill;
Initial(); Initial();
} }
protected override void OnDisappearing()
{
if (tokenSource != null && !tokenSource.IsCancellationRequested)
{
tokenSource.Cancel();
}
base.OnDisappearing();
}
private void Initial() private void Initial()
{ {
if (bill != null) if (bill != null)
{ {
Amount = Math.Abs(bill.Amount).ToString(CultureInfo.InvariantCulture); Amount = Math.Abs(bill.Amount).ToString(CultureInfo.InvariantCulture);
Name = bill.Name; Name = bill.Name;
walletId = bill.WalletId; SetValue(WalletProperty, App.Accounts.FirstOrDefault(a => a.Id == bill.WalletId) ?? Account.Empty);
categoryId = bill.CategoryId; SetValue(CategoryProperty, App.Categories.FirstOrDefault(c => c.Id == bill.CategoryId) ?? Category.Empty);
SetValue(WalletNameProperty, App.Accounts.FirstOrDefault(a => a.Id == walletId)?.Name); categoryChanged = true;
SetValue(CategoryNameProperty, App.Categories.FirstOrDefault(c => c.Id == categoryId)?.Name);
Store = bill.Store; Store = bill.Store;
CreatedDate = bill.CreateTime.Date; CreatedDate = bill.CreateTime.Date;
CreatedTime = bill.CreateTime.TimeOfDay; CreatedTime = bill.CreateTime.TimeOfDay;
@ -105,26 +126,59 @@ namespace Billing.Views
} }
else else
{ {
var first = App.Accounts.First(); SetValue(WalletProperty, App.Accounts.FirstOrDefault() ?? Account.Empty);
walletId = first.Id; SetValue(CategoryProperty, App.Categories.FirstOrDefault() ?? Category.Empty);
SetValue(WalletNameProperty, first.Name);
var firstCategory = App.Categories.First();
categoryId = firstCategory.Id;
SetValue(CategoryNameProperty, firstCategory.Name);
CreatedDate = createDate.Date; CreatedDate = createDate.Date;
CreatedTime = DateTime.Now.TimeOfDay; CreatedTime = DateTime.Now.TimeOfDay;
} }
} }
private bool focused; protected override void OnLoaded()
public override void OnLoaded()
{ {
if (!focused) if (bill == null)
{ {
focused = true;
editorAmount.SetFocus(); editorAmount.SetFocus();
} }
if (bill == null && App.SaveLocation)
{
_ = GetCurrentLocation();
}
else
{
SetValue(CheckBillProperty, new Command(OnCheckBill));
}
}
private async Task GetCurrentLocation()
{
try
{
var request = new GeolocationRequest(GeolocationAccuracy.Best, TimeSpan.FromSeconds(10));
tokenSource = new CancellationTokenSource();
var status = await Helper.CheckAndRequestPermissionAsync<Permissions.LocationWhenInUse>();
if (status != PermissionStatus.Granted)
{
return;
}
location = await Geolocation.GetLocationAsync(request, tokenSource.Token);
#if __IOS__
if (bill == null)
{
SetValue(ViewLocationProperty, new Command(OnViewLocation));
}
#endif
}
catch (FeatureNotSupportedException) { }
catch (FeatureNotEnabledException) { }
catch (PermissionException) { }
catch (Exception ex)
{
Helper.Error("location.get", ex);
}
finally
{
SetValue(CheckBillProperty, new Command(OnCheckBill));
}
} }
private async void OnCheckBill() private async void OnCheckBill()
@ -144,8 +198,9 @@ namespace Billing.Views
await this.ShowMessage(Resource.AmountRequired); await this.ShowMessage(Resource.AmountRequired);
return; return;
} }
var category = Category;
var wallet = Wallet;
amount = Math.Abs(amount); amount = Math.Abs(amount);
var category = App.Categories.FirstOrDefault(c => c.Id == categoryId);
if (category.Type == CategoryType.Spending) if (category.Type == CategoryType.Spending)
{ {
amount *= -1; amount *= -1;
@ -156,27 +211,42 @@ namespace Billing.Views
{ {
name = category.Name; name = category.Name;
} }
if (bill != null) Bill b = bill;
{ if (b == null)
bill.Amount = amount; {
bill.Name = name; b = new Bill();
bill.CategoryId = categoryId; }
bill.WalletId = walletId; b.Amount = amount;
bill.CreateTime = CreatedDate.Date.Add(CreatedTime); b.Name = name;
bill.Store = Store; b.CategoryId = category.Id;
bill.Note = Note; b.WalletId = wallet.Id;
b.CreateTime = CreatedDate.Date.Add(CreatedTime);
b.Store = Store;
b.Note = Note;
if (location != null)
{
b.Latitude = location.Latitude;
b.Longitude = location.Longitude;
b.Altitude = location.Altitude;
b.Accuracy = location.Accuracy;
b.IsGps = location.IsFromMockProvider;
}
BillChecked?.Invoke(this, b);
category.LastAccountId = wallet.Id;
category.LastUsed = DateTime.Now;
await StoreHelper.SaveCategoryItemAsync(category);
try
{
HapticFeedback.Perform();
}
catch (FeatureNotSupportedException) { }
catch (Exception ex)
{
Helper.Error("haptic.feedback", ex);
} }
BillChecked?.Invoke(this, bill ?? new Bill
{
Id = -1,
Amount = amount,
Name = name,
CategoryId = categoryId,
WalletId = walletId,
CreateTime = CreatedDate.Date.Add(CreatedTime),
Store = Store,
Note = Note
});
} }
} }
@ -188,7 +258,7 @@ namespace Billing.Views
} }
using (Tap.Start()) using (Tap.Start())
{ {
var page = new CategorySelectPage(categoryId); var page = new CategorySelectPage(categoryChanged ? Category.Id : -1);
page.CategoryTapped += CategorySelectPage_Tapped; page.CategoryTapped += CategorySelectPage_Tapped;
await Navigation.PushAsync(page); await Navigation.PushAsync(page);
} }
@ -196,8 +266,16 @@ namespace Billing.Views
private void CategorySelectPage_Tapped(object sender, UICategory e) private void CategorySelectPage_Tapped(object sender, UICategory e)
{ {
categoryId = e.Category.Id; SetValue(CategoryProperty, e.Category);
SetValue(CategoryNameProperty, e.Name); categoryChanged = true;
if (e.Category.LastAccountId != null)
{
var wallet = App.Accounts.FirstOrDefault(a => a.Id == e.Category.LastAccountId.Value);
if (wallet != null)
{
SetValue(WalletProperty, wallet);
}
}
} }
private async void OnSelectWallet() private async void OnSelectWallet()
@ -208,22 +286,39 @@ namespace Billing.Views
} }
using (Tap.Start()) using (Tap.Start())
{ {
var source = App.Accounts.Select(a => new SelectItem<int> var page = new ItemSelectPage<Account>(App.Accounts);
{
Value = a.Id,
Name = a.Name,
Icon = a.Icon
});
var page = new ItemSelectPage<SelectItem<int>>(source);
page.ItemTapped += Wallet_ItemTapped; page.ItemTapped += Wallet_ItemTapped;
await Navigation.PushAsync(page); await Navigation.PushAsync(page);
} }
} }
private void Wallet_ItemTapped(object sender, SelectItem<int> account) private void Wallet_ItemTapped(object sender, Account account)
{ {
walletId = account.Value; SetValue(WalletProperty, account);
SetValue(WalletNameProperty, account.Name); }
private async void OnViewLocation()
{
if (bill == null && location == null)
{
return;
}
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
var page = new ViewLocationPage(bill ?? new Bill
{
Name = Name,
Store = Store,
Longitude = location.Longitude,
Latitude = location.Latitude
});
page.Synced += (sender, loc) => location = loc;
await Navigation.PushAsync(page);
}
} }
} }
} }

View File

@ -41,7 +41,7 @@
<ViewCell Height="120"> <ViewCell Height="120">
<Grid BackgroundColor="{DynamicResource OptionTintColor}" <Grid BackgroundColor="{DynamicResource OptionTintColor}"
ColumnDefinitions=".35*, .65*" Padding="10"> ColumnDefinitions=".35*, .65*" Padding="10">
<ui:ColorPicker Grid.Column="1" ColorChanged="ColorPicker_ColorChanged"/> <ui:ColorPicker Grid.Column="1" Command="{Binding ColorPickerCommand}"/>
</Grid> </Grid>
</ViewCell> </ViewCell>
</TableSection> </TableSection>

View File

@ -10,10 +10,10 @@ namespace Billing.Views
{ {
public partial class AddCategoryPage : BillingPage public partial class AddCategoryPage : BillingPage
{ {
private static readonly BindableProperty CategoryNameProperty = BindableProperty.Create(nameof(CategoryName), typeof(string), typeof(AddCategoryPage)); private static readonly BindableProperty CategoryNameProperty = Helper.Create<string, AddCategoryPage>(nameof(CategoryName));
private static readonly BindableProperty CategoryIconProperty = BindableProperty.Create(nameof(CategoryIcon), typeof(string), typeof(AddCategoryPage)); private static readonly BindableProperty CategoryIconProperty = Helper.Create<string, AddCategoryPage>(nameof(CategoryIcon));
private static readonly BindableProperty TintColorProperty = BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(AddCategoryPage)); private static readonly BindableProperty TintColorProperty = Helper.Create<Color, AddCategoryPage>(nameof(TintColor));
private static readonly BindableProperty TintColorStringProperty = BindableProperty.Create(nameof(TintColorString), typeof(string), typeof(AddCategoryPage)); private static readonly BindableProperty TintColorStringProperty = Helper.Create<string, AddCategoryPage>(nameof(TintColorString));
public string CategoryName public string CategoryName
{ {
@ -38,6 +38,7 @@ namespace Billing.Views
public Command CheckCategory { get; } public Command CheckCategory { get; }
public Command SelectIcon { get; } public Command SelectIcon { get; }
public Command ColorPickerCommand { get; }
public event EventHandler<Category> CategoryChecked; public event EventHandler<Category> CategoryChecked;
@ -55,14 +56,13 @@ namespace Billing.Views
{ {
CategoryName = category.Name; CategoryName = category.Name;
CategoryIcon = category.Icon; CategoryIcon = category.Icon;
if (category.TintColor == Color.Transparent || if (category.TintColor.IsTransparent())
category.TintColor == default)
{ {
TintColor = BaseTheme.CurrentPrimaryColor; TintColor = BaseTheme.CurrentPrimaryColor;
} }
else else
{ {
TintColor = category.TintColor; TintColor = category.TintColor.ToColor();
} }
} }
else else
@ -73,25 +73,23 @@ namespace Billing.Views
CheckCategory = new Command(OnCheckCategory); CheckCategory = new Command(OnCheckCategory);
SelectIcon = new Command(OnSelectIcon); SelectIcon = new Command(OnSelectIcon);
ColorPickerCommand = new Command(OnColorPickerCommand);
InitializeComponent(); InitializeComponent();
} }
private bool focused; protected override void OnLoaded()
public override void OnLoaded()
{ {
if (!focused) editorName.SetFocus();
{
focused = true;
editorName.SetFocus();
}
} }
private void ColorPicker_ColorChanged(object sender, Color e) private void OnColorPickerCommand(object o)
{ {
TintColor = e; if (o is Color color)
TintColorString = Helper.WrapColorString(e.ToHex()); {
TintColor = color;
TintColorString = Helper.WrapColorString(color.ToHex());
}
} }
private async void OnCheckCategory() private async void OnCheckCategory()
@ -104,15 +102,15 @@ namespace Billing.Views
{ {
var currentColor = BaseTheme.CurrentPrimaryColor; var currentColor = BaseTheme.CurrentPrimaryColor;
var tintColor = TintColor; var tintColor = TintColor;
var color = (tintColor == currentColor ? Color.Transparent : tintColor).ToLong();
var category = App.Categories.FirstOrDefault(c => c.Id == categoryId); var category = App.Categories.FirstOrDefault(c => c.Id == categoryId);
if (category == null) if (category == null)
{ {
CategoryChecked?.Invoke(this, new Category CategoryChecked?.Invoke(this, new Category
{ {
Id = -1,
Name = CategoryName, Name = CategoryName,
Icon = CategoryIcon, Icon = CategoryIcon,
TintColor = tintColor == currentColor ? Color.Transparent : tintColor, TintColor = color,
ParentId = parent?.Id, ParentId = parent?.Id,
Type = parent?.Type ?? CategoryType.Spending Type = parent?.Type ?? CategoryType.Spending
}); });
@ -121,7 +119,7 @@ namespace Billing.Views
{ {
category.Name = CategoryName; category.Name = CategoryName;
category.Icon = CategoryIcon; category.Icon = CategoryIcon;
category.TintColor = tintColor == currentColor ? Color.Transparent : tintColor; category.TintColor = color;
CategoryChecked?.Invoke(this, category); CategoryChecked?.Invoke(this, category);
} }
await Navigation.PopAsync(); await Navigation.PopAsync();

View File

@ -20,6 +20,7 @@
<ui:BalanceColorConverter x:Key="colorConverter"/> <ui:BalanceColorConverter x:Key="colorConverter"/>
<ui:TimeConverter x:Key="timeConverter"/> <ui:TimeConverter x:Key="timeConverter"/>
<ui:IconConverter x:Key="iconConverter"/> <ui:IconConverter x:Key="iconConverter"/>
<ui:TintColorConverter x:Key="tintColorConverter"/>
</ContentPage.Resources> </ContentPage.Resources>
<Shell.TitleView> <Shell.TitleView>
@ -31,13 +32,8 @@
VerticalOptions="Center" LongCommand="{Binding TitleLongPressed}"> VerticalOptions="Center" LongCommand="{Binding TitleLongPressed}">
<Label Text="{Binding SelectedDate, Converter={StaticResource titleDateConverter}}" <Label Text="{Binding SelectedDate, Converter={StaticResource titleDateConverter}}"
TextColor="{DynamicResource PrimaryColor}" TextColor="{DynamicResource PrimaryColor}"
FontSize="{OnPlatform Android=20, iOS=18}"> FontFamily="{x:Static ui:Definition.SemiBoldFontFamily}"
<Label.FontFamily> FontSize="{OnPlatform Android=20, iOS=18}"/>
<OnPlatform x:TypeArguments="x:String"
Android="OpenSans-SemiBold.ttf#OpenSans-SemiBold"
iOS="OpenSans-Bold"/>
</Label.FontFamily>
</Label>
</ui:LongPressGrid> </ui:LongPressGrid>
<ui:TintImageButton Source="calendar.png" WidthRequest="20" HeightRequest="20" <ui:TintImageButton Source="calendar.png" WidthRequest="20" HeightRequest="20"
VerticalOptions="Center" HorizontalOptions="Start" VerticalOptions="Center" HorizontalOptions="Start"
@ -101,6 +97,7 @@
CommandParameter="{Binding .}"/> CommandParameter="{Binding .}"/>
</Grid.GestureRecognizers> </Grid.GestureRecognizers>
<ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}" <ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}"
ui:TintHelper.TintColor="{Binding TintColor, Converter={StaticResource tintColorConverter}}"
WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/> WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/>
<Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}" <Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}"
VerticalOptions="Center" VerticalOptions="Center"

View File

@ -1,4 +1,5 @@
using Billing.Models; using Billing.Models;
using Billing.Store;
using Billing.UI; using Billing.UI;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -44,8 +45,6 @@ namespace Billing.Views
public Command EditBilling { get; } public Command EditBilling { get; }
public Command DeleteBilling { get; } public Command DeleteBilling { get; }
private bool initialized;
public BillPage() public BillPage()
{ {
TitleDateTap = new Command(OnTitleDateTapped); TitleDateTap = new Command(OnTitleDateTapped);
@ -56,26 +55,26 @@ namespace Billing.Views
InitializeComponent(); InitializeComponent();
} }
public override void OnLoaded() protected override void OnLoaded()
{ {
if (!initialized) billingDate.SetDateTime(DateTime.Today);
}
protected override void OnRefresh()
{
Task.Run(() =>
{ {
initialized = true; var bills = App.Bills.Where(b => Helper.IsSameDay(b.CreateTime, SelectedDate));
billingDate.SetDateTime(DateTime.Today); Bills = new List<UIBill>(bills.OrderBy(b => b.CreateTime).Select(b => Helper.WrapBill(b)));
} RefreshBalance(Bills);
MainThread.BeginInvokeOnMainThread(async () => await scrollView.ScrollToAsync(0, 0, true));
});
} }
private void OnDateSelected(object sender, DateEventArgs e) private void OnDateSelected(object sender, DateEventArgs e)
{ {
SelectedDate = e.Date; SelectedDate = e.Date;
OnRefresh();
Task.Run(() =>
{
var bills = App.Bills.Where(b => Helper.IsSameDay(b.CreateTime, e.Date));
Bills = new List<UIBill>(bills.OrderBy(b => b.CreateTime).Select(b => Helper.WrapBill(b)));
RefreshBalance(Bills);
MainThread.BeginInvokeOnMainThread(async () => await scrollView.ScrollToAsync(0, 0, true));
});
} }
private void RefreshBalance(List<UIBill> bills) private void RefreshBalance(List<UIBill> bills)
@ -90,7 +89,9 @@ namespace Billing.Views
private void UpdateBill(UIBill bill) private void UpdateBill(UIBill bill)
{ {
bill.Icon = App.Categories.FirstOrDefault(c => c.Id == bill.Bill.CategoryId)?.Icon ?? BaseModel.ICON_DEFAULT; var category = App.Categories.FirstOrDefault(c => c.Id == bill.Bill.CategoryId);
bill.Icon = category?.Icon ?? Definition.DefaultIcon;
bill.TintColor = category?.TintColor ?? Definition.TransparentColor;
bill.Name = bill.Bill.Name; bill.Name = bill.Bill.Name;
bill.DateCreation = bill.Bill.CreateTime; bill.DateCreation = bill.Bill.CreateTime;
bill.Amount = bill.Bill.Amount; bill.Amount = bill.Bill.Amount;
@ -113,6 +114,16 @@ namespace Billing.Views
private void OnTitleDateLongPressed() private void OnTitleDateLongPressed()
{ {
billingDate.SetDateTime(DateTime.Today); billingDate.SetDateTime(DateTime.Today);
try
{
HapticFeedback.Perform();
}
catch (FeatureNotSupportedException) { }
catch (Exception ex)
{
Helper.Error("haptic.feedback", ex);
}
} }
private async void OnEditBilling(object o) private async void OnEditBilling(object o)
@ -161,27 +172,19 @@ namespace Billing.Views
App.Bills.Remove(bill.Bill); App.Bills.Remove(bill.Bill);
billsLayout.Refresh(bills); billsLayout.Refresh(bills);
RefreshBalance(bills); RefreshBalance(bills);
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) if (e.Id <= 0)
{ {
int maxId;
if (App.Bills.Count > 0)
{
maxId = App.Bills.Max(b => b.Id);
}
else
{
maxId = -1;
}
e.Id = maxId + 1;
App.Bills.Add(e); App.Bills.Add(e);
var bills = Bills; var bills = Bills;
bills.Add(Helper.WrapBill(e)); bills.Add(Helper.WrapBill(e));
@ -192,28 +195,50 @@ namespace Billing.Views
var bill = Bills.FirstOrDefault(b => b.Bill == e); var bill = Bills.FirstOrDefault(b => b.Bill == e);
if (bill != null) if (bill != null)
{ {
UpdateBill(bill); if (bill.DateCreation != e.CreateTime)
{
var bills = App.Bills.Where(b => Helper.IsSameDay(b.CreateTime, SelectedDate));
Bills = new List<UIBill>(bills.OrderBy(b => b.CreateTime).Select(b => Helper.WrapBill(b)));
RefreshBalance(Bills);
RankPage.Instance?.SetNeedRefresh();
await StoreHelper.SaveBillItemAsync(e);
return;
}
else
{
UpdateBill(bill);
}
} }
} }
RefreshBalance(Bills); RefreshBalance(Bills);
Task.Run(App.WriteBills); RankPage.Instance?.SetNeedRefresh();
await StoreHelper.SaveBillItemAsync(e);
} }
} }
public class UIBill : BindableObject public class UIBill : BindableObject
{ {
public static readonly BindableProperty IconProperty = BindableProperty.Create(nameof(Icon), typeof(string), typeof(UIBill)); public static readonly BindableProperty IconProperty = Helper.Create<string, UIBill>(nameof(Icon));
public static readonly BindableProperty NameProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(UIBill)); public static readonly BindableProperty TintColorProperty = Helper.Create<long, UIBill>(nameof(TintColor));
public static readonly BindableProperty DateCreationProperty = BindableProperty.Create(nameof(DateCreation), typeof(DateTime), typeof(UIBill)); public static readonly BindableProperty NameProperty = Helper.Create<string, UIBill>(nameof(Name));
public static readonly BindableProperty AmountProperty = BindableProperty.Create(nameof(Amount), typeof(decimal), typeof(UIBill)); public static readonly BindableProperty DateCreationProperty = Helper.Create<DateTime, UIBill>(nameof(DateCreation));
public static readonly BindableProperty WalletProperty = BindableProperty.Create(nameof(Wallet), typeof(string), typeof(UIBill)); public static readonly BindableProperty AmountProperty = Helper.Create<decimal, UIBill>(nameof(Amount));
public static readonly BindableProperty WalletProperty = Helper.Create<string, UIBill>(nameof(Wallet));
public string Icon public string Icon
{ {
get => (string)GetValue(IconProperty); get => (string)GetValue(IconProperty);
set => SetValue(IconProperty, value); set => SetValue(IconProperty, value);
} }
public long TintColor
{
get => (long)GetValue(TintColorProperty);
set => SetValue(TintColorProperty, value);
}
public string Name public string Name
{ {
get => (string)GetValue(NameProperty); get => (string)GetValue(NameProperty);

View File

@ -10,6 +10,7 @@
<ContentPage.Resources> <ContentPage.Resources>
<ui:IconConverter x:Key="iconConverter"/> <ui:IconConverter x:Key="iconConverter"/>
<ui:TintColorConverter x:Key="tintColorConverter"/>
</ContentPage.Resources> </ContentPage.Resources>
<ScrollView> <ScrollView>
@ -32,7 +33,7 @@
CommandParameter="{Binding .}"/> CommandParameter="{Binding .}"/>
</Grid.GestureRecognizers> </Grid.GestureRecognizers>
<ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}" <ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}"
ui:TintHelper.TintColor="{Binding TintColor}" ui:TintHelper.TintColor="{Binding TintColor, Converter={StaticResource tintColorConverter}}"
WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/> WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/>
<Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}" <Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}"
HorizontalOptions="FillAndExpand" VerticalOptions="Center" HorizontalOptions="FillAndExpand" VerticalOptions="Center"

View File

@ -1,19 +1,18 @@
using Billing.Languages; using Billing.Languages;
using Billing.Models; using Billing.Models;
using Billing.Themes; using Billing.Store;
using Billing.UI; using Billing.UI;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Xamarin.Forms; using Xamarin.Forms;
namespace Billing.Views namespace Billing.Views
{ {
public partial class CategoryPage : BillingPage public partial class CategoryPage : BillingPage
{ {
private static readonly BindableProperty CategoriesProperty = BindableProperty.Create(nameof(Categories), typeof(IList), typeof(CategoryPage)); private static readonly BindableProperty CategoriesProperty = Helper.Create<IList, CategoryPage>(nameof(Categories));
private static readonly BindableProperty IsTopCategoryProperty = BindableProperty.Create(nameof(IsTopCategory), typeof(bool), typeof(CategoryPage)); private static readonly BindableProperty IsTopCategoryProperty = Helper.Create<bool, CategoryPage>(nameof(IsTopCategory));
public IList Categories public IList Categories
{ {
@ -68,9 +67,7 @@ namespace Billing.Views
Icon = category.Icon, Icon = category.Icon,
Name = category.Name, Name = category.Name,
IsTopCategory = IsTopCategory, IsTopCategory = IsTopCategory,
TintColor = category.TintColor == Color.Transparent || category.TintColor == default ? TintColor = category.TintColor
BaseTheme.CurrentPrimaryColor :
category.TintColor
}; };
} }
@ -112,7 +109,7 @@ namespace Billing.Views
Categories.Remove(c); Categories.Remove(c);
groupLayout.Refresh(Categories); groupLayout.Refresh(Categories);
App.Categories.Remove(c.Category); App.Categories.Remove(c.Category);
_ = Task.Run(App.WriteCategories); await StoreHelper.DeleteCategoryItemAsync(c.Category);
} }
} }
} }
@ -144,21 +141,11 @@ namespace Billing.Views
} }
} }
private void OnCategoryChecked(object sender, Category category) private async void OnCategoryChecked(object sender, Category category)
{ {
if (category.Id < 0) if (category.Id <= 0)
{ {
// add // add
int maxId;
if (App.Categories.Count > 0)
{
maxId = App.Categories.Max(b => b.Id);
}
else
{
maxId = -1;
}
category.Id = maxId + 1;
App.Categories.Add(category); App.Categories.Add(category);
Categories.Add(WrapCategory(category)); Categories.Add(WrapCategory(category));
} }
@ -183,26 +170,24 @@ namespace Billing.Views
} }
groupLayout.Refresh(Categories); groupLayout.Refresh(Categories);
Task.Run(App.WriteCategories); await StoreHelper.SaveCategoryItemAsync(category);
} }
private void UpdateCategory(UICategory c) private void UpdateCategory(UICategory c)
{ {
c.Name = c.Category.Name; c.Name = c.Category.Name;
c.Icon = c.Category.Icon; c.Icon = c.Category.Icon;
c.TintColor = c.Category.TintColor == Color.Transparent || c.Category.TintColor == default ? c.TintColor = c.Category.TintColor;
BaseTheme.CurrentPrimaryColor :
c.Category.TintColor;
} }
} }
public class UICategory : BindableObject public class UICategory : BindableObject
{ {
public static readonly BindableProperty IsCheckedProperty = BindableProperty.Create(nameof(IsChecked), typeof(bool), typeof(UICategory)); public static readonly BindableProperty IsCheckedProperty = Helper.Create<bool, UICategory>(nameof(IsChecked));
public static readonly BindableProperty IconProperty = BindableProperty.Create(nameof(Icon), typeof(string), typeof(UICategory)); public static readonly BindableProperty IconProperty = Helper.Create<string, UICategory>(nameof(Icon));
public static readonly BindableProperty NameProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(UICategory)); public static readonly BindableProperty NameProperty = Helper.Create<string, UICategory>(nameof(Name));
public static readonly BindableProperty TintColorProperty = BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(UICategory)); public static readonly BindableProperty TintColorProperty = Helper.Create<long, UICategory>(nameof(TintColor));
public static readonly BindableProperty IsTopCategoryProperty = BindableProperty.Create(nameof(IsTopCategory), typeof(bool), typeof(UICategory)); public static readonly BindableProperty IsTopCategoryProperty = Helper.Create<bool, UICategory>(nameof(IsTopCategory));
public bool IsChecked public bool IsChecked
{ {
@ -219,9 +204,9 @@ namespace Billing.Views
get => (string)GetValue(NameProperty); get => (string)GetValue(NameProperty);
set => SetValue(NameProperty, value); set => SetValue(NameProperty, value);
} }
public Color TintColor public long TintColor
{ {
get => (Color)GetValue(TintColorProperty); get => (long)GetValue(TintColorProperty);
set => SetValue(TintColorProperty, value); set => SetValue(TintColorProperty, value);
} }
public bool IsTopCategory public bool IsTopCategory
@ -242,6 +227,10 @@ namespace Billing.Views
{ {
public string Key { get; } public string Key { get; }
public CategoryGrouping(string key) : base()
{
Key = key;
}
public CategoryGrouping(string key, IEnumerable<UICategory> categories) : base(categories) public CategoryGrouping(string key, IEnumerable<UICategory> categories) : base(categories)
{ {
Key = key; Key = key;

View File

@ -13,6 +13,7 @@
<ContentPage.Resources> <ContentPage.Resources>
<ui:IconConverter x:Key="iconConverter"/> <ui:IconConverter x:Key="iconConverter"/>
<ui:SelectBackgroundColorConverter x:Key="backgroundConverter"/> <ui:SelectBackgroundColorConverter x:Key="backgroundConverter"/>
<ui:TintColorConverter x:Key="tintColorConverter"/>
</ContentPage.Resources> </ContentPage.Resources>
<Grid ColumnDefinitions=".5*, .5*"> <Grid ColumnDefinitions=".5*, .5*">
@ -36,7 +37,7 @@
CommandParameter="{Binding .}"/> CommandParameter="{Binding .}"/>
</Grid.GestureRecognizers> </Grid.GestureRecognizers>
<ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}" <ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}"
ui:TintHelper.TintColor="{Binding TintColor}" ui:TintHelper.TintColor="{Binding TintColor, Converter={StaticResource tintColorConverter}}"
WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/> WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/>
<Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}" <Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}"
HorizontalOptions="FillAndExpand" VerticalOptions="Center" HorizontalOptions="FillAndExpand" VerticalOptions="Center"
@ -58,7 +59,7 @@
CommandParameter="{Binding .}"/> CommandParameter="{Binding .}"/>
</Grid.GestureRecognizers> </Grid.GestureRecognizers>
<ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}" <ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}"
ui:TintHelper.TintColor="{Binding TintColor}" ui:TintHelper.TintColor="{Binding TintColor, Converter={StaticResource tintColorConverter}}"
WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/> WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/>
<Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}" <Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}"
HorizontalOptions="FillAndExpand" VerticalOptions="Center" HorizontalOptions="FillAndExpand" VerticalOptions="Center"

View File

@ -1,6 +1,5 @@
using Billing.Languages; using Billing.Languages;
using Billing.Models; using Billing.Models;
using Billing.Themes;
using Billing.UI; using Billing.UI;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -11,8 +10,8 @@ namespace Billing.Views
{ {
public partial class CategorySelectPage : BillingPage public partial class CategorySelectPage : BillingPage
{ {
private static readonly BindableProperty TopCategoriesProperty = BindableProperty.Create(nameof(TopCategories), typeof(List<CategoryGrouping>), typeof(CategorySelectPage)); private static readonly BindableProperty TopCategoriesProperty = Helper.Create<List<CategoryGrouping>, CategorySelectPage>(nameof(TopCategories));
private static readonly BindableProperty SubCategoriesProperty = BindableProperty.Create(nameof(SubCategories), typeof(List<UICategory>), typeof(CategorySelectPage)); private static readonly BindableProperty SubCategoriesProperty = Helper.Create<List<UICategory>, CategorySelectPage>(nameof(SubCategories));
public List<CategoryGrouping> TopCategories public List<CategoryGrouping> TopCategories
{ {
@ -31,12 +30,10 @@ namespace Billing.Views
public event EventHandler<UICategory> CategoryTapped; public event EventHandler<UICategory> CategoryTapped;
private readonly int categoryId; private readonly int categoryId;
private readonly Color defaultColor;
public CategorySelectPage(int id) public CategorySelectPage(int id)
{ {
categoryId = id; categoryId = id;
defaultColor = BaseTheme.CurrentPrimaryColor;
TapTopCategory = new Command(OnTopCategoryTapped); TapTopCategory = new Command(OnTopCategoryTapped);
TapSubCategory = new Command(OnSubCategoryTapped); TapSubCategory = new Command(OnSubCategoryTapped);
@ -45,6 +42,22 @@ namespace Billing.Views
new(Resource.Spending, App.Categories.Where(c => c.Type == CategoryType.Spending && c.ParentId == null).Select(c => WrapCategory(c))), new(Resource.Spending, App.Categories.Where(c => c.Type == CategoryType.Spending && c.ParentId == null).Select(c => WrapCategory(c))),
new(Resource.Income, App.Categories.Where(c => c.Type == CategoryType.Income && c.ParentId == null).Select(c => WrapCategory(c))) new(Resource.Income, App.Categories.Where(c => c.Type == CategoryType.Income && c.ParentId == null).Select(c => WrapCategory(c)))
}; };
UICategory last;
if (App.Categories.Any(c => c.LastUsed != null))
{
last = new UICategory(null)
{
IsChecked = true,
Icon = "rank",
Name = Resource.LastSelected,
TintColor = Definition.TransparentColor
};
TopCategories.Insert(0, new(Resource.Recent) { last });
}
else
{
last = null;
}
UICategory cat; UICategory cat;
var category = App.Categories.FirstOrDefault(c => c.Id == categoryId); var category = App.Categories.FirstOrDefault(c => c.Id == categoryId);
@ -58,7 +71,7 @@ namespace Billing.Views
} }
else else
{ {
cat = TopCategories.SelectMany(g => g).FirstOrDefault(c => c.Category.Id == category.ParentId); cat = TopCategories.SelectMany(g => g).FirstOrDefault(c => c.Category?.Id == category.ParentId) ?? last;
} }
DoRefreshSubCategories(cat); DoRefreshSubCategories(cat);
@ -72,7 +85,7 @@ namespace Billing.Views
IsChecked = c.Id == categoryId, IsChecked = c.Id == categoryId,
Icon = c.Icon, Icon = c.Icon,
Name = c.Name, Name = c.Name,
TintColor = c.TintColor == Color.Transparent || c.TintColor == default ? defaultColor : c.TintColor TintColor = c.TintColor
}; };
} }
@ -83,7 +96,18 @@ namespace Billing.Views
{ {
m.IsChecked = m == category; m.IsChecked = m == category;
} }
SubCategories = App.Categories.Where(c => c.ParentId == category.Category.Id).Select(c => WrapCategory(c)).ToList(); if (category == null)
{
return;
}
if (category.Category == null)
{
SubCategories = App.Categories.Where(c => c.ParentId != null && c.LastUsed != null).OrderByDescending(c => c.LastUsed).Take(10).Select(c => WrapCategory(c)).ToList();
}
else
{
SubCategories = App.Categories.Where(c => c.ParentId == category.Category.Id).Select(c => WrapCategory(c)).ToList();
}
} }
private async void OnTopCategoryTapped(object o) private async void OnTopCategoryTapped(object o)
@ -97,7 +121,7 @@ namespace Billing.Views
if (o is UICategory category) if (o is UICategory category)
{ {
DoRefreshSubCategories(category); DoRefreshSubCategories(category);
if (SubCategories.Count == 0) if (SubCategories?.Count == 0)
{ {
CategoryTapped?.Invoke(this, category); CategoryTapped?.Invoke(this, category);
await Navigation.PopAsync(); await Navigation.PopAsync();

View File

@ -1,5 +1,4 @@
using Billing.Models; using Billing.UI;
using Billing.UI;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -9,7 +8,7 @@ namespace Billing.Views
{ {
public partial class IconSelectPage : BillingPage public partial class IconSelectPage : BillingPage
{ {
public static readonly BindableProperty IconsSourceProperty = BindableProperty.Create(nameof(IconsSource), typeof(IList<BillingIcon>), typeof(IconSelectPage)); public static readonly BindableProperty IconsSourceProperty = Helper.Create<IList<BillingIcon>, IconSelectPage>(nameof(IconsSource));
public IList<BillingIcon> IconsSource public IList<BillingIcon> IconsSource
{ {
@ -37,7 +36,7 @@ namespace Billing.Views
{ {
var source = new List<BillingIcon> var source = new List<BillingIcon>
{ {
new() { Icon = BaseModel.ICON_DEFAULT }, new() { Icon = Definition.DefaultIcon },
new() { Icon = "wallet" }, new() { Icon = "wallet" },
new() { Icon = "dollar" }, new() { Icon = "dollar" },
new() { Icon = "creditcard" }, new() { Icon = "creditcard" },
@ -67,6 +66,7 @@ namespace Billing.Views
new() { Icon = "taxi" }, new() { Icon = "taxi" },
new() { Icon = "fitness" }, new() { Icon = "fitness" },
new() { Icon = "party" }, new() { Icon = "party" },
new() { Icon = "share" },
}; };
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)
@ -103,7 +103,7 @@ namespace Billing.Views
public class BillingIcon : BindableObject public class BillingIcon : BindableObject
{ {
public static readonly BindableProperty IsCheckedProperty = BindableProperty.Create(nameof(IsChecked), typeof(bool), typeof(BillingIcon)); public static readonly BindableProperty IsCheckedProperty = Helper.Create<bool, BillingIcon>(nameof(IsChecked));
public bool IsChecked public bool IsChecked
{ {

View File

@ -4,6 +4,7 @@
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:chart="clr-namespace:Microcharts.Forms;assembly=Microcharts.Forms" xmlns:chart="clr-namespace:Microcharts.Forms;assembly=Microcharts.Forms"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Billing.Views.RankPage" x:Class="Billing.Views.RankPage"
x:Name="rankPage" x:Name="rankPage"
@ -12,31 +13,29 @@
Shell.TabBarIsVisible="True"> Shell.TabBarIsVisible="True">
<Shell.TitleView> <Shell.TitleView>
<Grid> <Grid ColumnSpacing="10" ColumnDefinitions="30, *, 30">
<StackLayout Margin="30, 0, 0, 0" HorizontalOptions="Center" VerticalOptions="Center" <ui:TintImageButton Source="left.png" WidthRequest="20" HeightRequest="20"
Orientation="Horizontal" Spacing="10"> VerticalOptions="Center" HorizontalOptions="Center"
<ui:TintImageButton Source="left.png" WidthRequest="20" HeightRequest="20" Command="{Binding LeftCommand}"/>
VerticalOptions="Center" HorizontalOptions="Start" <Label Grid.Column="1" Text="{Binding Title}"
Command="{Binding LeftCommand}"/> TextColor="{DynamicResource PrimaryColor}"
<Label Text="{Binding Title}" FontSize="{OnPlatform Android=20, iOS=18}"
TextColor="{DynamicResource PrimaryColor}" FontFamily="{x:Static ui:Definition.SemiBoldFontFamily}"
FontSize="{OnPlatform Android=20, iOS=18}"> VerticalOptions="Center"
<Label.FontFamily> HorizontalOptions="Center">
<OnPlatform x:TypeArguments="x:String" <Label.GestureRecognizers>
Android="OpenSans-SemiBold.ttf#OpenSans-SemiBold" <TapGestureRecognizer Command="{Binding FilterCommand}"/>
iOS="OpenSans-Bold"/> </Label.GestureRecognizers>
</Label.FontFamily> </Label>
</Label> <ui:TintImageButton Grid.Column="2" Source="right.png" WidthRequest="20" HeightRequest="20"
<ui:TintImageButton Source="right.png" WidthRequest="20" HeightRequest="20" VerticalOptions="Center" HorizontalOptions="Center"
VerticalOptions="Center" HorizontalOptions="End" Command="{Binding RightCommand}"/>
Command="{Binding RightCommand}"/>
</StackLayout>
</Grid> </Grid>
</Shell.TitleView> </Shell.TitleView>
<ContentPage.ToolbarItems> <!--<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" IconImageSource="filter.png" Command="{Binding FilterCommand}"/> <ToolbarItem Order="Primary" IconImageSource="filter.png" Command="{Binding FilterCommand}"/>
</ContentPage.ToolbarItems> </ContentPage.ToolbarItems>-->
<ContentPage.Resources> <ContentPage.Resources>
<ui:NegativeConverter x:Key="negativeConverter"/> <ui:NegativeConverter x:Key="negativeConverter"/>
@ -45,10 +44,11 @@
<ui:BalanceColorConverter x:Key="colorConverter"/> <ui:BalanceColorConverter x:Key="colorConverter"/>
<ui:TimeConverter x:Key="timeConverter" IncludeDate="True"/> <ui:TimeConverter x:Key="timeConverter" IncludeDate="True"/>
<ui:IconConverter x:Key="iconConverter"/> <ui:IconConverter x:Key="iconConverter"/>
<ui:TintColorConverter x:Key="tintColorConverter"/>
<Style x:Key="titleLabel" TargetType="Label"> <Style x:Key="titleLabel" TargetType="Label">
<Setter Property="FontSize" Value="16"/> <Setter Property="FontSize" Value="16"/>
<Setter Property="Margin" Value="10, 20, 10, 10"/> <Setter Property="Margin" Value="10, 20, 10, 10"/>
<!--<Setter Property="TextColor" Value="{DynamicResource SecondaryTextColor}"/>--> <Setter Property="TextColor" Value="{DynamicResource TextColor}"/>
</Style> </Style>
<Style x:Key="promptLabel" TargetType="Label"> <Style x:Key="promptLabel" TargetType="Label">
<Setter Property="HeightRequest" Value="240"/> <Setter Property="HeightRequest" Value="240"/>
@ -58,12 +58,16 @@
<Setter Property="VerticalTextAlignment" Value="Center"/> <Setter Property="VerticalTextAlignment" Value="Center"/>
<Setter Property="TextColor" Value="{DynamicResource SecondaryTextColor}"/> <Setter Property="TextColor" Value="{DynamicResource SecondaryTextColor}"/>
</Style> </Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{DynamicResource TextColor}"/>
<Setter Property="FontSize" Value="14"/>
</Style>
</ContentPage.Resources> </ContentPage.Resources>
<Grid> <Grid>
<ScrollView x:Name="scroller"> <ScrollView x:Name="scroller" Scrolled="Scroller_Scrolled">
<StackLayout> <StackLayout>
<Grid Margin="0, 10, 0, 0" Padding="8" ColumnSpacing="8" ColumnDefinitions="*, Auto" HeightRequest="24" <Grid Padding="8" ColumnSpacing="8" ColumnDefinitions="*, Auto" HeightRequest="24"
BackgroundColor="{DynamicResource PromptBackgroundColor}"> BackgroundColor="{DynamicResource PromptBackgroundColor}">
<StackLayout Grid.Column="1" Orientation="Horizontal" Spacing="6"> <StackLayout Grid.Column="1" Orientation="Horizontal" Spacing="6">
<Label Text="{r:Text Income}" TextColor="{DynamicResource GreenColor}" <Label Text="{r:Text Income}" TextColor="{DynamicResource GreenColor}"
@ -108,6 +112,7 @@
CommandParameter="{Binding .}"/> CommandParameter="{Binding .}"/>
</Grid.GestureRecognizers> </Grid.GestureRecognizers>
<ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}" <ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}"
ui:TintHelper.TintColor="{Binding TintColor, Converter={StaticResource tintColorConverter}}"
WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/> WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/>
<Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}" <Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}"
VerticalOptions="Center" VerticalOptions="Center"
@ -133,8 +138,8 @@
<ui:BlurryPanel x:Name="panelFilter" VerticalOptions="Start" Opacity="0" <ui:BlurryPanel x:Name="panelFilter" VerticalOptions="Start" Opacity="0"
BackgroundColor="{DynamicResource WindowBackgroundColor}" BackgroundColor="{DynamicResource WindowBackgroundColor}"
HeightRequest="{Binding Height, Source={x:Reference gridFilter}}"/> HeightRequest="{Binding Height, Source={x:Reference gridFilter}}"/>
<Grid x:Name="gridFilter" VerticalOptions="Start" Opacity="0" Padding="10"> <Grid x:Name="gridFilter" VerticalOptions="Start" Opacity="0" RowDefinitions="Auto, Auto, Auto, Auto">
<ui:SegmentedControl Margin="6, 6, 6, 3" VerticalOptions="Center" <ui:SegmentedControl VerticalOptions="Center" Margin="10, 10, 10, 3"
SelectedSegmentIndex="{Binding SegmentType, Mode=TwoWay}" SelectedSegmentIndex="{Binding SegmentType, Mode=TwoWay}"
SelectedTextColor="{DynamicResource TextColor}" SelectedTextColor="{DynamicResource TextColor}"
TintColor="{DynamicResource PromptBackgroundColor}"> TintColor="{DynamicResource PromptBackgroundColor}">
@ -143,6 +148,26 @@
<ui:SegmentedControlOption Text="{r:Text Income}"/> <ui:SegmentedControlOption Text="{r:Text Income}"/>
</ui:SegmentedControl.Children> </ui:SegmentedControl.Children>
</ui:SegmentedControl> </ui:SegmentedControl>
<ui:OptionPicker Grid.Row="1" VerticalOptions="Center" Margin="10, 3"
HorizontalTextAlignment="Center" FontSize="16"
ItemsSource="{Binding DateTypes}"
SelectedIndex="{Binding SegmentDate, Mode=TwoWay}"
TextColor="{DynamicResource TextColor}"
ios:Picker.UpdateMode="WhenFinished"/>
<Grid Grid.Row="2" ColumnDefinitions="*, Auto, *" Margin="10, 3">
<ui:OptionDatePicker Date="{Binding StartPickerDate, Mode=TwoWay}"
FontSize="16" TextColor="{DynamicResource TextColor}"
VerticalOptions="Center"
ios:DatePicker.UpdateMode="WhenFinished"/>
<Label Grid.Column="1" Text="{r:Text To}" TextColor="{DynamicResource SecondaryTextColor}"
VerticalOptions="Center"/>
<ui:OptionDatePicker Grid.Column="2" Date="{Binding EndPickerDate, Mode=TwoWay}"
FontSize="16" TextColor="{DynamicResource TextColor}"
VerticalOptions="Center"
ios:DatePicker.UpdateMode="WhenFinished"/>
</Grid>
<Grid Grid.Row="3" HeightRequest="1" BackgroundColor="{DynamicResource PromptBackgroundColor}"
IsVisible="{OnPlatform iOS=False}"/>
</Grid> </Grid>
</Grid> </Grid>
</ui:BillingPage> </ui:BillingPage>

View File

@ -1,4 +1,5 @@
using Billing.Models; using Billing.Models;
using Billing.Store;
using Billing.Themes; using Billing.Themes;
using Billing.UI; using Billing.UI;
using Microcharts; using Microcharts;
@ -14,9 +15,39 @@ using Resource = Billing.Languages.Resource;
namespace Billing.Views namespace Billing.Views
{ {
public enum DateType : int
{
Custom = 0,
Monthly,
Today,
PastMonth,
PastQuarter,
PastSixMonths,
PastYear,
Total
}
public partial class RankPage : BillingPage public partial class RankPage : BillingPage
{ {
private static RankPage instance;
public static RankPage Instance => instance;
private static readonly DateTime today = DateTime.Today;
private static readonly BindableProperty SegmentTypeProperty = Helper.Create<int, RankPage>(nameof(SegmentType), defaultValue: 0, propertyChanged: OnSegmentTypeChanged); private static readonly BindableProperty SegmentTypeProperty = Helper.Create<int, RankPage>(nameof(SegmentType), defaultValue: 0, propertyChanged: OnSegmentTypeChanged);
private static readonly BindableProperty SegmentDateProperty = Helper.Create<int, RankPage>(nameof(SegmentDate), defaultValue: 1, propertyChanged: OnSegmentDateChanged);
private static readonly BindableProperty StartDateProperty = Helper.Create<DateTime, RankPage>(nameof(StartDate),
defaultValue: today.AddDays(1 - today.Day),
propertyChanged: OnDateChanged);
private static readonly BindableProperty EndDateProperty = Helper.Create<DateTime, RankPage>(nameof(EndDate),
defaultValue: today.AddDays(DateTime.DaysInMonth(today.Year, today.Month) - today.Day).LastMoment(),
propertyChanged: OnDateChanged);
private static readonly BindableProperty StartPickerDateProperty = Helper.Create<DateTime, RankPage>(nameof(StartPickerDate),
defaultValue: today.AddDays(1 - today.Day),
propertyChanged: OnPickerStartDateChanged);
private static readonly BindableProperty EndPickerDateProperty = Helper.Create<DateTime, RankPage>(nameof(EndPickerDate),
defaultValue: today.AddDays(DateTime.DaysInMonth(today.Year, today.Month) - today.Day),
propertyChanged: OnPickerEndDateChanged);
private static readonly BindableProperty ChartProperty = Helper.Create<Chart, RankPage>(nameof(Chart)); private static readonly BindableProperty ChartProperty = Helper.Create<Chart, RankPage>(nameof(Chart));
private static readonly BindableProperty CategoryChartProperty = Helper.Create<Chart, RankPage>(nameof(CategoryChart)); private static readonly BindableProperty CategoryChartProperty = Helper.Create<Chart, RankPage>(nameof(CategoryChart));
private static readonly BindableProperty TopBillsProperty = Helper.Create<IList<UIBill>, RankPage>(nameof(TopBills)); private static readonly BindableProperty TopBillsProperty = Helper.Create<IList<UIBill>, RankPage>(nameof(TopBills));
@ -34,8 +65,40 @@ namespace Billing.Views
1 => CategoryType.Income, 1 => CategoryType.Income,
_ => CategoryType.Spending _ => CategoryType.Spending
}; };
page.OnFilterCommand(false); page.LoadData();
page.SetMonth(page.current); }
private static void OnSegmentDateChanged(RankPage page, int old, int @new)
{
page.OnDateTypeCommand((DateType)@new);
}
private static void OnDateChanged(RankPage page, DateTime old = default, DateTime @new = default)
{
page.isLocked = true;
page.StartPickerDate = page.StartDate.Date;
page.EndPickerDate = page.EndDate.Date;
page.isLocked = false;
if (!page.isFreezed)
{
var format = Resource.TitleShortDateFormat;
page.Title = page.StartDate.ToString(format) + " ~ " + page.EndDate.ToString(format);
page.LoadData();
}
}
private static void OnPickerStartDateChanged(RankPage page, DateTime _, DateTime @new)
{
if (!page.isLocked)
{
page.SegmentDate = 0;
page.StartDate = @new.Date;
}
}
private static void OnPickerEndDateChanged(RankPage page, DateTime _, DateTime @new)
{
if (!page.isLocked)
{
page.SegmentDate = 0;
page.EndDate = @new.Date.LastMoment();
}
} }
public int SegmentType public int SegmentType
@ -43,6 +106,31 @@ namespace Billing.Views
get => (int)GetValue(SegmentTypeProperty); get => (int)GetValue(SegmentTypeProperty);
set => SetValue(SegmentTypeProperty, value); set => SetValue(SegmentTypeProperty, value);
} }
public int SegmentDate
{
get => (int)GetValue(SegmentDateProperty);
set => SetValue(SegmentDateProperty, value);
}
public DateTime StartDate
{
get => (DateTime)GetValue(StartDateProperty);
set => SetValue(StartDateProperty, value);
}
public DateTime EndDate
{
get => (DateTime)GetValue(EndDateProperty);
set => SetValue(EndDateProperty, value);
}
public DateTime StartPickerDate
{
get => (DateTime)GetValue(StartPickerDateProperty);
set => SetValue(StartPickerDateProperty, value);
}
public DateTime EndPickerDate
{
get => (DateTime)GetValue(EndPickerDateProperty);
set => SetValue(EndPickerDateProperty, value);
}
public Chart Chart public Chart Chart
{ {
get => (Chart)GetValue(ChartProperty); get => (Chart)GetValue(ChartProperty);
@ -77,59 +165,262 @@ namespace Billing.Views
public decimal Spending => (decimal)GetValue(SpendingProperty); public decimal Spending => (decimal)GetValue(SpendingProperty);
public decimal Balance => (decimal)GetValue(BalanceProperty); public decimal Balance => (decimal)GetValue(BalanceProperty);
public List<string> DateTypes { get; }
public Command LeftCommand { get; } public Command LeftCommand { get; }
public Command RightCommand { get; } public Command RightCommand { get; }
public Command FilterCommand { get; } public Command FilterCommand { get; }
public Command EditBilling { get; } public Command EditBilling { get; }
private DateTime current;
private DateTime end;
private IEnumerable<Bill> bills; private IEnumerable<Bill> bills;
private CategoryType type = CategoryType.Spending; private CategoryType type = CategoryType.Spending;
private bool isFilterToggled; private bool isFilterToggled;
private bool isFreezed;
private bool isLocked;
private bool needRefresh = true;
private const int FILTER_HEIGHT = 100;
private readonly SKTypeface font; private readonly SKTypeface font;
public RankPage() public RankPage()
{ {
instance = this;
LeftCommand = new Command(OnLeftCommand); LeftCommand = new Command(OnLeftCommand);
RightCommand = new Command(OnRightCommand); RightCommand = new Command(OnRightCommand);
FilterCommand = new Command(OnFilterCommand); FilterCommand = new Command(OnFilterCommand);
EditBilling = new Command(OnEditBilling); EditBilling = new Command(OnEditBilling);
var style = SKFontManager.Default.GetFontStyles("PingFang SC"); #if __IOS__
var style = SKFontManager.Default.GetFontStyles("PingFang SC");
if (style != null) if (style != null)
{ {
font = style.CreateTypeface(SKFontStyle.Normal); font = style.CreateTypeface(SKFontStyle.Normal);
} }
else
#endif
font = SKFontManager.Default.MatchCharacter(0x4e00);
DateTypes = new List<string>
{
Resource.Custom,
Resource.Monthly,
Resource.Today,
Resource.PastMonth,
Resource.PastQuarter,
Resource.PastSixMonths,
Resource.PastYear,
Resource.Total
};
InitializeComponent(); InitializeComponent();
gridFilter.TranslationY = -60; gridFilter.TranslationY = -FILTER_HEIGHT;
panelFilter.TranslationY = -60; panelFilter.TranslationY = -FILTER_HEIGHT;
} }
public override void OnLoaded() public void SetNeedRefresh()
{ {
SetMonth(DateTime.Today); needRefresh = true;
}
protected override void OnAppearing()
{
if (needRefresh)
{
needRefresh = false;
OnDateChanged(this);
}
}
protected override void OnRefresh()
{
OnDateChanged(this);
}
private void OnDateTypeCommand(DateType index)
{
if (index < DateType.Monthly || index > DateType.Total)
{
return;
}
if (scroller.ScrollY > 0)
{
scroller.ScrollToAsync(0, 0, true);
}
isFreezed = true;
var today = DateTime.Today;
switch (index)
{
case DateType.Monthly:
StartDate = today.AddDays(1 - today.Day);
EndDate = today.AddDays(DateTime.DaysInMonth(today.Year, today.Month) - today.Day).LastMoment();
break;
case DateType.Today:
StartDate = today;
EndDate = today.LastMoment();
break;
case DateType.PastMonth:
StartDate = today.AddMonths(-1).AddDays(1);
EndDate = today.LastMoment();
break;
case DateType.PastQuarter:
StartDate = today.AddMonths(-3).AddDays(1);
EndDate = today.LastMoment();
break;
case DateType.PastSixMonths:
StartDate = today.AddMonths(-6).AddDays(1);
EndDate = today.LastMoment();
break;
case DateType.PastYear:
StartDate = today.AddYears(-1).AddDays(1);
EndDate = today.LastMoment();
break;
case DateType.Total:
//StartDate = App.Bills.Min(b => b.CreateTime).Date;
//EndDate = App.Bills.Max(b => b.CreateTime).Date.LastMoment();
DateTime min = DateTime.MaxValue;
DateTime max = DateTime.MinValue;
App.Bills.ForEach(b =>
{
if (b.CreateTime < min)
{
min = b.CreateTime;
}
if (b.CreateTime > max)
{
max = b.CreateTime;
}
});
if (min == DateTime.MaxValue && max == DateTime.MinValue)
{
return;
}
StartDate = min.Date;
EndDate = max.Date.LastMoment();
break;
}
isFreezed = false;
OnDateChanged(this);
}
private bool IsPreset(DateTime start, DateTime end)
{
return start.Month == end.Month &&
start.Day == 1 &&
end.Day == DateTime.DaysInMonth(end.Year, end.Month);
} }
private void OnLeftCommand() private void OnLeftCommand()
{ {
var type = (DateType)SegmentDate;
if (type < DateType.Monthly || type >= DateType.Total)
{
return;
}
if (scroller.ScrollY > 0) if (scroller.ScrollY > 0)
{ {
scroller.ScrollToAsync(0, 0, true); scroller.ScrollToAsync(0, 0, true);
} }
SetMonth(current.AddMonths(-1)); isFreezed = true;
var start = StartDate;
var end = EndDate;
if (type == DateType.Monthly || IsPreset(start, end))
{
start = start.AddMonths(-1);
end = start.AddDays(DateTime.DaysInMonth(start.Year, start.Month) - 1).LastMoment();
}
else if (type == DateType.PastMonth)
{
start = start.AddMonths(-1);
end = end.AddMonths(-1);
}
else if (type == DateType.PastQuarter)
{
start = start.AddMonths(-3);
end = end.AddMonths(-3);
}
else if (type == DateType.PastSixMonths)
{
start = start.AddMonths(-6);
end = end.AddMonths(-6);
}
else if (type == DateType.PastYear)
{
start = start.AddYears(-1);
end = end.AddYears(-1);
}
else
{
var days = (end.Date - start.Date).TotalDays + 1;
start = start.AddDays(-days);
end = end.AddDays(-days);
}
if (start.Year < 1900)
{
isFreezed = false;
return;
}
StartDate = start;
EndDate = end;
isFreezed = false;
OnDateChanged(this);
} }
private void OnRightCommand() private void OnRightCommand()
{ {
var type = (DateType)SegmentDate;
if (type < DateType.Monthly || type >= DateType.Total)
{
return;
}
if (scroller.ScrollY > 0) if (scroller.ScrollY > 0)
{ {
scroller.ScrollToAsync(0, 0, true); scroller.ScrollToAsync(0, 0, true);
} }
SetMonth(current.AddMonths(1)); isFreezed = true;
var start = StartDate;
var end = EndDate;
if (type == DateType.Monthly || IsPreset(start, end))
{
start = start.AddMonths(1);
end = start.AddDays(DateTime.DaysInMonth(start.Year, start.Month) - 1).LastMoment();
}
else if (type == DateType.PastMonth)
{
start = start.AddMonths(1);
end = end.AddMonths(1);
}
else if (type == DateType.PastQuarter)
{
start = start.AddMonths(3);
end = end.AddMonths(3);
}
else if (type == DateType.PastSixMonths)
{
start = start.AddMonths(6);
end = end.AddMonths(6);
}
else if (type == DateType.PastYear)
{
start = start.AddYears(1);
end = end.AddYears(1);
}
else
{
var days = (end.Date - start.Date).TotalDays + 1;
start = start.AddDays(days);
end = end.AddDays(days);
}
if (end.Year > DateTime.Today.Year + 100)
{
isFreezed = false;
return;
}
StartDate = start;
EndDate = end;
isFreezed = false;
OnDateChanged(this);
} }
private async void OnFilterCommand(object o) private async void OnFilterCommand(object o)
@ -146,6 +437,7 @@ namespace Billing.Views
ViewExtensions.CancelAnimations(panelFilter); ViewExtensions.CancelAnimations(panelFilter);
if (isFilterToggled) if (isFilterToggled)
{ {
await scroller.ScrollToAsync(scroller.ScrollX, scroller.ScrollY, false);
await Task.WhenAll( await Task.WhenAll(
gridFilter.TranslateTo(0, 0, easing: Easing.CubicOut), gridFilter.TranslateTo(0, 0, easing: Easing.CubicOut),
gridFilter.FadeTo(1, easing: Easing.CubicOut), gridFilter.FadeTo(1, easing: Easing.CubicOut),
@ -155,9 +447,9 @@ namespace Billing.Views
else else
{ {
await Task.WhenAll( await Task.WhenAll(
gridFilter.TranslateTo(0, -60, easing: Easing.CubicIn), gridFilter.TranslateTo(0, -FILTER_HEIGHT, easing: Easing.CubicIn),
gridFilter.FadeTo(0, easing: Easing.CubicIn), gridFilter.FadeTo(0, easing: Easing.CubicIn),
panelFilter.TranslateTo(0, -60, easing: Easing.CubicIn), panelFilter.TranslateTo(0, -FILTER_HEIGHT, easing: Easing.CubicIn),
panelFilter.FadeTo(0, easing: Easing.CubicIn)); panelFilter.FadeTo(0, easing: Easing.CubicIn));
} }
} }
@ -179,18 +471,9 @@ namespace Billing.Views
} }
} }
private void UpdateBill(UIBill bill) private async void RefreshBalance(DateTime start, DateTime end)
{
bill.Icon = App.Categories.FirstOrDefault(c => c.Id == bill.Bill.CategoryId)?.Icon ?? BaseModel.ICON_DEFAULT;
bill.Name = bill.Bill.Name;
bill.DateCreation = bill.Bill.CreateTime;
bill.Amount = bill.Bill.Amount;
bill.Wallet = App.Accounts.FirstOrDefault(a => a.Id == bill.Bill.WalletId)?.Name;
}
private async void RefreshBalance()
{ {
var bills = await Task.Run(() => App.Bills.Where(b => b.CreateTime >= current && b.CreateTime <= end)); var bills = await Task.Run(() => App.Bills.Where(b => b.CreateTime >= start && b.CreateTime <= end));
var income = bills.Where(b => b.Amount > 0).Sum(b => b.Amount); var income = bills.Where(b => b.Amount > 0).Sum(b => b.Amount);
var spending = -bills.Where(b => b.Amount < 0).Sum(b => b.Amount); var spending = -bills.Where(b => b.Amount < 0).Sum(b => b.Amount);
SetValue(IncomeProperty, income); SetValue(IncomeProperty, income);
@ -198,43 +481,33 @@ namespace Billing.Views
SetValue(BalanceProperty, income - spending); SetValue(BalanceProperty, income - spending);
} }
private void OnBillChecked(object sender, Bill e) private async void OnBillChecked(object sender, Bill e)
{ {
var bill = TopBills.FirstOrDefault(b => b.Bill == e); await StoreHelper.SaveBillItemAsync(e);
if (bill != null) LoadData();
{
UpdateBill(bill);
}
RefreshBalance();
Task.Run(App.WriteBills);
} }
private async void SetMonth(DateTime date) private async void LoadData()
{ {
current = date.AddDays(1 - date.Day); var start = StartDate;
end = current.AddDays(DateTime.DaysInMonth(current.Year, current.Month)); var end = EndDate;
var format = Resource.DateRangeFormat;
Title = current.ToString(format) + " ~ " + end.AddDays(-1).ToString(format);
var spending = type == CategoryType.Spending; var spending = type == CategoryType.Spending;
bills = await Task.Run(() => App.Bills.Where(b => (b.Amount > 0 ^ spending) && b.CreateTime >= current && b.CreateTime <= end)); bills = await Task.Run(() => App.Bills.Where(b => (b.Amount > 0 ^ spending) && b.CreateTime >= start && b.CreateTime <= end));
var primaryColor = BaseTheme.CurrentPrimaryColor.ToSKColor(); var primaryColor = BaseTheme.CurrentPrimaryColor.ToSKColor();
var textColor = BaseTheme.CurrentSecondaryTextColor.ToSKColor(); var textColor = BaseTheme.CurrentSecondaryTextColor.ToSKColor();
_ = Task.Run(() => LoadReportChart(primaryColor, textColor)); _ = Task.Run(() => LoadReportChart(primaryColor, textColor, start, end));
_ = Task.Run(() => LoadCategoryChart(primaryColor, textColor)); _ = Task.Run(() => LoadCategoryChart(primaryColor, textColor));
_ = Task.Run(LoadTopBills); _ = Task.Run(LoadTopBills);
RefreshBalance(); RefreshBalance(start, end);
} }
private void LoadReportChart(SKColor primaryColor, SKColor textColor) private void LoadReportChart(SKColor primaryColor, SKColor textColor, DateTime start, DateTime end)
{ {
var entries = new List<ChartEntry>(); var entries = new List<ChartEntry>();
for (var day = current; day <= end; day = day.AddDays(1)) for (var day = start; day <= end; day = day.AddDays(1))
{ {
var daybills = bills.Where(b => Helper.IsSameDay(b.CreateTime, day)); var daybills = bills.Where(b => Helper.IsSameDay(b.CreateTime, day));
decimal amount = Math.Abs(daybills.Sum(b => b.Amount)); decimal amount = Math.Abs(daybills.Sum(b => b.Amount));
@ -350,5 +623,13 @@ namespace Billing.Views
} }
}); });
} }
private void Scroller_Scrolled(object sender, ScrolledEventArgs e)
{
if (isFilterToggled)
{
OnFilterCommand(false);
}
}
} }
} }

View File

@ -11,18 +11,24 @@
BindingContext="{x:Reference settingPage}" BindingContext="{x:Reference settingPage}"
Shell.TabBarIsVisible="True"> Shell.TabBarIsVisible="True">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" IconImageSource="share.png" Command="{Binding ShareCommand}"/>
</ContentPage.ToolbarItems>
<TableView Intent="Settings" HasUnevenRows="True"> <TableView Intent="Settings" HasUnevenRows="True">
<TableSection Title="{r:Text About}"> <TableSection Title="{r:Text About}">
<ui:OptionTextCell Height="36" Title="{r:Text Version}" <ui:OptionTextCell Height="44" Title="{r:Text Version}"
Detail="{Binding Version}"/> Detail="{Binding Version}"/>
</TableSection> </TableSection>
<TableSection Title="{r:Text Feature}"> <TableSection Title="{r:Text Feature}">
<ui:OptionSelectCell Height="36" Title="{r:Text CategoryManage}" <ui:OptionSelectCell Height="44" Title="{r:Text CategoryManage}"
Detail="{r:Text Detail}" Detail="{r:Text Detail}"
Command="{Binding CategoryCommand}"/> Command="{Binding CategoryCommand}"/>
<ui:OptionSwitchCell Height="44" Title="{r:Text SaveLocation}"
IsToggled="{Binding SaveLocation, Mode=TwoWay}"/>
</TableSection> </TableSection>
<TableSection Title="{r:Text Preference}"> <TableSection Title="{r:Text Preference}">
<ui:OptionEntryCell Height="36" Title="{r:Text PrimaryColor}" <ui:OptionEntryCell Height="44" Title="{r:Text PrimaryColor}"
Text="{Binding PrimaryColor, Mode=TwoWay}" Text="{Binding PrimaryColor, Mode=TwoWay}"
Keyboard="Text"/> Keyboard="Text"/>
<ViewCell Height="120"> <ViewCell Height="120">
@ -31,9 +37,14 @@
<!--<Label Text="" LineBreakMode="TailTruncation" <!--<Label Text="" LineBreakMode="TailTruncation"
VerticalOptions="Center" VerticalOptions="Center"
TextColor="{DynamicResource TextColor}"/>--> TextColor="{DynamicResource TextColor}"/>-->
<ui:ColorPicker Grid.Column="1" ColorChanged="ColorPicker_ColorChanged"/> <ui:ColorPicker Grid.Column="1" Command="{Binding ColorPickerCommand}"/>
</Grid> </Grid>
</ViewCell> </ViewCell>
</TableSection> </TableSection>
<TableSection Title="{r:Text Diagnostic}">
<ui:OptionSelectCell Height="44" Title="{r:Text ShareLogs}"
Detail="{Binding ManyRecords}"
Command="{Binding ShareLogsCommand}"/>
</TableSection>
</TableView> </TableView>
</ui:BillingPage> </ui:BillingPage>

View File

@ -1,50 +1,89 @@
using System.IO;
using Billing.Store;
using Billing.Themes; using Billing.Themes;
using Billing.UI; using Billing.UI;
using System.Globalization;
using Xamarin.Essentials; using Xamarin.Essentials;
using Xamarin.Forms; using Xamarin.Forms;
using Resource = Billing.Languages.Resource;
namespace Billing.Views namespace Billing.Views
{ {
public partial class SettingPage : BillingPage public partial class SettingPage : BillingPage
{ {
private static readonly BindableProperty VersionProperty = BindableProperty.Create(nameof(Version), typeof(string), typeof(SettingPage)); private static readonly BindableProperty VersionProperty = Helper.Create<string, SettingPage>(nameof(Version));
private static readonly BindableProperty PrimaryColorProperty = BindableProperty.Create(nameof(PrimaryColor), typeof(string), typeof(SettingPage)); private static readonly BindableProperty SaveLocationProperty = Helper.Create<bool, SettingPage>(nameof(SaveLocation));
private static readonly BindableProperty PrimaryColorProperty = Helper.Create<string, SettingPage>(nameof(PrimaryColor));
private static readonly BindableProperty ManyRecordsProperty = Helper.Create<string, SettingPage>(nameof(ManyRecords));
public string Version => (string)GetValue(VersionProperty); public string Version => (string)GetValue(VersionProperty);
public bool SaveLocation
{
get => (bool)GetValue(SaveLocationProperty);
set => SetValue(SaveLocationProperty, value);
}
public string PrimaryColor public string PrimaryColor
{ {
get => (string)GetValue(PrimaryColorProperty); get => (string)GetValue(PrimaryColorProperty);
set => SetValue(PrimaryColorProperty, value); set => SetValue(PrimaryColorProperty, value);
} }
public string ManyRecords => (string)GetValue(ManyRecordsProperty);
public Command ShareCommand { get; }
public Command CategoryCommand { get; } public Command CategoryCommand { get; }
public Command ColorPickerCommand { get; }
public Command ShareLogsCommand { get; }
public SettingPage() public SettingPage()
{ {
ShareCommand = new Command(OnShareCommand);
CategoryCommand = new Command(OnCategoryCommand); CategoryCommand = new Command(OnCategoryCommand);
ColorPickerCommand = new Command(OnColorPickerCommand);
ShareLogsCommand = new Command(OnShareLogsCommand);
InitializeComponent(); InitializeComponent();
var (main, build) = Definition.GetVersion(); SetValue(VersionProperty, $"{AppInfo.VersionString} ({AppInfo.BuildString})");
SetValue(VersionProperty, $"{main} ({build})");
} }
protected override void OnAppearing() protected override async void OnAppearing()
{ {
base.OnAppearing(); base.OnAppearing();
//SetValue(VersionProperty, $"{AppInfo.VersionString} ({AppInfo.BuildString})"); SaveLocation = App.SaveLocation;
var colorString = Preferences.Get(Definition.PrimaryColorKey, Helper.DEFAULT_COLOR); var colorString = Preferences.Get(Definition.PrimaryColorKey, Helper.DEFAULT_COLOR);
PrimaryColor = Helper.WrapColorString(colorString); PrimaryColor = Helper.WrapColorString(colorString);
var count = await StoreHelper.GetLogsCount();
SetValue(ManyRecordsProperty, string.Format(Resource.ManyRecords, count));
} }
protected override void OnDisappearing() protected override void OnDisappearing()
{ {
base.OnDisappearing(); base.OnDisappearing();
var color = PrimaryColor; App.SetSaveLocation(SaveLocation);
Preferences.Set(Definition.PrimaryColorKey, color); Preferences.Set(Definition.SaveLocationKey, SaveLocation);
Light.Instance.RefreshColor(Color.FromHex(color)); Preferences.Set(Definition.PrimaryColorKey, PrimaryColor);
}
protected override async void OnRefresh()
{
var count = await StoreHelper.GetLogsCount();
SetValue(ManyRecordsProperty, string.Format(Resource.ManyRecords, count));
}
private async void OnShareCommand()
{
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
await Share.RequestAsync(new ShareFileRequest
{
File = new ShareFile(StoreHelper.DatabasePath, "application/vnd.sqlite3")
});
}
} }
private async void OnCategoryCommand() private async void OnCategoryCommand()
@ -60,9 +99,73 @@ namespace Billing.Views
} }
} }
private void ColorPicker_ColorChanged(object sender, Color e) private void OnColorPickerCommand(object o)
{ {
PrimaryColor = Helper.WrapColorString(e.ToHex()); if (o is Color color)
{
PrimaryColor = Helper.WrapColorString(color.ToHex());
Light.Instance.RefreshColor(color);
}
}
private async void OnShareLogsCommand()
{
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
string file;
var count = await StoreHelper.GetLogsCount();
if (count > 0)
{
file = await StoreHelper.ExportLogs();
}
else
{
file = StoreHelper.GetLogFile();
}
if (file != null && File.Exists(file))
{
#if __IOS__
var sendEmail = Resource.SendEmail;
var shareLogs = Resource.ShareLogs;
var result = await DisplayActionSheet(Resource.HowToShareDiagnostic, Resource.Cancel, null, sendEmail, shareLogs);
if (result == sendEmail)
{
try
{
await Email.ComposeAsync(new EmailMessage
{
To = { "tsorgy@gmail.com " },
Subject = Resource.ShareLogs,
Attachments =
{
new(file, "text/csv")
}
});
}
catch (System.Exception ex)
{
Helper.Error("email.send", ex);
}
}
else if (result == shareLogs)
{
await Share.RequestAsync(new ShareFileRequest
{
File = new ShareFile(file, "text/csv")
});
}
#else
await Share.RequestAsync(new ShareFileRequest
{
File = new ShareFile(file, "text/csv")
});
#endif
}
}
} }
} }
} }

View File

@ -0,0 +1,129 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Billing.Models;
using Billing.UI;
using Xamarin.Essentials;
using Xamarin.Forms;
using Xamarin.Forms.Maps;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
using Map = Xamarin.Forms.Maps.Map;
namespace Billing.Views
{
public class ViewLocationPage : BillingPage
{
public event EventHandler<Location> Synced;
private readonly Bill bill;
private CancellationTokenSource tokenSource;
public ViewLocationPage(Bill bill)
{
On<Xamarin.Forms.PlatformConfiguration.iOS>().SetUseSafeArea(false);
this.bill = bill;
Title = bill.Name;
ToolbarItems.Add(new ToolbarItem
{
IconImageSource = "location.png",
Order = ToolbarItemOrder.Primary,
Command = new Command(OnSynced)
});
if (bill.Latitude != null && bill.Longitude != null)
{
var (longitude, latitude) = (bill.Longitude.Value, bill.Latitude.Value).Wgs84ToGcj02();
var position = new Position(latitude, longitude);
var mapSpan = new MapSpan(position, 0.01, 0.01);
Content = new Map(mapSpan)
{
Pins =
{
new Pin
{
Label = bill.Name,
Type = PinType.Generic,
Position = position,
Address = bill.Store
}
}
};
}
}
protected override void OnDisappearing()
{
if (tokenSource != null && !tokenSource.IsCancellationRequested)
{
tokenSource.Cancel();
}
base.OnDisappearing();
}
private async void OnSynced()
{
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
if (tokenSource != null)
{
return;
}
var location = await GetCurrentLocation();
if (location != null)
{
Synced?.Invoke(this, location);
MainThread.BeginInvokeOnMainThread(() =>
{
var (longitude, latitude) = (location.Longitude, location.Latitude).Wgs84ToGcj02();
var position = new Position(latitude, longitude);
var mapSpan = new MapSpan(position, 0.01, 0.01);
Content = new Map(mapSpan)
{
Pins =
{
new Pin
{
Label = bill.Name,
Type = PinType.Generic,
Position = position,
Address = bill.Store
}
}
};
});
}
}
}
private async Task<Location> GetCurrentLocation()
{
try
{
var request = new GeolocationRequest(GeolocationAccuracy.Best, TimeSpan.FromSeconds(10));
tokenSource = new CancellationTokenSource();
var status = await Helper.CheckAndRequestPermissionAsync<Permissions.LocationWhenInUse>();
if (status != PermissionStatus.Granted)
{
return null;
}
return await Geolocation.GetLocationAsync(request, tokenSource.Token);
}
catch (FeatureNotSupportedException) { }
catch (FeatureNotEnabledException) { }
catch (PermissionException) { }
catch (Exception ex)
{
Helper.Error("location.get", ex);
}
tokenSource = null;
return null;
}
}
}

View File

@ -9,7 +9,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Billing.iOS", "Billing\Bill
EndProject EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Billing.Shared", "Billing.Shared\Billing.Shared.shproj", "{6AC75D01-70D6-4A07-8685-BC52AFD97A7A}" Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Billing.Shared", "Billing.Shared\Billing.Shared.shproj", "{6AC75D01-70D6-4A07-8685-BC52AFD97A7A}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svg2Png", "Svg2Png\Svg2Png.csproj", "{6A012FCA-3B1C-4593-ADD7-0751E5815C67}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svg2Png", "Svg2Png\Svg2Png.csproj", "{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}"
EndProject EndProject
Global Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution GlobalSection(SharedMSBuildProjectFiles) = preSolution
@ -62,18 +62,18 @@ Global
{5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator {5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
{5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator {5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
{5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Release|iPhoneSimulator.Deploy.0 = Release|iPhoneSimulator {5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Release|iPhoneSimulator.Deploy.0 = Release|iPhoneSimulator
{6A012FCA-3B1C-4593-ADD7-0751E5815C67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator
{6A012FCA-3B1C-4593-ADD7-0751E5815C67}.Debug|Any CPU.Build.0 = Debug|Any CPU {43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator
{6A012FCA-3B1C-4593-ADD7-0751E5815C67}.Debug|iPhone.ActiveCfg = Debug|Any CPU {43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|iPhone.ActiveCfg = Debug|iPhoneSimulator
{6A012FCA-3B1C-4593-ADD7-0751E5815C67}.Debug|iPhone.Build.0 = Debug|Any CPU {43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|iPhone.Build.0 = Debug|iPhoneSimulator
{6A012FCA-3B1C-4593-ADD7-0751E5815C67}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU {43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator
{6A012FCA-3B1C-4593-ADD7-0751E5815C67}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator
{6A012FCA-3B1C-4593-ADD7-0751E5815C67}.Release|Any CPU.ActiveCfg = Release|Any CPU {43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Release|Any CPU.ActiveCfg = Release|iPhoneSimulator
{6A012FCA-3B1C-4593-ADD7-0751E5815C67}.Release|Any CPU.Build.0 = Release|Any CPU {43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Release|Any CPU.Build.0 = Release|iPhoneSimulator
{6A012FCA-3B1C-4593-ADD7-0751E5815C67}.Release|iPhone.ActiveCfg = Release|Any CPU {43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Release|iPhone.ActiveCfg = Release|iPhoneSimulator
{6A012FCA-3B1C-4593-ADD7-0751E5815C67}.Release|iPhone.Build.0 = Release|Any CPU {43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Release|iPhone.Build.0 = Release|iPhoneSimulator
{6A012FCA-3B1C-4593-ADD7-0751E5815C67}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
{6A012FCA-3B1C-4593-ADD7-0751E5815C67}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -16,12 +16,12 @@
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest> <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix> <MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix> <MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
<TargetFrameworkVersion>v12.0</TargetFrameworkVersion> <TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
<AndroidEnableSGenConcurrent>true</AndroidEnableSGenConcurrent> <AndroidEnableSGenConcurrent>true</AndroidEnableSGenConcurrent>
<AndroidUseAapt2>true</AndroidUseAapt2> <AndroidUseAapt2>true</AndroidUseAapt2>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType> <AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<NuGetPackageImportStamp> <NuGetPackageImportStamp></NuGetPackageImportStamp>
</NuGetPackageImportStamp> <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<LangVersion>9.0</LangVersion> <LangVersion>9.0</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -37,11 +37,12 @@
<EnableLLVM>false</EnableLLVM> <EnableLLVM>false</EnableLLVM>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot> <AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>false</BundleAssemblies> <BundleAssemblies>false</BundleAssemblies>
<AndroidSupportedAbis>x86_64;x86</AndroidSupportedAbis> <AndroidSupportedAbis>x86;x86_64;arm64-v8a</AndroidSupportedAbis>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>false</EmbedAssembliesIntoApk>
<MandroidI18n />
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>false</DebugSymbols>
<DebugType>portable</DebugType> <DebugType>portable</DebugType>
<Optimize>true</Optimize> <Optimize>true</Optimize>
<OutputPath>bin\Release</OutputPath> <OutputPath>bin\Release</OutputPath>
@ -52,25 +53,28 @@
<AotAssemblies>false</AotAssemblies> <AotAssemblies>false</AotAssemblies>
<EnableLLVM>false</EnableLLVM> <EnableLLVM>false</EnableLLVM>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot> <AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>false</BundleAssemblies> <BundleAssemblies>true</BundleAssemblies>
<AndroidSupportedAbis>arm64-v8a</AndroidSupportedAbis> <AndroidSupportedAbis>x86_64;arm64-v8a</AndroidSupportedAbis>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<AndroidCreatePackagePerAbi>true</AndroidCreatePackagePerAbi>
<AndroidLinkTool>r8</AndroidLinkTool>
<MandroidI18n />
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Mono.Android" /> <Reference Include="Mono.Android" />
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Numerics" /> <Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microcharts.Forms"> <PackageReference Include="Microcharts.Forms" Version="0.9.5.9" />
<Version>0.9.5.9</Version>
</PackageReference>
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" /> <PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2337" /> <PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.1" /> <PackageReference Include="Xamarin.Forms" Version="5.0.0.2401" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.2" />
<PackageReference Include="Xamarin.Forms.Maps" Version="5.0.0.2401" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Definition.cs" /> <Compile Include="Definition.cs" />
@ -90,6 +94,8 @@
<Compile Include="Renderers\TintImageButtonRenderer.cs" /> <Compile Include="Renderers\TintImageButtonRenderer.cs" />
<Compile Include="Renderers\BillingPageRenderer.cs" /> <Compile Include="Renderers\BillingPageRenderer.cs" />
<Compile Include="Renderers\BlurryPanelRenderer.cs" /> <Compile Include="Renderers\BlurryPanelRenderer.cs" />
<Compile Include="Renderers\SegmentedControlRenderer.cs" />
<Compile Include="Renderers\OptionPickerRenderer.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidAsset Include="Assets\fa-brands-400.ttf" /> <AndroidAsset Include="Assets\fa-brands-400.ttf" />
@ -129,6 +135,66 @@
<Generator> <Generator>
</Generator> </Generator>
</AndroidResource> </AndroidResource>
<AndroidResource Include="Resources\layout\RadioGroup.xml">
<SubType>
</SubType>
<Generator>
</Generator>
</AndroidResource>
<AndroidResource Include="Resources\layout\RadioButton.xml">
<SubType>
</SubType>
<Generator>
</Generator>
</AndroidResource>
<AndroidResource Include="Resources\color\segmented_control_text.xml">
<SubType>
</SubType>
<Generator>
</Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\segmented_control_background.xml">
<SubType>
</SubType>
<Generator>
</Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\segmented_control_first_background.xml">
<SubType>
</SubType>
<Generator>
</Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\segmented_control_last_background.xml">
<SubType>
</SubType>
<Generator>
</Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable\share.png">
<SubType>
</SubType>
<Generator>
</Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable-mdpi\share.png">
<SubType>
</SubType>
<Generator>
</Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable-xhdpi\share.png">
<SubType>
</SubType>
<Generator>
</Generator>
</AndroidResource>
<AndroidResource Include="Resources\drawable-xxhdpi\share.png">
<SubType>
</SubType>
<Generator>
</Generator>
</AndroidResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidResource Include="Resources\drawable\splash_logo.png" /> <AndroidResource Include="Resources\drawable\splash_logo.png" />
@ -543,6 +609,9 @@
<ItemGroup> <ItemGroup>
<AndroidResource Include="Resources\drawable\left.png" /> <AndroidResource Include="Resources\drawable\left.png" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\xml\shortcuts.xml" />
</ItemGroup>
<Import Project="..\..\Billing.Shared\Billing.Shared.projitems" Label="Shared" /> <Import Project="..\..\Billing.Shared\Billing.Shared.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project> </Project>

View File

@ -2,20 +2,9 @@
{ {
public static partial class Definition public static partial class Definition
{ {
public static partial (string main, long build) GetVersion() public const string RegularFontFamily = "OpenSans-Regular.ttf#OpenSans-Regular";
{ public const string SemiBoldFontFamily = "OpenSans-SemiBold.ttf#OpenSans-SemiBold";
var context = Android.App.Application.Context; public const string BoldFontFamily = "OpenSans-Bold.ttf#OpenSans-Bold";
var manager = context.PackageManager; public const string BrandsFontFamily = "fa-brands-400.ttf#FontAwesome6Brands-Regular";
var info = manager.GetPackageInfo(context.PackageName, 0);
string version = info.VersionName;
long build = info.LongVersionCode;
return (version, build);
}
public static partial string GetRegularFontFamily() => "OpenSans-Regular.ttf#OpenSans-Regular";
public static partial string GetSemiBoldFontFamily() => "OpenSans-SemiBold.ttf#OpenSans-SemiBold";
public static partial string GetBoldFontFamily() => "OpenSans-Bold.ttf#OpenSans-Bold";
public static partial string GetBrandsFontFamily() => "fa-brands-400.ttf#FontAwesome6Brands-Regular";
} }
} }

View File

@ -1,7 +1,11 @@
using Android.App; using Android.App;
using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.Runtime; using Android.Runtime;
using Android.OS; using Android.OS;
using Android.Net;
using Android.Provider;
using Android.Database;
namespace Billing.Droid namespace Billing.Droid
{ {
@ -18,9 +22,26 @@ namespace Billing.Droid
{ {
base.OnCreate(savedInstanceState); base.OnCreate(savedInstanceState);
string url;
if (Intent.ActionView.Equals(Intent.Action) && Intent.Data is Uri uri)
{
if (uri.Authority == "org.tsanie.billing.shortcuts")
{
url = uri.Path;
}
else
{
url = GetFilePath(BaseContext, uri);
}
}
else
{
url = null;
}
Xamarin.Essentials.Platform.Init(this, savedInstanceState); Xamarin.Essentials.Platform.Init(this, savedInstanceState);
Xamarin.Forms.Forms.Init(this, savedInstanceState); Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App()); Xamarin.FormsMaps.Init(this, savedInstanceState);
LoadApplication(new App(url));
} }
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults) public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults)
@ -29,5 +50,83 @@ namespace Billing.Droid
base.OnRequestPermissionsResult(requestCode, permissions, grantResults); base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
} }
private string GetFilePath(Context context, Uri uri)
{
if (DocumentsContract.IsDocumentUri(context, uri))
{
Uri contentUri;
string[] split;
switch (uri.Authority)
{
case "com.android.externalstorage.documents":
split = DocumentsContract.GetDocumentId(uri).Split(':');
if (split[0] == "primary")
{
var external = ExternalCacheDir.Path;
external = external[..external.IndexOf("/Android/")];
return external + "/" + split[1];
}
break;
case "com.android.providers.downloads.documents":
contentUri = ContentUris.WithAppendedId(
Uri.Parse("content://downloads/public_downloads"),
long.Parse(DocumentsContract.GetDocumentId(uri)));
return GetDataColumn(context, contentUri, null, null);
case "com.android.providers.media.documents":
split = DocumentsContract.GetDocumentId(uri).Split(':');
contentUri = split[0] switch
{
"image" => MediaStore.Images.Media.ExternalContentUri,
"video" => MediaStore.Video.Media.ExternalContentUri,
"audio" => MediaStore.Audio.Media.ExternalContentUri,
_ => null
};
return GetDataColumn(context, contentUri, "_id=?", new[] { split[1] });
}
}
else if (uri.Scheme == "content")
{
if (uri.Authority == "com.speedsoftware.rootexplorer.fileprovider")
{
var path = uri.Path;
if (path.StartsWith("/root/"))
{
return path[5..];
}
return path;
}
return GetDataColumn(context, uri, null, null);
}
else if (uri.Scheme == "file")
{
return uri.Path;
}
return null;
}
private string GetDataColumn(Context context, Uri uri, string selection, string[] selectionArgs)
{
ICursor cursor = null;
try
{
cursor = context.ContentResolver.Query(uri, new[] { "_data" }, selection, selectionArgs, null);
if (cursor != null && cursor.MoveToFirst())
{
var index = cursor.GetColumnIndexOrThrow("_data");
return cursor.GetString(index);
}
}
finally
{
if (cursor != null)
{
cursor.Close();
}
}
return null;
}
} }
} }

View File

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="0.8.309" package="org.tsanie.billing" android:installLocation="auto" android:versionCode="8"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="1.2.411" package="org.tsanie.billing" android:installLocation="auto" android:versionCode="21s">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="30" />
<application android:label="@string/applabel" android:theme="@style/MainTheme"></application> <application android:label="@string/applabel" android:theme="@style/MainTheme" android:requestLegacyExternalStorage="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<queries>
<intent>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
</queries>
</manifest> </manifest>

View File

@ -27,3 +27,11 @@ using Android.App;
// Add some common permissions, these can be removed if not needed // Add some common permissions, these can be removed if not needed
[assembly: UsesPermission(Android.Manifest.Permission.Internet)] [assembly: UsesPermission(Android.Manifest.Permission.Internet)]
[assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)] [assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)]
[assembly: UsesPermission(Android.Manifest.Permission.ReadExternalStorage)]
[assembly: UsesPermission(Android.Manifest.Permission.ManageExternalStorage)]
[assembly: UsesPermission(Android.Manifest.Permission.Vibrate)]
[assembly: UsesPermission(Android.Manifest.Permission.AccessCoarseLocation)]
[assembly: UsesPermission(Android.Manifest.Permission.AccessFineLocation)]
[assembly: UsesFeature("android.hardware.location", Required = false)]
[assembly: UsesFeature("android.hardware.location.gps", Required = false)]
[assembly: UsesFeature("android.hardware.location.network", Required = false)]

View File

@ -18,7 +18,7 @@ namespace Billing.Droid.Renderers
base.OnAttachedToWindow(); base.OnAttachedToWindow();
if (Element is BillingPage page) if (Element is BillingPage page)
{ {
page.OnLoaded(); page.TriggerLoad();
} }
} }
} }

View File

@ -18,10 +18,12 @@ namespace Billing.Droid.Renderers
{ {
base.OnElementChanged(e); base.OnElementChanged(e);
if (e.NewElement != null) var control = Control;
if (e.NewElement != null && control != null)
{ {
var drawable = new ColorDrawable(e.NewElement.BackgroundColor.ToAndroid()); var drawable = new ColorDrawable(e.NewElement.BackgroundColor.ToAndroid());
Control.SetBackground(drawable); control.SetBackground(drawable);
control.Gravity = Android.Views.GravityFlags.CenterHorizontal;
} }
} }
} }

View File

@ -0,0 +1,29 @@
using Android.Content;
using Android.Graphics.Drawables;
using Billing.Droid.Renderers;
using Billing.UI;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(OptionPicker), typeof(OptionPickerRenderer))]
namespace Billing.Droid.Renderers
{
public class OptionPickerRenderer : PickerRenderer
{
public OptionPickerRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
var control = Control;
if (e.NewElement != null && control != null)
{
var drawable = new ColorDrawable(e.NewElement.BackgroundColor.ToAndroid());
control.SetBackground(drawable);
}
}
}
}

View File

@ -18,10 +18,11 @@ namespace Billing.Droid.Renderers
{ {
base.OnElementChanged(e); base.OnElementChanged(e);
if (e.NewElement != null) var control = Control;
if (e.NewElement != null && control != null)
{ {
var drawable = new ColorDrawable(e.NewElement.BackgroundColor.ToAndroid()); var drawable = new ColorDrawable(e.NewElement.BackgroundColor.ToAndroid());
Control.SetBackground(drawable); control.SetBackground(drawable);
} }
} }
} }

View File

@ -0,0 +1,245 @@
using Android.Content;
using Android.Graphics.Drawables;
using Android.Views;
using Android.Widget;
using Billing.Droid.Renderers;
using Billing.UI;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using RadioButton = Android.Widget.RadioButton;
[assembly: ExportRenderer(typeof(SegmentedControl), typeof(SegmentedControlRenderer))]
namespace Billing.Droid.Renderers
{
public class SegmentedControlRenderer : ViewRenderer<SegmentedControl, RadioGroup>
{
RadioGroup nativeControl;
RadioButton _rb;
readonly Context context;
public SegmentedControlRenderer(Context context) : base(context)
{
this.context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<SegmentedControl> e)
{
base.OnElementChanged(e);
if (Control == null)
{
// Instantiate the native control and assign it to the Control property with
// the SetNativeControl method
var layoutInflater = LayoutInflater.From(context);
var view = layoutInflater.Inflate(Resource.Layout.RadioGroup, null);
nativeControl = (RadioGroup)layoutInflater.Inflate(Resource.Layout.RadioGroup, null);
var density = context.Resources.DisplayMetrics.Density;
for (var i = 0; i < Element.Children.Count; i++)
{
var o = Element.Children[i];
var rb = (RadioButton)layoutInflater.Inflate(Resource.Layout.RadioButton, null);
var width = rb.Paint.MeasureText(o.Text) * density + 0.5f;
rb.LayoutParameters = new RadioGroup.LayoutParams((int)width, LayoutParams.WrapContent, 1f);
rb.Text = o.Text;
if (i == 0)
rb.SetBackgroundResource(Resource.Drawable.segmented_control_first_background);
else if (i == Element.Children.Count - 1)
rb.SetBackgroundResource(Resource.Drawable.segmented_control_last_background);
else
rb.SetBackgroundResource(Resource.Drawable.segmented_control_background);
ConfigureRadioButton(i, rb);
nativeControl.AddView(rb);
}
var option = (RadioButton)nativeControl.GetChildAt(Element.SelectedSegmentIndex);
if (option != null)
option.Checked = true;
SetNativeControl(nativeControl);
}
if (e.OldElement != null)
{
// Unsubscribe from event handlers and cleanup any resources
if (nativeControl != null)
nativeControl.CheckedChange -= NativeControl_ValueChanged;
}
if (e.NewElement != null)
{
// Configure the control and subscribe to event handlers
nativeControl.CheckedChange += NativeControl_ValueChanged;
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (nativeControl == null || Element == null) return;
switch (e.PropertyName)
{
case "Renderer":
Element?.SendValueChanged();
break;
case nameof(SegmentedControl.SelectedSegmentIndex):
var option = (RadioButton)nativeControl.GetChildAt(Element.SelectedSegmentIndex);
if (option != null)
option.Checked = true;
if (Element.SelectedSegmentIndex < 0)
{
var layoutInflater = LayoutInflater.From(context);
nativeControl = (RadioGroup)layoutInflater.Inflate(Resource.Layout.RadioGroup, null);
for (var i = 0; i < Element.Children.Count; i++)
{
var o = Element.Children[i];
var rb = (RadioButton)layoutInflater.Inflate(Resource.Layout.RadioButton, null);
var width = rb.Paint.MeasureText(o.Text);
rb.LayoutParameters = new RadioGroup.LayoutParams((int)width, LayoutParams.WrapContent, 1f);
rb.Text = o.Text;
if (i == 0)
rb.SetBackgroundResource(Resource.Drawable.segmented_control_first_background);
else if (i == Element.Children.Count - 1)
rb.SetBackgroundResource(Resource.Drawable.segmented_control_last_background);
else
rb.SetBackgroundResource(Resource.Drawable.segmented_control_background);
ConfigureRadioButton(i, rb);
nativeControl.AddView(rb);
}
nativeControl.CheckedChange += NativeControl_ValueChanged;
SetNativeControl(nativeControl);
}
Element.SendValueChanged();
break;
case nameof(SegmentedControl.TintColor):
OnPropertyChanged();
break;
case nameof(SegmentedControl.IsEnabled):
OnPropertyChanged();
break;
case nameof(SegmentedControl.SelectedTextColor):
var v = (RadioButton)nativeControl.GetChildAt(Element.SelectedSegmentIndex);
v.SetTextColor(Element.SelectedTextColor.ToAndroid());
break;
}
}
void OnPropertyChanged()
{
if (nativeControl != null && Element != null)
{
for (var i = 0; i < Element.Children.Count; i++)
{
var rb = (RadioButton)nativeControl.GetChildAt(i);
ConfigureRadioButton(i, rb);
}
}
}
void ConfigureRadioButton(int i, RadioButton rb)
{
var textColor = Element.SelectedTextColor;
if (i == Element.SelectedSegmentIndex)
{
rb.SetTextColor(textColor.ToAndroid());
rb.Paint.FakeBoldText = true;
_rb = rb;
}
else
{
var tColor = Element.IsEnabled ?
textColor.IsDefault ? Element.TintColor.ToAndroid() : textColor.MultiplyAlpha(.6).ToAndroid() :
Element.DisabledColor.ToAndroid();
rb.SetTextColor(tColor);
}
GradientDrawable selectedShape;
GradientDrawable unselectedShape;
var gradientDrawable = (StateListDrawable)rb.Background;
var drawableContainerState = (DrawableContainer.DrawableContainerState)gradientDrawable.GetConstantState();
var children = drawableContainerState.GetChildren();
// Doesnt works on API < 18
selectedShape = children[0] is GradientDrawable selected ? selected : (GradientDrawable)((InsetDrawable)children[0]).Drawable;
unselectedShape = children[1] is GradientDrawable unselected ? unselected : (GradientDrawable)((InsetDrawable)children[1]).Drawable;
var color = Element.IsEnabled ? Element.TintColor.ToAndroid() : Element.DisabledColor.ToAndroid();
selectedShape.SetStroke(3, color);
selectedShape.SetColor(color);
unselectedShape.SetStroke(3, color);
rb.Enabled = Element.IsEnabled;
}
void NativeControl_ValueChanged(object sender, RadioGroup.CheckedChangeEventArgs e)
{
var rg = (RadioGroup)sender;
if (rg.CheckedRadioButtonId != -1)
{
var id = rg.CheckedRadioButtonId;
var radioButton = rg.FindViewById(id);
var radioId = rg.IndexOfChild(radioButton);
var rb = (RadioButton)rg.GetChildAt(radioId);
var textColor = Element.SelectedTextColor;
var color = Element.IsEnabled ?
textColor.IsDefault ? Element.TintColor.ToAndroid() : textColor.MultiplyAlpha(.6).ToAndroid() :
Element.DisabledColor.ToAndroid();
if (_rb != null)
{
_rb.SetTextColor(color);
_rb.Paint.FakeBoldText = false;
}
rb.SetTextColor(Element.SelectedTextColor.ToAndroid());
rb.Paint.FakeBoldText = true;
_rb = rb;
Element.SelectedSegmentIndex = radioId;
}
}
protected override void Dispose(bool disposing)
{
if (nativeControl != null)
{
nativeControl.CheckedChange -= NativeControl_ValueChanged;
nativeControl.Dispose();
nativeControl = null;
_rb = null;
}
try
{
base.Dispose(disposing);
}
catch
{
return;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="@color/normal"/>
<item android:color="@color/selected" />
</selector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 B

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 B

After

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<inset android:insetRight="-1dp">
<shape android:id="@+id/shape_id" xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/selected" />
<stroke android:width="1dp" android:color="@color/selected" />
</shape>
</inset>
</item>
<item android:state_checked="false">
<inset android:insetRight="-1dp">
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/normal" />
<stroke android:width="1dp" android:color="@color/selected" />
</shape>
</inset>
</item>
</selector>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<inset android:insetRight="-1dp">
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/selected" />
<stroke android:width="1dp" android:color="@color/selected" />
<corners android:topLeftRadius="2sp" android:bottomLeftRadius="2sp" />
</shape>
</inset>
</item>
<item android:state_checked="false">
<inset android:insetRight="-1dp">
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/normal" />
<stroke android:width="1dp" android:color="@color/selected" />
<corners android:topLeftRadius="2sp" android:bottomLeftRadius="2sp" />
</shape>
</inset>
</item>
</selector>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/selected" />
<stroke android:width="1dp" android:color="@color/selected" />
<corners android:topRightRadius="2sp" android:bottomRightRadius="2sp" />
</shape>
</item>
<item android:state_checked="false">
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/normal" />
<stroke android:width="1dp" android:color="@color/selected" />
<corners android:topRightRadius="2sp" android:bottomRightRadius="2sp" />
</shape>
</item>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<RadioButton xmlns:android="http://schemas.android.com/apk/res/android"
android:button="@null"
android:gravity="center"
android:background="@drawable/segmented_control_background"
android:textColor="@color/segmented_control_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:minHeight="30dp"
android:textSize="12sp" />

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:id="@+id/SegControl" />

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<resources> <resources>
<string name="applabel">记账本</string> <string name="applabel">记账本</string>
<string name="newbill">记录一条</string>
</resources> </resources>

View File

@ -3,5 +3,7 @@
<color name="colorPrimary">#183153</color> <color name="colorPrimary">#183153</color>
<color name="colorPrimaryDark">#2B0B98</color> <color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color> <color name="colorAccent">#2B0B98</color>
<color name="normal">@android:color/transparent</color>
<color name="selected">#007AFF</color>
<color name="splash_background">?android:attr/colorBackground</color> <color name="splash_background">?android:attr/colorBackground</color>
</resources> </resources>

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<resources> <resources>
<string name="applabel">Billing</string> <string name="applabel">Billing</string>
<string name="newbill">Write a bill</string>
</resources> </resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut android:shortcutId="newbill" android:enabled="true" android:icon="@drawable/daily" android:shortcutShortLabel="@string/newbill" android:shortcutLongLabel="@string/newbill" android:shortcutDisabledMessage="@string/newbill">
<intent android:action="android.intent.action.VIEW" android:targetPackage="org.tsanie.billing" android:targetClass="org.tsanie.billing.SplashScreen" android:data="content://org.tsanie.billing.shortcuts/newbill" />
</shortcut>
</shortcuts>

View File

@ -1,5 +1,6 @@
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Net;
using Android.OS; using Android.OS;
using AndroidX.AppCompat.App; using AndroidX.AppCompat.App;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -11,6 +12,15 @@ namespace Billing.Droid
NoHistory = true, NoHistory = true,
Theme = "@style/MainTheme.Splash", Theme = "@style/MainTheme.Splash",
Name = "org.tsanie.billing.SplashScreen")] Name = "org.tsanie.billing.SplashScreen")]
[IntentFilter(
new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
//DataScheme = "file",
DataMimeType = "*/*",
DataPathPattern = ".*\\.db3")]
[MetaData(
"android.app.shortcuts",
Resource = "@xml/shortcuts")]
public class SplashActivity : AppCompatActivity public class SplashActivity : AppCompatActivity
{ {
public override void OnCreate(Bundle savedInstanceState, PersistableBundle persistentState) public override void OnCreate(Bundle savedInstanceState, PersistableBundle persistentState)
@ -23,7 +33,16 @@ namespace Billing.Droid
protected override void OnResume() protected override void OnResume()
{ {
base.OnResume(); base.OnResume();
Task.Run(() => StartActivity(new Intent(Application.Context, typeof(MainActivity)))); Task.Run(() =>
{
var intent = new Intent(Application.Context, typeof(MainActivity));
if (Intent?.Data is Uri uri)
{
intent.SetAction(Intent.ActionView);
intent.SetData(uri);
}
StartActivity(intent);
});
} }
} }
} }

View File

@ -19,9 +19,55 @@ namespace Billing.iOS
public override bool FinishedLaunching(UIApplication app, NSDictionary options) public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{ {
Xamarin.Forms.Forms.Init(); Xamarin.Forms.Forms.Init();
LoadApplication(new App()); Xamarin.FormsMaps.Init();
string action;
if (options != null && options.TryGetValue(UIApplication.LaunchOptionsShortcutItemKey, out var obj) && obj is UIApplicationShortcutItem shortcut)
{
action = "/" + shortcut.Type;
}
else
{
action = null;
}
LoadApplication(new App(action));
return base.FinishedLaunching(app, options); return base.FinishedLaunching(app, options);
} }
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
if (url?.IsFileUrl == true)
{
return App.OpenUrl(url.Path);
}
return false;
}
public override async void PerformActionForShortcutItem(UIApplication application, UIApplicationShortcutItem shortcutItem, UIOperationHandler completionHandler)
{
if (shortcutItem == null || string.IsNullOrEmpty(shortcutItem.Type))
{
return;
}
if (App.Accounts.Count == 0)
{
await App.InitializeData();
}
Xamarin.Essentials.MainThread.BeginInvokeOnMainThread(async () =>
{
var state = Xamarin.Forms.Shell.Current.CurrentState.Location;
var route = state.OriginalString;
var current = Xamarin.Forms.Shell.Current;
if (!route.StartsWith("//Bills"))
{
await current.GoToAsync("//Bills");
}
else if (route.EndsWith("/Details") || route.StartsWith("//Bills/D_FAULT_AddBillPage"))
{
return;
}
await current.GoToAsync("Details");
});
}
} }
} }

View File

@ -1 +1,3 @@
"CFBundleDisplayName" = "Billing"; "CFBundleDisplayName" = "Billing";
"BillingShortcutNew" = "Write a bill";
"NSLocationWhenInUseUsageDescription" = "When &quot;Save Location&quot; is checked, the Billing app needs to access the location.";

View File

@ -14,8 +14,9 @@
<AssemblyName>Billing.iOS</AssemblyName> <AssemblyName>Billing.iOS</AssemblyName>
<MtouchEnableSGenConc>true</MtouchEnableSGenConc> <MtouchEnableSGenConc>true</MtouchEnableSGenConc>
<MtouchHttpClientHandler>NSUrlSessionHandler</MtouchHttpClientHandler> <MtouchHttpClientHandler>NSUrlSessionHandler</MtouchHttpClientHandler>
<ProvisioningType>automatic</ProvisioningType> <ProvisioningType>manual</ProvisioningType>
<LangVersion>9.0</LangVersion> <LangVersion>9.0</LangVersion>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhoneSimulator' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhoneSimulator' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -38,6 +39,7 @@
<MtouchLink>None</MtouchLink> <MtouchLink>None</MtouchLink>
<MtouchArch>x86_64</MtouchArch> <MtouchArch>x86_64</MtouchArch>
<CodesignKey>iPhone Distribution</CodesignKey> <CodesignKey>iPhone Distribution</CodesignKey>
<MtouchUseLlvm>true</MtouchUseLlvm>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhone' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhone' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -64,6 +66,9 @@
<CodesignKey>iPhone Distribution</CodesignKey> <CodesignKey>iPhone Distribution</CodesignKey>
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements> <CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
<MtouchLink>SdkOnly</MtouchLink> <MtouchLink>SdkOnly</MtouchLink>
<MtouchInterpreter>-all</MtouchInterpreter>
<MtouchUseLlvm>true</MtouchUseLlvm>
<OptimizePNGs>true</OptimizePNGs>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Definition.cs" /> <Compile Include="Definition.cs" />
@ -91,6 +96,19 @@
<BundleResource Include="Resources\OpenSans-SemiBold.ttf" /> <BundleResource Include="Resources\OpenSans-SemiBold.ttf" />
<Compile Include="Renderers\BlurryPanelRenderer.cs" /> <Compile Include="Renderers\BlurryPanelRenderer.cs" />
<Compile Include="Renderers\SegmentedControlRenderer.cs" /> <Compile Include="Renderers\SegmentedControlRenderer.cs" />
<Compile Include="Renderers\OptionPickerRenderer.cs" />
<BundleResource Include="Resources\share.png" />
<BundleResource Include="Resources\share%402x.png" />
<BundleResource Include="Resources\share%403x.png" />
<BundleResource Include="Resources\sync.png" />
<BundleResource Include="Resources\sync%402x.png" />
<BundleResource Include="Resources\sync%403x.png" />
<BundleResource Include="Resources\location.png" />
<BundleResource Include="Resources\location%402x.png" />
<BundleResource Include="Resources\location%403x.png" />
<BundleResource Include="Resources\pin.png" />
<BundleResource Include="Resources\pin%402x.png" />
<BundleResource Include="Resources\pin%403x.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Contents.json"> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Contents.json">
@ -165,15 +183,14 @@
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="Xamarin.iOS" /> <Reference Include="Xamarin.iOS" />
<Reference Include="System.Numerics" /> <Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microcharts.Forms"> <PackageReference Include="Microcharts.Forms" Version="0.9.5.9" />
<Version>0.9.5.9</Version>
</PackageReference>
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" /> <PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2337" /> <PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.1" /> <PackageReference Include="Xamarin.Forms" Version="5.0.0.2401" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.2" />
<PackageReference Include="Xamarin.Forms.Maps" Version="5.0.0.2401" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<BundleResource Include="Resources\dollar.png" /> <BundleResource Include="Resources\dollar.png" />

View File

@ -2,13 +2,9 @@
{ {
public static partial class Definition public static partial class Definition
{ {
public static partial (string main, long build) GetVersion() => ( public const string RegularFontFamily = "OpenSans-Regular";
Foundation.NSBundle.MainBundle.ObjectForInfoDictionary("CFBundleShortVersionString").ToString(), public const string SemiBoldFontFamily = "OpenSans-SemiBold";
int.Parse(Foundation.NSBundle.MainBundle.ObjectForInfoDictionary("CFBundleVersion").ToString()) public const string BoldFontFamily = "OpenSans-Bold";
); public const string BrandsFontFamily = "FontAwesome6Brands-Regular";
public static partial string GetRegularFontFamily() => "OpenSans-Regular";
public static partial string GetSemiBoldFontFamily() => "OpenSans-SemiBold";
public static partial string GetBoldFontFamily() => "OpenSans-Bold";
public static partial string GetBrandsFontFamily() => "FontAwesome6Brands-Regular";
} }
} }

View File

@ -41,11 +41,68 @@
<string>OpenSans-Regular.ttf</string> <string>OpenSans-Regular.ttf</string>
<string>OpenSans-SemiBold.ttf</string> <string>OpenSans-SemiBold.ttf</string>
</array> </array>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>SQLite Database</string>
<key>LSItemContentTypes</key>
<array>
<string>org.tsanie.billing.db3</string>
<string>public.database</string>
</array>
<key>CFBundleTypeIconFiles</key>
<array>
<string>Assets.xcassets/AppIcon.appiconset/Icon180</string>
</array>
<key>LSHandlerRank</key>
<string>Alternate</string>
</dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>SQLite Database</string>
<key>UTTypeIdentifier</key>
<string>org.tsanie.billing.db3</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<string>db3</string>
<key>public.mime-type</key>
<string>application/vnd.sqlite3</string>
</dict>
</dict>
</array>
<key>CFBundleVersion</key>
<string>21</string>
<key>CFBundleShortVersionString</key>
<string>1.2.411</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>mailto</string>
</array>
<key>UIFileSharingEnabled</key> <key>UIFileSharingEnabled</key>
<true/> <true/>
<key>CFBundleVersion</key> <key>LSSupportsOpeningDocumentsInPlace</key>
<string>8</string> <true/>
<key>CFBundleShortVersionString</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>0.8.309</string> <string>When &quot;Save Location&quot; is checked, the Billing app needs to access the location.</string>
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemIconType</key>
<string>UIApplicationShortcutIconTypeCompose</string>
<key>UIApplicationShortcutItemTitle</key>
<string>BillingShortcutNew</string>
<key>UIApplicationShortcutItemType</key>
<string>newbill</string>
</dict>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -55,7 +55,7 @@ namespace Billing.iOS.Renderers
base.ViewDidAppear(animated); base.ViewDidAppear(animated);
if (Element is BillingPage page) if (Element is BillingPage page)
{ {
page.OnLoaded(); page.TriggerLoad();
} }
} }
} }

View File

@ -14,9 +14,10 @@ namespace Billing.iOS.Renderers
base.OnElementChanged(e); base.OnElementChanged(e);
var control = Control; var control = Control;
if (control != null) if (e.NewElement != null && control != null)
{ {
control.BorderStyle = UITextBorderStyle.None; control.BorderStyle = UITextBorderStyle.None;
control.TextAlignment = UITextAlignment.Center;
} }
} }
} }

View File

@ -0,0 +1,46 @@
using System.ComponentModel;
using Billing.iOS.Renderers;
using Billing.UI;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(OptionPicker), typeof(OptionPickerRenderer))]
namespace Billing.iOS.Renderers
{
public class OptionPickerRenderer : PickerRenderer
{
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == nameof(OptionPicker.BorderStyle))
{
var control = Control;
if (control != null && Element is OptionPicker picker)
{
control.BorderStyle = picker.BorderStyle switch
{
BorderStyle.RoundedRect => UITextBorderStyle.RoundedRect,
_ => UITextBorderStyle.None
};
}
}
}
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
var control = Control;
if (control != null && e.NewElement is OptionPicker picker)
{
control.BorderStyle = picker.BorderStyle switch
{
BorderStyle.RoundedRect => UITextBorderStyle.RoundedRect,
_ => UITextBorderStyle.None
};
}
}
}
}

View File

@ -92,7 +92,7 @@ namespace Billing.iOS.Renderers
{ {
//var color = Element.SelectedTextColor; //var color = Element.SelectedTextColor;
//UIColor c = color == default ? UIColor.LabelColor : color.ToUIColor(); //UIColor c = color == default ? UIColor.LabelColor : color.ToUIColor();
UIColor c = UIColor.LabelColor; UIColor c = UIColor.Label;
var attribute = new UITextAttributes var attribute = new UITextAttributes
{ {
TextColor = c TextColor = c
@ -112,7 +112,7 @@ namespace Billing.iOS.Renderers
//var tintColor = element.TintColor; //var tintColor = element.TintColor;
//if (tintColor == default) //if (tintColor == default)
//{ //{
return UIColor.SystemGray6Color; return UIColor.SystemGray6;
//} //}
//else //else
//{ //{
@ -124,7 +124,7 @@ namespace Billing.iOS.Renderers
//var disabledColor = element.DisabledColor; //var disabledColor = element.DisabledColor;
//if (disabledColor == default) //if (disabledColor == default)
//{ //{
return UIColor.SecondaryLabelColor; return UIColor.SecondaryLabel;
//} //}
//else //else
//{ //{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 B

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Some files were not shown because too many files have changed in this diff Show More