Compare commits

...

44 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
fde8931dbd complete basic report page 2022-03-09 17:26:56 +08:00
abffc0627e complete ranking page 2022-03-09 16:21:03 +08:00
b5c531d128 report page 2022-03-09 15:03:20 +08:00
db055fa205 add chart 2022-03-09 00:54:03 +08:00
ae619c8fee add microcharts 2022-03-08 19:46:16 +08:00
e1d80b6c62 ui fix 2022-03-08 15:10:45 +08:00
63ee572e8b allow to select a date & fix issue 2022-03-08 14:19:50 +08:00
91db3caa15 change fonts 2022-03-08 08:54:05 +08:00
4076b2c9ed fix issue 2022-03-07 21:31:27 +08:00
c3718d9d6c UI fix 2022-03-07 17:37:13 +08:00
9f783c1b5f tiny fix 2022-03-07 17:36:03 +08:00
46464e19dc category management 2022-03-07 17:34:09 +08:00
49e4e46cdb fix sort issue 2022-03-06 22:19:06 +08:00
0c3b8c52bd tiny fix 2022-03-05 08:40:18 +08:00
bc5cf4afa5 fix issue 2022-03-03 22:34:18 +08:00
255 changed files with 17225 additions and 4542 deletions

View File

@ -1,9 +1,11 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Billing.Languages;
using Billing.Models;
using Billing.Store;
using Billing.Themes;
using Billing.UI;
using Xamarin.Essentials;
using Xamarin.Forms;
@ -11,39 +13,55 @@ namespace Billing
{
public class App : Application
{
internal const string NewBillAction = "/newbill";
public static AppTheme CurrentTheme { get; private set; }
public static PlatformCulture CurrentCulture { get; private set; }
public static List<Bill> Bills => bills ??= new List<Bill>();
public static List<Account> Accounts => accounts ??= new List<Account>();
public static List<Category> Categories => categories ??= new List<Category>();
public static bool SaveLocation => saveLocation;
public static string MainRoute => mainRoute;
private static List<Bill> bills;
private static List<Account> accounts;
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();
saveLocation = Preferences.Get(Definition.SaveLocationKey, false);
InitResources();
MainPage = new MainShell();
Shell.Current.GoToAsync("//Settings");
}
public static void WriteAccounts() => StoreHelper.WriteAccounts(accounts);
public static void WriteBills() => StoreHelper.WriteBills(bills);
protected override void OnStart()
{
Helper.Debug($"personal folder: {StoreHelper.PersonalFolder}");
Helper.Debug($"cache folder: {StoreHelper.CacheFolder}");
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()
@ -81,5 +99,56 @@ namespace Billing
// TODO: status bar
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>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Languages\en.xml" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Languages\zh-CN.xml" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)SplashPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)App.cs" />
@ -18,10 +22,11 @@
<Compile Include="$(MSBuildThisFileDirectory)Helper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Languages\PlatformCulture.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Languages\Resource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)LocationExtension.cs" />
<Compile Include="$(MSBuildThisFileDirectory)MainShell.xaml.cs">
<DependentUpon>MainShell.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Models\BaseModel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\Category.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\Account.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Themes\BaseTheme.cs" />
@ -29,8 +34,10 @@
<Compile Include="$(MSBuildThisFileDirectory)Themes\Light.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\BillingDate.xaml.cs">
<DependentUpon>BillingDate.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)UI\BillingPage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\ColorPicker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\Converters.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\CustomControl.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\CustomEffect.cs" />
@ -40,26 +47,55 @@
<Compile Include="$(MSBuildThisFileDirectory)UI\WrapLayout.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Views\AccountPage.xaml.cs">
<DependentUpon>AccountPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\AddAccountPage.xaml.cs">
<DependentUpon>AddAccountPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\AddBillPage.xaml.cs">
<DependentUpon>AddBillPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\AddCategoryPage.xaml.cs">
<DependentUpon>AddCategoryPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\BillPage.xaml.cs">
<DependentUpon>BillPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\CategoryPage.xaml.cs">
<DependentUpon>CategoryPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\CategorySelectPage.xaml.cs">
<DependentUpon>CategorySelectPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\IconSelectPage.xaml.cs">
<DependentUpon>IconSelectPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\RankPage.xaml.cs">
<DependentUpon>RankPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\SettingPage.xaml.cs">
<DependentUpon>SettingPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)UI\OptionsCells.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Store\StoreHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\Bill.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>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)MainShell.xaml">
@ -93,10 +129,35 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\IconSelectPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="$(MSBuildThisFileDirectory)Store\" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\CategoryPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\AddCategoryPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\CategorySelectPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\RankPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@ -1,5 +1,13 @@
using System;
using Billing.Models;
using Billing.UI;
using Billing.Views;
using System;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Billing
{
@ -11,33 +19,35 @@ namespace Billing
var time = DateTime.Now.ToString("HH:mm:ss.fff");
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
#pragma warning disable IDE0060 // Remove unused parameter
public static void Debug(string message)
{
}
#pragma warning restore IDE0060 // Remove unused parameter
#endif
public static void Error(string category, Exception ex)
{
Error(category, ex?.ToString() ?? "unknown error");
}
public static void Error(string category, string message)
{
}
#pragma warning restore IDE0060 // Remove unused parameter
#if DEBUG
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
_ = Store.StoreHelper.SaveLogItemAsync(new Logs()
{
LogTime = DateTime.Now,
Category = category,
Detail = message
});
}
public static bool NetworkAvailable
{
get
@ -53,5 +63,99 @@ namespace Billing
}
}
}
public const string DEFAULT_COLOR = "#183153";
public static string WrapColorString(string color)
{
if (color == null)
{
return DEFAULT_COLOR;
}
if (color.Length > 7)
{
var alpha = color[1..3];
if (int.TryParse(alpha, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int a) && a == 255)
{
return "#" + color[3..];
}
}
return color;
}
public static UIBill WrapBill(Bill b)
{
var category = App.Categories.FirstOrDefault(c => c.Id == b.CategoryId);
return new UIBill(b)
{
Icon = category?.Icon ?? Definition.DefaultIcon,
TintColor = category?.TintColor ?? Definition.TransparentColor,
Name = b.Name,
DateCreation = b.CreateTime,
Amount = b.Amount,
Wallet = App.Accounts.FirstOrDefault(a => a.Id == b.WalletId)?.Name
};
}
public static bool IsSameDay(DateTime day1, DateTime day2)
{
return day1.Year == day2.Year && day1.DayOfYear == day2.DayOfYear;
}
public static DateTime FirstDay(DateTime date)
{
var week = date.DayOfWeek;
if (week == DayOfWeek.Sunday)
{
return date;
}
return date.AddDays(-(int)week);
}
public static BindableProperty Create<TResult, TOwner>(string name, TResult defaultValue = default, PropertyValueChanged<TResult, TOwner> propertyChanged = null)
where TOwner : BindableObject
{
if (propertyChanged == null)
{
return BindableProperty.Create(name, typeof(TResult), typeof(TOwner), defaultValue: defaultValue);
}
return BindableProperty.Create(name, typeof(TResult), typeof(TOwner),
defaultValue: defaultValue,
propertyChanged: (obj, old, @new) =>
{
if (old != null && !(old is TResult))
{
return;
}
if (@new != null && !(@new is TResult))
{
return;
}
propertyChanged((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,11 +10,22 @@ namespace Billing.Languages
internal class Resource
{
public static string Ok => Text(nameof(Ok));
public static string Cancel => Text(nameof(Cancel));
public static string Yes => Text(nameof(Yes));
public static string No => Text(nameof(No));
public static string ConfirmDeleteAccount => Text(nameof(ConfirmDeleteAccount));
public static string ConfirmDeleteBill => Text(nameof(ConfirmDeleteBill));
public static string TitleDateFormat => Text(nameof(TitleDateFormat));
public static string TitleShortDateFormat => Text(nameof(TitleShortDateFormat));
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 CreditCard => Text(nameof(CreditCard));
public static string DebitCard => Text(nameof(DebitCard));
@ -26,6 +37,17 @@ namespace Billing.Languages
public static string AccountRequired => Text(nameof(AccountRequired));
public static string NeedAccount => Text(nameof(NeedAccount));
public static string AmountRequired => Text(nameof(AmountRequired));
public static string Income => Text(nameof(Income));
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 AddCategory => Text(nameof(AddCategory));
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
public static string Clothing => Text(nameof(Clothing));
@ -36,9 +58,25 @@ namespace Billing.Languages
public static string Entertainment => Text(nameof(Entertainment));
public static string Learn => Text(nameof(Learn));
public static string Medical => Text(nameof(Medical));
public static string OtherSpending => Text(nameof(OtherSpending));
public static string Salary => Text(nameof(Salary));
public static string Earnings => Text(nameof(Earnings));
public static string Bonus => Text(nameof(Bonus));
public static string OtherIncome => Text(nameof(OtherIncome));
public static string Jewellery => Text(nameof(Jewellery));
public static string Cosmetics => Text(nameof(Cosmetics));
public static string Brunch => Text(nameof(Brunch));
public static string Dinner => Text(nameof(Dinner));
public static string Fruit => Text(nameof(Fruit));
public static string UtilityBill => Text(nameof(UtilityBill));
public static string PropertyFee => Text(nameof(PropertyFee));
public static string Rent => Text(nameof(Rent));
public static string Maintenance => Text(nameof(Maintenance));
public static string Bus => Text(nameof(Bus));
public static string LightRail => Text(nameof(LightRail));
public static string Taxi => Text(nameof(Taxi));
public static string Fitness => Text(nameof(Fitness));
public static string Party => Text(nameof(Party));
#endregion
static readonly Dictionary<string, LanguageResource> dict = new();

View File

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<Ok>OK</Ok>
<Cancel>Cancel</Cancel>
<About>About</About>
<Version>Version</Version>
<Preference>Preference</Preference>
<PrimaryColor>Primary Color</PrimaryColor>
<Accounts>Accounts</Accounts>
<Bills>Bills</Bills>
<Report>Report</Report>
<Settings>Settings</Settings>
<Sunday>Su</Sunday>
<Monday>Mo</Monday>
@ -18,6 +20,19 @@
<NoRecords>Bills not yet generated</NoRecords>
<TapToMemo>Click here to record</TapToMemo>
<TitleDateFormat>MM/dd/yyyy</TitleDateFormat>
<TitleShortDateFormat>MM/dd/yyyy</TitleShortDateFormat>
<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>
<Assets>Assets</Assets>
<Liability>Liability</Liability>
@ -50,6 +65,8 @@
<AmountRequired>Please enter the amount.</AmountRequired>
<Income>Income</Income>
<Spending>Spending</Spending>
<LastSelected>Last Selected</LastSelected>
<Recent>Recent</Recent>
<Clothing>Clothing</Clothing>
<Food>Food</Food>
<Drinks>Drinks</Drinks>
@ -58,11 +75,44 @@
<Entertainment>Entertainment</Entertainment>
<Learn>Learn</Learn>
<Medical>Medical</Medical>
<OtherSpending>Other spending</OtherSpending>
<Salary>Salary</Salary>
<Earnings>Earnings</Earnings>
<Bonus>Bonus</Bonus>
<OtherIncome>Other income</OtherIncome>
<Jewellery>Jewellery</Jewellery>
<Cosmetics>Cosmetics</Cosmetics>
<Brunch>Brunch</Brunch>
<Dinner>Dinner</Dinner>
<Fruit>Fruit</Fruit>
<UtilityBill>Utility bills</UtilityBill>
<PropertyFee>Property fee</PropertyFee>
<Rent>Rent</Rent>
<Maintenance>Maintenance</Maintenance>
<Bus>Bus</Bus>
<LightRail>Light rail</LightRail>
<Taxi>Taxi</Taxi>
<Fitness>Fitness</Fitness>
<Party>Party</Party>
<Yes>Yes</Yes>
<No>No</No>
<ConfirmDeleteAccount>Are you sure you want to delete the account?</ConfirmDeleteAccount>
<ConfirmDeleteBill>Are you sure you want to delete the billing?</ConfirmDeleteBill>
<Feature>Feature</Feature>
<CategoryManage>Category Management</CategoryManage>
<Detail>Detail</Detail>
<SaveLocation>Save Location</SaveLocation>
<AddCategory>Add Category</AddCategory>
<ConfirmDeleteCategory>Are you sure you want to delete the category: {0}?</ConfirmDeleteCategory>
<SelectCategory>Select Category</SelectCategory>
<TrackingChart>Tracking Chart</TrackingChart>
<NoResult>(no results)</NoResult>
<Top10>Top 10</Top10>
<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>

View File

@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<Ok>确定</Ok>
<Cancel>取消</Cancel>
<About>关于</About>
<Version>版本号</Version>
<Preference>偏好</Preference>
<PrimaryColor>主要配色</PrimaryColor>
<Accounts>账户</Accounts>
<Bills>账单</Bills>
<Report>报表</Report>
<Settings>设置</Settings>
<Sunday>周日</Sunday>
<Monday>周一</Monday>
@ -18,6 +20,19 @@
<NoRecords>还未产生账单</NoRecords>
<TapToMemo>点此记录</TapToMemo>
<TitleDateFormat>yyyy年MM月dd日</TitleDateFormat>
<TitleShortDateFormat>yyyy/MM/dd</TitleShortDateFormat>
<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>
<Assets>资产</Assets>
<Liability>负债</Liability>
@ -50,6 +65,8 @@
<AmountRequired>请输入金额。</AmountRequired>
<Income>收入</Income>
<Spending>支出</Spending>
<LastSelected>最后选择</LastSelected>
<Recent>最近</Recent>
<Clothing>衣物</Clothing>
<Food>食品</Food>
<Drinks>饮料</Drinks>
@ -58,11 +75,44 @@
<Entertainment>娱乐</Entertainment>
<Learn>学习</Learn>
<Medical>医疗</Medical>
<OtherSpending>其他支出</OtherSpending>
<Salary>工资</Salary>
<Earnings>收益</Earnings>
<Bonus>奖金</Bonus>
<OtherIncome>其他收入</OtherIncome>
<Jewellery>首饰</Jewellery>
<Cosmetics>化妆品</Cosmetics>
<Brunch>早午餐</Brunch>
<Dinner>晚餐</Dinner>
<Fruit>水果</Fruit>
<UtilityBill>水电费</UtilityBill>
<PropertyFee>物业费</PropertyFee>
<Rent>房租</Rent>
<Maintenance>维修保养</Maintenance>
<Bus>公交车</Bus>
<LightRail>轻轨</LightRail>
<Taxi>出租车</Taxi>
<Fitness>健身</Fitness>
<Party>聚会</Party>
<Yes></Yes>
<No></No>
<ConfirmDeleteAccount>是否确认删除该账户?</ConfirmDeleteAccount>
<ConfirmDeleteBill>是否确认删除该账单?</ConfirmDeleteBill>
<Feature>功能</Feature>
<CategoryManage>分类管理</CategoryManage>
<Detail>详细</Detail>
<SaveLocation>保存位置</SaveLocation>
<AddCategory>新建分类</AddCategory>
<ConfirmDeleteCategory>是否确认删除该分类:{0}</ConfirmDeleteCategory>
<SelectCategory>选择类别</SelectCategory>
<TrackingChart>跟踪图表</TrackingChart>
<NoResult>(无记录)</NoResult>
<Top10>Top 10</Top10>
<CategoryRank>分类排行</CategoryRank>
<Diagnostic>诊断</Diagnostic>
<ShareLogs>发送日志</ShareLogs>
<ManyRecords>{0} 条记录</ManyRecords>
<SendEmail>发送邮件</SendEmail>
<HowToShareDiagnostic>您想以哪种方式分享诊断日志?</HowToShareDiagnostic>
<ViewLocation>查看位置</ViewLocation>
</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" ?>
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Billing"
xmlns:v="clr-namespace:Billing.Views"
xmlns:r="clr-namespace:Billing.Languages"
x:Class="Billing.MainShell"
@ -9,10 +10,14 @@
TitleColor="{DynamicResource PrimaryColor}"
Shell.NavBarHasShadow="True">
<FlyoutItem>
<ShellContent ContentTemplate="{DataTemplate local:SplashPage}" Route="Splash"/>
</FlyoutItem>
<TabBar>
<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:RankPage}" Route="Ranks" Title="{r:Text Report}" Icon="rank.png"/>
<ShellContent ContentTemplate="{DataTemplate v:SettingPage}" Route="Settings" Title="{r:Text Settings}" Icon="settings.png"/>
</TabBar>
</Shell>

View File

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

View File

@ -1,9 +1,15 @@
using System.Xml.Linq;
using SQLite;
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 string Icon { get; set; } = ICON_DEFAULT;
public AccountCategory Category { get; set; }
@ -11,28 +17,6 @@ namespace Billing.Models
public decimal Initial { get; set; }
public decimal Balance { get; set; }
public string Memo { get; set; }
public override void OnXmlDeserialize(XElement node)
{
Id = Read(node, nameof(Id), 0);
Icon = Read(node, nameof(Icon), ICON_DEFAULT);
Category = (AccountCategory)Read(node, nameof(Category), 0);
Name = Read(node, nameof(Name), string.Empty);
Initial = Read(node, nameof(Initial), 0m);
Balance = Read(node, nameof(Balance), 0m);
Memo = Read(node, nameof(Memo), null);
}
public override void OnXmlSerialize(XElement node)
{
Write(node, nameof(Id), Id);
Write(node, nameof(Icon), Icon);
Write(node, nameof(Category), (int)Category);
Write(node, nameof(Name), Name);
Write(node, nameof(Initial), Initial);
Write(node, nameof(Balance), Balance);
Write(node, nameof(Memo), Memo);
}
}
public enum AccountCategory

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

View File

@ -1,39 +1,25 @@
using System.Xml.Linq;
using SQLite;
using System;
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 CategoryType Type { get; set; }
public string Icon { get; set; } = ICON_DEFAULT;
public string Name { get; set; }
public long TintColor { get; set; } = TRANSPARENT_COLOR;
public int? ParentId { get; set; }
public override void OnXmlDeserialize(XElement node)
{
Id = Read(node, nameof(Id), 0);
Type = (CategoryType)Read(node, nameof(Type), 0);
Icon = Read(node, nameof(Icon), ICON_DEFAULT);
Name = Read(node, nameof(Name), string.Empty);
var 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 (ParentId != null)
{
Write(node, nameof(ParentId), ParentId.Value);
}
}
public DateTime? LastUsed { get; set; }
public int? LastAccountId { get; set; }
}
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.Threading.Tasks;
using Billing.Models;
using Billing.UI;
using SQLite;
using Xamarin.Essentials;
using Resource = Billing.Languages.Resource;
@ -14,113 +14,309 @@ namespace Billing.Store
public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
public static readonly string CacheFolder = FileSystem.CacheDirectory;
private const string accountFile = "accounts.xml";
private const string billFile = "bills.xml";
private const string categoryFile = "categories.xml";
public static string DatabasePath => Path.Combine(PersonalFolder, dbfile);
private static StoreHelper instance;
private static StoreHelper Instance => instance ??= new StoreHelper();
#region Sqlite3
private const string dbfile = ".master.db3";
private static SQLiteAsyncConnection database;
#endregion
public static List<Account> GetAccounts() => Instance.GetAccountsInternal();
public static void WriteAccounts(IEnumerable<Account> accounts) => Instance.WriteAccountsInternal(accounts);
public static List<Bill> GetBills() => Instance.GetBillsInternal();
public static void WriteBills(IEnumerable<Bill> bills) => Instance.WriteBillsInternal(bills);
public static List<Category> GetCategories() => Instance.GetCategoriesInternal();
public static void WriteCategories(IEnumerable<Category> categories) => Instance.WriteCategoriesInternal(categories);
private StoreHelper() { }
private List<Account> GetAccountsInternal()
public static async Task<bool> ReloadDatabase(string file)
{
return GetList<Account>(Path.Combine(PersonalFolder, accountFile));
}
private void WriteAccountsInternal(IEnumerable<Account> accounts)
{
var filename = Path.Combine(PersonalFolder, accountFile);
WriteList(filename, accounts);
}
private List<Bill> GetBillsInternal()
{
return GetList<Bill>(Path.Combine(PersonalFolder, billFile));
}
private void WriteBillsInternal(IEnumerable<Bill> bills)
{
var filename = Path.Combine(PersonalFolder, billFile);
WriteList(filename, bills);
}
private List<Category> GetCategoriesInternal()
{
var list = GetList<Category>(Path.Combine(PersonalFolder, categoryFile));
if (list == null || list.Count == 0)
var path = DatabasePath;
if (string.Equals(file, path, StringComparison.OrdinalIgnoreCase))
{
list = new List<Category>
{
// TODO: sample categories
new() { Id = 1, Name = Resource.Clothing, Icon = "clothes" },
new() { Id = 2, Name = Resource.Food, Icon = "food" },
new() { Id = 3, Name = Resource.Drinks, Icon = "drink" },
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, Type = CategoryType.Income, Name = Resource.Salary, Icon = "#brand#buffer" },
new() { Id = 10, Type = CategoryType.Income, Name = Resource.Earnings, Icon = "#brand#btc" },
new() { Id = 20, 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;
return false;
}
try
{
using var stream = File.Open(filename, FileMode.Create);
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))
if (database != null)
{
using var stream = File.OpenRead(file);
var list = ModelExtensionHelper.FromStream<T>(stream);
return list;
await database.CloseAsync();
}
}
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;
}
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
}
}

