diff --git a/FlowerApp/App.xaml b/FlowerApp/App.xaml index 9959494..715938f 100644 --- a/FlowerApp/App.xaml +++ b/FlowerApp/App.xaml @@ -9,6 +9,8 @@ + + diff --git a/FlowerApp/AppShell.xaml b/FlowerApp/AppShell.xaml index 7e5f03c..9f45c91 100644 --- a/FlowerApp/AppShell.xaml +++ b/FlowerApp/AppShell.xaml @@ -9,7 +9,7 @@ + Route="Garden" Icon="flower_tulip.png"> (BindableProperty property) { @@ -17,7 +24,7 @@ public class AppContentPage : ContentPage, ILoggerContent bool hasLoading = true; ContentView? loading; -#if __IOS__ +#if IOS private async Task DoLoading(bool flag) #else private Task DoLoading(bool flag) @@ -38,7 +45,7 @@ public class AppContentPage : ContentPage, ILoggerContent { if (flag) { -#if __IOS__ +#if IOS loading.IsVisible = true; await loading.FadeTo(1, easing: Easing.CubicOut); #else @@ -48,7 +55,7 @@ public class AppContentPage : ContentPage, ILoggerContent } else { -#if __IOS__ +#if IOS await loading.FadeTo(0, easing: Easing.CubicIn); loading.IsVisible = false; #else @@ -57,7 +64,7 @@ public class AppContentPage : ContentPage, ILoggerContent #endif } } -#if __ANDROID__ +#if ANDROID return Task.CompletedTask; #endif } @@ -79,4 +86,141 @@ public class AppContentPage : ContentPage, ILoggerContent }); return source.Task; } + + async Task GetLastLocationAsyncInternal() + { + try + { + var location = await Geolocation.Default.GetLastKnownLocationAsync(); + return location; + } + catch (FeatureNotSupportedException fnsEx) + { + this.LogError(fnsEx, $"Not supported on device, {fnsEx.Message}."); + } + catch (FeatureNotEnabledException fneEx) + { + this.LogError(fneEx, $"Not enabled on device, {fneEx.Message}."); + } + catch (PermissionException) + { + this.LogWarning($"User denied."); + } + catch (Exception ex) + { + this.LogError(ex, $"Error occurs while getting cached location, {ex.Message}"); + } + return null; + } + + protected Task GetLastLocationAsync() + { + if (MainThread.IsMainThread) + { + return GetLastLocationAsyncInternal(); + } + var source = new TaskCompletionSource(); + MainThread.BeginInvokeOnMainThread(async () => + { + var location = await GetLastLocationAsyncInternal(); + source.TrySetResult(location); + }); + return source.Task; + } + + TaskCompletionSource? locationTaskSource; + CancellationTokenSource? locationCancellationTokenSource; + + async Task GetCurrentLocationAsyncInternal() + { + if (locationTaskSource == null) + { + locationTaskSource = new TaskCompletionSource(); + + try + { + var request = new GeolocationRequest(GeolocationAccuracy.Best, TimeSpan.FromSeconds(10)); +#if IOS + request.RequestFullAccuracy = true; +#endif + + locationCancellationTokenSource = new CancellationTokenSource(); + + var location = await Geolocation.Default.GetLocationAsync(request, locationCancellationTokenSource.Token); + locationTaskSource.SetResult(location); + } + catch (Exception ex) + { + this.LogError(ex, $"Error occurs while getting current location, {ex.Message}"); + } + } + + return await locationTaskSource.Task; + } + + protected Task GetCurrentLocationAsync() + { + if (MainThread.IsMainThread) + { + return GetCurrentLocationAsyncInternal(); + } + var source = new TaskCompletionSource(); + MainThread.BeginInvokeOnMainThread(async () => + { + var location = await GetCurrentLocationAsyncInternal(); + source.TrySetResult(location); + }); + return source.Task; + } + + protected void CancelRequestLocation() + { + if (locationCancellationTokenSource?.IsCancellationRequested == false) + { + locationCancellationTokenSource.Cancel(); + } + } + + async Task TakePhotoInternal() + { + var status = await Permissions.CheckStatusAsync(); + + if (status == PermissionStatus.Denied) + { + await this.AlertError(L("needCameraPermission", "Flower Story needs access to the camera to take photos.")); +#if IOS + var settingsUrl = UIKit.UIApplication.OpenSettingsUrlString; + await Launcher.TryOpenAsync(settingsUrl); +#endif + return null; + } + + if (status != PermissionStatus.Granted) + { + status = await Permissions.RequestAsync(); + } + + if (status != PermissionStatus.Granted) + { + return null; + } + + var file = await MediaPicker.Default.CapturePhotoAsync(); + return file; + } + + protected Task TakePhoto() + { + if (MainThread.IsMainThread) + { + return TakePhotoInternal(); + } + var source = new TaskCompletionSource(); + MainThread.BeginInvokeOnMainThread(async () => + { + var file = await TakePhotoInternal(); + source.TrySetResult(file); + }); + return source.Task; + } } diff --git a/FlowerApp/Controls/AppConverters.cs b/FlowerApp/Controls/AppConverters.cs index fd35382..33e5236 100644 --- a/FlowerApp/Controls/AppConverters.cs +++ b/FlowerApp/Controls/AppConverters.cs @@ -2,7 +2,7 @@ namespace Blahblah.FlowerApp; -internal class VisibleIfNotNullConverter : IValueConverter +class VisibleIfNotNullConverter : IValueConverter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { @@ -18,3 +18,24 @@ internal class VisibleIfNotNullConverter : IValueConverter throw new NotImplementedException(); } } + + +class DateTimeStringConverter : IValueConverter +{ + public string Format { get; init; } = "MM/dd HH:mm:ss"; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is long time) + { + var date = DateTimeOffset.FromUnixTimeMilliseconds(time); + return date.ToLocalTime().ToString(Format); + } + return value; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/FlowerApp/Controls/FlowerClientItem.cs b/FlowerApp/Controls/FlowerClientItem.cs index 10efb16..5c12f57 100644 --- a/FlowerApp/Controls/FlowerClientItem.cs +++ b/FlowerApp/Controls/FlowerClientItem.cs @@ -7,6 +7,7 @@ public class FlowerClientItem : BindableObject { static readonly BindableProperty NameProperty = CreateProperty(nameof(Name)); static readonly BindableProperty CategoryIdProperty = CreateProperty(nameof(CategoryId)); + static readonly BindableProperty DaysProperty = CreateProperty(nameof(Days)); static readonly BindableProperty CoverProperty = CreateProperty(nameof(Cover)); static readonly BindableProperty BoundsProperty = CreateProperty(nameof(Bounds)); @@ -23,6 +24,11 @@ public class FlowerClientItem : BindableObject get => (int)GetValue(CategoryIdProperty); set => SetValue(CategoryIdProperty, value); } + public string Days + { + get => (string)GetValue(DaysProperty); + set => SetValue(DaysProperty, value); + } public ImageSource? Cover { get => (ImageSource?)GetValue(CoverProperty); diff --git a/FlowerApp/Controls/ItemSelectorPage.cs b/FlowerApp/Controls/ItemSelectorPage.cs new file mode 100644 index 0000000..cd86d1d --- /dev/null +++ b/FlowerApp/Controls/ItemSelectorPage.cs @@ -0,0 +1,124 @@ +using static Blahblah.FlowerApp.Extensions; + +namespace Blahblah.FlowerApp.Controls; + +class ItemSelectorPage : ContentPage where T : IdTextItem +{ + public EventHandler? Selected; + + public ItemSelectorPage(string title, T[] source, bool multiple = false, K[]? selected = null, string display = nameof(IdTextItem.Text), string? detail = null) + { + Title = title; + + var itemsSource = source.Select(t => new SelectableItem + { + Item = t, + IsSelected = selected != null && selected.Contains(t.Id) + }).ToArray(); + + var list = new ListView + { + SelectionMode = ListViewSelectionMode.None, + ItemsSource = itemsSource, + ItemTemplate = new DataTemplate(() => + { + var content = new Grid + { + Margin = new Thickness(12, 0), + ColumnSpacing = 12, + ColumnDefinitions = + { + new(30), + new(GridLength.Star), + new(GridLength.Auto) + }, + Children = + { + new SecondaryLabel + { + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center, + Text = Res.Check, + FontFamily = "FontAwesome" + } + .Binding(IsVisibleProperty, nameof(SelectableItem.IsSelected)), + + new Label + { + VerticalOptions = LayoutOptions.Center + } + .Binding(Label.TextProperty, $"{nameof(SelectableItem.Item)}.{display}") + .GridColumn(1) + } + }; + if (detail != null) + { + content.Children.Add( + new SecondaryLabel + { + VerticalOptions = LayoutOptions.Center + } + .Binding(Label.TextProperty, $"{nameof(SelectableItem.Item)}.{detail}") + .GridColumn(2)); + } + return new ViewCell + { + View = content + }; + }) + }; + + list.ItemTapped += List_ItemTapped; + + Content = list; + } + + private async void List_ItemTapped(object? sender, ItemTappedEventArgs e) + { + if (e.Item is SelectableItem item) + { + Selected?.Invoke(this, item.Item); + await Navigation.PopAsync(); + } + } +} + +class SelectableItem : BindableObject +{ + public static BindableProperty IsSelectedProperty = CreateProperty>(nameof(IsSelected)); + public static BindableProperty ItemProperty = CreateProperty>(nameof(Item)); + + public bool IsSelected + { + get => (bool)GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); + } + public T Item + { + get => (T)GetValue(ItemProperty); + set => SetValue(ItemProperty, value); + } +} + +class IdTextItem : BindableObject +{ + public static BindableProperty IdProperty = CreateProperty>(nameof(Id)); + public static BindableProperty TextProperty = CreateProperty>(nameof(Text)); + public static BindableProperty DetailProperty = CreateProperty>(nameof(Detail)); + + public T Id + { + get => (T)GetValue(IdProperty); + set => SetValue(IdProperty, value); + } + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + public string? Detail + { + get => (string?)GetValue(DetailProperty); + set => SetValue(DetailProperty, value); + } +} \ No newline at end of file diff --git a/FlowerApp/Controls/OptionCell.cs b/FlowerApp/Controls/OptionCell.cs new file mode 100644 index 0000000..26791f2 --- /dev/null +++ b/FlowerApp/Controls/OptionCell.cs @@ -0,0 +1,351 @@ +using System.ComponentModel; +using static Blahblah.FlowerApp.Extensions; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace Blahblah.FlowerApp.Controls; + +public class TitleLabel : Label { } + +public class SecondaryLabel : Label { } + +public class IconLabel : Label { } + +public class OptionEntry : Entry { } + +public class OptionEditor : Editor { } + +public class OptionDatePicker : DatePicker { } + +public class OptionTimePicker : TimePicker { } + +public abstract class OptionCell : ViewCell +{ + public static readonly BindableProperty IconProperty = CreateProperty(nameof(Icon)); + public static readonly BindableProperty TitleProperty = CreateProperty(nameof(Title)); + public static readonly BindableProperty IsRequiredProperty = CreateProperty(nameof(IsRequired)); + + [TypeConverter(typeof(ImageSourceConverter))] + public ImageSource Icon + { + get => (ImageSource)GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + public string Title + { + get => (string)GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + public bool IsRequired + { + get => (bool)GetValue(IsRequiredProperty); + set => SetValue(IsRequiredProperty, value); + } + + protected abstract View Content { get; } + + public OptionCell() + { + View = new Grid + { + BindingContext = this, + Padding = new Thickness(20, 0), + ColumnSpacing = 12, + ColumnDefinitions = + { + new(GridLength.Auto), + new(new GridLength(.35, GridUnitType.Star)), + new(new GridLength(.65, GridUnitType.Star)) + }, + RowDefinitions = { new(44) }, + Children = + { + new Image + { + WidthRequest = 20, + HeightRequest = 20, + Aspect = Aspect.AspectFit, + VerticalOptions = LayoutOptions.Center + } + .Binding(VisualElement.IsVisibleProperty, nameof(Icon), converter: new VisibleIfNotNullConverter()) + .Binding(Image.SourceProperty, nameof(Icon)), + + new Grid + { + ColumnDefinitions = + { + new(GridLength.Auto), + new(GridLength.Star) + }, + Children = + { + new TitleLabel + { + LineBreakMode = LineBreakMode.TailTruncation, + VerticalOptions = LayoutOptions.Center + } + .Binding(Label.TextProperty, nameof(Title)), + + new SecondaryLabel + { + VerticalOptions = LayoutOptions.Center, + Text = "*" + } + .GridColumn(1) + .AppThemeBinding(Label.TextColorProperty, Res.Red100, Res.Red300) + .Binding(VisualElement.IsVisibleProperty, nameof(IsRequired)), + } + } + .GridColumn(1), + + Content.GridColumn(2) + } + } + .AppThemeBinding(VisualElement.BackgroundColorProperty, Colors.White, Res.Gray900); + } +} + +public abstract class OptionVerticalCell : OptionCell +{ + public OptionVerticalCell() + { + View = new Grid + { + BindingContext = this, + Padding = new Thickness(20, 0), + ColumnSpacing = 12, + ColumnDefinitions = + { + new(GridLength.Auto), + new(GridLength.Star) + }, + RowDefinitions = + { + new(44), + new(GridLength.Star) + }, + Children = + { + new Image + { + WidthRequest = 20, + HeightRequest = 20, + Aspect = Aspect.AspectFit, + VerticalOptions = LayoutOptions.Center + } + .Binding(VisualElement.IsVisibleProperty, nameof(Icon), converter: new VisibleIfNotNullConverter()) + .Binding(Image.SourceProperty, nameof(Icon)), + + new TitleLabel + { + LineBreakMode = LineBreakMode.TailTruncation, + VerticalOptions = LayoutOptions.Center + } + .GridColumn(1) + .Binding(Label.TextProperty, nameof(Title)), + + Content.GridRow(1).GridColumn(1) + } + } + .AppThemeBinding(VisualElement.BackgroundColorProperty, Colors.White, Res.Gray900); + } +} + +public class OptionTextCell : OptionCell +{ + public static readonly BindableProperty DetailProperty = CreateProperty(nameof(Detail)); + + public string Detail + { + get => (string)GetValue(DetailProperty); + set => SetValue(DetailProperty, value); + } + + protected override View Content => new SecondaryLabel + { + HorizontalOptions = LayoutOptions.End, + VerticalOptions = LayoutOptions.Center + } + .Binding(Label.TextProperty, nameof(Detail)); +} + +public class OptionEntryCell : OptionCell +{ + public static readonly BindableProperty TextProperty = CreateProperty(nameof(Text), defaultBindingMode: BindingMode.TwoWay); + public static readonly BindableProperty KeyboardProperty = CreateProperty(nameof(Keyboard), defaultValue: Keyboard.Default); + public static readonly BindableProperty PlaceholderProperty = CreateProperty(nameof(Placeholder)); + + public event EventHandler? Unfocused; + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + public Keyboard Keyboard + { + get => (Keyboard)GetValue(KeyboardProperty); + set => SetValue(KeyboardProperty, value); + } + public string Placeholder + { + get => (string)GetValue(PlaceholderProperty); + set => SetValue(PlaceholderProperty, value); + } + + protected override View Content + { + get + { + var entry = new OptionEntry() + .Binding(Entry.TextProperty, nameof(Text)) + .Binding(InputView.KeyboardProperty, nameof(Keyboard)) + .Binding(Entry.PlaceholderProperty, nameof(Placeholder)); + entry.Unfocused += Entry_Unfocused; + return entry; + } + } + + private void Entry_Unfocused(object? sender, FocusEventArgs e) + { + Unfocused?.Invoke(this, e); + } +} + +public class OptionEditorCell : OptionVerticalCell +{ + public static readonly BindableProperty TextProperty = CreateProperty(nameof(Text), defaultBindingMode: BindingMode.TwoWay); + public static readonly BindableProperty KeyboardProperty = CreateProperty(nameof(Keyboard), defaultValue: Keyboard.Default); + public static readonly BindableProperty PlaceholderProperty = CreateProperty(nameof(Placeholder)); + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + public Keyboard Keyboard + { + get => (Keyboard)GetValue(KeyboardProperty); + set => SetValue(KeyboardProperty, value); + } + public string Placeholder + { + get => (string)GetValue(PlaceholderProperty); + set => SetValue(PlaceholderProperty, value); + } + + protected override View Content => new OptionEditor() + .Binding(Editor.TextProperty, nameof(Text)) + .Binding(InputView.KeyboardProperty, nameof(Keyboard)) + .Binding(Editor.PlaceholderProperty, nameof(Placeholder)); +} + +public class OptionSwitchCell : OptionCell +{ + public static readonly BindableProperty IsToggledProperty = CreateProperty(nameof(IsToggled), defaultBindingMode: BindingMode.TwoWay); + + public bool IsToggled + { + get => (bool)GetValue(IsToggledProperty); + set => SetValue(IsToggledProperty, value); + } + + protected override View Content => new Switch + { + HorizontalOptions = LayoutOptions.End, + VerticalOptions = LayoutOptions.Center + } + .Binding(Switch.IsToggledProperty, nameof(IsToggled)); +} + +public class OptionSelectCell : OptionTextCell +{ + public static readonly BindableProperty CommandProperty = CreateProperty(nameof(Command)); + public static readonly BindableProperty CommandParameterProperty = CreateProperty(nameof(CommandParameter)); + + public Command Command + { + get => (Command)GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + public object CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + + public event EventHandler? DetailTapped; + + protected override View Content + { + get + { + var tap = new TapGestureRecognizer(); + tap.Tapped += OnTapped; + + return new StackLayout + { + Orientation = StackOrientation.Horizontal, + HorizontalOptions = LayoutOptions.End, + Children = + { + new SecondaryLabel + { + VerticalOptions = LayoutOptions.Center, + } + .Binding(Label.TextProperty, nameof(Detail)), + + new IconLabel + { + VerticalOptions = LayoutOptions.Center, + Margin = new Thickness(6, 0), + Text = Res.Right + } + }, + GestureRecognizers = { tap } + }; + } + } + + private void OnTapped(object? sender, TappedEventArgs e) + { + DetailTapped?.Invoke(this, e); + } +} + +public class OptionDateTimePickerCell : OptionCell +{ + public static readonly BindableProperty DateProperty = CreateProperty(nameof(Date), defaultBindingMode: BindingMode.TwoWay); + public static readonly BindableProperty TimeProperty = CreateProperty(nameof(Time), defaultBindingMode: BindingMode.TwoWay); + + public DateTime Date + { + get => (DateTime)GetValue(DateProperty); + set => SetValue(DateProperty, value); + } + public TimeSpan Time + { + get => (TimeSpan)GetValue(TimeProperty); + set => SetValue(TimeProperty, value); + } + + protected override View Content => new Grid + { + ColumnDefinitions = + { + new(GridLength.Star), + new(GridLength.Auto), + new(GridLength.Auto) + }, + ColumnSpacing = 6, + Children = + { + new OptionDatePicker() + .Binding(DatePicker.DateProperty, nameof(Date)) + .GridColumn(1), + + new OptionTimePicker() + .Binding(TimePicker.TimeProperty, nameof(Time)) + .GridColumn(2) + } + }; +} diff --git a/FlowerApp/Data/Constants.cs b/FlowerApp/Data/Constants.cs index 8f915a7..2da0582 100644 --- a/FlowerApp/Data/Constants.cs +++ b/FlowerApp/Data/Constants.cs @@ -11,7 +11,7 @@ internal sealed class Constants public const string LastTokenName = "last_token"; public const string BaseUrl = "https://app.blahblaho.com"; - public const string AppVersion = "0.2.801"; + public const string AppVersion = "0.3.802"; public const string UserAgent = $"FlowerApp/{AppVersion}"; public const SQLiteOpenFlags SQLiteFlags = @@ -64,14 +64,14 @@ internal record Definitions public required Dictionary Events { get; init; } } -internal record NamedItem(string Name, string? Description) +public record NamedItem(string Name, string? Description) { public string Name { get; init; } = Name; public string? Description { get; init; } = Description; } -internal record EventItem(string Name, string? Description, bool Unique) : NamedItem(Name, Description) +public record EventItem(string Name, string? Description, bool Unique) : NamedItem(Name, Description) { public bool Unique { get; init; } = Unique; } \ No newline at end of file diff --git a/FlowerApp/Data/FlowerDatabase.cs b/FlowerApp/Data/FlowerDatabase.cs index 3509856..1719202 100644 --- a/FlowerApp/Data/FlowerDatabase.cs +++ b/FlowerApp/Data/FlowerDatabase.cs @@ -21,6 +21,8 @@ public class FlowerDatabase : ILoggerContent private Dictionary? events; + public Dictionary? Categories => categories; + public string Category(int categoryId) { if (categories?.TryGetValue(categoryId, out var category) == true) @@ -168,6 +170,18 @@ public class FlowerDatabase : ILoggerContent } } + public async Task GetLogCount() + { + await Init(); + return await database.Table().Where(l => l.OwnerId < 0 || l.OwnerId == AppResources.User.Id).CountAsync(); + } + + public async Task GetLogs() + { + await Init(); + return await database.Table().Where(l => l.OwnerId < 0 || l.OwnerId == AppResources.User.Id).OrderByDescending(l => l.LogUnixTime).ToArrayAsync(); + } + public async Task AddLog(LogItem log) { await Init(); diff --git a/FlowerApp/Data/Model/FlowerItem.cs b/FlowerApp/Data/Model/FlowerItem.cs index 4bc7b0c..dd39b38 100644 --- a/FlowerApp/Data/Model/FlowerItem.cs +++ b/FlowerApp/Data/Model/FlowerItem.cs @@ -41,4 +41,10 @@ public class FlowerItem [Ignore] public int? Distance { get; set; } + + public override string ToString() + { + // TODO: + return $"id: {Id}, owner: {OwnerId}, category: {CategoryId}, name: {Name}, date: {DateBuyUnixTime}, ..."; + } } diff --git a/FlowerApp/Extensions.cs b/FlowerApp/Extensions.cs index 84f8368..beb192d 100644 --- a/FlowerApp/Extensions.cs +++ b/FlowerApp/Extensions.cs @@ -11,9 +11,9 @@ internal sealed class Extensions return LocalizationResource.GetText(key, defaultValue); } - public static BindableProperty CreateProperty(string propertyName, T? defaultValue = default) + public static BindableProperty CreateProperty(string propertyName, T? defaultValue = default, BindingMode defaultBindingMode = BindingMode.OneWay, BindableProperty.BindingPropertyChangedDelegate? propertyChanged = null) { - return BindableProperty.Create(propertyName, typeof(T), typeof(V), defaultValue); + return BindableProperty.Create(propertyName, typeof(T), typeof(V), defaultValue, defaultBindingMode, propertyChanged: propertyChanged); } public static async Task FetchAsync(string url, CancellationToken cancellation = default) @@ -40,21 +40,51 @@ internal sealed class Extensions { response.EnsureSuccessStatusCode(); var content = response.Content; - if (content.Headers.TryGetValues("Authorization", out var values) && - values.FirstOrDefault() is string oAuth) - { - Constants.SetAuthorization(oAuth); - var result = await content.ReadFromJsonAsync(cancellationToken: cancellation); - return result; - } + var result = await content.ReadFromJsonAsync(cancellationToken: cancellation); + return result; } return default; } + + public static async Task UploadAsync(string url, MultipartFormDataContent data, CancellationToken cancellation = default) + { + using var client = new HttpClient(); + var authorization = Constants.Authorization; + if (authorization != null) + { + client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", authorization); + } + using var response = await client.PostAsJsonAsync($"{Constants.BaseUrl}/{url}", data, cancellation); + if (response != null) + { + response.EnsureSuccessStatusCode(); + var content = response.Content; + var result = await content.ReadFromJsonAsync(cancellationToken: cancellation); + return result; + } + return default; + } + + public static async Task CacheFileAsync(FileResult file) + { + string cache = Path.Combine(FileSystem.CacheDirectory, file.FileName); + + using Stream source = await file.OpenReadAsync(); + using FileStream fs = File.OpenWrite(cache); + await source.CopyToAsync(fs); + + return cache; + } } internal static class LoggerExtension { - const LogLevel MinimumLogLevel = LogLevel.Information; + const LogLevel MinimumLogLevel = +#if DEBUG + LogLevel.Information; +#else + LogLevel.Warning; +#endif public static void LogInformation(this ILoggerContent content, string message) { @@ -77,7 +107,7 @@ internal static class LoggerExtension { logger.Log(level, exception, "[{time:MM/dd HH:mm:ss}] - {message}", DateTime.UtcNow, message); - if (content.Database is FlowerDatabase database) + if (level >= MinimumLogLevel && content.Database is FlowerDatabase database) { _ = database.AddLog(new Data.Model.LogItem { @@ -97,6 +127,42 @@ internal static class LoggerExtension internal static class PageExtension { + public static T Binding(this T obj, BindableProperty property, string path, BindingMode mode = BindingMode.Default, IValueConverter? converter = null) where T : BindableObject + { + obj.SetBinding(property, path, mode, converter); + return obj; + } + + public static T AppThemeBinding(this T obj, BindableProperty property, Color light, Color dark) where T : BindableObject + { + obj.SetAppThemeColor(property, light, dark); + return obj; + } + + public static T GridColumn(this T view, int column) where T : BindableObject + { + Grid.SetColumn(view, column); + return view; + } + + public static T GridRow(this T view, int row) where T : BindableObject + { + Grid.SetRow(view, row); + return view; + } + + public static T GridColumnSpan(this T view, int columnSpan) where T : BindableObject + { + Grid.SetColumnSpan(view, columnSpan); + return view; + } + + public static T GridRowSpan(this T view, int rowSpan) where T : BindableObject + { + Grid.SetRowSpan(view, rowSpan); + return view; + } + public static Task AlertError(this ContentPage page, string error) { return Alert(page, LocalizationResource.GetText("error", "Error"), error); @@ -136,4 +202,22 @@ internal static class PageExtension }); return taskSource.Task; } +} + +internal static class Res +{ + public const string Filter = "\ue17c"; + public const string Camera = "\uf030"; + public const string Image = "\uf03e"; + public const string Heart = "\uf004"; + public const string Right = "\uf105"; + public const string XMarkLarge = "\ue59b"; + public const string Gear = "\uf013"; + public const string List = "\uf0ae"; + public const string Flag = "\uf2b4"; + public const string Check = "\uf00c"; + + public static readonly Color Gray900 = Color.FromRgb(0x21, 0x21, 0x21); + public static readonly Color Red100 = Color.FromRgb(0xF4, 0x43, 0x36); + public static readonly Color Red300 = Color.FromRgb(0xFF, 0xCD, 0xD2); } \ No newline at end of file diff --git a/FlowerApp/FlowerApp.csproj b/FlowerApp/FlowerApp.csproj index 846307d..3e99067 100644 --- a/FlowerApp/FlowerApp.csproj +++ b/FlowerApp/FlowerApp.csproj @@ -1,7 +1,7 @@  - net8.0-android;net8.0-ios + net8.0-ios Exe Blahblah.FlowerApp @@ -18,8 +18,8 @@ 2a32c3a1-d02e-450d-b524-5dbea90f13ed - 0.2.801 - 3 + 0.3.802 + 5 15.0 23.0 @@ -53,14 +53,16 @@ - + - + - + + + @@ -85,6 +87,9 @@ True Localizations.resx + + HomePage.xaml + @@ -99,11 +104,15 @@ - + + + + + MSBuild:Compile @@ -114,5 +123,14 @@ MSBuild:Compile + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + diff --git a/FlowerApp/HomePage.xaml b/FlowerApp/HomePage.xaml new file mode 100644 index 0000000..ba4e9ea --- /dev/null +++ b/FlowerApp/HomePage.xaml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + +