home page for App

This commit is contained in:
Tsanie Lily 2023-07-27 22:07:24 +08:00
parent 02ac33fc07
commit befbc7fc9b
40 changed files with 1175 additions and 398 deletions

View File

@ -3,13 +3,13 @@
x:Class="Blahblah.FlowerApp.AppShell" x:Class="Blahblah.FlowerApp.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Blahblah.FlowerApp" xmlns:l="clr-namespace:Blahblah.FlowerApp"
Shell.FlyoutBehavior="Disabled" Shell.FlyoutBehavior="Disabled"
Title="Flower Story"> Title="Flower Story">
<ShellContent <ShellContent
Title="Home" Title="{l:Lang home, Default=Garden Square}"
ContentTemplate="{DataTemplate local:MainPage}" ContentTemplate="{DataTemplate l:HomePage}"
Route="MainPage" /> Route="HomePage" />
</Shell> </Shell>

View File

@ -0,0 +1,59 @@
namespace Blahblah.FlowerApp;
public class AppContentPage : ContentPage
{
protected static string L(string key, string defaultValue = "")
{
return LocalizationResource.GetText(key, defaultValue);
}
protected static Task<T?> FetchAsync<T>(string url, CancellationToken cancellation = default)
{
return Extensions.FetchAsync<T>(url, cancellation);
}
protected T GetValue<T>(BindableProperty property)
{
return (T)GetValue(property);
}
protected Task AlertError(string error)
{
return Alert(LocalizationResource.GetText("error", "Error"), error);
}
protected Task Alert(string title, string message, string? cancel = null)
{
cancel ??= LocalizationResource.GetText("ok", "Ok");
if (MainThread.IsMainThread)
{
return DisplayAlert(title, message, cancel);
}
var taskSource = new TaskCompletionSource();
MainThread.BeginInvokeOnMainThread(async () =>
{
await DisplayAlert(title, message, cancel);
taskSource.TrySetResult();
});
return taskSource.Task;
}
protected Task<bool> 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<bool>();
MainThread.BeginInvokeOnMainThread(async () =>
{
var result = await DisplayAlert(title, question, accept, cancel);
taskSource.TrySetResult(result);
});
return taskSource.Task;
}
}

View File

@ -0,0 +1,8 @@
namespace Blahblah.FlowerApp;
internal class AppResources
{
public const string EmptyCover = "empty_flower.jpg";
public static readonly Size EmptySize = new(512, 339);
}

View File

@ -0,0 +1,57 @@
using Blahblah.FlowerApp.Data.Model;
using static Blahblah.FlowerApp.PropertyExtension;
namespace Blahblah.FlowerApp.Controls;
public class FlowerClientItem : BindableObject
{
static readonly BindableProperty NameProperty = CreateProperty<string, FlowerClientItem>(nameof(Name));
static readonly BindableProperty CategoryProperty = CreateProperty<int, FlowerClientItem>(nameof(Category));
static readonly BindableProperty CoverProperty = CreateProperty<ImageSource?, FlowerClientItem>(nameof(Cover));
static readonly BindableProperty BoundsProperty = CreateProperty<Rect, FlowerClientItem>(nameof(Bounds));
public int Id { get; }
public FlowerItem? FlowerItem { get; }
public string Name
{
get => (string)GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
public int Category
{
get => (int)GetValue(CategoryProperty);
set => SetValue(CategoryProperty, value);
}
public ImageSource? Cover
{
get => (ImageSource?)GetValue(CoverProperty);
set => SetValue(CoverProperty, value);
}
public Rect Bounds
{
get => (Rect)GetValue(BoundsProperty);
set => SetValue(BoundsProperty, value);
}
public int? Width { get; set; }
public int? Height { get; set; }
public FlowerClientItem(int id)
{
Id = id;
}
public FlowerClientItem(FlowerItem item) : this(item.Id)
{
FlowerItem = item;
Name = item.Name;
Category = item.Category;
if (item.Photos?.Length > 0 && item.Photos[0] is PhotoItem cover)
{
Width = cover.Width;
Height = cover.Height;
}
}
}

View File

@ -0,0 +1,9 @@
namespace Blahblah.FlowerApp;
internal sealed class PropertyExtension
{
public static BindableProperty CreateProperty<T, V>(string propertyName, T? defaultValue = default)
{
return BindableProperty.Create(propertyName, typeof(T), typeof(V), defaultValue);
}
}

View File

@ -1,12 +0,0 @@
namespace Blahblah.FlowerApp.Controls;
public class WaterfallLayout : View
{
public static readonly BindableProperty ColumnsProperty = BindableProperty.Create(nameof(Columns), typeof(int), typeof(WaterfallLayout), defaultValue: 2);
public int Columns
{
get => (int)GetValue(ColumnsProperty);
set => SetValue(ColumnsProperty, value);
}
}

View File

@ -1,19 +1,76 @@
using SQLite; using Microsoft.Extensions.Logging;
using SQLite;
namespace Blahblah.FlowerApp.Data; namespace Blahblah.FlowerApp.Data;
public sealed class Constants internal sealed class Constants
{ {
public const string CategoryOther = "other"; public const string CategoryOther = "other";
public const string EventUnknown = "unknown"; public const string EventUnknown = "unknown";
public const string BaseUrl = "https://flower.tsanie.org"; public const string ApiVersionName = "api_version";
public const string LastTokenName = "last_token";
public const string BaseUrl = "https://app.blahblaho.com";
public const SQLiteOpenFlags SQLiteFlags = public const SQLiteOpenFlags SQLiteFlags =
SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.ReadWrite |
SQLiteOpenFlags.Create | SQLiteOpenFlags.Create |
SQLiteOpenFlags.SharedCache; SQLiteOpenFlags.SharedCache;
private const string dbFilename = "flowerstory.db3"; const string dbFilename = "flowerstory.db3";
public static string DatabasePath => Path.Combine(FileSystem.AppDataDirectory, dbFilename); public static string DatabasePath => Path.Combine(FileSystem.AppDataDirectory, dbFilename);
static string? apiVersion;
public static string? ApiVersion => apiVersion;
static string? authorization;
public static string? Authorization => authorization;
public static void SetAuthorization(string auth)
{
authorization = auth;
}
public static async Task<Definitions?> Initialize(ILogger logger, string? version, CancellationToken cancellation = default)
{
try
{
var v = await Extensions.FetchAsync<string>("api/version", cancellation);
apiVersion = v;
if (v != version)
{
var definition = await Extensions.FetchAsync<Definitions>($"api/consts?{v}", cancellation);
return definition;
}
}
catch (Exception ex)
{
logger.LogError("error occurs on fetching version and definitions, {error}", ex);
}
return null;
}
}
internal record Definitions
{
public required string ApiVersion { get; init; }
public required Dictionary<int, NamedItem> Categories { get; init; }
public required Dictionary<int, EventItem> Events { get; init; }
}
internal 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 bool Unique { get; init; } = Unique;
} }