View File

@ -5,11 +5,14 @@ namespace Billing.Themes
{
public abstract class BaseTheme : ResourceDictionary
{
public const string CascadiaFontRegular = nameof(CascadiaFontRegular);
public const string CascadiaFontBold = nameof(CascadiaFontBold);
public const string RobotoCondensedFontRegular = nameof(RobotoCondensedFontRegular);
public const string RobotoCondensedFontBold = nameof(RobotoCondensedFontBold);
public const string BrandsFontRegular = nameof(BrandsFontRegular);
public const double DefaultFontSize = 15.0;
public static Color CurrentPrimaryColor => (Color)Application.Current.Resources[PrimaryColor];
public static Color CurrentTextColor => (Color)Application.Current.Resources[TextColor];
public static Color CurrentSecondaryTextColor => (Color)Application.Current.Resources[SecondaryTextColor];
public const string FontSemiBold = nameof(FontSemiBold);
public const string FontBold = nameof(FontBold);
public const string WindowBackgroundColor = nameof(WindowBackgroundColor);
public const string OptionTintColor = nameof(OptionTintColor);
@ -30,12 +33,8 @@ namespace Billing.Themes
protected void InitResources()
{
var robotoRegularFontFamily = Definition.GetRobotoCondensedRegularFontFamily();
Add(CascadiaFontRegular, Definition.GetCascadiaRegularFontFamily());
Add(CascadiaFontBold, Definition.GetCascadiaBoldFontFamily());
Add(RobotoCondensedFontRegular, Definition.GetRobotoCondensedRegularFontFamily());
Add(RobotoCondensedFontBold, Definition.GetRobotoCondensedBoldFontFamily());
Add(BrandsFontRegular, Definition.GetBrandsFontFamily());
Add(FontSemiBold, Definition.SemiBoldFontFamily);
Add(FontBold, Definition.BoldFontFamily);
Add(PrimaryColor, PrimaryMauiColor);
Add(SecondaryColor, SecondaryMauiColor);
@ -45,58 +44,63 @@ namespace Billing.Themes
{
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.FontFamilyProperty, Value = robotoRegularFontFamily }
new Setter { Property = Label.FontFamilyProperty, Value = Definition.RegularFontFamily }
}
});
Add(new Style(typeof(OptionEntry))
{
Setters =
{
new Setter { Property = Entry.FontSizeProperty, Value = Device.GetNamedSize(NamedSize.Small, typeof(Entry)) },
new Setter { Property = Entry.FontFamilyProperty, Value = robotoRegularFontFamily }
new Setter { Property = Entry.FontSizeProperty, Value = DefaultFontSize },
new Setter { Property = Entry.FontFamilyProperty, Value = Definition.RegularFontFamily }
}
});
Add(new Style(typeof(OptionEditor))
{
Setters =
{
new Setter { Property = Editor.FontSizeProperty, Value = Device.GetNamedSize(NamedSize.Small, typeof(Editor)) },
new Setter { Property = Editor.FontFamilyProperty, Value = robotoRegularFontFamily }
new Setter { Property = Editor.FontSizeProperty, Value = DefaultFontSize },
new Setter { Property = Editor.FontFamilyProperty, Value = Definition.RegularFontFamily }
}
});
Add(new Style(typeof(OptionDatePicker))
{
Setters =
{
new Setter { Property = DatePicker.FontSizeProperty, Value = Device.GetNamedSize(NamedSize.Small, typeof(DatePicker)) },
new Setter { Property = DatePicker.FontFamilyProperty, Value = robotoRegularFontFamily }
new Setter { Property = DatePicker.FontSizeProperty, Value = DefaultFontSize },
new Setter { Property = DatePicker.FontFamilyProperty, Value = Definition.RegularFontFamily }
}
});
Add(new Style(typeof(OptionTimePicker))
{
Setters =
{
new Setter { Property = TimePicker.FontSizeProperty, Value = Device.GetNamedSize(NamedSize.Small, typeof(TimePicker)) },
new Setter { Property = TimePicker.FontFamilyProperty, Value = robotoRegularFontFamily }
new Setter { Property = TimePicker.FontSizeProperty, Value = DefaultFontSize },
new Setter { Property = TimePicker.FontFamilyProperty, Value = Definition.RegularFontFamily }
}
});
Add(new Style(typeof(Button))
Add(new Style(typeof(OptionPicker))
{
Setters =
{
new Setter { Property = Button.TextColorProperty, Value = SecondaryMauiColor },
new Setter { Property = Button.FontFamilyProperty, Value = robotoRegularFontFamily },
new Setter { Property = VisualElement.BackgroundColorProperty, Value = PrimaryMauiColor },
new Setter { Property = Button.PaddingProperty, Value = new Thickness(14, 10) }
new Setter { Property = Picker.FontSizeProperty, Value = DefaultFontSize },
new Setter { Property = Picker.FontFamilyProperty, Value = Definition.RegularFontFamily }
}
});
Add(new Style(typeof(TintImage))
{
Setters =
{
new Setter { Property = TintImage.PrimaryColorProperty, Value = PrimaryMauiColor }
new Setter { Property = TintHelper.TintColorProperty, Value = PrimaryMauiColor }
}
});
Add(new Style(typeof(TintImageButton))
{
Setters =
{
new Setter { Property = TintHelper.TintColorProperty, Value = PrimaryMauiColor }
}
});
}

View File

@ -17,7 +17,7 @@
<Setter Property="HorizontalOptions" Value="Center"/>
</Style>
<ControlTemplate x:Key="weekDay">
<Grid Padding="0, 8, 0, 0" RowDefinitions="Auto, 3" RowSpacing="{OnPlatform Android=0, iOS=4}">
<Grid Padding="0, 8, 0, 0" RowDefinitions="Auto, 3" RowSpacing="4">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding OnDayTapped, Source={x:Reference billingDate}}" CommandParameter="{TemplateBinding BillingDay}"/>
</Grid.GestureRecognizers>
@ -25,14 +25,14 @@
TextColor="{DynamicResource TextColor}"
FontFamily="{TemplateBinding BillingDay.FontFamily}"
Opacity="{TemplateBinding BillingDay.TextOpacity}"/>
<StackLayout Grid.Row="1"
<StackLayout Grid.Row="1" Margin="8, 0"
IsVisible="{TemplateBinding BillingDay.IsSelected}"
BackgroundColor="{DynamicResource PrimaryColor}"
Opacity="{TemplateBinding BillingDay.Opacity}"/>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="weekEnd">
<Grid Padding="0, 8, 0, 0" RowDefinitions="Auto, 3" RowSpacing="{OnPlatform Android=0, iOS=4}">
<Grid Padding="0, 8, 0, 0" RowDefinitions="Auto, 3" RowSpacing="4">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding OnDayTapped, Source={x:Reference billingDate}}" CommandParameter="{TemplateBinding BillingDay}"/>
</Grid.GestureRecognizers>
@ -40,7 +40,7 @@
TextColor="{DynamicResource RedColor}"
FontFamily="{TemplateBinding BillingDay.FontFamily}"
Opacity="{TemplateBinding BillingDay.TextOpacity}"/>
<StackLayout Grid.Row="1"
<StackLayout Grid.Row="1" Margin="8, 0"
IsVisible="{TemplateBinding BillingDay.IsSelected}"
BackgroundColor="{DynamicResource PrimaryColor}"
Opacity="{TemplateBinding BillingDay.Opacity}"/>

View File

@ -7,13 +7,13 @@ namespace Billing.UI
{
#region UI Properties
public static readonly BindableProperty SundayProperty = BindableProperty.Create(nameof(Sunday), typeof(BillingDay), typeof(BillingDate));
public static readonly BindableProperty MondayProperty = BindableProperty.Create(nameof(Monday), typeof(BillingDay), typeof(BillingDate));
public static readonly BindableProperty TuesdayProperty = BindableProperty.Create(nameof(Tuesday), typeof(BillingDay), typeof(BillingDate));
public static readonly BindableProperty WednesdayProperty = BindableProperty.Create(nameof(Wednesday), typeof(BillingDay), typeof(BillingDate));
public static readonly BindableProperty ThursdayProperty = BindableProperty.Create(nameof(Thursday), typeof(BillingDay), typeof(BillingDate));
public static readonly BindableProperty FridayProperty = BindableProperty.Create(nameof(Friday), typeof(BillingDay), typeof(BillingDate));
public static readonly BindableProperty SaturdayProperty = BindableProperty.Create(nameof(Saturday), typeof(BillingDay), typeof(BillingDate));
public static readonly BindableProperty SundayProperty = Helper.Create<BillingDay, BillingDate>(nameof(Sunday));
public static readonly BindableProperty MondayProperty = Helper.Create<BillingDay, BillingDate>(nameof(Monday));
public static readonly BindableProperty TuesdayProperty = Helper.Create<BillingDay, BillingDate>(nameof(Tuesday));
public static readonly BindableProperty WednesdayProperty = Helper.Create<BillingDay, BillingDate>(nameof(Wednesday));
public static readonly BindableProperty ThursdayProperty = Helper.Create<BillingDay, BillingDate>(nameof(Thursday));
public static readonly BindableProperty FridayProperty = Helper.Create<BillingDay, BillingDate>(nameof(Friday));
public static readonly BindableProperty SaturdayProperty = Helper.Create<BillingDay, BillingDate>(nameof(Saturday));
public BillingDay Sunday => (BillingDay)GetValue(SundayProperty);
public BillingDay Monday => (BillingDay)GetValue(MondayProperty);
@ -39,38 +39,36 @@ namespace Billing.UI
};
}
public static readonly BindableProperty LocatedDateProperty = BindableProperty.Create(nameof(LocatedDate), typeof(DateTime), typeof(BillingDate), propertyChanged: OnLocatedDatePropertyChanged);
public static readonly BindableProperty SelectedDateProperty = BindableProperty.Create(nameof(SelectedDate), typeof(DateTime), typeof(BillingDate), propertyChanged: OnSelectedDatePropertyChanged);
public static readonly BindableProperty LocatedDateProperty = Helper.Create<DateTime, BillingDate>(nameof(LocatedDate), propertyChanged: OnLocatedDatePropertyChanged);
public static readonly BindableProperty SelectedDateProperty = Helper.Create<DateTime, BillingDate>(nameof(SelectedDate), propertyChanged: OnSelectedDatePropertyChanged);
private static void OnLocatedDatePropertyChanged(BindableObject obj, object old, object @new)
private static void OnLocatedDatePropertyChanged(BillingDate billingDate, DateTime old, DateTime date)
{
if (obj is BillingDate billingDate && @new is DateTime date)
var week = (int)date.DayOfWeek;
var tmpDate = date.AddDays(-week);
for (var i = 0; i < 7; i++)
{
var week = (int)date.DayOfWeek;
var tmpDate = date.AddDays(-week);
for (var i = 0; i < 7; i++)
{
var prop = GetWeekProperty(i);
var day = new BillingDay(tmpDate);
billingDate.SetValue(prop, day);
tmpDate = tmpDate.AddDays(1);
}
var day = new BillingDay(tmpDate);
billingDate[i] = day;
tmpDate = tmpDate.AddDays(1);
}
}
private static void OnSelectedDatePropertyChanged(BindableObject obj, object old, object @new)
private static void OnSelectedDatePropertyChanged(BillingDate billingDate, DateTime old, DateTime selected)
{
if (obj is BillingDate billingDate && @new is DateTime selected)
for (var i = 0; i < 7; i++)
{
for (var i = 0; i < 7; i++)
{
var prop = GetWeekProperty(i);
var day = (BillingDay)billingDate.GetValue(prop);
day.Refresh(selected);
}
var day = billingDate[i];
day.Refresh(selected);
}
}
public BillingDay this[int index]
{
get => (BillingDay)GetValue(GetWeekProperty(index));
set => SetValue(GetWeekProperty(index), value);
}
public DateTime LocatedDate
{
get => (DateTime)GetValue(LocatedDateProperty);
@ -99,13 +97,26 @@ namespace Billing.UI
public void SetDateTime(DateTime selectedDate, DateTime? locatedDate = null)
{
if (Helper.IsSameDay(selectedDate, SelectedDate))
{
return;
}
DateTime located;
if (locatedDate != null)
{
LocatedDate = locatedDate.Value;
located = Helper.FirstDay(locatedDate.Value);
}
else
{
LocatedDate = selectedDate;
located = Helper.FirstDay(selectedDate);
}
if (LocatedDate != located)
{
LocatedDate = located;
}
else
{
RestoreState(this[(int)selectedDate.DayOfWeek]);
}
SelectedDate = selectedDate;
DateSelected?.Invoke(this, new DateEventArgs(selectedDate));
@ -114,43 +125,48 @@ namespace Billing.UI
private void OnDayChanged(object o)
{
var selected = SelectedDate;
if (o is BillingDay day && (selected.Year != day.Date.Year || selected.DayOfYear != day.Date.DayOfYear))
if (o is BillingDay day && !Helper.IsSameDay(selected, day.Date))
{
for (var i = 0; i < 7; i++)
{
var d = (BillingDay)GetValue(GetWeekProperty(i));
if (d.IsSelected)
{
this.AbortAnimation("unselected");
this.Animate("unselected", v =>
{
d.Opacity = v;
},
start: 1, end: 0,
easing: Easing.CubicOut,
finished: (v, b) =>
{
d.Opacity = 0;
d.IsSelected = false;
});
}
}
RestoreState(day);
SelectedDate = day.Date;
this.AbortAnimation("selected");
this.Animate("selected", v =>
{
day.Opacity = v;
},
start: 0, end: 1,
easing: Easing.CubicOut,
finished: (v, b) =>
{
day.Opacity = 1;
});
DateSelected?.Invoke(this, new DateEventArgs(day.Date));
}
}
private void RestoreState(BillingDay day)
{
for (var i = 0; i < 7; i++)
{
var d = (BillingDay)GetValue(GetWeekProperty(i));
if (d.IsSelected)
{
this.AbortAnimation("unselected");
this.Animate("unselected", v =>
{
d.Opacity = v;
},
start: 1, end: 0,
easing: Easing.CubicOut,
finished: (v, b) =>
{
d.Opacity = 0;
d.IsSelected = false;
});
}
}
this.AbortAnimation("selected");
this.Animate("selected", v =>
{
day.Opacity = v;
},
start: 0, end: 1,
easing: Easing.CubicOut,
finished: (v, b) =>
{
day.Opacity = 1;
});
}
private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
if (e.StatusType == GestureStatus.Started)
@ -176,7 +192,6 @@ namespace Billing.UI
x1 = null;
var ms = (DateTime.Now - lastDate).TotalMilliseconds;
var speed = totalX / ms;
Helper.Debug($"completed, speed: {speed}");
if (speed < -0.7)
{
LocatedDate = LocatedDate.AddDays(7);
@ -185,7 +200,7 @@ namespace Billing.UI
{
LocatedDate = LocatedDate.AddDays(-7);
}
OnSelectedDatePropertyChanged(this, null, SelectedDate);
OnSelectedDatePropertyChanged(this, default, SelectedDate);
}
}
}
@ -202,50 +217,37 @@ namespace Billing.UI
public class BillingDayView : ContentView
{
public static readonly BindableProperty BillingDayProperty = BindableProperty.Create(nameof(BillingDay), typeof(BillingDay), typeof(BillingDayView));
public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(Command), typeof(BillingDayView));
public static readonly BindableProperty BillingDayProperty = Helper.Create<BillingDay, BillingDayView>(nameof(BillingDay));
public BillingDay BillingDay
{
get => (BillingDay)GetValue(BillingDayProperty);
set => SetValue(BillingDayProperty, value);
}
public Command Command
{
get => (Command)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
}
public class BillingDay : BindableObject
{
public static readonly BindableProperty DateProperty = BindableProperty.Create(nameof(Date), typeof(DateTime), typeof(BillingDay), propertyChanged: OnDatePropertyChanged);
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(BillingDay));
public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(BillingDay), defaultValue: Definition.GetCascadiaRegularFontFamily());
public static readonly BindableProperty IsSelectedProperty = BindableProperty.Create(nameof(IsSelected), typeof(bool), typeof(BillingDay));
public static readonly BindableProperty OpacityProperty = BindableProperty.Create(nameof(Opacity), typeof(double), typeof(BillingDay), defaultValue: 1.0);
public static readonly BindableProperty TextOpacityProperty = BindableProperty.Create(nameof(TextOpacity), typeof(double), typeof(BillingDay), defaultValue: 1.0);
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 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 OpacityProperty = Helper.Create<double, BillingDay>(nameof(Opacity), defaultValue: 1.0);
public static readonly BindableProperty TextOpacityProperty = Helper.Create<double, BillingDay>(nameof(TextOpacity), defaultValue: 1.0);
private static void OnDatePropertyChanged(BindableObject obj, object old, object @new)
private static void OnDatePropertyChanged(BillingDay day, DateTime old, DateTime date)
{
if (obj is BillingDay day && @new is DateTime date)
if (date.Day == 1)
{
if (date.Day == 1)
{
day.SetValue(TextProperty, date.ToString("MMM"));
}
else
{
day.SetValue(TextProperty, date.Day.ToString());
}
day.SetValue(TextProperty, date.ToString("MMM"));
}
else
{
day.SetValue(TextProperty, date.Day.ToString());
}
}
public DateTime Date
{
get => (DateTime)GetValue(DateProperty);
set => SetValue(DateProperty, value);
}
public DateTime Date => (DateTime)GetValue(DateProperty);
public string Text => (string)GetValue(TextProperty);
public string FontFamily => (string)GetValue(FontFamilyProperty);
public bool IsSelected
@ -262,20 +264,20 @@ namespace Billing.UI
public BillingDay(DateTime date)
{
Date = date;
SetValue(DateProperty, date);
}
public void Refresh(DateTime selected)
{
var date = Date;
if (date.Year == selected.Year && date.DayOfYear == selected.DayOfYear)
if (Helper.IsSameDay(date, selected))
{
SetValue(IsSelectedProperty, true);
SetValue(FontFamilyProperty, Definition.GetCascadiaBoldFontFamily());
IsSelected = true;
SetValue(FontFamilyProperty, Definition.BoldFontFamily);
}
else
{
SetValue(FontFamilyProperty, Definition.GetCascadiaRegularFontFamily());
SetValue(FontFamilyProperty, Definition.RegularFontFamily);
}
if (date.Year == selected.Year && date.Month == selected.Month)
{

View File

@ -1,13 +1,44 @@
using Billing.Themes;
using System;
using Billing.Themes;
using Xamarin.Forms;
namespace Billing.UI
{
public abstract class BillingPage : ContentPage
{
public event EventHandler Loaded;
public event EventHandler Refreshed;
private bool loaded;
public BillingPage()
{
SetDynamicResource(BackgroundColorProperty, BaseTheme.WindowBackgroundColor);
Shell.SetTabBarIsVisible(this, false);
}
protected virtual void OnLoaded()
{
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

@ -0,0 +1,141 @@
using SkiaSharp;
using SkiaSharp.Views.Forms;
using System;
using Xamarin.Forms;
namespace Billing.UI
{
public class ColorPicker : SKCanvasView
{
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
{
get => (Color)GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
public Command Command
{
get => (Command)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
private SKPoint? lastTouch;
public ColorPicker()
{
EnableTouchEvents = true;
}
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);
var skInfo = e.Info;
var skSurface = e.Surface;
var skCanvas = skSurface.Canvas;
var width = skInfo.Width;
var height = skInfo.Height;
skCanvas.Clear(SKColors.White);
using (var paint = new SKPaint())
{
paint.IsAntialias = true;
var colors = new SKColor[]
{
new SKColor(255, 0, 0), // Red
new SKColor(255, 255, 0), // Yellow
new SKColor(0, 255, 0), // Green (Lime)
new SKColor(0, 255, 255), // Aqua
new SKColor(0, 0, 255), // Blue
new SKColor(255, 0, 255), // Fuchsia
new SKColor(255, 0, 0), // Red
};
using var shader = SKShader.CreateLinearGradient(
new SKPoint(0, 0),
new SKPoint(width, 0),
colors,
null,
SKShaderTileMode.Clamp);
paint.Shader = shader;
skCanvas.DrawPaint(paint);
}
using (var paint = new SKPaint())
{
paint.IsAntialias = true;
var colors = new SKColor[]
{
SKColors.Transparent,
SKColors.Black
};
using var shader = SKShader.CreateLinearGradient(
new SKPoint(0, 0),
new SKPoint(0, height),
colors,
null,
SKShaderTileMode.Clamp);
paint.Shader = shader;
skCanvas.DrawPaint(paint);
}
if (lastTouch != null)
{
var touch = lastTouch.Value;
SKColor touchColor;
using (SKBitmap bitmap = new(skInfo))
{
IntPtr dstPixels = bitmap.GetPixels();
skSurface.ReadPixels(
skInfo,
dstPixels,
skInfo.RowBytes,
(int)touch.X, (int)touch.Y);
touchColor = bitmap.GetPixel(0, 0);
}
using (SKPaint paintTouch = new())
{
paintTouch.Style = SKPaintStyle.Fill;
paintTouch.Color = SKColors.White;
paintTouch.IsAntialias = true;
skCanvas.DrawCircle(
touch.X,
touch.Y,
18,
paintTouch);
paintTouch.Color = touchColor;
skCanvas.DrawCircle(
touch.X,
touch.Y,
12,
paintTouch);
}
var color = touchColor.ToFormsColor();
Color = color;
Command?.Execute(color);
}
}
protected override void OnTouch(SKTouchEventArgs e)
{
base.OnTouch(e);
lastTouch = e.Location;
var size = CanvasSize;
if (e.Location.X > 0 && e.Location.X < size.Width &&
e.Location.Y > 0 && e.Location.Y < size.Height)
{
e.Handled = true;
InvalidateSurface();
}
}
}
}

View File

@ -14,6 +14,10 @@ namespace Billing.UI
{
if (value is DateTime date)
{
if (date.Year <= 1900)
{
return null;
}
return date.ToString(Resource.TitleDateFormat);
}
return value;
@ -107,10 +111,16 @@ namespace Billing.UI
public class TimeConverter : IValueConverter
{
public bool IncludeDate { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is DateTime time)
{
if (IncludeDate)
{
return time.ToString("MM-dd HH:mm");
}
return time.ToString("HH:mm");
}
return string.Empty;
@ -195,13 +205,13 @@ namespace Billing.UI
{
if (!int.TryParse(key, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int i))
{
return ImageSource.FromFile(BaseModel.ICON_DEFAULT);
return ImageSource.FromFile(Definition.DefaultIcon);
}
glyph = char.ConvertFromUtf32(i);
}
return new FontImageSource
{
FontFamily = Definition.GetBrandsFontFamily(),
FontFamily = Definition.BrandsFontFamily,
Size = 20,
Glyph = glyph,
Color = Color.Black
@ -217,4 +227,40 @@ namespace Billing.UI
throw new NotImplementedException();
}
}
public class SelectBackgroundColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool b && b)
{
return Application.Current.Resources[BaseTheme.PromptBackgroundColor];
}
return default(Color);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
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

@ -3,17 +3,19 @@ using Xamarin.Forms;
namespace Billing.UI
{
public class TintImage : Image
public class TintHelper
{
public static readonly BindableProperty PrimaryColorProperty = BindableProperty.Create(nameof(PrimaryColor), typeof(Color?), typeof(TintImage));
public const string TintColor = nameof(TintColor);
public static readonly BindableProperty TintColorProperty = BindableProperty.CreateAttached(TintColor, typeof(Color?), typeof(TintHelper), null);
public Color? PrimaryColor
{
get => (Color?)GetValue(PrimaryColorProperty);
set => SetValue(PrimaryColorProperty, value);
}
public static void SetTintColor(BindableObject obj, Color? color) => obj.SetValue(TintColorProperty, color);
public static Color? GetTintColor(BindableObject obj) => (Color?)obj.GetValue(TintColorProperty);
}
public class TintImage : Image { }
public class TintImageButton : ImageButton { }
public class LongPressButton : Button
{
public event EventHandler LongPressed;
@ -32,8 +34,8 @@ namespace Billing.UI
public class LongPressGrid : Grid
{
public static readonly BindableProperty LongCommandProperty = BindableProperty.Create(nameof(LongCommand), typeof(Command), typeof(LongPressGrid));
public static readonly BindableProperty LongCommandParameterProperty = BindableProperty.Create(nameof(LongCommandParameter), typeof(object), typeof(LongPressGrid));
public static readonly BindableProperty LongCommandProperty = Helper.Create<Command, LongPressGrid>(nameof(LongCommand));
public static readonly BindableProperty LongCommandParameterProperty = Helper.Create<object, LongPressGrid>(nameof(LongCommandParameter));
public Command LongCommand
{
@ -60,4 +62,6 @@ namespace Billing.UI
}
}
}
public class BlurryPanel : ContentView { }
}

View File

@ -1,23 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Xml.Linq;
using Billing.Languages;
using Billing.Models;
using Xamarin.Forms;
namespace Billing.UI
{
public static partial class Definition
{
public static string PrimaryColorKey = "PrimaryColor";
public static partial (string main, long build) GetVersion();
public static partial string GetCascadiaRegularFontFamily();
public static partial string GetCascadiaBoldFontFamily();
public static partial string GetRobotoCondensedRegularFontFamily();
public static partial string GetRobotoCondensedBoldFontFamily();
public static partial string GetBrandsFontFamily();
public const string SaveLocationKey = "SaveLocationKey";
public const string PrimaryColorKey = "PrimaryColor";
public const string DefaultIcon = "ic_default";
public const long TransparentColor = 0x00ffffffL;
}
public static class ExtensionHelper
@ -93,50 +86,39 @@ namespace Billing.UI
var result = await page.DisplayActionSheet(message, Resource.No, yes);
return result == yes;
}
}
public static class ModelExtensionHelper
{
public static List<T> FromStream<T>(Stream stream) where T : IModel, new()
public static DateTime LastMoment(this DateTime date)
{
XDocument doc = XDocument.Load(stream);
var root = doc.Root;
var list = new List<T>();
foreach (XElement ele in root.Elements("item"))
{
if (ele.Attribute("null")?.Value == "1")
{
list.Add(default);
}
else
{
T value = new();
value.OnXmlDeserialize(ele);
list.Add(value);
}
}
return list;
// add 23:59:59.999...
return date.AddTicks(863999999999);
}
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");
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);
}
return (color & 0xff000000L) == 0x00000000L;
}
XDocument doc = new(new XDeclaration("1.0", "utf-8", "yes"), root);
doc.Save(stream, SaveOptions.DisableFormatting);
public static Color ToColor(this long color)
{
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 static readonly BindableProperty GroupHeaderTemplateProperty = BindableProperty.Create(nameof(GroupHeaderTemplate), typeof(DataTemplate), typeof(GroupStackLayout));
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(GroupStackLayout));
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IList), typeof(GroupStackLayout), propertyChanged: OnItemsSourcePropertyChanged);
public static readonly BindableProperty SpacingProperty = BindableProperty.Create(nameof(Spacing), typeof(double), typeof(GroupStackLayout), defaultValue: 4d);
public static readonly BindableProperty RowHeightProperty = BindableProperty.Create(nameof(RowHeight), typeof(double), typeof(GroupStackLayout), defaultValue: 32d);
public static readonly BindableProperty GroupHeightProperty = BindableProperty.Create(nameof(GroupHeight), typeof(double), typeof(GroupStackLayout), defaultValue: 24d);
public static readonly BindableProperty GroupHeaderTemplateProperty = Helper.Create<DataTemplate, GroupStackLayout>(nameof(GroupHeaderTemplate));
public static readonly BindableProperty ItemTemplateProperty = Helper.Create<DataTemplate, GroupStackLayout>(nameof(ItemTemplate));
public static readonly BindableProperty ItemsSourceProperty = Helper.Create<IList, GroupStackLayout>(nameof(ItemsSource), propertyChanged: OnItemsSourcePropertyChanged);
public static readonly BindableProperty SpacingProperty = Helper.Create<double, GroupStackLayout>(nameof(Spacing), defaultValue: 4d);
public static readonly BindableProperty RowHeightProperty = Helper.Create<double, GroupStackLayout>(nameof(RowHeight), defaultValue: 32d);
public static readonly BindableProperty GroupHeightProperty = Helper.Create<double, GroupStackLayout>(nameof(GroupHeight), defaultValue: 24d);
public DataTemplate GroupHeaderTemplate
{
@ -43,17 +43,16 @@ namespace Billing.UI
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;
if (@new == null)
if (list == null)
{
//stack.cachedLayout.Clear();
stack.Children.Clear();
stack.InvalidateLayout();
}
else if (@new is IList list)
else
{
stack.freezed = true;
//stack.cachedLayout.Clear();
@ -122,8 +121,9 @@ namespace Billing.UI
{
return;
}
var padding = Padding;
var spacing = Spacing;
var lastHeight = 0d;
var lastHeight = padding.Top;
var rowHeight = RowHeight;
var groupHeight = GroupHeight;
foreach (var item in Children)
@ -154,7 +154,7 @@ namespace Billing.UI
{
itemHeight = rowHeight;
}
var rect = new Rectangle(0, lastHeight, width, itemHeight);
var rect = new Rectangle(padding.Left, lastHeight, width, itemHeight);
//item.Layout(rect);
LayoutChildIntoBoundingRegion(item, rect);
lastHeight += itemHeight + spacing;
@ -185,7 +185,8 @@ namespace Billing.UI
lastHeight += rowHeight + spacing;
}
}
lastSizeRequest = new SizeRequest(new Size(widthConstraint, lastHeight));
var padding = Padding;
lastSizeRequest = new SizeRequest(new Size(widthConstraint, lastHeight + padding.Top + padding.Bottom));
return lastSizeRequest;
}
}

View File

