Compare commits
44 Commits
526da1ede8
...
master
Author | SHA1 | Date | |
---|---|---|---|
776cc7da49 | |||
1072a1a15c | |||
469b1a8627 | |||
ba289b6087 | |||
b7affae8ab | |||
7ca377b8c2 | |||
f27b0a2564 | |||
a214110c8c | |||
4067bc2768 | |||
ba7b3e7389 | |||
5cbcfbcd56 | |||
ef5e91aad1 | |||
60f7824cb5 | |||
b46b150f6a | |||
cac4735bc4 | |||
d3af69b31e | |||
8ba6f4bf85 | |||
5b209cc19c | |||
77b4e54734 | |||
9a8f1289ed | |||
c43bfb51be | |||
51ac42b9fc | |||
6d2e0624ab | |||
28897c96ea | |||
5ec4119025 | |||
f5f16d43f4 | |||
71c1a7f0f1 | |||
74053a328e | |||
84ec2df987 | |||
fde8931dbd | |||
abffc0627e | |||
b5c531d128 | |||
db055fa205 | |||
ae619c8fee | |||
e1d80b6c62 | |||
63ee572e8b | |||
91db3caa15 | |||
4076b2c9ed | |||
c3718d9d6c | |||
9f783c1b5f | |||
46464e19dc | |||
49e4e46cdb | |||
0c3b8c52bd | |||
bc5cf4afa5 |
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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>
|
@ -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>
|
88
Billing.Shared/LocationExtension.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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
|
||||
|
7
Billing.Shared/Models/IIdItem.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Billing.Models
|
||||
{
|
||||
public interface IIdItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
}
|
14
Billing.Shared/Models/Logs.cs
Normal 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; }
|
||||
}
|
||||
}
|
9
Billing.Shared/SplashPage.xaml
Normal 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>
|
23
Billing.Shared/SplashPage.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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}"/>
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
141
Billing.Shared/UI/ColorPicker.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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 { }
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
70
Billing.Shared/UI/SegmentedControl.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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}"
|
||||
|
@ -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,
|
||||
|
@ -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}"/>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
50
Billing.Shared/Views/AddCategoryPage.xaml
Normal 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>
|
148
Billing.Shared/Views/AddCategoryPage.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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);
|
||||
|
48
Billing.Shared/Views/CategoryPage.xaml
Normal 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>
|
239
Billing.Shared/Views/CategoryPage.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
73
Billing.Shared/Views/CategorySelectPage.xaml
Normal 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>
|
149
Billing.Shared/Views/CategorySelectPage.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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">
|
||||
|
@ -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
|
||||
{
|
||||
|
173
Billing.Shared/Views/RankPage.xaml
Normal 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>
|
635
Billing.Shared/Views/RankPage.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
129
Billing.Shared/Views/ViewLocationPage.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
16
Billing.sln
@ -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
|
||||
|
BIN
Billing/Billing.Android/Assets/OpenSans-Bold.ttf
Normal file
BIN
Billing/Billing.Android/Assets/OpenSans-Regular.ttf
Normal file
BIN
Billing/Billing.Android/Assets/OpenSans-SemiBold.ttf
Normal 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" />
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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)]
|
||||
|
25
Billing/Billing.Android/Renderers/BillingPageRenderer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
Billing/Billing.Android/Renderers/BlurryPanelRenderer.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
29
Billing/Billing.Android/Renderers/OptionPickerRenderer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
245
Billing/Billing.Android/Renderers/SegmentedControlRenderer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
45
Billing/Billing.Android/Renderers/TintImageButtonRenderer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15549
Billing/Billing.Android/Resources/Resource.designer.cs
generated
@ -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>
|
BIN
Billing/Billing.Android/Resources/drawable-mdpi/brunch.png
Normal file
After Width: | Height: | Size: 540 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/color.png
Normal file
After Width: | Height: | Size: 518 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/dinner.png
Normal file
After Width: | Height: | Size: 970 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/fee.png
Normal file
After Width: | Height: | Size: 763 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/filter.png
Normal file
After Width: | Height: | Size: 190 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/fitness.png
Normal file
After Width: | Height: | Size: 563 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/fruit.png
Normal file
After Width: | Height: | Size: 455 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/gem.png
Normal file
After Width: | Height: | Size: 789 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/left.png
Normal file
After Width: | Height: | Size: 262 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/location.png
Normal file
After Width: | Height: | Size: 797 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/maintenance.png
Normal file
After Width: | Height: | Size: 589 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/makeup.png
Normal file
After Width: | Height: | Size: 677 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/party.png
Normal file
After Width: | Height: | Size: 970 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/pin.png
Normal file
After Width: | Height: | Size: 443 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/rail.png
Normal file
After Width: | Height: | Size: 481 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/rank.png
Normal file
After Width: | Height: | Size: 264 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/rent.png
Normal file
After Width: | Height: | Size: 543 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/share.png
Normal file
After Width: | Height: | Size: 403 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/sync.png
Normal file
After Width: | Height: | Size: 799 B |
BIN
Billing/Billing.Android/Resources/drawable-mdpi/taxi.png
Normal file
After Width: | Height: | Size: 471 B |
BIN
Billing/Billing.Android/Resources/drawable-xhdpi/brunch.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
Billing/Billing.Android/Resources/drawable-xhdpi/color.png
Normal file
After Width: | Height: | Size: 985 B |
BIN
Billing/Billing.Android/Resources/drawable-xhdpi/dinner.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
Billing/Billing.Android/Resources/drawable-xhdpi/fee.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
Billing/Billing.Android/Resources/drawable-xhdpi/filter.png
Normal file
After Width: | Height: | Size: 214 B |