This commit is contained in:
2022-02-23 16:09:24 +08:00
commit 11f4666b8e
146 changed files with 32476 additions and 0 deletions

61
Billing.Shared/App.cs Normal file
View File

@@ -0,0 +1,61 @@
using Billing.Languages;
using Billing.Themes;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Billing;
public class App : Application
{
public static AppTheme CurrentTheme { get; private set; }
public static PlatformCulture CurrentCulture { get; private set; }
public App()
{
CurrentCulture = new PlatformCulture();
InitResources();
MainPage = new MainShell();
Shell.Current.GoToAsync("//Bills");
}
protected override void OnStart()
{
}
protected override void OnResume()
{
SetTheme(AppInfo.RequestedTheme);
}
private void InitResources()
{
var theme = AppInfo.RequestedTheme;
SetTheme(theme, true);
}
private void SetTheme(AppTheme theme, bool force = false)
{
if (force || theme != CurrentTheme)
{
CurrentTheme = theme;
}
else
{
return;
}
Helper.Debug($"application theme: {theme}");
BaseTheme instance;
if (theme == AppTheme.Dark)
{
instance = Dark.Instance;
}
else
{
instance = Light.Instance;
}
// TODO: status bar
Resources = instance;
}
}

View File

@@ -0,0 +1,3 @@
using Xamarin.Forms.Xaml;
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' &lt; '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>6ac75d01-70d6-4a07-8685-bc52afd97a7a</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>Billing</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Languages\en.xml" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Languages\zh-CN.xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)App.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AssemblyInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Languages\PlatformCulture.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Languages\Resource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)MainShell.xaml.cs">
<DependentUpon>MainShell.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Themes\BaseTheme.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Themes\Dark.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Themes\Light.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\BillingDate.xaml.cs">
<DependentUpon>BillingDate.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)UI\BillingPage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\Converters.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\CustomControl.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\Definition.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Views\AccountPage.xaml.cs">
<DependentUpon>AccountPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\AddBillPage.xaml.cs">
<DependentUpon>AddBillPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\BillPage.xaml.cs">
<DependentUpon>BillPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\SettingPage.xaml.cs">
<DependentUpon>SettingPage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)MainShell.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)UI\BillingDate.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\AccountPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\AddBillPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\BillPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\SettingPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>6ac75d01-70d6-4a07-8685-bc52afd97a7a</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="Billing.Shared.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

23
Billing.Shared/Helper.cs Normal file
View File

@@ -0,0 +1,23 @@
using System;
namespace Billing;
internal static class Helper
{
public static void Debug(string message)
{
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);
}
}

View File