@ -5,7 +5,7 @@ using Xamarin.Forms;
namespace Billing.UI
{
public class ItemSelectPage<T> : ContentPage
public class ItemSelectPage<T> : BillingPage
{
public event EventHandler<T> ItemTapped;

View File

@ -4,16 +4,32 @@ using Xamarin.Forms;
namespace Billing.UI
{
public enum BorderStyle
{
None = 0,
RoundedRect
}
public class OptionEntry : Entry { }
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 OptionTimePicker : TimePicker { }
public abstract class OptionCell : ViewCell
{
public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(OptionCell));
public static readonly BindableProperty BackgroundColorProperty = BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(OptionCell));
public static readonly BindableProperty IconProperty = BindableProperty.Create(nameof(Icon), typeof(ImageSource), typeof(OptionCell));
public static readonly BindableProperty TitleProperty = Helper.Create<string, OptionCell>(nameof(Title));
public static readonly BindableProperty BackgroundColorProperty = Helper.Create<Color, OptionCell>(nameof(BackgroundColor));
public static readonly BindableProperty IconProperty = Helper.Create<ImageSource, OptionCell>(nameof(Icon));
public string Title
{
@ -43,8 +59,8 @@ namespace Billing.UI
ColumnDefinitions =
{
new ColumnDefinition { Width = GridLength.Auto },
new ColumnDefinition { Width = new GridLength(.3, GridUnitType.Star) },
new ColumnDefinition { Width = new GridLength(.7, GridUnitType.Star) }
new ColumnDefinition { Width = new GridLength(.35, GridUnitType.Star) },
new ColumnDefinition { Width = new GridLength(.65, GridUnitType.Star) }
},
Children =
{
@ -124,7 +140,7 @@ namespace Billing.UI
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
{
@ -143,8 +159,8 @@ namespace Billing.UI
public class OptionSelectCell : OptionTextCell
{
public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(Command), typeof(OptionSelectCell));
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(OptionSelectCell));
public static readonly BindableProperty CommandProperty = Helper.Create<Command, OptionSelectCell>(nameof(Command));
public static readonly BindableProperty CommandParameterProperty = Helper.Create<object, OptionSelectCell>(nameof(CommandParameter));
public Command Command
{
@ -189,7 +205,8 @@ namespace Billing.UI
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 = Helper.Create<Color?, OptionImageCell>(nameof(TintColor));
[TypeConverter(typeof(ImageSourceConverter))]
public ImageSource ImageSource
@ -197,6 +214,12 @@ namespace Billing.UI
get => (ImageSource)GetValue(ImageSourceProperty);
set => SetValue(ImageSourceProperty, value);
}
[TypeConverter(typeof(ColorTypeConverter))]
public Color? TintColor
{
get => (Color?)GetValue(TintColorProperty);
set => SetValue(TintColorProperty, value);
}
protected override View Content => new StackLayout
{
@ -211,7 +234,15 @@ namespace Billing.UI
VerticalOptions = LayoutOptions.Center,
Margin = new Thickness(6, 0)
}
.Binding(Image.SourceProperty, nameof(ImageSource)),
.Binding(Image.SourceProperty, nameof(ImageSource))
.Binding(TintHelper.TintColorProperty, nameof(TintColor)),
new Label
{
VerticalOptions = LayoutOptions.Center
}
.Binding(Label.TextProperty, nameof(Detail))
.DynamicResource(Label.TextColorProperty, BaseTheme.SecondaryTextColor),
new TintImage
{
@ -232,7 +263,7 @@ namespace Billing.UI
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
{
@ -250,7 +281,7 @@ namespace Billing.UI
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
{
@ -261,7 +292,7 @@ namespace Billing.UI
protected override View Content => new OptionDatePicker
{
HorizontalOptions = LayoutOptions.End,
VerticalOptions = LayoutOptions.Center
VerticalOptions = LayoutOptions.Fill
}
.Binding(DatePicker.DateProperty, nameof(Date), mode: BindingMode.TwoWay)
.DynamicResource(DatePicker.TextColorProperty, BaseTheme.TextColor)
@ -270,7 +301,7 @@ namespace Billing.UI
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
{
@ -281,7 +312,7 @@ namespace Billing.UI
protected override View Content => new OptionTimePicker
{
HorizontalOptions = LayoutOptions.End,
VerticalOptions = LayoutOptions.Center
VerticalOptions = LayoutOptions.Fill
}
.Binding(TimePicker.TimeProperty, nameof(Time), mode: BindingMode.TwoWay)
.DynamicResource(TimePicker.TextColorProperty, BaseTheme.TextColor)
@ -290,9 +321,9 @@ namespace Billing.UI
public class OptionEntryCell : OptionCell
{
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(OptionEntryCell));
public static readonly BindableProperty KeyboardProperty = BindableProperty.Create(nameof(Keyboard), typeof(Keyboard), typeof(OptionEntryCell));
public static readonly BindableProperty PlaceholderProperty = BindableProperty.Create(nameof(Placeholder), typeof(string), typeof(OptionEntryCell));
public static readonly BindableProperty TextProperty = Helper.Create<string, OptionEntryCell>(nameof(Text));
public static readonly BindableProperty KeyboardProperty = Helper.Create<Keyboard, OptionEntryCell>(nameof(Keyboard), defaultValue: Keyboard.Default);
public static readonly BindableProperty PlaceholderProperty = Helper.Create<string, OptionEntryCell>(nameof(Placeholder));
public string Text
{
@ -320,7 +351,8 @@ namespace Billing.UI
{
HorizontalOptions = LayoutOptions.Fill,
HorizontalTextAlignment = TextAlignment.End,
VerticalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Fill,
VerticalTextAlignment = TextAlignment.Center,
ReturnType = ReturnType.Next
}
.Binding(Entry.TextProperty, nameof(Text), mode: BindingMode.TwoWay)
@ -342,10 +374,10 @@ namespace Billing.UI
public class OptionEditorCell : OptionVerticalCell
{
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(OptionEditorCell));
public static readonly BindableProperty FontSizeProperty = BindableProperty.Create(nameof(FontSize), typeof(double), typeof(OptionEditorCell), 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 PlaceholderProperty = BindableProperty.Create(nameof(Placeholder), typeof(string), typeof(OptionEditorCell));
public static readonly BindableProperty TextProperty = Helper.Create<string, OptionEditorCell>(nameof(Text));
public static readonly BindableProperty FontSizeProperty = Helper.Create<double, OptionEditorCell>(nameof(FontSize), defaultValue: Device.GetNamedSize(NamedSize.Default, typeof(Editor)));
public static readonly BindableProperty KeyboardProperty = Helper.Create<Keyboard, OptionEditorCell>(nameof(Keyboard), defaultValue: Keyboard.Default);
public static readonly BindableProperty PlaceholderProperty = Helper.Create<string, OptionEditorCell>(nameof(Placeholder));
public string Text
{
@ -369,7 +401,17 @@ namespace Billing.UI
set => SetValue(PlaceholderProperty, value);
}
protected override View Content => new OptionEditor
OptionEditor editor;
public void SetFocus()
{
if (editor != null)
{
editor.Focus();
}
}
protected override View Content => editor = new OptionEditor
{
HorizontalOptions = LayoutOptions.Fill,
VerticalOptions = LayoutOptions.Fill

View File

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
namespace Billing.UI
{
public class SegmentedControl : View, IViewContainer<SegmentedControlOption>
{
public IList<SegmentedControlOption> Children { get; set; }
public SegmentedControl()
{
Children = new List<SegmentedControlOption>();
}
public static readonly BindableProperty TintColorProperty = Helper.Create<Color, SegmentedControl>(nameof(TintColor));
public static readonly BindableProperty DisabledColorProperty = Helper.Create<Color, SegmentedControl>(nameof(DisabledColor));
public static readonly BindableProperty SelectedTextColorProperty = Helper.Create<Color, SegmentedControl>(nameof(SelectedTextColor));
public static readonly BindableProperty SelectedSegmentIndexProperty = Helper.Create<int, SegmentedControl>(nameof(SelectedSegmentIndex));
public Color TintColor
{
get => (Color)GetValue(TintColorProperty);
set => SetValue(TintColorProperty, value);
}
public Color DisabledColor
{
get => (Color)GetValue(DisabledColorProperty);
set => SetValue(DisabledColorProperty, value);
}
public Color SelectedTextColor
{
get => (Color)GetValue(SelectedTextColorProperty);
set => SetValue(SelectedTextColorProperty, value);
}
public int SelectedSegmentIndex
{
get => (int)GetValue(SelectedSegmentIndexProperty);
set => SetValue(SelectedSegmentIndexProperty, value);
}
public SegmentedControlOption SelectedSegment => Children[SelectedSegmentIndex];
public event EventHandler<ValueChangedEventArgs> ValueChanged;
//[EditorBrowsable(EditorBrowsableState.Never)]
public void SendValueChanged()
{
ValueChanged?.Invoke(this, new ValueChangedEventArgs { NewValue = SelectedSegmentIndex });
}
}
public class SegmentedControlOption : View
{
public static readonly BindableProperty TextProperty = Helper.Create<string, SegmentedControlOption>(nameof(Text));
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public object Value { get; set; }
}
public class ValueChangedEventArgs : EventArgs
{
public int NewValue { get; set; }
}
}

View File

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

View File

@ -9,7 +9,8 @@
x:Name="accountPage"
x:DataType="v:AccountPage"
Title="{r:Text Accounts}"
BindingContext="{x:Reference accountPage}">
BindingContext="{x:Reference accountPage}"
Shell.TabBarIsVisible="True">
<ContentPage.Resources>
<ui:MoneyConverter x:Key="moneyConverter"/>
@ -27,9 +28,9 @@
<!--<Grid.Effects>
<ui:ShadowEffect Offect="0, 3" Color="Black" Opacity=".4"/>
</Grid.Effects>-->
<StackLayout VerticalOptions="Center" Margin="20, 0" Spacing="0">
<StackLayout VerticalOptions="Center" Margin="20, 0" Spacing="4">
<Label FontSize="Small" Text="{r:Text Balance}"/>
<Label FontSize="24" FontFamily="{DynamicResource RobotoCondensedFontBold}"
<Label FontSize="24" FontFamily="{DynamicResource FontSemiBold}"
Text="{Binding Balance, Converter={StaticResource moneyConverter}}"/>
</StackLayout>
<Grid Grid.Column="1" Margin="20, 0" VerticalOptions="Center" HorizontalOptions="End"
@ -43,7 +44,8 @@
Text="{Binding Liability, Converter={StaticResource moneyConverter}}"/>
</Grid>
</Grid>
<ui:GroupStackLayout x:Name="groupLayout" ItemsSource="{Binding Accounts}" Margin="0, 10, 0, 0" GroupHeight="40">
<ui:GroupStackLayout x:Name="groupLayout" ItemsSource="{Binding Accounts}" Margin="0, 10, 0, 0"
GroupHeight="36" RowHeight="44">
<ui:GroupStackLayout.GroupHeaderTemplate>
<DataTemplate x:DataType="v:AccountGrouping">
<StackLayout Orientation="Horizontal" Padding="10, 0" VerticalOptions="End">

View File

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Billing.Languages;
using Billing.Models;
using Billing.Store;
using Billing.UI;
using Xamarin.Forms;
@ -10,10 +10,10 @@ namespace Billing.Views
{
public partial class AccountPage : BillingPage
{
private static readonly BindableProperty BalanceProperty = BindableProperty.Create(nameof(Balance), typeof(decimal), typeof(AccountPage));
private static readonly BindableProperty AssetProperty = BindableProperty.Create(nameof(Asset), typeof(decimal), typeof(AccountPage));
private static readonly BindableProperty LiabilityProperty = BindableProperty.Create(nameof(Liability), typeof(decimal), typeof(AccountPage));
private static readonly BindableProperty AccountsProperty = BindableProperty.Create(nameof(Accounts), typeof(List<AccountGrouping>), typeof(AccountPage));
private static readonly BindableProperty BalanceProperty = Helper.Create<decimal, AccountPage>(nameof(Balance));
private static readonly BindableProperty AssetProperty = Helper.Create<decimal, AccountPage>(nameof(Asset));
private static readonly BindableProperty LiabilityProperty = Helper.Create<decimal, AccountPage>(nameof(Liability));
private static readonly BindableProperty AccountsProperty = Helper.Create<List<AccountGrouping>, AccountPage>(nameof(Accounts));
public decimal Balance => (decimal)GetValue(BalanceProperty);
public decimal Asset => (decimal)GetValue(AssetProperty);
@ -46,8 +46,19 @@ namespace Billing.Views
AddToAccountGroup(account);
}
}
groupLayout.Refresh(accounts);
RefreshBalance(true);
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)
@ -64,17 +75,6 @@ namespace Billing.Views
private void AddToAccountGroup(Account account)
{
int maxId;
if (accounts.Count > 0)
{
maxId = accounts.Max(g => g.Max(a => a.Id));
}
else
{
maxId = -1;
}
account.Id = maxId + 1;
var group = accounts.FirstOrDefault(g => g.Key == account.Category);
if (group == null)
{
@ -120,34 +120,40 @@ namespace Billing.Views
if (group == null)
{
Helper.Error("account.delete", "unexpected deleting account, cannot find the current category");
return;
}
group.Remove(account);
if (group.Count == 0)
}
else
{
accounts.Remove(group);
group.Remove(account);
if (group.Count == 0)
{
accounts.Remove(group);
}
}
groupLayout.Refresh(accounts);
RefreshBalance();
groupLayout.Refresh(accounts);
RankPage.Instance?.SetNeedRefresh();
_ = Task.Run(App.WriteAccounts);
await StoreHelper.DeleteAccountItemAsync(account);
}
}
}
}
private void AccountChecked(object sender, AccountEventArgs e)
private async void AccountChecked(object sender, AccountEventArgs e)
{
var add = e.Account.Id < 0;
var add = e.Account.Id <= 0;
if (add)
{
App.Accounts.Add(e.Account);
AddToAccountGroup(e.Account);
}
groupLayout.Refresh(accounts);
RefreshBalance(!add);
groupLayout.Refresh(accounts);
Task.Run(App.WriteAccounts);
RankPage.Instance?.SetNeedRefresh();
await StoreHelper.SaveAccountItemAsync(e.Account);
}
}

View File

@ -21,13 +21,15 @@
<ContentPage.Content>
<TableView Intent="Settings" HasUnevenRows="True">
<TableSection Title=" ">
<ui:OptionEditorCell Height="120" Icon="pencil.png" FontSize="20" Keyboard="Text"
<ui:OptionEditorCell x:Name="editorName" Height="120" Icon="pencil.png"
FontSize="20" Keyboard="Text"
Title="{r:Text AccountName}"
Text="{Binding AccountName, Mode=TwoWay}"
Placeholder="{r:Text AccountNamePlaceholder}"/>
<ui:OptionImageCell Height="44" Icon="face.png"
Title="{r:Text Icon}"
ImageSource="{Binding AccountIcon, Converter={StaticResource iconConverter}}"
TintColor="{DynamicResource PrimaryColor}"
Command="{Binding SelectIcon}"/>
<ui:OptionSelectCell Height="44" Icon="project.png"
Title="{r:Text Category}"

View File

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

View File

@ -10,13 +10,20 @@
BindingContext="{x:Reference billPage}">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" IconImageSource="pin.png" Command="{Binding ViewLocation}"/>
<ToolbarItem Order="Primary" IconImageSource="check.png" Command="{Binding CheckBill}"/>
</ContentPage.ToolbarItems>
<ContentPage.Resources>
<ui:IconConverter x:Key="iconConverter"/>
<ui:TintColorConverter x:Key="tintColorConverter"/>
</ContentPage.Resources>
<ContentPage.Content>
<TableView Intent="Settings" HasUnevenRows="True">
<TableSection Title=" ">
<ui:OptionEditorCell Height="120" Icon="yuan.png" FontSize="20" Keyboard="Numeric"
<ui:OptionEditorCell x:Name="editorAmount" Height="120" Icon="yuan.png"
FontSize="20" Keyboard="Numeric"
Text="{Binding Amount, Mode=TwoWay}"
Placeholder="0.00"/>
</TableSection>
@ -28,14 +35,18 @@
Title="{r:Text Name}"
Text="{Binding Name, Mode=TwoWay}"
Placeholder="{r:Text NamePlaceholder}"/>
<ui:OptionSelectCell Height="44" Icon="project.png"
Title="{r:Text Category}"
Detail="{Binding CategoryName}"
Command="{Binding SelectCategory}"/>
<ui:OptionSelectCell Height="44" Icon="wallet.png"
Title="{r:Text Account}"
Detail="{Binding WalletName}"
Command="{Binding SelectWallet}"/>
<ui:OptionImageCell Height="44" Icon="project.png"
Title="{r:Text Category}"
Detail="{Binding Category.Name}"
ImageSource="{Binding Category.Icon, Converter={StaticResource iconConverter}}"
TintColor="{Binding Category.TintColor, Converter={StaticResource tintColorConverter}}"
Command="{Binding SelectCategory}"/>
<ui:OptionImageCell Height="44" Icon="wallet.png"
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"
Title="{r:Text Store}"
Text="{Binding Store, Mode=TwoWay}"/>

View File

@ -1,24 +1,31 @@
using System;
using System.Globalization;
using System.Linq;
using Billing.Languages;
using System.Threading;
using System.Threading.Tasks;
using Billing.Models;
using Billing.Store;
using Billing.UI;
using Xamarin.Essentials;
using Xamarin.Forms;
using Resource = Billing.Languages.Resource;
namespace Billing.Views
{
public partial class AddBillPage : BillingPage
{
private static readonly BindableProperty AmountProperty = BindableProperty.Create(nameof(Amount), typeof(string), typeof(AddBillPage));
private static readonly BindableProperty NameProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(AddBillPage));
private static readonly BindableProperty CategoryNameProperty = BindableProperty.Create(nameof(CategoryName), typeof(string), typeof(AddBillPage));
private static readonly BindableProperty WalletNameProperty = BindableProperty.Create(nameof(WalletName), typeof(string), typeof(AddBillPage));
private static readonly BindableProperty StoreProperty = BindableProperty.Create(nameof(Store), typeof(string), typeof(AddBillPage));
private static readonly BindableProperty CreatedDateProperty = BindableProperty.Create(nameof(CreatedDate), typeof(DateTime), typeof(AddBillPage));
private static readonly BindableProperty CreatedTimeProperty = BindableProperty.Create(nameof(CreatedTime), typeof(TimeSpan), typeof(AddBillPage));
private static readonly BindableProperty NoteProperty = BindableProperty.Create(nameof(Note), typeof(string), typeof(AddBillPage));
private static readonly BindableProperty CheckBillProperty = Helper.Create<Command, AddBillPage>(nameof(CheckBill), defaultValue: new Command(() => { }, () => false));
private static readonly BindableProperty AmountProperty = Helper.Create<string, AddBillPage>(nameof(Amount));
private static readonly BindableProperty NameProperty = Helper.Create<string, AddBillPage>(nameof(Name), defaultValue: string.Empty);
private static readonly BindableProperty CategoryProperty = Helper.Create<Category, AddBillPage>(nameof(Category));
private static readonly BindableProperty WalletProperty = Helper.Create<Account, AddBillPage>(nameof(Wallet));
private static readonly BindableProperty StoreProperty = Helper.Create<string, AddBillPage>(nameof(Store));
private static readonly BindableProperty CreatedDateProperty = Helper.Create<DateTime, AddBillPage>(nameof(CreatedDate));
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
{
get => (string)GetValue(AmountProperty);
@ -29,8 +36,8 @@ namespace Billing.Views
get => (string)GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
public string CategoryName => (string)GetValue(CategoryNameProperty);
public string WalletName => (string)GetValue(WalletNameProperty);
public Category Category => (Category)GetValue(CategoryProperty);
public Account Wallet => (Account)GetValue(WalletProperty);
public string Store
{
get => (string)GetValue(StoreProperty);
@ -52,22 +59,23 @@ namespace Billing.Views
set => SetValue(NoteProperty, value);
}
public Command CheckBill { get; }
public Command SelectCategory { get; }
public Command SelectWallet { get; }
public Command ViewLocation => (Command)GetValue(ViewLocationProperty);
public event EventHandler<Bill> BillChecked;
private readonly Bill bill;
private readonly DateTime createDate;
private int walletId;
private int categoryId;
private bool categoryChanged;
private CancellationTokenSource tokenSource;
private Location location;
public AddBillPage() : this(DateTime.Today) { }
public AddBillPage(DateTime date)
{
createDate = date;
CheckBill = new Command(OnCheckBill);
SelectCategory = new Command(OnSelectCategory);
SelectWallet = new Command(OnSelectWallet);
InitializeComponent();
@ -79,25 +87,38 @@ namespace Billing.Views
public AddBillPage(Bill bill)
{
this.bill = bill;
CheckBill = new Command(OnCheckBill);
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();
Title = Resource.EditBill;
Initial();
}
protected override void OnDisappearing()
{
if (tokenSource != null && !tokenSource.IsCancellationRequested)
{
tokenSource.Cancel();
}
base.OnDisappearing();
}
private void Initial()
{
if (bill != null)
{
Amount = Math.Abs(bill.Amount).ToString(CultureInfo.InvariantCulture);
Name = bill.Name;
walletId = bill.WalletId;
categoryId = bill.CategoryId;
SetValue(WalletNameProperty, App.Accounts.FirstOrDefault(a => a.Id == walletId)?.Name);
SetValue(CategoryNameProperty, App.Categories.FirstOrDefault(c => c.Id == categoryId)?.Name);
SetValue(WalletProperty, App.Accounts.FirstOrDefault(a => a.Id == bill.WalletId) ?? Account.Empty);
SetValue(CategoryProperty, App.Categories.FirstOrDefault(c => c.Id == bill.CategoryId) ?? Category.Empty);
categoryChanged = true;
Store = bill.Store;
CreatedDate = bill.CreateTime.Date;
CreatedTime = bill.CreateTime.TimeOfDay;
@ -105,17 +126,61 @@ namespace Billing.Views
}
else
{
var first = App.Accounts.First();
walletId = first.Id;
SetValue(WalletNameProperty, first.Name);
var firstCategory = App.Categories.First();
categoryId = firstCategory.Id;
SetValue(CategoryNameProperty, firstCategory.Name);
SetValue(WalletProperty, App.Accounts.FirstOrDefault() ?? Account.Empty);
SetValue(CategoryProperty, App.Categories.FirstOrDefault() ?? Category.Empty);
CreatedDate = createDate.Date;
CreatedTime = DateTime.Now.TimeOfDay;
}
}
protected override void OnLoaded()
{
if (bill == null)
{
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()
{
if (Tap.IsBusy)
@ -133,8 +198,9 @@ namespace Billing.Views
await this.ShowMessage(Resource.AmountRequired);
return;
}
var category = Category;
var wallet = Wallet;
amount = Math.Abs(amount);
var category = App.Categories.FirstOrDefault(c => c.Id == categoryId);
if (category.Type == CategoryType.Spending)
{
amount *= -1;
@ -145,27 +211,42 @@ namespace Billing.Views
{
name = category.Name;
}
if (bill != null)
{
bill.Amount = amount;
bill.Name = name;
bill.CategoryId = categoryId;
bill.WalletId = walletId;
bill.CreateTime = CreatedDate.Date.Add(CreatedTime);
bill.Store = Store;
bill.Note = Note;
Bill b = bill;
if (b == null)
{
b = new Bill();
}
b.Amount = amount;
b.Name = name;
b.CategoryId = category.Id;
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
});
}
}
@ -177,22 +258,24 @@ namespace Billing.Views
}
using (Tap.Start())
{
var source = App.Categories.Select(c => new SelectItem<int>
{
Value = c.Id,
Name = c.Name,
Icon = c.Icon
});
var page = new ItemSelectPage<SelectItem<int>>(source);
page.ItemTapped += Category_ItemTapped;
var page = new CategorySelectPage(categoryChanged ? Category.Id : -1);
page.CategoryTapped += CategorySelectPage_Tapped;
await Navigation.PushAsync(page);
}
}
private void Category_ItemTapped(object sender, SelectItem<int> category)
private void CategorySelectPage_Tapped(object sender, UICategory e)
{
categoryId = category.Value;
SetValue(CategoryNameProperty, category.Name);
SetValue(CategoryProperty, e.Category);
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()
@ -203,22 +286,39 @@ namespace Billing.Views
}
using (Tap.Start())
{
var source = App.Accounts.Select(a => new SelectItem<int>
{
Value = a.Id,
Name = a.Name,
Icon = a.Icon
});
var page = new ItemSelectPage<SelectItem<int>>(source);
var page = new ItemSelectPage<Account>(App.Accounts);
page.ItemTapped += Wallet_ItemTapped;
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(WalletNameProperty, account.Name);
SetValue(WalletProperty, account);
}
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

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8" ?>
<ui:BillingPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:r="clr-namespace:Billing.Languages"
xmlns:ui="clr-namespace:Billing.UI"
xmlns:v="clr-namespace:Billing.Views"
x:Class="Billing.Views.AddCategoryPage"
x:Name="addCategoryPage"
x:DataType="v:AddCategoryPage"
BindingContext="{x:Reference addCategoryPage}">
<ContentPage.Resources>
<ui:IconConverter x:Key="iconConverter"/>
</ContentPage.Resources>
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" IconImageSource="check.png" Command="{Binding CheckCategory}"/>
</ContentPage.ToolbarItems>
<ContentPage.Content>
<TableView Intent="Settings" HasUnevenRows="True">
<TableSection Title=" ">
<ui:OptionEditorCell x:Name="editorName" Height="120" Icon="pencil.png"
FontSize="20" Keyboard="Text"
Title="{r:Text Name}"
Text="{Binding CategoryName, Mode=TwoWay}"
Placeholder="{r:Text NamePlaceholder}"/>
<ui:OptionImageCell Height="44" Icon="face.png"
Title="{r:Text Icon}"
ImageSource="{Binding CategoryIcon, Converter={StaticResource iconConverter}}"
TintColor="{Binding TintColor}"
Command="{Binding SelectIcon}"/>
</TableSection>
<TableSection>
<TableSection.Title>
<OnPlatform x:TypeArguments="x:String" Android=" "/>
</TableSection.Title>
<ui:OptionEntryCell Height="44" Icon="color.png" Keyboard="Numeric"
Title="{r:Text PrimaryColor}"
Text="{Binding TintColorString, Mode=TwoWay}"/>
<ViewCell Height="120">
<Grid BackgroundColor="{DynamicResource OptionTintColor}"
ColumnDefinitions=".35*, .65*" Padding="10">
<ui:ColorPicker Grid.Column="1" Command="{Binding ColorPickerCommand}"/>
</Grid>
</ViewCell>
</TableSection>
</TableView>
</ContentPage.Content>
</ui:BillingPage>

View File

@ -0,0 +1,148 @@
using Billing.Languages;
using Billing.Models;
using Billing.Themes;
using Billing.UI;
using System;
using System.Linq;
using Xamarin.Forms;
namespace Billing.Views
{
public partial class AddCategoryPage : BillingPage
{
private static readonly BindableProperty CategoryNameProperty = Helper.Create<string, AddCategoryPage>(nameof(CategoryName));
private static readonly BindableProperty CategoryIconProperty = Helper.Create<string, AddCategoryPage>(nameof(CategoryIcon));
private static readonly BindableProperty TintColorProperty = Helper.Create<Color, AddCategoryPage>(nameof(TintColor));
private static readonly BindableProperty TintColorStringProperty = Helper.Create<string, AddCategoryPage>(nameof(TintColorString));
public string CategoryName
{
get => (string)GetValue(CategoryNameProperty);
set => SetValue(CategoryNameProperty, value);
}
public string CategoryIcon
{
get => (string)GetValue(CategoryIconProperty);
set => SetValue(CategoryIconProperty, value);
}
public Color TintColor
{
get => (Color)GetValue(TintColorProperty);
set => SetValue(TintColorProperty, value);
}
public string TintColorString
{
get => (string)GetValue(TintColorStringProperty);
set => SetValue(TintColorStringProperty, value);
}
public Command CheckCategory { get; }
public Command SelectIcon { get; }
public Command ColorPickerCommand { get; }
public event EventHandler<Category> CategoryChecked;
private readonly int categoryId;
private readonly Category parent;
public AddCategoryPage(int id = -1, Category parent = null)
{
categoryId = id;
this.parent = parent;
var category = App.Categories.FirstOrDefault(c => c.Id == id);
Title = category?.Name ?? Resource.AddCategory;
if (category != null)
{
CategoryName = category.Name;
CategoryIcon = category.Icon;
if (category.TintColor.IsTransparent())
{
TintColor = BaseTheme.CurrentPrimaryColor;
}
else
{
TintColor = category.TintColor.ToColor();
}
}
else
{
TintColor = BaseTheme.CurrentPrimaryColor;
}
TintColorString = Helper.WrapColorString(TintColor.ToHex());
CheckCategory = new Command(OnCheckCategory);
SelectIcon = new Command(OnSelectIcon);
ColorPickerCommand = new Command(OnColorPickerCommand);
InitializeComponent();
}
protected override void OnLoaded()
{
editorName.SetFocus();
}
private void OnColorPickerCommand(object o)
{
if (o is Color color)
{
TintColor = color;
TintColorString = Helper.WrapColorString(color.ToHex());
}
}
private async void OnCheckCategory()
{
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
var currentColor = BaseTheme.CurrentPrimaryColor;
var tintColor = TintColor;
var color = (tintColor == currentColor ? Color.Transparent : tintColor).ToLong();
var category = App.Categories.FirstOrDefault(c => c.Id == categoryId);
if (category == null)
{
CategoryChecked?.Invoke(this, new Category
{
Name = CategoryName,
Icon = CategoryIcon,
TintColor = color,
ParentId = parent?.Id,
Type = parent?.Type ?? CategoryType.Spending
});
}
else
{
category.Name = CategoryName;
category.Icon = CategoryIcon;
category.TintColor = color;
CategoryChecked?.Invoke(this, category);
}
await Navigation.PopAsync();
}
}
private async void OnSelectIcon()
{
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
var page = new IconSelectPage(CategoryIcon);
page.IconChecked += Category_IconChecked;
await Navigation.PushAsync(page);
}
}
private void Category_IconChecked(object sender, string icon)
{
CategoryIcon = icon;
}
}
}

View File

@ -3,12 +3,14 @@
xmlns:r="clr-namespace:Billing.Languages"
xmlns:ui="clr-namespace:Billing.UI"
xmlns:v="clr-namespace:Billing.Views"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Billing.Views.BillPage"
x:DataType="v:BillPage"
x:Name="billPage"
BindingContext="{x:Reference billPage}"
Title="{r:Text Bills}">
Title="{r:Text Bills}"
Shell.TabBarIsVisible="True">
<ContentPage.Resources>
<ui:TitleDateConverter x:Key="titleDateConverter"/>
@ -18,17 +20,24 @@
<ui:BalanceColorConverter x:Key="colorConverter"/>
<ui:TimeConverter x:Key="timeConverter"/>
<ui:IconConverter x:Key="iconConverter"/>
<ui:TintColorConverter x:Key="tintColorConverter"/>
</ContentPage.Resources>
<Shell.TitleView>
<Grid ColumnSpacing="16" ColumnDefinitions="20, *">
<ui:TintImage Source="calendar.png" WidthRequest="20" HeightRequest="20" VerticalOptions="Center"/>
<ui:LongPressButton Grid.Column="1" Text="{Binding SelectedDate, Converter={StaticResource titleDateConverter}}"
TextColor="{DynamicResource PrimaryColor}"
HorizontalOptions="{OnPlatform iOS=Center, Android=Start}"
FontFamily="{DynamicResource RobotoCondensedFontBold}"
FontAttributes="Bold" FontSize="20" VerticalOptions="Center"
LongPressed="OnTitleDateLongPressed"/>
<Grid>
<DatePicker x:Name="titleDatePicker" IsVisible="False" Date="{Binding SelectedDate, Mode=TwoWay}"
ios:DatePicker.UpdateMode="WhenFinished"
DateSelected="TitlePicker_DateSelected"/>
<ui:LongPressGrid HorizontalOptions="{OnPlatform iOS=Center, Android=Start}" Padding="30, 0, 0, 0"
VerticalOptions="Center" LongCommand="{Binding TitleLongPressed}">
<Label Text="{Binding SelectedDate, Converter={StaticResource titleDateConverter}}"
TextColor="{DynamicResource PrimaryColor}"
FontFamily="{x:Static ui:Definition.SemiBoldFontFamily}"
FontSize="{OnPlatform Android=20, iOS=18}"/>
</ui:LongPressGrid>
<ui:TintImageButton Source="calendar.png" WidthRequest="20" HeightRequest="20"
VerticalOptions="Center" HorizontalOptions="Start"
Command="{Binding TitleDateTap}"/>
</Grid>
</Shell.TitleView>
@ -37,7 +46,7 @@
</ContentPage.ToolbarItems>
<Grid RowDefinitions="Auto, Auto, *">
<ui:BillingDate x:Name="billingDate" SelectedDate="{Binding SelectedDate}" DateSelected="OnDateSelected"/>
<ui:BillingDate x:Name="billingDate" DateSelected="OnDateSelected"/>
<Grid Grid.Row="1" Padding="8" ColumnSpacing="8" ColumnDefinitions="*, Auto"
BackgroundColor="{DynamicResource PromptBackgroundColor}"
IsVisible="{Binding NoBills}">
@ -74,9 +83,9 @@
</StackLayout>
</Grid>
<!-- bill list -->
<ScrollView Grid.Row="2">
<ScrollView x:Name="scrollView" Grid.Row="2">
<ui:GroupStackLayout x:Name="billsLayout" ItemsSource="{Binding Bills}"
Margin="0, 10, 0, 0" RowHeight="50">
Padding="0, 10, 0, 0" RowHeight="50">
<ui:GroupStackLayout.ItemTemplate>
<DataTemplate x:DataType="v:UIBill">
<ui:LongPressGrid Padding="20, 0" ColumnSpacing="10"
@ -88,6 +97,7 @@
CommandParameter="{Binding .}"/>
</Grid.GestureRecognizers>
<ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}"
ui:TintHelper.TintColor="{Binding TintColor, Converter={StaticResource tintColorConverter}}"
WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/>
<Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}"
VerticalOptions="Center"

View File

@ -1,22 +1,29 @@
using Billing.Languages;
using Billing.Models;
using Billing.Store;
using Billing.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xamarin.Essentials;
using Xamarin.Forms;
using Resource = Billing.Languages.Resource;
namespace Billing.Views
{
public partial class BillPage : BillingPage
{
private static readonly BindableProperty SelectedDateProperty = BindableProperty.Create(nameof(SelectedDate), typeof(DateTime), typeof(BillPage));
private static readonly BindableProperty BillsProperty = BindableProperty.Create(nameof(Bills), typeof(List<UIBill>), typeof(BillPage));
private static readonly BindableProperty NoBillsProperty = BindableProperty.Create(nameof(NoBills), typeof(bool), typeof(BillPage));
private static readonly BindableProperty IncomeProperty = BindableProperty.Create(nameof(Income), typeof(decimal), typeof(BillPage));
private static readonly BindableProperty SpendingProperty = BindableProperty.Create(nameof(Spending), typeof(decimal), typeof(BillPage));
private static readonly BindableProperty BalanceProperty = BindableProperty.Create(nameof(Balance), typeof(decimal), typeof(BillPage));
private static readonly BindableProperty SelectedDateProperty = Helper.Create<DateTime, BillPage>(nameof(SelectedDate), propertyChanged: OnSelectedDateChanged);
private static readonly BindableProperty BillsProperty = Helper.Create<List<UIBill>, BillPage>(nameof(Bills));
private static readonly BindableProperty NoBillsProperty = Helper.Create<bool, BillPage>(nameof(NoBills), defaultValue: true);
private static readonly BindableProperty IncomeProperty = Helper.Create<decimal, BillPage>(nameof(Income));
private static readonly BindableProperty SpendingProperty = Helper.Create<decimal, BillPage>(nameof(Spending));
private static readonly BindableProperty BalanceProperty = Helper.Create<decimal, BillPage>(nameof(Balance));
private static void OnSelectedDateChanged(BillPage page, DateTime old, DateTime @new)
{
page.titleDatePicker.Unfocus();
}
public DateTime SelectedDate
{
@ -33,29 +40,41 @@ namespace Billing.Views
public decimal Spending => (decimal)GetValue(SpendingProperty);
public decimal Balance => (decimal)GetValue(BalanceProperty);
public Command TitleDateTap { get; }
public Command TitleLongPressed { get; }
public Command EditBilling { get; }
public Command DeleteBilling { get; }
public BillPage()
{
TitleDateTap = new Command(OnTitleDateTapped);
TitleLongPressed = new Command(OnTitleDateLongPressed);
EditBilling = new Command(OnEditBilling);
DeleteBilling = new Command(OnDeleteBilling);
InitializeComponent();
}
billingDate.SetDateTime(DateTime.Now);
protected override void OnLoaded()
{
billingDate.SetDateTime(DateTime.Today);
}
protected override void OnRefresh()
{
Task.Run(() =>
{
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);
MainThread.BeginInvokeOnMainThread(async () => await scrollView.ScrollToAsync(0, 0, true));
});
}
private void OnDateSelected(object sender, DateEventArgs e)
{
SelectedDate = e.Date;
var bills = App.Bills.Where(b =>
b.CreateTime.Year == e.Date.Year &&
b.CreateTime.Month == e.Date.Month &&
b.CreateTime.Day == e.Date.Day);
Bills = new List<UIBill>(bills.Select(b => WrapBill(b)));
RefreshBalance(Bills);
OnRefresh();
}
private void RefreshBalance(List<UIBill> bills)
@ -68,30 +87,43 @@ namespace Billing.Views
SetValue(BalanceProperty, income - spending);
}
private UIBill WrapBill(Bill b)
{
return new UIBill(b)
{
Icon = App.Categories.FirstOrDefault(c => c.Id == b.CategoryId)?.Icon ?? BaseModel.ICON_DEFAULT,
Name = b.Name,
DateCreation = b.CreateTime,
Amount = b.Amount,
Wallet = App.Accounts.FirstOrDefault(a => a.Id == b.WalletId)?.Name
};
}
private void 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.DateCreation = bill.Bill.CreateTime;
bill.Amount = bill.Bill.Amount;
bill.Wallet = App.Accounts.FirstOrDefault(a => a.Id == bill.Bill.WalletId)?.Name;
}
private void OnTitleDateLongPressed(object sender, EventArgs e)
private void OnTitleDateTapped()
{
billingDate.SetDateTime(DateTime.Now);
titleDatePicker.Focus();
}
private void TitlePicker_DateSelected(object sender, DateChangedEventArgs e)
{
if (e.NewDate.Year > 1900)
{
billingDate.SetDateTime(e.NewDate);
}
}
private void OnTitleDateLongPressed()
{
billingDate.SetDateTime(DateTime.Today);
try
{
HapticFeedback.Perform();
}
catch (FeatureNotSupportedException) { }
catch (Exception ex)
{
Helper.Error("haptic.feedback", ex);
}
}
private async void OnEditBilling(object o)
@ -140,58 +172,73 @@ namespace Billing.Views
App.Bills.Remove(bill.Bill);
billsLayout.Refresh(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);
Bills.Add(WrapBill(e));
billsLayout.Refresh(Bills);
var bills = Bills;
bills.Add(Helper.WrapBill(e));
Bills = bills.OrderBy(b => b.DateCreation).ToList();
}
else
{
var bill = Bills.FirstOrDefault(b => b.Bill == e);
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);
Task.Run(App.WriteBills);
RankPage.Instance?.SetNeedRefresh();
await StoreHelper.SaveBillItemAsync(e);
}
}
public class UIBill : BindableObject
{
public static readonly BindableProperty IconProperty = BindableProperty.Create(nameof(Icon), typeof(string), typeof(UIBill));
public static readonly BindableProperty NameProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(UIBill));
public static readonly BindableProperty DateCreationProperty = BindableProperty.Create(nameof(DateCreation), typeof(DateTime), typeof(UIBill));
public static readonly BindableProperty AmountProperty = BindableProperty.Create(nameof(Amount), typeof(decimal), typeof(UIBill));
public static readonly BindableProperty WalletProperty = BindableProperty.Create(nameof(Wallet), typeof(string), typeof(UIBill));
public static readonly BindableProperty IconProperty = Helper.Create<string, UIBill>(nameof(Icon));
public static readonly BindableProperty TintColorProperty = Helper.Create<long, UIBill>(nameof(TintColor));
public static readonly BindableProperty NameProperty = Helper.Create<string, UIBill>(nameof(Name));
public static readonly BindableProperty DateCreationProperty = Helper.Create<DateTime, UIBill>(nameof(DateCreation));
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
{
get => (string)GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public long TintColor
{
get => (long)GetValue(TintColorProperty);
set => SetValue(TintColorProperty, value);
}
public string Name
{
get => (string)GetValue(NameProperty);

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8" ?>
<ui:BillingPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:ui="clr-namespace:Billing.UI"
xmlns:v="clr-namespace:Billing.Views"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Billing.Views.CategoryPage"
x:Name="categoryPage"
x:DataType="v:CategoryPage"
BindingContext="{x:Reference categoryPage}">
<ContentPage.Resources>
<ui:IconConverter x:Key="iconConverter"/>
<ui:TintColorConverter x:Key="tintColorConverter"/>
</ContentPage.Resources>
<ScrollView>
<ui:GroupStackLayout x:Name="groupLayout" ItemsSource="{Binding Categories}" Padding="0, 10, 0, 0"
GroupHeight="36" RowHeight="44">
<ui:GroupStackLayout.GroupHeaderTemplate>
<DataTemplate x:DataType="v:CategoryGrouping">
<StackLayout Orientation="Horizontal" Padding="10, 0" VerticalOptions="End">
<Label Text="{Binding Key}" TextColor="{DynamicResource SecondaryTextColor}"/>
</StackLayout>
</DataTemplate>
</ui:GroupStackLayout.GroupHeaderTemplate>
<ui:GroupStackLayout.ItemTemplate>
<DataTemplate x:DataType="v:UICategory">
<ui:LongPressGrid Padding="20, 0" ColumnSpacing="10" ColumnDefinitions="Auto, *, Auto"
LongCommand="{Binding LongPressed, Source={x:Reference categoryPage}}"
LongCommandParameter="{Binding .}">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Tapped, Source={x:Reference categoryPage}}"
CommandParameter="{Binding .}"/>
</Grid.GestureRecognizers>
<ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}"
ui:TintHelper.TintColor="{Binding TintColor, Converter={StaticResource tintColorConverter}}"
WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/>
<Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}"
HorizontalOptions="FillAndExpand" VerticalOptions="Center"
FontSize="Default" FontAttributes="Bold"/>
<ui:TintImage Grid.Column="2" Source="right.png" HeightRequest="20"
IsVisible="{Binding IsTopCategory}"/>
</ui:LongPressGrid>
</DataTemplate>
</ui:GroupStackLayout.ItemTemplate>
</ui:GroupStackLayout>
</ScrollView>
</ui:BillingPage>

