Compare commits

...

10 commits

Author SHA1 Message Date
f43e1a7bc1 minor fixes
- no bio if empty
- fixed prami size on mobile
2024-06-18 16:46:01 +10:00
f2025df922 Share target updates (including pics!)
Still need to work out the "multiple windows" type issue.
2024-06-18 16:23:03 +10:00
63d844e7a3 Share new status (from share target, Android) 2024-06-18 11:58:51 +10:00
413051ad4c Added refresh (no pull-to-refresh, yet) 2024-06-18 11:44:18 +10:00
996e1286cb fixed small copy-paste error 2024-06-18 11:43:38 +10:00
df05e8a819 Made the api a singleton and got a share intent working
The result of the share intent needs more work, though.
2024-06-14 17:20:04 +10:00
a8c36eaea8 Layout and resource fixes for mobile 2024-06-14 11:44:23 +10:00
2c38dc2733 Fixes, adjustments and tidy up 2024-06-14 10:45:31 +10:00
8c8634f3c3 Fixes and editing statuses 2024-06-13 20:02:51 +10:00
ed5ad5a7b1 Making things a little more colourful 2024-06-13 16:35:40 +10:00
63 changed files with 1108 additions and 472 deletions

View file

@ -1,5 +1,6 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">
@ -46,7 +47,6 @@
loading = true;
await InvokeAsync(StateHasChanged);
RestService api = new RestService();
if(!string.IsNullOrEmpty(Pic.Id)) {
await api.PostPicDescription(State.SelectedAddressName, Pic.Id, Description);
await State.RefreshPics();

View file

@ -0,0 +1,88 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">
<div class="row">
<div class="min square extra">
<button class="transparent square extra no-margin">
<object id="status-emoji" class="large emoji @(Emoji == null ? "animated" : string.Empty)" data-emoji="@(Emoji ?? "🫥")">@(Emoji ?? "🫥")</object>
<menu class="no-wrap">
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@@^1/index.js"></script>
<emoji-picker emoji-version="15.1"></emoji-picker>
<script>
document.querySelector('emoji-picker')
.addEventListener('emoji-click', event => {
document.getElementById('status-emoji').setAttribute('data-emoji', event.detail.unicode)
const input = document.getElementById('status-emoji-input')
input.value = event.detail.unicode
var event = new Event('change');
input.dispatchEvent(event);
})
</script>
</menu>
</button>
</div>
<div class="field textarea label border max">
<InputTextArea @bind-Value="Content"></InputTextArea>
<label>Status</label>
</div>
</div>
<nav class="right-align no-space">
<InputText id="status-emoji-input" class="invisible" @bind-Value="Emoji"></InputText>
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
<button @onclick="PatchStatus" disabled="@loading">
@if (loading) {
<span>Saving...</span>
}
else {
<i class="fa-solid fa-floppy-disk"></i> <span>Save</span>
}
</button>
</nav>
</dialog>
@code {
private Status? _status;
public Status? Status {
get => _status;
set {
_status = value;
Content = _status?.Content;
Emoji = _status?.Emoji;
InvokeAsync(StateHasChanged);
}
}
public string? Content { get; set; }
public string? Emoji { get; set; }
private bool loading = false;
[Parameter]
public string id { get; set; }
protected override async Task OnInitializedAsync() {
Content = Status?.Content;
Emoji = Status?.Emoji;
}
public async Task PatchStatus() {
loading = true;
await InvokeAsync(StateHasChanged);
if (!string.IsNullOrEmpty(Status?.Id)) {
await api.PatchStatus(State.SelectedAddressName, Status.Id, Content, Emoji);
await State.RefreshStatuses();
await InvokeAsync(StateHasChanged);
}
await JS.InvokeVoidAsync("ui", "#" + id);
// clear input
Content = string.Empty;
Emoji = string.Empty;
Status = null;
loading = false;
await InvokeAsync(StateHasChanged);
}
}

View file

@ -1,5 +1,6 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@if(Html != null) {
<iframe id="@id" frameborder="0" scrolling="no" srcdoc="@Html" onload="() => iframeResize({ license: 'GPLv3' })"></iframe>
}
@ -11,11 +12,6 @@
public string id { get; set; }
public MarkupString? Html { get; set; }
protected override void OnInitialized() {
base.OnInitialized();
State.CurrentPage = Page.Other;
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
if(firstRender){
await Reload();
@ -24,7 +20,6 @@
public async Task Reload() {
if (Html == null){
RestService api = new RestService();
Html = await api.GetHtml(Url);
string? HtmlString = Html?.ToString();
HtmlString = HtmlString?.Replace("</head>", "<base target='_blank'></head>");

View file

@ -0,0 +1,78 @@
@inject CustomAuthenticationStateProvider AuthStateProvider;
@inject State State;
<AuthorizeView>
<Authorized>
<NavLink>
<button class="transparent circle small large">
<img class="responsive avatar" src="https://profiles.cache.lol/@State.SelectedAddressName/picture" alt="@State.SelectedAddressName">
<menu class="no-wrap">
<a class="m s row">
<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>
</button>
<small class="s m address"><i class="fa-solid fa-fw fa-at tiny"></i>@State.SelectedAddressName</small>
</NavLink>
<div class="l">
Hey, @State.Name. <br />
<a class="address" href="/person/@State.SelectedAddressName"><i class="fa-solid fa-fw fa-at tiny"></i>@State.SelectedAddressName</a>
</div>
</Authorized>
<NotAuthorized>
<NavLink>
<button class="transparent square small large">
<img class="responsive" src="/img/prami-neighbourhood.svg">
<menu class="no-wrap">
<a class="m s row">
<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>
</button>
<small class="s m address">Omg.lol</small>
</NavLink>
<div class="l">
Hey there. <br />
<a href="/login">Login?</a>
</div>
</NotAuthorized>
</AuthorizeView>
@code {
public void changeAddress(AddressResponseData address) {
State.SelectedAddress = address;
}
}

View file

@ -1,4 +1,5 @@
@inherits LayoutComponentBase
@implements IDisposable
@inject NavigatorService NavigatorService
@inject NavigationManager NavigationManager
@inject State State
@ -11,5 +12,22 @@
protected override void OnInitialized() {
base.OnInitialized();
NavigatorService.NavigationManager = NavigationManager;
State.IntentReceived += IntentRecieved;
if (!string.IsNullOrEmpty(State.ShareString) || !string.IsNullOrEmpty(State.SharePhoto)) {
IntentRecieved();
}
}
private void IntentRecieved(object? sender = null, EventArgs? e = null) {
if (!string.IsNullOrEmpty(State.ShareString)) {
NavigationManager.NavigateTo($"/sharetext");
}
else if (!string.IsNullOrEmpty(State.SharePhoto)) {
NavigationManager.NavigateTo($"/sharepic");
}
}
void IDisposable.Dispose() {
State.IntentReceived -= IntentRecieved;
}
}

View file

@ -0,0 +1,16 @@
<NavLink class="nav-link" href="/statuslog/latest">
<i class="square fa-solid fa-message-smile"></i>
<div class="label">Status.lol</div>
</NavLink>
<NavLink class="nav-link" href="/pics">
<i class="square fa-solid fa-images"></i>
<div class="label">Some.pics</div>
</NavLink>
<NavLink class="nav-link" href="/ephemeral">
<i class="square fa-light fa-comment-dots"></i>
<div class="label">Eph.emer.al</div>
</NavLink>
<NavLink class="nav-link" href="/now">
<i class="square fa-duotone fa-seedling"></i>
<div class="label">Now.garden</div>
</NavLink>

View file

@ -1,188 +1,14 @@
@inject CustomAuthenticationStateProvider AuthStateProvider;
@inject State State;
<nav class="left drawer l">
<nav class="left drawer l">
<header>
<nav>
<AuthorizeView>
<Authorized>
<button class="transparent circle large">
<img class="responsive avatar" src="https://profiles.cache.lol/@State.SelectedAddressName/picture" alt="@State.SelectedAddressName">
<menu class="no-wrap">
@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>
}
<a class="row" @onclick='() => AuthStateProvider.Logout()'>
<i class="fa-solid fa-door-open"></i>
<span>Logout</span>
</a>
</menu>
</button>
<div>
Hey, @State.Name. <br />
<a class="address" href="/person/@State.SelectedAddressName"><i class="fa-solid fa-fw fa-at tiny"></i>@State.SelectedAddressName</a>
</div>
</Authorized>
<NotAuthorized>
<button class="transparent square large">
<img class="responsive" src="/img/prami-neighbourhood.svg">
<menu class="no-wrap">
<a class="row" href="/login">
<i class="fa-solid fa-door-closed"></i>
<span>Login</span>
</a>
</menu>
</button>
<div>
Hey there. <br />
<a href="/login">Login?</a>
</div>
</NotAuthorized>
</AuthorizeView>
<AvatarMenu></AvatarMenu>
</nav>
</header>
<NavLink class="row nav-link" href="/statuslog/latest">
<i class="fa-solid fa-message-smile"></i>
<div>Status.lol</div>
</NavLink>
<NavLink class="row nav-link" href="/pics">
<i class="fa-solid fa-images"></i>
<div>Some.pics</div>
</NavLink>
<NavLink class="row nav-link" href="/ephemeral">
<i class="fa-light fa-comment-dots"></i>
<div>Eph.emer.al</div>
</NavLink>
<NavLink class="row nav-link" href="/now">
<i class="fa-duotone fa-seedling"></i>
<div>Now.garden</div>
</NavLink>
<NavLinks></NavLinks>
</nav>
<nav class="left m">
<header>
<AuthorizeView>
<Authorized>
<button class="transparent circle">
<img class="responsive" src="https://profiles.cache.lol/@State.SelectedAddressName/picture" alt="@State.SelectedAddressName">
<menu class="no-wrap">
<a class="row">
<img class="tiny" data-emoji="👋">
<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" 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>
}
<a class="row" @onclick='() => AuthStateProvider.Logout()'>
<i class="fa-solid fa-door-open"></i>
<span>Logout</span>
</a>
</menu>
</button>
</Authorized>
<NotAuthorized>
<button class="transparent square">
<img class="responsive" src="/img/prami-neighbourhood.svg">
<menu class="no-wrap">
<a class="row">
<i class="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>
</button>
</NotAuthorized>
</AuthorizeView>
</header>
<NavLink class="nav-link" href="/statuslog/latest">
<i class="fa-solid fa-message-smile"></i>
<small>Status.lol</small>
</NavLink>
<NavLink class="nav-link" href="/pics">
<i class="fa-solid fa-image"></i>
<small>Some.pics</small>
</NavLink>
<NavLink class="nav-link" href="/ephemeral">
<i class="fa-light fa-comment-dots"></i>
<small>Eph.emer.al</small>
</NavLink>
<NavLink class="nav-link" href="/now">
<i class="fa-duotone fa-seedling"></i>
<small>Now.garden</small>
</NavLink>
<nav class="bottom m s scroll">
<NavLinks></NavLinks>
<AvatarMenu></AvatarMenu>
</nav>
<nav class="bottom s">
<NavLink class="nav-link" href="/statuslog/latest">
<i class="fa-solid fa-message-smile"></i>
<small>Status.lol</small>
</NavLink>
<NavLink class="nav-link" href="/pics">
<i class="fa-solid fa-image"></i>
<small>Some.pics</small>
</NavLink>
<NavLink class="nav-link" href="/ephemeral">
<i class="fa-light fa-comment-dots"></i>
<small>Eph.emer.al</small>
</NavLink>
<NavLink class="nav-link" href="/now">
<i class="fa-duotone fa-seedling"></i>
<small>Now.garden</small>
</NavLink>
<AuthorizeView>
<Authorized>
<NavLink>
<button class="transparent circle small">
<img class="responsive" src="https://profiles.cache.lol/@State.SelectedAddressName/picture" alt="@State.SelectedAddressName">
<menu>
<a class="row">
<img class="tiny" data-emoji="👋">
<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" 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>
}
<a class="row" @onclick='() => AuthStateProvider.Logout()'>
<i class="fa-solid fa-door-open"></i>
<span>Logout</span>
</a>
</menu>
</button>
<small class="address"><i class="fa-solid fa-fw fa-at tiny"></i>@State.SelectedAddressName</small>
</NavLink>
</Authorized>
<NotAuthorized>
<button class="transparent square">
<img class="responsive" src="/img/prami-neighbourhood.svg">
<menu>
<a class="row">
<i class="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>
</button>
</NotAuthorized>
</AuthorizeView>
</nav>
@code {
public void changeAddress(AddressResponseData address) {
State.SelectedAddress = address;
}
}

View file

@ -1,8 +1,10 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@inject NavigationManager navigationManager
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active">
<h5>Share a picture</h5>
<div class="row">
<button @onclick="PicFromMedia"><i class="fa-solid fa-image"></i> Select a picture</button>
@ -13,10 +15,12 @@
<input type="text">
<label>Select a picture</label>
</div> *@
@if(File != null && Base64File != null && FileSize != null){
</div>
<div class="row">
@if(Base64File != null && FileSize != null){
<img class="extra" src="@Base64Url">
<small>
@File.ContentType (@formatSizeUnits(FileSize))
@FileContentType (@formatSizeUnits(FileSize))
</small>
}
@ -42,27 +46,35 @@
@code {
// private IBrowserFile? File { get; set; }
private FileResult? File { get; set; }
private string? Base64File { get; set; }
private long? FileSize { get; set; }
private string? Base64Url {
get {
if(File == null || Base64File == null) return null;
return $"data:{File.ContentType};base64,{Base64File}";
}
}
private string Description { get; set; }
private bool loading = false;
[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 string? Base64Url {
get {
if (FileContentType == null || Base64File == null) return null;
return $"data:{FileContentType};base64,{Base64File}";
}
}
public async Task PostPic() {
loading = true;
await InvokeAsync(StateHasChanged);
RestService api = new RestService();
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)) {
await api.PostPicDescription(State.SelectedAddressName, response.Id, Description);
await State.RefreshPics();
@ -78,6 +90,13 @@
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
await base.OnAfterRenderAsync(firstRender);
if (firstRender) {
string tmp = "tmp";
}
}
// private async Task ChangeFile(InputFileChangeEventArgs e){
// File = e.File;
// }
@ -107,7 +126,8 @@
}
private async Task PopulateFileDetails() {
FileSize = await State.FileSize(File);
Base64File = await State.Base64FromFile(File);
FileContentType = File.ContentType;
FileSize = await Utilities.FileSize(File);
Base64File = await Utilities.Base64FromFile(File);
}
}

View file

@ -1,13 +1,15 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@inject NavigationManager navigationManager
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active">
<h5>Share your status</h5>
<div class="row">
<div class="min square extra">
<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="status-emoji" class="large emoji @(Emoji == null ? "animated" : string.Empty)" data-emoji="@(Emoji ?? "🫥")">@(Emoji ?? "🫥")</object>
<menu class="no-wrap">
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@@^1/index.js"></script>
<emoji-picker emoji-version="15.1"></emoji-picker>
@ -25,7 +27,7 @@
</button>
</div>
<div class="field textarea border max">
<InputTextArea @bind-Value="statusContent"></InputTextArea>
<InputTextArea @bind-Value="Content"></InputTextArea>
</div>
</div>
<nav class="right-align no-space">
@ -35,7 +37,7 @@
<span>Post this to Mastodon</span>
</label>
}
<InputText id="status-emoji-input" class="invisible" @bind-Value="statusEmoji"></InputText>
<InputText id="status-emoji-input" class="invisible" @bind-Value="Emoji"></InputText>
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
<button @onclick="PostStatus" disabled="@loading">
@if (loading) {
@ -49,20 +51,25 @@
</dialog>
@code {
private string statusContent = string.Empty;
private string? statusEmoji = null;
private bool postToMastodon = true;
private bool loading = false;
[Parameter]
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() {
await JS.InvokeVoidAsync("console.log", "hey from post status");
StatusPost post = new StatusPost
{
Emoji = statusEmoji,
Content = statusContent
Emoji = Emoji,
Content = Content
};
if (State?.SelectedAddress?.Preferences?.Statuslog?.MastodonPosting ?? false){
@ -70,17 +77,18 @@
}
loading = true;
InvokeAsync(StateHasChanged);
RestService api = new RestService();
var result = await api.StatusPost(State.SelectedAddressName, post);
await InvokeAsync(StateHasChanged);
var result = await api.StatusPost(State!.SelectedAddressName!, post);
if(result != null){
State.RefreshStatuses().ContinueWith(t => InvokeAsync(StateHasChanged));
await State.RefreshStatuses();
await InvokeAsync(StateHasChanged);
navigationManager.NavigateTo("/statuslog/latest");
}
this.Active = false;
await JS.InvokeVoidAsync("ui", "#" + id);
statusContent = string.Empty;
statusEmoji = null;
Content = string.Empty;
Emoji = null;
postToMastodon = true;
loading = false;
}

View file

@ -1,12 +1,15 @@
<div class="row center-align">
<h3>
<i class="@icon extra"></i> @title
<h1><i class="@icon page-heading-icon"></i></h1>
</div>
<div class="row center-align">
<h3 class="page-heading">
@title
</h3>
</div>
<div class="row center-align">
<p>@Description</p>
</div>
<div class="space"></div>
<div class="space margin"></div>
@code {
[Parameter]

View file

@ -1,6 +1,10 @@
@page "/ephemeral"
@implements IDisposable
@inject IJSRuntime JS
@inject State State
<RefreshButton></RefreshButton>
<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>
</PageHeading>
@ -24,7 +28,21 @@
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (messages == null || messages.Count == 0) messages = await State.GetEphemeralMessages();
State.PropertyChanged += StateChanged;
State.CanRefresh = true;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("removeElementById", "ephemeral-loading");
}
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
messages = await State.GetEphemeralMessages(true);
State.IsRefreshing = false;
}
}
public void Dispose() {
State.PropertyChanged -= StateChanged;
State.CanRefresh = false;
}
}