@@ -0,0 +1,61 @@
namespace Billing.Languages
{
public partial class PlatformCulture
{
public string PlatformString { get; set; }
public string LanguageCode { get; set; }
public string LocaleCode { get; set; }
public string Language => string.IsNullOrEmpty(LocaleCode) ? LanguageCode : LanguageCode + "-" + LocaleCode;
public partial string GetNamespace();
public partial void Init();
public PlatformCulture()
{
Init();
}
private void Init(string cultureString)
{
if (string.IsNullOrEmpty(cultureString))
{
cultureString = "zh-CN";
}
PlatformString = cultureString.Replace('_', '-');
if (PlatformString.Contains('-'))
{
var parts = PlatformString.Split('-');
LanguageCode = parts[0];
LocaleCode = parts[^1];
}
else
{
LanguageCode = PlatformString;
LocaleCode = string.Empty;
}
}
private string ToDotnetFallbackLanguage()
{
string netLanguage = LanguageCode switch
{
// fallback to Portuguese (Portugal)
"pt" => "pt-PT",
// equivalent to German (Switzerland) for this app
"gsw" => "de-CH",
// add more application-specific cases here (if required)
// ONLY use cultures that have been tested and known to work
// use the first part of the identifier (two chars, usually);
_ => LanguageCode,
};
Helper.Debug($".NET Fallback Language/Locale: {LanguageCode} to {netLanguage} (application-specific)");
return netLanguage;
}
public override string ToString() => PlatformString;
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Billing.Languages
{
internal class Resource
{
public static string TitleDateFormat => Text(nameof(TitleDateFormat));
static readonly Dictionary<string, LanguageResource> dict = new();
public static string Text(string name, params object[] args)
{
string current = App.CurrentCulture.PlatformString;
if (!dict.TryGetValue(current, out LanguageResource lang))
{
lang = new LanguageResource(App.CurrentCulture);
dict.Add(current, lang);
}
if (args == null || args.Length == 0)
{
return lang[name];
}
return string.Format(lang[name], args);
}
private class LanguageResource
{
private readonly Dictionary<string, string> strings;
public string this[string key]
{
get
{
if (strings?.TryGetValue(key, out string val) == true)
{
return val;
}
return key;
}
}
public LanguageResource(PlatformCulture lang)
{
try
{
var ns = lang.GetNamespace();
var langId = $"{ns}.Languages.{lang.Language}.xml";
var langCodeId = $"{ns}.Languages.{lang.LanguageCode}.xml";
var assembly = typeof(LanguageResource).Assembly;
var name = assembly.GetManifestResourceNames().FirstOrDefault(n =>
string.Equals(n, langId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(n, langCodeId, StringComparison.OrdinalIgnoreCase));
if (name == null)
{
name = $"{ns}.Languages.zh-CN.xml";
}
var xml = new XmlDocument();
using (var stream = assembly.GetManifestResourceStream(name))
{
xml.Load(stream);
}
strings = new Dictionary<string, string>();
foreach (XmlElement ele in xml.DocumentElement)
{
strings[ele.Name] = ele.InnerText;
}
}
catch (Exception ex)
{
Helper.Error("language.ctor", $"failed to load json resource: {lang}, {ex.Message}");
}
}
}
}
[ContentProperty(nameof(Text))]
public class TextExtension : IMarkupExtension
{
public string Text { get; set; }
public object ProvideValue(IServiceProvider serviceProvider)
{
if (Text == null)
{
return string.Empty;
}
return Resource.Text(Text);
}
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<Accounts>Accounts</Accounts>
<Bills>Bills</Bills>
<Settings>Settings</Settings>
<Sunday>Su</Sunday>
<Monday>Mo</Monday>
<Tuesday>Tu</Tuesday>
<Wednesday>We</Wednesday>
<Thursday>Th</Thursday>
<Friday>Fr</Friday>
<Saturday>Sa</Saturday>
<NoRecords>Bills not yet generated</NoRecords>
<Memo>Click here to record</Memo>
<TitleDateFormat>MM/dd/yyyy</TitleDateFormat>
</root>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<Accounts>账户</Accounts>
<Bills>账单</Bills>
<Settings>设置</Settings>
<Sunday>周日</Sunday>
<Monday>周一</Monday>
<Tuesday>周二</Tuesday>
<Wednesday>周三</Wednesday>
<Thursday>周四</Thursday>
<Friday>周五</Friday>
<Saturday>周六</Saturday>
<NoRecords>还未产生账单</NoRecords>
<Memo>点此记录</Memo>
<TitleDateFormat>yyyy年MM月dd日</TitleDateFormat>
</root>

View File

@@ -0,0 +1,18 @@
<?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:v="clr-namespace:Billing.Views"
xmlns:r="clr-namespace:Billing.Languages"
x:Class="Billing.MainShell"
BackgroundColor="{DynamicResource WindowBackgroundColor}"
ForegroundColor="{DynamicResource PrimaryColor}"
TitleColor="{DynamicResource PrimaryColor}"
Shell.NavBarHasShadow="True">
<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:SettingPage}" Route="Settings" Title="{r:Text Settings}" Icon="settings.png"/>
</TabBar>
</Shell>

View File

@@ -0,0 +1,11 @@
using Xamarin.Forms;
namespace Billing;
public partial class MainShell : Shell
{
public MainShell()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,65 @@
using Billing.UI;
using Xamarin.Forms;
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 WindowBackgroundColor = nameof(WindowBackgroundColor);
public const string PromptBackgroundColor = nameof(PromptBackgroundColor);
public const string PrimaryColor = nameof(PrimaryColor);
public const string SecondaryColor = nameof(SecondaryColor);
public const string TabBarBackgroundColor = nameof(TabBarBackgroundColor);
public const string TabBarTitleColor = nameof(TabBarTitleColor);
public const string TabBarUnselectedColor = nameof(TabBarUnselectedColor);
public const string OutRangeDayColor = nameof(OutRangeDayColor);
public const string TextColor = nameof(TextColor);
public const string WeekendColor = nameof(WeekendColor);
protected abstract Color PrimaryMauiColor { get; }
protected abstract Color SecondaryMauiColor { get; }
protected void InitResources()
{
var robotoRegularFontFamily = Definition.GetRobotoCondensedRegularFontFamily();
Add(CascadiaFontRegular, Definition.GetCascadiaRegularFontFamily());
Add(CascadiaFontBold, Definition.GetCascadiaBoldFontFamily());
Add(RobotoCondensedFontRegular, Definition.GetRobotoCondensedRegularFontFamily());
Add(RobotoCondensedFontBold, Definition.GetRobotoCondensedBoldFontFamily());
Add(PrimaryColor, PrimaryMauiColor);
Add(SecondaryColor, SecondaryMauiColor);
Add(TabBarTitleColor, PrimaryMauiColor);
Add(new Style(typeof(Label))
{
Setters =
{
new Setter { Property = Label.FontSizeProperty, Value = Device.GetNamedSize(NamedSize.Small, typeof(Label)) },
new Setter { Property = Label.TextColorProperty, Value = PrimaryMauiColor },
new Setter { Property = Label.FontFamilyProperty, Value = robotoRegularFontFamily }
}
});
Add(new Style(typeof(Button))
{
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) }
}
});
Add(new Style(typeof(TintImage))
{
Setters =
{
new Setter { Property = TintImage.PrimaryColorProperty, Value = PrimaryMauiColor }
}
});
}
}

View File

@@ -0,0 +1,40 @@
using Xamarin.Forms;
namespace Billing.Themes;
public class Dark : BaseTheme
{
private static Dark _instance;
public static Dark Instance => _instance ??= new Dark();
protected override Color PrimaryMauiColor => Color.White;
protected override Color SecondaryMauiColor => Color.LightGray;
public Dark()
{
InitColors();
InitResources();
}
private void InitColors()
{
Add(WindowBackgroundColor, Color.Black);
Add(PromptBackgroundColor, Color.FromRgb(0x1f, 0x1f, 0x1f));
Add(TabBarBackgroundColor, Color.Black);
Add(TabBarUnselectedColor, Color.FromRgb(0x82, 0x82, 0x82));
Add(OutRangeDayColor, Color.DarkGray);
Add(TextColor, Color.FromRgb(0xcc, 0xcc, 0xcc));
Add(WeekendColor, Color.FromRgb(211, 5, 5));
Add(new Style(typeof(TabBar))
{
Setters =
{
new Setter { Property = Shell.TabBarBackgroundColorProperty, Value = Color.Black },
new Setter { Property = Shell.TabBarTitleColorProperty, Value = PrimaryMauiColor },
new Setter { Property = Shell.TabBarUnselectedColorProperty, Value = Color.FromRgb(0x82, 0x82, 0x82) }
}
});
}
}

View File

@@ -0,0 +1,40 @@
using Xamarin.Forms;
namespace Billing.Themes;
public class Light : BaseTheme
{
private static Light _instance;
public static Light Instance => _instance ??= new Light();
protected override Color PrimaryMauiColor => Color.FromRgb(0x18, 0x31, 0x53);
protected override Color SecondaryMauiColor => Color.White;
public Light()
{
InitColors();
InitResources();
}
private void InitColors()
{
Add(WindowBackgroundColor, Color.White);
Add(PromptBackgroundColor, Color.FromRgb(0xe0, 0xe0, 0xe0));
Add(TabBarBackgroundColor, Color.White);
Add(TabBarUnselectedColor, Color.FromRgb(0x82, 0x82, 0x82));
Add(OutRangeDayColor, Color.LightGray);
Add(TextColor, Color.FromRgb(0x33, 0x33, 0x33));
Add(WeekendColor, Color.FromRgb(211, 64, 85));
Add(new Style(typeof(TabBar))
{
Setters =
{
new Setter { Property = Shell.TabBarBackgroundColorProperty, Value = Color.White },
new Setter { Property = Shell.TabBarTitleColorProperty, Value = PrimaryMauiColor },
new Setter { Property = Shell.TabBarUnselectedColorProperty, Value = Color.FromRgb(0x82, 0x82, 0x82) }
}
});
}
}

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:r="clr-namespace:Billing.Languages"
xmlns:ui="clr-namespace:Billing.UI"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Billing.UI.BillingDate"
x:DataType="ui:BillingDate"
x:Name="billingDate"
BindingContext="{x:Reference billingDate}">
<ContentView.Resources>
<Style x:Key="centerLabel" TargetType="Label">
<Setter Property="HorizontalOptions" Value="Center"/>
<Setter Property="FontSize" Value="Small"/>
<Setter Property="TextColor" Value="{DynamicResource TextColor}"/>
</Style>
<Style x:Key="dateLabel" TargetType="Label">
<Setter Property="HorizontalOptions" Value="Center"/>
</Style>
<ControlTemplate x:Key="weekDay">
<Grid Padding="0, 8, 0, 0" RowDefinitions="Auto, 3" RowSpacing="0">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding OnDayTapped, Source={x:Reference billingDate}}" CommandParameter="{TemplateBinding BillingDay}"/>
</Grid.GestureRecognizers>
<Label Style="{StaticResource dateLabel}" Text="{TemplateBinding BillingDay.Text}"
TextColor="{DynamicResource TextColor}"
FontFamily="{TemplateBinding BillingDay.FontFamily}"
Opacity="{TemplateBinding BillingDay.TextOpacity}"/>
<StackLayout Grid.Row="1"
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="0">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding OnDayTapped, Source={x:Reference billingDate}}" CommandParameter="{TemplateBinding BillingDay}"/>
</Grid.GestureRecognizers>
<Label Style="{StaticResource dateLabel}" Text="{TemplateBinding BillingDay.Text}"
TextColor="{DynamicResource WeekendColor}"
FontFamily="{TemplateBinding BillingDay.FontFamily}"
Opacity="{TemplateBinding BillingDay.TextOpacity}"/>
<StackLayout Grid.Row="1"
IsVisible="{TemplateBinding BillingDay.IsSelected}"
BackgroundColor="{DynamicResource PrimaryColor}"
Opacity="{TemplateBinding BillingDay.Opacity}"/>
</Grid>
</ControlTemplate>
<Style TargetType="ui:BillingDayView">
<Setter Property="ControlTemplate" Value="{StaticResource weekDay}"/>
</Style>
</ContentView.Resources>
<ContentView.Content>
<Grid Padding="0, 8, 0, 4" ColumnDefinitions="*,*,*,*,*,*,*" RowDefinitions="Auto, Auto">
<Grid.GestureRecognizers>
<PanGestureRecognizer PanUpdated="OnPanUpdated"/>
</Grid.GestureRecognizers>
<Label Grid.Column="0" Text="{r:Text Sunday}" Style="{StaticResource centerLabel}"/>
<Label Grid.Column="1" Text="{r:Text Monday}" Style="{StaticResource centerLabel}"/>
<Label Grid.Column="2" Text="{r:Text Tuesday}" Style="{StaticResource centerLabel}"/>
<Label Grid.Column="3" Text="{r:Text Wednesday}" Style="{StaticResource centerLabel}"/>
<Label Grid.Column="4" Text="{r:Text Thursday}" Style="{StaticResource centerLabel}"/>
<Label Grid.Column="5" Text="{r:Text Friday}" Style="{StaticResource centerLabel}"/>
<Label Grid.Column="6" Text="{r:Text Saturday}" Style="{StaticResource centerLabel}"/>
<ui:BillingDayView Grid.Row="1" Grid.Column="0" BillingDay="{Binding Sunday}" ControlTemplate="{StaticResource weekEnd}"/>
<ui:BillingDayView Grid.Row="1" Grid.Column="1" BillingDay="{Binding Monday}"/>
<ui:BillingDayView Grid.Row="1" Grid.Column="2" BillingDay="{Binding Tuesday}"/>
<ui:BillingDayView Grid.Row="1" Grid.Column="3" BillingDay="{Binding Wednesday}"/>
<ui:BillingDayView Grid.Row="1" Grid.Column="4" BillingDay="{Binding Thursday}"/>
<ui:BillingDayView Grid.Row="1" Grid.Column="5" BillingDay="{Binding Friday}"/>
<ui:BillingDayView Grid.Row="1" Grid.Column="6" BillingDay="{Binding Saturday}" ControlTemplate="{StaticResource weekEnd}"/>
</Grid>
</ContentView.Content>
</ContentView>

