feature: add flower
This commit is contained in:
75
FlowerApp/Views/Garden/AddFlowerPage.xaml
Normal file
75
FlowerApp/Views/Garden/AddFlowerPage.xaml
Normal file
@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<l:AppContentPage
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:l="clr-namespace:Blahblah.FlowerApp"
|
||||
xmlns:ctl="clr-namespace:Blahblah.FlowerApp.Controls"
|
||||
xmlns:garden="clr-namespace:Blahblah.FlowerApp.Views.Garden"
|
||||
x:Class="Blahblah.FlowerApp.Views.Garden.AddFlowerPage"
|
||||
x:Name="addFlowerPage"
|
||||
x:DataType="garden:AddFlowerPage"
|
||||
Title="{l:Lang addFlower, Default=Add Flower}">
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{l:Lang save, Default=Save}" Clicked="Save_Clicked"/>
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<garden:CoverConverter x:Key="coverConverter"/>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<Grid RowDefinitions="Auto,*" BindingContext="{x:Reference addFlowerPage}">
|
||||
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="20" Margin="20">
|
||||
<VerticalStackLayout>
|
||||
<Grid Padding="0,10,10,0">
|
||||
<Image Source="{Binding Cover, Converter={StaticResource coverConverter}}" WidthRequest="80" HeightRequest="80" Aspect="AspectFill">
|
||||
<Image.Clip>
|
||||
<EllipseGeometry Center="40,40" RadiusX="40" RadiusY="40"/>
|
||||
</Image.Clip>
|
||||
</Image>
|
||||
<Button Style="{StaticResource iconButton}" Text="{x:Static l:Res.XMarkLarge}" Clicked="ButtonClearCover_Clicked"
|
||||
IsVisible="{Binding Cover, Converter={StaticResource notNullConverter}}"
|
||||
HorizontalOptions="End" VerticalOptions="Start" Margin="0,-20,-20,0"
|
||||
TextColor="{AppThemeBinding Light={StaticResource Red100Accent}, Dark={StaticResource Red300Accent}}"/>
|
||||
</Grid>
|
||||
<HorizontalStackLayout HorizontalOptions="Center" Padding="0,0,10,0">
|
||||
<Button Style="{StaticResource iconButton}" Text="{x:Static l:Res.Camera}" Clicked="ButtonTakePhoto_Clicked"/>
|
||||
<Button Style="{StaticResource iconButton}" Text="{x:Static l:Res.Image}" Clicked="ButtonSelectPhoto_Clicked"/>
|
||||
</HorizontalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
<VerticalStackLayout Grid.Column="1" Padding="0,10,0,0">
|
||||
<HorizontalStackLayout>
|
||||
<Label Text="{l:Lang flowerName, Default=Flower name}" FontSize="16" Margin="6,0" VerticalOptions="Center"/>
|
||||
<ctl:SecondaryLabel Text="*" VerticalOptions="Center"
|
||||
TextColor="{AppThemeBinding Light={StaticResource Red100Accent}, Dark={StaticResource Red300Accent}}"/>
|
||||
</HorizontalStackLayout>
|
||||
<Entry Text="{Binding FlowerName}" Placeholder="{l:Lang enterFlowerName, Default=Please enter the flower name}" Margin="0,12"/>
|
||||
<Frame BorderColor="Transparent" Padding="8,6" BackgroundColor="#b6e8e8" CornerRadius="8" HorizontalOptions="Start">
|
||||
<Label Text="{Binding CurrentLocationString}"/>
|
||||
</Frame>
|
||||
</VerticalStackLayout>
|
||||
</Grid>
|
||||
<TableView Grid.Row="1" Intent="Settings" HasUnevenRows="True" BackgroundColor="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}">
|
||||
<TableSection>
|
||||
<ctl:OptionEntryCell IsRequired="True" Title="{l:Lang locationColon, Default=Location:}"
|
||||
Placeholder="{Binding FlowerLocation}"/>
|
||||
<ctl:OptionSelectCell IsRequired="True" Title="{l:Lang flowerCategoryColon, Default=Flower Category:}"
|
||||
Detail="{Binding Category}" Tapped="FlowerCategory_Tapped"/>
|
||||
<ctl:OptionDateTimePickerCell IsRequired="True" Title="{l:Lang purchaseTimeColon, Default=Purchase time:}"
|
||||
Date="{Binding PurchaseDate}" Time="{Binding PurchaseTime}"/>
|
||||
<ctl:OptionSelectCell Title="{l:Lang purchaseFromColon, Default=Purchase from:}"
|
||||
Detail="{Binding PurchaseFrom}"/>
|
||||
<ctl:OptionEntryCell Title="{l:Lang costColon, Default=Cost:}" Keyboard="Numeric"
|
||||
Placeholder="{l:Lang enterCost, Default=Please enter the cost}"
|
||||
Text="{Binding Cost}"/>
|
||||
<ctl:OptionEditorCell Title="{l:Lang memoColon, Default=Memo:}"
|
||||
Text="{Binding Memo}" Height="200"/>
|
||||
</TableSection>
|
||||
</TableView>
|
||||
<Frame Grid.RowSpan="2" x:Name="loading" BorderColor="Transparent" Margin="0" Padding="20" BackgroundColor="#40000000"
|
||||
IsVisible="False" Opacity="0" HorizontalOptions="Center" VerticalOptions="Center">
|
||||
<ActivityIndicator HorizontalOptions="Center" VerticalOptions="Center" IsRunning="True"/>
|
||||
</Frame>
|
||||
</Grid>
|
||||
|
||||
</l:AppContentPage>
|
390
FlowerApp/Views/Garden/AddFlowerPage.xaml.cs
Normal file
390
FlowerApp/Views/Garden/AddFlowerPage.xaml.cs
Normal file
@ -0,0 +1,390 @@
|
||||
using Blahblah.FlowerApp.Controls;
|
||||
using Blahblah.FlowerApp.Data;
|
||||
using Blahblah.FlowerApp.Data.Model;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using static Blahblah.FlowerApp.Extensions;
|
||||
|
||||
namespace Blahblah.FlowerApp.Views.Garden;
|
||||
|
||||
public partial class AddFlowerPage : AppContentPage
|
||||
{
|
||||
static readonly BindableProperty CurrentLocationProperty = CreateProperty<Location?, AddFlowerPage>(nameof(CurrentLocation), propertyChanged: OnCurrentLocationPropertyChanged);
|
||||
static readonly BindableProperty CurrentLocationStringProperty = CreateProperty<string?, AddFlowerPage>(nameof(CurrentLocationString), defaultValue: L("locating", "Locating..."));
|
||||
|
||||
static void OnCurrentLocationPropertyChanged(BindableObject bindable, object old, object @new)
|
||||
{
|
||||
if (bindable is AddFlowerPage page)
|
||||
{
|
||||
if (@new is Location loc)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
string? city = null;
|
||||
try
|
||||
{
|
||||
var location = WebUtility.UrlEncode($"{{\"x\":{loc.Longitude},\"y\":{loc.Latitude},\"spatialReference\":{{\"wkid\":4326}}}}");
|
||||
using var client = new HttpClient();
|
||||
var result = await client.GetFromJsonAsync<GeoResult>($"https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode?location={location}&distance=100&f=json");
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
if (result.Address == null)
|
||||
{
|
||||
page.LogWarning($"failed to query geo location, with message: {result.Error?.Message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
city = result.Address.City;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
city = L("unknown", "Unknown");
|
||||
page.LogError(ex, $"error occurs when quering geo location: {ex.Message}");
|
||||
}
|
||||
page.SetValue(CurrentLocationStringProperty, city ?? L("unknown", "Unknown"));
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
page.SetValue(CurrentLocationStringProperty, L("unknown", "Unknown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Location? CurrentLocation
|
||||
{
|
||||
get => GetValue<Location?>(CurrentLocationProperty);
|
||||
set => SetValue(CurrentLocationProperty, value);
|
||||
}
|
||||
public string? CurrentLocationString
|
||||
{
|
||||
get => GetValue<string?>(CurrentLocationStringProperty);
|
||||
set => SetValue(CurrentLocationStringProperty, value);
|
||||
}
|
||||
|
||||
static readonly BindableProperty CoverProperty = CreateProperty<string?, AddFlowerPage>(nameof(Cover));
|
||||
static readonly BindableProperty FlowerNameProperty = CreateProperty<string, AddFlowerPage>(nameof(FlowerName));
|
||||
static readonly BindableProperty FlowerLocationProperty = CreateProperty<string?, AddFlowerPage>(nameof(FlowerLocation), defaultValue: L("selectFlowerLocation", "Please select the location"));
|
||||
static readonly BindableProperty CategoryProperty = CreateProperty<string, AddFlowerPage>(nameof(Category), defaultValue: L("selectFlowerCategory", "Please select the flower category"));
|
||||
static readonly BindableProperty PurchaseDateProperty = CreateProperty<DateTime, AddFlowerPage>(nameof(PurchaseDate));
|
||||
static readonly BindableProperty PurchaseTimeProperty = CreateProperty<TimeSpan, AddFlowerPage>(nameof(PurchaseTime));
|
||||
static readonly BindableProperty PurchaseFromProperty = CreateProperty<string?, AddFlowerPage>(nameof(PurchaseFrom), defaultValue: L("selectPurchaseFrom", "Please select where are you purchase from"));
|
||||
static readonly BindableProperty CostProperty = CreateProperty<string?, AddFlowerPage>(nameof(Cost));
|
||||
static readonly BindableProperty MemoProperty = CreateProperty<string?, AddFlowerPage>(nameof(Memo));
|
||||
public string? Cover
|
||||
{
|
||||
get => GetValue<string?>(CoverProperty);
|
||||
set => SetValue(CoverProperty, value);
|
||||
}
|
||||
public string FlowerName
|
||||
{
|
||||
get => GetValue<string>(FlowerNameProperty);
|
||||
set => SetValue(FlowerNameProperty, value);
|
||||
}
|
||||
public string? FlowerLocation
|
||||
{
|
||||
get => GetValue<string?>(FlowerLocationProperty);
|
||||
set => SetValue(FlowerLocationProperty, value);
|
||||
}
|
||||
public string Category
|
||||
{
|
||||
get => GetValue<string>(CategoryProperty);
|
||||
set => SetValue(CategoryProperty, value);
|
||||
}
|
||||
public DateTime PurchaseDate
|
||||
{
|
||||
get => GetValue<DateTime>(PurchaseDateProperty);
|
||||
set => SetValue(PurchaseDateProperty, value);
|
||||
}
|
||||
public TimeSpan PurchaseTime
|
||||
{
|
||||
get => GetValue<TimeSpan>(PurchaseTimeProperty);
|
||||
set => SetValue(PurchaseTimeProperty, value);
|
||||
}
|
||||
public string? PurchaseFrom
|
||||
{
|
||||
get => GetValue<string>(PurchaseFromProperty);
|
||||
set => SetValue(PurchaseFromProperty, value);
|
||||
}
|
||||
public string? Cost
|
||||
{
|
||||
get => GetValue<string?>(CostProperty);
|
||||
set => SetValue(CostProperty, value);
|
||||
}
|
||||
public string? Memo
|
||||
{
|
||||
get => GetValue<string>(MemoProperty);
|
||||
set => SetValue(MemoProperty, value);
|
||||
}
|
||||
|
||||
string? selectedLocation;
|
||||
int? selectedCategoryId;
|
||||
|
||||
public AddFlowerPage(FlowerDatabase database, ILogger logger) : base(database, logger)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
PurchaseDate = now.Date;
|
||||
PurchaseTime = now.TimeOfDay;
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
|
||||
#if DEBUG
|
||||
MainThread.BeginInvokeOnMainThread(() => CurrentLocation = new Location(29.56128954116272, 106.5447724580102));
|
||||
#else
|
||||
MainThread.BeginInvokeOnMainThread(GetLocation);
|
||||
}
|
||||
|
||||
bool accuracyLocation = false;
|
||||
|
||||
async void GetLocation()
|
||||
{
|
||||
var location = await GetLastLocationAsync();
|
||||
CurrentLocation = location;
|
||||
|
||||
if (!accuracyLocation)
|
||||
{
|
||||
location = await GetCurrentLocationAsync();
|
||||
CurrentLocation = location;
|
||||
|
||||
accuracyLocation = location != null;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private async void ButtonTakePhoto_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (MediaPicker.Default.IsCaptureSupported)
|
||||
{
|
||||
var photo = await TakePhoto();
|
||||
|
||||
if (photo != null)
|
||||
{
|
||||
string cache = await CacheFileAsync(photo);
|
||||
Cover = cache;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.AlertError(L("notSupportedCapture", "Your device does not support taking photos."));
|
||||
}
|
||||
}
|
||||
|
||||
private async void ButtonSelectPhoto_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
var photo = await MediaPicker.Default.PickPhotoAsync();
|
||||
|
||||
if (photo != null)
|
||||
{
|
||||
string cache = await CacheFileAsync(photo);
|
||||
Cover = cache;
|
||||
}
|
||||
}
|
||||
|
||||
private void ButtonClearCover_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
Cover = null;
|
||||
}
|
||||
|
||||
private async void FlowerCategory_Tapped(object sender, EventArgs e)
|
||||
{
|
||||
var categories = Database.Categories;
|
||||
if (categories == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var page = new ItemSelectorPage<int, IdTextItem<int>>(
|
||||
L("flowerCategory", "Flower category"),
|
||||
categories.Select(c => new IdTextItem<int>
|
||||
{
|
||||
Id = c.Key,
|
||||
Text = c.Value.Name,
|
||||
Detail = c.Value.Description
|
||||
}).ToArray(),
|
||||
selected: selectedCategoryId != null ?
|
||||
new[] { selectedCategoryId.Value } :
|
||||
Array.Empty<int>(),
|
||||
detail: nameof(IdTextItem<int>.Detail));
|
||||
page.Selected += FlowerCategory_Selected;
|
||||
|
||||
await Navigation.PushAsync(page);
|
||||
}
|
||||
|
||||
private void FlowerCategory_Selected(object? sender, IdTextItem<int> category)
|
||||
{
|
||||
selectedCategoryId = category.Id;
|
||||
Category = category.Text;
|
||||
}
|
||||
|
||||
private async void Save_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
var name = FlowerName;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
await this.Alert(Title, L("flowerNameRequired", "Flower name is required."));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: selectedLocation
|
||||
var location = FlowerLocation;
|
||||
if (string.IsNullOrEmpty(location))
|
||||
{
|
||||
await this.Alert(Title, L("locationRequired", "Location is required."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedCategoryId == null)
|
||||
{
|
||||
await this.Alert(Title, L("flowerCategoryRequired", "Flower category is required."));
|
||||
return;
|
||||
}
|
||||
|
||||
var purchaseDate = new DateTimeOffset((PurchaseDate + PurchaseTime).ToUniversalTime());
|
||||
var purchaseFrom = PurchaseFrom;
|
||||
if (!decimal.TryParse(Cost, out decimal cost) || cost < 0)
|
||||
{
|
||||
await this.Alert(Title, L("costInvalid", "Cost must be a positive number."));
|
||||
return;
|
||||
}
|
||||
|
||||
var memo = Memo;
|
||||
|
||||
var item = new FlowerItem
|
||||
{
|
||||
Name = name,
|
||||
CategoryId = selectedCategoryId.Value,
|
||||
DateBuyUnixTime = purchaseDate.ToUnixTimeMilliseconds(),
|
||||
Purchase = purchaseFrom,
|
||||
Cost = cost,
|
||||
Memo = memo,
|
||||
OwnerId = AppResources.User.Id
|
||||
};
|
||||
var loc = CurrentLocation;
|
||||
if (loc != null)
|
||||
{
|
||||
item.Latitude = loc.Latitude;
|
||||
item.Longitude = loc.Longitude;
|
||||
}
|
||||
|
||||
var cover = Cover;
|
||||
if (cover != null)
|
||||
{
|
||||
item.Photos = new[] { new PhotoItem { Url = cover } };
|
||||
}
|
||||
|
||||
await Loading(true);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await DoAddFlowerAsync(item);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, $"error occurs while adding flower, {item}");
|
||||
await this.AlertError(L("failedAddFlower", "Failed to add flower, {error}, please try again later.").Replace("{error}", ex.Message));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async Task DoAddFlowerAsync(FlowerItem item)
|
||||
{
|
||||
var data = new MultipartFormDataContent
|
||||
{
|
||||
{ new StringContent(item.CategoryId.ToString()), "categoryId" },
|
||||
{ new StringContent(item.Name), "name" },
|
||||
{ new StringContent(item.DateBuyUnixTime.ToString()), "dateBuy" }
|
||||
};
|
||||
if (item.Cost != null)
|
||||
{
|
||||
data.Add(new StringContent($"{item.Cost}"), "cost");
|
||||
}
|
||||
if (item.Purchase != null)
|
||||
{
|
||||
data.Add(new StringContent(item.Purchase), "purchase");
|
||||
}
|
||||
if (item.Memo != null)
|
||||
{
|
||||
data.Add(new StringContent(item.Memo), "memo");
|
||||
}
|
||||
if (item.Latitude != null && item.Longitude != null)
|
||||
{
|
||||
data.Add(new StringContent($"{item.Latitude}"), "lat");
|
||||
data.Add(new StringContent($"{item.Longitude}"), "lon");
|
||||
}
|
||||
if (item.Photos?.Length > 0)
|
||||
{
|
||||
data.Add(new StreamContent(File.OpenRead(item.Photos[0].Url)), "cover");
|
||||
}
|
||||
var result = await UploadAsync<FlowerItem>("api/flower/add", data);
|
||||
|
||||
this.LogInformation($"upload successfully, {result}");
|
||||
}
|
||||
}
|
||||
|
||||
class CoverConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string s && !string.IsNullOrEmpty(s))
|
||||
{
|
||||
return s;
|
||||
}
|
||||
return "empty_flower.jpg";
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
record GeoResult
|
||||
{
|
||||
[JsonPropertyName("address")]
|
||||
public AddressResult? Address { get; init; }
|
||||
|
||||
[JsonPropertyName("error")]
|
||||
public ErrorResult? Error { get; init; }
|
||||
}
|
||||
|
||||
record ErrorResult
|
||||
{
|
||||
[JsonPropertyName("code")]
|
||||
public int Code { get; init; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; init; } = null!;
|
||||
}
|
||||
|
||||
record AddressResult
|
||||
{
|
||||
[JsonPropertyName("City")]
|
||||
public string? City { get; init; }
|
||||
|
||||
[JsonPropertyName("District")]
|
||||
public string? District { get; init; }
|
||||
|
||||
[JsonPropertyName("CntryName")]
|
||||
public string? CountryName { get; init; }
|
||||
|
||||
[JsonPropertyName("CountryCode")]
|
||||
public string? CountryCode { get; init; }
|
||||
|
||||
[JsonPropertyName("Region")]
|
||||
public string? Region { get; init; }
|
||||
|
||||
[JsonPropertyName("RegionAbbr")]
|
||||
public string? RegionAbbr { get; init; }
|
||||
}
|
Reference in New Issue
Block a user