View File

@ -13,6 +13,130 @@ public class FlowerDatabase
public FlowerDatabase(ILogger<FlowerDatabase> logger) public FlowerDatabase(ILogger<FlowerDatabase> logger)
{ {
this.logger = logger; this.logger = logger;
Task.Run(async () =>
{
try
{
await Setup();
}
catch (Exception ex)
{
logger.LogError("error occurs on setup, {error}", ex);
}
});
}
private Dictionary<int, NamedItem>? categories;
private Dictionary<int, EventItem>? events;
public string Category(int categoryId)
{
if (categories?.TryGetValue(categoryId, out var category) == true)
{
return category.Name;
}
return Constants.CategoryOther;
}
public string Event(int eventId)
{
if (events?.TryGetValue(eventId, out var @event) == true)
{
return @event.Name;
}
return Constants.EventUnknown;
}
private async Task Setup()
{
await Init();
#if DEBUG
Constants.SetAuthorization("RF4mfoUur0vHtWzHwD42ka0FhIfGaPnBxoQgrXOYEDg=");
#else
var token = await database.Table<ParamItem>().FirstOrDefaultAsync(p => p.Code == Constants.LastTokenName);
if (token != null)
{
Constants.SetAuthorization(token.Value);
}
#endif
var version = await database.Table<ParamItem>().FirstOrDefaultAsync(p => p.Code == Constants.ApiVersionName);
var definition = await Constants.Initialize(logger, version?.Value);
if (definition != null)
{
categories = definition.Categories;
events = definition.Events;
logger.LogInformation("new version founded, from ({from}) to ({to})", version?.Value, definition.ApiVersion);
if (version == null)
{
version = new ParamItem
{
Code = Constants.ApiVersionName,
Value = definition.ApiVersion
};
}
else
{
version.Value = definition.ApiVersion;
}
await database.InsertOrReplaceAsync(version);
// replace local definitions
await database.DeleteAllAsync<DefinitionItem>();
var defs = new List<DefinitionItem>();
foreach (var category in definition.Categories)
{
defs.Add(new DefinitionItem
{
DefinitionType = 0,
DefinitionId = category.Key,
Name = category.Value.Name,
Description = category.Value.Description
});
}
foreach (var @event in definition.Events)
{
defs.Add(new DefinitionItem
{
DefinitionType = 1,
DefinitionId = @event.Key,
Name = @event.Value.Name,
Description = @event.Value.Description,
Unique = @event.Value.Unique
});
}
var rows = await database.InsertAllAsync(defs);
logger.LogInformation("{count} definitions, {rows} rows inserted", defs.Count, rows);
}
else
{
// use local definitions
var defs = await database.Table<DefinitionItem>().ToListAsync();
var cates = new Dictionary<int, NamedItem>();
var evts = new Dictionary<int, EventItem>();
foreach (var d in defs)
{
if (d.DefinitionType == 0)
{
// category
cates[d.DefinitionId] = new NamedItem(d.Name, d.Description);
}
else if (d.DefinitionType == 1)
{
// event
evts[d.DefinitionId] = new EventItem(d.Name, d.Description, d.Unique ?? false);
}
}
categories = cates;
events = evts;
}
} }
private async Task Init() private async Task Init()
@ -27,7 +151,13 @@ public class FlowerDatabase
#if DEBUG #if DEBUG
var result = var result =
#endif #endif
await database.CreateTablesAsync<FlowerItem, RecordItem, PhotoItem, UserItem>(); await database.CreateTablesAsync(CreateFlags.None,
typeof(FlowerItem),
typeof(RecordItem),
typeof(PhotoItem),
typeof(UserItem),
typeof(DefinitionItem),
typeof(ParamItem));
#if DEBUG #if DEBUG
foreach (var item in result.Results) foreach (var item in result.Results)

View File

@ -0,0 +1,29 @@
using SQLite;
namespace Blahblah.FlowerApp.Data.Model;
[Table("definitions")]
public class DefinitionItem
{
[Column("did"), PrimaryKey, AutoIncrement]
public int Id { get; set; }
/// <summary>
/// - 0: category
/// - 1: event
/// </summary>
[Column("type"), NotNull]
public int DefinitionType { get; set; }
[Column("id"), NotNull]
public int DefinitionId { get; set; }
[Column("name"), NotNull]
public string Name { get; set; } = null!;
[Column("description")]
public string? Description { get; set; }
[Column("unique")]
public bool? Unique { get; set; }
}

View File

@ -20,8 +20,6 @@ public class FlowerItem
[Column("datebuy"), NotNull] [Column("datebuy"), NotNull]
public long DateBuyUnixTime { get; set; } public long DateBuyUnixTime { get; set; }
public DateTimeOffset DateBuy => DateTimeOffset.FromUnixTimeMilliseconds(DateBuyUnixTime);
[Column("cost")] [Column("cost")]
public decimal? Cost { get; set; } public decimal? Cost { get; set; }
@ -37,5 +35,9 @@ public class FlowerItem
[Column("longitude")] [Column("longitude")]
public double? Longitude { get; set; } public double? Longitude { get; set; }
[Ignore]
public PhotoItem[]? Photos { get; set; }
[Ignore]
public int? Distance { get; set; } public int? Distance { get; set; }
} }

View File

@ -0,0 +1,22 @@
using SQLite;
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]
public string Code { get; set; } = null!;
[Column("value"), NotNull]
public string Value { get; set; } = null!;
[Column("description")]
public string? Description { get; set; }
}

View File

@ -29,8 +29,12 @@ public class PhotoItem
[Column("dateupload"), NotNull] [Column("dateupload"), NotNull]
public long DateUploadUnixTime { get; set; } public long DateUploadUnixTime { get; set; }
public DateTimeOffset DateUpload => DateTimeOffset.FromUnixTimeMilliseconds(DateUploadUnixTime);
[Column("url")] [Column("url")]
public string Url { get; set; } = null!; public string Url { get; set; } = null!;
[Column("width")]
public int? Width { get; set; }
[Column("height")]
public int? Height { get; set; }
} }

View File

@ -19,8 +19,6 @@ public class RecordItem
[Column("date"), NotNull] [Column("date"), NotNull]
public long DateUnixTime { get; set; } public long DateUnixTime { get; set; }
public DateTimeOffset Date => DateTimeOffset.FromUnixTimeMilliseconds(DateUnixTime);
[Column("byuid")] [Column("byuid")]
public int? ByUserId { get; set; } public int? ByUserId { get; set; }

View File

@ -4,15 +4,15 @@ namespace Blahblah.FlowerApp.Data.Model;
public class UserItem public class UserItem
{ {
[Column("uid"), PrimaryKey, AutoIncrement] [Column("uid"), PrimaryKey, NotNull]
public int Id { get; set; } public int Id { get; set; }
[Column("token"), Indexed, NotNull]
public string Token { get; set; } = null!;
[Column("id"), NotNull] [Column("id"), NotNull]
public string UserId { get; set; } = null!; public string UserId { get; set; } = null!;
[Column("token"), NotNull]
public string Token { get; set; } = null!;
[Column("name"), NotNull] [Column("name"), NotNull]
public string Name { get; set; } = null!; public string Name { get; set; } = null!;
@ -22,8 +22,6 @@ public class UserItem
[Column("regdate"), NotNull] [Column("regdate"), NotNull]
public long RegisterDateUnixTime { get; set; } public long RegisterDateUnixTime { get; set; }
public DateTimeOffset RegisterDate => DateTimeOffset.FromUnixTimeMilliseconds(RegisterDateUnixTime);
//[Column("activedate")] //[Column("activedate")]
//public long? ActiveDateUnixTime { get; set; } //public long? ActiveDateUnixTime { get; set; }

18
FlowerApp/Extensions.cs Normal file
View File

@ -0,0 +1,18 @@
using Blahblah.FlowerApp.Data;
using System.Net.Http.Json;
namespace Blahblah.FlowerApp;
internal sealed class Extensions
{
public static async Task<T?> FetchAsync<T>(string url, CancellationToken cancellation = default)
{
using var client = new HttpClient();
var authorization = Constants.Authorization;
if (authorization != null)
{
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", authorization);
}
return await client.GetFromJsonAsync<T>($"{Constants.BaseUrl}/{url}", cancellation);
}
}

View File

@ -68,6 +68,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Maui" Version="5.2.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="7.0.9" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="7.0.9" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
@ -92,9 +93,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Handlers\" />
<Folder Include="Platforms\Android\Handlers\" /> <Folder Include="Platforms\Android\Handlers\" />
<Folder Include="Platforms\Android\Controls\" />
<Folder Include="Platforms\iOS\Controls\" />
</ItemGroup> </ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties XamarinHotReloadDebuggerTimeoutExceptionFlowerAppHideInfoBar="True" /></VisualStudio></ProjectExtensions>
<!--<ItemGroup> <!--<ItemGroup>
<Folder Include="Properties\" /> <Folder Include="Properties\" />
</ItemGroup>--> </ItemGroup>-->

View File

@ -1,20 +0,0 @@
using Blahblah.FlowerApp.Controls;
namespace Blahblah.FlowerApp.Handlers;
public partial class WaterfallLayoutHandler
{
static readonly IPropertyMapper<WaterfallLayout, WaterfallLayoutHandler> PropertyMapper = new PropertyMapper<WaterfallLayout, WaterfallLayoutHandler>(ViewMapper)
{
[nameof(WaterfallLayout.Columns)] = MapColumns
};
static readonly CommandMapper<WaterfallLayout, WaterfallLayoutHandler> CommandMapper = new(ViewCommandMapper)
{
};
public WaterfallLayoutHandler() : base(PropertyMapper, CommandMapper)
{
}
}

View File

@ -60,6 +60,60 @@ namespace Blahblah.FlowerApp {
} }
} }
/// <summary>
/// Looks up a localized string similar to Error.
/// </summary>
internal static string error {
get {
return ResourceManager.GetString("error", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to get flowers, please try again..
/// </summary>
internal static string failedGetFlowers {
get {
return ResourceManager.GetString("failedGetFlowers", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Garden Square.
/// </summary>
internal static string home {
get {
return ResourceManager.GetString("home", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No.
/// </summary>
internal static string no {
get {
return ResourceManager.GetString("no", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Nothing here....
/// </summary>
internal static string nothing {
get {
return ResourceManager.GetString("nothing", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Ok.
/// </summary>
internal static string ok {
get {
return ResourceManager.GetString("ok", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Unknown. /// Looks up a localized string similar to Unknown.
/// </summary> /// </summary>
@ -68,5 +122,14 @@ namespace Blahblah.FlowerApp {
return ResourceManager.GetString("unknown", resourceCulture); return ResourceManager.GetString("unknown", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Yes.
/// </summary>
internal static string yes {
get {
return ResourceManager.GetString("yes", resourceCulture);
}
}
} }
} }

View File

@ -117,7 +117,28 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="error" xml:space="preserve">
<value>Error</value>
</data>
<data name="failedGetFlowers" xml:space="preserve">
<value>Failed to get flowers, please try again.</value>
</data>
<data name="home" xml:space="preserve">
<value>Garden Square</value>
</data>
<data name="no" xml:space="preserve">
<value>No</value>
</data>
<data name="nothing" xml:space="preserve">
<value>Nothing here...</value>
</data>
<data name="ok" xml:space="preserve">
<value>Ok</value>
</data>
<data name="unknown" xml:space="preserve"> <data name="unknown" xml:space="preserve">
<value>Unknown</value> <value>Unknown</value>
</data> </data>
<data name="yes" xml:space="preserve">
<value>Yes</value>
</data>
</root> </root>

View File

@ -117,7 +117,28 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="error" xml:space="preserve">
<value>错误</value>
</data>
<data name="failedGetFlowers" xml:space="preserve">
<value>获取花草失败,请重试。</value>
</data>
<data name="home" xml:space="preserve">
<value>花园广场</value>
</data>
<data name="no" xml:space="preserve">
<value>否</value>
</data>
<data name="nothing" xml:space="preserve">
<value>没有任何东西...</value>
</data>
<data name="ok" xml:space="preserve">
<value>好</value>
</data>
<data name="unknown" xml:space="preserve"> <data name="unknown" xml:space="preserve">
<value>未知</value> <value>未知</value>
</data> </data>
<data name="yes" xml:space="preserve">
<value>是</value>
</data>
</root> </root>

View File

@ -1,46 +1,51 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" <l:AppContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:l="clr-namespace:Blahblah.FlowerApp" xmlns:l="clr-namespace:Blahblah.FlowerApp"
xmlns:ctl="clr-namespace:Blahblah.FlowerApp.Controls" xmlns:ctl="clr-namespace:Blahblah.FlowerApp.Controls"
x:Class="Blahblah.FlowerApp.MainPage" xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Name="mainPage" x:Class="Blahblah.FlowerApp.HomePage"
x:DataType="l:MainPage"> x:Name="homePage"
x:DataType="l:HomePage">
<ScrollView BindingContext="{x:Reference mainPage}"> <ContentPage.ToolbarItems>
<!--<VerticalStackLayout <ToolbarItem Text="Filter"/>
Spacing="25" </ContentPage.ToolbarItems>
Padding="30,0"
VerticalOptions="Center">
<Image <ContentPage.Resources>
Source="dotnet_bot.png" <DataTemplate x:Key="flowerTemplate" x:DataType="ctl:FlowerClientItem">
SemanticProperties.Description="Cute dot net bot waving hi to you!" <Frame HasShadow="False" Padding="0"
HeightRequest="200" CornerRadius="12" BorderColor="Transparent"
HorizontalOptions="Center" /> AbsoluteLayout.LayoutFlags="XProportional"
AbsoluteLayout.LayoutBounds="{Binding Bounds}">
<Grid RowDefinitions="*, 30" ColumnDefinitions="Auto, *"
RowSpacing="6" ColumnSpacing="4">
<Image Grid.ColumnSpan="2" Source="{Binding Cover}"/>
<Frame Grid.Row="1" HasShadow="False" Margin="0" Padding="0"
WidthRequest="30" HeightRequest="30" CornerRadius="15"
BorderColor="Transparent" BackgroundColor="LightGray"
VerticalOptions="Center"></Frame>
<Label Grid.Column="1" Grid.Row="1" Text="{Binding Name}"
VerticalOptions="Center"/>
</Grid>
</Frame>
</DataTemplate>
</ContentPage.Resources>
<Label <RefreshView Refreshing="RefreshView_Refreshing" IsRefreshing="{Binding IsRefreshing}"
Text="{l:Lang unknown, Default=Unknown}" BindingContext="{x:Reference homePage}">
SemanticProperties.HeadingLevel="Level1" <ScrollView>
FontSize="32" <AbsoluteLayout Margin="12"
HorizontalOptions="Center" /> BindableLayout.ItemsSource="{Binding Flowers}"
BindableLayout.ItemTemplate="{StaticResource flowerTemplate}">
<Label <BindableLayout.EmptyView>
Text="Welcome to .NET Multi-platform App UI" <Label Text="{l:Lang nothing, Default=Nothing here...}"
SemanticProperties.HeadingLevel="Level2" HorizontalTextAlignment="Center"
SemanticProperties.Description="Welcome to dot net Multi platform App U I" AbsoluteLayout.LayoutFlags="SizeProportional"
FontSize="18" AbsoluteLayout.LayoutBounds="0,20,1,1"/>
HorizontalOptions="Center" /> </BindableLayout.EmptyView>
</AbsoluteLayout>
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
</VerticalStackLayout>-->
<ctl:WaterfallLayout/>
</ScrollView> </ScrollView>
</RefreshView>
</ContentPage> </l:AppContentPage>

View File

@ -1,15 +1,41 @@
using Blahblah.FlowerApp.Data; using Blahblah.FlowerApp.Controls;
using Blahblah.FlowerApp.Data;
using Blahblah.FlowerApp.Data.Model;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Blahblah.FlowerApp.PropertyExtension;
namespace Blahblah.FlowerApp; namespace Blahblah.FlowerApp;
public partial class MainPage : ContentPage public partial class HomePage : AppContentPage
{ {
int count = 0; static readonly BindableProperty FlowersProperty = CreateProperty<FlowerClientItem[], HomePage>(nameof(Flowers));
static readonly BindableProperty IsRefreshingProperty = CreateProperty<bool, HomePage>(nameof(IsRefreshing));
public FlowerClientItem[] Flowers
{
get => GetValue<FlowerClientItem[]>(FlowersProperty);
set => SetValue(FlowersProperty, value);
}
public bool IsRefreshing
{
get => GetValue<bool>(IsRefreshingProperty);
set => SetValue(IsRefreshingProperty, value);
}
readonly FlowerDatabase database; readonly FlowerDatabase database;
readonly ILogger logger; readonly ILogger logger;
public MainPage(FlowerDatabase database, ILogger<MainPage> logger) bool loaded = false;
double pageWidth;
const int margin = 12;
const int cols = 2;
double[] ys = null!;
int yIndex;
int itemWidth;
int emptyHeight;
public HomePage(FlowerDatabase database, ILogger<HomePage> logger)
{ {
this.database = database; this.database = database;
this.logger = logger; this.logger = logger;
@ -17,33 +43,115 @@ public partial class MainPage : ContentPage
InitializeComponent(); InitializeComponent();
} }
protected override void OnAppearing() protected override void OnSizeAllocated(double width, double height)
{ {
base.OnAppearing(); base.OnSizeAllocated(width, height);
Task.Run(async () => pageWidth = width - margin * 2;
if (!loaded)
{
loaded = true;
IsRefreshing = true;
}
else if (Flowers?.Length > 0)
{
DoInitSize();
foreach (var item in Flowers)
{
DoResizeItem(item);
}
}
}
private void DoInitSize()
{
ys = new double[cols];
yIndex = 0;
itemWidth = (int)(pageWidth / cols) - margin * (cols - 1) / 2;
emptyHeight = (int)(itemWidth * AppResources.EmptySize.Height / AppResources.EmptySize.Width);
}
private void DoResizeItem(FlowerClientItem item)
{
int height;
if (item.Width > 0 && item.Height > 0)
{
height = itemWidth * item.Height.Value / item.Width.Value;
}
else
{
height = emptyHeight;
}
height += 36;
double yMin = double.MaxValue;
for (var i = 0; i < cols; i++)
{
if (ys[i] < yMin)
{
yMin = ys[i];
yIndex = i;
}
}
ys[yIndex] += height + margin;
item.Bounds = new Rect(
yIndex,
yMin,
itemWidth,
height);
}
private async void DoRefreshSquare()
{ {
try try
{ {
var list = await database.GetFlowers(); var result = await FetchAsync<FlowerResult>("api/flower/latest?photo=true");
logger.LogInformation("got {count} flowers.", list.Length); if (result?.Count > 0)
{
DoInitSize();
var flowers = result.Flowers.Select(f =>
{
var item = new FlowerClientItem(f);
if (f.Photos?.Length > 0 && f.Photos[0] is PhotoItem cover)
{
item.Cover = new UriImageSource { Uri = new Uri($"{Constants.BaseUrl}/{cover.Url}") };
}
else
{
item.Cover = "empty_flower.jpg";
}
DoResizeItem(item);
return item;
});
logger.LogInformation("got {count} flowers.", result.Count);
Flowers = flowers.ToArray();
}
else
{
Flowers = Array.Empty<FlowerClientItem>();
logger.LogInformation("no flowers.");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError("error occurs in MainPage, {exception}", ex); await AlertError(L("failedGetFlowers", "Failed to get flowers, please try again."));
logger.LogError("error occurs in HomePage, {exception}", ex);
}
finally
{
IsRefreshing = false;
} }
});
} }
//private void OnCounterClicked(object sender, EventArgs e) private void RefreshView_Refreshing(object sender, EventArgs e)
//{ {
// count++; Task.Run(DoRefreshSquare);
}
// if (count == 1) }
// CounterBtn.Text = $"Clicked {count} time";
// else public record FlowerResult
// CounterBtn.Text = $"Clicked {count} times"; {
public FlowerItem[] Flowers { get; init; } = null!;
// SemanticScreenReader.Announce(CounterBtn.Text);
//} public int Count { get; init; }
} }

View File

@ -1,6 +1,5 @@
using Blahblah.FlowerApp.Controls; using Blahblah.FlowerApp.Data;
using Blahblah.FlowerApp.Data; using CommunityToolkit.Maui;
using Blahblah.FlowerApp.Handlers;
#if DEBUG #if DEBUG
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
#endif #endif
@ -14,6 +13,7 @@ public static class MauiProgram
var builder = MauiApp.CreateBuilder(); var builder = MauiApp.CreateBuilder();
builder builder
.UseMauiApp<App>() .UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts => .ConfigureFonts(fonts =>
{ {
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
@ -21,14 +21,13 @@ public static class MauiProgram
}) })
.ConfigureMauiHandlers(handlers => .ConfigureMauiHandlers(handlers =>
{ {
handlers.AddHandler<WaterfallLayout, WaterfallLayoutHandler>();
}); });
#if DEBUG #if DEBUG
builder.Logging.AddDebug(); builder.Logging.AddDebug();
#endif #endif
builder.Services.AddSingleton<MainPage>(); builder.Services.AddSingleton<HomePage>();
builder.Services.AddSingleton<FlowerDatabase>(); builder.Services.AddSingleton<FlowerDatabase>();
builder.Services.AddLocalization(); builder.Services.AddLocalization();

View File

@ -1,125 +0,0 @@
using CoreGraphics;
using Foundation;
using UIKit;
namespace Blahblah.FlowerApp.Platforms.iOS.Controls;
public class FlowLayout : UICollectionViewFlowLayout
{
public CalculateCellHeightHandler? CalculateCellHeight { get; set; }
private readonly List<UICollectionViewLayoutAttributes> layoutAttributes = new();
private CGSize oldBounds;
private bool dirty;
private int cols;
private nfloat[]? yArray;
private nfloat maxHeight;
public FlowLayout(int cols = 2)
{
SetCols(cols);
}
public void SetCols(int cols)
{
if (this.cols != cols)
{
dirty = true;
this.cols = cols;
}
}
public void Invalidate()
{
dirty = true;
}
private void Clean()
{
dirty = false;
yArray = new nfloat[cols];
maxHeight = 0f;
layoutAttributes.Clear();
}
public override CGSize CollectionViewContentSize => new(CollectionView.Bounds.Width, maxHeight);
public override bool ShouldInvalidateLayoutForBoundsChange(CGRect newBounds)
{
return newBounds.Width != CollectionView.Bounds.Width;
}
public override void PrepareLayout()
{
base.PrepareLayout();
var bounds = CollectionView.Bounds.Size;
if (dirty || oldBounds.Width != bounds.Width)
{
oldBounds = bounds;
Clean();
}
var sectionLeft = SectionInset.Left;
var sectionTop = SectionInset.Top;
var minSpacing = MinimumInteritemSpacing;
var itemWidth = (bounds.Width - sectionLeft - SectionInset.Right - minSpacing * (cols - 1)) / cols;
var itemCount = CollectionView.NumberOfItemsInSection(0);
for (nint i = layoutAttributes.Count; i < itemCount; i++)
{
var indexPath = NSIndexPath.FromItemSection(i, 0);
var attr = UICollectionViewLayoutAttributes.CreateForCell(indexPath);
var itemHeight = CalculateCellHeight?.Invoke(indexPath, itemWidth) ?? 20;
nfloat value = nfloat.MaxValue;
int minHeightIndex = 0;
if (yArray?.Length >= cols)
{
for (var n = 0; n < cols; n++)
{
if (yArray[n] < value)
{
value = yArray[n];
minHeightIndex = n;
}
}
}
var itemY = value;
if (itemY < sectionTop)
{
itemY = sectionTop;
}
if (i >= cols)
{
itemY += minSpacing;
}
var itemX = sectionLeft + (itemWidth + minSpacing) * minHeightIndex;
attr.Frame = new CGRect(itemX, itemY, itemWidth, itemHeight);
layoutAttributes.Add(attr);
if (yArray != null)
{
yArray[minHeightIndex] = itemY + itemHeight;
}
}
nfloat y = 0f;
if (yArray != null)
{
for (var i = 0; i < yArray.Length; i++)
{
if (yArray[i] > y)
{
y = yArray[i];
}
}
}
maxHeight = y + SectionInset.Bottom;
}
public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
{
return layoutAttributes.Where(a => a.Frame.IntersectsWith(rect)).ToArray();
}
}
public delegate nfloat CalculateCellHeightHandler(NSIndexPath indexPath, nfloat itemWidth);

View File

@ -1,86 +0,0 @@
using Blahblah.FlowerApp.Controls;
using Foundation;
using UIKit;
namespace Blahblah.FlowerApp.Platforms.iOS.Controls;
public class MauiWaterfallLayout : UIView, IUICollectionViewDataSource, IUICollectionViewDelegate
{
WaterfallLayout? layout;
UICollectionView? collectionView;
public MauiWaterfallLayout(WaterfallLayout layout)
{
this.layout = layout;
var flow = new FlowLayout(layout.Columns)
{
//CalculateCellHeight =
MinimumInteritemSpacing = 12f,
SectionInset = new UIEdgeInsets(12f, 12f, 12f, 12f)
};
var refreshControl = new UIRefreshControl
{
TintColor = UIColor.SystemFill
};
refreshControl.ValueChanged += (sender, e) =>
{
if (sender is UIRefreshControl control)
{
control.EndRefreshing();
}
};
collectionView = new UICollectionView(Bounds, flow)
{
DataSource = this,
Delegate = this,
RefreshControl = refreshControl,
TranslatesAutoresizingMaskIntoConstraints = false
};
AddSubview(collectionView);
var safe = SafeAreaLayoutGuide;
AddConstraints(new[]
{
NSLayoutConstraint.Create(collectionView, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, safe, NSLayoutAttribute.Leading, 1f, 0f),
NSLayoutConstraint.Create(collectionView, NSLayoutAttribute.Trailing, NSLayoutRelation.Equal, safe, NSLayoutAttribute.Trailing, 1f, 0f),
NSLayoutConstraint.Create(collectionView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, safe, NSLayoutAttribute.Top, 1f, 0f),
NSLayoutConstraint.Create(collectionView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, safe, NSLayoutAttribute.Bottom, 1f, 0f),
});
}
public UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
return new UICollectionViewCell();
}
public nint GetItemsCount(UICollectionView collectionView, nint section)
{
return 5;
}
public void UpdateColumns()
{
if (layout != null && collectionView?.CollectionViewLayout is FlowLayout flow)
{
flow.SetCols(layout.Columns);
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (collectionView != null)
{
collectionView.Dispose();
collectionView = null;
}
layout = null;
}
base.Dispose(disposing);
}
}

View File

@ -1,26 +0,0 @@
using Blahblah.FlowerApp.Controls;
using Blahblah.FlowerApp.Platforms.iOS.Controls;
using Microsoft.Maui.Handlers;
namespace Blahblah.FlowerApp.Handlers;
partial class WaterfallLayoutHandler : ViewHandler<WaterfallLayout, MauiWaterfallLayout>
{
static void MapColumns(WaterfallLayoutHandler handler, WaterfallLayout layout)
{
handler.PlatformView?.UpdateColumns();
}
protected override MauiWaterfallLayout CreatePlatformView() => new MauiWaterfallLayout(VirtualView);
protected override void ConnectHandler(MauiWaterfallLayout platformView)
{
base.ConnectHandler(platformView);
}
protected override void DisconnectHandler(MauiWaterfallLayout platformView)
{
platformView.Dispose();
base.DisconnectHandler(platformView);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View File

@ -103,10 +103,10 @@ public enum EventTypes
/// <summary> /// <summary>
/// 事件类 /// 事件类
/// </summary> /// </summary>
/// <param name="Key">名称</param> /// <param name="Name">名称</param>
/// <param name="Description">描述</param> /// <param name="Description">描述</param>
/// <param name="Unique">是否唯一</param> /// <param name="Unique">是否唯一</param>
public record Event(string Key, string? Description = null, bool Unique = false) : NamedItem(Key, Description) public record Event(string Name, string? Description = null, bool Unique = false) : NamedItem(Name, Description)
{ {
/// <summary> /// <summary>
/// 是否唯一 /// 是否唯一
@ -118,14 +118,14 @@ public record Event(string Key, string? Description = null, bool Unique = false)
/// <summary> /// <summary>
/// 命名对象类 /// 命名对象类
/// </summary> /// </summary>
/// <param name="Key">名称</param> /// <param name="Name">名称</param>
/// <param name="Description">描述</param> /// <param name="Description">描述</param>
public record NamedItem(string Key, string? Description = null) public record NamedItem(string Name, string? Description = null)
{ {
/// <summary> /// <summary>
/// 名称 /// 名称
/// </summary> /// </summary>
public string Key { get; init; } = Key; public string Name { get; init; } = Name;
/// <summary> /// <summary>
/// 描述 /// 描述

View File

@ -52,6 +52,7 @@ public partial class ApiController : BaseController
{ {
return Ok(new DefinitionResult return Ok(new DefinitionResult
{ {
ApiVersion = Program.Version,
Categories = Constants.Categories, Categories = Constants.Categories,
Events = Constants.Events, Events = Constants.Events,
}); });

View File

@ -5,6 +5,11 @@
/// </summary> /// </summary>
public record DefinitionResult public record DefinitionResult
{ {
/// <summary>
/// API 版本号
/// </summary>
public required string ApiVersion { get; init; }
/// <summary> /// <summary>
/// 花草分类 /// 花草分类
/// </summary> /// </summary>

View File

@ -218,6 +218,7 @@ public abstract partial class BaseController : ControllerBase
using var ms = new MemoryStream(); using var ms = new MemoryStream();
ms.Write(headers, 0, count); ms.Write(headers, 0, count);
#if __OLD_READER__
// reading // reading
const int size = 16384; const int size = 16384;
var buffer = new byte[size]; var buffer = new byte[size];
@ -225,17 +226,24 @@ public abstract partial class BaseController : ControllerBase
{ {
ms.Write(buffer, 0, count); ms.Write(buffer, 0, count);
} }
#else
stream.CopyTo(ms);
#endif
var data = ms.ToArray(); var data = ms.ToArray();
var name = file.FileName; var name = file.FileName;
var ext = Path.GetExtension(name); var ext = Path.GetExtension(name);
var path = $"{WebUtility.UrlEncode(Path.GetFileNameWithoutExtension(name))}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}"; var path = $"{WebUtility.UrlEncode(Path.GetFileNameWithoutExtension(name))}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}";
var image = SkiaSharp.SKImage.FromEncodedData(data);
return new FileResult return new FileResult
{ {
Filename = name, Filename = name,
FileType = ext, FileType = ext,
Path = path, Path = path,
Content = data Content = data,
Width = image.Width,
Height = image.Height
}; };
} }
} }
@ -364,30 +372,20 @@ public record FileResult
/// <summary> /// <summary>
/// 储存路径 /// 储存路径
/// </summary> /// </summary>
public required string Path { get; set; } public required string Path { get; init; }
/// <summary> /// <summary>
/// 文件内容 /// 文件内容
/// </summary> /// </summary>
[Required]
public required byte[] Content { get; init; } public required byte[] Content { get; init; }
}
/// <summary>
/// 照片宽度
/// <summary> /// </summary>
/// 照片参数 public required int Width { get; init; }
/// </summary>
public record PhotoParameter /// <summary>
{ /// 照片高度
/// <summary> /// </summary>
/// 花草 id public required int Height { get; init; }
/// </summary>
[Required]
public int Id { get; set; }
/// <summary>
/// 封面照片
/// </summary>
[Required]
public required IFormFile Photo { get; set; }
} }

View File

@ -88,6 +88,6 @@ partial class BaseController
/// <returns></returns> /// <returns></returns>
protected int AddPhotoItem(PhotoItem item) protected int AddPhotoItem(PhotoItem item)
{ {
return database.Database.ExecuteSql($"INSERT INTO \"photos\"(\"fid\",\"rid\",\"filetype\",\"filename\",\"path\",\"dateupload\") VALUES({item.FlowerId},{item.RecordId},{item.FileType},{item.FileName},{item.Path},{item.DateUploadUnixTime})"); return database.Database.ExecuteSql($"INSERT INTO \"photos\"(\"fid\",\"uid\",\"rid\",\"filetype\",\"filename\",\"path\",\"dateupload\",\"width\",\"height\") VALUES({item.FlowerId},{item.OwnerId},{item.RecordId},{item.FileType},{item.FileName},{item.Path},{item.DateUploadUnixTime},{item.Width},{item.Height})");
} }
} }

View File

@ -279,7 +279,9 @@ public class EventApiController : BaseController
FileType = file.FileType, FileType = file.FileType,
FileName = file.Filename, FileName = file.Filename,
Path = file.Path, Path = file.Path,
DateUploadUnixTime = now DateUploadUnixTime = now,
Width = file.Width,
Height = file.Height
}; };
AddPhotoItem(p); AddPhotoItem(p);
@ -402,7 +404,9 @@ public class EventApiController : BaseController
FileType = file.FileType, FileType = file.FileType,
FileName = file.Filename, FileName = file.Filename,
Path = file.Path, Path = file.Path,
DateUploadUnixTime = now DateUploadUnixTime = now,
Width = file.Width,
Height = file.Height
}; };
AddPhotoItem(cover); AddPhotoItem(cover);
@ -498,7 +502,9 @@ public class EventApiController : BaseController
FileType = file.FileType, FileType = file.FileType,
FileName = file.Filename, FileName = file.Filename,
Path = file.Path, Path = file.Path,
DateUploadUnixTime = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() DateUploadUnixTime = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
Width = file.Width,
Height = file.Height
}; };
AddPhotoItem(p); AddPhotoItem(p);
@ -594,7 +600,9 @@ public class EventApiController : BaseController
FileType = file.FileType, FileType = file.FileType,
FileName = file.Filename, FileName = file.Filename,
Path = file.Path, Path = file.Path,
DateUploadUnixTime = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() DateUploadUnixTime = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
Width = file.Width,
Height = file.Height
}; };
AddPhotoItem(p); AddPhotoItem(p);

View File

@ -518,7 +518,9 @@ public class FlowerApiController : BaseController
FileType = file.FileType, FileType = file.FileType,
FileName = file.Filename, FileName = file.Filename,
Path = file.Path, Path = file.Path,
DateUploadUnixTime = now DateUploadUnixTime = now,
Width = file.Width,
Height = file.Height
}; };
AddPhotoItem(cover); AddPhotoItem(cover);
@ -639,7 +641,9 @@ public class FlowerApiController : BaseController
FileType = file.FileType, FileType = file.FileType,
FileName = file.Filename, FileName = file.Filename,
Path = file.Path, Path = file.Path,
DateUploadUnixTime = now DateUploadUnixTime = now,
Width = file.Width,
Height = file.Height
}; };
database.Photos.Add(item); database.Photos.Add(item);
SaveDatabase(); SaveDatabase();
@ -776,7 +780,9 @@ public class FlowerApiController : BaseController
FileType = file.FileType, FileType = file.FileType,
FileName = file.Filename, FileName = file.Filename,
Path = file.Path, Path = file.Path,
DateUploadUnixTime = now DateUploadUnixTime = now,
Width = file.Width,
Height = file.Height
}; };
AddPhotoItem(cover); AddPhotoItem(cover);
@ -893,7 +899,9 @@ public class FlowerApiController : BaseController
FileType = file.FileType, FileType = file.FileType,
FileName = file.Filename, FileName = file.Filename,
Path = file.Path, Path = file.Path,
DateUploadUnixTime = now DateUploadUnixTime = now,
Width = file.Width,
Height = file.Height
}; };
AddPhotoItem(cover); AddPhotoItem(cover);

View File

@ -94,6 +94,18 @@ public class PhotoItem
[JsonIgnore] [JsonIgnore]
public DateTimeOffset DateUpload => DateTimeOffset.FromUnixTimeMilliseconds(DateUploadUnixTime); public DateTimeOffset DateUpload => DateTimeOffset.FromUnixTimeMilliseconds(DateUploadUnixTime);
/// <summary>
/// 图片宽度
/// </summary>
[Column("width")]
public int? Width { get; set; }
/// <summary>
/// 图片高度
/// </summary>
[Column("height")]
public int? Height { get; set; }
/// <summary> /// <summary>
/// 前端显示的 URL /// 前端显示的 URL
/// </summary> /// </summary>

View File

@ -0,0 +1,353 @@
// <auto-generated />
using System;
using Blahblah.FlowerStory.Server.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Blahblah.FlowerStory.Server.Migrations
{
[DbContext(typeof(FlowerDatabase))]
[Migration("20230724084100_Add-Photo-Size")]
partial class AddPhotoSize
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.9");
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("fid");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER")
.HasColumnName("categoryid");
b.Property<decimal?>("Cost")
.HasColumnType("real")
.HasColumnName("cost");
b.Property<long>("DateBuyUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("datebuy")
.HasAnnotation("Relational:JsonPropertyName", "dateBuy");
b.Property<double?>("Latitude")
.HasColumnType("REAL")
.HasColumnName("latitude");
b.Property<double?>("Longitude")
.HasColumnType("REAL")
.HasColumnName("longitude");
b.Property<string>("Memo")
.HasColumnType("TEXT")
.HasColumnName("memo");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<string>("Purchase")
.HasColumnType("TEXT")
.HasColumnName("purchase");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.ToTable("flowers");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.PhotoItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("pid");
b.Property<long>("DateUploadUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("dateupload")
.HasAnnotation("Relational:JsonPropertyName", "dateUpload");
b.Property<string>("FileName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("filename");
b.Property<string>("FileType")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("filetype");
b.Property<int?>("FlowerId")
.HasColumnType("INTEGER")
.HasColumnName("fid");
b.Property<int?>("Height")
.HasColumnType("INTEGER")
.HasColumnName("height");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<int?>("RecordId")
.HasColumnType("INTEGER")
.HasColumnName("rid");
b.Property<int?>("Width")
.HasColumnType("INTEGER")
.HasColumnName("width");
b.HasKey("Id");
b.HasIndex("FlowerId");
b.HasIndex("OwnerId");
b.HasIndex("RecordId");
b.ToTable("photos");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("rid");
b.Property<int?>("ByUserId")
.HasColumnType("INTEGER")
.HasColumnName("byuid");
b.Property<string>("ByUserName")
.HasColumnType("TEXT")
.HasColumnName("byname");
b.Property<long>("DateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("date")
.HasAnnotation("Relational:JsonPropertyName", "date");
b.Property<int>("EventId")
.HasColumnType("INTEGER")
.HasColumnName("eid");
b.Property<int>("FlowerId")
.HasColumnType("INTEGER")
.HasColumnName("fid");
b.Property<double?>("Latitude")
.HasColumnType("REAL")
.HasColumnName("latitude");
b.Property<double?>("Longitude")
.HasColumnType("REAL")
.HasColumnName("longitude");
b.Property<string>("Memo")
.HasColumnType("TEXT")
.HasColumnName("memo");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.HasKey("Id");
b.HasIndex("FlowerId");
b.HasIndex("OwnerId");
b.ToTable("records");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.TokenItem", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("tid");
b.Property<long>("ActiveDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("activedate")
.HasAnnotation("Relational:JsonPropertyName", "activeDate");
b.Property<string>("ClientAgent")
.HasColumnType("TEXT")
.HasColumnName("clientagent");
b.Property<string>("ClientApp")
.HasColumnType("TEXT")
.HasColumnName("clientapp");
b.Property<string>("DeviceId")
.HasColumnType("TEXT")
.HasColumnName("deviceid");
b.Property<long>("ExpireDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("expiredate")
.HasAnnotation("Relational:JsonPropertyName", "expireDate");
b.Property<int>("ExpireSeconds")
.HasColumnType("INTEGER")
.HasColumnName("expiresecs");
b.Property<long>("LogonDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("logondate")
.HasAnnotation("Relational:JsonPropertyName", "logonDate");
b.Property<int>("UserId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<string>("VerifyCode")
.HasColumnType("TEXT")
.HasColumnName("verifycode");
b.HasKey("Id");
b.ToTable("tokens");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.UserItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<long?>("ActiveDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("activedate");
b.Property<byte[]>("Avatar")
.HasColumnType("BLOB")
.HasColumnName("avatar");
b.Property<string>("Email")
.HasColumnType("TEXT")
.HasColumnName("email");
b.Property<int>("Level")
.HasColumnType("INTEGER")
.HasColumnName("level");
b.Property<string>("Mobile")
.HasColumnType("TEXT")
.HasColumnName("mobile");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("password");
b.Property<long>("RegisterDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("regdate")
.HasAnnotation("Relational:JsonPropertyName", "registerDate");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("id");
b.HasKey("Id");
b.ToTable("users");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b =>
{
b.HasOne("Blahblah.FlowerStory.Server.Data.Model.UserItem", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.PhotoItem", b =>
{
b.HasOne("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", "Flower")
.WithMany("Photos")
.HasForeignKey("FlowerId");
b.HasOne("Blahblah.FlowerStory.Server.Data.Model.UserItem", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Blahblah.FlowerStory.Server.Data.Model.RecordItem", "Record")
.WithMany("Photos")
.HasForeignKey("RecordId");
b.Navigation("Flower");
b.Navigation("Owner");
b.Navigation("Record");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b =>
{
b.HasOne("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", "Flower")
.WithMany()
.HasForeignKey("FlowerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Blahblah.FlowerStory.Server.Data.Model.UserItem", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Flower");
b.Navigation("Owner");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b =>
{
b.Navigation("Photos");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b =>
{
b.Navigation("Photos");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Blahblah.FlowerStory.Server.Migrations
{
/// <inheritdoc />
public partial class AddPhotoSize : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "height",
table: "photos",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "width",
table: "photos",
type: "INTEGER",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "height",
table: "photos");
migrationBuilder.DropColumn(
name: "width",
table: "photos");
}
}
}

View File

@ -15,7 +15,7 @@ namespace Blahblah.FlowerStory.Server.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); modelBuilder.HasAnnotation("ProductVersion", "7.0.9");
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b => modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b =>
{ {
@ -95,6 +95,10 @@ namespace Blahblah.FlowerStory.Server.Migrations
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasColumnName("fid"); .HasColumnName("fid");
b.Property<int?>("Height")
.HasColumnType("INTEGER")
.HasColumnName("height");
b.Property<int>("OwnerId") b.Property<int>("OwnerId")
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasColumnName("uid"); .HasColumnName("uid");
@ -108,6 +112,10 @@ namespace Blahblah.FlowerStory.Server.Migrations
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasColumnName("rid"); .HasColumnName("rid");
b.Property<int?>("Width")
.HasColumnType("INTEGER")
.HasColumnName("width");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("FlowerId"); b.HasIndex("FlowerId");

View File

@ -11,7 +11,7 @@ public class Program
/// <inheritdoc/> /// <inheritdoc/>
public const string ProjectName = "Flower Story"; public const string ProjectName = "Flower Story";
/// <inheritdoc/> /// <inheritdoc/>
public const string Version = "0.6.713"; public const string Version = "0.7.727";
/// <inheritdoc/> /// <inheritdoc/>
public static void Main(string[] args) public static void Main(string[] args)

View File

@ -11,12 +11,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.9">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="SkiaSharp" Version="2.88.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup> </ItemGroup>