View File

@@ -0,0 +1,290 @@
using Billing.Themes;
using System;
using Xamarin.Forms;
namespace Billing.UI;
public partial class BillingDate : ContentView
{
#region UI Properties
private static readonly BindableProperty SundayProperty = BindableProperty.Create(nameof(Sunday), typeof(BillingDay), typeof(BillingDate));
private static readonly BindableProperty MondayProperty = BindableProperty.Create(nameof(Monday), typeof(BillingDay), typeof(BillingDate));
private static readonly BindableProperty TuesdayProperty = BindableProperty.Create(nameof(Tuesday), typeof(BillingDay), typeof(BillingDate));
private static readonly BindableProperty WednesdayProperty = BindableProperty.Create(nameof(Wednesday), typeof(BillingDay), typeof(BillingDate));
private static readonly BindableProperty ThursdayProperty = BindableProperty.Create(nameof(Thursday), typeof(BillingDay), typeof(BillingDate));
private static readonly BindableProperty FridayProperty = BindableProperty.Create(nameof(Friday), typeof(BillingDay), typeof(BillingDate));
private static readonly BindableProperty SaturdayProperty = BindableProperty.Create(nameof(Saturday), typeof(BillingDay), typeof(BillingDate));
public BillingDay Sunday => (BillingDay)GetValue(SundayProperty);
public BillingDay Monday => (BillingDay)GetValue(MondayProperty);
public BillingDay Tuesday => (BillingDay)GetValue(TuesdayProperty);
public BillingDay Wednesday => (BillingDay)GetValue(WednesdayProperty);
public BillingDay Thursday => (BillingDay)GetValue(ThursdayProperty);
public BillingDay Friday => (BillingDay)GetValue(FridayProperty);
public BillingDay Saturday => (BillingDay)GetValue(SaturdayProperty);
#endregion
private static BindableProperty GetWeekProperty(int week)
{
return (DayOfWeek)week switch
{
DayOfWeek.Monday => MondayProperty,
DayOfWeek.Tuesday => TuesdayProperty,
DayOfWeek.Wednesday => WednesdayProperty,
DayOfWeek.Thursday => ThursdayProperty,
DayOfWeek.Friday => FridayProperty,
DayOfWeek.Saturday => SaturdayProperty,
_ => SundayProperty
};
}
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);
private static void OnLocatedDatePropertyChanged(BindableObject obj, object old, object @new)
{
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 prop = GetWeekProperty(i);
var day = new BillingDay(tmpDate);
billingDate.SetValue(prop, day);
tmpDate = tmpDate.AddDays(1);
}
}
}
private static void OnSelectedDatePropertyChanged(BindableObject obj, object old, object @new)
{
if (obj is BillingDate billingDate && @new is DateTime selected)
{
for (var i = 0; i < 7; i++)
{
var prop = GetWeekProperty(i);
var day = (BillingDay)billingDate.GetValue(prop);
day.Refresh(selected);
}
}
}
public DateTime LocatedDate
{
get => (DateTime)GetValue(LocatedDateProperty);
set => SetValue(LocatedDateProperty, value);
}
public DateTime SelectedDate
{
get => (DateTime)GetValue(SelectedDateProperty);
set => SetValue(SelectedDateProperty, value);
}
public Command OnDayTapped { get; }
public event EventHandler<DateEventArgs> DateSelected;
private DateTime lastDate;
private double? x1;
private double x2;
public BillingDate()
{
OnDayTapped = new Command(OnDayChanged);
InitializeComponent();
}
public void SetDateTime(DateTime selectedDate, DateTime? locatedDate = null)
{
if (locatedDate != null)
{
LocatedDate = locatedDate.Value;
}
else
{
LocatedDate = selectedDate;
}
SelectedDate = selectedDate;
DateSelected?.Invoke(this, new DateEventArgs(selectedDate));
}
private void OnDayChanged(object o)
{
var selected = SelectedDate;
if (o is BillingDay day && (selected.Year != day.Date.Year || selected.DayOfYear != day.Date.DayOfYear))
{
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;
});
}
}
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 OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
if (e.StatusType == GestureStatus.Started)
{
lastDate = DateTime.Now;
x1 = null;
}
else if (e.StatusType == GestureStatus.Running)
{
if (x1 == null)
{
x1 = e.TotalX;
}
x2 = e.TotalX;
}
else if (e.StatusType == GestureStatus.Completed)
{
if (x1 == null)
{
return;
}
var totalX = x2 - x1.Value;
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);
}
else if (speed > 0.7)
{
LocatedDate = LocatedDate.AddDays(-7);
}
OnSelectedDatePropertyChanged(this, null, SelectedDate);
}
}
}
public class DateEventArgs : EventArgs
{
public DateTime Date { get; }
public DateEventArgs(DateTime date)
{
Date = date;
}
}
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 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
{
private static readonly BindableProperty DateProperty = BindableProperty.Create(nameof(Date), typeof(DateTime), typeof(BillingDay), propertyChanged: OnDatePropertyChanged);
private static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(BillingDay));
private static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(BillingDay), defaultValue: Definition.GetCascadiaRegularFontFamily());
private static readonly BindableProperty IsSelectedProperty = BindableProperty.Create(nameof(IsSelected), typeof(bool), typeof(BillingDay));
private static readonly BindableProperty OpacityProperty = BindableProperty.Create(nameof(Opacity), typeof(double), typeof(BillingDay), defaultValue: 1.0);
private static readonly BindableProperty TextOpacityProperty = BindableProperty.Create(nameof(TextOpacity), typeof(double), typeof(BillingDay), defaultValue: 1.0);
private static void OnDatePropertyChanged(BindableObject obj, object old, object @new)
{
if (obj is BillingDay day && @new is DateTime date)
{
if (date.Day == 1)
{
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 string Text => (string)GetValue(TextProperty);
public string FontFamily => (string)GetValue(FontFamilyProperty);
public bool IsSelected
{
get => (bool)GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
public double Opacity
{
get => (double)GetValue(OpacityProperty);
set => SetValue(OpacityProperty, value);
}
public double TextOpacity => (double)GetValue(TextOpacityProperty);
public BillingDay(DateTime date)
{
Date = date;
}
public void Refresh(DateTime selected)
{
var date = Date;
if (date.Year == selected.Year && date.DayOfYear == selected.DayOfYear)
{
SetValue(IsSelectedProperty, true);
SetValue(FontFamilyProperty, Definition.GetCascadiaBoldFontFamily());
}
else
{
SetValue(FontFamilyProperty, Definition.GetCascadiaRegularFontFamily());
}
if (date.Year == selected.Year && date.Month == selected.Month)
{
SetValue(TextOpacityProperty, 1.0);
}
else
{
SetValue(TextOpacityProperty, .4);
}
}
}

View File

@@ -0,0 +1,12 @@
using Billing.Themes;
using Xamarin.Forms;
namespace Billing.UI;
public abstract class BillingPage : ContentPage
{
public BillingPage()
{
SetDynamicResource(BackgroundColorProperty, BaseTheme.WindowBackgroundColor);
}
}

View File

@@ -0,0 +1,23 @@
using Billing.Languages;
using System;
using System.Globalization;
using Xamarin.Forms;
namespace Billing.UI;
public class TitleDateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is DateTime date)
{
return date.ToString(Resource.TitleDateFormat);
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}

View File

@@ -0,0 +1,31 @@
using System;
using Xamarin.Forms;
namespace Billing.UI;
public class TintImage : Image
{
public static readonly BindableProperty PrimaryColorProperty = BindableProperty.Create(nameof(PrimaryColor), typeof(Color?), typeof(TintImage));
public Color? PrimaryColor
{
get => (Color?)GetValue(PrimaryColorProperty);
set => SetValue(PrimaryColorProperty, value);
}
}
public class LongPressButton : Button
{
public event EventHandler LongPressed;
public LongPressButton()
{
Padding = 0;
BackgroundColor = Color.Transparent;
}
public void TriggerLongPress()
{
LongPressed?.Invoke(this, EventArgs.Empty);
}
}

View File

@@ -0,0 +1,9 @@
namespace Billing.UI;
public static partial class Definition
{
public static partial string GetCascadiaRegularFontFamily();
public static partial string GetCascadiaBoldFontFamily();
public static partial string GetRobotoCondensedRegularFontFamily();
public static partial string GetRobotoCondensedBoldFontFamily();
}

View File

@@ -0,0 +1,13 @@
<?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:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Billing.Views.AccountPage"
Title="{r:Text Accounts}">
<StackLayout>
<Label Text="Welcome to Account Page!"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
</StackLayout>
</ui:BillingPage>

View File

@@ -0,0 +1,11 @@
using Billing.UI;
namespace Billing.Views;
public partial class AccountPage : BillingPage
{
public AccountPage()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,14 @@
<?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.Views.AddBillPage"
x:DataType="v:AddBillPage"
Title="Add Billing">
<StackLayout>
<Label Text="Add a billing here..."
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
</StackLayout>
</ui:BillingPage>

View File

@@ -0,0 +1,11 @@
using Billing.UI;
namespace Billing.Views;
public partial class AddBillPage : BillingPage
{
public AddBillPage()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,56 @@
<?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.BillPage"
x:DataType="v:BillPage"
x:Name="billPage"
BindingContext="{x:Reference billPage}"
Title="{r:Text Bills}">
<ContentPage.Resources>
<ui:TitleDateConverter x:Key="titleDateConverter"/>
</ContentPage.Resources>
<Shell.TitleView>
<Grid ColumnSpacing="16" ColumnDefinitions="20,*,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>
</Shell.TitleView>
<Grid RowDefinitions="Auto,*">
<ui:BillingDate x:Name="billingDate" SelectedDate="{Binding SelectedDate}" DateSelected="OnDateSelected"/>
<ScrollView Grid.Row="1">
<Grid Padding="8" ColumnSpacing="8" ColumnDefinitions="Auto, *, Auto"
BackgroundColor="{DynamicResource PromptBackgroundColor}"
VerticalOptions="Start">
<ui:TintImage Source="bars.png" WidthRequest="23" HeightRequest="23"/>
<Label Grid.Column="1" Text="{r:Text NoRecords}" TextColor="{DynamicResource TextColor}"
VerticalOptions="Center"/>
<StackLayout Grid.Column="2" Orientation="Horizontal" Spacing="6">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding AddBilling}"/>
</StackLayout.GestureRecognizers>
<Label Text="{r:Text Memo}" TextColor="{DynamicResource PrimaryColor}"
VerticalOptions="Center"/>
<!--<Label Style="{DynamicResource IconLightStyle}"
Text="{x:Static local:Definition.IconRight}"
TextColor="{DynamicResource TabBarUnselectedColor}"/>-->
<ui:TintImage Source="right.png" WidthRequest="24" HeightRequest="24"/>
</StackLayout>
</Grid>
</ScrollView>
<!--<ui:CircleButton Grid.Row="1" VerticalOptions="End" HorizontalOptions="End"
Margin="20" Padding="0"
BackgroundColor="{DynamicResource PrimaryColor}"
ImageSource="plus.png" HeightRequest="24" WidthRequest="24"/>-->
</Grid>
</ui:BillingPage>

View File

@@ -0,0 +1,44 @@
using Billing.UI;
using System;
using Xamarin.Forms;
namespace Billing.Views;
public partial class BillPage : BillingPage
{
private static readonly BindableProperty SelectedDateProperty = BindableProperty.Create(nameof(SelectedDate), typeof(DateTime), typeof(BillPage));
public DateTime SelectedDate
{
get => (DateTime)GetValue(SelectedDateProperty);
set => SetValue(SelectedDateProperty, value);
}
public Command AddBilling { get; }
public BillPage()
{
AddBilling = new Command(OnAddBilling);
InitializeComponent();
billingDate.SetDateTime(DateTime.Now);
}
private void OnDateSelected(object sender, DateEventArgs e)
{
SelectedDate = e.Date;
// TODO: while selecting date
}
private void OnTitleDateLongPressed(object sender, EventArgs e)
{
billingDate.SetDateTime(DateTime.Now);
}
private async void OnAddBilling()
{
await Navigation.PushAsync(new AddBillPage());
}
}

View File

@@ -0,0 +1,13 @@
<?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:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Billing.Views.SettingPage"
Title="{r:Text Settings}">
<StackLayout>
<Label Text="Welcome to Settings Page!"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
</StackLayout>
</ui:BillingPage>

View File

@@ -0,0 +1,11 @@
using Billing.UI;
namespace Billing.Views;
public partial class SettingPage : BillingPage
{
public SettingPage()
{
InitializeComponent();
}
}