View file

@ -1,7 +0,0 @@
@page "/"
@inject CustomAuthenticationStateProvider AuthStateProvider;
<h1><i data-emoji="👋"></i> Hello, lol!</h1>
<a @onclick='() => AuthStateProvider.Logout()'>Logout</a>
Welcome to your new app.

View file

@ -1,6 +1,10 @@
@page "/now"
@implements IDisposable
@inject IJSRuntime JS
@inject State State
<RefreshButton></RefreshButton>
<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>
</PageHeading>
@ -12,7 +16,7 @@
<article class="now">
<nav>
<a class="author" href="/person/@now.Address#now">
<i class="fa-duotone fa-seedling"></i> @now.Address
<h6><i class="fa-duotone fa-seedling"></i> @now.Address</h6>
</a>
</nav>
<nav>
@ -31,7 +35,21 @@
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (garden == null || garden.Count == 0) garden = await State.GetNowGarden();
State.PropertyChanged += StateChanged;
State.CanRefresh = true;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("removeElementById", "now-loading");
}
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
garden = await State.GetNowGarden(true);
State.IsRefreshing = false;
}
}
public void Dispose() {
State.PropertyChanged -= StateChanged;
State.CanRefresh = false;
}
}

View file

@ -3,16 +3,18 @@
@inject IJSRuntime JS
@inject NavigationManager Nav
<RefreshButton></RefreshButton>
<div class="row center-align">
<h3><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 class="row center-align">
<img class="profile avatar" src="https://profiles.cache.lol/@Address/picture" alt="@Address" />
</div>
<div class="responsive">
<div class="tabs">
<div class="tabs scroll">
<a data-ui="#profile" @onclick="ReloadProfile">
<i class="fa-solid fa-id-card"></i>
<span>@(Address).omg.lol</span>
@ -41,21 +43,35 @@
</div>
<div id="statuses" class="page padding active">
<div id="info" class="box basis transparent">
<article id="bio" class="container shadowed blue-2-bg gray-9-fg">
@if (bio == null) {
<p><progress class="circle small"></progress></p>
}
else {
@bio
}
</article>
</div>
<StatusList StatusFunc="@(async() => await State.GetStatuses(Address))"></StatusList>
@if(bio == null || !string.IsNullOrEmpty(bio.ToString())) {
<div id="info" class="box basis transparent">
<article id="bio" class="container shadowed blue-2-bg gray-9-fg">
@if (bio == null) {
<p><progress class="circle small"></progress></p>
}
else {
@bio
}
</article>
</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">
<i class="fa-solid fa-pen-to-square"></i>
</button>
<NewStatusDialog id="post-modal"></NewStatusDialog>
}
</div>
<div id="pics" class="page padding">
<PicList PicsFunc="@(async() => await State.GetPics(Address))" Editable="@Editable"></PicList>
<PicList @ref="PicList" PicsFunc="@(async(refresh) => await State.GetPics(Address, refresh))" Editable="@IsMe"></PicList>
@if (IsMe) {
<button class="fab circle extra large-elevate" data-ui="#post-modal">
<i class="fa-solid fa-camera-retro"></i>
</button>
<NewPicDialog id="post-modal"></NewPicDialog>
}
</div>
@if(now != null){
<div id="now" class="page no-padding">
@ -66,8 +82,16 @@
</div>
@code {
private string _address;
[Parameter]
public string Address { get; set; }
public string Address {
get => _address;
set {
_address = value;
if (StatusList != null) StatusList.StatusFunc = async (refresh) => await State.GetStatuses(_address, refresh);
if (PicList != null) PicList.PicsFunc = async (refresh) => await State.GetPics(_address, refresh);
}
}
public string ProfileUrl {
get => $"https://{Address}.omg.lol/";
}
@ -75,7 +99,10 @@
public ExternalPageComponent? NowPage { get; set; }
public ExternalPageComponent? ProfilePage { get; set; }
private bool Editable {
private StatusList StatusList { get; set; }
private PicList PicList { get; set; }
private bool IsMe {
get => Address == State.SelectedAddressName;
}

View file

@ -1,6 +1,8 @@
@page "/pics"
@inject State State
<RefreshButton></RefreshButton>
<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>
</PageHeading>
@ -15,7 +17,7 @@
</AuthorizeView>
<div id="pics" class="responsive card-grid">
<PicList PicsFunc="@(async() => await State.GetPics())"></PicList>
<PicList PicsFunc="@(async(refresh) => await State.GetPics(refresh))"></PicList>
</div>
@code {

View 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;
}
}
}

View file

@ -0,0 +1,47 @@
@page "/sharetext"
@inject NavigationManager navigationManager
@inject AuthenticationStateProvider AuthStateProvider
@inject State State
<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>
<AuthorizeView>
<Authorized>
<button class="fab circle extra large-elevate" data-ui="#post-modal">
<i class="fa-solid fa-pen-to-square"></i>
</button>
<NewStatusDialog id="post-modal" Active="true" Content="@Text"></NewStatusDialog>
</Authorized>
</AuthorizeView>
@code {
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;
}
}
}