View File

@ -0,0 +1,239 @@
using Billing.Languages;
using Billing.Models;
using Billing.Store;
using Billing.UI;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;
namespace Billing.Views
{
public partial class CategoryPage : BillingPage
{
private static readonly BindableProperty CategoriesProperty = Helper.Create<IList, CategoryPage>(nameof(Categories));
private static readonly BindableProperty IsTopCategoryProperty = Helper.Create<bool, CategoryPage>(nameof(IsTopCategory));
public IList Categories
{
get => (IList)GetValue(CategoriesProperty);
set => SetValue(CategoriesProperty, value);
}
public bool IsTopCategory => (bool)GetValue(IsTopCategoryProperty);
public Command LongPressed { get; }
public Command Tapped { get; }
private readonly int parentId;
private readonly Category parent;
public CategoryPage(int pid = -1)
{
parentId = pid;
parent = App.Categories.FirstOrDefault(c => c.Id == pid);
Title = parent?.Name ?? Resource.CategoryManage;
if (parent != null)
{
SetValue(IsTopCategoryProperty, false);
Categories = App.Categories.Where(c => c.ParentId == pid).Select(c => WrapCategory(c)).ToList();
ToolbarItems.Add(new ToolbarItem
{
Order = ToolbarItemOrder.Primary,
IconImageSource = "plus.png",
Command = new Command(OnAddCategory)
});
}
else
{
SetValue(IsTopCategoryProperty, true);
Categories = new List<CategoryGrouping>
{
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)))
};
}
LongPressed = new Command(OnLongPressed);
Tapped = new Command(OnTapped);
InitializeComponent();
}
private UICategory WrapCategory(Category category)
{
return new UICategory(category)
{
Icon = category.Icon,
Name = category.Name,
IsTopCategory = IsTopCategory,
TintColor = category.TintColor
};
}
private async void OnAddCategory()
{
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
var page = new AddCategoryPage(parent: parent);
page.CategoryChecked += OnCategoryChecked;
await Navigation.PushAsync(page);
}
}
private async void OnLongPressed(object o)
{
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
if (o is UICategory c)
{
if (parentId < 0)
{
var page = new AddCategoryPage(c.Category.Id);
page.CategoryChecked += OnCategoryChecked;
await Navigation.PushAsync(page);
}
else
{
var result = await this.ShowConfirm(string.Format(Resource.ConfirmDeleteCategory, c.Category.Name));
if (result)
{
Categories.Remove(c);
groupLayout.Refresh(Categories);
App.Categories.Remove(c.Category);
await StoreHelper.DeleteCategoryItemAsync(c.Category);
}
}
}
}
}
private async void OnTapped(object o)
{
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
if (o is UICategory c)
{
if (parentId < 0)
{
var page = new CategoryPage(c.Category.Id);
await Navigation.PushAsync(page);
}
else
{
var page = new AddCategoryPage(c.Category.Id);
page.CategoryChecked += OnCategoryChecked;
await Navigation.PushAsync(page);
}
}
}
}
private async void OnCategoryChecked(object sender, Category category)
{
if (category.Id <= 0)
{
// add
App.Categories.Add(category);
Categories.Add(WrapCategory(category));
}
else
{
foreach (var o in Categories)
{
if (o is CategoryGrouping grouping)
{
var c = grouping.FirstOrDefault(c => c.Category == category);
if (c != null)
{
UpdateCategory(c);
}
}
else if (o is UICategory c && c.Category == category)
{
UpdateCategory(c);
break;
}
}
}
groupLayout.Refresh(Categories);
await StoreHelper.SaveCategoryItemAsync(category);
}
private void UpdateCategory(UICategory c)
{
c.Name = c.Category.Name;
c.Icon = c.Category.Icon;
c.TintColor = c.Category.TintColor;
}
}
public class UICategory : BindableObject
{
public static readonly BindableProperty IsCheckedProperty = Helper.Create<bool, UICategory>(nameof(IsChecked));
public static readonly BindableProperty IconProperty = Helper.Create<string, UICategory>(nameof(Icon));
public static readonly BindableProperty NameProperty = Helper.Create<string, UICategory>(nameof(Name));
public static readonly BindableProperty TintColorProperty = Helper.Create<long, UICategory>(nameof(TintColor));
public static readonly BindableProperty IsTopCategoryProperty = Helper.Create<bool, UICategory>(nameof(IsTopCategory));
public bool IsChecked
{
get => (bool)GetValue(IsCheckedProperty);
set => SetValue(IsCheckedProperty, value);
}
public string Icon
{
get => (string)GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public string Name
{
get => (string)GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
public long TintColor
{
get => (long)GetValue(TintColorProperty);
set => SetValue(TintColorProperty, value);
}
public bool IsTopCategory
{
get => (bool)GetValue(IsTopCategoryProperty);
set => SetValue(IsTopCategoryProperty, value);
}
public Category Category { get; }
public UICategory(Category category)
{
Category = category;
}
}
public class CategoryGrouping : List<UICategory>, IGrouping<string, UICategory>
{
public string Key { get; }
public CategoryGrouping(string key) : base()
{
Key = key;
}
public CategoryGrouping(string key, IEnumerable<UICategory> categories) : base(categories)
{
Key = key;
}
}
}

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8" ?>
<ui:BillingPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:r="clr-namespace:Billing.Languages"
xmlns:ui="clr-namespace:Billing.UI"
xmlns:v="clr-namespace:Billing.Views"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Billing.Views.CategorySelectPage"
x:Name="categorySelectPage"
x:DataType="v:CategorySelectPage"
BindingContext="{x:Reference categorySelectPage}"
Title="{r:Text SelectCategory}">
<ContentPage.Resources>
<ui:IconConverter x:Key="iconConverter"/>
<ui:SelectBackgroundColorConverter x:Key="backgroundConverter"/>
<ui:TintColorConverter x:Key="tintColorConverter"/>
</ContentPage.Resources>
<Grid ColumnDefinitions=".5*, .5*">
<ScrollView>
<ui:GroupStackLayout ItemsSource="{Binding TopCategories}" Padding="0, 10, 0, 0"
GroupHeight="36" RowHeight="44">
<ui:GroupStackLayout.GroupHeaderTemplate>
<DataTemplate x:DataType="v:CategoryGrouping">
<StackLayout Orientation="Horizontal" Padding="10, 0" VerticalOptions="End">
<Label Text="{Binding Key}"
TextColor="{DynamicResource SecondaryTextColor}"/>
</StackLayout>
</DataTemplate>
</ui:GroupStackLayout.GroupHeaderTemplate>
<ui:GroupStackLayout.ItemTemplate>
<DataTemplate x:DataType="v:UICategory">
<Grid Padding="20, 0, 10, 0" ColumnSpacing="10" ColumnDefinitions="Auto, *"
BackgroundColor="{Binding IsChecked, Converter={StaticResource backgroundConverter}}">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding TapTopCategory, Source={x:Reference categorySelectPage}}"
CommandParameter="{Binding .}"/>
</Grid.GestureRecognizers>
<ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}"
ui:TintHelper.TintColor="{Binding TintColor, Converter={StaticResource tintColorConverter}}"
WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/>
<Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}"
HorizontalOptions="FillAndExpand" VerticalOptions="Center"
FontSize="Default" FontAttributes="Bold"/>
</Grid>
</DataTemplate>
</ui:GroupStackLayout.ItemTemplate>
</ui:GroupStackLayout>
</ScrollView>
<ScrollView Grid.Column="1">
<ui:GroupStackLayout ItemsSource="{Binding SubCategories}" RowHeight="44" Padding="0, 50, 0, 0">
<ui:GroupStackLayout.ItemTemplate>
<DataTemplate x:DataType="v:UICategory">
<Grid Padding="20, 0, 10, 0" ColumnSpacing="10" ColumnDefinitions="Auto, *"
BackgroundColor="{Binding IsChecked, Converter={StaticResource backgroundConverter}}">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding TapSubCategory, Source={x:Reference categorySelectPage}}"
CommandParameter="{Binding .}"/>
</Grid.GestureRecognizers>
<ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}"
ui:TintHelper.TintColor="{Binding TintColor, Converter={StaticResource tintColorConverter}}"
WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/>
<Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}"
HorizontalOptions="FillAndExpand" VerticalOptions="Center"
FontSize="Default" FontAttributes="Bold"/>
</Grid>
</DataTemplate>
</ui:GroupStackLayout.ItemTemplate>
</ui:GroupStackLayout>
</ScrollView>
</Grid>
</ui:BillingPage>

View File

@ -0,0 +1,149 @@
using Billing.Languages;
using Billing.Models;
using Billing.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;
namespace Billing.Views
{
public partial class CategorySelectPage : BillingPage
{
private static readonly BindableProperty TopCategoriesProperty = Helper.Create<List<CategoryGrouping>, CategorySelectPage>(nameof(TopCategories));
private static readonly BindableProperty SubCategoriesProperty = Helper.Create<List<UICategory>, CategorySelectPage>(nameof(SubCategories));
public List<CategoryGrouping> TopCategories
{
get => (List<CategoryGrouping>)GetValue(TopCategoriesProperty);
set => SetValue(TopCategoriesProperty, value);
}
public List<UICategory> SubCategories
{
get => (List<UICategory>)GetValue(SubCategoriesProperty);
set => SetValue(SubCategoriesProperty, value);
}
public Command TapTopCategory { get; }
public Command TapSubCategory { get; }
public event EventHandler<UICategory> CategoryTapped;
private readonly int categoryId;
public CategorySelectPage(int id)
{
categoryId = id;
TapTopCategory = new Command(OnTopCategoryTapped);
TapSubCategory = new Command(OnSubCategoryTapped);
TopCategories = new List<CategoryGrouping>
{
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)))
};
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;
var category = App.Categories.FirstOrDefault(c => c.Id == categoryId);
if (category == null)
{
cat = TopCategories.Where(g => g.Count > 0).Select(g => g.First()).FirstOrDefault();
}
else if (category.ParentId == null)
{
cat = TopCategories.SelectMany(g => g).FirstOrDefault(c => c.Category == category);
}
else
{
cat = TopCategories.SelectMany(g => g).FirstOrDefault(c => c.Category?.Id == category.ParentId) ?? last;
}
DoRefreshSubCategories(cat);
InitializeComponent();
}
private UICategory WrapCategory(Category c)
{
return new UICategory(c)
{
IsChecked = c.Id == categoryId,
Icon = c.Icon,
Name = c.Name,
TintColor = c.TintColor
};
}
private void DoRefreshSubCategories(UICategory category)
{
var many = TopCategories.SelectMany(g => g);
foreach (var m in many)
{
m.IsChecked = m == category;
}
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)
{
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
if (o is UICategory category)
{
DoRefreshSubCategories(category);
if (SubCategories?.Count == 0)
{
CategoryTapped?.Invoke(this, category);
await Navigation.PopAsync();
}
}
}
}
private async void OnSubCategoryTapped(object o)
{
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
if (o is UICategory category)
{
CategoryTapped?.Invoke(this, category);
await Navigation.PopAsync();
}
}
}
}
}

View File

@ -16,7 +16,7 @@
<Grid RowDefinitions="*, Auto">
<ScrollView>
<ui:WrapLayout ItemsSource="{Binding IconsSource}" Margin="10">
<ui:WrapLayout ItemsSource="{Binding IconsSource}" Padding="10">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="v:BillingIcon">
<Grid WidthRequest="60" HeightRequest="60">

View File

