Editing pics and other changes

This commit is contained in:
Gordon Pedersen 2024-06-07 14:25:21 +10:00
parent bacd83fe07
commit 0e1f734f9f
10 changed files with 242 additions and 29 deletions

View file

@ -0,0 +1,64 @@
@inject IJSRuntime JS
@inject State State
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">
<img src="@Pic.Url" />
<div class="row">
<div class="field textarea label border max">
<InputTextArea @bind-Value="Description"></InputTextArea>
<label>Description</label>
</div>
</div>
<nav class="right-align no-space">
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
<button @onclick="PostPic" disabled="@loading">
@if (loading) {
<span>Saving...</span>
}
else {
<i class="fa-solid fa-floppy-disk"></i> <span>Save</span>
}
</button>
</nav>
</dialog>
@code {
private Pic? _pic;
[Parameter]
public Pic? Pic {
get => _pic;
set {
_pic = value;
Description = _pic?.Description;
}
}
public string? Description { get; set; }
private bool loading = false;
[Parameter]
public string id { get; set; }
protected override async Task OnInitializedAsync() {
Description = Pic?.Description;
}
public async Task PostPic() {
loading = true;
await InvokeAsync(StateHasChanged);
RestService api = new RestService();
if(!string.IsNullOrEmpty(Pic.Id)) {
await api.PostPicDescription(State.SelectedAddressName, Pic.Id, Description);
await State.RefreshPics();
await InvokeAsync(StateHasChanged);
}
await JS.InvokeVoidAsync("ui", "#" + id);
// clear input
Description = string.Empty;
Pic = null;
loading = false;
await InvokeAsync(StateHasChanged);
}
}

View file

@ -5,15 +5,18 @@
<dialog id="@id"> <dialog id="@id">
<h5>Share a picture</h5> <h5>Share a picture</h5>
<div class="row"> <div class="row">
<div class="field label prefix border"> <button @onclick="PicFromMedia"><i class="fa-solid fa-image"></i> Select a picture</button>
<button @onclick="PicFromPhoto"><i class="fa-solid fa-camera"></i> Take a photo</button>
@* <div class="field label prefix border">
<i class="fa-solid fa-image"></i> <i class="fa-solid fa-image"></i>
<InputFile OnChange="@ChangeFile" accept="image/gif, image/heic, image/heif, image/jpeg, image/png, image/svg+xml, image/webp"></InputFile> <InputFile OnChange="@ChangeFile" accept="image/gif, image/heic, image/heif, image/jpeg, image/png, image/svg+xml, image/webp"></InputFile>
<input type="text"> <input type="text">
<label>Select a picture</label> <label>Select a picture</label>
</div> </div> *@
@if(File != null){ @if(File != null && Base64File != null && FileSize != null){
<img class="extra" src="@Base64Url">
<small> <small>
@File.ContentType (@formatSizeUnits(File.Size)) @File.ContentType (@formatSizeUnits(FileSize))
</small> </small>
} }
@ -38,7 +41,17 @@
</dialog> </dialog>
@code { @code {
private IBrowserFile? File { get; set; } // private IBrowserFile? File { get; set; }
private FileResult? File { get; set; }
private string? Base64File { get; set; }
private long? FileSize { get; set; }
private string? Base64Url {
get {
if(File == null || Base64File == null) return null;
return $"data:{File.ContentType};base64,{Base64File}";
}
}
private string Description { get; set; } private string Description { get; set; }
private bool loading = false; private bool loading = false;
[Parameter] [Parameter]
@ -65,11 +78,12 @@
} }
private async Task ChangeFile(InputFileChangeEventArgs e){ // private async Task ChangeFile(InputFileChangeEventArgs e){
File = e.File; // File = e.File;
} // }
private string formatSizeUnits(long bytes){ private string formatSizeUnits(long? bytes){
if(bytes == null) return "?? bytes";
string formatted = "0 bytes"; string formatted = "0 bytes";
if (bytes >= 1073741824) { formatted = $"{(bytes / 1073741824):.##} GB"; } if (bytes >= 1073741824) { formatted = $"{(bytes / 1073741824):.##} GB"; }
else if (bytes >= 1048576) { formatted = $"{(bytes / 1048576):.##} MB"; } else if (bytes >= 1048576) { formatted = $"{(bytes / 1048576):.##} MB"; }
@ -79,4 +93,21 @@
return formatted; return formatted;
} }
private async Task PicFromMedia(EventArgs e) {
File = await MediaPicker.Default.PickPhotoAsync();
await PopulateFileDetails();
}
private async Task PicFromPhoto(EventArgs e) {
File = await MediaPicker.Default.CapturePhotoAsync();
await PopulateFileDetails();
}
private async Task PopulateFileDetails() {
FileSize = await State.FileSize(File);
Base64File = await State.Base64FromFile(File);
}
} }