View file

@ -1,6 +1,9 @@
@page "/statuslog/latest"
@page "/"
@page "/statuslog/latest"
@inject State State
<RefreshButton></RefreshButton>
<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>
</PageHeading>
@ -15,7 +18,7 @@
</AuthorizeView>
<div id="statuses" class="responsive">
<StatusList StatusFunc="@(async() => await State.GetStatuses())"></StatusList>
<StatusList StatusFunc="@(async(refresh) => await State.GetStatuses(refresh))"></StatusList>
</div>
@code {

View file

@ -28,7 +28,7 @@
@code {
[Parameter]
public Pic Pic {get; set;}
[Parameter]
[Parameter]
public bool Editable { get; set; } = false;
[Parameter]
public EditPicDialog? Dialog { get; set; }

View file

@ -1,30 +0,0 @@
<template id="PicCard">
<article class="no-padding">
<img src="{{Pic.Url}}" loading="lazy">
<div class="padding">
<nav>
<a class="author" href="/person/{{Pic.Address}}#pics">
<i class="fa-solid fa-fw fa-at"></i>{{Pic.Address}}
</a>
<span class="max"></span>
<a class="chip transparent-border right">
<i class="fa fa-clock"></i> {{Pic.RelativeTime}}
</a>
<span class="max"></span>
<button class="share-button transparent circle">
<input type="hidden" name="url" value="{{Pic.Url}}" />
<input type="hidden" name="description" value="{{Pic.Description}}" />
<i class="fa-solid fa-share-nodes"></i>
</button>
</nav>
<p class="description">{{Pic.Description}}</p>
<nav>
<div class="max"></div>
<button class="edit-button">
<input type="hidden" name="id" value="{{Pic.Id}}" />
<i class="fa-solid fa-pencil"></i> Edit
</button>
</nav>
</div>
</article>
</template>

View file

@ -1,23 +0,0 @@
<template id="PicCard">
<article class="no-padding">
<img src="{{Pic.Url}}" loading="lazy">
<div class="padding">
<nav>
<a class="author" href="/person/{{Pic.Address}}#pics">
<i class="fa-solid fa-fw fa-at"></i>{{Pic.Address}}
</a>
<span class="max"></span>
<a class="chip transparent-border right">
<i class="fa fa-clock"></i> {{Pic.RelativeTime}}
</a>
<span class="max"></span>
<button class="share-button transparent circle">
<input type="hidden" name="url" value="{{Pic.Url}}" />
<input type="hidden" name="description" value="{{Pic.Description}}" />
<i class="fa-solid fa-share-nodes"></i>
</button>
</nav>
<p class="description">{{Pic.DescriptionHtml}}</p>
</div>
</article>
</template>

View file

@ -1,4 +1,5 @@
@inject IJSRuntime JS
@implements IDisposable
@inject IJSRuntime JS
@inject State State
@if (Editable) {
@ -13,7 +14,7 @@
@code {
[Parameter]
public Func<Task<List<Pic>?>> PicsFunc { get; set; }
public Func<bool, Task<List<Pic>?>> PicsFunc { get; set; }
[Parameter]
public bool Editable { get; set; } = false;
@ -24,8 +25,22 @@
// TODO: There is a noticable rendering delay between the pics loading and the page rendering
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (pics == null || pics.Count == 0) pics = await PicsFunc();
if (pics == null || pics.Count == 0) pics = await PicsFunc(false);
State.PropertyChanged += StateChanged;
State.CanRefresh = true;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("removeElementById", "pics-loading");
}
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
pics = await PicsFunc(true);
State.IsRefreshing = false;
}
}
public void Dispose() {
State.PropertyChanged -= StateChanged;
State.CanRefresh = false;
}
}

View file

@ -0,0 +1,18 @@
@inject State State
<button id="refreshButton" class="absolute transparent circle top right margin" @onclick="() => State.IsRefreshing = true">
<i class="fa-solid fa-arrow-rotate-right @(State.IsRefreshing ? "fa-spin" : "")"></i>
</button>
@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.IsRefreshing)) {
await InvokeAsync(StateHasChanged);
}
}
}

View file

@ -1,4 +1,5 @@
@using Microsoft.AspNetCore.Components.Authorization
@inject NavigationManager navigationManager
<Router AppAssembly="@typeof(MauiProgram).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
@ -16,3 +17,15 @@
</CascadingAuthenticationState>
</NotFound>
</Router>
@code
{
protected override void OnAfterRender(bool firstRender) {
string? shareString = Preferences.Get("shareString", null);
if (!string.IsNullOrWhiteSpace(shareString)) {
Preferences.Remove("shareString");
navigationManager.NavigateTo($"/sharetext/{shareString}");
}
}
}

View file

@ -1,22 +1,31 @@
<article class="status">
@inject IJSRuntime JS
<article class="status gray-9-fg" style="background-color:@(Status.Background)">
<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">
<a class="author" href="/person/@status.Address">
<i class="fa-solid fa-fw fa-at"></i>@status.Address
</a>
@status.HtmlContent
<nav class="no-margin">
<a class="chip transparent-border">
<i class="fa fa-clock"></i> @status.RelativeTime
</a>
<a class="chip transparent-border" href="@status.ExternalUrl" target="_blank">
<i class="fa fa-message-dots"></i> Respond
<div class="row">
<a class="author" href="/person/@Status.Address">
<i class="fa-solid fa-fw fa-at"></i>@Status.Address
</a>
<div class="max"></div>
<button class="transparent circle" @onclick="ShareClick">
<i class="fa-solid fa-share-nodes"></i>
</button>
@if (Editable) {
<button class="small circle small-elevate" @onclick="EditStatus">
<i class="fa-solid fa-pencil"></i>
</button>
}
</div>
@Status.HtmlContent
<nav class="no-margin">
<a class="chip transparent-border">
<i class="fa fa-clock"></i> @Status.RelativeTime
</a>
<a class="chip transparent-border" href="@Status.ExternalUrl" target="_blank">
<i class="fa fa-message-dots"></i> Respond
</a>
</nav>
</div>
</div>
@ -24,11 +33,21 @@
@code {
[Parameter]
public Status status { get; set; }
public Status Status { get; set; }
[Parameter]
public bool Editable { get; set; } = false;
[Parameter]
public EditStatusDialog? Dialog { get; set; }
private async Task EditStatus(EventArgs e) {
Dialog.Status = Status;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("ui", "#" + Dialog?.id);
}
public async Task ShareClick(EventArgs e){
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",
Subject = "I saw this on status.lol"
});

View file

@ -1,24 +1,45 @@
@inject IJSRuntime JS
@implements IDisposable
@inject IJSRuntime JS
@inject State State
@if (Editable) {
<EditStatusDialog @ref="Dialog" id="EditStatusModal"></EditStatusDialog>
}
@if(statuses != null) foreach(Status status in statuses) {
<StatusCard status="@status"></StatusCard>
<StatusCard Status="@status" Editable="Editable" Dialog="Dialog"></StatusCard>
}
<LoadingCard id="statusLoading" icon="fa-solid fa-message-smile"></LoadingCard>
@code {
[Parameter]
public Func<Task<List<Status>?>> StatusFunc { get; set; }
public Func<bool, Task<List<Status>?>> StatusFunc { get; set; }
[Parameter]
public bool Editable { get; set; } = false;
public EditStatusDialog? Dialog { get; set; }
private List<Status>? statuses;
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
if (statuses == null || statuses.Count == 0) statuses = await StatusFunc();
if (statuses == null || statuses.Count == 0) statuses = await StatusFunc(false);
State.PropertyChanged += StateChanged;
State.CanRefresh = true;
await InvokeAsync(StateHasChanged);
await JS.InvokeVoidAsync("removeElementById", "statusLoading");
}
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) {
statuses = await StatusFunc(true);
State.IsRefreshing = false;
}
}
public void Dispose() {
State.PropertyChanged -= StateChanged;
State.CanRefresh = false;
}
}

View file

@ -1,4 +1,5 @@
@using System.Net.Http
@using System.ComponentModel
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing

View file

@ -12,16 +12,18 @@ public partial class LoginWebViewPage : ContentPage
private AuthenticationStateProvider AuthStateProvider { get; set; }
private NavigatorService NavigatorService { get; set; }
private IConfiguration Configuration { get; set; }
private RestService api { get; set; }
private string? client_id;
private string? client_secret;
private string? redirect_uri;
public LoginWebViewPage(AuthenticationStateProvider authStateProvider, NavigatorService navigatorService, IConfiguration configuration)
public LoginWebViewPage(AuthenticationStateProvider authStateProvider, NavigatorService navigatorService, IConfiguration configuration, RestService restService)
{
this.AuthStateProvider = authStateProvider;
this.NavigatorService = navigatorService;
this.Configuration = configuration;
this.api = restService;
InitializeComponent();
client_id = configuration.GetValue<string>("client_id");
client_secret = configuration.GetValue<string>("client_secret");
@ -45,10 +47,8 @@ public partial class LoginWebViewPage : ContentPage
var query = HttpUtility.ParseQueryString(uri.Query);
string? code = query.Get("code");
if (!string.IsNullOrEmpty(code)) {
RestService api = new RestService();
string? token = await api.OAuth(code, client_id, client_secret, redirect_uri);
if (!string.IsNullOrEmpty(token)) {
Debug.WriteLine($"Fuck yeah, a token! {token}");
await ((CustomAuthenticationStateProvider)this.AuthStateProvider).Login(token);
NavigatorService.NavigationManager.NavigateTo(NavigatorService.NavigationManager.Uri, forceLoad: true);
await Shell.Current.GoToAsync("..");

View file

@ -16,10 +16,5 @@ namespace Neighbourhood.omg.lol {
Shell.Current.GoToAsync(nameof(LoginWebViewPage));
}
}
protected override void OnAppearing() {
base.OnAppearing();
Debug.WriteLine("And now you're back. From outer space.");
}
}
}

View file

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components.Authorization;
using Markdig;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Neighbourhood.omg.lol.Models;
@ -17,6 +18,7 @@ namespace Neighbourhood.omg.lol {
builder.Services.AddTransient<LoginWebViewPage>();
builder.Services.AddTransient<EphemeralWebPage>();
builder.Services.AddSingleton<RestService>();
builder.Services.AddSingleton<State>();
builder.Services.AddSingleton<NavigatorService>();

View file

@ -11,7 +11,7 @@ namespace Neighbourhood.omg.lol.Models {
public TimeData Updated { get; set; }
public string UpdatedRelative {
get => State.RelativeTimeFromUnix(Convert.ToInt64(Updated.UnixEpochTime));
get => Utilities.RelativeTimeFromUnix(Convert.ToInt64(Updated.UnixEpochTime));
}
}
}

13
Models/PatchStatus.cs Normal file
View file

@ -0,0 +1,13 @@
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; }
}
}

View file

@ -0,0 +1,13 @@
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; }
}
}

View file

@ -17,7 +17,7 @@ namespace Neighbourhood.omg.lol.Models {
public long Size { get; set; }
public string Mime { get; set; }
public string Description { get; set; }
public string DescriptionHtml { get => Description == null ? string.Empty : Markdown.ToHtml(Description); }
public string DescriptionHtml { get => Description == null ? string.Empty : Utilities.MdToHtml(Description); }
[JsonPropertyName("exif")]
public JsonElement ExifJson { get; set; }

View file

@ -1,32 +1,28 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.ComponentModel;
using System.Text.Json;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class State {
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; }
public Page CurrentPage { 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; }
public List<Status>? Statuses { get; set; }
public List<Pic>? Pics { get; set; }
public List<NowData>? NowGarden { get; set; }
public List<MarkupString>? EphemeralMessages { get; set; }
// data for selected address
public List<Status>? CachedAddressStatuses { get; set; }
public List<Pic>? CachedAddressPics { get; set; }
public MarkupString? CachedAddressBio { get; set; }
@ -43,8 +39,59 @@ namespace Neighbourhood.omg.lol.Models {
}
}
// share intent stuff
public event EventHandler<EventArgs>? IntentReceived;
private string? _shareString;
public string? ShareString {
get => _shareString;
set {
_shareString = value;
IntentReceived?.Invoke(this, EventArgs.Empty);
}
}
public string? ShareStringSubject { get; set; }
private string? _sharePhoto;
public string? SharePhoto {
get => _sharePhoto;
set {
_sharePhoto = value;
IntentReceived?.Invoke(this, EventArgs.Empty);
}
}
public long? SharePhotoSize { get; set; }
public string? SharePhotoContentType { get; set; }
public string? SharePhotoText { get; set; }
// 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)));
}
}
// api service
private RestService api { get; set; }
public State(RestService restService) {
api = restService;
}
public async Task PopulateAccountDetails(string token) {
RestService api = new RestService(token);
api.AddToken(token);
string accountJson = Preferences.Default.Get("accountdetails", string.Empty);
string addressJson = Preferences.Default.Get("accountaddresses", string.Empty);
@ -78,19 +125,18 @@ namespace Neighbourhood.omg.lol.Models {
AccountInfo = null;
AddressList = null;
SelectedAddress = null;
api.RemoveToken();
}
public async Task<MarkupString?> GetBio(string address, bool forceRefresh = false) {
CachedAddress = address;
if (forceRefresh || CachedAddressBio == null) {
RestService api = new RestService();
CachedAddressBio = await api.StatuslogBio(address);
}
return CachedAddressBio;
}
public async Task<List<MarkupString>?> GetEphemeralMessages(bool forceRefresh = false) {
RestService api = new RestService();
if(forceRefresh || this.EphemeralMessages == null || this.EphemeralMessages.Count == 0) {
this.EphemeralMessages = await api.Ephemeral();
}
@ -98,7 +144,6 @@ namespace Neighbourhood.omg.lol.Models {
}
public async Task<List<Status>?> GetStatuses(bool forceRefresh = false) {
RestService api = new RestService();
if (forceRefresh || this.Statuses == null || this.Statuses.Count == 0) {
this.Statuses = await api.StatuslogLatest();
}
@ -107,7 +152,6 @@ namespace Neighbourhood.omg.lol.Models {
public async Task<List<Status>?> GetStatuses(string address, bool forceRefresh = false) {
this.CachedAddress = address;
RestService api = new RestService();
if (forceRefresh || this.CachedAddressStatuses == null || this.CachedAddressStatuses.Count == 0) {
this.CachedAddressStatuses = await api.Statuslog(address);
}
@ -115,7 +159,6 @@ namespace Neighbourhood.omg.lol.Models {
}
public async Task<List<NowData>?> GetNowGarden(bool forceRefresh = false) {
RestService api = new RestService();
if (forceRefresh || this.NowGarden == null || this.NowGarden.Count == 0) {
this.NowGarden = await api.NowGarden();
}
@ -124,7 +167,6 @@ namespace Neighbourhood.omg.lol.Models {
public async Task<List<Pic>?> GetPics(bool forceRefresh = false) {
if(forceRefresh || this.Pics == null || this.Pics.Count == 0) {
RestService api = new RestService();
this.Pics = await api.SomePics();
}
return this.Pics;
@ -133,49 +175,14 @@ namespace Neighbourhood.omg.lol.Models {
public async Task<List<Pic>?> GetPics(string address, bool forceRefresh = false) {
CachedAddress = address;
if (forceRefresh || this.CachedAddressPics == null || this.CachedAddressPics.Count == 0) {
RestService api = new RestService();
CachedAddressPics = (await api.SomePics(address)) ?? new List<Pic>();
}
return CachedAddressPics;
}
public async Task<long> FileSize(FileResult file) {
using var fileStream = await file.OpenReadAsync();
return fileStream.Length;
}
public async Task<string> Base64FromFile(FileResult file) {
using var memoryStream = new MemoryStream();
using var fileStream = await file.OpenReadAsync();
await fileStream.CopyToAsync(memoryStream);
byte[] bytes = memoryStream.ToArray();
return Convert.ToBase64String(bytes);
}
public async Task RefreshStatuses() => await GetStatuses(forceRefresh: true);
public async Task RefreshPics() => await GetPics(forceRefresh: true);
public async Task RefreshNow() => await GetNowGarden(forceRefresh: true);
public static string RelativeTimeFromUnix(long unix) {
DateTimeOffset createdTime = DateTimeOffset.UnixEpoch.AddSeconds(unix);
TimeSpan offset = DateTimeOffset.UtcNow - createdTime;
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;
}
}
public enum Page {
None = 0,
Status,
Pics,
Ephemeral,
NowGarden,
Other
}
}

View file

@ -9,6 +9,7 @@ namespace Neighbourhood.omg.lol.Models {
public string RelativeTime { get; set; }
public string Emoji { get; set; }
public string Background { get; set; }
public string BackgroundColor { get => Background; set => Background = "#" + value; }
public string Content { get; set; }
public string RenderedMarkdown { get; set; }
public string ExternalUrl { get; set; }
@ -20,10 +21,7 @@ namespace Neighbourhood.omg.lol.Models {
}
public MarkupString HtmlContent {
get {
if(!string.IsNullOrEmpty(RenderedMarkdown)) return (MarkupString)RenderedMarkdown;
else return (MarkupString)Markdown.ToHtml(Content);
}
get => Utilities.MdToHtmlMarkup(Content);
}
public string Url {

51
Models/Utilities.cs Normal file
View file

@ -0,0 +1,51 @@
using Markdig;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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;
}
}
}

View file

@ -77,7 +77,7 @@
<MauiIcon Include="Resources\AppIcon\icon_background.svg" ForegroundFile="Resources\AppIcon\icon_foreground.svg" Color="#f3eb76" BaseSize="1024,1024" />
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#343a40" BaseSize="128,128" />
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
@ -90,6 +90,111 @@
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<MauiFont Remove="Resources\Fonts\fa-brands-400.ttf" />
<MauiFont Remove="Resources\Fonts\fa-brands-400.woff2" />
<MauiFont Remove="Resources\Fonts\fa-duotone-900.ttf" />
<MauiFont Remove="Resources\Fonts\fa-duotone-900.woff2" />
<MauiFont Remove="Resources\Fonts\fa-light-300.ttf" />
<MauiFont Remove="Resources\Fonts\fa-light-300.woff2" />
<MauiFont Remove="Resources\Fonts\fa-regular-400.ttf" />
<MauiFont Remove="Resources\Fonts\fa-regular-400.woff2" />
<MauiFont Remove="Resources\Fonts\fa-solid-900.ttf" />
<MauiFont Remove="Resources\Fonts\fa-solid-900.woff2" />
<MauiFont Remove="Resources\Fonts\fa-thin-100.ttf" />
<MauiFont Remove="Resources\Fonts\fa-thin-100.woff2" />
<MauiFont Remove="Resources\Fonts\fa-v4compatibility.ttf" />
<MauiFont Remove="Resources\Fonts\fa-v4compatibility.woff2" />
<MauiFont Remove="Resources\Fonts\omg.lol-icons.woff2" />
<MauiFont Remove="Resources\Fonts\seguiemj.ttf" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\Fonts\fa-brands-400.ttf" />
<None Remove="Resources\Fonts\fa-brands-400.woff2" />
<None Remove="Resources\Fonts\fa-duotone-900.ttf" />
<None Remove="Resources\Fonts\fa-duotone-900.woff2" />
<None Remove="Resources\Fonts\fa-light-300.ttf" />
<None Remove="Resources\Fonts\fa-light-300.woff2" />
<None Remove="Resources\Fonts\fa-regular-400.ttf" />
<None Remove="Resources\Fonts\fa-regular-400.woff2" />
<None Remove="Resources\Fonts\fa-solid-900.ttf" />
<None Remove="Resources\Fonts\fa-solid-900.woff2" />
<None Remove="Resources\Fonts\fa-thin-100.ttf" />
<None Remove="Resources\Fonts\fa-thin-100.woff2" />
<None Remove="Resources\Fonts\fa-v4compatibility.ttf" />
<None Remove="Resources\Fonts\fa-v4compatibility.woff2" />
<None Remove="Resources\Fonts\omg.lol-icons.woff2" />
<None Remove="Resources\Fonts\seguiemj.ttf" />
</ItemGroup>
<ItemGroup>
<Content Include="Resources\Fonts\fa-brands-400.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-brands-400.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-duotone-900.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-duotone-900.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-light-300.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-light-300.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-regular-400.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-regular-400.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-solid-900.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-solid-900.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-thin-100.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-thin-100.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-v4compatibility.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-v4compatibility.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\omg.lol-icons.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\seguiemj.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Markdig" Version="0.37.0" />

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="au.death.lol.omg.neighbourhood" android:versionCode="1" android:versionName="1.0.0">
<application android:allowBackup="true" android:icon="@mipmap/icon_background" android:supportsRtl="true" android:label="Neighbourhood.omg.lol"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View file

@ -1,9 +1,62 @@
using Android.App;

using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Net;
using Android.OS;
using Microsoft.Extensions.DependencyInjection;
using Neighbourhood.omg.lol.Models;
namespace Neighbourhood.omg.lol {
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
[Activity(Theme = "@style/Maui.SplashTheme", LaunchMode = LaunchMode.SingleTop, MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
[IntentFilter([Intent.ActionSend], Categories = [Intent.CategoryDefault], DataMimeType = "text/plain")]
[IntentFilter([Intent.ActionSend], Categories = [Intent.CategoryDefault], DataMimeType = "*/*")]
public class MainActivity : MauiAppCompatActivity {
protected override void OnCreate(Bundle savedInstanceState) {
base.OnCreate(savedInstanceState);
// In case the app was opened (on first load) with an `ActionView` intent
OnNewIntent(this.Intent);
}
protected override void OnNewIntent(Intent? intent) {
base.OnNewIntent(intent);
if (intent != null && intent.Type != null) {
if (intent.Type.StartsWith("text/")) //string
{
string? subject = intent.GetStringExtra(Intent.ExtraSubject);
string? shareString = intent.GetStringExtra(Intent.ExtraText);
if (!string.IsNullOrWhiteSpace(shareString)) {
State state = IPlatformApplication.Current!.Services.GetService<State>()!;
state.ShareStringSubject = subject;
state.ShareString = shareString;
}
}
else if (intent.Type.StartsWith("image/")) //image
{
string? shareString = intent.GetStringExtra(Intent.ExtraText);
var extra = intent.GetParcelableExtra(Intent.ExtraStream);
if (extra is Android.Net.Uri) {
Stream? stream = ContentResolver?.OpenInputStream(extra as Android.Net.Uri);
byte[] bytes = new byte[stream?.Length ?? 0];
stream?.Read(bytes, 0, bytes.Length);
string base64String = Convert.ToBase64String(bytes);
if (!string.IsNullOrWhiteSpace(base64String)) {
State state = IPlatformApplication.Current!.Services.GetService<State>()!;
state.SharePhotoContentType = intent.Type;
state.SharePhotoSize = bytes.Length;
state.SharePhotoText = shareString;
state.SharePhoto = base64String;
}
}
}
else if (intent.Type.Equals(Intent.ActionSendMultiple)) //Multiple file
{
var uriList = intent.GetParcelableArrayListExtra(Intent.ExtraStream);
}
}
}
}
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorPrimary">#343a40</color>
<color name="colorPrimaryDark">#343a40</color>
<color name="colorAccent">#2B0B98</color>
</resources>

View file

@ -8,13 +8,13 @@
<filter filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB" id="filter_1">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 0" />
<feOffset dx="0" dy="40" />
<feGaussianBlur stdDeviation="20" />
<feOffset dx="0" dy="27" />
<feGaussianBlur stdDeviation="13.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2509804 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect0_dropShadow" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 0" />
<feOffset dx="0" dy="15" />
<feGaussianBlur stdDeviation="15" />
<feOffset dx="0" dy="10" />
<feGaussianBlur stdDeviation="10.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.29803923 0" />
<feBlend mode="normal" in2="effect0_dropShadow" result="effect1_dropShadow" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape" />
@ -22,15 +22,15 @@
</defs>
<g id="neighbourhood-purple">
<rect x="0" y="0" width="100%" height="100%" fill="url(#grad1)"/>
<path d="M864.362 79.8798L864.362 134.95M662.099 55.0981L975.99 200.559M343.467 201.034L657.357 55.5735M276.97 55.0979L465.304 142.375M47 159.732L273.001 55" id="Shape" fill="none" fill-rule="evenodd" stroke="#8C53E7" stroke-width="44" stroke-linecap="round" stroke-linejoin="round" filter="url(#filter_1)" />
<g filter="url(#filter_1)">
<g id="Group" transform="translate(72.13778 248.08119)">
<path d="M439.862 703.781C372.32 703.781 304.78 678.013 253.244 626.482L77.3003 450.537C-25.7668 347.47 -25.7668 180.368 77.3003 77.3012C176.882 -22.2802 336.238 -25.6497 439.862 67.1961C543.49 -25.6464 702.847 -22.277 802.425 77.3012C905.492 180.368 905.492 347.47 802.425 450.537L626.48 626.482C574.949 678.013 507.406 703.781 439.862 703.781" id="Path" fill="#B776FC" stroke="none" />
<path d="M367.283 305.824C400.495 360.018 479.232 360.018 512.441 305.824" id="Path" fill="none" stroke="#22184C" stroke-width="33" stroke-linecap="round" />
<path d="M481.297 222.748C481.297 205.5 495.28 191.518 512.527 191.518C529.776 191.518 543.758 205.5 543.758 222.748C543.758 239.996 529.776 253.978 512.527 253.978C495.28 253.978 481.297 239.996 481.297 222.748Z" id="Oval" fill="#22184C" fill-rule="evenodd" stroke="none" />
<path d="M335.791 222.748C335.791 205.5 349.773 191.518 367.021 191.518C384.269 191.518 398.251 205.5 398.251 222.748C398.251 239.996 384.269 253.978 367.021 253.978C349.773 253.978 335.791 239.996 335.791 222.748Z" id="Oval" fill="#22184C" fill-rule="evenodd" stroke="none" />
<path d="M107.502 285.032C107.502 227.507 154.136 180.873 211.662 180.873C269.187 180.873 315.821 227.507 315.821 285.032C315.821 342.558 269.187 389.192 211.662 389.192C154.136 389.192 107.502 342.558 107.502 285.032Z" id="Oval" fill="#8C53E7" fill-rule="evenodd" stroke="none" />
<path d="M563.903 285.032C563.903 227.507 610.537 180.873 668.063 180.873C725.589 180.873 772.222 227.507 772.222 285.032C772.222 342.558 725.589 389.192 668.063 389.192C610.537 389.192 563.903 342.558 563.903 285.032Z" id="Oval" fill="#8C53E7" fill-rule="evenodd" stroke="none" />
<path d="M750.419 224.839L750.419 262.111M613.471 208.066L826 306.515M397.732 306.837L610.26 208.388M352.708 208.066L480.225 267.135M197 278.883L350.021 208" id="Shape" fill="none" fill-rule="evenodd" stroke="#8C53E7" stroke-width="29" stroke-linecap="round" stroke-linejoin="round" filter="url(#filter_1)" clip-path="url(#clip_1)" />
<g filter="url(#filter_1)" clip-path="url(#clip_1)">
<g id="Group" transform="translate(214.02026 338.67798)">
<path d="M297.822 476.322C252.091 476.322 206.361 458.882 171.467 424.006L52.3385 304.925C-17.4462 235.169 -17.4462 122.074 52.3385 52.3178C119.763 -15.0793 227.66 -17.3598 297.822 45.4785C367.986 -17.3576 475.883 -15.0771 543.306 52.3178C613.09 122.074 613.09 235.169 543.306 304.925L424.177 424.006C389.286 458.882 343.554 476.322 297.822 476.322" id="Path" fill="#B776FC" stroke="none" />
<path d="M248.68 206.983C271.167 243.662 324.478 243.662 346.964 206.983" id="Path" fill="none" stroke="#22184C" stroke-width="22" stroke-linecap="round" />
<path d="M325.877 150.757C325.877 139.083 335.344 129.62 347.022 129.62C358.7 129.62 368.167 139.083 368.167 150.757C368.167 162.43 358.7 171.893 347.022 171.893C335.344 171.893 325.877 162.43 325.877 150.757Z" id="Oval" fill="#22184C" fill-rule="evenodd" stroke="none" />
<path d="M227.357 150.757C227.357 139.083 236.824 129.62 248.502 129.62C260.181 129.62 269.648 139.083 269.648 150.757C269.648 162.43 260.181 171.893 248.502 171.893C236.824 171.893 227.357 162.43 227.357 150.757Z" id="Oval" fill="#22184C" fill-rule="evenodd" stroke="none" />
<path d="M72.7875 192.911C72.7875 153.977 104.362 122.416 143.312 122.416C182.261 122.416 213.836 153.977 213.836 192.911C213.836 231.845 182.261 263.407 143.312 263.407C104.362 263.407 72.7875 231.845 72.7875 192.911Z" id="Oval" fill="#8C53E7" fill-rule="evenodd" stroke="none" />
<path d="M381.808 192.911C381.808 153.977 413.382 122.416 452.332 122.416C491.281 122.416 522.856 153.977 522.856 192.911C522.856 231.845 491.281 263.407 452.332 263.407C413.382 263.407 381.808 231.845 381.808 192.911Z" id="Oval" fill="#8C53E7" fill-rule="evenodd" stroke="none" />
</g>
</g>
</g>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -1,36 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient gradientUnits="objectBoundingBox" x1="0.5" y1="0" x2="0.5" y2="1" id="gradient_1">
<stop offset="0%" stop-color="#F8F081" />
<stop offset="100%" stop-color="#E1DA51" />
</linearGradient>
<filter filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB" id="filter_1">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 0" />
<feOffset dx="0" dy="40" />
<feGaussianBlur stdDeviation="20" />
<feOffset dx="0" dy="27" />
<feGaussianBlur stdDeviation="13.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2509804 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect0_dropShadow" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 0" />
<feOffset dx="0" dy="15" />
<feGaussianBlur stdDeviation="15" />
<feOffset dx="0" dy="10" />
<feGaussianBlur stdDeviation="10.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.29803923 0" />
<feBlend mode="normal" in2="effect0_dropShadow" result="effect1_dropShadow" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape" />
</filter>
</defs>
<g id="neighbourhood-purple">
<path d="M1024 2056L1024 2056L1024 3080L0 3080L0 2056L1024 2056Z" id="neighbourhood-purple" fill="url(#gradient_1)" stroke="none" />
<path d="M864.362 79.8798L864.362 134.95M662.099 55.0981L975.99 200.559M343.467 201.034L657.357 55.5735M276.97 55.0979L465.304 142.375M47 159.732L273.001 55" id="Shape" fill="none" fill-rule="evenodd" stroke="#8C53E7" stroke-width="44" stroke-linecap="round" stroke-linejoin="round" filter="url(#filter_1)" />
<g filter="url(#filter_1)">
<g id="Group" transform="translate(72.13778 248.08119)">
<path d="M439.862 703.781C372.32 703.781 304.78 678.013 253.244 626.482L77.3003 450.537C-25.7668 347.47 -25.7668 180.368 77.3003 77.3012C176.882 -22.2802 336.238 -25.6497 439.862 67.1961C543.49 -25.6464 702.847 -22.277 802.425 77.3012C905.492 180.368 905.492 347.47 802.425 450.537L626.48 626.482C574.949 678.013 507.406 703.781 439.862 703.781" id="Path" fill="#B776FC" stroke="none" />
<path d="M367.283 305.824C400.495 360.018 479.232 360.018 512.441 305.824" id="Path" fill="none" stroke="#22184C" stroke-width="33" stroke-linecap="round" />
<path d="M481.297 222.748C481.297 205.5 495.28 191.518 512.527 191.518C529.776 191.518 543.758 205.5 543.758 222.748C543.758 239.996 529.776 253.978 512.527 253.978C495.28 253.978 481.297 239.996 481.297 222.748Z" id="Oval" fill="#22184C" fill-rule="evenodd" stroke="none" />
<path d="M335.791 222.748C335.791 205.5 349.773 191.518 367.021 191.518C384.269 191.518 398.251 205.5 398.251 222.748C398.251 239.996 384.269 253.978 367.021 253.978C349.773 253.978 335.791 239.996 335.791 222.748Z" id="Oval" fill="#22184C" fill-rule="evenodd" stroke="none" />
<path d="M107.502 285.032C107.502 227.507 154.136 180.873 211.662 180.873C269.187 180.873 315.821 227.507 315.821 285.032C315.821 342.558 269.187 389.192 211.662 389.192C154.136 389.192 107.502 342.558 107.502 285.032Z" id="Oval" fill="#8C53E7" fill-rule="evenodd" stroke="none" />
<path d="M563.903 285.032C563.903 227.507 610.537 180.873 668.063 180.873C725.589 180.873 772.222 227.507 772.222 285.032C772.222 342.558 725.589 389.192 668.063 389.192C610.537 389.192 563.903 342.558 563.903 285.032Z" id="Oval" fill="#8C53E7" fill-rule="evenodd" stroke="none" />
<path d="M750.419 224.839L750.419 262.111M613.471 208.066L826 306.515M397.732 306.837L610.26 208.388M352.708 208.066L480.225 267.135M197 278.883L350.021 208" id="Shape" fill="none" fill-rule="evenodd" stroke="#8C53E7" stroke-width="29" stroke-linecap="round" stroke-linejoin="round" filter="url(#filter_1)" clip-path="url(#clip_1)" />
<g filter="url(#filter_1)" clip-path="url(#clip_1)">
<g id="Group" transform="translate(214.02026 338.67798)">
<path d="M297.822 476.322C252.091 476.322 206.361 458.882 171.467 424.006L52.3385 304.925C-17.4462 235.169 -17.4462 122.074 52.3385 52.3178C119.763 -15.0793 227.66 -17.3598 297.822 45.4785C367.986 -17.3576 475.883 -15.0771 543.306 52.3178C613.09 122.074 613.09 235.169 543.306 304.925L424.177 424.006C389.286 458.882 343.554 476.322 297.822 476.322" id="Path" fill="#B776FC" stroke="none" />
<path d="M248.68 206.983C271.167 243.662 324.478 243.662 346.964 206.983" id="Path" fill="none" stroke="#22184C" stroke-width="22" stroke-linecap="round" />
<path d="M325.877 150.757C325.877 139.083 335.344 129.62 347.022 129.62C358.7 129.62 368.167 139.083 368.167 150.757C368.167 162.43 358.7 171.893 347.022 171.893C335.344 171.893 325.877 162.43 325.877 150.757Z" id="Oval" fill="#22184C" fill-rule="evenodd" stroke="none" />
<path d="M227.357 150.757C227.357 139.083 236.824 129.62 248.502 129.62C260.181 129.62 269.648 139.083 269.648 150.757C269.648 162.43 260.181 171.893 248.502 171.893C236.824 171.893 227.357 162.43 227.357 150.757Z" id="Oval" fill="#22184C" fill-rule="evenodd" stroke="none" />
<path d="M72.7875 192.911C72.7875 153.977 104.362 122.416 143.312 122.416C182.261 122.416 213.836 153.977 213.836 192.911C213.836 231.845 182.261 263.407 143.312 263.407C104.362 263.407 72.7875 231.845 72.7875 192.911Z" id="Oval" fill="#8C53E7" fill-rule="evenodd" stroke="none" />
<path d="M381.808 192.911C381.808 153.977 413.382 122.416 452.332 122.416C491.281 122.416 522.856 153.977 522.856 192.911C522.856 231.845 491.281 263.407 452.332 263.407C413.382 263.407 381.808 231.845 381.808 192.911Z" id="Oval" fill="#8C53E7" fill-rule="evenodd" stroke="none" />
</g>
</g>
</g>

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,8 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<?xml version="1.0" encoding="utf-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB" id="filter_1">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 0" />
<feOffset dx="0" dy="27" />
<feGaussianBlur stdDeviation="13.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2509804 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect0_dropShadow" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 128 0" />
<feOffset dx="0" dy="10" />
<feGaussianBlur stdDeviation="10.5" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.29803923 0" />
<feBlend mode="normal" in2="effect0_dropShadow" result="effect1_dropShadow" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape" />
</filter>
</defs>
<g id="neighbourhood-purple">
<path d="M750.419 224.839L750.419 262.111M613.471 208.066L826 306.515M397.732 306.837L610.26 208.388M352.708 208.066L480.225 267.135M197 278.883L350.021 208" id="Shape" fill="none" fill-rule="evenodd" stroke="#8C53E7" stroke-width="29" stroke-linecap="round" stroke-linejoin="round" filter="url(#filter_1)" clip-path="url(#clip_1)" />
<g filter="url(#filter_1)" clip-path="url(#clip_1)">
<g id="Group" transform="translate(214.02026 338.67798)">
<path d="M297.822 476.322C252.091 476.322 206.361 458.882 171.467 424.006L52.3385 304.925C-17.4462 235.169 -17.4462 122.074 52.3385 52.3178C119.763 -15.0793 227.66 -17.3598 297.822 45.4785C367.986 -17.3576 475.883 -15.0771 543.306 52.3178C613.09 122.074 613.09 235.169 543.306 304.925L424.177 424.006C389.286 458.882 343.554 476.322 297.822 476.322" id="Path" fill="#B776FC" stroke="none" />
<path d="M248.68 206.983C271.167 243.662 324.478 243.662 346.964 206.983" id="Path" fill="none" stroke="#22184C" stroke-width="22" stroke-linecap="round" />
<path d="M325.877 150.757C325.877 139.083 335.344 129.62 347.022 129.62C358.7 129.62 368.167 139.083 368.167 150.757C368.167 162.43 358.7 171.893 347.022 171.893C335.344 171.893 325.877 162.43 325.877 150.757Z" id="Oval" fill="#22184C" fill-rule="evenodd" stroke="none" />
<path d="M227.357 150.757C227.357 139.083 236.824 129.62 248.502 129.62C260.181 129.62 269.648 139.083 269.648 150.757C269.648 162.43 260.181 171.893 248.502 171.893C236.824 171.893 227.357 162.43 227.357 150.757Z" id="Oval" fill="#22184C" fill-rule="evenodd" stroke="none" />
<path d="M72.7875 192.911C72.7875 153.977 104.362 122.416 143.312 122.416C182.261 122.416 213.836 153.977 213.836 192.911C213.836 231.845 182.261 263.407 143.312 263.407C104.362 263.407 72.7875 231.845 72.7875 192.911Z" id="Oval" fill="#8C53E7" fill-rule="evenodd" stroke="none" />
<path d="M381.808 192.911C381.808 153.977 413.382 122.416 452.332 122.416C491.281 122.416 522.856 153.977 522.856 192.911C522.856 231.845 491.281 263.407 452.332 263.407C413.382 263.407 381.808 231.845 381.808 192.911Z" id="Oval" fill="#8C53E7" fill-rule="evenodd" stroke="none" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -21,14 +21,18 @@ namespace Neighbourhood.omg.lol {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = true
};
addToken(token);
AddToken(token);
}
private void addToken(string? token = null) {
public void AddToken(string? token = null) {
if (token == null) token = Task.Run(() => SecureStorage.GetAsync("accounttoken")).GetAwaiter().GetResult();
if (token != null) _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
public void RemoveToken() {
_client.DefaultRequestHeaders.Remove("Authorization");
}
private async Task<T?> Get<T>(string uri, CancellationToken cancellationToken = default) where T : IOmgLolResponseData {
T? responseData = default(T);
try {
@ -94,6 +98,25 @@ namespace Neighbourhood.omg.lol {
return responseData;
}
private async Task<TResponse?> Patch<TResponse, TData>(string uri, TData data, CancellationToken cancellationToken = default) where TResponse : IOmgLolResponseData {
TResponse? responseData = default(TResponse);
try {
HttpResponseMessage response = await _client.PatchAsJsonAsync(uri, data, _serializerOptions, cancellationToken: cancellationToken);
string str = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode) {
OmgLolResponse<TResponse>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<TResponse>>(_serializerOptions, cancellationToken: cancellationToken);
if (responseObj != null && responseObj.Request.Success) {
responseData = responseObj.Response;
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return responseData;
}
public async Task<List<Status>> StatuslogLatest() =>
(await Get<StatusResponseData>("/statuslog/latest"))?.Statuses ?? new List<Status>();
@ -102,7 +125,7 @@ namespace Neighbourhood.omg.lol {
public async Task<MarkupString> StatuslogBio(string address) {
StatusBioResponseData? responseData = await Get<StatusBioResponseData>($"/address/{address}/statuses/bio");
return (MarkupString)Markdown.ToHtml(responseData?.Bio ?? "");
return Utilities.MdToHtmlMarkup(responseData?.Bio ?? "");
}
public async Task<AccountResponseData?> AccountInfo() =>
@ -148,6 +171,10 @@ namespace Neighbourhood.omg.lol {
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<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<List<NowData>?> NowGarden() =>
(await Get<NowResponseData>($"/now/garden"))?.Garden ?? new List<NowData>();

View file

@ -1,5 +1,5 @@
@import url(../vendor/color.css);
@import url(../vendor/beer.min.css);
@import url(../vendor/beer.min.css);
@import url(../vendor/color.css);
@import url(../vendor/type.css);
@import url(../vendor/fluent-emoji/SegoeUIEmoji.css);
/*@import url(../vendor/fluent-emoji/3d.css);*/
@ -13,11 +13,35 @@
--spacing: 1.5rem;
--radius: .75em;
--small-radius: .13em;
/* --color: var(--white);
--color: var(--gray-1);
--background: var(--gray-8);
--shadow: var(--black);
--button-shadow: var(--gray-7);*/
--button-shadow: var(--gray-7);
}
body.dark {
--background: var(--gray-8);
--on-background: var(--gray-1);
--surface: var(--gray-9);
--on-surface: var(--gray-4);
/* --surface-variant: #49454e;
--on-surface-variant: #cac4cf;
--outline: #948f99;
--outline-variant: #49454e;
--shadow: #000000;
--scrim: #000000;
--inverse-surface: #e6e1e6;
--inverse-on-surface: #313033;
--inverse-primary: #6750a4;
--surface-dim: #141316;
--surface-bright: #3a383c;
--surface-container-lowest: #0f0e11;
--surface-container-low: #1c1b1e;
--surface-container: #201f22;
--surface-container-high: #2b292d;
--surface-container-highest: #363438;*/
}
html, body {
font-family: var(--font);
}
@ -43,7 +67,7 @@ p, li {
line-height: 160%;
}
.author, .address {
.author, .address, .page-heading {
font-family: 'VC Honey Deck', var(--font);
}
.author {
@ -58,15 +82,37 @@ img {
margin: 0 auto;
}
.status .emoji, #status-emoji {
.status .emoji {
margin-bottom: auto;
inline-size: 5.5rem;
block-size: 5.5rem;
font-size: 5rem;
line-height: 5.5rem;
text-indent: -10px;
}
.status .emoji, #status-emoji {
margin-bottom: auto;
inline-size: 3.5rem;
block-size: 3.5rem;
font-size: 3.1rem;
line-height: 3.5rem;
text-indent: -6px;
}
:is(h1, h2, h3, h4, h5, h6) :is(i.fa-at, i:only-child){
margin-right: 0;
inline-size: auto;
}
h1 i:only-child {
margin-block-start: 1rem;
}
.chip i {
border-radius: 0;
}
.status nav .chip, .status nav {
color: var(--gray-7);
}
@ -253,7 +299,7 @@ article.ephemeral {
flex-shrink: 1;
}
.page-container > .page {
.page-container > .page.active {
display:flex;
flex-direction:column;
flex-grow: 1;
@ -267,6 +313,13 @@ article.ephemeral {
}
.now {
background-color: var(--green-2);
color: var(--black);
}
main, .page-container {
flex-grow:1;
display: flex;
@ -274,7 +327,9 @@ main, .page-container {
}
main {
overflow: hidden;
overflow-x: hidden;
overflow-y: auto;
}
.hover {
@ -288,3 +343,56 @@ main {
#info :is(p, ul, ol) {
margin-bottom: var(--spacing);
}
.fa-seedling { color: var(--green-9) !important; }
.fa-message-smile { color: var(--blue-4) !important; }
.fa-images { color: var(--yellow-6) !important; }
.fa-id-card { color: var(--pink-4) !important; }
.fa-comment-dots { color: var(--gray-6) !important; }
nav.bottom.s :is(small, .label) {
font-size: 0.75rem;
}
i.tiny {
---size: 1em;
}
.row p {
white-space: normal;
max-width: 100%;
}
a.row.indent {
margin-left: 1rem;
border-left: 1px solid var(--outline);
}
:is(.circle,.square).large.small:not(i,img,video,svg) {
block-size: 2rem;
inline-size: 2rem;
}
:is(button,.button,.chip).large.small > .responsive {
inline-size: 2rem;
}
/* s */
@media only screen and (max-width: 600px) {
}
/* m */
@media only screen and (min-width: 601px) and (max-width: 992px) {
}
/* l */
@media only screen and (min-width: 993px) {
:is(.circle,.square).large.small:not(i,img,video,svg) {
block-size: 3rem;
inline-size: 3rem;
}
:is(button,.button,.chip).large.small > .responsive {
inline-size: 3rem;
}
}

View file

@ -1,13 +1,13 @@
/* omg.lol Icon Type */
@import url('/css/lol/icons/omg.lol-glyphs.css');
@import url('/vendor/type/icons/omg.lol-glyphs.css');
@font-face {
font-family: 'omg.lol Icons';
font-style: normal;
font-weight: 900;
font-display: block;
src: url('/css/lol/icons/omg.lol-icons.woff2') format('woff2');
src: url('/vendor/type/icons/omg.lol-icons.woff2') format('woff2');
}
.omg-icon {