@ -1,5 +1,4 @@
using Billing.Models;
using Billing.UI;
using Billing.UI;
using System;
using System.Collections.Generic;
using System.Linq;
@ -9,7 +8,7 @@ namespace Billing.Views
{
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
{
@ -37,14 +36,37 @@ namespace Billing.Views
{
var source = new List<BillingIcon>
{
new() { Icon = BaseModel.ICON_DEFAULT },
new() { Icon = Definition.DefaultIcon },
new() { Icon = "wallet" },
new() { Icon = "dollar" },
new() { Icon = "creditcard" },
new() { Icon = "debitcard" },
new() { Icon = "cmb" },
new() { Icon = "rcb" },
new() { Icon = "yuebao" },
new() { Icon = "zhaozhaoying" }
new() { Icon = "zhaozhaoying" },
new() { Icon = "clothes" },
new() { Icon = "food" },
new() { Icon = "drink" },
new() { Icon = "daily" },
new() { Icon = "trans" },
new() { Icon = "face" },
new() { Icon = "learn" },
new() { Icon = "medical" },
new() { Icon = "gem" },
new() { Icon = "makeup" },
new() { Icon = "brunch" },
new() { Icon = "dinner" },
new() { Icon = "fruit" },
new() { Icon = "bill" },
new() { Icon = "fee" },
new() { Icon = "rent" },
new() { Icon = "maintenance" },
new() { Icon = "rail" },
new() { Icon = "taxi" },
new() { Icon = "fitness" },
new() { Icon = "party" },
new() { Icon = "share" },
};
source.AddRange(IconConverter.IconPreset.Select(icon => new BillingIcon { Icon = $"#brand#{icon.Key}" }));
foreach (var icon in source)
@ -59,22 +81,29 @@ namespace Billing.Views
private async void OnIconCheck(object o)
{
if (o is string icon)
if (Tap.IsBusy)
{
foreach (var ic in IconsSource)
return;
}
using (Tap.Start())
{
if (o is string icon)
{
ic.IsChecked = ic.Icon == icon;
foreach (var ic in IconsSource)
{
ic.IsChecked = ic.Icon == icon;
}
iconChecked = icon;
IconChecked?.Invoke(this, icon);
await Navigation.PopAsync();
}
iconChecked = icon;
IconChecked?.Invoke(this, icon);
await Navigation.PopAsync();
}
}
}
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
{

View File

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="utf-8" ?>
<ui:BillingPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:r="clr-namespace:Billing.Languages"
xmlns:ui="clr-namespace:Billing.UI"
xmlns:v="clr-namespace:Billing.Views"
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"
x:Class="Billing.Views.RankPage"
x:Name="rankPage"
x:DataType="v:RankPage"
BindingContext="{x:Reference rankPage}"
Shell.TabBarIsVisible="True">
<Shell.TitleView>
<Grid ColumnSpacing="10" ColumnDefinitions="30, *, 30">
<ui:TintImageButton Source="left.png" WidthRequest="20" HeightRequest="20"
VerticalOptions="Center" HorizontalOptions="Center"
Command="{Binding LeftCommand}"/>
<Label Grid.Column="1" Text="{Binding Title}"
TextColor="{DynamicResource PrimaryColor}"
FontSize="{OnPlatform Android=20, iOS=18}"
FontFamily="{x:Static ui:Definition.SemiBoldFontFamily}"
VerticalOptions="Center"
HorizontalOptions="Center">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding FilterCommand}"/>
</Label.GestureRecognizers>
</Label>
<ui:TintImageButton Grid.Column="2" Source="right.png" WidthRequest="20" HeightRequest="20"
VerticalOptions="Center" HorizontalOptions="Center"
Command="{Binding RightCommand}"/>
</Grid>
</Shell.TitleView>
<!--<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" IconImageSource="filter.png" Command="{Binding FilterCommand}"/>
</ContentPage.ToolbarItems>-->
<ContentPage.Resources>
<ui:NegativeConverter x:Key="negativeConverter"/>
<ui:MoneyConverter x:Key="moneyConverter" Absolute="True"/>
<ui:MoneyConverter x:Key="moneyRawConverter"/>
<ui:BalanceColorConverter x:Key="colorConverter"/>
<ui:TimeConverter x:Key="timeConverter" IncludeDate="True"/>
<ui:IconConverter x:Key="iconConverter"/>
<ui:TintColorConverter x:Key="tintColorConverter"/>
<Style x:Key="titleLabel" TargetType="Label">
<Setter Property="FontSize" Value="16"/>
<Setter Property="Margin" Value="10, 20, 10, 10"/>
<Setter Property="TextColor" Value="{DynamicResource TextColor}"/>
</Style>
<Style x:Key="promptLabel" TargetType="Label">
<Setter Property="HeightRequest" Value="240"/>
<Setter Property="FontSize" Value="15"/>
<Setter Property="VerticalOptions" Value="Start"/>
<Setter Property="HorizontalTextAlignment" Value="Center"/>
<Setter Property="VerticalTextAlignment" Value="Center"/>
<Setter Property="TextColor" Value="{DynamicResource SecondaryTextColor}"/>
</Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{DynamicResource TextColor}"/>
<Setter Property="FontSize" Value="14"/>
</Style>
</ContentPage.Resources>
<Grid>
<ScrollView x:Name="scroller" Scrolled="Scroller_Scrolled">
<StackLayout>
<Grid Padding="8" ColumnSpacing="8" ColumnDefinitions="*, Auto" HeightRequest="24"
BackgroundColor="{DynamicResource PromptBackgroundColor}">
<StackLayout Grid.Column="1" Orientation="Horizontal" Spacing="6">
<Label Text="{r:Text Income}" TextColor="{DynamicResource GreenColor}"
VerticalOptions="Center" FontSize="12"/>
<Label Text="{Binding Income, Converter={StaticResource moneyConverter}}"
TextColor="{DynamicResource TextColor}"
VerticalOptions="Center" FontSize="12"/>
<Label Text="{r:Text Spending}" TextColor="{DynamicResource RedColor}"
VerticalOptions="Center" FontSize="12" Margin="10, 0, 0, 0"/>
<Label Text="{Binding Spending, Converter={StaticResource moneyConverter}}"
TextColor="{DynamicResource TextColor}"
VerticalOptions="Center" FontSize="12"/>
<Label Text="{r:Text Balance}"
VerticalOptions="Center" FontSize="12" Margin="10, 0, 0, 0"/>
<Label Text="{Binding Balance, Converter={StaticResource moneyRawConverter}}"
TextColor="{DynamicResource TextColor}"
VerticalOptions="Center" FontSize="12"/>
</StackLayout>
</Grid>
<Label Text="{r:Text TrackingChart}" Style="{StaticResource titleLabel}"/>
<chart:ChartView HeightRequest="240" Chart="{Binding Chart}"
IsVisible="{Binding NoResultChart, Converter={StaticResource negativeConverter}}"/>
<Label Text="{r:Text NoResult}" Style="{StaticResource promptLabel}"
IsVisible="{Binding NoResultChart}"/>
<Label Text="{r:Text CategoryRank}" Style="{StaticResource titleLabel}"/>
<chart:ChartView HeightRequest="240" Chart="{Binding CategoryChart}"
IsVisible="{Binding NoResultCategoryChart, Converter={StaticResource negativeConverter}}"/>
<Label Text="{r:Text NoResult}" Style="{StaticResource promptLabel}"
IsVisible="{Binding NoResultCategoryChart}"/>
<Label Text="{r:Text Top10}" Style="{StaticResource titleLabel}"/>
<ui:GroupStackLayout IsVisible="{Binding NoResultTopBills, Converter={StaticResource negativeConverter}}"
ItemsSource="{Binding TopBills}" RowHeight="50">
<ui:GroupStackLayout.ItemTemplate>
<DataTemplate x:DataType="v:UIBill">
<Grid Padding="20, 0" ColumnSpacing="10"
ColumnDefinitions="Auto, *, Auto" RowDefinitions="Auto, Auto">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding EditBilling, Source={x:Reference rankPage}}"
CommandParameter="{Binding .}"/>
</Grid.GestureRecognizers>
<ui:TintImage Source="{Binding Icon, Converter={StaticResource iconConverter}}"
ui:TintHelper.TintColor="{Binding TintColor, Converter={StaticResource tintColorConverter}}"
WidthRequest="26" HeightRequest="20" VerticalOptions="Center"/>
<Label Grid.Column="1" Text="{Binding Name}" TextColor="{DynamicResource TextColor}"
VerticalOptions="Center"
FontSize="Default" FontAttributes="Bold"/>
<Label Grid.Column="2" Text="{Binding Amount, Converter={StaticResource moneyConverter}}"
TextColor="{Binding Amount, Converter={StaticResource colorConverter}}"
VerticalOptions="Center"/>
<StackLayout Grid.Row="1" Grid.Column="1" Spacing="6" Orientation="Horizontal">
<Label Text="{Binding DateCreation, Converter={StaticResource timeConverter}}"
FontSize="10" TextColor="{DynamicResource SecondaryTextColor}"/>
<Label Text="{Binding Wallet}"
FontSize="10" TextColor="{DynamicResource SecondaryTextColor}"/>
</StackLayout>
</Grid>
</DataTemplate>
</ui:GroupStackLayout.ItemTemplate>
</ui:GroupStackLayout>
<Label Text="{r:Text NoResult}" Style="{StaticResource promptLabel}"
IsVisible="{Binding NoResultTopBills}"/>
</StackLayout>
</ScrollView>
<ui:BlurryPanel x:Name="panelFilter" VerticalOptions="Start" Opacity="0"
BackgroundColor="{DynamicResource WindowBackgroundColor}"
HeightRequest="{Binding Height, Source={x:Reference gridFilter}}"/>
<Grid x:Name="gridFilter" VerticalOptions="Start" Opacity="0" RowDefinitions="Auto, Auto, Auto, Auto">
<ui:SegmentedControl VerticalOptions="Center" Margin="10, 10, 10, 3"
SelectedSegmentIndex="{Binding SegmentType, Mode=TwoWay}"
SelectedTextColor="{DynamicResource TextColor}"
TintColor="{DynamicResource PromptBackgroundColor}">
<ui:SegmentedControl.Children>
<ui:SegmentedControlOption Text="{r:Text Spending}"/>
<ui:SegmentedControlOption Text="{r:Text Income}"/>
</ui:SegmentedControl.Children>
</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>
</ui:BillingPage>

View File

@ -0,0 +1,635 @@
using Billing.Models;
using Billing.Store;
using Billing.Themes;
using Billing.UI;
using Microcharts;
using SkiaSharp;
using SkiaSharp.Views.Forms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xamarin.Essentials;
using Xamarin.Forms;
using Resource = Billing.Languages.Resource;
namespace Billing.Views
{
public enum DateType : int
{
Custom = 0,
Monthly,
Today,
PastMonth,
PastQuarter,
PastSixMonths,
PastYear,
Total
}
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 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 CategoryChartProperty = Helper.Create<Chart, RankPage>(nameof(CategoryChart));
private static readonly BindableProperty TopBillsProperty = Helper.Create<IList<UIBill>, RankPage>(nameof(TopBills));
private static readonly BindableProperty NoResultChartProperty = Helper.Create<bool, RankPage>(nameof(NoResultChart));
private static readonly BindableProperty NoResultCategoryChartProperty = Helper.Create<bool, RankPage>(nameof(NoResultCategoryChart));
private static readonly BindableProperty NoResultTopBillsProperty = Helper.Create<bool, RankPage>(nameof(NoResultTopBills));
private static readonly BindableProperty IncomeProperty = Helper.Create<decimal, RankPage>(nameof(Income));
private static readonly BindableProperty SpendingProperty = Helper.Create<decimal, RankPage>(nameof(Spending));
private static readonly BindableProperty BalanceProperty = Helper.Create<decimal, RankPage>(nameof(Balance));
private static void OnSegmentTypeChanged(RankPage page, int old, int @new)
{
page.type = @new switch
{
1 => CategoryType.Income,
_ => CategoryType.Spending
};
page.LoadData();
}
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
{
get => (int)GetValue(SegmentTypeProperty);
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
{
get => (Chart)GetValue(ChartProperty);
set => SetValue(ChartProperty, value);
}
public Chart CategoryChart
{
get => (Chart)GetValue(CategoryChartProperty);
set => SetValue(CategoryChartProperty, value);
}
public IList<UIBill> TopBills
{
get => (IList<UIBill>)GetValue(TopBillsProperty);
set => SetValue(TopBillsProperty, value);
}
public bool NoResultChart
{
get => (bool)GetValue(NoResultChartProperty);
set => SetValue(NoResultChartProperty, value);
}
public bool NoResultCategoryChart
{
get => (bool)GetValue(NoResultCategoryChartProperty);
set => SetValue(NoResultCategoryChartProperty, value);
}
public bool NoResultTopBills
{
get => (bool)GetValue(NoResultTopBillsProperty);
set => SetValue(NoResultTopBillsProperty, value);
}
public decimal Income => (decimal)GetValue(IncomeProperty);
public decimal Spending => (decimal)GetValue(SpendingProperty);
public decimal Balance => (decimal)GetValue(BalanceProperty);
public List<string> DateTypes { get; }
public Command LeftCommand { get; }
public Command RightCommand { get; }
public Command FilterCommand { get; }
public Command EditBilling { get; }
private IEnumerable<Bill> bills;
private CategoryType type = CategoryType.Spending;
private bool isFilterToggled;
private bool isFreezed;
private bool isLocked;
private bool needRefresh = true;
private const int FILTER_HEIGHT = 100;
private readonly SKTypeface font;
public RankPage()
{
instance = this;
LeftCommand = new Command(OnLeftCommand);
RightCommand = new Command(OnRightCommand);
FilterCommand = new Command(OnFilterCommand);
EditBilling = new Command(OnEditBilling);
#if __IOS__
var style = SKFontManager.Default.GetFontStyles("PingFang SC");
if (style != null)
{
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();
gridFilter.TranslationY = -FILTER_HEIGHT;
panelFilter.TranslationY = -FILTER_HEIGHT;
}
public void SetNeedRefresh()
{
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()
{
var type = (DateType)SegmentDate;
if (type < DateType.Monthly || type >= DateType.Total)
{
return;
}
if (scroller.ScrollY > 0)
{
scroller.ScrollToAsync(0, 0, true);
}
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()
{
var type = (DateType)SegmentDate;
if (type < DateType.Monthly || type >= DateType.Total)
{
return;
}
if (scroller.ScrollY > 0)
{
scroller.ScrollToAsync(0, 0, true);
}
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)
{
if (o is bool flag)
{
isFilterToggled = flag;
}
else
{
isFilterToggled = !isFilterToggled;
}
ViewExtensions.CancelAnimations(gridFilter);
ViewExtensions.CancelAnimations(panelFilter);
if (isFilterToggled)
{
await scroller.ScrollToAsync(scroller.ScrollX, scroller.ScrollY, false);
await Task.WhenAll(
gridFilter.TranslateTo(0, 0, easing: Easing.CubicOut),
gridFilter.FadeTo(1, easing: Easing.CubicOut),
panelFilter.TranslateTo(0, 0, easing: Easing.CubicOut),
panelFilter.FadeTo(1, easing: Easing.CubicOut));
}
else
{
await Task.WhenAll(
gridFilter.TranslateTo(0, -FILTER_HEIGHT, easing: Easing.CubicIn),
gridFilter.FadeTo(0, easing: Easing.CubicIn),
panelFilter.TranslateTo(0, -FILTER_HEIGHT, easing: Easing.CubicIn),
panelFilter.FadeTo(0, easing: Easing.CubicIn));
}
}
private async void OnEditBilling(object o)
{
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
if (o is UIBill bill)
{
var page = new AddBillPage(bill.Bill);
page.BillChecked += OnBillChecked;
await Navigation.PushAsync(page);
}
}
}
private async void RefreshBalance(DateTime start, DateTime 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 spending = -bills.Where(b => b.Amount < 0).Sum(b => b.Amount);
SetValue(IncomeProperty, income);
SetValue(SpendingProperty, spending);
SetValue(BalanceProperty, income - spending);
}
private async void OnBillChecked(object sender, Bill e)
{
await StoreHelper.SaveBillItemAsync(e);
LoadData();
}
private async void LoadData()
{
var start = StartDate;
var end = EndDate;
var spending = type == CategoryType.Spending;
bills = await Task.Run(() => App.Bills.Where(b => (b.Amount > 0 ^ spending) && b.CreateTime >= start && b.CreateTime <= end));
var primaryColor = BaseTheme.CurrentPrimaryColor.ToSKColor();
var textColor = BaseTheme.CurrentSecondaryTextColor.ToSKColor();
_ = Task.Run(() => LoadReportChart(primaryColor, textColor, start, end));
_ = Task.Run(() => LoadCategoryChart(primaryColor, textColor));
_ = Task.Run(LoadTopBills);
RefreshBalance(start, end);
}
private void LoadReportChart(SKColor primaryColor, SKColor textColor, DateTime start, DateTime end)
{
var entries = new List<ChartEntry>();
for (var day = start; day <= end; day = day.AddDays(1))
{
var daybills = bills.Where(b => Helper.IsSameDay(b.CreateTime, day));
decimal amount = Math.Abs(daybills.Sum(b => b.Amount));
if (amount > 0)
{
entries.Add(new((float)amount)
{
Label = day.ToString("MM-dd"),
ValueLabel = amount.ToString("#,##0.##"),
Color = primaryColor,
TextColor = textColor,
ValueLabelColor = textColor
});
}
}
MainThread.BeginInvokeOnMainThread(() =>
{
if (entries.Count > 0)
{
NoResultChart = false;
Chart = new LineChart
{
BackgroundColor = SKColors.Transparent,
LabelTextSize = 24,
Entries = entries
};
}
else
{
Chart = null;
NoResultChart = true;
}
});
}
private void LoadCategoryChart(SKColor primaryColor, SKColor textColor)
{
var entries = new List<ChartEntry>();
var groups = bills.GroupBy(b => b.CategoryId);
var dict = new Dictionary<string, decimal>();
decimal all = 0m;
foreach (var g in groups)
{
var categoryId = g.Key;
var category = App.Categories.FirstOrDefault(c => c.Id == categoryId);
if (category?.ParentId != null)
{
category = App.Categories.FirstOrDefault(c => c.Id == category.ParentId) ?? category;
}
if (category != null)
{
var total = Math.Abs(g.Sum(g => g.Amount));
all += total;
if (dict.ContainsKey(category.Name))
{
dict[category.Name] += total;
}
else
{
dict.Add(category.Name, total);
}
}
}
foreach (var kv in dict)
{
entries.Add(new((float)kv.Value)
{
Label = kv.Key,
ValueLabel = (kv.Value * 100 / all).ToString("0.#") + "%",
Color = primaryColor,
TextColor = textColor,
ValueLabelColor = textColor
});
}
MainThread.BeginInvokeOnMainThread(() =>
{
if (entries.Count > 0)
{
NoResultCategoryChart = false;
CategoryChart = new RadarChart
{
BackgroundColor = SKColors.Transparent,
LabelTextSize = 30,
Typeface = font,
Entries = entries
};
}
else
{
CategoryChart = null;
NoResultCategoryChart = true;
}
});
}
private void LoadTopBills()
{
List<UIBill> topBills = bills.OrderByDescending(b => Math.Abs(b.Amount)).Take(10).Select(b => Helper.WrapBill(b)).ToList();
MainThread.BeginInvokeOnMainThread(() =>
{
if (topBills.Count > 0)
{
NoResultTopBills = false;
TopBills = topBills;
}
else
{
TopBills = null;
NoResultTopBills = true;
}
});
}
private void Scroller_Scrolled(object sender, ScrolledEventArgs e)
{
if (isFilterToggled)
{
OnFilterCommand(false);
}
}
}
}

View File

@ -8,21 +8,43 @@
x:Name="settingPage"
x:DataType="v:SettingPage"
Title="{r:Text Settings}"
BindingContext="{x:Reference settingPage}">
BindingContext="{x:Reference settingPage}"
Shell.TabBarIsVisible="True">
<TableView Intent="Settings" RowHeight="44">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" IconImageSource="share.png" Command="{Binding ShareCommand}"/>
</ContentPage.ToolbarItems>
<TableView Intent="Settings" HasUnevenRows="True">
<TableSection Title="{r:Text About}">
<ui:OptionTextCell Title="{r:Text Version}"
<ui:OptionTextCell Height="44" Title="{r:Text Version}"
Detail="{Binding Version}"/>
</TableSection>
<TableSection Title="{r:Text Feature}">
<ui:OptionSelectCell Height="44" Title="{r:Text CategoryManage}"
Detail="{r:Text Detail}"
Command="{Binding CategoryCommand}"/>
<ui:OptionSwitchCell Height="44" Title="{r:Text SaveLocation}"
IsToggled="{Binding SaveLocation, Mode=TwoWay}"/>
</TableSection>
<TableSection Title="{r:Text Preference}">
<ui:OptionEntryCell Title="{r:Text PrimaryColor}"
Text="{Binding Red, Mode=TwoWay}"
Keyboard="Numeric"/>
<ui:OptionEntryCell Text="{Binding Green, Mode=TwoWay}"
Keyboard="Numeric"/>
<ui:OptionEntryCell Text="{Binding Blue, Mode=TwoWay}"
Keyboard="Numeric"/>
<ui:OptionEntryCell Height="44" Title="{r:Text PrimaryColor}"
Text="{Binding PrimaryColor, Mode=TwoWay}"
Keyboard="Text"/>
<ViewCell Height="120">
<Grid BackgroundColor="{DynamicResource OptionTintColor}"
ColumnDefinitions=".35*, .65*" Padding="10">
<!--<Label Text="" LineBreakMode="TailTruncation"
VerticalOptions="Center"
TextColor="{DynamicResource TextColor}"/>-->
<ui:ColorPicker Grid.Column="1" Command="{Binding ColorPickerCommand}"/>
</Grid>
</ViewCell>
</TableSection>
<TableSection Title="{r:Text Diagnostic}">
<ui:OptionSelectCell Height="44" Title="{r:Text ShareLogs}"
Detail="{Binding ManyRecords}"
Command="{Binding ShareLogsCommand}"/>
</TableSection>
</TableView>
</ui:BillingPage>

View File

@ -1,61 +1,171 @@
using System.IO;
using Billing.Store;
using Billing.Themes;
using Billing.UI;
using Xamarin.Essentials;
using Xamarin.Forms;
using Resource = Billing.Languages.Resource;
namespace Billing.Views
{
public partial class SettingPage : BillingPage
{
private static readonly BindableProperty VersionProperty = BindableProperty.Create(nameof(Version), typeof(string), typeof(SettingPage));
private static readonly BindableProperty RedProperty = BindableProperty.Create(nameof(Red), typeof(byte), typeof(SettingPage));
private static readonly BindableProperty GreenProperty = BindableProperty.Create(nameof(Green), typeof(byte), typeof(SettingPage));
private static readonly BindableProperty BlueProperty = BindableProperty.Create(nameof(Blue), typeof(byte), typeof(SettingPage));
private static readonly BindableProperty VersionProperty = Helper.Create<string, SettingPage>(nameof(Version));
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 byte Red
{
get => (byte)GetValue(RedProperty);
set => SetValue(RedProperty, value);
public bool SaveLocation
{
get => (bool)GetValue(SaveLocationProperty);
set => SetValue(SaveLocationProperty, value);
}
public byte Green
public string PrimaryColor
{
get => (byte)GetValue(GreenProperty);
set => SetValue(GreenProperty, value);
}
public byte Blue
{
get => (byte)GetValue(BlueProperty);
set => SetValue(BlueProperty, value);
get => (string)GetValue(PrimaryColorProperty);
set => SetValue(PrimaryColorProperty, value);
}
public string ManyRecords => (string)GetValue(ManyRecordsProperty);
public Command ShareCommand { get; }
public Command CategoryCommand { get; }
public Command ColorPickerCommand { get; }
public Command ShareLogsCommand { get; }
public SettingPage()
{
ShareCommand = new Command(OnShareCommand);
CategoryCommand = new Command(OnCategoryCommand);
ColorPickerCommand = new Command(OnColorPickerCommand);
ShareLogsCommand = new Command(OnShareLogsCommand);
InitializeComponent();
var (main, build) = Definition.GetVersion();
SetValue(VersionProperty, $"{main} ({build})");
SetValue(VersionProperty, $"{AppInfo.VersionString} ({AppInfo.BuildString})");
}
protected override void OnAppearing()
protected override async void OnAppearing()
{
base.OnAppearing();
//SetValue(VersionProperty, $"{AppInfo.VersionString} ({AppInfo.BuildString})");
var colorString = Preferences.Get(Definition.PrimaryColorKey, "#183153");
var color = Color.FromHex(colorString);
Red = (byte)(color.R * 255);
Green = (byte)(color.G * 255);
Blue = (byte)(color.B * 255);
SaveLocation = App.SaveLocation;
var colorString = Preferences.Get(Definition.PrimaryColorKey, Helper.DEFAULT_COLOR);
PrimaryColor = Helper.WrapColorString(colorString);
var count = await StoreHelper.GetLogsCount();
SetValue(ManyRecordsProperty, string.Format(Resource.ManyRecords, count));
}
protected override void OnDisappearing()
{
base.OnDisappearing();
var color = Color.FromRgb(Red, Green, Blue);
Preferences.Set(Definition.PrimaryColorKey, color.ToHex());
Light.Instance.RefreshColor(color);
App.SetSaveLocation(SaveLocation);
Preferences.Set(Definition.SaveLocationKey, SaveLocation);
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()
{
if (Tap.IsBusy)
{
return;
}
using (Tap.Start())
{
var page = new CategoryPage();
await Navigation.PushAsync(page);
}
}
private void OnColorPickerCommand(object o)
{
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
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Billing.Shared", "Billing.Shared\Billing.Shared.shproj", "{6AC75D01-70D6-4A07-8685-BC52AFD97A7A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svg2Png", "Svg2Png\Svg2Png.csproj", "{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svg2Png", "Svg2Png\Svg2Png.csproj", "{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
@ -44,9 +44,9 @@ Global
{B4CD3B27-C58F-4B6B-B60E-35E515A73E5B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{B4CD3B27-C58F-4B6B-B60E-35E515A73E5B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{B4CD3B27-C58F-4B6B-B60E-35E515A73E5B}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU
{5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Debug|Any CPU.ActiveCfg = Debug|iPhone
{5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Debug|Any CPU.Build.0 = Debug|iPhone
{5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Debug|Any CPU.Deploy.0 = Debug|iPhone
{5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator
{5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator
{5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Debug|Any CPU.Deploy.0 = Debug|iPhoneSimulator
{5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Debug|iPhone.ActiveCfg = Debug|iPhone
{5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Debug|iPhone.Build.0 = Debug|iPhone
{5C4F1C35-6F66-4063-9605-A9F37FCABBA8}.Debug|iPhone.Deploy.0 = Debug|iPhone
@ -62,10 +62,10 @@ Global
{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.Deploy.0 = Release|iPhoneSimulator
{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|iPhone.Build.0 = Debug|Any CPU
{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator
{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator
{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|iPhone.ActiveCfg = Debug|iPhoneSimulator
{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|iPhone.Build.0 = Debug|iPhoneSimulator
{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator
{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator
{43BB5B21-61E0-42BB-ADF1-DBCD662E61E1}.Release|Any CPU.ActiveCfg = Release|iPhoneSimulator

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -16,12 +16,12 @@
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
<TargetFrameworkVersion>v12.0</TargetFrameworkVersion>
<TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
<AndroidEnableSGenConcurrent>true</AndroidEnableSGenConcurrent>
<AndroidUseAapt2>true</AndroidUseAapt2>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<NuGetPackageImportStamp></NuGetPackageImportStamp>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -37,11 +37,12 @@
<EnableLLVM>false</EnableLLVM>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>false</BundleAssemblies>
<AndroidSupportedAbis>x86_64;x86</AndroidSupportedAbis>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<AndroidSupportedAbis>x86;x86_64;arm64-v8a</AndroidSupportedAbis>
<EmbedAssembliesIntoApk>false</EmbedAssembliesIntoApk>
<MandroidI18n />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugSymbols>false</DebugSymbols>
<DebugType>portable</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release</OutputPath>
@ -52,21 +53,28 @@
<AotAssemblies>false</AotAssemblies>
<EnableLLVM>false</EnableLLVM>
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
<BundleAssemblies>false</BundleAssemblies>
<AndroidSupportedAbis>arm64-v8a</AndroidSupportedAbis>
<BundleAssemblies>true</BundleAssemblies>
<AndroidSupportedAbis>x86_64;arm64-v8a</AndroidSupportedAbis>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<AndroidCreatePackagePerAbi>true</AndroidCreatePackagePerAbi>
<AndroidLinkTool>r8</AndroidLinkTool>
<MandroidI18n />
</PropertyGroup>
<ItemGroup>
<Reference Include="Mono.Android" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Numerics.Vectors" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2337" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.1" />
<PackageReference Include="Microcharts.Forms" Version="0.9.5.9" />
<PackageReference Include="SkiaSharp.Views.Forms" Version="2.80.3" />
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<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>
<Compile Include="Definition.cs" />
@ -83,16 +91,20 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Renderers\OptionEditorRenderer.cs" />
<Compile Include="SplashActivity.cs" />
<Compile Include="Renderers\TintImageButtonRenderer.cs" />
<Compile Include="Renderers\BillingPageRenderer.cs" />
<Compile Include="Renderers\BlurryPanelRenderer.cs" />
<Compile Include="Renderers\SegmentedControlRenderer.cs" />
<Compile Include="Renderers\OptionPickerRenderer.cs" />
</ItemGroup>
<ItemGroup>
<AndroidAsset Include="Assets\CascadiaCode-Bold.ttf" />
<AndroidAsset Include="Assets\CascadiaCode-Regular.ttf" />
<AndroidAsset Include="Assets\RobotoCondensed-Regular.ttf" />
<AndroidAsset Include="Assets\RobotoCondensed-Bold.ttf" />
<AndroidAsset Include="Assets\fa-brands-400.ttf" />
<AndroidAsset Include="Assets\OpenSans-Bold.ttf" />
<AndroidAsset Include="Assets\OpenSans-Regular.ttf" />
<None Include="Resources\AboutResources.txt" />
<None Include="Assets\AboutAssets.txt" />
<None Include="Properties\AndroidManifest.xml" />
<AndroidAsset Include="Assets\OpenSans-SemiBold.ttf" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\values\styles.xml" />
@ -111,14 +123,77 @@
<AndroidResource Include="Resources\mipmap-anydpi-v26\icon_round.xml" />
<AndroidResource Include="Resources\values\icon_background.xml" />
<AndroidResource Include="Resources\drawable\icon_foreground.xml" />
<AndroidResource Include="Resources\drawable\xamarin_logo.png" />
<AndroidResource Include="Resources\values\strings.xml">
<SubType></SubType>
<Generator></Generator>
<SubType>
</SubType>
<Generator>
</Generator>
</AndroidResource>
<AndroidResource Include="Resources\values-zh-rCN\strings.xml">
<SubType></SubType>
<Generator></Generator>
<SubType>
</SubType>
<Generator>
</Generator>
</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>
@ -343,7 +418,199 @@
<AndroidResource Include="Resources\drawable-xxhdpi\trans.png" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\values-zh-rCN\" />
<AndroidResource Include="Resources\drawable\color.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\color.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\color.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\color.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\brunch.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\dinner.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\fee.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\fitness.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\fruit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\gem.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\maintenance.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\makeup.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\party.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\rail.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\rent.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\taxi.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\brunch.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\dinner.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\fee.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\fitness.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\fruit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\gem.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\maintenance.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\makeup.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\party.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\rail.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\rent.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\taxi.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\brunch.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\dinner.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\fee.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\fitness.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\fruit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\gem.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\maintenance.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\makeup.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\party.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\rail.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\rent.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\taxi.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\brunch.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\dinner.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\fee.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\fitness.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\fruit.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\gem.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\maintenance.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\makeup.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\party.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\rail.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\rent.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\taxi.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\rank.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\rank.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\rank.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\rank.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\filter.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xxhdpi\left.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\filter.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\left.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\filter.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\left.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\filter.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\left.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\xml\shortcuts.xml" />
</ItemGroup>
<Import Project="..\..\Billing.Shared\Billing.Shared.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />

View File

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

View File

@ -1,7 +1,11 @@
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Runtime;
using Android.OS;
using Android.Net;
using Android.Provider;
using Android.Database;
namespace Billing.Droid
{
@ -18,9 +22,26 @@ namespace Billing.Droid
{
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.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)
@ -29,5 +50,83 @@ namespace Billing.Droid
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"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="3" android:versionName="0.3.303" package="org.tsanie.billing" android:installLocation="auto">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="31" />
<application android:label="@string/applabel" android:theme="@style/MainTheme"></application>
<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="30" />
<application android:label="@string/applabel" android:theme="@style/MainTheme" android:requestLegacyExternalStorage="true"></application>
<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>

View File

@ -27,3 +27,11 @@ using Android.App;
// Add some common permissions, these can be removed if not needed
[assembly: UsesPermission(Android.Manifest.Permission.Internet)]
[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

@ -0,0 +1,25 @@
using Android.Content;
using Billing.Droid.Renderers;
using Billing.UI;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(BillingPage), typeof(BillingPageRenderer))]
namespace Billing.Droid.Renderers
{
public class BillingPageRenderer : PageRenderer
{
public BillingPageRenderer(Context context) : base(context)
{
}
protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
if (Element is BillingPage page)
{
page.TriggerLoad();
}
}
}
}

View File

@ -0,0 +1,30 @@
using Android.Content;
using Billing.Droid.Renderers;
using Billing.UI;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(BlurryPanel), typeof(BlurryPanelRenderer))]
namespace Billing.Droid.Renderers
{
public class BlurryPanelRenderer : ViewRenderer
{
public BlurryPanelRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
var color = e.NewElement.BackgroundColor;
if (!color.IsDefault)
{
SetBackgroundColor(color.MultiplyAlpha(.94).ToAndroid());
}
}
}
}
}

View File

@ -18,10 +18,12 @@ namespace Billing.Droid.Renderers
{
base.OnElementChanged(e);
if (e.NewElement != null)
var control = Control;
if (e.NewElement != null && control != null)
{
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);
if (e.NewElement != null)
var control = Control;
if (e.NewElement != null && control != null)
{
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;
}
}
}
}

View File

@ -0,0 +1,45 @@
using System.ComponentModel;
using Android.Content;
using Billing.Droid.Renderers;
using Billing.UI;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(TintImageButton), typeof(TintImageButtonRenderer))]
namespace Billing.Droid.Renderers
{
public class TintImageButtonRenderer : ImageButtonRenderer
{
public TintImageButtonRenderer(Context context) : base(context)
{
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == TintHelper.TintColor ||
e.PropertyName == nameof(Image.Source))
{
if (Drawable != null && Element is TintImageButton image)
{
Drawable.SetTint(TintHelper.GetTintColor(image)?.ToAndroid() ?? 0);
}
}
}
protected override void OnElementChanged(ElementChangedEventArgs<ImageButton> e)
{
base.OnElementChanged(e);
if (e.NewElement is TintImageButton image)
{
SetBackgroundColor(Android.Graphics.Color.Transparent);
if (Drawable != null)
{
Drawable.SetTint(TintHelper.GetTintColor(image)?.ToAndroid() ?? 0);
}
}
}
}
}

View File

@ -18,12 +18,12 @@ namespace Billing.Droid.Renderers
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == nameof(TintImage.PrimaryColor) ||
if (e.PropertyName == TintHelper.TintColor ||
e.PropertyName == nameof(Image.Source))
{
if (Control?.Drawable != null && Element is TintImage image)
{
Control.Drawable.SetTint(image.PrimaryColor?.ToAndroid() ?? 0);
Control.Drawable.SetTint(TintHelper.GetTintColor(image)?.ToAndroid() ?? 0);
}
}
}
@ -34,7 +34,7 @@ namespace Billing.Droid.Renderers
if (Control?.Drawable != null && Element is TintImage image)
{
Control.Drawable.SetTint(image.PrimaryColor?.ToAndroid() ?? 0);
Control.Drawable.SetTint(TintHelper.GetTintColor(image)?.ToAndroid() ?? 0);
}
}
}

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.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 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.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

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