Compare commits
65 commits
pull-to-re
...
main
Author | SHA1 | Date | |
---|---|---|---|
d142010786 | |||
8d3a454a6b | |||
b089cb39e6 | |||
72c6b590f1 | |||
b074b9d453 | |||
1fe82c18af | |||
04a5641650 | |||
a25df4368e | |||
d14ee65af4 | |||
a02b14782b | |||
278811c2c2 | |||
85bc105ddd | |||
6e5b206c20 | |||
909038ff79 | |||
cc779051bf | |||
cee1c3dfd2 | |||
905257789b | |||
e017269685 | |||
859f8348f0 | |||
1bf7e74a5a | |||
640cd73271 | |||
bdea7870ea | |||
7b462e3cfc | |||
2dc66abcd7 | |||
3d8047c01c | |||
f24ef392f7 | |||
0fcda98b9f | |||
25f362bfc5 | |||
b42bf2023d | |||
a5d860c916 | |||
93b49f2533 | |||
9c3f578eb6 | |||
2274c34f07 | |||
822bf52407 | |||
fd0300fccb | |||
1b0155d064 | |||
908f126987 | |||
b1cb4e21c4 | |||
efa31a414c | |||
61c8ed5604 | |||
2b1a6f203f | |||
039fa52955 | |||
d3dde2ca21 | |||
ce21244517 | |||
65276d4984 | |||
2fcb6e1fae | |||
d438c7c3cc | |||
ce86f2acf7 | |||
434c84ce5c | |||
c7400ca42f | |||
9369cf15ea | |||
d6a33fb793 | |||
34caee585f | |||
57885b4c5e | |||
295ea4834e | |||
518f181c4f | |||
e4212c9436 | |||
5c64890daf | |||
e9716b4157 | |||
d0ac64e1be | |||
f43e1a7bc1 | |||
f2025df922 | |||
63d844e7a3 | |||
413051ad4c | |||
996e1286cb |
173 changed files with 3453 additions and 12825 deletions
16
App.xaml.cs
16
App.xaml.cs
|
@ -1,16 +0,0 @@
|
||||||
namespace Neighbourhood.omg.lol {
|
|
||||||
public partial class App : Application {
|
|
||||||
public App(NavigatorService navigatorService) {
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
#if WINDOWS
|
|
||||||
MainPage = new WindowsAppShell();
|
|
||||||
#else
|
|
||||||
MainPage = new AppShell();
|
|
||||||
#endif
|
|
||||||
NavigatorService = navigatorService;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal NavigatorService NavigatorService { get; private set; }
|
|
||||||
}
|
|
||||||
}
|
|
310
Classes/ApiService.cs
Normal file
310
Classes/ApiService.cs
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Neighbourhood.omg.lol.Models;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Neighbourhood.omg.lol
|
||||||
|
{
|
||||||
|
public class ApiService {
|
||||||
|
HttpClient _client;
|
||||||
|
JsonSerializerOptions _serializerOptions;
|
||||||
|
public const string BaseUrl = "https://api.omg.lol";
|
||||||
|
private string? apiToken = null;
|
||||||
|
|
||||||
|
public ApiService(string? token = null) {
|
||||||
|
_client = new HttpClient();
|
||||||
|
_client.BaseAddress = new Uri(BaseUrl);
|
||||||
|
_client.DefaultRequestHeaders.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue(App.Name, App.Version));
|
||||||
|
_serializerOptions = new JsonSerializerOptions {
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
#if DEBUG
|
||||||
|
WriteIndented = true
|
||||||
|
#else
|
||||||
|
WriteIndented = false
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
AddToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize json convenience function with default serializer options
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize</typeparam>
|
||||||
|
/// <param name="str">The string to deserialize</param>
|
||||||
|
/// <returns>The deserialized object if successful, otherwise default</returns>
|
||||||
|
public T? Deserialize<T>(string str) {
|
||||||
|
T? responseObj = default;
|
||||||
|
try {
|
||||||
|
responseObj = JsonSerializer.Deserialize<T>(str, _serializerOptions);
|
||||||
|
}
|
||||||
|
catch (JsonException ex) {
|
||||||
|
Debug.WriteLine(@"\tERROR {0}", ex.Message);
|
||||||
|
Debug.WriteLine(str);
|
||||||
|
}
|
||||||
|
return responseObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Base Requests
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decode the response from an API call
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse">The type of response object we are trying to get</typeparam>
|
||||||
|
/// <param name="response">The raw Http Response Message</param>
|
||||||
|
/// <param name="cancellationToken">A cancellation token to cancel the operation</param>
|
||||||
|
/// <returns>The decoded object if successfull, otherwise default</returns>
|
||||||
|
private async Task<TResponse?> DecodeResponse<TResponse>(HttpResponseMessage response, CancellationToken cancellationToken = default)
|
||||||
|
where TResponse : IOmgLolResponseData
|
||||||
|
{
|
||||||
|
TResponse? responseData = default;
|
||||||
|
try {
|
||||||
|
string str = await response.Content.ReadAsStringAsync();
|
||||||
|
if (response.IsSuccessStatusCode) {
|
||||||
|
OmgLolResponse<TResponse>? responseObj = Deserialize<OmgLolResponse<TResponse>>(str);
|
||||||
|
if (responseObj?.Request == null || (responseObj?.Request?.Success ?? false)) {
|
||||||
|
responseData = responseObj!.Response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
OmgLolResponse<TResponse>? responseObj = Deserialize<OmgLolResponse<TResponse>>(str);
|
||||||
|
throw responseObj == null ? new OmgLolApiException<TResponse>(str) : new OmgLolApiException<TResponse>(responseObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Debug.WriteLine(@"\tERROR {0}", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs a request for the supplied uri, with the supplied Http Method,
|
||||||
|
/// with the supplied data in the body (if present)
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse">The type of response we are expecting</typeparam>
|
||||||
|
/// <typeparam name="TData">The type of data we are sending</typeparam>
|
||||||
|
/// <param name="uri">The uri to request</param>
|
||||||
|
/// <param name="method">The Http Method to use for the request</param>
|
||||||
|
/// <param name="data">The data to send in the body of the request</param>
|
||||||
|
/// <param name="file">A FileResult for the file to send in the body of the request as binary data</param>
|
||||||
|
/// <param name="cancellationToken">A cancellation token</param>
|
||||||
|
/// <returns>The returned data if successful, otherwise default</returns>
|
||||||
|
private async Task<TResponse?> Request<TResponse, TData>(string uri, HttpMethod method, TData? data = default, FileResult? file = null, bool useAuthToken = true, CancellationToken cancellationToken = default)
|
||||||
|
where TResponse : IOmgLolResponseData
|
||||||
|
{
|
||||||
|
TResponse? responseData = default;
|
||||||
|
try {
|
||||||
|
HttpRequestMessage request = new HttpRequestMessage(method, uri);
|
||||||
|
Stream? fileStream = null;
|
||||||
|
if (file != null) {
|
||||||
|
// append "binary" query parameter (if not already present)
|
||||||
|
Uri url = new Uri(_client.BaseAddress?.AbsoluteUri + uri);
|
||||||
|
if (string.IsNullOrEmpty(url.Query)) uri += "?binary";
|
||||||
|
else if (!url.Query.Contains("binary")) uri += "&binary";
|
||||||
|
request = new HttpRequestMessage(method, uri);
|
||||||
|
|
||||||
|
fileStream = await file.OpenReadAsync();
|
||||||
|
HttpContent fileStreamContent = new StreamContent(fileStream);
|
||||||
|
fileStreamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(file.ContentType ?? "application/octet-stream");
|
||||||
|
fileStreamContent.Headers.ContentLength = fileStream.Length;
|
||||||
|
request.Content = fileStreamContent;
|
||||||
|
}
|
||||||
|
else if (data != null) {
|
||||||
|
string json = JsonSerializer.Serialize(data, _serializerOptions);
|
||||||
|
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(useAuthToken) {
|
||||||
|
if (apiToken == null) apiToken = Task.Run(() => SecureStorage.GetAsync("accounttoken")).GetAwaiter().GetResult();
|
||||||
|
if (apiToken != null) request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponseMessage response = await _client.SendAsync(request, cancellationToken: cancellationToken);
|
||||||
|
responseData = await DecodeResponse<TResponse>(response, cancellationToken);
|
||||||
|
|
||||||
|
fileStream?.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Debug.WriteLine(@"\tERROR {0}", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET request
|
||||||
|
private async Task<TResponse?> Get<TResponse>(string uri, bool useAuthToken = true, CancellationToken cancellationToken = default)
|
||||||
|
where TResponse : IOmgLolResponseData
|
||||||
|
=> await Request<TResponse, object>(uri, HttpMethod.Get, useAuthToken: useAuthToken, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// POST request
|
||||||
|
private async Task<TResponse?> Post<TResponse, TData>(string uri, TData data, CancellationToken cancellationToken = default)
|
||||||
|
where TResponse : IOmgLolResponseData
|
||||||
|
=> await Request<TResponse, TData>(uri, HttpMethod.Post, data: data, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// POST request, but with a file as binary data
|
||||||
|
private async Task<TResponse?> PostBinary<TResponse>(string uri, FileResult? fileResult = null, CancellationToken cancellationToken = default)
|
||||||
|
where TResponse : IOmgLolResponseData
|
||||||
|
=> await Request<TResponse, object>(uri, HttpMethod.Post, file: fileResult, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// PUT request
|
||||||
|
private async Task<TResponse?> Put<TResponse, TData>(string uri, TData data, CancellationToken cancellationToken = default)
|
||||||
|
where TResponse : IOmgLolResponseData
|
||||||
|
=> await Request<TResponse, TData>(uri, HttpMethod.Put, data: data, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// PATCH request
|
||||||
|
private async Task<TResponse?> Patch<TResponse, TData>(string uri, TData data, CancellationToken cancellationToken = default)
|
||||||
|
where TResponse : IOmgLolResponseData
|
||||||
|
=> await Request<TResponse, TData>(uri, HttpMethod.Patch, data: data, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
// Delete request
|
||||||
|
private async Task<TResponse?> Delete<TResponse>(string uri, CancellationToken cancellationToken = default)
|
||||||
|
where TResponse : IOmgLolResponseData
|
||||||
|
=> await Request<TResponse, object>(uri, HttpMethod.Delete, cancellationToken: cancellationToken);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Specific Requests
|
||||||
|
public async Task<List<Status>> StatuslogLatest() =>
|
||||||
|
(await Get<StatusResponseData>("/statuslog/latest"))?.Statuses ?? new List<Status>();
|
||||||
|
|
||||||
|
public async Task<List<Status>> Statuslog(string address) =>
|
||||||
|
(await Get<StatusResponseData>($"/address/{address}/statuses"))?.Statuses ?? new List<Status>();
|
||||||
|
|
||||||
|
public async Task<string> StatuslogBio(string address) =>
|
||||||
|
(await Get<StatusBioResponseData>($"/address/{address}/statuses/bio"))?.Bio ?? string.Empty;
|
||||||
|
|
||||||
|
public async Task<string> PostStatuslogBio(string address, string bio) =>
|
||||||
|
(await Post<StatusBioResponseData, PostStatusBio>($"/address/{address}/statuses/bio", new PostStatusBio() { Content = bio }))?.Bio ?? string.Empty;
|
||||||
|
|
||||||
|
public async Task<AccountResponseData?> AccountInfo() =>
|
||||||
|
await Get<AccountResponseData>("/account/application/info");
|
||||||
|
|
||||||
|
public async Task<AddressResponseList?> Addresses() =>
|
||||||
|
await Get<AddressResponseList>("/account/application/addresses");
|
||||||
|
|
||||||
|
public async Task<StatusPostResponseData?> StatusPost(string address, StatusPost statusPost) =>
|
||||||
|
await Post<StatusPostResponseData, StatusPost>($"/address/{address}/statuses", statusPost);
|
||||||
|
|
||||||
|
public async Task<List<Pic>> SomePics() =>
|
||||||
|
(await Get<SomePicsResponseData>("/pics"))?.Pics ?? new List<Pic>();
|
||||||
|
|
||||||
|
public async Task<List<Pic>> SomePics(string address) =>
|
||||||
|
(await Get<SomePicsResponseData>($"/address/{address}/pics"))?.Pics ?? new List<Pic>();
|
||||||
|
|
||||||
|
public async Task<PutPicResponseData?> PutPic(string address, string base64Image) =>
|
||||||
|
(await Put<PutPicResponseData, PutPic>($"/address/{address}/pics/upload", new PutPic { Pic = base64Image }));
|
||||||
|
|
||||||
|
public async Task<PutPicResponseData?> PutPic(string address, byte[] bytes) =>
|
||||||
|
await PutPic(address, Convert.ToBase64String(bytes));
|
||||||
|
|
||||||
|
public async Task<PutPicResponseData?> PostPicDescription(string address, string id, string? description) =>
|
||||||
|
(await Post<PutPicResponseData, PostPic>($"/address/{address}/pics/{id}", new PostPic { Description = description }));
|
||||||
|
public async Task<BasicResponseData?> DeletePic(string address, string id) =>
|
||||||
|
(await Delete<BasicResponseData>($"/address/{address}/pics/{id}"));
|
||||||
|
|
||||||
|
public async Task<PatchStatusResponseData?> PatchStatus(string address, string id, string content, string? emoji) =>
|
||||||
|
(await Patch<PatchStatusResponseData, PatchStatus>($"/address/{address}/statuses/", new PatchStatus { Id = id, Content = content, Emoji = emoji }));
|
||||||
|
|
||||||
|
public async Task<BasicResponseData?> DeleteStatus(string address, string id) =>
|
||||||
|
(await Delete<BasicResponseData>($"/address/{address}/statuses/{id}"));
|
||||||
|
|
||||||
|
public async Task<List<NowData>?> NowGarden() =>
|
||||||
|
(await Get<NowResponseData>($"/now/garden"))?.Garden ?? new List<NowData>();
|
||||||
|
|
||||||
|
public async Task<List<string>?> Directory() =>
|
||||||
|
(await Get<DirectoryResponseData>($"/directory"))?.Directory ?? new List<string>();
|
||||||
|
|
||||||
|
public async Task<NowContentData?> GetNowPage(string address) =>
|
||||||
|
(await Get<NowPageResponseData>($"/address/{address}/now"))?.Now;
|
||||||
|
|
||||||
|
public async Task<BasicResponseData?> PostNowPage(string address, string content, bool listed) =>
|
||||||
|
await Post<BasicResponseData, NowContentData>($"/address/{address}/now", new NowContentData { Content = content, Listed = listed ? 1 : 0 });
|
||||||
|
|
||||||
|
public async Task<List<MarkupString>> Ephemeral() =>
|
||||||
|
(await Get<EphemeralResponseData>($"/ephemeral"))?.Content?.Select(s => (MarkupString)s)?.ToList() ?? new List<MarkupString>();
|
||||||
|
|
||||||
|
public async Task<BasicResponseData?> PostEphemeral(string content) =>
|
||||||
|
await Post<BasicResponseData, EphemeralData>("/ephemeral", new EphemeralData { Content = content });
|
||||||
|
|
||||||
|
public async Task<ProfileResponseData?> GetProfile(string address) =>
|
||||||
|
await Get<ProfileResponseData>($"/address/{address}/web");
|
||||||
|
|
||||||
|
public async Task<BasicResponseData?> PostProfile(string address, string content, bool publish = true) =>
|
||||||
|
await Post<BasicResponseData, PostProfile>($"/address/{address}/web", new PostProfile { Content = content, Publish = publish });
|
||||||
|
|
||||||
|
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<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>($"/address/{address}/pfp", fileResult: image);
|
||||||
|
|
||||||
|
public async Task<List<Paste>> GetPastes(string address) =>
|
||||||
|
(await Get<PastesResponseData>($"/address/{address}/pastebin", useAuthToken: false))?.Pastebin ?? new List<Paste>();
|
||||||
|
public async Task<List<Paste>> GetMyPastes(string address) =>
|
||||||
|
(await Get<PastesResponseData>($"/address/{address}/pastebin", useAuthToken: true))?.Pastebin ?? new List<Paste>();
|
||||||
|
|
||||||
|
public async Task<BasicResponseData?> DeletePaste(string address, string title) =>
|
||||||
|
await Delete<BasicResponseData>($"/address/{address}/pastebin/{title}");
|
||||||
|
|
||||||
|
public async Task<PostPasteResponseData?> PostPaste(string address, string title, string content, bool listed) =>
|
||||||
|
await Post<PostPasteResponseData, Paste>($"/address/{address}/pastebin/", new Paste() { Title = title, Content = content, IsListed = listed });
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Auth
|
||||||
|
/// <summary>
|
||||||
|
/// Add the api token into the default headers
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">The api token</param>
|
||||||
|
public void AddToken(string? token = null) {
|
||||||
|
if (token == null) token = Task.Run(() => SecureStorage.GetAsync("accounttoken")).GetAwaiter().GetResult();
|
||||||
|
if (token != null) apiToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove the api token from the default headers
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveToken() {
|
||||||
|
_client.DefaultRequestHeaders.Remove("Authorization");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> OAuth(string code, string client_id, string client_secret, string redirect_uri) {
|
||||||
|
string? token = null;
|
||||||
|
string uri = $"/oauth/?code={code}&client_id={client_id}&client_secret={client_secret}&redirect_uri={redirect_uri}&scope=everything";
|
||||||
|
try {
|
||||||
|
HttpResponseMessage response = await _client.GetAsync(uri);
|
||||||
|
if (response.IsSuccessStatusCode) {
|
||||||
|
TokenResponseData? responseObj = await response.Content.ReadFromJsonAsync<TokenResponseData>(_serializerOptions);
|
||||||
|
if (responseObj != null && !string.IsNullOrEmpty(responseObj.AccessToken)) {
|
||||||
|
token = responseObj.AccessToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Debug.WriteLine(@"\tERROR {0}", ex.Message);
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public async Task<MarkupString?> GetHtml(string url) {
|
||||||
|
string? raw = null;
|
||||||
|
try {
|
||||||
|
HttpResponseMessage response = await _client.GetAsync(url);
|
||||||
|
if (response.IsSuccessStatusCode) {
|
||||||
|
raw = await response.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Debug.WriteLine(@"\tERROR {0}", ex.Message);
|
||||||
|
}
|
||||||
|
return string.IsNullOrEmpty(raw) ? null : (MarkupString)raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,5 @@
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using Neighbourhood.omg.lol.Models;
|
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol {
|
namespace Neighbourhood.omg.lol {
|
||||||
public class CustomAuthenticationStateProvider : AuthenticationStateProvider {
|
public class CustomAuthenticationStateProvider : AuthenticationStateProvider {
|
5
Classes/FeatureFlags.cs
Normal file
5
Classes/FeatureFlags.cs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
namespace Neighbourhood.omg.lol {
|
||||||
|
public static class FeatureFlags {
|
||||||
|
public static bool Following { get; } = true;
|
||||||
|
}
|
||||||
|
}
|
8
Classes/NavigatorService.cs
Normal file
8
Classes/NavigatorService.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Neighbourhood.omg.lol {
|
||||||
|
public class NavigatorService {
|
||||||
|
internal NavigationManager? NavigationManager { get; set; }
|
||||||
|
internal Page? Page { get; set; }
|
||||||
|
}
|
||||||
|
}
|
339
Classes/State.cs
Normal file
339
Classes/State.cs
Normal file
|
@ -0,0 +1,339 @@
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Neighbourhood.omg.lol.Models;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Neighbourhood.omg.lol {
|
||||||
|
public class State : INotifyPropertyChanged {
|
||||||
|
|
||||||
|
// Events
|
||||||
|
public event EventHandler<EventArgs>? IntentReceived;
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
// Create the OnPropertyChanged method to raise the event
|
||||||
|
// The calling member's name will be used as the parameter.
|
||||||
|
protected void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
|
protected void OnIntentRecieved() => IntentReceived?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
// Main data lists
|
||||||
|
public List<Status>? Statuses { get; set; }
|
||||||
|
public List<Pic>? Pics { get; set; }
|
||||||
|
public List<NowData>? NowGarden { get; set; }
|
||||||
|
public List<MarkupString>? EphemeralMessages { get; set; }
|
||||||
|
public List<string>? AddressDirectory { get; set; }
|
||||||
|
|
||||||
|
public List<FeedItem>? Feed { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, Theme>? Themes { get; set; }
|
||||||
|
|
||||||
|
// Account data
|
||||||
|
private AccountResponseData? _accountInfo;
|
||||||
|
public AccountResponseData? AccountInfo {
|
||||||
|
get => _accountInfo;
|
||||||
|
set {
|
||||||
|
_accountInfo = value;
|
||||||
|
OnPropertyChanged(nameof(AccountInfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public AddressResponseList? AddressList { get; set; }
|
||||||
|
|
||||||
|
public bool IsAuthorized { get => AccountInfo != null; }
|
||||||
|
public string? Name { get => AccountInfo?.Name; }
|
||||||
|
public string? Email { get => AccountInfo?.Email; }
|
||||||
|
public IEnumerable<string>? AddressNames { get => AddressList?.Select(a => a.Address); }
|
||||||
|
|
||||||
|
// Selected Address
|
||||||
|
private AddressResponseData? _selectedAddress;
|
||||||
|
public AddressResponseData? SelectedAddress {
|
||||||
|
get {
|
||||||
|
if (_selectedAddress == null) {
|
||||||
|
string selectedAddressJson = Preferences.Default.Get("selectedaddress", string.Empty);
|
||||||
|
if (!string.IsNullOrEmpty(selectedAddressJson)) _selectedAddress = JsonSerializer.Deserialize<AddressResponseData>(selectedAddressJson);
|
||||||
|
}
|
||||||
|
return _selectedAddress;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if (_selectedAddress != value) {
|
||||||
|
_selectedAddress = value;
|
||||||
|
if (_selectedAddress == null) Preferences.Default.Remove("selectedaddress");
|
||||||
|
else {
|
||||||
|
string selectedAddressJson = JsonSerializer.Serialize(_selectedAddress);
|
||||||
|
Preferences.Default.Set("selectedaddress", selectedAddressJson);
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string>? Following { get; private set; }
|
||||||
|
public string? SelectedAddressName { get => SelectedAddress?.Address; }
|
||||||
|
|
||||||
|
// data for selected address
|
||||||
|
public List<Status>? CachedAddressStatuses { get; set; }
|
||||||
|
public List<Pic>? CachedAddressPics { get; set; }
|
||||||
|
public List<Paste>? CachedAddressPastes { get; set; }
|
||||||
|
public MarkupString? CachedAddressBio { get; set; }
|
||||||
|
private string? _cachedAddress;
|
||||||
|
public string? CachedAddress {
|
||||||
|
get => _cachedAddress;
|
||||||
|
set {
|
||||||
|
if (_cachedAddress != value) {
|
||||||
|
_cachedAddress = value;
|
||||||
|
CachedAddressStatuses = new List<Status>();
|
||||||
|
CachedAddressPics = new List<Pic>();
|
||||||
|
CachedAddressPastes = new List<Paste>();
|
||||||
|
CachedAddressBio = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// share intent stuff
|
||||||
|
private string? _shareString;
|
||||||
|
public string? ShareString {
|
||||||
|
get => _shareString;
|
||||||
|
set {
|
||||||
|
_shareString = value;
|
||||||
|
OnIntentRecieved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public string? ShareStringSubject { get; set; }
|
||||||
|
|
||||||
|
private string? _sharePhoto;
|
||||||
|
public string? SharePhoto {
|
||||||
|
get => _sharePhoto;
|
||||||
|
set {
|
||||||
|
_sharePhoto = value;
|
||||||
|
OnIntentRecieved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public long? SharePhotoSize { get; set; }
|
||||||
|
public string? SharePhotoContentType { get; set; }
|
||||||
|
public string? SharePhotoText { get; set; }
|
||||||
|
|
||||||
|
// refreshing
|
||||||
|
private bool _isRefreshing;
|
||||||
|
public bool IsRefreshing {
|
||||||
|
get => _isRefreshing;
|
||||||
|
private set {
|
||||||
|
_isRefreshing = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendRefresh() => IsRefreshing = true;
|
||||||
|
|
||||||
|
private static int _refresherCount = 0;
|
||||||
|
private static Mutex mutex = new Mutex();
|
||||||
|
|
||||||
|
public class RefreshToken : IDisposable {
|
||||||
|
public event EventHandler? Disposed;
|
||||||
|
public void Dispose() => Disposed?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
public RefreshToken GetRefreshToken() {
|
||||||
|
mutex.WaitOne();
|
||||||
|
_refresherCount++;
|
||||||
|
mutex.ReleaseMutex();
|
||||||
|
RefreshToken token = new RefreshToken();
|
||||||
|
token.Disposed += RefreshToken_Disposed;
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshToken_Disposed(object? sender, EventArgs e) {
|
||||||
|
mutex.WaitOne();
|
||||||
|
_refresherCount--;
|
||||||
|
if (_refresherCount == 0) IsRefreshing = false;
|
||||||
|
mutex.ReleaseMutex();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _canRefresh;
|
||||||
|
public bool CanRefresh {
|
||||||
|
get => _canRefresh;
|
||||||
|
set {
|
||||||
|
_canRefresh = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// api service
|
||||||
|
private ApiService api { get; set; }
|
||||||
|
|
||||||
|
public State(ApiService restService) {
|
||||||
|
api = restService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PopulateAccountDetails(string token) {
|
||||||
|
api.AddToken(token);
|
||||||
|
|
||||||
|
string accountJson = Preferences.Default.Get("accountdetails", string.Empty);
|
||||||
|
string addressJson = Preferences.Default.Get("accountaddresses", string.Empty);
|
||||||
|
string selectedAddressJson = Preferences.Default.Get("selectedaddress", string.Empty);
|
||||||
|
string followingJson = Preferences.Default.Get("following", string.Empty);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(accountJson)) AccountInfo = JsonSerializer.Deserialize<AccountResponseData>(accountJson);
|
||||||
|
if (!string.IsNullOrEmpty(addressJson)) AddressList = JsonSerializer.Deserialize<AddressResponseList>(addressJson);
|
||||||
|
if (!string.IsNullOrEmpty(selectedAddressJson)) SelectedAddress = JsonSerializer.Deserialize<AddressResponseData>(selectedAddressJson);
|
||||||
|
if (!string.IsNullOrEmpty(followingJson)) Following = JsonSerializer.Deserialize<List<string>>(followingJson);
|
||||||
|
|
||||||
|
// if we haven't got account info, attempt to retrieve it.
|
||||||
|
if (AccountInfo == null) {
|
||||||
|
AccountInfo = await api.AccountInfo();
|
||||||
|
if (AccountInfo != null) {
|
||||||
|
// quick fix for users without names (such as the review account)
|
||||||
|
if(AccountInfo.Name == null) AccountInfo.Name = AccountInfo.Email.Split('@').FirstOrDefault() ?? "person";
|
||||||
|
Preferences.Default.Set("accountdetails", JsonSerializer.Serialize(AccountInfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we don't have the list of addresses, attempt to retrieve that.
|
||||||
|
if (AddressList == null) {
|
||||||
|
AddressList = await api.Addresses();
|
||||||
|
if (AddressList != null) {
|
||||||
|
Preferences.Default.Set("accountaddresses", JsonSerializer.Serialize(AddressList));
|
||||||
|
SelectedAddress = AddressList.FirstOrDefault();
|
||||||
|
Preferences.Default.Set("selectedaddress", JsonSerializer.Serialize(SelectedAddress));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAccountDetails() {
|
||||||
|
await Task.Run(() => {
|
||||||
|
Preferences.Default.Clear();
|
||||||
|
AccountInfo = null;
|
||||||
|
AddressList = null;
|
||||||
|
SelectedAddress = null;
|
||||||
|
Following = null;
|
||||||
|
api.RemoveToken();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsFollowing(string address) => Following?.Contains(address) ?? false;
|
||||||
|
public async Task Follow(string address) {
|
||||||
|
if (Following == null) Following = new List<string>();
|
||||||
|
Following.Add(address);
|
||||||
|
Preferences.Default.Set("following", JsonSerializer.Serialize(Following));
|
||||||
|
await GetFeed(forceRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Unfollow(string address) {
|
||||||
|
if (Following == null) Following = new List<string>();
|
||||||
|
Following.Remove(address);
|
||||||
|
Preferences.Default.Set("following", JsonSerializer.Serialize(Following));
|
||||||
|
await GetFeed(forceRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MarkupString?> GetBio(string address, bool forceRefresh = false) {
|
||||||
|
CachedAddress = address;
|
||||||
|
if (forceRefresh || CachedAddressBio == null) {
|
||||||
|
CachedAddressBio = Utilities.MdToHtmlMarkup(await api.StatuslogBio(address));
|
||||||
|
}
|
||||||
|
return CachedAddressBio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<MarkupString>?> GetEphemeralMessages(bool forceRefresh = false) {
|
||||||
|
if (forceRefresh || this.EphemeralMessages == null || this.EphemeralMessages.Count == 0) {
|
||||||
|
this.EphemeralMessages = await api.Ephemeral();
|
||||||
|
}
|
||||||
|
return this.EphemeralMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>?> GetDirectory(bool forceRefresh = false) {
|
||||||
|
if (forceRefresh || this.AddressDirectory == null || this.AddressDirectory.Count == 0) {
|
||||||
|
IdnMapping idn = new IdnMapping();
|
||||||
|
this.AddressDirectory = (await api.Directory())?.Select(s => {
|
||||||
|
if (s.StartsWith("xn--")) return idn.GetUnicode(s);
|
||||||
|
else return s;
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
return this.AddressDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Status>?> GetStatuses(bool forceRefresh = false) {
|
||||||
|
if (forceRefresh || this.Statuses == null || this.Statuses.Count == 0) {
|
||||||
|
this.Statuses = await api.StatuslogLatest();
|
||||||
|
}
|
||||||
|
return this.Statuses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Status>?> GetStatuses(string address, bool forceRefresh = false) {
|
||||||
|
this.CachedAddress = address;
|
||||||
|
if (forceRefresh || this.CachedAddressStatuses == null || this.CachedAddressStatuses.Count == 0) {
|
||||||
|
this.CachedAddressStatuses = await api.Statuslog(address);
|
||||||
|
}
|
||||||
|
return this.CachedAddressStatuses;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<NowData>?> GetNowGarden(bool forceRefresh = false) {
|
||||||
|
if (forceRefresh || this.NowGarden == null || this.NowGarden.Count == 0) {
|
||||||
|
this.NowGarden = await api.NowGarden();
|
||||||
|
}
|
||||||
|
return this.NowGarden;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Pic>?> GetPics(bool forceRefresh = false) {
|
||||||
|
if (forceRefresh || this.Pics == null || this.Pics.Count == 0) {
|
||||||
|
this.Pics = await api.SomePics();
|
||||||
|
}
|
||||||
|
return this.Pics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Pic>?> GetPics(string address, bool forceRefresh = false) {
|
||||||
|
CachedAddress = address;
|
||||||
|
if (forceRefresh || this.CachedAddressPics == null || this.CachedAddressPics.Count == 0) {
|
||||||
|
CachedAddressPics = (await api.SomePics(address)) ?? new List<Pic>();
|
||||||
|
}
|
||||||
|
return CachedAddressPics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Paste>?> GetPastes(string address, bool forceRefresh = false) {
|
||||||
|
CachedAddress = address;
|
||||||
|
if (forceRefresh || this.CachedAddressPastes == null || this.CachedAddressPastes.Count == 0) {
|
||||||
|
if (AddressNames?.Contains(address) ?? false) {
|
||||||
|
CachedAddressPastes = (await api.GetMyPastes(address)) ?? new List<Paste>();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
CachedAddressPastes = (await api.GetPastes(address)) ?? new List<Paste>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CachedAddressPastes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshStatuses() {
|
||||||
|
await GetStatuses(forceRefresh: true);
|
||||||
|
if(SelectedAddressName != null)
|
||||||
|
await GetStatuses(SelectedAddressName, forceRefresh: true);
|
||||||
|
}
|
||||||
|
public async Task RefreshPics() {
|
||||||
|
await GetPics(forceRefresh: true);
|
||||||
|
if (SelectedAddressName != null)
|
||||||
|
await GetPics(SelectedAddressName, forceRefresh: true );
|
||||||
|
}
|
||||||
|
public async Task RefreshNow() => await GetNowGarden(forceRefresh: true);
|
||||||
|
|
||||||
|
public async Task RefreshPastes() {
|
||||||
|
if (SelectedAddressName != null)
|
||||||
|
await GetPastes(SelectedAddressName, forceRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IOrderedEnumerable<FeedItem>> GetFeed(bool forceRefresh = false) {
|
||||||
|
if(forceRefresh || Feed == null || Feed.Count == 0) {
|
||||||
|
Feed = new List<FeedItem>();
|
||||||
|
foreach(string address in Following ?? new List<string>()) {
|
||||||
|
Feed.AddRange((await GetStatuses(address, forceRefresh))?.Select(s => new FeedItem { Status = s }) ?? new List<FeedItem>());
|
||||||
|
Feed.AddRange((await GetPics(address, forceRefresh))?.Select(p => new FeedItem { Pic = p }) ?? new List<FeedItem>());
|
||||||
|
Feed.AddRange((await GetPastes(address, forceRefresh))?.Select(p => new FeedItem { Paste = p }) ?? new List<FeedItem>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
51
Classes/Utilities.cs
Normal file
51
Classes/Utilities.cs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
using Markdig;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Neighbourhood.omg.lol
|
||||||
|
{
|
||||||
|
public static class Utilities
|
||||||
|
{
|
||||||
|
|
||||||
|
private static MarkdownPipeline markdownPipeline { get; }
|
||||||
|
= new MarkdownPipelineBuilder().UseAutoLinks().Build();
|
||||||
|
|
||||||
|
public static string MdToHtml(string markdown) =>
|
||||||
|
Markdown.ToHtml(markdown, markdownPipeline);
|
||||||
|
|
||||||
|
public static MarkupString MdToHtmlMarkup(string markdown) =>
|
||||||
|
(MarkupString)MdToHtml(markdown);
|
||||||
|
|
||||||
|
public static async Task<long> FileSize(FileResult file)
|
||||||
|
{
|
||||||
|
using var fileStream = await file.OpenReadAsync();
|
||||||
|
return fileStream.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static 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 static string RelativeTimeFromUnix(long unix)
|
||||||
|
{
|
||||||
|
DateTimeOffset createdTime = DateTimeOffset.UnixEpoch.AddSeconds(unix);
|
||||||
|
TimeSpan offset = DateTimeOffset.UtcNow - createdTime;
|
||||||
|
|
||||||
|
var offsetString = string.Empty;
|
||||||
|
if (Math.Floor(offset.TotalDays) == 1) offsetString = $"{Math.Floor(offset.TotalDays)} day ago";
|
||||||
|
else if (offset.TotalDays > 1) offsetString = $"{Math.Floor(offset.TotalDays)} days ago";
|
||||||
|
else if (Math.Floor(offset.TotalHours) == 1) offsetString = $"{Math.Floor(offset.TotalHours)} hour ago";
|
||||||
|
else if (offset.TotalHours > 1) offsetString = $"{Math.Floor(offset.TotalHours)} hours ago";
|
||||||
|
else if (Math.Floor(offset.TotalMinutes) == 1) offsetString = $"{Math.Floor(offset.TotalMinutes)} minute ago";
|
||||||
|
else if (offset.TotalMinutes > 1) offsetString = $"{Math.Floor(offset.TotalMinutes)} minutes ago";
|
||||||
|
else if (Math.Floor(offset.TotalSeconds) == 1) offsetString = $"{Math.Floor(offset.TotalSeconds)} second ago";
|
||||||
|
else offsetString = $"{Math.Floor(offset.TotalSeconds)} seconds ago";
|
||||||
|
|
||||||
|
return offsetString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
Components/EditBioDialog.razor
Normal file
81
Components/EditBioDialog.razor
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject State State
|
||||||
|
@inject ApiService api
|
||||||
|
|
||||||
|
<div class="overlay" data-ui="#@id"></div>
|
||||||
|
<dialog id="@id">
|
||||||
|
<h5>Edit your statuslog bio</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="max markdown-editor">
|
||||||
|
@if (Bio != null) {
|
||||||
|
<MarkdownEditor @ref="Editor"
|
||||||
|
@bind-Value="@Bio"
|
||||||
|
Theme="material-darker"
|
||||||
|
MaxHeight="100%"
|
||||||
|
AutoDownloadFontAwesome="false"
|
||||||
|
>
|
||||||
|
<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" />
|
||||||
|
</Toolbar>
|
||||||
|
</MarkdownEditor>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="no-space">
|
||||||
|
<div class="max"></div>
|
||||||
|
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
|
||||||
|
<button @onclick="PostBio" disabled="@loading">
|
||||||
|
@if (loading) {
|
||||||
|
<span>Saving...</span>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i> <span>Save</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private MarkdownEditor? Editor;
|
||||||
|
public string? Bio { get; set; }
|
||||||
|
[Parameter]
|
||||||
|
public string? Address { get; set; }
|
||||||
|
private bool loading = true;
|
||||||
|
[Parameter]
|
||||||
|
public string? id { get; set; }
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
Bio = await api.StatuslogBio(Address ?? State.SelectedAddressName!);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await Editor!.SetValueAsync(Bio);
|
||||||
|
loading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PostBio() {
|
||||||
|
loading = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
// Post the bio
|
||||||
|
await api.PostStatuslogBio(Address!, Bio ?? string.Empty);
|
||||||
|
State.CachedAddressBio = Utilities.MdToHtmlMarkup(Bio ?? string.Empty);
|
||||||
|
|
||||||
|
await JS.InvokeVoidAsync("ui", "#" + id);
|
||||||
|
// reset input
|
||||||
|
await OnInitializedAsync();
|
||||||
|
loading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
State.SendRefresh();
|
||||||
|
}
|
||||||
|
}
|
130
Components/EditPasteDialog.razor
Normal file
130
Components/EditPasteDialog.razor
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject State State
|
||||||
|
@inject ApiService api
|
||||||
|
|
||||||
|
<div class="overlay" data-ui="#@id"></div>
|
||||||
|
<dialog id="@id">
|
||||||
|
<div class="row">
|
||||||
|
<div class="field text label border max">
|
||||||
|
<InputText @bind-Value="Title"></InputText>
|
||||||
|
<label>Content</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="field textarea label border max">
|
||||||
|
<InputTextArea @bind-Value="Content"></InputTextArea>
|
||||||
|
<label>Content</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="no-space">
|
||||||
|
@if (Paste != null)
|
||||||
|
{
|
||||||
|
if (confirmDelete)
|
||||||
|
{
|
||||||
|
<button @onclick="ConfirmDeletePaste" disabled="@loading" class="red-7-bg white-fg">
|
||||||
|
<i class="fa-solid fa-exclamation-triangle"></i> <span>Are you sure?</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<button @onclick="DeletePaste" disabled="@loading" class="red-7-bg white-fg">
|
||||||
|
<i class="fa-solid fa-trash"></i> <span>Delete</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<div class="max"></div>
|
||||||
|
<label class="checkbox">
|
||||||
|
<InputCheckbox @bind-Value="Listed"></InputCheckbox>
|
||||||
|
<span>Listed?</span>
|
||||||
|
</label>
|
||||||
|
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
|
||||||
|
<button @onclick="PostPaste" disabled="@loading">
|
||||||
|
@if (loading) {
|
||||||
|
<span>Saving...</span>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i> <span>Save</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private Paste? _paste;
|
||||||
|
|
||||||
|
public Paste? Paste {
|
||||||
|
get => _paste;
|
||||||
|
set {
|
||||||
|
_paste = value;
|
||||||
|
Title = _paste?.Title;
|
||||||
|
Content = _paste?.Content;
|
||||||
|
Listed = _paste?.IsListed ?? false;
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? Content { get; set; }
|
||||||
|
public bool Listed { get; set; }
|
||||||
|
private bool loading = false;
|
||||||
|
[Parameter]
|
||||||
|
public string? id { get; set; }
|
||||||
|
private bool confirmDelete { get; set; }
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
Title = Paste?.Title;
|
||||||
|
Content = Paste?.Content;
|
||||||
|
Listed = Paste?.IsListed ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeletePaste() {
|
||||||
|
if (!confirmDelete) confirmDelete = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ConfirmDeletePaste() {
|
||||||
|
if (confirmDelete) {
|
||||||
|
loading = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Paste?.Title)) {
|
||||||
|
await api.DeletePaste(State.SelectedAddressName!, Paste.Title);
|
||||||
|
await State.RefreshPastes();
|
||||||
|
State.SendRefresh();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
await JS.InvokeVoidAsync("ui", "#" + id);
|
||||||
|
// clear input
|
||||||
|
Title = string.Empty;
|
||||||
|
Content = string.Empty;
|
||||||
|
Listed = false;
|
||||||
|
loading = false;
|
||||||
|
confirmDelete = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PostPaste() {
|
||||||
|
loading = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Paste?.Title)) {
|
||||||
|
await api.PostPaste(State.SelectedAddressName!, Title, Content, Listed);
|
||||||
|
await State.RefreshPastes();
|
||||||
|
State.SendRefresh();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
await JS.InvokeVoidAsync("ui", "#" + id);
|
||||||
|
// clear input
|
||||||
|
Paste = null;
|
||||||
|
Title = string.Empty;
|
||||||
|
Content = string.Empty;
|
||||||
|
Listed = false;
|
||||||
|
confirmDelete = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,30 @@
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@inject State State
|
@inject State State
|
||||||
@inject RestService api
|
@inject ApiService api
|
||||||
|
|
||||||
<div class="overlay" data-ui="#@id"></div>
|
<div class="overlay" data-ui="#@id"></div>
|
||||||
<dialog id="@id">
|
<dialog id="@id">
|
||||||
<img src="@Pic?.Url" />
|
<div class="padding center-align">
|
||||||
|
<img src="@Pic?.Url" class="small-height square" />
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="field textarea label border max">
|
<div class="field textarea label border max">
|
||||||
<InputTextArea @bind-Value="Description"></InputTextArea>
|
<InputTextArea @bind-Value="Description"></InputTextArea>
|
||||||
<label>Description</label>
|
<label>Description</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav class="right-align no-space">
|
<nav class="no-space">
|
||||||
|
@if (confirmDelete) {
|
||||||
|
<button @onclick="ConfirmDeletePic" disabled="@loading" class="red-7-bg white-fg">
|
||||||
|
<i class="fa-solid fa-exclamation-triangle"></i> <span>Are you sure?</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<button @onclick="DeletePic" disabled="@loading" class="red-7-bg white-fg">
|
||||||
|
<i class="fa-solid fa-trash"></i> <span>Delete</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<div class="max"></div>
|
||||||
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
|
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
|
||||||
<button @onclick="PostPic" disabled="@loading">
|
<button @onclick="PostPic" disabled="@loading">
|
||||||
@if (loading) {
|
@if (loading) {
|
||||||
|
@ -32,24 +45,56 @@
|
||||||
set {
|
set {
|
||||||
_pic = value;
|
_pic = value;
|
||||||
Description = _pic?.Description;
|
Description = _pic?.Description;
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
private bool loading = false;
|
private bool loading = false;
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string id { get; set; }
|
public string? id { get; set; }
|
||||||
|
private bool confirmDelete { get; set; }
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
|
await base.OnInitializedAsync();
|
||||||
Description = Pic?.Description;
|
Description = Pic?.Description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DeletePic() {
|
||||||
|
if (!confirmDelete) confirmDelete = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ConfirmDeletePic() {
|
||||||
|
if (confirmDelete) {
|
||||||
|
loading = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Pic?.Id)) {
|
||||||
|
await api.DeletePic(State.SelectedAddressName!, Pic.Id);
|
||||||
|
await State.RefreshPics();
|
||||||
|
State.SendRefresh();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
await JS.InvokeVoidAsync("ui", "#" + id);
|
||||||
|
// clear input
|
||||||
|
Description = string.Empty;
|
||||||
|
Pic = null;
|
||||||
|
loading = false;
|
||||||
|
confirmDelete = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task PostPic() {
|
public async Task PostPic() {
|
||||||
loading = true;
|
loading = true;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
if(!string.IsNullOrEmpty(Pic.Id)) {
|
if(!string.IsNullOrEmpty(Pic?.Id)) {
|
||||||
await api.PostPicDescription(State.SelectedAddressName, Pic.Id, Description);
|
await api.PostPicDescription(State.SelectedAddressName!, Pic.Id, Description);
|
||||||
await State.RefreshPics();
|
await State.RefreshPics();
|
||||||
|
State.SendRefresh();
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +103,7 @@
|
||||||
Description = string.Empty;
|
Description = string.Empty;
|
||||||
Pic = null;
|
Pic = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
|
confirmDelete = false;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
117
Components/EditProfilePicDialog.razor
Normal file
117
Components/EditProfilePicDialog.razor
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject State State
|
||||||
|
@inject ApiService 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@inject State State
|
@inject State State
|
||||||
@inject RestService api
|
@inject ApiService api
|
||||||
|
|
||||||
<div class="overlay" data-ui="#@id"></div>
|
<div class="overlay" data-ui="#@id"></div>
|
||||||
<dialog id="@id">
|
<dialog id="@id">
|
||||||
|
@ -29,7 +29,18 @@
|
||||||
<label>Status</label>
|
<label>Status</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav class="right-align no-space">
|
<nav class="no-space">
|
||||||
|
@if (confirmDelete) {
|
||||||
|
<button @onclick="ConfirmDeleteStatus" disabled="@loading" class="red-7-bg white-fg">
|
||||||
|
<i class="fa-solid fa-exclamation-triangle"></i> <span>Are you sure?</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<button @onclick="DeleteStatus" disabled="@loading" class="red-7-bg white-fg">
|
||||||
|
<i class="fa-solid fa-trash"></i> <span>Delete</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<div class="max"></div>
|
||||||
<InputText id="status-emoji-input" class="invisible" @bind-Value="Emoji"></InputText>
|
<InputText id="status-emoji-input" class="invisible" @bind-Value="Emoji"></InputText>
|
||||||
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
|
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
|
||||||
<button @onclick="PatchStatus" disabled="@loading">
|
<button @onclick="PatchStatus" disabled="@loading">
|
||||||
|
@ -59,20 +70,51 @@
|
||||||
public string? Emoji { get; set; }
|
public string? Emoji { get; set; }
|
||||||
private bool loading = false;
|
private bool loading = false;
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string id { get; set; }
|
public string? id { get; set; }
|
||||||
|
private bool confirmDelete { get; set; }
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
|
await base.OnInitializedAsync();
|
||||||
Content = Status?.Content;
|
Content = Status?.Content;
|
||||||
Emoji = Status?.Emoji;
|
Emoji = Status?.Emoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DeleteStatus() {
|
||||||
|
if (!confirmDelete) confirmDelete = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ConfirmDeleteStatus() {
|
||||||
|
if(confirmDelete) {
|
||||||
|
loading = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Status?.Id)) {
|
||||||
|
await api.DeleteStatus(State.SelectedAddressName!, Status.Id);
|
||||||
|
await State.RefreshStatuses();
|
||||||
|
State.SendRefresh();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
await JS.InvokeVoidAsync("ui", "#" + id);
|
||||||
|
// clear input
|
||||||
|
Content = string.Empty;
|
||||||
|
Emoji = string.Empty;
|
||||||
|
Status = null;
|
||||||
|
loading = false;
|
||||||
|
confirmDelete = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task PatchStatus() {
|
public async Task PatchStatus() {
|
||||||
loading = true;
|
loading = true;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Status?.Id)) {
|
if (!string.IsNullOrEmpty(Status?.Id)) {
|
||||||
await api.PatchStatus(State.SelectedAddressName, Status.Id, Content, Emoji);
|
await api.PatchStatus(State.SelectedAddressName!, Status.Id, Content ?? String.Empty, Emoji);
|
||||||
await State.RefreshStatuses();
|
await State.RefreshStatuses();
|
||||||
|
State.SendRefresh();
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +124,7 @@
|
||||||
Emoji = string.Empty;
|
Emoji = string.Empty;
|
||||||
Status = null;
|
Status = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
|
confirmDelete = false;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@inject State State
|
@inject State State
|
||||||
@inject RestService api
|
@inject ApiService api
|
||||||
@if(Html != null) {
|
@if(Html != null) {
|
||||||
<iframe id="@id" frameborder="0" scrolling="no" srcdoc="@Html" onload="() => iframeResize({ license: 'GPLv3' })"></iframe>
|
<iframe id="@id" frameborder="0" scrolling="no" srcdoc="@Html" onload="() => iframeResize({ license: 'GPLv3' })"></iframe>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Url { get; set; }
|
public string? Url { get; set; }
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string id { get; set; }
|
public string? id { get; set; }
|
||||||
|
[Parameter]
|
||||||
|
public string? SrcString { get; set; }
|
||||||
public MarkupString? Html { get; set; }
|
public MarkupString? Html { get; set; }
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||||
|
@ -19,13 +21,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Reload() {
|
public async Task Reload() {
|
||||||
if (Html == null){
|
if (Url != null){
|
||||||
Html = await api.GetHtml(Url);
|
Html = await api.GetHtml(Url);
|
||||||
string? HtmlString = Html?.ToString();
|
SrcString = 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>");
|
if(SrcString != null) {
|
||||||
Html = (MarkupString)HtmlString;
|
SrcString = SrcString?.Replace("</head>", "<base target='_blank'></head>");
|
||||||
|
SrcString = SrcString?.Replace("</body>", "<script src='https://cdn.jsdelivr.net/npm/@iframe-resizer/child@5.1.5'></script></body>");
|
||||||
|
Html = (MarkupString)(SrcString ?? string.Empty);
|
||||||
}
|
}
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await IframeResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task IframeResize() {
|
||||||
|
await JS.InvokeVoidAsync("iframeResize", new { license = "GPLv3" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,38 +6,7 @@
|
||||||
<button class="transparent circle small large">
|
<button class="transparent circle small large">
|
||||||
<img class="responsive avatar" src="https://profiles.cache.lol/@State.SelectedAddressName/picture" alt="@State.SelectedAddressName">
|
<img class="responsive avatar" src="https://profiles.cache.lol/@State.SelectedAddressName/picture" alt="@State.SelectedAddressName">
|
||||||
<menu class="no-wrap">
|
<menu class="no-wrap">
|
||||||
<a class="m s row">
|
<AvatarMenuLinks></AvatarMenuLinks>
|
||||||
<i class="emoji medium" data-emoji="👋">👋</i>
|
|
||||||
<span>Hey, @State.Name.</span>
|
|
||||||
</a>
|
|
||||||
@foreach (AddressResponseData address in State.AddressList ?? new List<AddressResponseData>()) {
|
|
||||||
<a class="row @(address == State.SelectedAddress ? "active" : "")" @onclick="() => changeAddress(address)">
|
|
||||||
<img class="tiny circle avatar" src="https://profiles.cache.lol/@address.Address/picture" alt="@address.Address" />
|
|
||||||
<span class="address"><i class="fa-solid fa-fw fa-at tiny"></i>@address.Address</span>
|
|
||||||
</a>
|
|
||||||
if (address.Address == State.SelectedAddressName) {
|
|
||||||
<a class="indent row" href="/person/@State.SelectedAddressName#profile">
|
|
||||||
<i class="fa-solid fa-id-card"></i>
|
|
||||||
<span>Profile</span>
|
|
||||||
</a>
|
|
||||||
<a class="indent row" href="/person/@State.SelectedAddressName#statuses">
|
|
||||||
<i class="fa-solid fa-message-smile"></i>
|
|
||||||
<span>Statuslog</span>
|
|
||||||
</a>
|
|
||||||
<a class="indent row" href="/person/@State.SelectedAddressName#pics">
|
|
||||||
<i class="fa-solid fa-images"></i>
|
|
||||||
<span>Pics</span>
|
|
||||||
</a>
|
|
||||||
<a class="indent row" href="/person/@State.SelectedAddressName#now">
|
|
||||||
<i class="fa-duotone fa-seedling"></i>
|
|
||||||
<span>/Now</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<a class="row" @onclick='() => AuthStateProvider.Logout()'>
|
|
||||||
<i class="fa-solid fa-door-open"></i>
|
|
||||||
<span>Logout</span>
|
|
||||||
</a>
|
|
||||||
</menu>
|
</menu>
|
||||||
</button>
|
</button>
|
||||||
<small class="s m address"><i class="fa-solid fa-fw fa-at tiny"></i>@State.SelectedAddressName</small>
|
<small class="s m address"><i class="fa-solid fa-fw fa-at tiny"></i>@State.SelectedAddressName</small>
|
||||||
|
@ -49,30 +18,17 @@
|
||||||
</Authorized>
|
</Authorized>
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
<NavLink>
|
<NavLink>
|
||||||
<button class="transparent square large">
|
<button class="transparent square small large">
|
||||||
<img class="responsive" src="/img/prami-neighbourhood.svg">
|
<img class="responsive" src="/img/prami-neighbourhood.svg">
|
||||||
<menu class="no-wrap">
|
<menu class="no-wrap">
|
||||||
<a class="m s row">
|
<AvatarMenuLinks></AvatarMenuLinks>
|
||||||
<i class="emoji medium" data-emoji="👋">👋</i>
|
|
||||||
<span>Hey there.</span>
|
|
||||||
</a>
|
|
||||||
<a class="row" href="/login">
|
|
||||||
<i class="fa-solid fa-door-closed"></i>
|
|
||||||
<span>Login</span>
|
|
||||||
</a>
|
|
||||||
</menu>
|
</menu>
|
||||||
</button>
|
</button>
|
||||||
<small class="s m address">Omg.lol</small>
|
<small class="s m honey">Omg.lol</small>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<div class="l">
|
<div class="l">
|
||||||
Hey there. <br />
|
Hey there. <br />
|
||||||
<a href="/login">Login?</a>
|
<a href="/login">Login?</a>
|
||||||
</div>
|
</div>
|
||||||
</NotAuthorized>
|
</NotAuthorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
|
|
||||||
@code {
|
|
||||||
public void changeAddress(AddressResponseData address) {
|
|
||||||
State.SelectedAddress = address;
|
|
||||||
}
|
|
||||||
}
|
|
59
Components/Layout/AvatarMenuLinks.razor
Normal file
59
Components/Layout/AvatarMenuLinks.razor
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
@inject CustomAuthenticationStateProvider AuthStateProvider;
|
||||||
|
@inject State State;
|
||||||
|
<a class="m s row">
|
||||||
|
<i class="emoji medium" data-emoji="👋">👋</i>
|
||||||
|
<span>Hey, @(State.Name ?? "there").</span>
|
||||||
|
</a>
|
||||||
|
<a class="s row" href="/now">
|
||||||
|
<i class="square fa-duotone fa-seedling"></i>
|
||||||
|
<span>Now.garden</span>
|
||||||
|
</a>
|
||||||
|
<a class="s row" href="/directory">
|
||||||
|
<i class="square fa-duotone fa-address-book"></i>
|
||||||
|
<span>Directory</span>
|
||||||
|
</a>
|
||||||
|
@foreach (AddressResponseData address in State.AddressList ?? new List<AddressResponseData>()) {
|
||||||
|
<a class="row @(address == State.SelectedAddress ? "active" : "")" @onclick="() => State.SelectedAddress = address">
|
||||||
|
<img class="tiny circle avatar" src="https://profiles.cache.lol/@address.Address/picture" alt="@address.Address" />
|
||||||
|
<span class="address"><i class="fa-solid fa-fw fa-at tiny"></i>@address.Address</span>
|
||||||
|
</a>
|
||||||
|
if (address.Address == State.SelectedAddressName) {
|
||||||
|
<a class="indent row" href="/person/@State.SelectedAddressName#profile">
|
||||||
|
<i class="fa-solid fa-id-card"></i>
|
||||||
|
<span>Profile</span>
|
||||||
|
</a>
|
||||||
|
<a class="indent row" href="/person/@State.SelectedAddressName#statuses">
|
||||||
|
<i class="fa-solid fa-message-smile"></i>
|
||||||
|
<span>Statuslog</span>
|
||||||
|
</a>
|
||||||
|
<a class="indent row" href="/person/@State.SelectedAddressName#pics">
|
||||||
|
<i class="fa-solid fa-images"></i>
|
||||||
|
<span>Pics</span>
|
||||||
|
</a>
|
||||||
|
<a class="indent row" href="/person/@State.SelectedAddressName#now">
|
||||||
|
<i class="fa-duotone fa-seedling"></i>
|
||||||
|
<span>/Now</span>
|
||||||
|
</a>
|
||||||
|
<a class="indent row" href="/person/@State.SelectedAddressName#pastebin">
|
||||||
|
<i class="fa-solid fa-clipboard"></i>
|
||||||
|
<span>Pastebin</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (State.IsAuthorized) {
|
||||||
|
<a class="row" @onclick='() => AuthStateProvider.Logout()'>
|
||||||
|
<i class="fa-solid fa-door-open"></i>
|
||||||
|
<span>Logout</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<a class="row" href="/login">
|
||||||
|
<i class="fa-solid fa-door-closed"></i>
|
||||||
|
<span>Login</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
<a class="row medium-opacity">
|
||||||
|
<i class="fa-solid fa-circle-info tiny"></i>
|
||||||
|
<small>@App.Name - @App.Version</small>
|
||||||
|
</a>
|
|
@ -2,83 +2,13 @@
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@inject NavigatorService NavigatorService
|
@inject NavigatorService NavigatorService
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject IJSRuntime JS
|
|
||||||
@inject State State
|
@inject State State
|
||||||
<NavMenu />
|
<NavMenu />
|
||||||
|
|
||||||
<main class="responsive max">
|
<main class="responsive max">
|
||||||
@Body
|
@Body
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
|
||||||
class Refresher {
|
|
||||||
constructor(targetEl) {
|
|
||||||
this.lastKnownScrollPosition = 0
|
|
||||||
this.ticking = false
|
|
||||||
this.atTop = true
|
|
||||||
this.atBottom = false
|
|
||||||
this.target = targetEl
|
|
||||||
|
|
||||||
this.target.addEventListener("scroll", this.scrollEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
scrolledTo = (scrollPos) => {
|
|
||||||
// Do something with the scroll position
|
|
||||||
if (this.atTop && scrollPos > 0) {
|
|
||||||
this.atTop = false
|
|
||||||
CSHARP.invokeMethodAsync("SetAtTop", false)
|
|
||||||
}
|
|
||||||
else if (!this.atTop && scrollPos == 0) {
|
|
||||||
// console.log("AT TOP")
|
|
||||||
this.atTop = true
|
|
||||||
CSHARP.invokeMethodAsync("SetAtTop", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const bottom = this.target.scrollHeight - Math.ceil(this.target.offsetHeight)
|
|
||||||
|
|
||||||
if (this.atBottom && scrollPos < bottom) {
|
|
||||||
this.atBottom = false
|
|
||||||
CSHARP.invokeMethodAsync("SetAtBottom", false)
|
|
||||||
}
|
|
||||||
else if (!this.atBottom && scrollPos >= bottom) {
|
|
||||||
// console.log("AT BOTTOM")
|
|
||||||
this.atBottom = true
|
|
||||||
CSHARP.invokeMethodAsync("SetAtBottom", true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollEvent = (event) => {
|
|
||||||
this.lastKnownScrollPosition = Math.ceil(event.target.scrollTop)
|
|
||||||
|
|
||||||
if (!this.ticking) {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
this.scrolledTo(this.lastKnownScrollPosition)
|
|
||||||
this.ticking = false
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ticking = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Dispose = () => {
|
|
||||||
this.target.removeEventListener("scroll", this.scrollEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
new Refresher(document.querySelector('main'))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
private DotNetObjectReference<State>? DotNetRef { get; set; }
|
|
||||||
protected override void OnAfterRender(bool firstRender) {
|
|
||||||
base.OnAfterRender(firstRender);
|
|
||||||
if (firstRender) {
|
|
||||||
// See warning about memory above in the article
|
|
||||||
DotNetRef = DotNetObjectReference.Create(State);
|
|
||||||
JS.InvokeVoidAsync("injectCSharp", DotNetRef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnInitialized() {
|
protected override void OnInitialized() {
|
||||||
base.OnInitialized();
|
base.OnInitialized();
|
||||||
NavigatorService.NavigationManager = NavigationManager;
|
NavigatorService.NavigationManager = NavigationManager;
|
||||||
|
@ -90,17 +20,14 @@
|
||||||
|
|
||||||
private void IntentRecieved(object? sender = null, EventArgs? e = null) {
|
private void IntentRecieved(object? sender = null, EventArgs? e = null) {
|
||||||
if (!string.IsNullOrEmpty(State.ShareString)) {
|
if (!string.IsNullOrEmpty(State.ShareString)) {
|
||||||
NavigationManager.NavigateTo($"/sharetext/{State.ShareString}");
|
NavigationManager.NavigateTo($"/sharetext");
|
||||||
State.ShareString = null;
|
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrEmpty(State.SharePhoto)) {
|
else if (!string.IsNullOrEmpty(State.SharePhoto)) {
|
||||||
NavigationManager.NavigateTo($"/sharepic/{State.SharePhoto}");
|
NavigationManager.NavigateTo($"/sharepic");
|
||||||
State.SharePhoto = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void IDisposable.Dispose() {
|
void IDisposable.Dispose() {
|
||||||
State.IntentReceived -= IntentRecieved;
|
State.IntentReceived -= IntentRecieved;
|
||||||
DotNetRef?.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<NavLink class="nav-link" href="/statuslog/latest">
|
@inject State State
|
||||||
|
<NavLink class="nav-link" href="/statuslog/latest">
|
||||||
<i class="square fa-solid fa-message-smile"></i>
|
<i class="square fa-solid fa-message-smile"></i>
|
||||||
<div class="label">Status.lol</div>
|
<div class="label">Status.lol</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -10,7 +11,38 @@
|
||||||
<i class="square fa-light fa-comment-dots"></i>
|
<i class="square fa-light fa-comment-dots"></i>
|
||||||
<div class="label">Eph.emer.al</div>
|
<div class="label">Eph.emer.al</div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink class="nav-link" href="/now">
|
@if (FeatureFlags.Following && State.IsAuthorized) {
|
||||||
<i class="square fa-duotone fa-seedling"></i>
|
<NavLink class="nav-link" href="/feed">
|
||||||
<div class="label">Now.garden</div>
|
<i class="square fa-solid fa-list-timeline"></i>
|
||||||
</NavLink>
|
<div class="label">Timeline</div>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink class="l m nav-link" href="/now">
|
||||||
|
<i class="square fa-duotone fa-seedling"></i>
|
||||||
|
<div class="label">Now.garden</div>
|
||||||
|
</NavLink>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<NavLink class="nav-link" href="/now">
|
||||||
|
<i class="square fa-duotone fa-seedling"></i>
|
||||||
|
<div class="label">Now.garden</div>
|
||||||
|
</NavLink>
|
||||||
|
}
|
||||||
|
<NavLink class="l m nav-link" href="/directory">
|
||||||
|
<i class="square fa-duotone fa-address-book"></i>
|
||||||
|
<div class="label">Directory</div>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
State.PropertyChanged += StateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
if (e.PropertyName == nameof(State.AccountInfo)) await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
State.PropertyChanged -= StateChanged;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,9 +8,9 @@
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string id { get; set; }
|
public string? id { get; set; }
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? @class { get; set; }
|
public string? @class { get; set; }
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string icon { get; set; }
|
public string? icon { get; set; }
|
||||||
}
|
}
|
||||||
|
|
58
Components/NewEphemeral.razor
Normal file
58
Components/NewEphemeral.razor
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject State State
|
||||||
|
@inject ApiService 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>Share a fleeting thought</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="field textarea border max">
|
||||||
|
<InputTextArea @bind-Value="Content"></InputTextArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<p>Your anonymous post will be shared for a while, and then it will disappear forever. It can’t be edited, so take care before submitting.</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<p><strong>If you need help, don’t suffer in silence. <a href="https://www.helpguide.org/find-help.htm">Talk to someone right now</a>.</strong></p>
|
||||||
|
</div>
|
||||||
|
<nav class="right-align no-space">
|
||||||
|
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
|
||||||
|
<button @onclick="PostEphemeral" disabled="@loading">
|
||||||
|
@if (loading) {
|
||||||
|
<span>Saving...</span>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i> <span>Save</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public string? id { get; set; }
|
||||||
|
[Parameter]
|
||||||
|
public bool Active { get; set; }
|
||||||
|
[Parameter]
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private bool loading = false;
|
||||||
|
|
||||||
|
public async Task PostEphemeral() {
|
||||||
|
loading = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
var result = await api.PostEphemeral(Content);
|
||||||
|
if (result != null) {
|
||||||
|
await State.RefreshStatuses();
|
||||||
|
State.SendRefresh();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Active = false;
|
||||||
|
await JS.InvokeVoidAsync("ui", "#" + id);
|
||||||
|
Content = string.Empty;
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +1,20 @@
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@inject State State
|
@inject State State
|
||||||
@inject RestService api
|
@inject ApiService api
|
||||||
|
@inject NavigationManager navigationManager
|
||||||
|
|
||||||
<div class="overlay" data-ui="#@id"></div>
|
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
|
||||||
<dialog id="@id">
|
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active">
|
||||||
<h5>Share a picture</h5>
|
<h5>Share a picture</h5>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button @onclick="PicFromMedia"><i class="fa-solid fa-image"></i> Select a picture</button>
|
<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>
|
<button @onclick="PicFromPhoto"><i class="fa-solid fa-camera"></i> Take a photo</button>
|
||||||
@* <div class="field label prefix border">
|
</div>
|
||||||
<i class="fa-solid fa-image"></i>
|
<div class="row">
|
||||||
<InputFile OnChange="@ChangeFile" accept="image/gif, image/heic, image/heif, image/jpeg, image/png, image/svg+xml, image/webp"></InputFile>
|
@if(Base64File != null && FileSize != null){
|
||||||
<input type="text">
|
|
||||||
<label>Select a picture</label>
|
|
||||||
</div> *@
|
|
||||||
@if(File != null && Base64File != null && FileSize != null){
|
|
||||||
<img class="extra" src="@Base64Url">
|
<img class="extra" src="@Base64Url">
|
||||||
<small>
|
<small>
|
||||||
@File.ContentType (@formatSizeUnits(FileSize))
|
@FileContentType (@formatSizeUnits(FileSize))
|
||||||
</small>
|
</small>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,29 +40,39 @@
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
// private IBrowserFile? File { get; set; }
|
// 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? Description { get; set; }
|
||||||
|
[Parameter]
|
||||||
|
public string? id { get; set; }
|
||||||
|
[Parameter]
|
||||||
|
public bool Active { get; set; }
|
||||||
|
|
||||||
|
private bool loading = false;
|
||||||
|
|
||||||
private FileResult? File { get; set; }
|
private FileResult? File { get; set; }
|
||||||
private string? Base64File { get; set; }
|
|
||||||
private long? FileSize { get; set; }
|
|
||||||
private string? Base64Url {
|
private string? Base64Url {
|
||||||
get {
|
get {
|
||||||
if(File == null || Base64File == null) return null;
|
if (FileContentType == null || Base64File == null) return null;
|
||||||
|
|
||||||
return $"data:{File.ContentType};base64,{Base64File}";
|
return $"data:{FileContentType};base64,{Base64File}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private string Description { get; set; }
|
|
||||||
private bool loading = false;
|
|
||||||
[Parameter]
|
|
||||||
public string id { get; set; }
|
|
||||||
|
|
||||||
public async Task PostPic() {
|
public async Task PostPic() {
|
||||||
loading = true;
|
loading = true;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
PutPicResponseData? response = await api.PutPic(State.SelectedAddressName, File);
|
PutPicResponseData? response = await api.PutPic(State.SelectedAddressName!, Base64File!);
|
||||||
if(!string.IsNullOrEmpty(Description) && response != null && !string.IsNullOrEmpty(response.Id)) {
|
if(!string.IsNullOrEmpty(Description) && response != null && !string.IsNullOrEmpty(response.Id)) {
|
||||||
await api.PostPicDescription(State.SelectedAddressName, response.Id, Description);
|
await api.PostPicDescription(State.SelectedAddressName!, response.Id, Description);
|
||||||
await State.RefreshPics();
|
await State.RefreshPics();
|
||||||
|
State.SendRefresh();
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,10 +85,6 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// private async Task ChangeFile(InputFileChangeEventArgs e){
|
|
||||||
// File = e.File;
|
|
||||||
// }
|
|
||||||
|
|
||||||
private string formatSizeUnits(long? bytes){
|
private string formatSizeUnits(long? bytes){
|
||||||
if(bytes == null) return "?? bytes";
|
if(bytes == null) return "?? bytes";
|
||||||
string formatted = "0 bytes";
|
string formatted = "0 bytes";
|
||||||
|
@ -94,7 +97,7 @@
|
||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private async Task PicFromMedia(EventArgs e) {
|
private async Task PicFromMedia(EventArgs e) {
|
||||||
File = await MediaPicker.Default.PickPhotoAsync();
|
File = await MediaPicker.Default.PickPhotoAsync();
|
||||||
|
@ -107,7 +110,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PopulateFileDetails() {
|
private async Task PopulateFileDetails() {
|
||||||
FileSize = await Utilities.FileSize(File);
|
if (File == null)
|
||||||
Base64File = await Utilities.Base64FromFile(File);
|
{
|
||||||
|
FileContentType = null;
|
||||||
|
FileSize = null;
|
||||||
|
Base64File = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FileContentType = File.ContentType;
|
||||||
|
FileSize = await Utilities.FileSize(File);
|
||||||
|
Base64File = await Utilities.Base64FromFile(File);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@inject State State
|
@inject State State
|
||||||
@inject RestService api
|
@inject ApiService api
|
||||||
|
@inject NavigationManager navigationManager
|
||||||
|
@inject NavigatorService navigatorService
|
||||||
|
|
||||||
<div class="overlay" data-ui="#@id"></div>
|
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
|
||||||
<dialog id="@id">
|
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active">
|
||||||
<h5>Share your status</h5>
|
<h5>Share your status</h5>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="min square extra">
|
<div class="min square extra">
|
||||||
<button class="transparent square extra no-margin">
|
<button class="transparent square extra no-margin">
|
||||||
<object id="status-emoji" class="large emoji @(statusEmoji == null ? "animated" : string.Empty)" data-emoji="@(statusEmoji ?? "🫥")">@(statusEmoji ?? "🫥")</object>
|
<object id="new-status-emoji" class="large emoji @(Emoji == null ? "animated" : string.Empty)" data-emoji="@(Emoji ?? "🫥")">@(Emoji ?? "🫥")</object>
|
||||||
<menu class="no-wrap">
|
<menu class="no-wrap">
|
||||||
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@@^1/index.js"></script>
|
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@@^1/index.js"></script>
|
||||||
<emoji-picker emoji-version="15.1"></emoji-picker>
|
<emoji-picker emoji-version="15.1"></emoji-picker>
|
||||||
<script>
|
<script>
|
||||||
document.querySelector('emoji-picker')
|
document.querySelector('emoji-picker')
|
||||||
.addEventListener('emoji-click', event => {
|
.addEventListener('emoji-click', event => {
|
||||||
document.getElementById('status-emoji').setAttribute('data-emoji', event.detail.unicode)
|
document.getElementById('new-status-emoji').setAttribute('data-emoji', event.detail.unicode)
|
||||||
const input = document.getElementById('status-emoji-input')
|
const input = document.getElementById('new-status-emoji-input')
|
||||||
input.value = event.detail.unicode
|
input.value = event.detail.unicode
|
||||||
var event = new Event('change');
|
var event = new Event('change');
|
||||||
input.dispatchEvent(event);
|
input.dispatchEvent(event);
|
||||||
|
@ -26,7 +28,8 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="field textarea border max">
|
<div class="field textarea border max">
|
||||||
<InputTextArea @bind-Value="statusContent"></InputTextArea>
|
<textarea @bind="@Content" @bind:event="oninput" />
|
||||||
|
<div class="right-align"><small class="@( Content.Length >= 500 ? "red" : Content.Length >= 260 ? "yellow-text" : "")">@Content.Length / 500</small></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav class="right-align no-space">
|
<nav class="right-align no-space">
|
||||||
|
@ -36,7 +39,7 @@
|
||||||
<span>Post this to Mastodon</span>
|
<span>Post this to Mastodon</span>
|
||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
<InputText id="status-emoji-input" class="invisible" @bind-Value="statusEmoji"></InputText>
|
<InputText id="new-status-emoji-input" class="invisible" @bind-Value="Emoji"></InputText>
|
||||||
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
|
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
|
||||||
<button @onclick="PostStatus" disabled="@loading">
|
<button @onclick="PostStatus" disabled="@loading">
|
||||||
@if (loading) {
|
@if (loading) {
|
||||||
|
@ -50,36 +53,55 @@
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private string statusContent = string.Empty;
|
|
||||||
private string? statusEmoji = null;
|
|
||||||
private bool postToMastodon = true;
|
|
||||||
private bool loading = false;
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string id { get; set; }
|
public string? id { get; set; }
|
||||||
|
[Parameter]
|
||||||
|
public bool Active { get; set; }
|
||||||
|
[Parameter]
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
[Parameter]
|
||||||
|
public string? Emoji { get; set; } = null;
|
||||||
|
[Parameter]
|
||||||
|
public bool postToMastodon { get; set; } = true;
|
||||||
|
|
||||||
|
private bool loading = false;
|
||||||
|
|
||||||
public async Task PostStatus() {
|
public async Task PostStatus() {
|
||||||
await JS.InvokeVoidAsync("console.log", "hey from post status");
|
|
||||||
|
|
||||||
StatusPost post = new StatusPost
|
StatusPost post = new StatusPost {
|
||||||
{
|
Emoji = Emoji,
|
||||||
Emoji = statusEmoji,
|
Content = Content
|
||||||
Content = statusContent
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (State?.SelectedAddress?.Preferences?.Statuslog?.MastodonPosting ?? false){
|
if (State?.SelectedAddress?.Preferences?.Statuslog?.MastodonPosting ?? false) {
|
||||||
post.SkipMastodonPost = !postToMastodon;
|
post.SkipMastodonPost = !postToMastodon;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = true;
|
if(Content.Length >= 500) {
|
||||||
InvokeAsync(StateHasChanged);
|
bool answer = await navigatorService.Page!.DisplayAlert(
|
||||||
var result = await api.StatusPost(State.SelectedAddressName, post);
|
"Character limit reached",
|
||||||
if(result != null){
|
"Your message is over 500 characters, which is a lot for a status.\n"
|
||||||
State.RefreshStatuses().ContinueWith(t => InvokeAsync(StateHasChanged));
|
+ ((postToMastodon && !(post.SkipMastodonPost ?? true))? "If you continue, your post will not make it over to Mastodon.\n" : "")
|
||||||
|
+ "Do you wish to post it anyway?",
|
||||||
|
"Yes", "No"
|
||||||
|
);
|
||||||
|
if (!answer) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
var result = await api.StatusPost(State!.SelectedAddressName!, post);
|
||||||
|
if(result != null){
|
||||||
|
await State.RefreshStatuses();
|
||||||
|
State.SendRefresh();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
// navigationManager.NavigateTo("/statuslog/latest");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Active = false;
|
||||||
await JS.InvokeVoidAsync("ui", "#" + id);
|
await JS.InvokeVoidAsync("ui", "#" + id);
|
||||||
statusContent = string.Empty;
|
Content = string.Empty;
|
||||||
statusEmoji = null;
|
Emoji = null;
|
||||||
postToMastodon = true;
|
postToMastodon = true;
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,9 @@
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string icon { get; set; }
|
public string? icon { get; set; }
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string title { get; set; }
|
public string? title { get; set; }
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public RenderFragment Description { get; set; }
|
public RenderFragment? Description { get; set; }
|
||||||
}
|
}
|
||||||
|
|
88
Components/Pages/Directory.razor
Normal file
88
Components/Pages/Directory.razor
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
@page "/directory"
|
||||||
|
@using System.Text.RegularExpressions
|
||||||
|
@using System.Globalization
|
||||||
|
@implements IDisposable
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject State State
|
||||||
|
|
||||||
|
<RefreshButton></RefreshButton>
|
||||||
|
|
||||||
|
<PageHeading title="Address Directory" icon="fa-duotone fa-address-book">
|
||||||
|
<Description>Welcome to the <a href="https://home.omg.lol/directory">omg.lol member directory</a>! Everyone here is awesome. <i class="fa-solid fa-sparkles"></i></Description>
|
||||||
|
</PageHeading>
|
||||||
|
|
||||||
|
@if (groupedAddresses != null) {
|
||||||
|
IdnMapping idn = new IdnMapping();
|
||||||
|
<article id="directoryIndex" class="responsive">
|
||||||
|
<nav class="wrap">
|
||||||
|
@foreach (var group in groupedAddresses) {
|
||||||
|
<a @onclick='()=>{JS.InvokeVoidAsync("scrollToId", $"index-{group.Key}");}' class="button circle transparent honey">@group.Key</a>
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
|
</article>
|
||||||
|
<article id="directory" class="responsive">
|
||||||
|
@foreach(var group in groupedAddresses) {
|
||||||
|
<h3 class="honey" id="index-@group.Key">— @group.Key —</h3>
|
||||||
|
<ul>
|
||||||
|
@foreach(string address in group) {
|
||||||
|
string displayAddress = address;
|
||||||
|
string linkAddress = address;
|
||||||
|
if (group.Key == "😀") {
|
||||||
|
try {
|
||||||
|
linkAddress = idn.GetAscii(address);
|
||||||
|
displayAddress = $"{address} {linkAddress}";
|
||||||
|
}
|
||||||
|
catch (Exception) { }
|
||||||
|
}
|
||||||
|
<li>
|
||||||
|
<a class="chip medium no-border tiny-margin transparent" href="/person/@address">
|
||||||
|
<img class="circle avatar responsive" src="https://profiles.cache.lol/@linkAddress/picture">
|
||||||
|
<span>@displayAddress</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<LoadingCard id="address-loading" icon="fa-duotone fa-address-book"></LoadingCard>
|
||||||
|
}
|
||||||
|
@code {
|
||||||
|
private List<string>? addresses;
|
||||||
|
private IOrderedEnumerable<IGrouping<string, string>>? groupedAddresses;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
if (addresses == null || addresses.Count == 0){
|
||||||
|
addresses = await State.GetDirectory();
|
||||||
|
GroupAddresses();
|
||||||
|
}
|
||||||
|
State.PropertyChanged += StateChanged;
|
||||||
|
State.CanRefresh = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
|
||||||
|
using (State.GetRefreshToken()) {
|
||||||
|
addresses = await State.GetDirectory(true);
|
||||||
|
GroupAddresses();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GroupAddresses() {
|
||||||
|
groupedAddresses = addresses?.GroupBy(s => {
|
||||||
|
if (Regex.IsMatch(s, "^[0-9]", RegexOptions.IgnoreCase)) return "#";
|
||||||
|
else if (Regex.IsMatch(s, "^[a-z]", RegexOptions.IgnoreCase)) return s.First().ToString().ToUpper();
|
||||||
|
else return "😀";
|
||||||
|
}).OrderBy(g => g.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
State.PropertyChanged -= StateChanged;
|
||||||
|
State.CanRefresh = false;
|
||||||
|
}
|
||||||
|
}
|
98
Components/Pages/EditNow.razor
Normal file
98
Components/Pages/EditNow.razor
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
@page "/editNow"
|
||||||
|
@inject NavigationManager Nav
|
||||||
|
@inject ApiService api
|
||||||
|
@inject State State
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
|
<div class="max markdown-editor">
|
||||||
|
@if (markdownValue != null)
|
||||||
|
{
|
||||||
|
<MarkdownEditor @ref="Editor"
|
||||||
|
@bind-Value="@markdownValue"
|
||||||
|
Theme="material-darker"
|
||||||
|
MaxHeight="100%"
|
||||||
|
CustomButtonClicked="@OnCustomButtonClicked"
|
||||||
|
AutoDownloadFontAwesome="false"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<label class="checkbox" disabled="@loading">
|
||||||
|
<InputCheckbox @bind-Value="listed" disabled="@loading"></InputCheckbox>
|
||||||
|
<span>Include my page in the Now Garden</span>
|
||||||
|
</label>
|
||||||
|
<div class="max"></div>
|
||||||
|
<button class="transparent link" onclick="history.back();" disabled="@loading">Cancel</button>
|
||||||
|
<button @onclick="Save" disabled="@loading">
|
||||||
|
@if (loading) {
|
||||||
|
<span>Saving...</span>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i> <span>Save</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private MarkdownEditor? Editor;
|
||||||
|
private bool listed;
|
||||||
|
private string? markdownValue;
|
||||||
|
|
||||||
|
private bool loading = true;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
NowContentData? data = await api.GetNowPage(State.SelectedAddressName!);
|
||||||
|
if (data != null)
|
||||||
|
{
|
||||||
|
listed = data.Listed == 1;
|
||||||
|
markdownValue = data.Content;
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
await Editor!.SetValueAsync(markdownValue);
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
Task OnMarkdownValueChanged(string value) {
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Save() {
|
||||||
|
loading = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
var result = await api.PostNowPage(State.SelectedAddressName!, markdownValue ?? string.Empty, listed);
|
||||||
|
if (result != null) {
|
||||||
|
await State.RefreshNow();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
Nav.NavigateTo($"/person/{State.SelectedAddressName}#now");
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnCustomButtonClicked(MarkdownButtonEventArgs eventArgs) {
|
||||||
|
if (eventArgs.Name == "Help") {
|
||||||
|
await JS.InvokeVoidAsync("open", "https://home.omg.lol/info/editor", "_blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
142
Components/Pages/EditProfile.razor
Normal file
142
Components/Pages/EditProfile.razor
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
@page "/editProfile"
|
||||||
|
@inject NavigationManager Nav
|
||||||
|
@inject ApiService api
|
||||||
|
@inject State State
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
|
<div class="max markdown-editor">
|
||||||
|
@if (markdownValue != null)
|
||||||
|
{
|
||||||
|
<MarkdownEditor @ref="Editor"
|
||||||
|
@bind-Value="@markdownValue"
|
||||||
|
Theme="material-darker"
|
||||||
|
MaxHeight="100%"
|
||||||
|
CustomButtonClicked="@OnCustomButtonClicked"
|
||||||
|
AutoDownloadFontAwesome="false"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
@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 <style> element in your page’s <head>.</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 page’s <head> element.</small>
|
||||||
|
<div class="field textarea label border max">
|
||||||
|
<InputTextArea @bind-Value="head"></InputTextArea>
|
||||||
|
<label>Additional <head> Content</label>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
<nav>
|
||||||
|
<div class="max"></div>
|
||||||
|
<button class="transparent link" onclick="history.back();" disabled="@loading">Cancel</button>
|
||||||
|
<button @onclick="Save" disabled="@loading">
|
||||||
|
@if (loading) {
|
||||||
|
<span>Saving...</span>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i> <span>Save & Publish</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private MarkdownEditor? Editor;
|
||||||
|
private string? markdownValue;
|
||||||
|
private string? css;
|
||||||
|
private string? head;
|
||||||
|
private string? theme;
|
||||||
|
private Theme? selectedTheme;
|
||||||
|
private Dictionary<string, Theme>? themes;
|
||||||
|
|
||||||
|
private bool loading = true;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
ProfileResponseData? data = await api.GetProfile(State.SelectedAddressName!);
|
||||||
|
if (data != null) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Task OnMarkdownValueChanged(string value) {
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Save() {
|
||||||
|
loading = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
var result = await api.PostProfile(State.SelectedAddressName!,
|
||||||
|
new PostProfile() {
|
||||||
|
Content = markdownValue ?? string.Empty,
|
||||||
|
Css = string.IsNullOrEmpty(css) ? null : css,
|
||||||
|
Head = string.IsNullOrEmpty(head) ? null : head,
|
||||||
|
Theme = string.IsNullOrEmpty(theme) ? null : theme
|
||||||
|
});
|
||||||
|
if (result != null) {
|
||||||
|
await State.RefreshNow();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
Nav.NavigateTo($"/person/{State.SelectedAddressName}#profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnCustomButtonClicked(MarkdownButtonEventArgs eventArgs) {
|
||||||
|
if (eventArgs.Name == "Help") {
|
||||||
|
await JS.InvokeVoidAsync("open", "https://home.omg.lol/info/editor", "_blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ThemeChanged(Theme? _theme) {
|
||||||
|
theme = _theme?.Id;
|
||||||
|
selectedTheme = _theme;
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,28 @@
|
||||||
@page "/ephemeral"
|
@page "/ephemeral"
|
||||||
|
@implements IDisposable
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@inject State State
|
@inject State State
|
||||||
|
|
||||||
|
<RefreshButton></RefreshButton>
|
||||||
|
|
||||||
<PageHeading title="Eph.emer.al" icon="fa-light fa-comment-dots">
|
<PageHeading title="Eph.emer.al" icon="fa-light fa-comment-dots">
|
||||||
<Description><a href="https://eph.emer.al">Eph.emer.al</a> is a place for fleeting thoughts. Everything on this page will disappear after a while.</Description>
|
<Description><a href="https://eph.emer.al">Eph.emer.al</a> is a place for fleeting thoughts. Everything on this page will disappear after a while.</Description>
|
||||||
</PageHeading>
|
</PageHeading>
|
||||||
|
|
||||||
|
<AuthorizeView>
|
||||||
|
<Authorized>
|
||||||
|
<button class="fab circle extra large-elevate" data-ui="#ephemeral-modal">
|
||||||
|
<i class="fa-light fa-comment-plus"></i>
|
||||||
|
</button>
|
||||||
|
<NewEphemeral id="ephemeral-modal"></NewEphemeral>
|
||||||
|
</Authorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
|
||||||
<div id="ephemeral" class="responsive">
|
<div id="ephemeral" class="responsive">
|
||||||
|
|
||||||
@if (messages != null) {
|
@if (messages != null) {
|
||||||
foreach (MarkupString message in messages) {
|
foreach (MarkupString message in messages) {
|
||||||
<article class="ephemeral center">
|
<article class="ephemeral">
|
||||||
@message
|
@message
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
|
@ -24,7 +37,23 @@
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
if (messages == null || messages.Count == 0) messages = await State.GetEphemeralMessages();
|
if (messages == null || messages.Count == 0) messages = await State.GetEphemeralMessages();
|
||||||
|
State.PropertyChanged += StateChanged;
|
||||||
|
State.CanRefresh = true;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
await JS.InvokeVoidAsync("removeElementById", "ephemeral-loading");
|
await JS.InvokeVoidAsync("removeElementById", "ephemeral-loading");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
|
||||||
|
using (State.GetRefreshToken()) {
|
||||||
|
messages = await State.GetEphemeralMessages(true);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
State.PropertyChanged -= StateChanged;
|
||||||
|
State.CanRefresh = false;
|
||||||
|
}
|
||||||
}
|
}
|
113
Components/Pages/Feed.razor
Normal file
113
Components/Pages/Feed.razor
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
@page "/feed"
|
||||||
|
@implements IDisposable
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject State State
|
||||||
|
@inject NavigationManager Nav
|
||||||
|
|
||||||
|
<RefreshButton></RefreshButton>
|
||||||
|
|
||||||
|
<PageHeading title="Timeline" icon="fa-solid fa-list-timeline">
|
||||||
|
<Description>A feed of all the statuses and pics of the people you follow.</Description>
|
||||||
|
</PageHeading>
|
||||||
|
|
||||||
|
@if(!(State.Following?.Any() ?? false)) {
|
||||||
|
<PageHeading title="" icon="fa-light fa-face-sad-sweat">
|
||||||
|
<Description>
|
||||||
|
It looks like you're not following anyone yet.
|
||||||
|
</Description>
|
||||||
|
</PageHeading>
|
||||||
|
<p class="center-align">Check out the <a href="/directory">Directory</a> (or other parts of the app) to find awesome people to follow.</p>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<div class="responsive">
|
||||||
|
<div class="tabs scroll">
|
||||||
|
<a data-ui="#feed" class="active">
|
||||||
|
<i class="fa-solid fa-list-timeline"></i>
|
||||||
|
<span>Timeline</span>
|
||||||
|
</a>
|
||||||
|
<a data-ui="#following">
|
||||||
|
<i class="fa-duotone fa-address-book"></i>
|
||||||
|
<span>Following</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="responsive page-container">
|
||||||
|
<div id="feed" class="page no-padding active">
|
||||||
|
@if (feed != null){
|
||||||
|
foreach (FeedItem item in feed) {
|
||||||
|
if (item.IsStatus) {
|
||||||
|
<StatusCard Status="@item.Status"></StatusCard>
|
||||||
|
}
|
||||||
|
else if (item.IsPic) {
|
||||||
|
<PicCard Pic="@item.Pic"></PicCard>
|
||||||
|
}
|
||||||
|
else if (item.IsPaste) {
|
||||||
|
<PasteCard Paste="@item.Paste"></PasteCard>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<LoadingCard id="feedLoading" icon="fa-solid fa-list-timeline"></LoadingCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="following" class="page no-padding">
|
||||||
|
<ul>
|
||||||
|
@foreach (string address in State.Following ?? new List<string>()) {
|
||||||
|
string displayAddress = address;
|
||||||
|
string linkAddress = address;
|
||||||
|
@* if (group.Key == "😀") {
|
||||||
|
try {
|
||||||
|
linkAddress = idn.GetAscii(address);
|
||||||
|
displayAddress = $"{address} {linkAddress}";
|
||||||
|
}
|
||||||
|
catch (Exception) { }
|
||||||
|
} *@
|
||||||
|
<li class="vertical-margin row padding surface-container">
|
||||||
|
<img class="round" src="https://profiles.cache.lol/@linkAddress/picture">
|
||||||
|
<div class="max">
|
||||||
|
<a href="/person/@linkAddress" class="address"><i class="fa-solid fa-fw fa-at tiny"></i>@displayAddress</a>
|
||||||
|
</div>
|
||||||
|
<button id="follow-button" @onclick="() => UnfollowClick(address)">
|
||||||
|
<i class="fa-solid fa-minus"></i> Unfollow
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private IOrderedEnumerable<FeedItem>? feed;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
string fragment = new Uri(Nav.Uri).Fragment;
|
||||||
|
await JS.InvokeVoidAsync("ui", fragment);
|
||||||
|
if (feed == null || feed.Count() == 0) feed = await State.GetFeed();
|
||||||
|
State.PropertyChanged += StateChanged;
|
||||||
|
State.CanRefresh = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await JS.InvokeVoidAsync("removeElementById", "feedLoading");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
|
||||||
|
using (State.GetRefreshToken()) {
|
||||||
|
feed = await State.GetFeed(true);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UnfollowClick(string address) {
|
||||||
|
await State.Unfollow(address);
|
||||||
|
feed = await State.GetFeed(forceRefresh: true);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
State.PropertyChanged -= StateChanged;
|
||||||
|
State.CanRefresh = false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,10 @@
|
||||||
@page "/now"
|
@page "/now"
|
||||||
|
@implements IDisposable
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@inject State State
|
@inject State State
|
||||||
|
|
||||||
|
<RefreshButton></RefreshButton>
|
||||||
|
|
||||||
<PageHeading title="Now.garden" icon="fa-duotone fa-seedling">
|
<PageHeading title="Now.garden" icon="fa-duotone fa-seedling">
|
||||||
<Description>Feel free to stroll through the <a href="now.garden">now.garden</a> and take a look at what people are up to.</Description>
|
<Description>Feel free to stroll through the <a href="now.garden">now.garden</a> and take a look at what people are up to.</Description>
|
||||||
</PageHeading>
|
</PageHeading>
|
||||||
|
@ -12,7 +16,7 @@
|
||||||
<article class="now">
|
<article class="now">
|
||||||
<nav>
|
<nav>
|
||||||
<a class="author" href="/person/@now.Address#now">
|
<a class="author" href="/person/@now.Address#now">
|
||||||
<h6><i class="fa-duotone fa-seedling"></i> @now.Address</h6>
|
<h6><i class="fa-duotone fa-seedling"></i><span>@now.Address</span></h6>
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
<nav>
|
<nav>
|
||||||
|
@ -31,7 +35,23 @@
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
if (garden == null || garden.Count == 0) garden = await State.GetNowGarden();
|
if (garden == null || garden.Count == 0) garden = await State.GetNowGarden();
|
||||||
|
State.PropertyChanged += StateChanged;
|
||||||
|
State.CanRefresh = true;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
await JS.InvokeVoidAsync("removeElementById", "now-loading");
|
await JS.InvokeVoidAsync("removeElementById", "now-loading");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
|
||||||
|
using (State.GetRefreshToken()){
|
||||||
|
garden = await State.GetNowGarden(true);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
State.PropertyChanged -= StateChanged;
|
||||||
|
State.CanRefresh = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,28 @@
|
||||||
<h3 class="page-heading"><i class="fa-solid fa-fw fa-at"></i>@Address</h3>
|
<h3 class="page-heading"><i class="fa-solid fa-fw fa-at"></i>@Address</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="row center-align">
|
<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>
|
</div>
|
||||||
|
@if (FeatureFlags.Following) {
|
||||||
|
<div class="row center-align">
|
||||||
|
@if (State.IsFollowing(Address)) {
|
||||||
|
<button id="follow-button" @onclick="async() => {await State.Unfollow(Address);await InvokeAsync(StateHasChanged);}">
|
||||||
|
<i class="fa-solid fa-minus"></i> Unfollow
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<button id="follow-button" @onclick="async() => {await State.Follow(Address);await InvokeAsync(StateHasChanged);}">
|
||||||
|
<i class="fa-solid fa-plus"></i> Follow
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div class="responsive">
|
<div class="responsive">
|
||||||
<div class="tabs scroll">
|
<div class="tabs scroll">
|
||||||
<a data-ui="#profile" @onclick="ReloadProfile">
|
<a data-ui="#profile" @onclick="ReloadProfile">
|
||||||
|
@ -26,12 +45,16 @@
|
||||||
<i class="fa-solid fa-images"></i>
|
<i class="fa-solid fa-images"></i>
|
||||||
<span>Some.pics</span>
|
<span>Some.pics</span>
|
||||||
</a>
|
</a>
|
||||||
@if(now != null){
|
@if(now != null || IsMe){
|
||||||
<a data-ui="#now" @onclick="ReloadNow">
|
<a data-ui="#now" @onclick="ReloadNow">
|
||||||
<i class="fa-duotone fa-seedling"></i>
|
<i class="fa-duotone fa-seedling"></i>
|
||||||
<span>/Now</span>
|
<span>/Now</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
<a data-ui="#pastebin">
|
||||||
|
<i class="fa-solid fa-clipboard"></i>
|
||||||
|
<span>Paste.lol</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -39,68 +62,103 @@
|
||||||
<div id="profile" class="page no-padding">
|
<div id="profile" class="page no-padding">
|
||||||
<a href="@ProfileUrl" target="_blank" class="hover absolute top right chip fill large-elevate">Open in browser <i class="fa-solid fa-arrow-up-right-from-square tiny"></i></a>
|
<a href="@ProfileUrl" target="_blank" class="hover absolute top right chip fill large-elevate">Open in browser <i class="fa-solid fa-arrow-up-right-from-square tiny"></i></a>
|
||||||
<ExternalPageComponent id="profile_page" @ref="ProfilePage" Url="@ProfileUrl"></ExternalPageComponent>
|
<ExternalPageComponent id="profile_page" @ref="ProfilePage" Url="@ProfileUrl"></ExternalPageComponent>
|
||||||
|
@if (IsMe) {
|
||||||
|
<a href="/editProfile" class="button fab circle extra large-elevate center-align middle-align">
|
||||||
|
<i class="square fa-solid fa-file-pen" style="line-height:56px;"></i>
|
||||||
|
<span>Edit</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="statuses" class="page padding active">
|
<div id="statuses" class="page padding active">
|
||||||
<div id="info" class="box basis transparent">
|
@if(bio == null || !string.IsNullOrEmpty(bio.ToString())) {
|
||||||
<article id="bio" class="container shadowed blue-2-bg gray-9-fg">
|
<div id="info" class="box basis transparent">
|
||||||
@if (bio == null) {
|
<article id="bio" class="container shadowed blue-2-bg gray-9-fg">
|
||||||
<p><progress class="circle small"></progress></p>
|
@if (bio == null) {
|
||||||
}
|
<p><progress class="circle small"></progress></p>
|
||||||
else {
|
}
|
||||||
@bio
|
else {
|
||||||
}
|
@bio
|
||||||
</article>
|
}
|
||||||
</div>
|
</article>
|
||||||
<StatusList @ref="StatusList" StatusFunc="@(async(forceRefresh) => await State.GetStatuses(Address, forceRefresh))" Editable="@Editable"></StatusList>
|
</div>
|
||||||
@if(Address == State.SelectedAddressName) {
|
}
|
||||||
|
@if (IsMe) {
|
||||||
|
<EditBioDialog id="edit-bio" Address="@Address"></EditBioDialog>
|
||||||
|
<div class="row center-align">
|
||||||
|
<button data-ui="#edit-bio"><i class="fa-solid fa-pencil"></i> Edit Bio</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<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">
|
<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>
|
</button>
|
||||||
<NewStatusDialog id="post-modal"></NewStatusDialog>
|
<NewStatusDialog id="post-modal"></NewStatusDialog>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="pics" class="page padding">
|
<div id="pics" class="page padding">
|
||||||
<PicList @ref="PicList" PicsFunc="@(async(forceRefresh) => await State.GetPics(Address, forceRefresh))" Editable="@Editable"></PicList>
|
<PicList @ref="PicList" PicsFunc="@(async(refresh) => await State.GetPics(Address, refresh))" Editable="@IsMe"></PicList>
|
||||||
@if (Address == State.SelectedAddressName) {
|
@if (IsMe) {
|
||||||
<button class="fab circle extra large-elevate" data-ui="#post-modal">
|
<button class="fab circle extra large-elevate" data-ui="#post-modal">
|
||||||
<i class="fa-solid fa-camera-retro"></i>
|
<i class="fa-solid fa-camera-retro"></i>
|
||||||
</button>
|
</button>
|
||||||
<NewPicDialog id="post-modal"></NewPicDialog>
|
<NewPicDialog id="post-modal"></NewPicDialog>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if(now != null){
|
@if(now != null || IsMe){
|
||||||
<div id="now" class="page no-padding">
|
<div id="now" class="page no-padding">
|
||||||
<a href="@now.Url" target="_blank" class="hover absolute top right chip fill large-elevate">Open in browser <i class="fa-solid fa-arrow-up-right-from-square tiny"></i></a>
|
<a href="@NowPageUrl" target="_blank" class="hover absolute top right chip fill large-elevate">Open in browser <i class="fa-solid fa-arrow-up-right-from-square tiny"></i></a>
|
||||||
<ExternalPageComponent id="now_page" @ref="NowPage" Url="@now.Url"></ExternalPageComponent>
|
<ExternalPageComponent id="now_page" @ref="NowPage" Url="@NowPageUrl"></ExternalPageComponent>
|
||||||
|
@if (IsMe) {
|
||||||
|
<a href="/editNow" class="button fab circle extra large-elevate center-align middle-align">
|
||||||
|
<i class="square fa-solid fa-file-pen" style="line-height:56px;"></i>
|
||||||
|
<span>Edit</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<div id="pastebin" class="page padding">
|
||||||
|
<PasteList @ref="PasteList" PastesFunc="@(async(refresh) => await State.GetPastes(Address, refresh))" Editable="@IsMe"></PasteList>
|
||||||
|
@if (IsMe) {
|
||||||
|
<button class="fab circle extra large-elevate" data-ui="#paste-modal">
|
||||||
|
<i class="fa-solid fa-clipboard-medical"></i>
|
||||||
|
</button>
|
||||||
|
<EditPasteDialog id="paste-modal"></EditPasteDialog>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private string _address;
|
private string _address = string.Empty;
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Address {
|
public string Address {
|
||||||
get => _address;
|
get => _address;
|
||||||
set {
|
set {
|
||||||
_address = value;
|
_address = value;
|
||||||
if (StatusList != null) StatusList.StatusFunc = async (forceRefresh) => await State.GetStatuses(_address, forceRefresh);
|
if (StatusList != null) StatusList.StatusFunc = async (refresh) => await State.GetStatuses(_address, refresh);
|
||||||
if(PicList != null) PicList.PicsFunc = async (bool forceRefresh) => await State.GetPics(_address, forceRefresh);
|
if (PicList != null) PicList.PicsFunc = async (refresh) => await State.GetPics(_address, refresh);
|
||||||
|
if (PasteList != null) PasteList.PastesFunc = async (refresh) => await State.GetPastes(_address, refresh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public string ProfileUrl {
|
public string ProfileUrl {
|
||||||
get => $"https://{Address}.omg.lol/";
|
get => $"https://{Address}.omg.lol/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string NowPageUrl {
|
||||||
|
get => now?.Url ?? $"https://{Address}.omg.lol/now";
|
||||||
|
}
|
||||||
|
|
||||||
public ExternalPageComponent? NowPage { get; set; }
|
public ExternalPageComponent? NowPage { get; set; }
|
||||||
public ExternalPageComponent? ProfilePage { get; set; }
|
public ExternalPageComponent? ProfilePage { get; set; }
|
||||||
|
|
||||||
private StatusList StatusList { get; set; }
|
private StatusList? StatusList { get; set; }
|
||||||
private PicList PicList { get; set; }
|
private PicList? PicList { get; set; }
|
||||||
|
private PasteList? PasteList { get; set; }
|
||||||
|
|
||||||
private bool Editable {
|
private bool IsMe {
|
||||||
get => Address == State.SelectedAddressName;
|
get => State.AddressList?.Any(a => a.Address == Address) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MarkupString? bio;
|
private MarkupString? bio;
|
||||||
|
@ -116,6 +174,18 @@
|
||||||
if (fragment.EndsWith("now")) await ReloadNow();
|
if (fragment.EndsWith("now")) await ReloadNow();
|
||||||
else if (fragment.EndsWith("profile")) await ReloadProfile();
|
else if (fragment.EndsWith("profile")) await ReloadProfile();
|
||||||
bio = await State.GetBio(Address);
|
bio = await State.GetBio(Address);
|
||||||
|
State.PropertyChanged += StateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
|
||||||
|
using (State.GetRefreshToken()){
|
||||||
|
await ReloadNow();
|
||||||
|
await ReloadProfile();
|
||||||
|
bio = await State.GetBio(Address);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReloadNow() {
|
private async Task ReloadNow() {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
@page "/pics"
|
@page "/pics"
|
||||||
@inject State State
|
@inject State State
|
||||||
|
|
||||||
|
<RefreshButton></RefreshButton>
|
||||||
|
|
||||||
<PageHeading title="Some.pics" icon="fa-solid fa-images">
|
<PageHeading title="Some.pics" icon="fa-solid fa-images">
|
||||||
<Description>Sit back, relax, and look at <a href="https://some.pics/">some.pics</a></Description>
|
<Description>Sit back, relax, and look at <a href="https://some.pics/">some.pics</a></Description>
|
||||||
</PageHeading>
|
</PageHeading>
|
||||||
|
@ -15,12 +17,9 @@
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
|
|
||||||
<div id="pics" class="responsive card-grid">
|
<div id="pics" class="responsive card-grid">
|
||||||
<PicList PicsFunc="@(async(bool forceRefresh) => await State.GetPics(forceRefresh))"></PicList>
|
<PicList PicsFunc="@(async(refresh) => await State.GetPics(refresh))"></PicList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RefreshButton></RefreshButton>
|
|
||||||
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
}
|
}
|
51
Components/Pages/SharePic.razor
Normal file
51
Components/Pages/SharePic.razor
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
@page "/sharepic"
|
||||||
|
@inject NavigationManager navigationManager
|
||||||
|
@inject AuthenticationStateProvider AuthStateProvider
|
||||||
|
@inject State State
|
||||||
|
|
||||||
|
<PageHeading title="Some.pics" icon="fa-solid fa-images">
|
||||||
|
<Description>Upload an image to <a href="https://some.pics/">some.pics</a></Description>
|
||||||
|
</PageHeading>
|
||||||
|
|
||||||
|
<AuthorizeView>
|
||||||
|
<Authorized>
|
||||||
|
<button class="fab circle extra large-elevate" data-ui="#post-modal">
|
||||||
|
<i class="fa-solid fa-camera-retro"></i>
|
||||||
|
</button>
|
||||||
|
<NewPicDialog id="post-modal" Active="true" Base64File="@SharePhoto" FileSize="@SharePhotoSize" FileContentType="@SharePhotoContentType" Description="@SharePhotoText"></NewPicDialog>
|
||||||
|
</Authorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public string? SharePhoto { get; set; }
|
||||||
|
public long? SharePhotoSize { get; set; }
|
||||||
|
public string? SharePhotoContentType { get; set; }
|
||||||
|
public string? SharePhotoText { get; set; }
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
await checkLogin();
|
||||||
|
|
||||||
|
SharePhoto = State.SharePhoto;
|
||||||
|
SharePhotoContentType = State.SharePhotoContentType;
|
||||||
|
SharePhotoSize = State.SharePhotoSize;
|
||||||
|
SharePhotoText = State.SharePhotoText;
|
||||||
|
|
||||||
|
State.SharePhoto = null;
|
||||||
|
State.SharePhotoContentType = null;
|
||||||
|
State.SharePhotoSize = null;
|
||||||
|
State.SharePhotoText = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> checkLogin() {
|
||||||
|
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||||
|
var user = authState.User;
|
||||||
|
|
||||||
|
if (user.Identity is not null && user.Identity.IsAuthenticated) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
navigationManager.NavigateTo("/login");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,47 @@
|
||||||
@page "/sharetext/{Text}"
|
@page "/sharetext"
|
||||||
|
@inject NavigationManager navigationManager
|
||||||
|
@inject AuthenticationStateProvider AuthStateProvider
|
||||||
|
@inject State State
|
||||||
|
|
||||||
<h3>Sharing</h3>
|
<PageHeading title="Status.lol" icon="fa-solid fa-message-smile">
|
||||||
|
<Description>Share a post to <a href="https://status.lol">status.lol</a></Description>
|
||||||
|
</PageHeading>
|
||||||
|
|
||||||
<p>@Text</p>
|
<AuthorizeView>
|
||||||
|
<Authorized>
|
||||||
|
<button class="fab circle extra large-elevate" data-ui="#post-modal">
|
||||||
|
<i class="fa-solid fa-message-plus square"></i>
|
||||||
|
</button>
|
||||||
|
<NewStatusDialog id="post-modal" Active="true" Content="@Text"></NewStatusDialog>
|
||||||
|
</Authorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
public string? Text { get; set; }
|
||||||
public string Text { get; set; }
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
await checkLogin();
|
||||||
|
|
||||||
|
Text = State.ShareString;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(State.ShareStringSubject)) {
|
||||||
|
Text = $"{State.ShareStringSubject}\n\n{State.ShareString}";
|
||||||
|
}
|
||||||
|
|
||||||
|
State.ShareStringSubject = null;
|
||||||
|
State.ShareString = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> checkLogin() {
|
||||||
|
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||||
|
var user = authState.User;
|
||||||
|
|
||||||
|
if (user.Identity is not null && user.Identity.IsAuthenticated) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
navigationManager.NavigateTo("/login");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
@page "/statuslog/latest"
|
@page "/statuslog/latest"
|
||||||
@inject State State
|
@inject State State
|
||||||
|
|
||||||
|
<RefreshButton></RefreshButton>
|
||||||
|
|
||||||
<PageHeading title="Status.lol" icon="fa-solid fa-message-smile">
|
<PageHeading title="Status.lol" icon="fa-solid fa-message-smile">
|
||||||
<Description>The latest posts from everyone at <a href="https://status.lol">status.lol</a></Description>
|
<Description>The latest posts from everyone at <a href="https://status.lol">status.lol</a></Description>
|
||||||
</PageHeading>
|
</PageHeading>
|
||||||
|
@ -9,14 +11,17 @@
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
<button class="fab circle extra large-elevate" data-ui="#post-modal">
|
<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>
|
</button>
|
||||||
<NewStatusDialog id="post-modal"></NewStatusDialog>
|
<NewStatusDialog id="post-modal"></NewStatusDialog>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
|
|
||||||
<div id="statuses" class="responsive">
|
<div id="statuses" class="responsive">
|
||||||
<StatusList StatusFunc="@(async(forceRefresh) => await State.GetStatuses(forceRefresh))"></StatusList>
|
<StatusList StatusFunc="@(async(refresh) => await State.GetStatuses(refresh))"></StatusList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RefreshButton></RefreshButton>
|
@code {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
71
Components/PasteCard.razor
Normal file
71
Components/PasteCard.razor
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
@using CommunityToolkit.Maui.Alerts
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
|
<article class="paste">
|
||||||
|
@* TODO: link to paste view *@
|
||||||
|
<nav>
|
||||||
|
<h5 class="mono"><a href="/pastes/tbc">@Paste.Title</a></h5>
|
||||||
|
<div class="max"></div>
|
||||||
|
@if (MarkupView)
|
||||||
|
{
|
||||||
|
<button class="transparent circle" title="View Original" @onclick="() => { MarkupView = false; InvokeAsync(StateHasChanged); }"><i class="fa-solid fa-code"></i></button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<button class="transparent circle" title="View Markup" @onclick="() => { MarkupView = true; InvokeAsync(StateHasChanged); }"><i class="fa-solid fa-browser"></i></button>
|
||||||
|
}
|
||||||
|
<button class="transparent circle" title="Copy to Clipboard" @onclick="() => CopyPaste()"><i class="fa-solid fa-copy"></i></button>
|
||||||
|
<button class="transparent circle" @onclick="ShareClick">
|
||||||
|
<i class="fa-solid fa-share-nodes"></i>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
<small class="nowrap chip no-border"><i class="fa-solid fa-clock tiny"></i> @Paste.RelativeTime</small>
|
||||||
|
@if(MarkupView){
|
||||||
|
<div class="padding">
|
||||||
|
@Utilities.MdToHtmlMarkup(Paste.Content)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<pre><code class="padding margin">@((MarkupString)Paste.Content)</code></pre>
|
||||||
|
}
|
||||||
|
<nav>
|
||||||
|
<div class="max"></div>
|
||||||
|
@if (Editable) {
|
||||||
|
<button @onclick="EditPaste"><i class="fa-solid fa-pencil"></i> Edit</button>
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public Paste? Paste { get; set; }
|
||||||
|
[Parameter]
|
||||||
|
public bool Editable { get; set; } = false;
|
||||||
|
[Parameter]
|
||||||
|
public EditPasteDialog? Dialog { get; set; }
|
||||||
|
|
||||||
|
private bool MarkupView = false;
|
||||||
|
|
||||||
|
private async Task EditPaste(EventArgs e) {
|
||||||
|
Dialog!.Paste = Paste;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await JS.InvokeVoidAsync("ui", "#" + Dialog?.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ShareClick(EventArgs e) {
|
||||||
|
await Share.Default.RequestAsync(new ShareTextRequest {
|
||||||
|
Uri = Paste!.Url,
|
||||||
|
Text = Paste!.Content,
|
||||||
|
Title = Paste!.Title,
|
||||||
|
Subject = Paste!.Title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CopyPaste() {
|
||||||
|
if(Paste != null && !string.IsNullOrEmpty(Paste?.Content)) {
|
||||||
|
await Clipboard.Default.SetTextAsync(Paste?.Content);
|
||||||
|
var toast = Toast.Make("Copied to clipboard");
|
||||||
|
await toast.Show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
Components/PasteList.razor
Normal file
51
Components/PasteList.razor
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
@implements IDisposable
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject State State
|
||||||
|
|
||||||
|
@if (Editable) {
|
||||||
|
<EditPasteDialog @ref="Dialog" id="EditPasteModal"></EditPasteDialog>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (pastes != null) foreach (Paste paste in pastes) {
|
||||||
|
<PasteCard Paste="paste" Editable="Editable" Dialog="Dialog"></PasteCard>
|
||||||
|
}
|
||||||
|
|
||||||
|
<LoadingCard id="pastes-loading" icon="fa-solid fa-clipboard"></LoadingCard>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public Func<bool, Task<List<Paste>?>>? PastesFunc { get; set; }
|
||||||
|
[Parameter]
|
||||||
|
public bool Editable { get; set; } = false;
|
||||||
|
|
||||||
|
public EditPasteDialog? Dialog { get; set; }
|
||||||
|
|
||||||
|
private List<Paste>? pastes;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
if (PastesFunc == null) return;
|
||||||
|
|
||||||
|
if (pastes == null || pastes.Count == 0) pastes = await PastesFunc(false);
|
||||||
|
State.PropertyChanged += StateChanged;
|
||||||
|
State.CanRefresh = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await JS.InvokeVoidAsync("removeElementById", "pastes-loading");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
|
if (PastesFunc == null) return;
|
||||||
|
|
||||||
|
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
|
||||||
|
using (State.GetRefreshToken()) {
|
||||||
|
pastes = await PastesFunc(true);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() {
|
||||||
|
State.PropertyChanged -= StateChanged;
|
||||||
|
State.CanRefresh = false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,48 +1,59 @@
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
<article class="no-padding">
|
<article class="pic no-padding">
|
||||||
<img src="@Pic.Url" loading="lazy">
|
<img src="@Pic!.Url" loading="lazy">
|
||||||
<div class="padding">
|
<div class="padding row">
|
||||||
<nav>
|
<div class="emoji" data-emoji="🖼️">🖼️</div>
|
||||||
<a class="author" href="/person/@Pic.Address#pics">
|
<div class="max">
|
||||||
<i class="fa-solid fa-fw fa-at"></i>@Pic.Address
|
<nav>
|
||||||
</a>
|
<a class="author" href="/person/@Pic.Address#pics">
|
||||||
<span class="max"></span>
|
<i class="fa-solid fa-fw fa-at"></i>@Pic.Address
|
||||||
<a class="chip transparent-border right">
|
</a>
|
||||||
<i class="fa fa-clock"></i> @Pic.RelativeTime
|
<span class="max"></span>
|
||||||
</a>
|
</nav>
|
||||||
</nav>
|
@if(!string.IsNullOrWhiteSpace(Pic.Description)){
|
||||||
<p>@((MarkupString)Pic.DescriptionHtml)</p>
|
<p>@((MarkupString)Pic.DescriptionHtml)</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">
|
else {
|
||||||
<i class="fa-solid fa-share-nodes"></i>
|
<div class="padding padding yellow-2-bg yellow-9-fg">
|
||||||
</button>
|
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||||
</nav>
|
<span>This picture needs a description in order to be shared.</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<nav>
|
||||||
|
<a class="chip transparent-border">
|
||||||
|
<i class="fa fa-clock"></i> @Pic.RelativeTime
|
||||||
|
</a>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Pic Pic {get; set;}
|
public Pic? Pic {get; set;}
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool Editable { get; set; } = false;
|
public bool Editable { get; set; } = false;
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EditPicDialog? Dialog { get; set; }
|
public EditPicDialog? Dialog { get; set; }
|
||||||
|
|
||||||
private async Task EditPic(EventArgs e){
|
private async Task EditPic(EventArgs e){
|
||||||
Dialog.Pic = Pic;
|
Dialog!.Pic = Pic;
|
||||||
// await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
await JS.InvokeVoidAsync("ui", "#" + Dialog?.id);
|
await JS.InvokeVoidAsync("ui", "#" + Dialog?.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ShareClick(EventArgs e){
|
public async Task ShareClick(EventArgs e){
|
||||||
await Share.Default.RequestAsync(new ShareTextRequest{
|
await Share.Default.RequestAsync(new ShareTextRequest{
|
||||||
Uri = Pic.Url,
|
Uri = Pic!.Url,
|
||||||
Text = Pic.Description,
|
Text = Pic!.Description,
|
||||||
Title = "I saw this on some.pics",
|
Title = "I saw this on some.pics",
|
||||||
Subject = "I saw this on some.pics"
|
Subject = "I saw this on some.pics"
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
@inject IJSRuntime JS
|
@implements IDisposable
|
||||||
|
@inject IJSRuntime JS
|
||||||
@inject State State
|
@inject State State
|
||||||
|
|
||||||
@implements IDisposable
|
|
||||||
|
|
||||||
@if (Editable) {
|
@if (Editable) {
|
||||||
<EditPicDialog @ref="Dialog" id="EditPicModal"></EditPicDialog>
|
<EditPicDialog @ref="Dialog" id="EditPicModal"></EditPicDialog>
|
||||||
}
|
}
|
||||||
|
@ -15,55 +14,34 @@
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<bool, Task<List<Pic>?>> PicsFunc { get; set; }
|
public Func<bool, Task<List<Pic>?>>? PicsFunc { get; set; }
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool Editable { get; set; } = false;
|
public bool Editable { get; set; } = false;
|
||||||
|
|
||||||
public EditPicDialog? Dialog { get; set; }
|
public EditPicDialog? Dialog { get; set; }
|
||||||
|
|
||||||
private List<Pic>? pics;
|
private List<Pic>? pics;
|
||||||
private int count { get; set; } = 0;
|
|
||||||
private int pageSize { get; } = 10;
|
|
||||||
|
|
||||||
// TODO: There is a noticable rendering delay between the pics loading and the page rendering
|
// TODO: There is a noticable rendering delay between the pics loading and the page rendering
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
|
if (PicsFunc == null) return;
|
||||||
|
|
||||||
|
if (pics == null || pics.Count == 0) pics = await PicsFunc(false);
|
||||||
State.PropertyChanged += StateChanged;
|
State.PropertyChanged += StateChanged;
|
||||||
State.CanRefresh = true;
|
State.CanRefresh = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
if (pics == null || pics.Count == 0) await LoadNext();
|
|
||||||
await JS.InvokeVoidAsync("removeElementById", "pics-loading");
|
await JS.InvokeVoidAsync("removeElementById", "pics-loading");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> PullUp() {
|
|
||||||
int countBefore = count;
|
|
||||||
await LoadNext();
|
|
||||||
return (count != countBefore);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LoadNext(bool forceRefresh = false) {
|
|
||||||
if (pics == null) pics = new List<Pic>();
|
|
||||||
if (forceRefresh) {
|
|
||||||
count = 0;
|
|
||||||
pics.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
var allPics = await PicsFunc(forceRefresh);
|
|
||||||
if(allPics != null && count < allPics.Count) {
|
|
||||||
pics.AddRange(allPics.Skip(count).Take(pageSize));
|
|
||||||
count = pics.Count;
|
|
||||||
}
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
|
if (PicsFunc == null) return;
|
||||||
await LoadNext(true);
|
|
||||||
State.IsRefreshing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.PropertyName == nameof(State.AtBottom) && State.AtBottom) {
|
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
|
||||||
await LoadNext();
|
using (State.GetRefreshToken()){
|
||||||
|
pics = await PicsFunc(true);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
@inject State State
|
@inject State State
|
||||||
|
|
||||||
<button id="refreshButton" class="absolute transparent circle top right margin" @onclick="() => State.IsRefreshing = true">
|
<button id="refreshButton" class="absolute transparent circle top right margin" @onclick="() => State.SendRefresh()">
|
||||||
<i class="fa-solid fa-arrow-rotate-right @(State.IsRefreshing ? "fa-spin" : "")"></i>
|
<i class="fa-solid fa-arrow-rotate-right @(State.IsRefreshing ? "fa-spin" : "")"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@ -11,8 +11,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
if(e.PropertyName == nameof(State.IsRefreshing)) {
|
if (e.PropertyName == nameof(State.IsRefreshing)) {
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,7 +3,13 @@
|
||||||
<Router AppAssembly="@typeof(MauiProgram).Assembly">
|
<Router AppAssembly="@typeof(MauiProgram).Assembly">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
|
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
|
||||||
<Authorizing>Logging in...</Authorizing>
|
<Authorizing>
|
||||||
|
<div class="no-border responsive max no-padding center-align middle-align">
|
||||||
|
<div class="padding">
|
||||||
|
<img src="/img/prami-neighbourhood.svg" class="extra" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Authorizing>
|
||||||
<NotAuthorized><RedirectToLogin /></NotAuthorized>
|
<NotAuthorized><RedirectToLogin /></NotAuthorized>
|
||||||
</AuthorizeRouteView>
|
</AuthorizeRouteView>
|
||||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||||
|
@ -11,8 +17,13 @@
|
||||||
<NotFound>
|
<NotFound>
|
||||||
<CascadingAuthenticationState>
|
<CascadingAuthenticationState>
|
||||||
<LayoutView Layout="@typeof(Layout.MainLayout)">
|
<LayoutView Layout="@typeof(Layout.MainLayout)">
|
||||||
<img data-emoji="🦒" />
|
<div class="no-border responsive max no-padding center-align middle-align">
|
||||||
<p>Sorry, there's nothing here.</p>
|
<div class="padding">
|
||||||
|
<img src="/img/prami-neighbourhood.svg" class="extra" />
|
||||||
|
<p>Sorry, you seem to have landed nowhere.</p>
|
||||||
|
<small>@navigationManager.Uri</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</LayoutView>
|
</LayoutView>
|
||||||
</CascadingAuthenticationState>
|
</CascadingAuthenticationState>
|
||||||
</NotFound>
|
</NotFound>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
<article class="status gray-9-fg" style="background-color:@(Status.Background)">
|
<article class="status gray-9-fg" style="background-color:@(Status!.Background)">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="emoji" data-emoji="@Status.EmojiOrDefault">@Status.EmojiOrDefault</div>
|
<div class="emoji" data-emoji="@Status.EmojiOrDefault">@Status.EmojiOrDefault</div>
|
||||||
<div class="max">
|
<div class="max">
|
||||||
|
@ -23,9 +23,12 @@
|
||||||
<a class="chip transparent-border">
|
<a class="chip transparent-border">
|
||||||
<i class="fa fa-clock"></i> @Status.RelativeTime
|
<i class="fa fa-clock"></i> @Status.RelativeTime
|
||||||
</a>
|
</a>
|
||||||
<a class="chip transparent-border" href="@Status.ExternalUrl" target="_blank">
|
@if (!string.IsNullOrWhiteSpace(Status.ExternalUrl))
|
||||||
<i class="fa fa-message-dots"></i> Respond
|
{
|
||||||
</a>
|
<a class="chip transparent-border" href="@Status.ExternalUrl" target="_blank">
|
||||||
|
<i class="fa fa-message-dots"></i> @(new Uri(Status.ExternalUrl).Host)
|
||||||
|
</a>
|
||||||
|
}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,21 +36,21 @@
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Status Status { get; set; }
|
public Status? Status { get; set; }
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool Editable { get; set; } = false;
|
public bool Editable { get; set; } = false;
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EditStatusDialog? Dialog { get; set; }
|
public EditStatusDialog? Dialog { get; set; }
|
||||||
|
|
||||||
private async Task EditStatus(EventArgs e) {
|
private async Task EditStatus(EventArgs e) {
|
||||||
Dialog.Status = Status;
|
Dialog!.Status = Status;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
await JS.InvokeVoidAsync("ui", "#" + Dialog?.id);
|
await JS.InvokeVoidAsync("ui", "#" + Dialog?.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ShareClick(EventArgs e){
|
public async Task ShareClick(EventArgs e){
|
||||||
await Share.Default.RequestAsync(new ShareTextRequest{
|
await Share.Default.RequestAsync(new ShareTextRequest{
|
||||||
Text = $"{Status.Content}\n- from [@{Status.Address}]({Status.Url})",
|
Text = $"{Status!.Content}\n- from [@{Status.Address}]({Status.Url})",
|
||||||
Title = "I saw this on status.lol",
|
Title = "I saw this on status.lol",
|
||||||
Subject = "I saw this on status.lol"
|
Subject = "I saw this on status.lol"
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
<article class="status">
|
|
||||||
<div class="row">
|
|
||||||
<div class="large emoji skeleton round" />
|
|
||||||
<div class="max">
|
|
||||||
<span class="author skeleton"> </span>
|
|
||||||
<p class="skeleton"> </p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<nav>
|
|
||||||
<span class="chip transparent-border skeleton"> </span>
|
|
||||||
</nav>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,8 +1,7 @@
|
||||||
@inject IJSRuntime JS
|
@implements IDisposable
|
||||||
|
@inject IJSRuntime JS
|
||||||
@inject State State
|
@inject State State
|
||||||
|
|
||||||
@implements IDisposable
|
|
||||||
|
|
||||||
@if (Editable) {
|
@if (Editable) {
|
||||||
<EditStatusDialog @ref="Dialog" id="EditStatusModal"></EditStatusDialog>
|
<EditStatusDialog @ref="Dialog" id="EditStatusModal"></EditStatusDialog>
|
||||||
}
|
}
|
||||||
|
@ -15,55 +14,33 @@
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<bool,Task<List<Status>?>> StatusFunc { get; set; }
|
public Func<bool, Task<List<Status>?>>? StatusFunc { get; set; }
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool Editable { get; set; } = false;
|
public bool Editable { get; set; } = false;
|
||||||
|
|
||||||
public EditStatusDialog? Dialog { get; set; }
|
public EditStatusDialog? Dialog { get; set; }
|
||||||
|
|
||||||
private List<Status>? statuses;
|
private List<Status>? statuses;
|
||||||
private int count { get; set; } = 0;
|
|
||||||
private int pageSize { get; } = 50;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
|
if (StatusFunc == null) return;
|
||||||
|
|
||||||
|
if (statuses == null || statuses.Count == 0) statuses = await StatusFunc(false);
|
||||||
State.PropertyChanged += StateChanged;
|
State.PropertyChanged += StateChanged;
|
||||||
State.CanRefresh = true;
|
State.CanRefresh = true;
|
||||||
|
|
||||||
if (statuses == null || statuses.Count == 0) await LoadNext();
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
await JS.InvokeVoidAsync("removeElementById", "statusLoading");
|
await JS.InvokeVoidAsync("removeElementById", "statusLoading");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> PullUp() {
|
|
||||||
int countBefore = count;
|
|
||||||
await LoadNext();
|
|
||||||
return (count != countBefore);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LoadNext(bool forceRefresh = false) {
|
|
||||||
if (statuses == null) statuses = new List<Status>();
|
|
||||||
if (forceRefresh) {
|
|
||||||
count = 0;
|
|
||||||
statuses.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
var allStatuses = await StatusFunc(forceRefresh);
|
|
||||||
if (allStatuses != null && count < allStatuses.Count) {
|
|
||||||
statuses.AddRange(allStatuses.Skip(count).Take(pageSize));
|
|
||||||
count = statuses.Count;
|
|
||||||
}
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
|
||||||
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
|
if (StatusFunc == null) return;
|
||||||
await LoadNext(true);
|
|
||||||
State.IsRefreshing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(e.PropertyName == nameof(State.AtBottom) && State.AtBottom) {
|
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
|
||||||
await LoadNext();
|
using (State.GetRefreshToken()) {
|
||||||
|
statuses = await StatusFunc(true);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
11
Components/ThemeCard.razor
Normal file
11
Components/ThemeCard.razor
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<article class="theme" style="@theme?.PreviewCssData?.BackgroundCss ; @theme?.PreviewCssData?.TextCss">
|
||||||
|
<h5 class="honey">@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; }
|
||||||
|
}
|
82
Components/ThemeDialog.razor
Normal file
82
Components/ThemeDialog.razor
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
@inject State State
|
||||||
|
@inject ApiService 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="honey">@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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,4 +12,6 @@
|
||||||
@using Neighbourhood.omg.lol.Components
|
@using Neighbourhood.omg.lol.Components
|
||||||
@using Neighbourhood.omg.lol.Models
|
@using Neighbourhood.omg.lol.Models
|
||||||
@using Markdig
|
@using Markdig
|
||||||
@using BcdLib.Components
|
@using PSC.Blazor.Components.MarkdownEditor
|
||||||
|
@using PSC.Blazor.Components.MarkdownEditor.EventsArgs
|
||||||
|
@using PSC.Blazor.Components.MarkdownEditor.Enums
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
x:Class="Neighbourhood.omg.lol.EphemeralWebPage"
|
|
||||||
BackgroundColor="{DynamicResource PageBackgroundColor}">
|
|
||||||
<Grid>
|
|
||||||
<WebView x:Name="webview"
|
|
||||||
Navigating="webview_Navigating"
|
|
||||||
VerticalOptions="Fill"
|
|
||||||
HorizontalOptions="Fill" />
|
|
||||||
</Grid>
|
|
||||||
</ContentPage>
|
|
|
@ -1,18 +0,0 @@
|
||||||
namespace Neighbourhood.omg.lol;
|
|
||||||
|
|
||||||
public partial class EphemeralWebPage : ContentPage
|
|
||||||
{
|
|
||||||
private NavigatorService NavigatorService { get; set; }
|
|
||||||
|
|
||||||
public EphemeralWebPage(NavigatorService navigatorService)
|
|
||||||
{
|
|
||||||
this.NavigatorService = navigatorService;
|
|
||||||
InitializeComponent();
|
|
||||||
this.webview.Source = $"https://home.omg.lol/ephemeral";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void webview_Navigating(object sender, WebNavigatingEventArgs e) {
|
|
||||||
var cookies = this.webview.Cookies;
|
|
||||||
Uri uri = new Uri(e.Url);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
|
||||||
xmlns:local="clr-namespace:Neighbourhood.omg.lol"
|
|
||||||
xmlns:models="clr-namespace:Neighbourhood.omg.lol.Models"
|
|
||||||
x:DataType="models:State"
|
|
||||||
x:Class="Neighbourhood.omg.lol.MainPage"
|
|
||||||
BackgroundColor="{DynamicResource PageBackgroundColor}">
|
|
||||||
|
|
||||||
<RefreshView x:Name="refreshView" IsRefreshing="{Binding IsRefreshing}" IsEnabled="False">
|
|
||||||
<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html" UrlLoading="BlazorUrlLoading">
|
|
||||||
<BlazorWebView.RootComponents>
|
|
||||||
<RootComponent Selector="#app" ComponentType="{x:Type local:Components.Routes}" />
|
|
||||||
</BlazorWebView.RootComponents>
|
|
||||||
</BlazorWebView>
|
|
||||||
</RefreshView>
|
|
||||||
|
|
||||||
</ContentPage>
|
|
|
@ -1,33 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.AspNetCore.Components.WebView;
|
|
||||||
using Microsoft.AspNetCore.Components.WebView.Maui;
|
|
||||||
using Neighbourhood.omg.lol.Models;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol {
|
|
||||||
public partial class MainPage : ContentPage {
|
|
||||||
|
|
||||||
private State State { get; set; }
|
|
||||||
|
|
||||||
public MainPage() {
|
|
||||||
InitializeComponent();
|
|
||||||
State = IPlatformApplication.Current!.Services.GetService<State>()!;
|
|
||||||
BindingContext = State;
|
|
||||||
State.PropertyChanged += State_PropertyChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void State_PropertyChanged(object? sender, PropertyChangedEventArgs e) {
|
|
||||||
if (e.PropertyName == nameof(State.CanRefresh) || e.PropertyName == nameof(State.AtTop)) {
|
|
||||||
refreshView.IsEnabled = State.CanRefresh && State.AtTop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BlazorUrlLoading(object? sender, UrlLoadingEventArgs e) {
|
|
||||||
if(e.Url.Host == "home.omg.lol" && e.Url.AbsolutePath == "/oauth/authorize") {
|
|
||||||
e.UrlLoadingStrategy = UrlLoadingStrategy.CancelLoad;
|
|
||||||
Shell.Current.GoToAsync(nameof(LoginWebViewPage));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,7 @@
|
||||||
using BcdLib.Components;
|
using CommunityToolkit.Maui;
|
||||||
using Markdig;
|
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Neighbourhood.omg.lol.Models;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol {
|
namespace Neighbourhood.omg.lol {
|
||||||
public static class MauiProgram {
|
public static class MauiProgram {
|
||||||
|
@ -11,21 +9,20 @@ namespace Neighbourhood.omg.lol {
|
||||||
var builder = MauiApp.CreateBuilder();
|
var builder = MauiApp.CreateBuilder();
|
||||||
builder
|
builder
|
||||||
.UseMauiApp<App>()
|
.UseMauiApp<App>()
|
||||||
.ConfigureFonts(fonts => {
|
.UseMauiCommunityToolkit(options => {
|
||||||
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
|
options.SetShouldEnableSnackbarOnWindows(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddMauiBlazorWebView();
|
builder.Services.AddMauiBlazorWebView();
|
||||||
builder.Services.AddBcdLibPullComponent();
|
|
||||||
builder.Services.AddTransient<LoginWebViewPage>();
|
builder.Services.AddTransient<LoginWebViewPage>();
|
||||||
builder.Services.AddTransient<EphemeralWebPage>();
|
|
||||||
|
|
||||||
builder.Services.AddSingleton<RestService>();
|
builder.Services.AddSingleton<ApiService>();
|
||||||
builder.Services.AddSingleton<State>();
|
builder.Services.AddSingleton<State>();
|
||||||
builder.Services.AddSingleton<NavigatorService>();
|
builder.Services.AddSingleton<NavigatorService>();
|
||||||
|
|
||||||
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
|
using Stream stream = App.Assembly.GetManifestResourceStream($"{App.Name}.appsettings.json")!;
|
||||||
builder.Services.AddSingleton<IConfiguration>(configurationBuilder.AddUserSecrets<App>().Build());
|
var config = new ConfigurationBuilder().AddJsonStream(stream).Build();
|
||||||
|
builder.Configuration.AddConfiguration(config);
|
||||||
|
|
||||||
builder.Services.AddAuthorizationCore();
|
builder.Services.AddAuthorizationCore();
|
||||||
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
|
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
|
||||||
|
|
9
Models/API/AccountResponseData.cs
Normal file
9
Models/API/AccountResponseData.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class AccountResponseData : IOmgLolResponseData {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public TimeData Created { get; set; } = TimeData.Empty;
|
||||||
|
//TODO: api_key and settings
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,9 @@
|
||||||
using System;
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class AddressResponseData : IOmgLolResponseData {
|
public class AddressResponseData : IOmgLolResponseData {
|
||||||
public string Address { get; set; }
|
public string Address { get; set; } = string.Empty;
|
||||||
public string Message { get; set; }
|
public string Message { get; set; } = string.Empty;
|
||||||
public RegistrationData Registration { get; set; }
|
public RegistrationData? Registration { get; set; }
|
||||||
public ExpirationData Expiration { get; set; }
|
public ExpirationData? Expiration { get; set; }
|
||||||
public PreferenceData? Preferences { get; set; }
|
public PreferenceData? Preferences { get; set; }
|
||||||
|
|
||||||
public class RegistrationData : TimeData {
|
public class RegistrationData : TimeData {
|
5
Models/API/AddressResponseList.cs
Normal file
5
Models/API/AddressResponseList.cs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class AddressResponseList : List<AddressResponseData>, IOmgLolResponseList<AddressResponseData> {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
5
Models/API/BasicResponseData.cs
Normal file
5
Models/API/BasicResponseData.cs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class BasicResponseData : IOmgLolResponseData {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
7
Models/API/DirectoryResponseData.cs
Normal file
7
Models/API/DirectoryResponseData.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class DirectoryResponseData : IOmgLolResponseData {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
public List<string> Directory { get; set; } = new List<string>();
|
||||||
|
}
|
||||||
|
}
|
5
Models/API/EphemeralData.cs
Normal file
5
Models/API/EphemeralData.cs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class EphemeralData {
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
6
Models/API/EphemeralResponseData.cs
Normal file
6
Models/API/EphemeralResponseData.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
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>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
public interface IOmgLolResponseData {
|
public interface IOmgLolResponseData {
|
||||||
|
public string Message { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
4
Models/API/IOmgLolResponseList.cs
Normal file
4
Models/API/IOmgLolResponseList.cs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public interface IOmgLolResponseList<T> : IList<T>, IOmgLolResponseData where T : IOmgLolResponseData {
|
||||||
|
}
|
||||||
|
}
|
9
Models/API/NowContentData.cs
Normal file
9
Models/API/NowContentData.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class NowContentData {
|
||||||
|
public string? Content { get; set; }
|
||||||
|
public long? Updated { get; set; }
|
||||||
|
public int? Listed { get; set; }
|
||||||
|
public int? Nudge { get; set; }
|
||||||
|
public string? Metadata { get; set; }
|
||||||
|
}
|
||||||
|
}
|
12
Models/API/NowData.cs
Normal file
12
Models/API/NowData.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models
|
||||||
|
{
|
||||||
|
public class NowData {
|
||||||
|
public string Address { get; set; } = string.Empty;
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
public TimeData Updated { get; set; } = TimeData.Empty;
|
||||||
|
|
||||||
|
public string UpdatedRelative {
|
||||||
|
get => Utilities.RelativeTimeFromUnix(Convert.ToInt64(Updated.UnixEpochTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
Models/API/NowPageResponseData.cs
Normal file
6
Models/API/NowPageResponseData.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class NowPageResponseData : IOmgLolResponseData {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public NowContentData? Now { get; set; }
|
||||||
|
}
|
||||||
|
}
|
7
Models/API/NowResponseData.cs
Normal file
7
Models/API/NowResponseData.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class NowResponseData : IOmgLolResponseData {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public long Count { get; set; }
|
||||||
|
public List<NowData> Garden { get; set; } = new List<NowData>();
|
||||||
|
}
|
||||||
|
}
|
11
Models/API/OmgLolApiException.cs
Normal file
11
Models/API/OmgLolApiException.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class OmgLolApiException<T> : Exception where T : IOmgLolResponseData {
|
||||||
|
public OmgLolResponse<T>? Response { get; set; }
|
||||||
|
|
||||||
|
public OmgLolApiException(OmgLolResponse<T>? response) : base(response?.Response?.Message) {
|
||||||
|
Response = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OmgLolApiException(string? response) : base(response) { }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
public class OmgLolResponse<TResponseData> where TResponseData : IOmgLolResponseData {
|
public class OmgLolResponse<TResponseData> where TResponseData : IOmgLolResponseData {
|
||||||
public OmgLolRequestData Request { get; set; }
|
public OmgLolRequestData? Request { get; set; }
|
||||||
public TResponseData Response { get; set; }
|
public TResponseData? Response { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
6
Models/API/PastesResponseData.cs
Normal file
6
Models/API/PastesResponseData.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class PastesResponseData : IOmgLolResponseData {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public List<Paste> Pastebin { get; set; } = new List<Paste>();
|
||||||
|
}
|
||||||
|
}
|
7
Models/API/PatchStatus.cs
Normal file
7
Models/API/PatchStatus.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class PatchStatus {
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
public string? Emoji { get; set; }
|
||||||
|
}
|
||||||
|
}
|
7
Models/API/PatchStatusResponseData.cs
Normal file
7
Models/API/PatchStatusResponseData.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class PatchStatusResponseData : IOmgLolResponseData {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
6
Models/API/PostPasteResponseData.cs
Normal file
6
Models/API/PostPasteResponseData.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class PostPasteResponseData : IOmgLolResponseData {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
5
Models/API/PostPic.cs
Normal file
5
Models/API/PostPic.cs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class PostPic {
|
||||||
|
public string? Description { get; set; }
|
||||||
|
}
|
||||||
|
}
|
9
Models/API/PostProfile.cs
Normal file
9
Models/API/PostProfile.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
5
Models/API/PostStatusBio.cs
Normal file
5
Models/API/PostStatusBio.cs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class PostStatusBio {
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
8
Models/API/PreviewCssData.cs
Normal file
8
Models/API/PreviewCssData.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
16
Models/API/ProfileResponseData.cs
Normal file
16
Models/API/ProfileResponseData.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
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; }
|
||||||
|
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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
5
Models/API/PutPic.cs
Normal file
5
Models/API/PutPic.cs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class PutPic {
|
||||||
|
public string Pic { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
9
Models/API/PutPicResponseData.cs
Normal file
9
Models/API/PutPicResponseData.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class PutPicResponseData : IOmgLolResponseData {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public long Size { get; set; }
|
||||||
|
public string Mime { get; set; } = string.Empty;
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
6
Models/API/SomePicsResponseData.cs
Normal file
6
Models/API/SomePicsResponseData.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class SomePicsResponseData : IOmgLolResponseData {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public List<Pic>? Pics { get; set; }
|
||||||
|
}
|
||||||
|
}
|
8
Models/API/StatusBioResponseData.cs
Normal file
8
Models/API/StatusBioResponseData.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class StatusBioResponseData : IOmgLolResponseData {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public string Bio { get; set; } = string.Empty;
|
||||||
|
public string Css { get; set; } = string.Empty;
|
||||||
|
public string Head { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,4 @@
|
||||||
using System;
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class StatusPost {
|
public class StatusPost {
|
||||||
public string? Emoji { get; set; }
|
public string? Emoji { get; set; }
|
||||||
public string? Content { get; set; }
|
public string? Content { get; set; }
|
9
Models/API/StatusPostResponseData.cs
Normal file
9
Models/API/StatusPostResponseData.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class StatusPostResponseData : IOmgLolResponseData {
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public string? Id { get; set; }
|
||||||
|
public string? Url { get; set; }
|
||||||
|
public string? ExternalUrl { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
6
Models/API/StatusResponseData.cs
Normal file
6
Models/API/StatusResponseData.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
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>();
|
||||||
|
}
|
||||||
|
}
|
6
Models/API/ThemePreviewResponseData.cs
Normal file
6
Models/API/ThemePreviewResponseData.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
6
Models/API/ThemeResponseData.cs
Normal file
6
Models/API/ThemeResponseData.cs
Normal 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>();
|
||||||
|
}
|
||||||
|
}
|
7
Models/API/TokenResponseData.cs
Normal file
7
Models/API/TokenResponseData.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class TokenResponseData {
|
||||||
|
public string AccessToken { get; set; } = string.Empty;
|
||||||
|
public string TokenType { get; set; } = string.Empty;
|
||||||
|
public string Scope { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class AccountResponseData : IOmgLolResponseData {
|
|
||||||
public string Message { get; set; }
|
|
||||||
public string Email { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
// created, api_key and settings
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class AddressResponseList : List<AddressResponseData>, IOmgLolResponseList<AddressResponseData> {
|
|
||||||
}
|
|
||||||
}
|
|
13
Models/FeedItem.cs
Normal file
13
Models/FeedItem.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class FeedItem {
|
||||||
|
public Status? Status { get; set; }
|
||||||
|
public Pic? Pic { get; set; }
|
||||||
|
public Paste? Paste { get; set; }
|
||||||
|
|
||||||
|
public bool IsStatus { get => Status != null; }
|
||||||
|
public bool IsPic { get => Pic != null; }
|
||||||
|
public bool IsPaste { get => Paste != null; }
|
||||||
|
|
||||||
|
public DateTimeOffset? CreatedTime { get => Status?.CreatedTime ?? Pic?.CreatedTime ?? Paste?.ModifiedTime; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public interface IOmgLolResponseList<T> : IList<T>, IOmgLolResponseData where T : IOmgLolResponseData {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class NowData {
|
|
||||||
public string Address { get; set; }
|
|
||||||
public string Url { get; set; }
|
|
||||||
public TimeData Updated { get; set; }
|
|
||||||
|
|
||||||
public string UpdatedRelative {
|
|
||||||
get => Utilities.RelativeTimeFromUnix(Convert.ToInt64(Updated.UnixEpochTime));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class NowResponseData : IOmgLolResponseData {
|
|
||||||
public string Message { get; set; }
|
|
||||||
public long Count { get; set; }
|
|
||||||
public List<NowData> Garden { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
29
Models/Paste.cs
Normal file
29
Models/Paste.cs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
namespace Neighbourhood.omg.lol.Models {
|
||||||
|
public class Paste {
|
||||||
|
public string? Url;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
public long? ModifiedOn { get; set; }
|
||||||
|
public int Listed { get; set; }
|
||||||
|
|
||||||
|
public bool IsListed {
|
||||||
|
get => Listed != 0;
|
||||||
|
set => Listed = value ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTimeOffset ModifiedTime { get => DateTimeOffset.UnixEpoch.AddSeconds(ModifiedOn ?? 0); }
|
||||||
|
public string RelativeTime {
|
||||||
|
get {
|
||||||
|
TimeSpan offset = DateTimeOffset.UtcNow - ModifiedTime;
|
||||||
|
|
||||||
|
var offsetString = string.Empty;
|
||||||
|
if (offset.TotalDays >= 1) offsetString = $"{Math.Floor(offset.TotalDays)} days ago";
|
||||||
|
else if (offset.TotalHours >= 1) offsetString = $"{Math.Floor(offset.TotalHours)} hours, {offset.Minutes} minutes ago";
|
||||||
|
else if (offset.TotalMinutes >= 1) offsetString = $"{Math.Floor(offset.TotalMinutes)} minutes ago";
|
||||||
|
else offsetString = $"{Math.Floor(offset.TotalSeconds)} seconds ago";
|
||||||
|
|
||||||
|
return offsetString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class PatchStatus {
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string Content { get; set; }
|
|
||||||
public string? Emoji { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class PatchStatusResponseData : IOmgLolResponseData {
|
|
||||||
public string Message { get; set; }
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string Url { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +1,26 @@
|
||||||
using Markdig;
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
namespace Neighbourhood.omg.lol.Models
|
||||||
public class Pic {
|
{
|
||||||
public string Id { get; set; }
|
public class Pic {
|
||||||
public string Url { get; set; }
|
public string Id { get; set; } = string.Empty;
|
||||||
public string Address { get; set; }
|
public string Url { get; set; } = string.Empty;
|
||||||
|
public string Address { get; set; } = string.Empty;
|
||||||
public long Created { get; set; }
|
public long Created { get; set; }
|
||||||
public long Size { get; set; }
|
public long Size { get; set; }
|
||||||
public string Mime { get; set; }
|
public string Mime { get; set; } = string.Empty;
|
||||||
public string Description { get; set; }
|
public string Description { get; set; } = string.Empty;
|
||||||
public string DescriptionHtml { get => Description == null ? string.Empty : Utilities.MdToHtml(Description); }
|
public string DescriptionHtml { get => Description == null ? string.Empty : Utilities.MdToHtml(Description); }
|
||||||
|
|
||||||
|
public DateTimeOffset CreatedTime { get => DateTimeOffset.UnixEpoch.AddSeconds(Created); }
|
||||||
|
|
||||||
[JsonPropertyName("exif")]
|
[JsonPropertyName("exif")]
|
||||||
public JsonElement ExifJson { get; set; }
|
public JsonElement ExifJson { get; set; }
|
||||||
|
|
||||||
public string RelativeTime {
|
public string RelativeTime {
|
||||||
get {
|
get {
|
||||||
DateTimeOffset createdTime = DateTimeOffset.UnixEpoch.AddSeconds(Created);
|
TimeSpan offset = DateTimeOffset.UtcNow - CreatedTime;
|
||||||
TimeSpan offset = DateTimeOffset.UtcNow - createdTime;
|
|
||||||
|
|
||||||
var offsetString = string.Empty;
|
var offsetString = string.Empty;
|
||||||
if (offset.TotalDays >= 1) offsetString = $"{Math.Floor(offset.TotalDays)} days ago";
|
if (offset.TotalDays >= 1) offsetString = $"{Math.Floor(offset.TotalDays)} days ago";
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class PostPic {
|
|
||||||
public string Description { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class PutPic {
|
|
||||||
public string Pic { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class PutPicResponseData : IOmgLolResponseData {
|
|
||||||
public string Message { get; set; }
|
|
||||||
public string Id { get; set; }
|
|
||||||
public long Size { get; set; }
|
|
||||||
public string Mime { get; set; }
|
|
||||||
public string Url { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class SomePicsResponseData : IOmgLolResponseData {
|
|
||||||
public string Message { get; set; }
|
|
||||||
public List<Pic>? Pics { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
219
Models/State.cs
219
Models/State.cs
|
@ -1,219 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.JSInterop;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Neighbourhood.omg.lol.Models {
|
|
||||||
public class State : INotifyPropertyChanged {
|
|
||||||
// Main data lists
|
|
||||||
public List<Status>? Statuses { get; set; }
|
|
||||||
public List<Pic>? Pics { get; set; }
|
|
||||||
public List<NowData>? NowGarden { get; set; }
|
|
||||||
public List<MarkupString>? EphemeralMessages { get; set; }
|
|
||||||
|
|
||||||
// Account data
|
|
||||||
public AccountResponseData? AccountInfo { get; set; }
|
|
||||||
public AddressResponseList? AddressList { get; set; }
|
|
||||||
|
|
||||||
public string? Name { get => AccountInfo?.Name; }
|
|
||||||
public string? Email { get => AccountInfo?.Email; }
|
|
||||||
public IEnumerable<string>? AddressNames { get => AddressList?.Select(a => a.Address); }
|
|
||||||
|
|
||||||
// Selected Address
|
|
||||||
public AddressResponseData? SelectedAddress { get; set; }
|
|
||||||
public string? SelectedAddressName { get => SelectedAddress?.Address; }
|
|
||||||
|
|
||||||
// data for selected address
|
|
||||||
public List<Status>? CachedAddressStatuses { get; set; }
|
|
||||||
public List<Pic>? CachedAddressPics { get; set; }
|
|
||||||
public MarkupString? CachedAddressBio { get; set; }
|
|
||||||
private string? _cachedAddress;
|
|
||||||
public string? CachedAddress {
|
|
||||||
get => _cachedAddress;
|
|
||||||
set {
|
|
||||||
if (_cachedAddress != value) {
|
|
||||||
_cachedAddress = value;
|
|
||||||
CachedAddressStatuses = new List<Status>();
|
|
||||||
CachedAddressPics = new List<Pic>();
|
|
||||||
CachedAddressBio = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// share intent stuff
|
|
||||||
public event EventHandler<EventArgs>? IntentReceived;
|
|
||||||
private string? _shareString;
|
|
||||||
public string? ShareString {
|
|
||||||
get => _shareString;
|
|
||||||
set {
|
|
||||||
_shareString = value;
|
|
||||||
IntentReceived?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private string? _sharePhoto;
|
|
||||||
public string? SharePhoto {
|
|
||||||
get => _sharePhoto;
|
|
||||||
set {
|
|
||||||
_sharePhoto = value;
|
|
||||||
IntentReceived?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// refreshing
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
private bool _isRefreshing;
|
|
||||||
public bool IsRefreshing {
|
|
||||||
get => _isRefreshing;
|
|
||||||
set {
|
|
||||||
_isRefreshing = value;
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRefreshing)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _canRefresh;
|
|
||||||
public bool CanRefresh {
|
|
||||||
get => _canRefresh;
|
|
||||||
set {
|
|
||||||
_canRefresh = value;
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanRefresh)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _atTop;
|
|
||||||
public bool AtTop {
|
|
||||||
get => _atTop;
|
|
||||||
set {
|
|
||||||
if (_atTop != value) {
|
|
||||||
_atTop = value;
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AtTop)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _atBottom;
|
|
||||||
public bool AtBottom {
|
|
||||||
get => _atBottom;
|
|
||||||
set {
|
|
||||||
if (_atBottom != value) {
|
|
||||||
_atBottom = value;
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AtBottom)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// api service
|
|
||||||
private RestService api { get; set; }
|
|
||||||
|
|
||||||
public State(RestService restService) {
|
|
||||||
api = restService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PopulateAccountDetails(string token) {
|
|
||||||
api.AddToken(token);
|
|
||||||
|
|
||||||
string accountJson = Preferences.Default.Get("accountdetails", string.Empty);
|
|
||||||
string addressJson = Preferences.Default.Get("accountaddresses", string.Empty);
|
|
||||||
string selectedAddressJson = Preferences.Default.Get("selectedaddress", string.Empty);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(accountJson)) AccountInfo = JsonSerializer.Deserialize<AccountResponseData>(accountJson);
|
|
||||||
if (!string.IsNullOrEmpty(addressJson)) AddressList = JsonSerializer.Deserialize<AddressResponseList>(addressJson);
|
|
||||||
if (!string.IsNullOrEmpty(selectedAddressJson)) SelectedAddress = JsonSerializer.Deserialize<AddressResponseData>(selectedAddressJson);
|
|
||||||
|
|
||||||
// if we haven't got account info, attempt to retrieve it.
|
|
||||||
if (AccountInfo == null) {
|
|
||||||
AccountInfo = await api.AccountInfo();
|
|
||||||
if (AccountInfo != null) {
|
|
||||||
Preferences.Default.Set("accountdetails", JsonSerializer.Serialize(AccountInfo));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we don't have the list of addresses, attempt to retrieve that.
|
|
||||||
if (AddressList == null) {
|
|
||||||
AddressList = await api.Addresses();
|
|
||||||
if (AddressList != null) {
|
|
||||||
Preferences.Default.Set("accountaddresses", JsonSerializer.Serialize(AddressList));
|
|
||||||
SelectedAddress = AddressList.FirstOrDefault();
|
|
||||||
Preferences.Default.Set("selectedaddress", JsonSerializer.Serialize(SelectedAddress));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RemoveAccountDetails() {
|
|
||||||
Preferences.Default.Clear();
|
|
||||||
AccountInfo = null;
|
|
||||||
AddressList = null;
|
|
||||||
SelectedAddress = null;
|
|
||||||
api.RemoveToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MarkupString?> GetBio(string address, bool forceRefresh = false) {
|
|
||||||
CachedAddress = address;
|
|
||||||
if (forceRefresh || CachedAddressBio == null) {
|
|
||||||
CachedAddressBio = await api.StatuslogBio(address);
|
|
||||||
}
|
|
||||||
return CachedAddressBio;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<MarkupString>?> GetEphemeralMessages(bool forceRefresh = false) {
|
|
||||||
if(forceRefresh || this.EphemeralMessages == null || this.EphemeralMessages.Count == 0) {
|
|
||||||
this.EphemeralMessages = await api.Ephemeral();
|
|
||||||
}
|
|
||||||
return this.EphemeralMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Status>?> GetStatuses(bool forceRefresh = false) {
|
|
||||||
if (forceRefresh || this.Statuses == null || this.Statuses.Count == 0) {
|
|
||||||
this.Statuses = await api.StatuslogLatest();
|
|
||||||
}
|
|
||||||
return this.Statuses;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Status>?> GetStatuses(string address, bool forceRefresh = false) {
|
|
||||||
this.CachedAddress = address;
|
|
||||||
if (forceRefresh || this.CachedAddressStatuses == null || this.CachedAddressStatuses.Count == 0) {
|
|
||||||
this.CachedAddressStatuses = await api.Statuslog(address);
|
|
||||||
}
|
|
||||||
return this.CachedAddressStatuses;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<NowData>?> GetNowGarden(bool forceRefresh = false) {
|
|
||||||
if (forceRefresh || this.NowGarden == null || this.NowGarden.Count == 0) {
|
|
||||||
this.NowGarden = await api.NowGarden();
|
|
||||||
}
|
|
||||||
return this.NowGarden;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Pic>?> GetPics(bool forceRefresh = false) {
|
|
||||||
if(forceRefresh || this.Pics == null || this.Pics.Count == 0) {
|
|
||||||
this.Pics = await api.SomePics();
|
|
||||||
}
|
|
||||||
return this.Pics;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Pic>?> GetPics(string address, bool forceRefresh = false) {
|
|
||||||
CachedAddress = address;
|
|
||||||
if (forceRefresh || this.CachedAddressPics == null || this.CachedAddressPics.Count == 0) {
|
|
||||||
CachedAddressPics = (await api.SomePics(address)) ?? new List<Pic>();
|
|
||||||
}
|
|
||||||
return CachedAddressPics;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RefreshStatuses() => await GetStatuses(forceRefresh: true);
|
|
||||||
public async Task RefreshPics() => await GetPics(forceRefresh: true);
|
|
||||||
public async Task RefreshNow() => await GetNowGarden(forceRefresh: true);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[JSInvokable("SetAtTop")]
|
|
||||||
public void SetAtTop(bool atTop) {
|
|
||||||
if(AtTop != atTop) AtTop = atTop;
|
|
||||||
//if(CanRefresh != atTop) CanRefresh = atTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable("SetAtBottom")]
|
|
||||||
public void SetAtBottom(bool atBottom) {
|
|
||||||
if(AtBottom != atBottom) AtBottom = atBottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue