From 8419c9d389c92fc0c817225fadc0d7bbfc63ef3a Mon Sep 17 00:00:00 2001 From: Tsanie Lily Date: Mon, 31 Jul 2023 17:11:39 +0800 Subject: [PATCH] version up --- FlowerApp/AppShell.xaml | 18 +- FlowerApp/Controls/AppContentPage.cs | 97 ++++++----- FlowerApp/Controls/AppConverters.cs | 20 +++ FlowerApp/Controls/AppResources.cs | 23 ++- FlowerApp/Controls/FlowerClientItem.cs | 12 +- FlowerApp/Controls/ILoggerContent.cs | 11 ++ FlowerApp/Controls/ItemSearchHandler.cs | 34 ++++ FlowerApp/Controls/PropertyExtension.cs | 9 - FlowerApp/Data/Constants.cs | 9 +- FlowerApp/Data/FlowerDatabase.cs | 154 ++++++++++++------ FlowerApp/Data/Model/FlowerItem.cs | 7 +- FlowerApp/Data/Model/LogItem.cs | 34 ++++ FlowerApp/Data/Model/ParamItem.cs | 11 +- FlowerApp/Data/Model/PhotoItem.cs | 7 +- FlowerApp/Data/Model/RecordItem.cs | 9 +- FlowerApp/Data/Model/UserItem.cs | 14 +- FlowerApp/Extensions.cs | 121 ++++++++++++++ FlowerApp/FlowerApp.csproj | 24 ++- FlowerApp/Localizations.Designer.cs | 107 +++++++++++- FlowerApp/Localizations.resx | 39 ++++- ...tions.zh-CN.resx => Localizations.zh.resx} | 39 ++++- FlowerApp/LoginPage.xaml | 33 ++++ FlowerApp/LoginPage.xaml.cs | 107 ++++++++++++ FlowerApp/MainPage.xaml | 39 +++-- FlowerApp/MainPage.xaml.cs | 104 ++++++++++-- FlowerApp/MauiProgram.cs | 1 + FlowerApp/Platforms/Android/PageExtensions.cs | 23 +++ FlowerApp/Platforms/iOS/PageExtensions.cs | 39 +++++ FlowerApp/Resources/AppIcon/appiconfg.svg | 9 +- FlowerApp/Resources/Images/cube.svg | 1 + FlowerApp/Resources/Images/flower_tulip.svg | 1 + FlowerApp/Resources/Images/user.svg | 1 + FlowerApp/SquarePage.xaml | 18 ++ FlowerApp/SquarePage.xaml.cs | 9 + FlowerApp/UserPage.xaml | 18 ++ FlowerApp/UserPage.xaml.cs | 15 ++ FlowerStory.sln | 6 + Server/Controller/BaseController.cs | 5 - Server/Controller/FlowerApiController.cs | 101 +----------- Server/Controller/UserApiController.cs | 8 +- Server/Program.cs | 2 +- 41 files changed, 1053 insertions(+), 286 deletions(-) create mode 100644 FlowerApp/Controls/AppConverters.cs create mode 100644 FlowerApp/Controls/ILoggerContent.cs create mode 100644 FlowerApp/Controls/ItemSearchHandler.cs delete mode 100644 FlowerApp/Controls/PropertyExtension.cs create mode 100644 FlowerApp/Data/Model/LogItem.cs rename FlowerApp/{Localizations.zh-CN.resx => Localizations.zh.resx} (84%) create mode 100644 FlowerApp/LoginPage.xaml create mode 100644 FlowerApp/LoginPage.xaml.cs create mode 100644 FlowerApp/Platforms/Android/PageExtensions.cs create mode 100644 FlowerApp/Platforms/iOS/PageExtensions.cs create mode 100644 FlowerApp/Resources/Images/cube.svg create mode 100644 FlowerApp/Resources/Images/flower_tulip.svg create mode 100644 FlowerApp/Resources/Images/user.svg create mode 100644 FlowerApp/SquarePage.xaml create mode 100644 FlowerApp/SquarePage.xaml.cs create mode 100644 FlowerApp/UserPage.xaml create mode 100644 FlowerApp/UserPage.xaml.cs diff --git a/FlowerApp/AppShell.xaml b/FlowerApp/AppShell.xaml index aaa8eff..7e5f03c 100644 --- a/FlowerApp/AppShell.xaml +++ b/FlowerApp/AppShell.xaml @@ -7,9 +7,19 @@ Shell.FlyoutBehavior="Disabled" Title="Flower Story"> - + + + + + + + + + + + diff --git a/FlowerApp/Controls/AppContentPage.cs b/FlowerApp/Controls/AppContentPage.cs index 1ca619e..c1eead2 100644 --- a/FlowerApp/Controls/AppContentPage.cs +++ b/FlowerApp/Controls/AppContentPage.cs @@ -1,59 +1,82 @@ -namespace Blahblah.FlowerApp; +using Blahblah.FlowerApp.Data; +using Microsoft.Extensions.Logging; -public class AppContentPage : ContentPage +namespace Blahblah.FlowerApp; + +public class AppContentPage : ContentPage, ILoggerContent { - protected static string L(string key, string defaultValue = "") - { - return LocalizationResource.GetText(key, defaultValue); - } + public ILogger Logger { get; init; } = null!; - protected static Task FetchAsync(string url, CancellationToken cancellation = default) - { - return Extensions.FetchAsync(url, cancellation); - } + public FlowerDatabase Database { get; init; } = null!; protected T GetValue(BindableProperty property) { return (T)GetValue(property); } - protected Task AlertError(string error) + bool hasLoading = true; + ContentView? loading; + +#if __IOS__ + private async Task DoLoading(bool flag) +#else + private Task DoLoading(bool flag) +#endif { - return Alert(LocalizationResource.GetText("error", "Error"), error); + if (loading == null && hasLoading) + { + try + { + loading = (ContentView)FindByName("loading"); + } + catch + { + hasLoading = false; + } + } + if (loading != null) + { + if (flag) + { +#if __IOS__ + loading.IsVisible = true; + await loading.FadeTo(1, easing: Easing.CubicOut); +#else + loading.Opacity = 1; + loading.IsVisible = true; +#endif + } + else + { +#if __IOS__ + await loading.FadeTo(0, easing: Easing.CubicIn); + loading.IsVisible = false; +#else + loading.IsVisible = false; + loading.Opacity = 0; +#endif + } + } +#if __ANDROID__ + return Task.CompletedTask; +#endif } - protected Task Alert(string title, string message, string? cancel = null) + protected Task Loading(bool flag) { - cancel ??= LocalizationResource.GetText("ok", "Ok"); + IsBusy = flag; if (MainThread.IsMainThread) { - return DisplayAlert(title, message, cancel); + return DoLoading(flag); } - var taskSource = new TaskCompletionSource(); + + var source = new TaskCompletionSource(); MainThread.BeginInvokeOnMainThread(async () => { - await DisplayAlert(title, message, cancel); - taskSource.TrySetResult(); + await DoLoading(flag); + source.TrySetResult(); }); - return taskSource.Task; - } - - protected Task Confirm(string title, string question, string? accept = null, string? cancel = null) - { - accept ??= LocalizationResource.GetText("yes", "Yes"); - cancel ??= LocalizationResource.GetText("no", "No"); - - if (MainThread.IsMainThread) - { - return DisplayAlert(title, question, accept, cancel); - } - var taskSource = new TaskCompletionSource(); - MainThread.BeginInvokeOnMainThread(async () => - { - var result = await DisplayAlert(title, question, accept, cancel); - taskSource.TrySetResult(result); - }); - return taskSource.Task; + return source.Task; } } diff --git a/FlowerApp/Controls/AppConverters.cs b/FlowerApp/Controls/AppConverters.cs new file mode 100644 index 0000000..fd35382 --- /dev/null +++ b/FlowerApp/Controls/AppConverters.cs @@ -0,0 +1,20 @@ +using System.Globalization; + +namespace Blahblah.FlowerApp; + +internal class VisibleIfNotNullConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is string s) + { + return !string.IsNullOrEmpty(s); + } + return value != null; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/FlowerApp/Controls/AppResources.cs b/FlowerApp/Controls/AppResources.cs index d3458cb..794b992 100644 --- a/FlowerApp/Controls/AppResources.cs +++ b/FlowerApp/Controls/AppResources.cs @@ -1,8 +1,29 @@ -namespace Blahblah.FlowerApp; +using Blahblah.FlowerApp.Data.Model; +using static Blahblah.FlowerApp.Extensions; + +namespace Blahblah.FlowerApp; internal class AppResources { public const string EmptyCover = "empty_flower.jpg"; + public const int EmptyUserId = -1; + public static readonly Size EmptySize = new(512, 339); + + static readonly UserItem emptyUser = new() + { + Id = EmptyUserId, + Name = L("guest", "Guest") + }; + static UserItem? user; + + public static UserItem User => user ?? emptyUser; + + public static bool IsLogined => user != null; + + public static void SetUser(UserItem user) + { + AppResources.user = user; + } } diff --git a/FlowerApp/Controls/FlowerClientItem.cs b/FlowerApp/Controls/FlowerClientItem.cs index 211b7e4..10efb16 100644 --- a/FlowerApp/Controls/FlowerClientItem.cs +++ b/FlowerApp/Controls/FlowerClientItem.cs @@ -1,12 +1,12 @@ using Blahblah.FlowerApp.Data.Model; -using static Blahblah.FlowerApp.PropertyExtension; +using static Blahblah.FlowerApp.Extensions; namespace Blahblah.FlowerApp.Controls; public class FlowerClientItem : BindableObject { static readonly BindableProperty NameProperty = CreateProperty(nameof(Name)); - static readonly BindableProperty CategoryProperty = CreateProperty(nameof(Category)); + static readonly BindableProperty CategoryIdProperty = CreateProperty(nameof(CategoryId)); static readonly BindableProperty CoverProperty = CreateProperty(nameof(Cover)); static readonly BindableProperty BoundsProperty = CreateProperty(nameof(Bounds)); @@ -18,10 +18,10 @@ public class FlowerClientItem : BindableObject get => (string)GetValue(NameProperty); set => SetValue(NameProperty, value); } - public int Category + public int CategoryId { - get => (int)GetValue(CategoryProperty); - set => SetValue(CategoryProperty, value); + get => (int)GetValue(CategoryIdProperty); + set => SetValue(CategoryIdProperty, value); } public ImageSource? Cover { @@ -46,7 +46,7 @@ public class FlowerClientItem : BindableObject { FlowerItem = item; Name = item.Name; - Category = item.Category; + CategoryId = item.CategoryId; if (item.Photos?.Length > 0 && item.Photos[0] is PhotoItem cover) { diff --git a/FlowerApp/Controls/ILoggerContent.cs b/FlowerApp/Controls/ILoggerContent.cs new file mode 100644 index 0000000..d819967 --- /dev/null +++ b/FlowerApp/Controls/ILoggerContent.cs @@ -0,0 +1,11 @@ +using Blahblah.FlowerApp.Data; +using Microsoft.Extensions.Logging; + +namespace Blahblah.FlowerApp; + +public interface ILoggerContent +{ + public ILogger Logger { get; } + + public FlowerDatabase Database { get; } +} diff --git a/FlowerApp/Controls/ItemSearchHandler.cs b/FlowerApp/Controls/ItemSearchHandler.cs new file mode 100644 index 0000000..75780a0 --- /dev/null +++ b/FlowerApp/Controls/ItemSearchHandler.cs @@ -0,0 +1,34 @@ +using Blahblah.FlowerApp.Controls; +using static Blahblah.FlowerApp.Extensions; + +namespace Blahblah.FlowerApp; + +public class ItemSearchHandler : SearchHandler +{ + public static readonly BindableProperty FlowersProperty = CreateProperty(nameof(Flowers)); + + public FlowerClientItem[] Flowers + { + get => (FlowerClientItem[])GetValue(FlowersProperty); + set => SetValue(FlowersProperty, value); + } + + protected override void OnQueryChanged(string oldValue, string newValue) + { + base.OnQueryChanged(oldValue, newValue); + + if (string.IsNullOrWhiteSpace(newValue)) + { + ItemsSource = null; + } + else + { + ItemsSource = Flowers?.Where(f => f.Name.Contains(newValue, StringComparison.OrdinalIgnoreCase)).ToList(); + } + } + + protected override void OnItemSelected(object item) + { + base.OnItemSelected(item); + } +} diff --git a/FlowerApp/Controls/PropertyExtension.cs b/FlowerApp/Controls/PropertyExtension.cs deleted file mode 100644 index e5b52c2..0000000 --- a/FlowerApp/Controls/PropertyExtension.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Blahblah.FlowerApp; - -internal sealed class PropertyExtension -{ - public static BindableProperty CreateProperty(string propertyName, T? defaultValue = default) - { - return BindableProperty.Create(propertyName, typeof(T), typeof(V), defaultValue); - } -} diff --git a/FlowerApp/Data/Constants.cs b/FlowerApp/Data/Constants.cs index 277f166..42e8a2e 100644 --- a/FlowerApp/Data/Constants.cs +++ b/FlowerApp/Data/Constants.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Logging; -using SQLite; +using SQLite; namespace Blahblah.FlowerApp.Data; @@ -12,6 +11,8 @@ internal sealed class Constants public const string LastTokenName = "last_token"; public const string BaseUrl = "https://app.blahblaho.com"; + public const string AppVersion = "0.2.731"; + public const string UserAgent = $"FlowerApp/{AppVersion}"; public const SQLiteOpenFlags SQLiteFlags = SQLiteOpenFlags.ReadWrite | @@ -32,7 +33,7 @@ internal sealed class Constants authorization = auth; } - public static async Task Initialize(ILogger logger, string? version, CancellationToken cancellation = default) + public static async Task Initialize(ILoggerContent logger, string? version, CancellationToken cancellation = default) { try { @@ -47,7 +48,7 @@ internal sealed class Constants } catch (Exception ex) { - logger.LogError("error occurs on fetching version and definitions, {error}", ex); + logger.LogError(ex, $"error occurs on fetching version and definitions, {ex.Message}"); } return null; diff --git a/FlowerApp/Data/FlowerDatabase.cs b/FlowerApp/Data/FlowerDatabase.cs index 0a4ffec..3509856 100644 --- a/FlowerApp/Data/FlowerDatabase.cs +++ b/FlowerApp/Data/FlowerDatabase.cs @@ -4,27 +4,17 @@ using SQLite; namespace Blahblah.FlowerApp.Data; -public class FlowerDatabase +public class FlowerDatabase : ILoggerContent { - private SQLiteAsyncConnection database = null!; + public ILogger Logger { get; } - private readonly ILogger logger; + public FlowerDatabase Database => this; + + private SQLiteAsyncConnection database = null!; public FlowerDatabase(ILogger logger) { - this.logger = logger; - - Task.Run(async () => - { - try - { - await Setup(); - } - catch (Exception ex) - { - logger.LogError("error occurs on setup, {error}", ex); - } - }); + Logger = logger; } private Dictionary? categories; @@ -49,29 +39,68 @@ public class FlowerDatabase return Constants.EventUnknown; } - private async Task Setup() + private async Task Init() + { + if (database is not null) + { + return; + } + +#if DEBUG + Logger.LogInformation("database path: {path}", Constants.DatabasePath); +#endif + + database = new SQLiteAsyncConnection(Constants.DatabasePath, Constants.SQLiteFlags); + +#if DEBUG1 + var result = +#endif + await database.CreateTablesAsync(CreateFlags.None, + typeof(FlowerItem), + typeof(RecordItem), + typeof(PhotoItem), + typeof(UserItem), + typeof(DefinitionItem), + typeof(ParamItem), + typeof(LogItem)); + +#if DEBUG1 + foreach (var item in result.Results) + { + this.LogInformation($"create table {item.Key}, result: {item.Value}"); + } +#endif + } + + public async Task Setup() { await Init(); -#if DEBUG - Constants.SetAuthorization("RF4mfoUur0vHtWzHwD42ka0FhIfGaPnBxoQgrXOYEDg="); +#if DEBUG1 + var token = "RF4mfoUur0vHtWzHwD42ka0FhIfGaPnBxoQgrXOYEDg="; #else - var token = await database.Table().FirstOrDefaultAsync(p => p.Code == Constants.LastTokenName); - if (token != null) - { - Constants.SetAuthorization(token.Value); - } + var tk = await database.Table().FirstOrDefaultAsync(p => p.Code == Constants.LastTokenName); + var token = tk?.Value; #endif + if (token is string t) + { + Constants.SetAuthorization(t); + var user = await database.Table().FirstOrDefaultAsync(u => u.Token == t); + if (user != null) + { + AppResources.SetUser(user); + } + } var version = await database.Table().FirstOrDefaultAsync(p => p.Code == Constants.ApiVersionName); - var definition = await Constants.Initialize(logger, version?.Value); + var definition = await Constants.Initialize(this, version?.Value); if (definition != null) { categories = definition.Categories; events = definition.Events; - logger.LogInformation("new version founded, from ({from}) to ({to})", version?.Value, definition.ApiVersion); + this.LogInformation($"new version founded, from ({version?.Value}) to ({definition.ApiVersion})"); if (version == null) { @@ -113,7 +142,7 @@ public class FlowerDatabase }); } var rows = await database.InsertAllAsync(defs); - logger.LogInformation("{count} definitions, {rows} rows inserted", defs.Count, rows); + this.LogInformation($"{defs.Count} definitions, {rows} rows inserted"); } else { @@ -139,32 +168,10 @@ public class FlowerDatabase } } - private async Task Init() + public async Task AddLog(LogItem log) { - if (database is not null) - { - return; - } - - database = new SQLiteAsyncConnection(Constants.DatabasePath, Constants.SQLiteFlags); - -#if DEBUG - var result = -#endif - await database.CreateTablesAsync(CreateFlags.None, - typeof(FlowerItem), - typeof(RecordItem), - typeof(PhotoItem), - typeof(UserItem), - typeof(DefinitionItem), - typeof(ParamItem)); - -#if DEBUG - foreach (var item in result.Results) - { - logger.LogInformation("create table {table}, result: {result}", item.Key, item.Value); - } -#endif + await Init(); + return await database.InsertAsync(log); } public async Task GetFlowers() @@ -172,4 +179,47 @@ public class FlowerDatabase await Init(); return await database.Table().ToArrayAsync(); } + + public async Task UpdateFlowers(IEnumerable flowers) + { + await Init(); + + var ids = flowers.Select(f => f.Id).ToList(); + var count = await database.Table().DeleteAsync(f => ids.Contains(f.Id)); + await database.Table().DeleteAsync(p => p.RecordId == null && ids.Contains(p.FlowerId)); + + await database.InsertAllAsync(flowers); + foreach (var flower in flowers) + { + if (flower.Photos?.Length > 0) + { + await database.InsertAllAsync(flower.Photos); + } + } + return count; + } + + public async Task SetUser(UserItem user) + { + await Init(); + var count = user.Id > 0 ? + await database.Table().CountAsync(u => u.Id == user.Id) : + 0; + if (count > 0) + { + count = await database.UpdateAsync(user); + } + else + { + count = await database.InsertAsync(user); + } + if (count > 0) + { + var c = await database.Table().FirstOrDefaultAsync(p => p.Code == Constants.LastTokenName); + c ??= new ParamItem { Code = Constants.LastTokenName }; + c.Value = user.Token; + await database.InsertOrReplaceAsync(c); + } + return count; + } } diff --git a/FlowerApp/Data/Model/FlowerItem.cs b/FlowerApp/Data/Model/FlowerItem.cs index bc00eda..4bc7b0c 100644 --- a/FlowerApp/Data/Model/FlowerItem.cs +++ b/FlowerApp/Data/Model/FlowerItem.cs @@ -1,4 +1,5 @@ using SQLite; +using System.Text.Json.Serialization; namespace Blahblah.FlowerApp.Data.Model; @@ -12,19 +13,19 @@ public class FlowerItem public int OwnerId { get; set; } [Column("category"), NotNull] - public int Category { get; set; } + public int CategoryId { get; set; } [Column("Name"), NotNull] public string Name { get; set; } = null!; - [Column("datebuy"), NotNull] + [Column("datebuy"), JsonPropertyName("dateBuy"), NotNull] public long DateBuyUnixTime { get; set; } [Column("cost")] public decimal? Cost { get; set; } [Column("purchase")] - public string? PurchaseFrom { get; set; } + public string? Purchase { get; set; } [Column("memo")] public string? Memo { get; set; } diff --git a/FlowerApp/Data/Model/LogItem.cs b/FlowerApp/Data/Model/LogItem.cs new file mode 100644 index 0000000..574563f --- /dev/null +++ b/FlowerApp/Data/Model/LogItem.cs @@ -0,0 +1,34 @@ +using SQLite; + +namespace Blahblah.FlowerApp.Data.Model; + +[Table("logs")] +public class LogItem +{ + [Column("lid"), PrimaryKey, AutoIncrement] + public int Id { get; set; } + + [Column("logtime"), NotNull] + public long LogUnixTime { get; set; } + + [Column("uid"), NotNull] + public int OwnerId { get; set; } + + [Column("logtype"), NotNull] + public string LogType { get; set; } = null!; + + [Column("category"), NotNull] + public string Category { get; set; } = null!; + + [Column("message"), NotNull] + public string Message { get; set; } = null!; + + [Column("source")] + public string? Source { get; set; } = null!; + + [Column("description")] + public string? Description { get; set; } + + [Column("client")] + public string? ClientAgent { get; set; } +} diff --git a/FlowerApp/Data/Model/ParamItem.cs b/FlowerApp/Data/Model/ParamItem.cs index 38fe911..8a17564 100644 --- a/FlowerApp/Data/Model/ParamItem.cs +++ b/FlowerApp/Data/Model/ParamItem.cs @@ -5,15 +5,12 @@ namespace Blahblah.FlowerApp.Data.Model; [Table("params")] public class ParamItem { - [Column("pid"), PrimaryKey, AutoIncrement] - public int Id { get; set; } - - [Column("uid")] - public int? OwnerId { get; set; } - - [Column("code"), NotNull] + [Column("code"), PrimaryKey, NotNull] public string Code { get; set; } = null!; + [Column("uid"), NotNull] + public int OwnerId { get; set; } = AppResources.EmptyUserId; + [Column("value"), NotNull] public string Value { get; set; } = null!; diff --git a/FlowerApp/Data/Model/PhotoItem.cs b/FlowerApp/Data/Model/PhotoItem.cs index cff3a4d..c350cab 100644 --- a/FlowerApp/Data/Model/PhotoItem.cs +++ b/FlowerApp/Data/Model/PhotoItem.cs @@ -1,4 +1,5 @@ using SQLite; +using System.Text.Json.Serialization; namespace Blahblah.FlowerApp.Data.Model; @@ -14,8 +15,8 @@ public class PhotoItem [Column("fid"), NotNull] public int FlowerId { get; set; } - [Column("rid"), NotNull] - public int RecordId { get; set; } + [Column("rid")] + public int? RecordId { get; set; } [Column("filetype"), NotNull] public string FileType { get; set; } = null!; @@ -26,7 +27,7 @@ public class PhotoItem [Column("path"), NotNull] public string Path { get; set; } = null!; - [Column("dateupload"), NotNull] + [Column("dateupload"), JsonPropertyName("dateUpload"), NotNull] public long DateUploadUnixTime { get; set; } [Column("url")] diff --git a/FlowerApp/Data/Model/RecordItem.cs b/FlowerApp/Data/Model/RecordItem.cs index f2c3095..baa831f 100644 --- a/FlowerApp/Data/Model/RecordItem.cs +++ b/FlowerApp/Data/Model/RecordItem.cs @@ -1,7 +1,9 @@ using SQLite; +using System.Text.Json.Serialization; namespace Blahblah.FlowerApp.Data.Model; +[Table("records")] public class RecordItem { [Column("rid"), PrimaryKey, NotNull] @@ -14,9 +16,9 @@ public class RecordItem public int FlowerId { get; set; } [Column("event"), NotNull] - public int EventType { get; set; } + public int EventId { get; set; } - [Column("date"), NotNull] + [Column("date"), JsonPropertyName("date"), NotNull] public long DateUnixTime { get; set; } [Column("byuid")] @@ -33,4 +35,7 @@ public class RecordItem [Column("longitude")] public double? Longitude { get; set; } + + [Ignore] + public PhotoItem[]? Photos { get; set; } } diff --git a/FlowerApp/Data/Model/UserItem.cs b/FlowerApp/Data/Model/UserItem.cs index 0a0e497..dfd6aaa 100644 --- a/FlowerApp/Data/Model/UserItem.cs +++ b/FlowerApp/Data/Model/UserItem.cs @@ -1,7 +1,9 @@ using SQLite; +using System.Text.Json.Serialization; namespace Blahblah.FlowerApp.Data.Model; +[Table("users")] public class UserItem { [Column("uid"), PrimaryKey, NotNull] @@ -19,14 +21,9 @@ public class UserItem [Column("level"), NotNull] public int Level { get; set; } - [Column("regdate"), NotNull] + [Column("regdate"), JsonPropertyName("registerDate"), NotNull] public long RegisterDateUnixTime { get; set; } - //[Column("activedate")] - //public long? ActiveDateUnixTime { get; set; } - - //public DateTimeOffset? ActiveDate => ActiveDateUnixTime == null ? null : DateTimeOffset.FromUnixTimeMilliseconds(ActiveDateUnixTime.Value); - [Column("email")] public string? Email { get; set; } @@ -35,4 +32,9 @@ public class UserItem [Column("avatar")] public byte[]? Avatar { get; set; } + + public override string ToString() + { + return $"{{ Id: {Id}, Token: \"{Token}\", UserId: \"{UserId}\", Name: \"{Name}\", Level: {Level}, RegisterDate: \"{DateTimeOffset.FromUnixTimeMilliseconds(RegisterDateUnixTime)}\" }}"; + } } diff --git a/FlowerApp/Extensions.cs b/FlowerApp/Extensions.cs index 83619c3..8dd31ee 100644 --- a/FlowerApp/Extensions.cs +++ b/FlowerApp/Extensions.cs @@ -1,10 +1,21 @@ using Blahblah.FlowerApp.Data; +using Microsoft.Extensions.Logging; using System.Net.Http.Json; namespace Blahblah.FlowerApp; internal sealed class Extensions { + public static string L(string key, string defaultValue = "") + { + return LocalizationResource.GetText(key, defaultValue); + } + + public static BindableProperty CreateProperty(string propertyName, T? defaultValue = default) + { + return BindableProperty.Create(propertyName, typeof(T), typeof(V), defaultValue); + } + public static async Task FetchAsync(string url, CancellationToken cancellation = default) { using var client = new HttpClient(); @@ -15,4 +26,114 @@ internal sealed class Extensions } return await client.GetFromJsonAsync($"{Constants.BaseUrl}/{url}", cancellation); } + + public static async Task PostAsync(string url, T 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; + if (content.Headers.TryGetValues("Authorization", out var values) && + values.FirstOrDefault() is string oAuth) + { + Constants.SetAuthorization(oAuth); + var result = await content.ReadFromJsonAsync(cancellation); + return result; + } + } + return default; + } } + +internal static class LoggerExtension +{ + const LogLevel MinimumLogLevel = LogLevel.Information; + + public static void LogInformation(this ILoggerContent content, string message) + { + Log(content, LogLevel.Information, null, message); + } + + public static void LogWarning(this ILoggerContent content, string message) + { + Log(content, LogLevel.Warning, null, message); + } + + public static void LogError(this ILoggerContent content, Exception? exception, string message) + { + Log(content, LogLevel.Error, exception, message); + } + + private static void Log(ILoggerContent content, LogLevel level, Exception? exception, string message) + { + if (content?.Logger is ILogger logger) + { + logger.Log(level, exception, "[{time:MM/dd HH:mm:ss}] - {message}", DateTime.UtcNow, message); + + if (content.Database is FlowerDatabase database) + { + _ = database.AddLog(new Data.Model.LogItem + { + OwnerId = AppResources.User.Id, + LogType = level.ToString(), + LogUnixTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + Category = "logger", + ClientAgent = Constants.UserAgent, + Message = message, + Description = exception?.ToString(), + Source = exception?.Source + }); + } + } + } +} + +internal static class PageExtension +{ + public static Task AlertError(this ContentPage page, string error) + { + return Alert(page, LocalizationResource.GetText("error", "Error"), error); + } + + public static Task Alert(this ContentPage page, string title, string message, string? cancel = null) + { + cancel ??= LocalizationResource.GetText("ok", "Ok"); + + if (MainThread.IsMainThread) + { + return page.DisplayAlert(title, message, cancel); + } + var taskSource = new TaskCompletionSource(); + MainThread.BeginInvokeOnMainThread(async () => + { + await page.DisplayAlert(title, message, cancel); + taskSource.TrySetResult(); + }); + return taskSource.Task; + } + + public static Task Confirm(this ContentPage page, string title, string question, string? accept = null, string? cancel = null) + { + accept ??= LocalizationResource.GetText("yes", "Yes"); + cancel ??= LocalizationResource.GetText("no", "No"); + + if (MainThread.IsMainThread) + { + return page.DisplayAlert(title, question, accept, cancel); + } + var taskSource = new TaskCompletionSource(); + MainThread.BeginInvokeOnMainThread(async () => + { + var result = await page.DisplayAlert(title, question, accept, cancel); + taskSource.TrySetResult(result); + }); + return taskSource.Task; + } +} \ No newline at end of file diff --git a/FlowerApp/FlowerApp.csproj b/FlowerApp/FlowerApp.csproj index 33fa655..796117a 100644 --- a/FlowerApp/FlowerApp.csproj +++ b/FlowerApp/FlowerApp.csproj @@ -17,10 +17,10 @@ org.blahblah.flowerstory - 0.1.719 - 1 + 0.2.731 + 2 - 13.0 + 15.0 23.0 @@ -52,6 +52,7 @@ + @@ -67,6 +68,11 @@ + + + + + @@ -99,6 +105,18 @@ + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + \ No newline at end of file diff --git a/FlowerApp/Resources/Images/flower_tulip.svg b/FlowerApp/Resources/Images/flower_tulip.svg new file mode 100644 index 0000000..ea5b4b5 --- /dev/null +++ b/FlowerApp/Resources/Images/flower_tulip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/FlowerApp/Resources/Images/user.svg b/FlowerApp/Resources/Images/user.svg new file mode 100644 index 0000000..91c268d --- /dev/null +++ b/FlowerApp/Resources/Images/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/FlowerApp/SquarePage.xaml b/FlowerApp/SquarePage.xaml new file mode 100644 index 0000000..f294c34 --- /dev/null +++ b/FlowerApp/SquarePage.xaml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/FlowerApp/SquarePage.xaml.cs b/FlowerApp/SquarePage.xaml.cs new file mode 100644 index 0000000..cc6c86d --- /dev/null +++ b/FlowerApp/SquarePage.xaml.cs @@ -0,0 +1,9 @@ +namespace Blahblah.FlowerApp; + +public partial class SquarePage : AppContentPage +{ + public SquarePage() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/FlowerApp/UserPage.xaml b/FlowerApp/UserPage.xaml new file mode 100644 index 0000000..1a6a863 --- /dev/null +++ b/FlowerApp/UserPage.xaml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/FlowerApp/UserPage.xaml.cs b/FlowerApp/UserPage.xaml.cs new file mode 100644 index 0000000..2f9b5a4 --- /dev/null +++ b/FlowerApp/UserPage.xaml.cs @@ -0,0 +1,15 @@ +using Blahblah.FlowerApp.Data; +using Microsoft.Extensions.Logging; + +namespace Blahblah.FlowerApp; + +public partial class UserPage : AppContentPage +{ + public UserPage(FlowerDatabase database, ILogger logger) + { + Database = database; + Logger = logger; + + InitializeComponent(); + } +} \ No newline at end of file diff --git a/FlowerStory.sln b/FlowerStory.sln index 073f348..6bd7b77 100644 --- a/FlowerStory.sln +++ b/FlowerStory.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.7.33711.374 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", "Server\Server.csproj", "{A551F94A-1997-4A20-A1E8-157050D92CEF}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestCase", "TestCase\TestCase.csproj", "{BE89C419-EE4D-4F0C-BB8E-4BEE2BC3AB0C}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlowerApp", "FlowerApp\FlowerApp.csproj", "{FCBB0455-071E-407B-9CB6-553C6D283756}" EndProject Global @@ -17,6 +19,10 @@ Global {A551F94A-1997-4A20-A1E8-157050D92CEF}.Debug|Any CPU.Build.0 = Debug|Any CPU {A551F94A-1997-4A20-A1E8-157050D92CEF}.Release|Any CPU.ActiveCfg = Release|Any CPU {A551F94A-1997-4A20-A1E8-157050D92CEF}.Release|Any CPU.Build.0 = Release|Any CPU + {BE89C419-EE4D-4F0C-BB8E-4BEE2BC3AB0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE89C419-EE4D-4F0C-BB8E-4BEE2BC3AB0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE89C419-EE4D-4F0C-BB8E-4BEE2BC3AB0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE89C419-EE4D-4F0C-BB8E-4BEE2BC3AB0C}.Release|Any CPU.Build.0 = Release|Any CPU {FCBB0455-071E-407B-9CB6-553C6D283756}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FCBB0455-071E-407B-9CB6-553C6D283756}.Debug|Any CPU.Build.0 = Debug|Any CPU {FCBB0455-071E-407B-9CB6-553C6D283756}.Debug|Any CPU.Deploy.0 = Debug|Any CPU diff --git a/Server/Controller/BaseController.cs b/Server/Controller/BaseController.cs index fb6cdf0..9425e54 100644 --- a/Server/Controller/BaseController.cs +++ b/Server/Controller/BaseController.cs @@ -1,7 +1,6 @@ using Blahblah.FlowerStory.Server.Data; using Blahblah.FlowerStory.Server.Data.Model; using Microsoft.AspNetCore.Mvc; -using System.ComponentModel.DataAnnotations; using System.Net; using System.Security.Cryptography; using System.Text; @@ -55,10 +54,6 @@ public abstract partial class BaseController : ControllerBase /// 管理员用户 /// protected const int UserAdmin = 99; - /// - /// 封面事件 - /// - protected const int EventCover = 0; /// /// 数据库对象 diff --git a/Server/Controller/FlowerApiController.cs b/Server/Controller/FlowerApiController.cs index dc65d1e..c2513d5 100644 --- a/Server/Controller/FlowerApiController.cs +++ b/Server/Controller/FlowerApiController.cs @@ -232,9 +232,7 @@ public class FlowerApiController : BaseController { foreach (var f in flowers) { - f.Photos = database.Photos.Where(p => - database.Records.Any(r => - r.FlowerId == f.Id && r.EventId == EventCover && r.Id == p.RecordId)).ToList(); + f.Photos = database.Photos.Where(p => p.FlowerId == f.Id && p.RecordId == null).ToList(); foreach (var photo in f.Photos) { photo.Url = $"photo/flower/{f.Id}/{photo.Path}"; @@ -303,9 +301,7 @@ public class FlowerApiController : BaseController if (includePhoto == true) { - item.Photos = database.Photos.Where(p => - database.Records.Any(r => - r.FlowerId == item.Id && r.EventId == EventCover && r.Id == p.RecordId)).ToList(); + item.Photos = database.Photos.Where(p => p.FlowerId == item.Id && p.RecordId == null).ToList(); foreach (var photo in item.Photos) { photo.Url = $"photo/flower/{item.Id}/{photo.Path}"; @@ -487,25 +483,6 @@ public class FlowerApiController : BaseController return BadRequest(); } - var record = database.Records.SingleOrDefault(r => r.FlowerId == item.Id && r.EventId == EventCover); - if (record == null) - { - record = new RecordItem - { - OwnerId = user.Id, - FlowerId = item.Id, - EventId = EventCover, - DateUnixTime = now, - ByUserId = user.Id, - ByUserName = user.Name, - //Memo = flower.Memo, - Latitude = flower.Latitude, - Longitude = flower.Longitude - }; - database.Records.Add(record); - } - SaveDatabase(); - try { await ExecuteTransaction(async token => @@ -514,7 +491,6 @@ public class FlowerApiController : BaseController { OwnerId = user.Id, FlowerId = item.Id, - RecordId = record.Id, FileType = file.FileType, FileName = file.Filename, Path = file.Path, @@ -538,27 +514,7 @@ public class FlowerApiController : BaseController var photo = database.Photos.SingleOrDefault(p => p.Id == coverId && p.OwnerId == user.Id); if (photo != null) { - var record = database.Records.SingleOrDefault(r => r.FlowerId == item.Id && r.EventId == EventCover); - if (record == null) - { - record = new RecordItem - { - OwnerId = user.Id, - FlowerId = item.Id, - EventId = EventCover, - DateUnixTime = now, - ByUserId = user.Id, - ByUserName = user.Name, - //Memo = flower.Memo, - Latitude = flower.Latitude, - Longitude = flower.Longitude - }; - database.Records.Add(record); - SaveDatabase(); - } - photo.FlowerId = item.Id; - photo.RecordId = record.Id; SaveDatabase(); try @@ -737,33 +693,13 @@ public class FlowerApiController : BaseController } var now = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - var record = database.Records.SingleOrDefault(r => r.FlowerId == update.Id && r.EventId == EventCover); - if (record == null) + var photos = database.Photos.Where(p => p.FlowerId == update.Id && p.RecordId == null).ToList(); + if (photos.Count > 0) { - record = new RecordItem + database.Photos.Where(p => p.RecordId == null).ExecuteDelete(); + foreach (var photo in photos) { - OwnerId = user.Id, - FlowerId = update.Id, - EventId = EventCover, - DateUnixTime = now, - ByUserId = user.Id, - ByUserName = user.Name, - //Memo = flower.Memo, - Latitude = flower.Latitude, - Longitude = flower.Longitude - }; - database.Records.Add(record); - } - else - { - var photos = database.Photos.Where(p => p.RecordId == record.Id).ToList(); - if (photos.Count > 0) - { - database.Photos.Where(p => p.RecordId == record.Id).ExecuteDelete(); - foreach (var photo in photos) - { - DeleteFile(update.Id, photo.Path); - } + DeleteFile(update.Id, photo.Path); } } SaveDatabase(); @@ -776,7 +712,6 @@ public class FlowerApiController : BaseController { OwnerId = user.Id, FlowerId = update.Id, - RecordId = record.Id, FileType = file.FileType, FileName = file.Filename, Path = file.Path, @@ -865,23 +800,6 @@ public class FlowerApiController : BaseController } var now = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - var record = database.Records.SingleOrDefault(r => r.FlowerId == param.Id && r.EventId == EventCover); - if (record == null) - { - record = new RecordItem - { - OwnerId = user.Id, - FlowerId = param.Id, - EventId = EventCover, - DateUnixTime = now, - ByUserId = user.Id, - ByUserName = user.Name, - //Memo = "", - Latitude = param.Latitude, - Longitude = param.Longitude - }; - database.Records.Add(record); - } flower.Latitude = param.Latitude; flower.Longitude = param.Longitude; @@ -895,7 +813,6 @@ public class FlowerApiController : BaseController { OwnerId = user.Id, FlowerId = param.Id, - RecordId = record.Id, FileType = file.FileType, FileName = file.Filename, Path = file.Path, @@ -934,7 +851,7 @@ public class FlowerApiController : BaseController /// /// /// 花草唯一 id - /// 事件类型 id, 0 为封面 + /// 事件类型 id /// 验证通过则返回花草特定类型事件的照片列表 /// 返回花草特定类型事件的照片列表 /// 未找到登录会话或已过期 @@ -948,7 +865,7 @@ public class FlowerApiController : BaseController [ProducesErrorResponseType(typeof(ErrorResponse))] [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] - public ActionResult GetCovers([Required][FromQuery] int id, [FromQuery(Name = "eid")] int? eventId = 0) + public ActionResult GetPhotos([Required][FromQuery] int id, [FromQuery(Name = "eid")] int? eventId = 0) { var (result, user) = CheckPermission(); if (result != null) diff --git a/Server/Controller/UserApiController.cs b/Server/Controller/UserApiController.cs index 3c1d0ec..2136ec4 100644 --- a/Server/Controller/UserApiController.cs +++ b/Server/Controller/UserApiController.cs @@ -34,19 +34,19 @@ public partial class UserApiController : BaseController /// /// 登录参数 /// 成功登录则返回自定义认证头 - /// 返回自定义认证头 + /// 返回用户对象,返回头中包含认证信息 /// 认证失败 /// 未找到用户 /// 服务器错误 [Route("auth", Name = "authenticate")] - [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesErrorResponseType(typeof(ErrorResponse))] [HttpPost] [Consumes("application/json")] - public ActionResult Authenticate([FromBody] LoginParamter login) + public ActionResult Authenticate([FromBody] LoginParamter login) { #if DEBUG logger?.LogInformation("user \"{user}\" try to login with password \"{password}\"", login.Id, login.Password); @@ -100,7 +100,7 @@ public partial class UserApiController : BaseController SaveDatabase(); Response.Headers.Add(AuthHeader, token.Id); - return NoContent(); + return Ok(user); } /// diff --git a/Server/Program.cs b/Server/Program.cs index f53950d..5a1e6be 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -11,7 +11,7 @@ public class Program /// public const string ProjectName = "Flower Story"; /// - public const string Version = "0.7.727"; + public const string Version = "0.7.731"; /// public static void Main(string[] args)