View file

@ -34,7 +34,10 @@
</div> </div>
<div id="pics" class="page padding"> <div id="pics" class="page padding">
<PicList PicsFunc="@State.VirtualPicsFunc(Address)"></PicList> @if(Editable){
<EditPicDialog @ref="editPicDialog" id="EditPicModal"></EditPicDialog>
}
<PicList PicsFunc="@State.VirtualPicsFunc(Address)" Editable="@Editable" Dialog="@editPicDialog"></PicList>
</div> </div>
</div> </div>
@ -42,6 +45,12 @@
[Parameter] [Parameter]
public string Address { get; set; } public string Address { get; set; }
private EditPicDialog? editPicDialog { get; set; }
private bool Editable {
get => Address == State.SelectedAddressName;
}
private MarkupString? bio; private MarkupString? bio;
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {

View file

@ -19,7 +19,7 @@
</Authorized> </Authorized>
</AuthorizeView> </AuthorizeView>
<div id="pics" class="responsive"> <div id="pics" class="responsive card-grid">
<PicList PicsFunc="@State.VirtualPicsFunc()"></PicList> <PicList PicsFunc="@State.VirtualPicsFunc()"></PicList>
</div> </div>

50
Components/PicCard.razor Normal file
View file

@ -0,0 +1,50 @@
@inject IJSRuntime JS
<article class="no-padding">
<img src="@Pic.Url">
<div class="padding">
<nav>
<a class="author" href="/person/@Pic.Address#pics">
<i class="fa-solid fa-fw fa-at"></i>@Pic.Address
</a>
<span class="max"></span>
<a class="chip transparent-border right">
<i class="fa fa-clock"></i> @Pic.RelativeTime
</a>
</nav>
<p>@Pic.Description</p>
<nav>
<div class="max"></div>
@if(Editable) {
<button @onclick="EditPic"><i class="fa-solid fa-pencil"></i> Edit</button>
}
<button class="transparent circle" @onclick="ShareClick">
<i class="fa-solid fa-share-nodes"></i>
</button>
</nav>
</div>
</article>
@code {
[Parameter]
public Pic Pic {get; set;}
[Parameter]
public bool Editable { get; set; } = false;
[Parameter]
public EditPicDialog? Dialog { get; set; }
private async Task EditPic(EventArgs e){
Dialog.Pic = Pic;
// await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("ui", "#" + Dialog?.id);
}
public async Task ShareClick(EventArgs e){
await Share.Default.RequestAsync(new ShareTextRequest{
Uri = Pic.Url,
Text = Pic.Description,
Title = "I saw this on some.pics",
Subject = "I saw this on some.pics"
});
}
}

View file

@ -1,29 +1,20 @@
<Virtualize ItemsProvider="GetPics" Context="pic" ItemSize="180"> @inject IJSRuntime JS
<Virtualize ItemsProvider="GetPics" Context="pic" ItemSize="500">
<ItemContent> <ItemContent>
<article class="no-padding center"> <PicCard Pic="@pic" Editable="@Editable" Dialog="@Dialog"></PicCard>
<img class="responsive" src="@pic.Url">
<div class="padding">
<nav>
<a class="author" href="/person/@pic.Address#pics">
<i class="fa-solid fa-fw fa-at"></i>@pic.Address
</a>
<span class="max"></span>
<a class="chip transparent-border right">
<i class="fa fa-clock"></i> @pic.RelativeTime
</a>
</nav>
<p>@pic.Description</p>
</div>
</article>
</ItemContent> </ItemContent>
<Placeholder> <Placeholder>
<article class="no-padding center"></article> <article class="no-padding center" style="min-height:500px;"></article>
</Placeholder> </Placeholder>
</Virtualize> </Virtualize>
@code { @code {
[Parameter] [Parameter]
public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<Pic>>> PicsFunc { get; set; } public Func<ItemsProviderRequest, ValueTask<ItemsProviderResult<Pic>>> PicsFunc { get; set; }
[Parameter]
public bool Editable { get; set; } = false;
[Parameter]
public EditPicDialog? Dialog { get; set; }
private async ValueTask<ItemsProviderResult<Pic>> GetPics(ItemsProviderRequest request) { private async ValueTask<ItemsProviderResult<Pic>> GetPics(ItemsProviderRequest request) {
return await this.PicsFunc(request); return await this.PicsFunc(request);

View file

@ -15,10 +15,22 @@
<a class="chip transparent-border" href="@status.ExternalUrl" target="_blank"> <a class="chip transparent-border" href="@status.ExternalUrl" target="_blank">
<i class="fa fa-message-dots"></i> Respond <i class="fa fa-message-dots"></i> Respond
</a> </a>
<div class="max"></div>
<button class="transparent circle" @onclick="ShareClick">
<i class="fa-solid fa-share-nodes"></i>
</button>
</nav> </nav>
</article> </article>
@code { @code {
[Parameter] [Parameter]
public Status status { get; set; } public Status status { get; set; }
public async Task ShareClick(EventArgs e){
await Share.Default.RequestAsync(new ShareTextRequest{
Text = $"{status.Content}\n- from [@{status.Address}]({status.Url})",
Title = "I saw this on status.lol",
Subject = "I saw this on status.lol"
});
}
} }

View file

@ -117,8 +117,8 @@ namespace Neighbourhood.omg.lol.Models {
} }
public async Task<List<Pic>?> GetPics(bool forceRefresh = false) { public async Task<List<Pic>?> GetPics(bool forceRefresh = false) {
RestService api = new RestService();
if(forceRefresh || this.Pics == null || this.Pics.Count == 0) { if(forceRefresh || this.Pics == null || this.Pics.Count == 0) {
RestService api = new RestService();
this.Pics = await api.SomePics(); this.Pics = await api.SomePics();
} }
return this.Pics; return this.Pics;
@ -147,6 +147,19 @@ namespace Neighbourhood.omg.lol.Models {
} }
} }
public async Task<long> FileSize(FileResult file) {
using var fileStream = await file.OpenReadAsync();
return fileStream.Length;
}
public async Task<string> Base64FromFile(FileResult file) {
using var memoryStream = new MemoryStream();
using var fileStream = await file.OpenReadAsync();
await fileStream.CopyToAsync(memoryStream);
byte[] bytes = memoryStream.ToArray();
return Convert.ToBase64String(bytes);
}
public async Task RefreshStatuses() => await GetStatuses(forceRefresh: true); public async Task RefreshStatuses() => await GetStatuses(forceRefresh: true);
public async Task RefreshPics() => await GetPics(forceRefresh: true); public async Task RefreshPics() => await GetPics(forceRefresh: true);
} }

View file

@ -132,6 +132,16 @@ namespace Neighbourhood.omg.lol {
return await PutPic(address, bytes); return await PutPic(address, bytes);
} }
public async Task<PutPicResponseData?> PutPic(string address, FileResult file) {
byte[] bytes;
using var memoryStream = new MemoryStream();
using var fileStream = await file.OpenReadAsync();
await fileStream.CopyToAsync(memoryStream);
bytes = memoryStream.ToArray();
return await PutPic(address, bytes);
}
public async Task<PutPicResponseData?> PutPic(string address, byte[] bytes) => public async Task<PutPicResponseData?> PutPic(string address, byte[] bytes) =>
await PutPic(address, Convert.ToBase64String(bytes)); await PutPic(address, Convert.ToBase64String(bytes));

View file

@ -198,3 +198,36 @@ article.ephemeral {
.tabs a { .tabs a {
text-decoration:none; text-decoration:none;
} }
.card-grid{
display: flex;
flex-direction:row;
flex-wrap:wrap;
gap: .5rem;
}
#pics article {
margin: 1rem auto;
text-align:center;
}
#pics article>:not(:first-child) {
text-align: left;
}
#pics article nav {
flex-wrap: wrap;
}
#pics.card-grid article{
max-width: 24rem;
}
@media only screen and (max-width: 895px) {
#pics.card-grid article {
max-width: calc(100% - 1rem);
}
}
#pics article > img:first-child {
text-align: center;
}