Edit profile / profile pic

This commit is contained in:
Gordon Pedersen 2024-07-11 15:57:53 +10:00
parent 0fcda98b9f
commit f24ef392f7
43 changed files with 611 additions and 338 deletions

View file

@ -9,7 +9,7 @@ using System.Text.RegularExpressions;
namespace Neighbourhood.omg.lol
{
public class RestService {
public class RestService {
HttpClient _client;
JsonSerializerOptions _serializerOptions;
public const string BaseUrl = "https://api.omg.lol";
@ -25,6 +25,18 @@ namespace Neighbourhood.omg.lol
AddToken(token);
}
public T? Deserialize<T>(string str) {
T? responseObj = default(T);
try {
responseObj = JsonSerializer.Deserialize<T>(str, _serializerOptions);
}
catch (JsonException ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
Debug.WriteLine(str);
}
return responseObj;
}
public void AddToken(string? token = null) {
if (token == null) token = Task.Run(() => SecureStorage.GetAsync("accounttoken")).GetAwaiter().GetResult();
if (token != null) _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
@ -42,8 +54,8 @@ namespace Neighbourhood.omg.lol
string str = await response.Content.ReadAsStringAsync();
try {
OmgLolResponse<T>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<T>>(_serializerOptions, cancellationToken: cancellationToken);
if (responseObj?.Request?.Success ?? false) {
responseData = responseObj.Response;
if (responseObj?.Request == null || (responseObj?.Request?.Success ?? false)) {
responseData = responseObj!.Response;
}
}
catch (JsonException ex) {
@ -78,6 +90,81 @@ namespace Neighbourhood.omg.lol
return responseData;
}
private async Task<TResponse?> PostBinary<TResponse, TData>(string uri, FileResult? fileResult = null, CancellationToken cancellationToken = default) where TResponse : IOmgLolResponseData {
TResponse? responseData = default(TResponse);
try {
if (fileResult != null) using (var fileStream = await fileResult.OpenReadAsync()) {
Uri url = new Uri(_client.BaseAddress?.AbsoluteUri + uri);
if (string.IsNullOrEmpty(url.Query)) uri += "?binary";
else if (!url.Query.Contains("binary")) uri += "&binary";
HttpContent fileStreamContent = new StreamContent(fileStream);
fileStreamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(fileResult.ContentType ?? "application/octet-stream");
fileStreamContent.Headers.ContentLength = fileStream.Length;
HttpResponseMessage response = await _client.PostAsync(uri, fileStreamContent, cancellationToken: cancellationToken);
string str = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode) {
OmgLolResponse<TResponse>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<TResponse>>(_serializerOptions, cancellationToken: cancellationToken);
if (responseObj?.Request?.Success ?? false) {
responseData = responseObj.Response;
}
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return responseData;
}
private async Task<TResponse?> PostMultipart<TResponse, TData>(string uri, TData? data = null, FileResult? fileResult = null, CancellationToken cancellationToken = default)
where TResponse : IOmgLolResponseData where TData : class
{
if(fileResult != null) {
using (var fileStream = await fileResult.OpenReadAsync())
return await PostMultipart<TResponse, TData>(uri, data: data, fileStream: fileStream, fileName: fileResult.FileName, contentType: fileResult.ContentType);
}
else return await PostMultipart<TResponse, TData>(uri, data, fileStream: null);
}
private async Task<TResponse?> PostMultipart<TResponse, TData>(string uri, TData? data = null, Stream? fileStream = null, string? fileName = null, string? contentType = null, CancellationToken cancellationToken = default)
where TResponse : IOmgLolResponseData where TData : class
{
TResponse? responseData = default;
try {
using (MultipartFormDataContent formData = new MultipartFormDataContent()) {
if(fileStream != null) {
HttpContent fileStreamContent = new StreamContent(fileStream);
fileStreamContent.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("form-data") {
Name = "\"file\"",
FileName = $"\"{fileName}\"" ?? "\"unknown\""
};
fileStreamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType ?? "application/octet-stream");
formData.Add(fileStreamContent);
}
if (data != null) {
HttpContent jsonContent = JsonContent.Create(data, options: _serializerOptions);
formData.Add(jsonContent);
}
HttpResponseMessage response = await _client.PostAsync(uri, formData, cancellationToken: cancellationToken);
string str = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode) {
OmgLolResponse<TResponse>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<TResponse>>(_serializerOptions, cancellationToken: cancellationToken);
if (responseObj?.Request?.Success ?? false) {
responseData = responseObj.Response;
}
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return responseData;
}
private async Task<TResponse?> Put<TResponse, TData>(string uri, TData data, CancellationToken cancellationToken = default) where TResponse : IOmgLolResponseData {
TResponse? responseData = default(TResponse);
try {
@ -231,24 +318,14 @@ namespace Neighbourhood.omg.lol
public async Task<BasicResponseData?> PostProfile(string address, PostProfile data) =>
await Post<BasicResponseData, PostProfile>($"/address/{address}/web", data);
public async Task<Dictionary<string, Theme>?> GetThemes() =>
(await Get<ThemeResponseData>($"/theme/list"))?.Themes;
public async Task<List<MarkupString>> EphemeralScrape() {
List<string> notes = new List<string>();
Uri Uri = new Uri($"https://eph.emer.al/");
try {
var response = await _client.GetAsync(Uri);
var str = await response.Content.ReadAsStringAsync();
string pattern = @"<p class=""post"">(.*?)<\/p>";
var matches = Regex.Matches(str, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
foreach (Match match in matches) {
notes.Add(match.Groups[1].Value);
}
}
catch (Exception ex) {
Debug.WriteLine(ex);
}
return notes.Select(s => (MarkupString)s).ToList();
}
public async Task<MarkupString?> GetThemePreview(string theme) =>
(MarkupString)((await Get<ThemePreviewResponseData>($"/theme/{theme}/preview"))?.Html ?? string.Empty);
public async Task<BasicResponseData?> PostProfilePic(string address, FileResult image) =>
await PostBinary<BasicResponseData, object>($"/address/{address}/pfp", fileResult: image);
public async Task<string?> OAuth(string code, string client_id, string client_secret, string redirect_uri) {
string? token = null;

View file

@ -26,6 +26,8 @@ namespace Neighbourhood.omg.lol {
public List<StatusOrPic>? Feed { get; set; }
public Dictionary<string, Theme>? Themes { get; set; }
// Account data
private AccountResponseData? _accountInfo;
public AccountResponseData? AccountInfo {
@ -305,5 +307,12 @@ namespace Neighbourhood.omg.lol {
return Feed.OrderByDescending(s => s.CreatedTime);
}
public async Task<Dictionary<string, Theme>?> GetThemes(bool forceRefresh = false) {
if (forceRefresh || this.Themes == null || this.Themes.Count == 0) {
this.Themes = await api.GetThemes();
}
return this.Themes;
}
}
}

View file

@ -0,0 +1,117 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@inject NavigationManager navigationManager
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active">
<h5>Update your profile picture</h5>
<div class="padding center-align">
<img src="@(Base64Url ?? ExistingUrl)" class="small-height square" />
</div>
<div class="row">
<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>
<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>Uploading...</span>
}
else {
<i class="fa-solid fa-cloud-arrow-up"></i> <span>Upload</span>
}
</button>
</nav>
</dialog>
@code {
[Parameter]
public string? Address { get; set; }
public string ExistingUrl { get => $"https://profiles.cache.lol/{Address ?? ""}/picture"; }
// private IBrowserFile? File { get; set; }
[Parameter]
public string? Base64File { get; set; }
[Parameter]
public long? FileSize { get; set; }
[Parameter]
public string? FileContentType { get; set; }
[Parameter]
public string? id { get; set; }
[Parameter]
public bool Active { get; set; }
private bool loading = false;
private FileResult? File { get; set; }
private string? Base64Url {
get {
if (FileContentType == null || Base64File == null) return null;
return $"data:{FileContentType};base64,{Base64File}";
}
}
public async Task PostPic() {
loading = true;
await InvokeAsync(StateHasChanged);
//TODO: upload the profile pic
//PutPicResponseData? response = await api.PutPic(State.SelectedAddressName!, Base64File!);
if (Base64File != null && File != null)
{
// using var fileStream = await File.OpenReadAsync();
BasicResponseData? response = await api.PostProfilePic(Address!, File);
if (response != null)
{
await JS.InvokeVoidAsync("ui", "#" + id);
// clear input
File = null;
Base64File = null;
FileSize = null;
FileContentType = null;
await JS.InvokeVoidAsync("cacheBust", ExistingUrl);
}
loading = false;
await InvokeAsync(StateHasChanged);
}
}
private string formatSizeUnits(long? bytes) {
if (bytes == null) return "?? bytes";
string formatted = "0 bytes";
if (bytes >= 1073741824) { formatted = $"{(bytes / 1073741824):.##} GB"; }
else if (bytes >= 1048576) { formatted = $"{(bytes / 1048576):.##} MB"; }
else if (bytes >= 1024) { formatted = $"{(bytes / 1024):.##} KB"; }
else if (bytes > 1) { formatted = $"{bytes} bytes"; }
else if (bytes == 1) { formatted = $"{bytes} byte"; }
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() {
if (File == null) {
FileContentType = null;
FileSize = null;
Base64File = null;
}
else {
FileContentType = File.ContentType;
FileSize = await Utilities.FileSize(File);
Base64File = await Utilities.Base64FromFile(File);
}
}
}

View file

@ -10,6 +10,8 @@
public string? Url { get; set; }
[Parameter]
public string? id { get; set; }
[Parameter]
public string? SrcString { get; set; }
public MarkupString? Html { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender) {
@ -21,11 +23,18 @@
public async Task Reload() {
if (Url != null){
Html = await api.GetHtml(Url);
string? HtmlString = Html?.ToString();
HtmlString = HtmlString?.Replace("</head>", "<base target='_blank'></head>");
HtmlString = HtmlString?.Replace("</body>", "<script src='https://cdn.jsdelivr.net/npm/@iframe-resizer/child'></script></body>");
Html = (MarkupString)(HtmlString ?? string.Empty);
SrcString = Html?.ToString();
}
if(SrcString != null) {
SrcString = SrcString?.Replace("</head>", "<base target='_blank'></head>");
SrcString = SrcString?.Replace("</body>", "<script src='https://cdn.jsdelivr.net/npm/@iframe-resizer/child'></script></body>");
Html = (MarkupString)(SrcString ?? string.Empty);
}
await InvokeAsync(StateHasChanged);
await IframeResize();
}
public async Task IframeResize() {
await JS.InvokeVoidAsync("iframeResize", new { license = "GPLv3" });
}
}

View file

@ -9,12 +9,6 @@
<div class="row">
<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>
<InputFile OnChange="@ChangeFile" accept="image/gif, image/heic, image/heif, image/jpeg, image/png, image/svg+xml, image/webp"></InputFile>
<input type="text">
<label>Select a picture</label>
</div> *@
</div>
<div class="row">
@if(Base64File != null && FileSize != null){

View file

@ -2,14 +2,32 @@
@inject NavigationManager Nav
@inject RestService api
@inject State State
@inject IJSRuntime JS
<div class="max markdown-editor">
<MarkdownEditor @ref="Editor"
@bind-Value="@markdownValue"
Theme="material-darker"
MaxHeight="100%"
/>
@if (markdownValue != null)
{
<MarkdownEditor @ref="Editor"
@bind-Value="@markdownValue"
Theme="material-darker"
MaxHeight="100%"
CustomButtonClicked="@OnCustomButtonClicked">
<Toolbar>
<MarkdownToolbarButton Action="MarkdownAction.Bold" Icon="fa-solid fa-bold" Title="Bold" />
<MarkdownToolbarButton Action="MarkdownAction.Italic" Icon="fa-solid fa-italic" Title="Italic" />
<MarkdownToolbarButton Action="MarkdownAction.Heading" Icon="fa-solid fa-heading" Title="Heading" />
<MarkdownToolbarButton Action="MarkdownAction.Code" Icon="fa-solid fa-code" Title="Code" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Quote" Icon="fa-solid fa-quote-left" Title="Quote" />
<MarkdownToolbarButton Action="MarkdownAction.UnorderedList" Icon="fa-solid fa-list-ul" Title="Unordered List" />
<MarkdownToolbarButton Action="MarkdownAction.OrderedList" Icon="fa-solid fa-list-ol" Title="Ordered List" />
<MarkdownToolbarButton Action="MarkdownAction.Link" Icon="fa-solid fa-link" Title="Link" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Image" Icon="fa-solid fa-image" Title="Image" />
<MarkdownToolbarButton Action="MarkdownAction.HorizontalRule" Icon="fa-solid fa-horizontal-rule" Title="Horizontal Rule" />
<MarkdownToolbarButton Action="MarkdownAction.Guide" Icon="fa-solid fa-circle-question" Title="Guide" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Custom" Icon="omg-icon omg-prami" Title="Editor Information" Name="Help" />
</Toolbar>
</MarkdownEditor>
}
</div>
<nav>
@ -34,7 +52,7 @@
private bool listed;
private string? markdownValue;
private bool loading = false;
private bool loading = true;
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
@ -43,8 +61,13 @@
{
listed = data.Listed == 1;
markdownValue = data.Content;
loading = false;
await InvokeAsync(StateHasChanged);
await Editor!.SetValueAsync(markdownValue);
}
loading = false;
await InvokeAsync(StateHasChanged);
}
@ -64,4 +87,10 @@
loading = false;
}
public async Task OnCustomButtonClicked(MarkdownButtonEventArgs eventArgs) {
if (eventArgs.Name == "Help") {
await JS.InvokeVoidAsync("open", "https://home.omg.lol/info/editor", "_blank");
}
}
}

View file

@ -4,46 +4,59 @@
@inject State State
@inject IJSRuntime JS
<div class="max markdown-editor">
<MarkdownEditor @ref="Editor"
@bind-Value="@markdownValue"
Theme="material-darker"
MaxHeight="100%"
CustomButtonClicked="@OnCustomButtonClicked">
<Toolbar>
<MarkdownToolbarButton Action="MarkdownAction.Bold" Icon="fa-solid fa-bold" Title="Bold" />
<MarkdownToolbarButton Action="MarkdownAction.Italic" Icon="fa-solid fa-italic" Title="Italic" />
<MarkdownToolbarButton Action="MarkdownAction.Heading" Icon="fa-solid fa-heading" Title="Heading" />
<MarkdownToolbarButton Action="MarkdownAction.Code" Icon="fa-solid fa-code" Title="Code" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Quote" Icon="fa-solid fa-quote-left" Title="Quote" />
<MarkdownToolbarButton Action="MarkdownAction.UnorderedList" Icon="fa-solid fa-list-ul" Title="Unordered List" />
<MarkdownToolbarButton Action="MarkdownAction.OrderedList" Icon="fa-solid fa-list-ol" Title="Ordered List" />
<MarkdownToolbarButton Action="MarkdownAction.Link" Icon="fa-solid fa-link" Title="Link" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Image" Icon="fa-solid fa-image" Title="Image" />
<MarkdownToolbarButton Action="MarkdownAction.HorizontalRule" Icon="fa-solid fa-horizontal-rule" Title="Horizontal Rule" />
<MarkdownToolbarButton Action="MarkdownAction.Guide" Icon="fa-solid fa-circle-question" Title="Guide" Separator="true" />
<MarkdownToolbarButton Name="Help" Action="MarkdownAction.Custom"
Icon="omg-icon omg-prami"
Title="Editor Information" />
</Toolbar>
</MarkdownEditor>
@if (markdownValue != null)
{
<MarkdownEditor @ref="Editor"
@bind-Value="@markdownValue"
Theme="material-darker"
MaxHeight="100%"
CustomButtonClicked="@OnCustomButtonClicked">
<Toolbar>
<MarkdownToolbarButton Action="MarkdownAction.Bold" Icon="fa-solid fa-bold" Title="Bold" />
<MarkdownToolbarButton Action="MarkdownAction.Italic" Icon="fa-solid fa-italic" Title="Italic" />
<MarkdownToolbarButton Action="MarkdownAction.Heading" Icon="fa-solid fa-heading" Title="Heading" />
<MarkdownToolbarButton Action="MarkdownAction.Code" Icon="fa-solid fa-code" Title="Code" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Quote" Icon="fa-solid fa-quote-left" Title="Quote" />
<MarkdownToolbarButton Action="MarkdownAction.UnorderedList" Icon="fa-solid fa-list-ul" Title="Unordered List" />
<MarkdownToolbarButton Action="MarkdownAction.OrderedList" Icon="fa-solid fa-list-ol" Title="Ordered List" />
<MarkdownToolbarButton Action="MarkdownAction.Link" Icon="fa-solid fa-link" Title="Link" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Image" Icon="fa-solid fa-image" Title="Image" />
<MarkdownToolbarButton Action="MarkdownAction.HorizontalRule" Icon="fa-solid fa-horizontal-rule" Title="Horizontal Rule" />
<MarkdownToolbarButton Action="MarkdownAction.Guide" Icon="fa-solid fa-circle-question" Title="Guide" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Custom" Icon="omg-icon omg-prami" Title="Editor Information" Name="Help" />
</Toolbar>
</MarkdownEditor>
}
</div>
<details id="advanced">
<summary> Advanced </summary>
<small>Style you include here will be places in a &lt;style&gt; element in your pages &lt;head&gt;.</small>
<div class="field textarea label border max">
<InputTextArea @bind-Value="css"></InputTextArea>
<label>Custom CSS</label>
</div>
<small>Anything you put here will be included in your pages &lt;head&gt; element.</small>
<div class="field textarea label border max">
<InputTextArea @bind-Value="head"></InputTextArea>
<label>Additional &lt;head&gt; Content</label>
</div>
</details>
@if (markdownValue != null)
{
<details id="advanced">
<summary> Advanced </summary>
<h5>Theme:</h5>
<div class="row bottom-margin">
<ThemeDialog id="theme-modal" onthemechanged="ThemeChanged"></ThemeDialog>
<a data-ui="#theme-modal" class="row min" style="text-decoration:none;">
@if(selectedTheme != null) {
<ThemeCard theme="selectedTheme"></ThemeCard>
}
else {
<button>Choose a theme</button>
}
</a>
</div>
<small>Style you include here will be places in a &lt;style&gt; element in your pages &lt;head&gt;.</small>
<div class="field textarea label border max">
<InputTextArea @bind-Value="css"></InputTextArea>
<label>Custom CSS</label>
</div>
<small>Anything you put here will be included in your pages &lt;head&gt; element.</small>
<div class="field textarea label border max">
<InputTextArea @bind-Value="head"></InputTextArea>
<label>Additional &lt;head&gt; Content</label>
</div>
</details>
}
<nav>
<div class="max"></div>
<button class="transparent link" onclick="history.back();" disabled="@loading">Cancel</button>
@ -62,8 +75,11 @@
private string? markdownValue;
private string? css;
private string? head;
private string? theme;
private Theme? selectedTheme;
private Dictionary<string, Theme>? themes;
private bool loading = false;
private bool loading = true;
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
@ -72,9 +88,17 @@
markdownValue = data.Content;
css = data.Css;
head = data.Head;
theme = data.Theme;
themes = await State.GetThemes();
selectedTheme = themes?[theme];
loading = false;
await InvokeAsync(StateHasChanged);
await Editor!.SetValueAsync(markdownValue);
}
loading = false;
await InvokeAsync(StateHasChanged);
}
@ -89,7 +113,8 @@
new PostProfile() {
Content = markdownValue ?? string.Empty,
Css = string.IsNullOrEmpty(css) ? null : css,
Head = string.IsNullOrEmpty(head) ? null : head
Head = string.IsNullOrEmpty(head) ? null : head,
Theme = string.IsNullOrEmpty(theme) ? null : theme
});
if (result != null) {
await State.RefreshNow();
@ -98,6 +123,7 @@
}
loading = false;
await InvokeAsync(StateHasChanged);
}
public async Task OnCustomButtonClicked(MarkdownButtonEventArgs eventArgs) {
@ -105,4 +131,10 @@
await JS.InvokeVoidAsync("open", "https://home.omg.lol/info/editor", "_blank");
}
}
public void ThemeChanged(Theme? _theme) {
theme = _theme?.Id;
selectedTheme = _theme;
InvokeAsync(StateHasChanged);
}
}

View file

@ -9,7 +9,13 @@
<h3 class="page-heading"><i class="fa-solid fa-fw fa-at"></i>@Address</h3>
</div>
<div class="row center-align">
<img class="profile avatar" src="https://profiles.cache.lol/@Address/picture" alt="@Address" />
<div class="min">
@if (IsMe) {
<EditProfilePicDialog id="profile-pic" Address="@Address"></EditProfilePicDialog>
<button data-ui="#profile-pic" class="small circle small-elevate absolute top right no-margin" style="z-index:1;"><i class="fa-solid fa-pencil"></i></button>
}
<img class="profile avatar" src="https://profiles.cache.lol/@Address/picture" alt="@Address" />
</div>
</div>
@if (FeatureFlags.Following) {
<div class="row center-align">
@ -76,7 +82,7 @@
<StatusList @ref="StatusList" StatusFunc="@(async(refresh) => await State.GetStatuses(Address, refresh))" Editable="@IsMe"></StatusList>
@if(IsMe) {
<button class="fab circle extra large-elevate" data-ui="#post-modal">
<i class="fa-solid fa-pen-to-square"></i>
<i class="fa-solid fa-message-plus square"></i>
</button>
<NewStatusDialog id="post-modal"></NewStatusDialog>
}
@ -131,7 +137,7 @@
private PicList? PicList { get; set; }
private bool IsMe {
get => Address == State.SelectedAddressName;
get => State.AddressList?.Any(a => a.Address == Address) ?? false;
}
private MarkupString? bio;

View file

@ -10,7 +10,7 @@
<AuthorizeView>
<Authorized>
<button class="fab circle extra large-elevate" data-ui="#post-modal">
<i class="fa-solid fa-pen-to-square"></i>
<i class="fa-solid fa-message-plus square"></i>
</button>
<NewStatusDialog id="post-modal" Active="true" Content="@Text"></NewStatusDialog>
</Authorized>

View file

@ -11,7 +11,7 @@
<AuthorizeView>
<Authorized>
<button class="fab circle extra large-elevate" data-ui="#post-modal">
<i class="fa-solid fa-pen-to-square"></i>
<i class="fa-solid fa-message-plus square"></i>
</button>
<NewStatusDialog id="post-modal"></NewStatusDialog>
</Authorized>

View file

@ -0,0 +1,11 @@
<article class="theme" style="@theme?.PreviewCssData?.BackgroundCss ; @theme?.PreviewCssData?.TextCss">
<h5 class="address">@theme?.Name</h5>
<p class="small theme-author" style="@theme?.PreviewCssData?.LinkCss">
<i class="fa-solid fa-palette" style="@theme?.PreviewCssData?.IconCss"></i> by @theme?.Author
</p>
</article>
@code {
[Parameter]
public Theme? theme { get; set; }
}

View file

@ -0,0 +1,82 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@inject NavigationManager navigationManager
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active" style="overflow:auto;">
<h5>Choose a theme</h5>
<nav class="wrap max">
@if(themes != null) foreach(Theme theme in themes.Values) {
<a onclick="@(() => ClickTheme(theme))" class="min" style="text-decoration:none;">
<ThemeCard theme="@theme"></ThemeCard>
</a>
}
</nav>
<nav class="right-align no-space">
<button class="transparent link" data-ui="#@id">Cancel</button>
</nav>
</dialog>
<div class="overlay" data-ui="#@previewId"></div>
<dialog id="@previewId" style="overflow:auto;">
<h5 class="address">@activeTheme?.Name</h5>
<div class="max">
<p>@((MarkupString)(activeTheme?.Description ?? string.Empty)) A theme by <a href="@activeTheme?.AuthorUrl" target="_blank">@activeTheme?.Author</a>.</p>
@if(themePreview != null) {
<ExternalPageComponent id="profile_page" @ref="iframe" SrcString="@themePreview.ToString()"></ExternalPageComponent>
}
</div>
<nav class="right-align no-space">
<button class="transparent link" @onclick="CancelPreview">Back</button>
<button @onclick=UseTheme><i class="fa-solid fa-palette"></i> Use the @activeTheme?.Name theme</button>
</nav>
</dialog>
@code {
private Dictionary<string, Theme>? themes;
[Parameter]
public string? id { get; set; }
private string? previewId { get => $"{id}-preview"; }
[Parameter]
public bool Active { get; set; }
[Parameter]
public Action<Theme?>? onthemechanged { get; set; }
private Theme? activeTheme { get; set; }
private MarkupString? themePreview { get; set; }
private ExternalPageComponent iframe { get; set; }
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
activeTheme = null;
themes = await State.GetThemes();
await InvokeAsync(StateHasChanged);
}
public async Task ClickTheme(Theme theme) {
activeTheme = theme;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("ui", "#" + id);
await JS.InvokeVoidAsync("ui", "#" + previewId);
themePreview = await api.GetThemePreview(theme.Id);
await InvokeAsync(StateHasChanged);
@* iframe.SrcString = themePreview.ToString(); *@
await iframe.Reload();
}
public async Task CancelPreview() {
activeTheme = null;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("ui", "#" + previewId);
await JS.InvokeVoidAsync("ui", "#" + id);
}
public async Task UseTheme() {
// todo: update theme
onthemechanged?.Invoke(activeTheme);
activeTheme = null;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("ui", "#" + previewId);
}
}

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class AddressResponseData : IOmgLolResponseData {
public string Address { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class AddressResponseList : List<AddressResponseData>, IOmgLolResponseList<AddressResponseData> {
}
}

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class BasicResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
}

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class DirectoryResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class EphemeralData {
public string Content { get; set; } = string.Empty;
}

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class EphemeralResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public List<string> Content { get; set; } = new List<string>();

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public interface IOmgLolResponseList<T> : IList<T>, IOmgLolResponseData where T : IOmgLolResponseData {
}
}

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class NowContentData {
public string? Content { get; set; }
public long? Updated { get; set; }

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class NowPageResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public NowContentData? Now { get; set; }

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class NowResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public long Count { get; set; }

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class PatchStatus {
public string Id { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class PatchStatusResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Id { get; set; } = string.Empty;

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class PostPic {
public string? Description { get; set; }
}

View file

@ -1,13 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class PostProfile {
public string Content { get; set; } = string.Empty;
public bool Publish { get; set; } = true;
public string? Theme { get; set; }
public string? Css { get; set; }
public string? Head { get; set; }
}

View file

@ -0,0 +1,8 @@
namespace Neighbourhood.omg.lol.Models {
public class PreviewCssData {
public string BackgroundCss { get; set; } = string.Empty;
public string TextCss { get; set; } = string.Empty;
public string LinkCss { get; set; } = string.Empty;
public string IconCss { get; set; } = string.Empty;
}
}

View file

@ -1,21 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class ProfileResponseData: IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string Theme { get; set; } = string.Empty;
public string Css { get; set; } = string.Empty;
public string Head { get; set; } = string.Empty;
public string Verified { get; set; } = string.Empty;
public string? Css { get; set; }
public string? Head { get; set; }
public short Verified { get; set; }
public string Pfp { get; set; } = string.Empty;
public string Metadata { get; set; } = string.Empty;
public string Branding { get; set; } = string.Empty;
public string? Modified { get; set; }
}
}

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class PutPic {
public string Pic { get; set; } = string.Empty;
}

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class PutPicResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Id { get; set; } = string.Empty;

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class SomePicsResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public List<Pic>? Pics { get; set; }

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class StatusBioResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Bio { get; set; } = string.Empty;

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class StatusPost {
public string? Emoji { get; set; }
public string? Content { get; set; }

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class StatusPostResponseData : IOmgLolResponseData {
public string? Message { get; set; }
public string? Id { get; set; }

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class StatusResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public List<Status> Statuses { get; set; } = new List<Status>();

View file

@ -0,0 +1,6 @@
namespace Neighbourhood.omg.lol.Models {
public class ThemePreviewResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Html { get; set; } = string.Empty;
}
}

View file

@ -0,0 +1,6 @@
namespace Neighbourhood.omg.lol.Models {
public class ThemeResponseData : IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public Dictionary<string, Theme> Themes { get; set; } = new Dictionary<string, Theme>();
}
}

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class StatusOrPic {
public Status? Status { get; set; }
public Pic? Pic { get; set; }

19
Models/Theme.cs Normal file
View file

@ -0,0 +1,19 @@
namespace Neighbourhood.omg.lol.Models {
public class Theme {
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Created { get; set; } = string.Empty;
public string Updated { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public string AuthorUrl { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public string License { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string PreviewCss { get; set; } = string.Empty;
public string? SampleProfile { get; set; } = string.Empty;
public PreviewCssData? PreviewCssData {
get => new RestService().Deserialize<PreviewCssData>(this.PreviewCss);
}
}
}

View file

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
namespace Neighbourhood.omg.lol.Models {
public class TimeData {
public long? UnixEpochTime { get; set; }
public string? Iso8601Time { get; set; }

View file

@ -103,7 +103,7 @@
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
<None Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
@ -117,22 +117,11 @@
</ItemGroup>
<ItemGroup>
<MauiFont Remove="Resources\Fonts\fa-brands-400.ttf" />
<MauiFont Remove="Resources\Fonts\fa-brands-400.woff2" />
<MauiFont Remove="Resources\Fonts\fa-duotone-900.ttf" />
<MauiFont Remove="Resources\Fonts\fa-duotone-900.woff2" />
<MauiFont Remove="Resources\Fonts\fa-light-300.ttf" />
<MauiFont Remove="Resources\Fonts\fa-light-300.woff2" />
<MauiFont Remove="Resources\Fonts\fa-regular-400.ttf" />
<MauiFont Remove="Resources\Fonts\fa-regular-400.woff2" />
<MauiFont Remove="Resources\Fonts\fa-solid-900.ttf" />
<MauiFont Remove="Resources\Fonts\fa-solid-900.woff2" />
<MauiFont Remove="Resources\Fonts\fa-thin-100.ttf" />
<MauiFont Remove="Resources\Fonts\fa-thin-100.woff2" />
<MauiFont Remove="Resources\Fonts\fa-v4compatibility.ttf" />
<MauiFont Remove="Resources\Fonts\fa-v4compatibility.woff2" />
<MauiFont Remove="Resources\Fonts\omg.lol-icons.woff2" />
<MauiFont Remove="Resources\Fonts\seguiemj.ttf" />
<MauiAsset Remove="Resources\Raw\AboutAssets.txt" />
</ItemGroup>
<ItemGroup>
<MauiImage Remove="Resources\Images\dotnet_bot.svg" />
</ItemGroup>
<ItemGroup>
@ -154,73 +143,6 @@
<None Remove="Resources\Fonts\seguiemj.ttf" />
</ItemGroup>
<ItemGroup>
<Content Include="Resources\Fonts\fa-brands-400.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-brands-400.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-duotone-900.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-duotone-900.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-light-300.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-light-300.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-regular-400.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-regular-400.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-solid-900.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-solid-900.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-thin-100.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-thin-100.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-v4compatibility.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-v4compatibility.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\omg.lol-icons.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\seguiemj.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="appsettings.json" />
</ItemGroup>
@ -240,6 +162,73 @@
<PackageReference Include="PSC.Blazor.Components.MarkdownEditor" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<MauiFont Update="Resources\Fonts\fa-brands-400.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-brands-400.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-duotone-900.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-duotone-900.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-light-300.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-light-300.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-regular-400.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-regular-400.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-solid-900.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-solid-900.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-thin-100.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-thin-100.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-v4compatibility.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\fa-v4compatibility.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\omg.lol-icons.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
<MauiFont Update="Resources\Fonts\seguiemj.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</MauiFont>
</ItemGroup>
<ItemGroup>
<MauiXaml Update="XamlComponents\AppShell.xaml">
<Generator>MSBuild:Compile</Generator>

View file

@ -319,7 +319,7 @@ article.ephemeral {
flex-direction:column;
flex-grow: 1;
}
#now.page > iframe {
iframe {
width:100%;
flex-grow: 1;
border: none;
@ -502,3 +502,8 @@ article.now {
color: #EEFFFF;
font-family: monospace;
}
article.theme {
height: 10rem;
width: 15rem;
}

View file

@ -29,3 +29,19 @@ function toggleDetails(id) {
if (element instanceof HTMLDetailsElement)
element.open = !element.open
}
function cacheBust(url) {
fetch(new Request(url), {
headers: new Headers({
"pragma": "no-cache",
"cache-control": "no-cache"
}),
mode: 'no-cors',
cache: 'no-cache',
})
.finally(() => {
let els = document.querySelectorAll(`[src="${url}"]`)
els.forEach(el => el.removeAttribute('src'))
els.forEach(el => el.src = url)
})
}