Compare commits

...

25 commits
0.9.7 ... main

Author SHA1 Message Date
b77aac8c57 decided against adding api_key and settings to AccountResponseData (for now) 2025-02-11 09:49:24 +11:00
518a42b20a Setup imdone for task management 2025-02-10 20:52:51 +11:00
023c077b2b Minor dependency update to resolve vulnerability 2025-02-10 20:42:40 +11:00
4912ede42c minor styling fixes 2025-02-10 16:36:26 +11:00
cf3931f529 Fixed an issue with editing pastes getting the dialog stuck 2025-02-10 16:35:22 +11:00
d142010786 Bumped version number 2024-07-25 16:37:11 +10:00
8d3a454a6b changed the add paste icon 2024-07-25 16:36:56 +10:00
b089cb39e6 colour tweaks 2024-07-25 16:09:37 +10:00
72c6b590f1 Light theme! 2024-07-25 15:54:06 +10:00
b074b9d453 Added a "toast" message when copying a paste 2024-07-25 13:59:57 +10:00
1fe82c18af Added Pastes into the feed 2024-07-25 13:39:36 +10:00
04a5641650 fix for weird border on icon on startup 2024-07-25 13:28:17 +10:00
a25df4368e Fix for retrieving pastes while logged in 2024-07-25 13:27:48 +10:00
d14ee65af4 bumped version number 2024-07-23 17:20:39 +10:00
a02b14782b Added in pastes (from people)
might still need some work/testing
Also, should I add pastes in the feed?
2024-07-23 17:02:53 +10:00
278811c2c2 added icon back in for ios 2024-07-23 13:55:35 +10:00
85bc105ddd Package updates 2024-07-23 12:16:21 +10:00
6e5b206c20 Enable edit statuslog bio 2024-07-23 11:12:52 +10:00
909038ff79 Tidy up of the API service 2024-07-16 12:15:56 +10:00
cc779051bf Renamed RestService -> ApiService 2024-07-16 10:45:33 +10:00
cee1c3dfd2 cleaning up the global javascript 2024-07-16 10:42:43 +10:00
905257789b removed some superfluous styles from app.css 2024-07-16 10:38:57 +10:00
e017269685 More css reorg
Split out some separation of concerns:
- color
- font/typography
- icon/emoji related
- z-indexes
- the rest (mostly layout)
2024-07-16 10:35:01 +10:00
859f8348f0 Added character counter and alert for new statuses 2024-07-16 09:47:42 +10:00
1bf7e74a5a (in progress) cleaning up css 2024-07-12 17:08:22 +10:00
66 changed files with 1773 additions and 1004 deletions

View file

@ -0,0 +1,11 @@
#DONE update theme
<!--
order:-10
completed:2025-02-10T17:02:46+11:00
archived:true
archivedAt:2025-02-10T17:02:46+11:00
originalPath:Components\ThemeDialog.razor
originalLine:76
-->

View file

@ -0,0 +1,11 @@
#DONE upload the profile pic
<!--
order:0
completed:2025-02-10T16:51:21+11:00
archived:true
archivedAt:2025-02-10T16:51:21+11:00
originalPath:Components\EditProfilePicDialog.razor
originalLine:60
-->

61
.imdone/DONE/backlog.md Normal file
View file

@ -0,0 +1,61 @@
## Must Haves
- {check} View [latest statuslog entries](https://api.omg.lol/statuslog/latest)
- {check} View [all statuses of a single person](https://api.omg.lol/address/adam/statuses) (get [profile picture](https://profiles.cache.lol/adam/picture) and [statuslog bio](https://api.omg.lol/address/adam/statuses/bio)) Note: I'm calling this the profile page (even though omg.lol profile is a different thing)
- {check} [Log in](https://home.omg.lol/oauth/authorize?client_id=ea14dafd3e92cbcf93750c35cd81a031&scope=everything&redirect_uri=https://neatnik.net/adam/bucket/omgloloauth/&response_type=code) and [Authenticate](https://api.omg.lol/#token-get-oauth-exchange-an-authorization-code-for-an-access-token) (then [get all addresses](https://api.omg.lol/account/application/addresses) so we can pick one for other interactions)
- {check} Post a [new status](https://api.omg.lol/#token-post-statuslog-share-a-new-status) (checkbox for posting to mastodon)
- {check} Log out
- {check} Light/Dark themes (based on system theme)
## Should Haves
- {check} Share statuses, etc.
- {check} Have a character counter on statuses and a warning if going over length for posting to Mastodon.
- {check} Be a share target for creating statuses
- {check} View the [address directory](https://api.omg.lol/directory) (showing profile pics and linking to profile page)
- {check} Link to it via the account menu (There's not a lot of room in the nav)
- {check} View the [now garden](https://api.omg.lol/now/garden) (also, perhaps cache the now garden and link to the now page on a person's profile)
- {check} Updated profile page. Shows:
- {check} [profile picture](https://profiles.cache.lol/adam/picture)
- {check} [statuslog bio](https://api.omg.lol/address/adam/statuses/bio) text
- {check} [all statuses](https://api.omg.lol/address/adam/statuses)
- {check} Link to now page (if present in [now garden](https://api.omg.lol/now/garden))
- {check} Link to profile page (aka web page)
- {check} Link to person's some.pics
- {check} Link to person's pastebin
## Want to Haves
- {check} [Some.pics feed](https://api.omg.lol/pics) (plus seeing the some.pics of individuals, link on profile)
- {check} Be a share target for pictures
- {check} [Ephemeral feed](https://eph.emer.al/)
- {check} plus posting - ~~if/when an API becomes available~~ (Thanks Adam 😁)
- {check} Upload pics
- {check} Edit some.pics
- {check} delete pics
- {check} Edit statuses
- {check} delete statuses
- {check} Update / manage [now page](https://api.omg.lol/#now-page)
- {square} pull to refresh
- {check} Follow people (i.e. locally bookmark their statuslog profile)
- {check} A combined feed of all statuses and pics of everyone you're following
## Nice to Haves
- {check} Update profile picture
- {check} Update / manage statuslog bio
- {check} Update / manage [profile/web page](https://api.omg.lol/#web)
- {check} including [themes](https://api.omg.lol/#theme)
- {check} Update / manage [pastebin](https://api.omg.lol/#pastebin)
- {check} share and copy items
- {check} view as markup
- {check} visible in profile page
- {check} visible in feed
## Current Bugs
- {check} ~~Sharing to app multiple times throws an exception~~
- {check} ~~Need to update "Loading", "Logging in" and "nothing here" pages to match the splash screen (ish)~~
- {check} ~~Empty bio on person/statuses (just remove the div if the bio is empty)~~
- {check} ~~Need warnings on pics with no description~~
- {check} ~~respond appears on statuses with no external link~~
- {check} ~~statuses / pics don't refresh on update/delete~~
- {check} ~~own now page isn't showing properly in profile~~
- {check} ~~statuses with long words or urls won't wrap.~~
- {check} ~~Ephemeral scraping doesn't send a user agent string, so no longer works.~~

12
.imdone/DONE/test.md Normal file
View file

@ -0,0 +1,12 @@
#DONE test
<!--
created:2025-02-10T20:44:27+11:00
order:-20
completed:2025-02-10T20:44:57+11:00
archived:true
archivedAt:2025-02-10T20:44:57+11:00
originalPath:backlog.md
originalLine:64
-->

16
.imdone/actions/board.js Normal file
View file

@ -0,0 +1,16 @@
const path = require('path')
module.exports = function () {
const project = this.project
return [
{
title: "Open in vscode", // This is what displays in the main menu
keys: ['alt+o'], // This is the keyboard shortcut
icon: "code", // This is the font awesome icon that displays in the main menu
action (task) {
const url = `vscode://file/${path.join(project.path, task.path)}:${task.line}`
project.openUrl(url)
}
}
]
}

4
.imdone/actions/card.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = function (task) {
const project = this.project
return []
}

101
.imdone/config.yml Normal file
View file

@ -0,0 +1,101 @@
keepEmptyPriority: false
languages:
.razor:
name: razor
symbol: "//"
block:
start: "@*"
end: "*@"
ignore: "*"
code:
include_lists:
- TODO
- DOING
- DONE
- PLANNING
- FIXME
- ARCHIVE
- HACK
- CHANGED
- XXX
- IDEA
- NOTE
- REVIEW
- WAITING
lists:
- name: NOTE
hidden: false
id: 9886o1muwm6yiizyq
- name: Past Due Reminders
hidden: true
ignore: false
filter: 'remind = /./ and remind < "${now}" and list != DONE -remind'
id: 9886o1muwm6yiizyr
- name: What's Due?
hidden: true
ignore: false
filter: 'dueDate < "${in 15 days}" AND list != DONE +dueDate +order'
id: 9886o1muwm6yiizys
- name: WAITING
hidden: false
ignore: false
id: 9886o10uwm6yovnxl
- name: TODO
hidden: false
id: 9886o1muwm6yiizyt
- name: DOING
hidden: false
id: 9886o1muwm6yiizyu
- name: DONE
hidden: false
ignore: true
id: 9886o1muwm6yiizyv
- name: Recently Completed
filter: 'completedDate > "${14 days ago}" -completed'
hidden: false
id: 9886o1muwm6yiizyw
settings:
'0': object Object
openIn: default
openCodeIn: default
journalType: Single File
journalPath: null
appendNewCardsTo: backlog.md
newCardSyntax: MARKDOWN
replaceSpacesWith: '-'
plugins: {}
journalTemplate: null
markdownOnly: false
kudosProbability: 0.33
views: []
name: Neighbourhood.omg.lol
cards:
colors:
- color: red
filter: tags = "BUG"
- color: black
filter: tags = "Someday"
- color: green
filter: tags = "WantToHave"
template: |
<!--
created:${timestamp}
-->
trackChanges: false
metaNewLine: true
addCompletedMeta: true
addCheckBoxTasks: false
doneList: DONE
tokenPrefix: '#'
taskPrefix: ''
tagPrefix: '#'
metaSep: ':'
orderMeta: true
maxLines: 6
addNewCardsToTop: true
showTagsAndMeta: false
defaultList: TODO
computed: !<tag:yaml.org,2002:js/undefined> ''
archiveCompleted: true
archiveFolder: .imdone/DONE

124
.imdone/properties/card.js Normal file
View file

@ -0,0 +1,124 @@
let updatedAt = new Date()
module.exports = function ({ line, source, totals }) {
const project = this.project
const emoji = {
due: dueEmoji(totals),
recent: recentEmoji(totals),
wip: wipEmoji(totals),
chart: EMOJI.CHART
}
// These are the properties that are available to use in your cards
// Use ${property_name} to permanently insert the value of the property
// Use {{property_name}} to insert the value of the property at runtime
return {
date: `${new Date().toISOString().substring(0, 10)}`,
sourceLink: `[${source.path}:${line}](${source.path}:${line})`,
cardTotal: cardTotal(totals),
allTopics: project.allTopics, // This is an array of all the topics in the project
topicTable: getTopicTable(project), // This is a markdown table with the count of tasks for each topic/list intersection
emoji,
icons
}
}
const icons = {
filter: `<span class="icon is-small fa-xs"><svg aria-hidden="true" focusable="false" data-prefix="fa" data-icon="search" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-search fa-w-16"><path fill="currentColor" d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z" class=""></path></svg></span><span data-v-fd981bec="" class="icon is-small fa-xs"><svg aria-hidden="true" focusable="false" data-prefix="fa" data-icon="chevron-down" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="svg-inline--fa fa-chevron-down fa-w-14"><path fill="currentColor" d="M207.029 381.476L12.686 187.132c-9.373-9.373-9.373-24.569 0-33.941l22.667-22.667c9.357-9.357 24.522-9.375 33.901-.04L224 284.505l154.745-154.021c9.379-9.335 24.544-9.317 33.901.04l22.667 22.667c9.373 9.373 9.373 24.569 0 33.941L240.971 381.476c-9.373 9.372-24.569 9.372-33.942 0z" class=""></path></svg></span>`
,openFile: `<span class="icon is-medium"><svg version="1.1" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" class="octicon octicon-link"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></span>`
,kebab: `<span class="icon is-medium"><svg version="1.1" width="3" height="16" viewBox="0 0 3 16" aria-hidden="true" class="octicon octicon-kebab-vertical"><path data-v-5bf4cb66="" fill-rule="evenodd" d="M0 2.5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0zm0 5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0zM1.5 14a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"></path></svg></span>`
,clone: `<span class="icon copy-button is-medium" style=""><svg aria-hidden="true" focusable="false" data-prefix="fa" data-icon="clone" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-clone fa-w-16 fa-lg"><path fill="currentColor" d="M464 0c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48H176c-26.51 0-48-21.49-48-48V48c0-26.51 21.49-48 48-48h288M176 416c-44.112 0-80-35.888-80-80V128H48c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h288c26.51 0 48-21.49 48-48v-48H176z" class=""></path></svg></span>`
,editCard: `<span class="icon is-medium"><svg version="1.1" width="14" height="16" viewBox="0 0 14 16" aria-hidden="true" class="octicon octicon-pencil"><path fill-rule="evenodd" d="M0 12v3h3l8-8-3-3-8 8zm3 2H1v-2h1v1h1v1zm10.3-9.3L12 6 9 3l1.3-1.3a.996.996 0 0 1 1.41 0l1.59 1.59c.39.39.39 1.02 0 1.41z"></path></svg></span>`
}
const EMOJI = {
BAD: ':rotating_light:',
GREAT: ':rocket:',
SLEEP: ':sleeping:',
GOOD: ':2nd_place_medal:',
CHART: '<span style="font-size: 1.5em;">:chart:</span>'
}
function formatEmoji(emoji) {
return `<span style="font-size: 1.5em;">${emoji}</span>`
}
function dueEmoji(totals) {
const due = totals["What's Due?"]
let emoji = EMOJI.GOOD
if (due >= 3) {
emoji = EMOJI.BAD
} else if (due === 0) {
emoji = EMOJI.GREAT
}
return formatEmoji(emoji)
}
function recentEmoji(totals) {
const recentlyCompleted = totals['Recently Completed']
let emoji = EMOJI.GOOD
if (recentlyCompleted >= 3) {
emoji = EMOJI.GREAT
} else if (recentlyCompleted === 0) {
emoji = EMOJI.BAD
}
return formatEmoji(emoji)
}
function wipEmoji(totals) {
const doing = totals['DOING']
let emoji = EMOJI.GOOD
if (doing >= 3) {
emoji = EMOJI.BAD
} else if (doing === 0) {
emoji = EMOJI.SLEEP
} else if (doing === 1) {
emoji = EMOJI.GREAT
}
return formatEmoji(emoji)
}
function cardTotal(totals) {
let count = 0
Object.keys(totals).forEach((list) => {
count += totals[list]
})
return count
}
function getTopicTable(project) {
console.log('project.updatedAt', project.updatedAt)
console.log('updatedAt', updatedAt)
if (project.updatedAt < updatedAt) return ''
updatedAt = project.updatedAt
const lists = project.allLists.filter(list => !list.filter)
const topicTable = project.allTopics.map((topic) => {
return {
name: topic,
lists: [
...lists.map((list) => {
return {
name: list.name,
count: list.tasks.filter((task) => task.topics.includes(topic)).length
}
})
]
}
});
//convert topic table into a markdown table with topic name on the left and list names on the top and the count for each topic/list intersection
const table = `
| Topic | ${lists.map((list) => list.name).join(' | ')} |
| --- | ${lists.map(() => ' --- ').join(' | ')} |
${topicTable.map((topic) => {
const topicLink = `imdone://${project.path}?filter=topics="${encodeURIComponent(topic.name)}"`;
return `| [[${topic.name}]] | ${topic.lists.map((list) => `[${list.count}](${topicLink})`).join(' | ')} |`;
}).join('\n')}
`;
console.log(table);
return table
}

0
.imdone/style.css Normal file
View file

4
.imdone/tags.yml Normal file
View file

@ -0,0 +1,4 @@
tags:
- BUG
- Someday
- WantToHave

8
.imdoneignore Normal file
View file

@ -0,0 +1,8 @@
.vs
bin
obj
*.user
.imdone
Resources
.git
.vscode

310
Classes/ApiService.cs Normal file
View 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;
}
}
}

View file

@ -3,5 +3,6 @@
namespace Neighbourhood.omg.lol { namespace Neighbourhood.omg.lol {
public class NavigatorService { public class NavigatorService {
internal NavigationManager? NavigationManager { get; set; } internal NavigationManager? NavigationManager { get; set; }
internal Page? Page { get; set; }
} }
} }

View file

@ -1,362 +0,0 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Neighbourhood.omg.lol.Models;
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace Neighbourhood.omg.lol
{
public class RestService {
HttpClient _client;
JsonSerializerOptions _serializerOptions;
public const string BaseUrl = "https://api.omg.lol";
public RestService(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,
WriteIndented = true
};
AddToken(token);
}
public T? Deserialize<T>(string str) {
T? responseObj = default(T);
try {
responseObj = JsonSerializer.Deserialize<T>(str, _serializerOptions);
}
catch (JsonException ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
Debug.WriteLine(str);
}
return responseObj;
}
public void AddToken(string? token = null) {
if (token == null) token = Task.Run(() => SecureStorage.GetAsync("accounttoken")).GetAwaiter().GetResult();
if (token != null) _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
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 {
HttpResponseMessage response = await _client.GetAsync(uri, cancellationToken: cancellationToken);
if (response.IsSuccessStatusCode) {
string str = await response.Content.ReadAsStringAsync();
try {
OmgLolResponse<T>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<T>>(_serializerOptions, cancellationToken: cancellationToken);
if (responseObj?.Request == null || (responseObj?.Request?.Success ?? false)) {
responseData = responseObj!.Response;
}
}
catch (JsonException ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
Debug.WriteLine(str);
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return responseData;
}
private async Task<TResponse?> Post<TResponse, TData>(string uri, TData data, CancellationToken cancellationToken = default) where TResponse : IOmgLolResponseData {
TResponse? responseData = default(TResponse);
try {
HttpResponseMessage response = await _client.PostAsJsonAsync(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?.Request?.Success ?? false) {
responseData = responseObj.Response;
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return responseData;
}
private async Task<TResponse?> PostBinary<TResponse, TData>(string uri, FileResult? fileResult = null, CancellationToken cancellationToken = default) where TResponse : IOmgLolResponseData {
TResponse? responseData = default(TResponse);
try {
if (fileResult != null) using (var fileStream = await fileResult.OpenReadAsync()) {
Uri url = new Uri(_client.BaseAddress?.AbsoluteUri + uri);
if (string.IsNullOrEmpty(url.Query)) uri += "?binary";
else if (!url.Query.Contains("binary")) uri += "&binary";
HttpContent fileStreamContent = new StreamContent(fileStream);
fileStreamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(fileResult.ContentType ?? "application/octet-stream");
fileStreamContent.Headers.ContentLength = fileStream.Length;
HttpResponseMessage response = await _client.PostAsync(uri, fileStreamContent, cancellationToken: cancellationToken);
string str = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode) {
OmgLolResponse<TResponse>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<TResponse>>(_serializerOptions, cancellationToken: cancellationToken);
if (responseObj?.Request?.Success ?? false) {
responseData = responseObj.Response;
}
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return responseData;
}
private async Task<TResponse?> PostMultipart<TResponse, TData>(string uri, TData? data = null, FileResult? fileResult = null, CancellationToken cancellationToken = default)
where TResponse : IOmgLolResponseData where TData : class
{
if(fileResult != null) {
using (var fileStream = await fileResult.OpenReadAsync())
return await PostMultipart<TResponse, TData>(uri, data: data, fileStream: fileStream, fileName: fileResult.FileName, contentType: fileResult.ContentType);
}
else return await PostMultipart<TResponse, TData>(uri, data, fileStream: null);
}
private async Task<TResponse?> PostMultipart<TResponse, TData>(string uri, TData? data = null, Stream? fileStream = null, string? fileName = null, string? contentType = null, CancellationToken cancellationToken = default)
where TResponse : IOmgLolResponseData where TData : class
{
TResponse? responseData = default;
try {
using (MultipartFormDataContent formData = new MultipartFormDataContent()) {
if(fileStream != null) {
HttpContent fileStreamContent = new StreamContent(fileStream);
fileStreamContent.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("form-data") {
Name = "\"file\"",
FileName = $"\"{fileName}\"" ?? "\"unknown\""
};
fileStreamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType ?? "application/octet-stream");
formData.Add(fileStreamContent);
}
if (data != null) {
HttpContent jsonContent = JsonContent.Create(data, options: _serializerOptions);
formData.Add(jsonContent);
}
HttpResponseMessage response = await _client.PostAsync(uri, formData, cancellationToken: cancellationToken);
string str = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode) {
OmgLolResponse<TResponse>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<TResponse>>(_serializerOptions, cancellationToken: cancellationToken);
if (responseObj?.Request?.Success ?? false) {
responseData = responseObj.Response;
}
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return responseData;
}
private async Task<TResponse?> Put<TResponse, TData>(string uri, TData data, CancellationToken cancellationToken = default) where TResponse : IOmgLolResponseData {
TResponse? responseData = default(TResponse);
try {
string json = JsonSerializer.Serialize(data, _serializerOptions);
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, uri);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage response = await _client.SendAsync(request, cancellationToken: cancellationToken);
if (response.IsSuccessStatusCode) {
OmgLolResponse<TResponse>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<TResponse>>(_serializerOptions, cancellationToken: cancellationToken);
if (responseObj?.Request?.Success ?? false) {
responseData = responseObj.Response;
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return responseData;
}
private async Task<TResponse?> 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?.Request?.Success ?? false) {
responseData = responseObj.Response;
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return responseData;
}
private async Task<T?> Delete<T>(string uri, CancellationToken cancellationToken = default) where T : IOmgLolResponseData {
T? responseData = default(T);
try {
HttpResponseMessage response = await _client.DeleteAsync(uri, cancellationToken: cancellationToken);
if (response.IsSuccessStatusCode) {
string str = await response.Content.ReadAsStringAsync();
try {
OmgLolResponse<T>? responseObj = await response.Content.ReadFromJsonAsync<OmgLolResponse<T>>(_serializerOptions, cancellationToken: cancellationToken);
if (responseObj?.Request?.Success ?? false) {
responseData = responseObj.Response;
}
}
catch (JsonException ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
Debug.WriteLine(str);
}
}
}
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>();
public async Task<List<Status>> Statuslog(string address) =>
(await Get<StatusResponseData>($"/address/{address}/statuses"))?.Statuses ?? new List<Status>();
public async Task<MarkupString> StatuslogBio(string address) {
StatusBioResponseData? responseData = await Get<StatusBioResponseData>($"/address/{address}/statuses/bio");
return Utilities.MdToHtmlMarkup(responseData?.Bio ?? "");
}
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, IBrowserFile file) {
byte[] bytes;
using (var memoryStream = new MemoryStream()) {
await file.OpenReadStream().CopyToAsync(memoryStream);
bytes = memoryStream.ToArray();
}
return await PutPic(address, bytes);
}
public async Task<PutPicResponseData?> PutPic(string address, FileResult file) {
byte[] bytes;
using var memoryStream = new MemoryStream();
using var fileStream = await file.OpenReadAsync();
await fileStream.CopyToAsync(memoryStream);
bytes = memoryStream.ToArray();
return await PutPic(address, bytes);
}
public async Task<PutPicResponseData?> PutPic(string address, byte[] bytes) =>
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, object>($"/address/{address}/pfp", fileResult: image);
public async Task<string?> OAuth(string code, string client_id, string client_secret, string redirect_uri) {
string? token = null;
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;
}
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;
}
}
}

View file

@ -24,7 +24,7 @@ namespace Neighbourhood.omg.lol {
public List<MarkupString>? EphemeralMessages { get; set; } public List<MarkupString>? EphemeralMessages { get; set; }
public List<string>? AddressDirectory { get; set; } public List<string>? AddressDirectory { get; set; }
public List<StatusOrPic>? Feed { get; set; } public List<FeedItem>? Feed { get; set; }
public Dictionary<string, Theme>? Themes { get; set; } public Dictionary<string, Theme>? Themes { get; set; }
@ -73,6 +73,7 @@ namespace Neighbourhood.omg.lol {
// data for selected address // data for selected address
public List<Status>? CachedAddressStatuses { get; set; } public List<Status>? CachedAddressStatuses { get; set; }
public List<Pic>? CachedAddressPics { get; set; } public List<Pic>? CachedAddressPics { get; set; }
public List<Paste>? CachedAddressPastes { get; set; }
public MarkupString? CachedAddressBio { get; set; } public MarkupString? CachedAddressBio { get; set; }
private string? _cachedAddress; private string? _cachedAddress;
public string? CachedAddress { public string? CachedAddress {
@ -82,6 +83,7 @@ namespace Neighbourhood.omg.lol {
_cachedAddress = value; _cachedAddress = value;
CachedAddressStatuses = new List<Status>(); CachedAddressStatuses = new List<Status>();
CachedAddressPics = new List<Pic>(); CachedAddressPics = new List<Pic>();
CachedAddressPastes = new List<Paste>();
CachedAddressBio = null; CachedAddressBio = null;
} }
} }
@ -155,9 +157,9 @@ namespace Neighbourhood.omg.lol {
} }
// api service // api service
private RestService api { get; set; } private ApiService api { get; set; }
public State(RestService restService) { public State(ApiService restService) {
api = restService; api = restService;
} }
@ -224,7 +226,7 @@ namespace Neighbourhood.omg.lol {
public async Task<MarkupString?> GetBio(string address, bool forceRefresh = false) { public async Task<MarkupString?> GetBio(string address, bool forceRefresh = false) {
CachedAddress = address; CachedAddress = address;
if (forceRefresh || CachedAddressBio == null) { if (forceRefresh || CachedAddressBio == null) {
CachedAddressBio = await api.StatuslogBio(address); CachedAddressBio = Utilities.MdToHtmlMarkup(await api.StatuslogBio(address));
} }
return CachedAddressBio; return CachedAddressBio;
} }
@ -284,6 +286,19 @@ namespace Neighbourhood.omg.lol {
return CachedAddressPics; 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() { public async Task RefreshStatuses() {
await GetStatuses(forceRefresh: true); await GetStatuses(forceRefresh: true);
if(SelectedAddressName != null) if(SelectedAddressName != null)
@ -296,12 +311,18 @@ namespace Neighbourhood.omg.lol {
} }
public async Task RefreshNow() => await GetNowGarden(forceRefresh: true); public async Task RefreshNow() => await GetNowGarden(forceRefresh: true);
public async Task<IOrderedEnumerable<StatusOrPic>> GetFeed(bool forceRefresh = false) { 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) { if(forceRefresh || Feed == null || Feed.Count == 0) {
Feed = new List<StatusOrPic>(); Feed = new List<FeedItem>();
foreach(string address in Following ?? new List<string>()) { foreach(string address in Following ?? new List<string>()) {
Feed.AddRange((await GetStatuses(address, forceRefresh))?.Select(s => new StatusOrPic { Status = s }) ?? new List<StatusOrPic>()); Feed.AddRange((await GetStatuses(address, forceRefresh))?.Select(s => new FeedItem { Status = s }) ?? new List<FeedItem>());
Feed.AddRange((await GetPics(address, forceRefresh))?.Select(p => new StatusOrPic { Pic = p }) ?? new List<StatusOrPic>()); 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); return Feed.OrderByDescending(s => s.CreatedTime);

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

View file

@ -0,0 +1,131 @@
@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;
loading = false;
await InvokeAsync(StateHasChanged);
}
}

View 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">

View file

@ -1,6 +1,6 @@
@inject IJSRuntime JS @inject IJSRuntime JS
@inject State State @inject State State
@inject RestService api @inject ApiService api
@inject NavigationManager navigationManager @inject NavigationManager navigationManager
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div> <div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
@ -57,11 +57,8 @@
loading = true; loading = true;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
//TODO: upload the profile pic
//PutPicResponseData? response = await api.PutPic(State.SelectedAddressName!, Base64File!);
if (Base64File != null && File != null) if (Base64File != null && File != null)
{ {
// using var fileStream = await File.OpenReadAsync();
BasicResponseData? response = await api.PostProfilePic(Address!, File); BasicResponseData? response = await api.PostProfilePic(Address!, File);
if (response != null) if (response != null)
{ {

View 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">

View file

@ -1,6 +1,6 @@
@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>
} }

View file

@ -34,6 +34,10 @@
<i class="fa-duotone fa-seedling"></i> <i class="fa-duotone fa-seedling"></i>
<span>/Now</span> <span>/Now</span>
</a> </a>
<a class="indent row" href="/person/@State.SelectedAddressName#pastebin">
<i class="fa-solid fa-clipboard"></i>
<span>Pastebin</span>
</a>
} }
} }

View file

@ -1,6 +1,6 @@
@inject IJSRuntime JS @inject IJSRuntime JS
@inject State State @inject State State
@inject RestService api @inject ApiService api
@inject NavigationManager navigationManager @inject NavigationManager navigationManager
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div> <div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>

View file

@ -1,6 +1,6 @@
@inject IJSRuntime JS @inject IJSRuntime JS
@inject State State @inject State State
@inject RestService api @inject ApiService api
@inject NavigationManager navigationManager @inject NavigationManager navigationManager
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div> <div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>

View file

@ -1,7 +1,8 @@
@inject IJSRuntime JS @inject IJSRuntime JS
@inject State State @inject State State
@inject RestService api @inject ApiService api
@inject NavigationManager navigationManager @inject NavigationManager navigationManager
@inject NavigatorService navigatorService
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div> <div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active"> <dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active">
@ -27,7 +28,8 @@
</button> </button>
</div> </div>
<div class="field textarea border max"> <div class="field textarea border max">
<InputTextArea @bind-Value="Content"></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">
@ -66,16 +68,26 @@
public async Task PostStatus() { public async Task PostStatus() {
StatusPost post = new StatusPost StatusPost post = new StatusPost {
{
Emoji = Emoji, Emoji = Emoji,
Content = Content Content = Content
}; };
if (State?.SelectedAddress?.Preferences?.Statuslog?.MastodonPosting ?? false){ if (State?.SelectedAddress?.Preferences?.Statuslog?.MastodonPosting ?? false) {
post.SkipMastodonPost = !postToMastodon; post.SkipMastodonPost = !postToMastodon;
} }
if(Content.Length >= 500) {
bool answer = await navigatorService.Page!.DisplayAlert(
"Character limit reached",
"Your message is over 500 characters, which is a lot for a status.\n"
+ ((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; loading = true;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
var result = await api.StatusPost(State!.SelectedAddressName!, post); var result = await api.StatusPost(State!.SelectedAddressName!, post);

View file

@ -35,7 +35,7 @@
catch (Exception) { } catch (Exception) { }
} }
<li> <li>
<a class="chip medium no-border no-margin" href="/person/@address"> <a class="chip medium no-border tiny-margin transparent" href="/person/@address">
<img class="circle avatar responsive" src="https://profiles.cache.lol/@linkAddress/picture"> <img class="circle avatar responsive" src="https://profiles.cache.lol/@linkAddress/picture">
<span>@displayAddress</span> <span>@displayAddress</span>
</a> </a>

View file

@ -1,6 +1,6 @@
@page "/editNow" @page "/editNow"
@inject NavigationManager Nav @inject NavigationManager Nav
@inject RestService api @inject ApiService api
@inject State State @inject State State
@inject IJSRuntime JS @inject IJSRuntime JS
@ -11,7 +11,9 @@
@bind-Value="@markdownValue" @bind-Value="@markdownValue"
Theme="material-darker" Theme="material-darker"
MaxHeight="100%" MaxHeight="100%"
CustomButtonClicked="@OnCustomButtonClicked"> CustomButtonClicked="@OnCustomButtonClicked"
AutoDownloadFontAwesome="false"
>
<Toolbar> <Toolbar>
<MarkdownToolbarButton Action="MarkdownAction.Bold" Icon="fa-solid fa-bold" Title="Bold" /> <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.Italic" Icon="fa-solid fa-italic" Title="Italic" />

View file

@ -1,6 +1,6 @@
@page "/editProfile" @page "/editProfile"
@inject NavigationManager Nav @inject NavigationManager Nav
@inject RestService api @inject ApiService api
@inject State State @inject State State
@inject IJSRuntime JS @inject IJSRuntime JS
@ -11,7 +11,9 @@
@bind-Value="@markdownValue" @bind-Value="@markdownValue"
Theme="material-darker" Theme="material-darker"
MaxHeight="100%" MaxHeight="100%"
CustomButtonClicked="@OnCustomButtonClicked"> CustomButtonClicked="@OnCustomButtonClicked"
AutoDownloadFontAwesome="false"
>
<Toolbar> <Toolbar>
<MarkdownToolbarButton Action="MarkdownAction.Bold" Icon="fa-solid fa-bold" Title="Bold" /> <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.Italic" Icon="fa-solid fa-italic" Title="Italic" />

View file

@ -35,13 +35,16 @@ else {
<div class="responsive page-container"> <div class="responsive page-container">
<div id="feed" class="page no-padding active"> <div id="feed" class="page no-padding active">
@if (feed != null){ @if (feed != null){
foreach (StatusOrPic item in feed) { foreach (FeedItem item in feed) {
if (item.IsStatus) { if (item.IsStatus) {
<StatusCard Status="@item.Status"></StatusCard> <StatusCard Status="@item.Status"></StatusCard>
} }
else if (item.IsPic) { else if (item.IsPic) {
<PicCard Pic="@item.Pic"></PicCard> <PicCard Pic="@item.Pic"></PicCard>
} }
else if (item.IsPaste) {
<PasteCard Paste="@item.Paste"></PasteCard>
}
} }
} }
<LoadingCard id="feedLoading" icon="fa-solid fa-list-timeline"></LoadingCard> <LoadingCard id="feedLoading" icon="fa-solid fa-list-timeline"></LoadingCard>
@ -75,7 +78,7 @@ else {
} }
@code { @code {
private IOrderedEnumerable<StatusOrPic>? feed; private IOrderedEnumerable<FeedItem>? feed;
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync(); await base.OnInitializedAsync();

View file

@ -16,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>

View file

@ -51,6 +51,10 @@
<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>
@ -79,6 +83,12 @@
</article> </article>
</div> </div>
} }
@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> <StatusList @ref="StatusList" StatusFunc="@(async(refresh) => await State.GetStatuses(Address, refresh))" Editable="@IsMe"></StatusList>
@if(IsMe) { @if(IsMe) {
<button class="fab circle extra large-elevate" data-ui="#post-modal"> <button class="fab circle extra large-elevate" data-ui="#post-modal">
@ -109,6 +119,15 @@
} }
</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 {
@ -120,6 +139,7 @@
_address = value; _address = value;
if (StatusList != null) StatusList.StatusFunc = async (refresh) => await State.GetStatuses(_address, refresh); if (StatusList != null) StatusList.StatusFunc = async (refresh) => await State.GetStatuses(_address, refresh);
if (PicList != null) PicList.PicsFunc = async (refresh) => await State.GetPics(_address, refresh); 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 {
@ -135,6 +155,7 @@
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 IsMe { private bool IsMe {
get => State.AddressList?.Any(a => a.Address == Address) ?? false; get => State.AddressList?.Any(a => a.Address == Address) ?? false;

View file

@ -0,0 +1,76 @@
@using CommunityToolkit.Maui.Alerts
@inject IJSRuntime JS
<article class="paste">
@* TODO link to paste view
* <!--
* order:-178.75
* -->
*@
<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();
}
}
}

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

View file

@ -22,7 +22,10 @@
private List<Pic>? pics; private List<Pic>? pics;
// 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
// <!--
// order:-145
// -->
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync(); await base.OnInitializedAsync();
if (PicsFunc == null) return; if (PicsFunc == null) return;

View file

@ -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 {
}

View file

@ -1,6 +1,6 @@
@inject IJSRuntime JS @inject IJSRuntime JS
@inject State State @inject State State
@inject RestService api @inject ApiService api
@inject NavigationManager navigationManager @inject NavigationManager navigationManager
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div> <div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
@ -73,7 +73,6 @@
} }
public async Task UseTheme() { public async Task UseTheme() {
// todo: update theme
onthemechanged?.Invoke(activeTheme); onthemechanged?.Invoke(activeTheme);
activeTheme = null; activeTheme = null;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);

View file

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components.Authorization; using CommunityToolkit.Maui;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -7,12 +8,15 @@ namespace Neighbourhood.omg.lol {
public static MauiApp CreateMauiApp() { public static MauiApp CreateMauiApp() {
var builder = MauiApp.CreateBuilder(); var builder = MauiApp.CreateBuilder();
builder builder
.UseMauiApp<App>(); .UseMauiApp<App>()
.UseMauiCommunityToolkit(options => {
options.SetShouldEnableSnackbarOnWindows(true);
});
builder.Services.AddMauiBlazorWebView(); builder.Services.AddMauiBlazorWebView();
builder.Services.AddTransient<LoginWebViewPage>(); builder.Services.AddTransient<LoginWebViewPage>();
builder.Services.AddSingleton<RestService>(); builder.Services.AddSingleton<ApiService>();
builder.Services.AddSingleton<State>(); builder.Services.AddSingleton<State>();
builder.Services.AddSingleton<NavigatorService>(); builder.Services.AddSingleton<NavigatorService>();

View file

@ -4,6 +4,5 @@
public string Email { get; set; } = string.Empty; public string Email { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public TimeData Created { get; set; } = TimeData.Empty; public TimeData Created { get; set; } = TimeData.Empty;
//TODO: api_key and settings
} }
} }

View file

@ -1,4 +1,5 @@
namespace Neighbourhood.omg.lol.Models { namespace Neighbourhood.omg.lol.Models {
public class AddressResponseList : List<AddressResponseData>, IOmgLolResponseList<AddressResponseData> { public class AddressResponseList : List<AddressResponseData>, IOmgLolResponseList<AddressResponseData> {
public string Message { get; set; } = string.Empty;
} }
} }

View file

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

View 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) { }
}
}

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

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

View file

@ -0,0 +1,5 @@
namespace Neighbourhood.omg.lol.Models {
public class PostStatusBio {
public string Content { get; set; } = string.Empty;
}
}

View file

@ -1,6 +1,6 @@
namespace Neighbourhood.omg.lol.Models { namespace Neighbourhood.omg.lol.Models {
public class StatusPostResponseData : IOmgLolResponseData { public class StatusPostResponseData : IOmgLolResponseData {
public string? Message { get; set; } public string Message { get; set; } = string.Empty;
public string? Id { get; set; } public string? Id { get; set; }
public string? Url { get; set; } public string? Url { get; set; }
public string? ExternalUrl { get; set; } public string? ExternalUrl { get; set; }

View file

@ -1,11 +1,13 @@
namespace Neighbourhood.omg.lol.Models { namespace Neighbourhood.omg.lol.Models {
public class StatusOrPic { public class FeedItem {
public Status? Status { get; set; } public Status? Status { get; set; }
public Pic? Pic { get; set; } public Pic? Pic { get; set; }
public Paste? Paste { get; set; }
public bool IsStatus { get => Status != null; } public bool IsStatus { get => Status != null; }
public bool IsPic { get => Pic != null; } public bool IsPic { get => Pic != null; }
public bool IsPaste { get => Paste != null; }
public DateTimeOffset? CreatedTime { get => Status?.CreatedTime ?? Pic?.CreatedTime; } public DateTimeOffset? CreatedTime { get => Status?.CreatedTime ?? Pic?.CreatedTime ?? Paste?.ModifiedTime; }
} }
} }

29
Models/Paste.cs Normal file
View 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;
}
}
}
}

View file

@ -13,7 +13,7 @@
public string? SampleProfile { get; set; } = string.Empty; public string? SampleProfile { get; set; } = string.Empty;
public PreviewCssData? PreviewCssData { public PreviewCssData? PreviewCssData {
get => new RestService().Deserialize<PreviewCssData>(this.PreviewCss); get => new ApiService().Deserialize<PreviewCssData>(this.PreviewCss);
} }
} }
} }

View file

@ -43,22 +43,22 @@
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-ios|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-ios|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>7</ApplicationVersion> <ApplicationVersion>9</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-maccatalyst|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-maccatalyst|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>7</ApplicationVersion> <ApplicationVersion>9</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android34.0|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android34.0|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<AndroidKeyStore>True</AndroidKeyStore> <AndroidKeyStore>True</AndroidKeyStore>
<AndroidSigningKeyStore>D:\Neighbourhood.omg.lol\neighbourhood.omg.lol.keystore</AndroidSigningKeyStore> <AndroidSigningKeyStore>D:\_assets\neighbourhood.omg.lol\neighbourhood.omg.lol.keystore</AndroidSigningKeyStore>
<ApplicationVersion>7</ApplicationVersion> <ApplicationVersion>9</ApplicationVersion>
<AndroidSigningStorePass>a!zobzizl</AndroidSigningStorePass> <AndroidSigningStorePass>a!zobzizl</AndroidSigningStorePass>
<AndroidSigningKeyAlias>neighbourhood.omg.lol</AndroidSigningKeyAlias> <AndroidSigningKeyAlias>neighbourhood.omg.lol</AndroidSigningKeyAlias>
<AndroidSigningKeyPass>a!zobzizl</AndroidSigningKeyPass> <AndroidSigningKeyPass>a!zobzizl</AndroidSigningKeyPass>
@ -66,36 +66,37 @@
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.19041.0|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>7</ApplicationVersion> <ApplicationVersion>9</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-ios|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-ios|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>7</ApplicationVersion> <ApplicationVersion>9</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-maccatalyst|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-maccatalyst|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>7</ApplicationVersion> <ApplicationVersion>9</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android34.0|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android34.0|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>7</ApplicationVersion> <ApplicationVersion>9</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.19041.0|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId> <ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.7</ApplicationDisplayVersion> <ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>7</ApplicationVersion> <ApplicationVersion>9</ApplicationVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- App Icon --> <!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\icon.svg" />
<MauiIcon Include="Resources\AppIcon\icon_background.svg" ForegroundFile="Resources\AppIcon\icon_foreground.svg" Color="#f3eb76" BaseSize="1024,1024" /> <MauiIcon Include="Resources\AppIcon\icon_background.svg" ForegroundFile="Resources\AppIcon\icon_foreground.svg" Color="#f3eb76" BaseSize="1024,1024" />
<!-- Splash Screen --> <!-- Splash Screen -->
@ -115,23 +116,29 @@
<Content Remove="appsettings.json" /> <Content Remove="appsettings.json" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="Resources\AppIcon\icon.svg" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="appsettings.json" /> <EmbeddedResource Include="appsettings.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Maui" Version="9.0.2" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Markdig" Version="0.37.0" /> <PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.Maui.Controls" Version="8.0.70" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.70" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="8.0.70" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.Maui.Essentials" Version="8.0.40" /> <PackageReference Include="Microsoft.Maui.Essentials" Version="8.0.70" />
<PackageReference Include="PSC.Blazor.Components.MarkdownEditor" Version="8.0.0" /> <PackageReference Include="PSC.Blazor.Components.MarkdownEditor" Version="8.0.4" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

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

View file

@ -56,11 +56,15 @@ namespace Neighbourhood.omg.lol {
} }
else if (intent.Type.Equals(Intent.ActionSendMultiple)) //Multiple files else if (intent.Type.Equals(Intent.ActionSendMultiple)) //Multiple files
{ {
// TODO: we don't really support this at the moment. //NOTE we don't really support recieving multiple files from a share request at the moment.
//System.Collections.IList? uriList; // <!--
//if (OperatingSystem.IsAndroidVersionAtLeast(33)) // order:0
// -->
// System.Collections.IList? uriList;
// if (OperatingSystem.IsAndroidVersionAtLeast(33))
// uriList = intent.GetParcelableArrayListExtra(Intent.ExtraStream, Java.Lang.Class.FromType(typeof(Android.Net.Uri))); // uriList = intent.GetParcelableArrayListExtra(Intent.ExtraStream, Java.Lang.Class.FromType(typeof(Android.Net.Uri)));
//else uriList = intent.GetParcelableArrayListExtra(Intent.ExtraStream); // else uriList = intent.GetParcelableArrayListExtra(Intent.ExtraStream);
} }
} }
} }

View file

@ -4,7 +4,9 @@
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap"> xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
IgnorableNamespaces="uap rescap com desktop">
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" /> <Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
@ -36,6 +38,26 @@
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" /> <uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" /> <uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements> </uap:VisualElements>
<Extensions>
<!-- Specify which CLSID to activate when notification is clicked -->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="6e919706-2634-4d97-a93c-2213b2acc334" />
</desktop:Extension>
<!-- Register COM CLSID -->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Neighbourhood.omg.lol\Neighbourhood.omg.lol.exe" DisplayName="$targetnametoken$" Arguments="----AppNotificationActivated:">
<!-- Example path to executable: CommunityToolkit.Maui.Sample\CommunityToolkit.Maui.Sample.exe -->
<com:Class Id="6e919706-2634-4d97-a93c-2213b2acc334" />
</com:ExeServer>
</com:ComServer>
</com:Extension>
</Extensions>
</Application> </Application>
</Applications> </Applications>

View file

@ -2,31 +2,35 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UIDeviceFamily</key> <key>UIDeviceFamily</key>
<array> <array>
<integer>1</integer> <integer>1</integer>
<integer>2</integer> <integer>2</integer>
</array> </array>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
<array> <array>
<string>arm64</string> <string>arm64</string>
</array> </array>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string> <string>Assets.xcassets/icon.appiconset</string>
<key>CFBundleShortVersionString</key>
<string>0.9.9</string>
<key>MinimumOSVersion</key>
<string>14.2</string>
</dict> </dict>
</plist> </plist>

View file

@ -0,0 +1,37 @@
<?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 id="grad1" x1="0%" x2="0%" y1="0%" y2="100%">
<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="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">
<rect x="0" y="0" width="100%" height="100%" fill="url(#grad1)"/>
<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>

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -18,7 +18,8 @@ namespace Neighbourhood.omg.lol {
protected override Window CreateWindow(IActivationState? activationState) { protected override Window CreateWindow(IActivationState? activationState) {
// always create new windows. This allows share intents to not crash on android // always create new windows. This allows share intents to not crash on android
// (with the side effect that multiple windows are opened, which is messy but better than a crash) // (with the side effect that multiple windows are opened, which is messy but better than a crash)
return new Window(new AppShell()); NavigatorService.Page = new AppShell();
return new Window(NavigatorService.Page);
} }
} }
} }

View file

@ -10,13 +10,13 @@ public partial class LoginWebViewPage : ContentPage
private AuthenticationStateProvider AuthStateProvider { get; set; } private AuthenticationStateProvider AuthStateProvider { get; set; }
private NavigatorService NavigatorService { get; set; } private NavigatorService NavigatorService { get; set; }
private IConfiguration Configuration { get; set; } private IConfiguration Configuration { get; set; }
private RestService api { get; set; } private ApiService api { get; set; }
private string? client_id; private string? client_id;
private string? client_secret; private string? client_secret;
private string? redirect_uri; private string? redirect_uri;
public LoginWebViewPage(AuthenticationStateProvider authStateProvider, NavigatorService navigatorService, IConfiguration configuration, RestService restService) public LoginWebViewPage(AuthenticationStateProvider authStateProvider, NavigatorService navigatorService, IConfiguration configuration, ApiService restService)
{ {
this.AuthStateProvider = authStateProvider; this.AuthStateProvider = authStateProvider;
this.NavigatorService = navigatorService; this.NavigatorService = navigatorService;

63
backlog.md Normal file
View file

@ -0,0 +1,63 @@
- [Pull to refresh #WantToHave](#TODO:)
<!--
order:-177.5
-->
- [Be a share target for pastes? #WantToHave](#TODO:)
<!--
order:-171.25
-->
- [Update / manage [PURLs](https://api.omg.lol/#purls) #WantToHave](#TODO:)
<!--
order:-172.5
-->
- [Combined status / pics posting (upload pic in new status dialog and paste in link) #WantToHave](#TODO:)
<!--
order:-171.875
-->
- [Update / manage [Weblog](https://api.omg.lol/#weblog) (should probably wait for [Neato](https://neato.pub/)) #WantToHave](#WAITING:)
<!--
order:0
-->
- [Account settings #Someday](#TODO:)
<!--
order:-80
-->
- [Address preferences #Someday](#TODO:)
<!--
order:-90
-->
- [DNS Records #Someday](#TODO:)
<!--
order:-100
-->
- [Switchboard #Someday](#TODO:)
<!--
order:-110
-->
- [Email forwarding #Someday](#TODO:)
<!--
order:-120
-->
- [Keys #Someday](#TODO:)
<!--
order:-130
-->
- [Proofs #Someday](#TODO:)
<!--
order:-140
-->
- [Slow rendering, especially pics, due to large amount of data #BUG](#TODO:)
<!--
order:-150
-->
- [Sharing to app while the sharing page is already open does literally nothing #BUG](#TODO:)
- Not an issue? Only an issue when you have the app and the sharing app open side-by-side, which is rare?
<!--
order:-160
-->
- [Can't select svgs as pics. #BUG](#TODO:)
<!--
order:-170
-->

View file

@ -1,42 +1,4 @@
/* default styles suppload by template */ /* default styles suppload by template */
/*html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}*/
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
#blazor-error-ui { #blazor-error-ui {
background: lightyellow; background: lightyellow;
bottom: 0; bottom: 0;
@ -49,12 +11,12 @@ h1:focus {
z-index: 1000; z-index: 1000;
} }
#blazor-error-ui .dismiss { #blazor-error-ui .dismiss {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
right: 0.75rem; right: 0.75rem;
top: 0.5rem; top: 0.5rem;
} }
.blazor-error-boundary { .blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
@ -62,9 +24,9 @@ h1:focus {
color: white; color: white;
} }
.blazor-error-boundary::after { .blazor-error-boundary::after {
content: "An error has occurred." content: "An error has occurred."
} }
.status-bar-safe-area { .status-bar-safe-area {
display: none; display: none;
@ -76,7 +38,6 @@ h1:focus {
position: sticky; position: sticky;
top: 0; top: 0;
height: env(safe-area-inset-top); height: env(safe-area-inset-top);
background-color: #f7f7f7;
width: 100%; width: 100%;
z-index: 1; z-index: 1;
} }

107
wwwroot/css/color.css Normal file
View file

@ -0,0 +1,107 @@
/* Colours */
:root {
--background: var(--gray-8);
--shadow: var(--black);
}
body.dark {
--background: var(--gray-8);
--on-background: var(--gray-1);
--surface: var(--gray-9);
--on-surface: var(--gray-4);
--on-surface-variant: var(--gray-6);
}
@media (prefers-color-scheme: light) {
body.dark {
--primary: #6750a4;
--on-primary: #ffffff;
--primary-container: #e9ddff;
--on-primary-container: #22005d;
--secondary: #625b71;
--on-secondary: #ffffff;
--secondary-container: var(--gray-4);
--on-secondary-container: #1e192b;
--tertiary: #7e5260;
--on-tertiary: #ffffff;
--tertiary-container: #ffd9e3;
--on-tertiary-container: #31101d;
--error: #ba1a1a;
--on-error: #ffffff;
--error-container: #ffdad6;
--on-error-container: #410002;
--background: var(--gray-0);
--on-background: var(--gray-8);
--surface: var(--gray-2);
--on-surface: var(--gray-9);
--surface-variant: var(--gray-1);
--on-surface-variant: var(--gray-7);
--outline: #7a757f;
--outline-variant: #cac4cf;
--shadow: #000000;
--scrim: #000000;
--inverse-surface: var(--gray-1);
--inverse-on-surface: var(--gray-9);
--inverse-primary: #cfbcff;
--surface-dim: #ddd8dd;
--surface-bright: #fdf8fd;
--surface-container-lowest: #ffffff;
--surface-container-low: var(--gray-3);
--surface-container: var(--gray-3);
--surface-container-high: #ece7eb;
--surface-container-highest: #e6e1e6;
--overlay: rgb(0 0 0 / .5);
--active: rgb(0 0 0 / .1);
--elevate1: 0 .125rem .125rem 0 rgb(0 0 0 / .32);
--elevate2: 0 .25rem .5rem 0 rgb(0 0 0 / .4);
--elevate3: 0 .375rem .75rem 0 rgb(0 0 0 / .48);
--secondary-container: var(--gray-4);
}
}
body, nav:is(.left,.right) {
background-color: var(--background);
}
article {
background-color: var(--surface);
}
.author {
color: inherit
}
.status nav, .status nav .chip {
color: var(--gray-7)
}
.avatar::after {
background-color: var(--surface);
border: 1px dashed var(--on-surface)
}
article.ephemeral {
border: 2px dashed var(--outline)
}
article.now {
background-color: var(--green-2);
color: var(--black)
}
a.row.indent {
border-left: 1px solid var(--outline)
}
.markdown-editor .editor-preview {
background: var(--surface)
}
menu > details > a:is(:hover,:focus,.active), menu > details > summary:is(:hover,:focus,.active) {
background-color: var(--active)
}
#advanced :is(.field.textarea, textarea), .EasyMDEContainer, article.paste code {
background-color: #212121;
color: #eff
}

49
wwwroot/css/icon.css Normal file
View file

@ -0,0 +1,49 @@
/* Icons and emoji */
:root { --emoji-font: TwemojiCountryFlags, SegoeUIEmoji, 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Symbol' }
.animated[data-emoji|=🫥] { content: url(/vendor/fluent-emoji/1fae5/animated.png) }
.fa-seedling { color: var(--green-9) !important }
.fa-message-smile { color: var(--blue-5) !important }
.fa-images { color: var(--orange-5) !important }
.fa-id-card { color: var(--red-5) !important }
.fa-comment-dots { color: var(--gray-6) !important }
.fa-clipboard { color: var(--violet-5) !important }
i.tiny { ---size: 1em }
:is(h1,h2,h3,h4,h5,h6) i {
---size: 1em;
margin-right: .3em;
&.fa-at, &:only-child {
margin-right: 0;
inline-size: auto
}
}
h1 i:only-child { margin-block-start: 1rem }
#new-status-emoji, #status-emoji, :is(.status,.pic) .emoji {
margin-bottom: auto;
inline-size: 3.5rem;
block-size: 3.5rem;
font-size: 3.1rem;
line-height: 3.5rem;
text-indent: -6px
}
:not(#feed) > .pic .emoji { display: none }
.chip i { border-radius: 0 }
i[class*=fa-at] {
vertical-align: unset;
font-size: .75em
}
.editor-toolbar :is(.easymde-dropdown, button) { color: inherit }
.editor-toolbar i.separator {
---size: unset;
opacity: .25;
border-radius: 0
}

View file

@ -2,268 +2,120 @@
@import url(../vendor/color.css); @import url(../vendor/color.css);
@import url(../vendor/type.css); @import url(../vendor/type.css);
@import url(icon.css);
.animated[data-emoji|="🫥"] { content: url(/vendor/fluent-emoji/1fae5/animated.png); } @import url(type.css);
@import url(color.css);
@import url(z.css);
:root { :root {
--emoji-font: TwemojiCountryFlags, SegoeUIEmoji, 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Symbol';
--font: 'Lato', 'Helvetica Neue', Helvetica, Arial, sans-serif, var(--emoji-font);
--prami-svg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path fill="%23FF6BAE" stroke="none" d="M250 450C211.612 450 173.225 435.354 143.934 406.066L43.9346 306.066C-14.6446 247.487 -14.6446 152.513 43.9346 93.9341C100.533 37.3361 191.104 35.421 250 88.1907C308.898 35.4229 399.47 37.3379 456.066 93.9341C514.645 152.513 514.645 247.487 456.066 306.066L356.066 406.066C326.778 435.354 288.389 450 250 450" /><path fill="none" stroke="%23471036" stroke-width="19" stroke-linecap="round" d="M208.749 223.817C227.625 254.619 272.376 254.619 291.251 223.817" /><circle fill="%23471036" cx="291.3" cy="176.6" r="17.75" /><circle fill="%23471036" cx="208.6" cy="176.6" r="17.75" /><circle fill="%23E24097" cx="120.3" cy="212" r="59.2" /><circle fill="%23E24097" cx="379.7" cy="212" r="59.2" /></svg>');
--spacing: 1.5rem; --spacing: 1.5rem;
--radius: .75em;
--small-radius: .13em;
--color: var(--gray-1);
--background: var(--gray-8);
--shadow: var(--black);
--button-shadow: var(--gray-7);
--max-article-size: 75rem; --max-article-size: 75rem;
} }
body.dark { #app {
--background: var(--gray-8); position: relative
--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 { nav:is(.left, .right){
font-family: var(--font); margin-block-start: env(safe-area-inset-top);
} }
body { main {
font-size: 1.2em; position: relative;
overflow-x: hidden;
overflow-y: auto
} }
a { a:not(.author) { text-decoration: underline }
text-decoration: underline; :is(nav, .tabs, #directory) a { text-decoration: none }
}
:is(h1, h2, h3, h4, h5, h6) i {
---size: 1em;
margin-right: 0.3em;
}
:is(nav) a, .author {
text-decoration: none;
}
p, li {
line-height: 160%;
}
.author, .address, .page-heading, .honey {
font-family: 'VC Honey Deck', var(--font);
}
.author {
color: inherit;
font-size: 1.2em;
}
img { img {
max-inline-size: 100%; max-inline-size: 100%;
max-block-size: 100%; max-block-size: 100%;
max-width: 100%; max-width: 100%;
margin: 0 auto; margin: 0 auto
} }
:is(.status,.pic) .emoji { .avatar { position: relative }
margin-bottom: auto;
inline-size: 5.5rem;
block-size: 5.5rem;
font-size: 5rem;
line-height: 5.5rem;
text-indent: -10px;
}
:is(.status,.pic) .emoji, #status-emoji, #new-status-emoji { .avatar::after {
margin-bottom: auto; content: "";
inline-size: 3.5rem; position: absolute;
block-size: 3.5rem; left: 0;
font-size: 3.1rem; right: 0;
line-height: 3.5rem; top: 0;
text-indent: -6px; bottom: 0;
} background-size: contain;
background-repeat: no-repeat;
:not(#feed)>.pic .emoji{ background-position: center bottom;
display:none; border-radius: 50%
}
: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);
} }
.profile.avatar { .profile.avatar {
border-radius: .75rem; border-radius: .75rem;
max-block-size: 10rem; max-block-size: 10rem;
max-inline-size: 10rem; max-inline-size: 10rem
} }
#bio :is(h1, h2, h3, h4, h5, h6) { :is(h1,h2,h3,h4,h5,h6):is(:focus-visible) {
text-align:center; outline:none;
display: block;
} }
.skeleton { #bio :is(h1,h2,h3,h4,h5,h6) {
animation: skeleton-loading 1s linear infinite alternate; text-align: center;
min-height: 1em; display: block
}
@keyframes skeleton-loading {
0% {
background-color: hsl(200, 20%, 70%);
}
100% {
background-color: hsl(200, 20%, 95%);
}
}
.author.skeleton {
width: 6em;
display: inline-block;
}
p.skeleton {
width: 80%;
}
.chip.skeleton {
width: 10em;
}
main {
position: relative;
}
.skeleton-container {
flex:1 1 auto;
overflow: hidden;
}
nav header {
z-index: 101;
} }
:is(button,.button).tiny { :is(button,.button).tiny {
block-size: 1.5rem; block-size: 1.5rem;
/*max-inline-size: 1.5rem;*/
font-size: .875rem; font-size: .875rem;
border-radius: .75rem; border-radius: .75rem
}
nav.bottom.s:not(.drawer) > a:not(.button,.chip) {
inline-size: unset;
} }
nav.bottom.s:not(.drawer) > a:not(.button,.chip) { inline-size: unset }
nav.bottom.s:not(.drawer) :is(button,.button) > menu { nav.bottom.s:not(.drawer) :is(button,.button) > menu {
position: fixed; position: fixed;
margin-top:auto; margin-top: auto;
margin-bottom: 5rem; margin-bottom: 5rem;
margin-left:auto; margin-left: auto;
margin-right:0; margin-right: 0;
inline-size: auto; inline-size: auto;
min-inline-size: 12rem; min-inline-size: 12rem;
z-index: 100;
transform: none !important; transform: none !important;
inset: auto 0 0 auto; inset: auto 0 0 auto
}
i[class*=fa-at] {
vertical-align:unset;
font-size: .75em;
} }
.fab { .fab {
position:fixed; position: fixed;
right: 2rem; right: 2rem;
bottom: 2rem; bottom: 2rem
z-index: 1; }
@media only screen and (max-width:992px) {
.fab {
bottom: 7rem
}
:has(>main.responsive) {
max-block-size: calc(100vh - 5rem)
}
} }
dialog { dialog {
overflow:visible; overflow: visible;
width:80%; width: 80%
}
dialog#edit-bio {
overflow: scroll;
} }
.invisible { .invisible {
max-width:0px; max-width: 0;
max-height:0px; max-height: 0;
border: none; border: 0;
margin:0; margin: 0;
padding:0; padding: 0
}
@media only screen and (max-width: 992px) {
.fab {
bottom: 7rem;
}
*:has(> main.responsive) {
/*flex-direction: row;*/
max-block-size: calc(100vh - 5rem);
}
}
.avatar {
position:relative;
}
.avatar::after {
content: '';
position:absolute;
left:0; right:0;
top:0; bottom:0;
background-color: var(--surface-container-low);
/*background-image: var(--prami-svg);*/
background-size: contain;
background-repeat: no-repeat;
background-position: center bottom;
border-radius: 50%;
border: 1px dashed var(--gray-4);
z-index:1;
}
article.ephemeral {
max-width: 50rem;
border: 2px dashed var(--gray-7);
}
#pics article {
max-width: 50rem;
}
.tabs a {
text-decoration:none;
} }
.card-grid { .card-grid {
@ -271,238 +123,132 @@ article.ephemeral {
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
gap: .5rem; gap: .5rem;
justify-content: space-between; justify-content: space-between
} }
.card-grid > * { flex-grow: 1; flex-shrink: 1 }
#pics article { #now-garden { gap: 1rem }
margin: 1rem auto; #now-garden > :not(.now) { position: absolute }
text-align:center;
}
#pics article>:not(:first-child) {
text-align: left;
}
#pics article nav {
flex-wrap: wrap;
}
#pics.card-grid article{
max-width: 24rem;
}
@media only screen and (max-width: 895px) {
#pics.card-grid article {
max-width: calc(100% - 1rem);
}
}
#pics article > img:first-child {
text-align: center;
}
#now-garden{
gap:1rem;
}
#now-garden > :not(.now) {
position: absolute;
}
.card-grid > * {
flex-grow: 1;
flex-shrink: 1;
}
.page-container > .page.active {
display:flex;
flex-direction:column;
flex-grow: 1;
}
iframe {
width:100%;
flex-grow: 1;
border: none;
margin:0;
padding:0;
}
.now {
background-color: var(--green-2);
color: var(--black);
}
main, .page-container {
flex-grow:1;
display: flex;
flex-direction: column;
}
main {
overflow-x: hidden;
overflow-y: auto;
}
.hover {
z-index: 1;
}
#info :is(ul, ol) {
margin-left: var(--spacing);
}
#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:not(.s), .l:not(.s), .m.l:not(.s) {
display: none !important;
}
}
/* m */
@media only screen and (min-width: 601px) and (max-width: 992px) {
.s:not(.m), .l:not(.m), .s.l:not(.m) {
display: none !important;
}
}
/* 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;
}
}
#directory{
column-width: 14rem;
}
#directory a {
font-size: 1.1rem;
text-decoration: none;
}
article#directory ul{
list-style:none;
}
#directoryIndex a {
font-size: 1.5rem;
}
.editor-toolbar .easymde-dropdown, .editor-toolbar button {
color: inherit;
}
.editor-toolbar i.separator {
---size: unset;
opacity: 0.25;
border-radius: 0
}
.markdown-editor .editor-preview {
background: var(--surface);
}
.markdown-editor {
flex: auto;
display: flex;
flex-direction:column;
}
.markdown-editor + nav {
flex: none;
}
.markdown-editor > .EasyMDEContainer {
flex: 1;
display: flex;
flex-direction: column;
}
.markdown-editor > .EasyMDEContainer > .editor-toolbar{
flex: none;
}
.markdown-editor > .EasyMDEContainer > .CodeMirror {
flex: auto;
}
article { article {
overflow-wrap: anywhere; overflow-wrap: anywhere;
width: 100%;
max-width: var(--max-article-size);
margin-inline: auto
} }
nav label:is(.checkbox, .radio, .switch) { article.now { margin: 0; width: auto }
article.theme { height: 10rem; width: 15rem }
article.ephemeral { max-width: 50rem }
article.pic {
max-width: 50rem;
margin: 1rem auto;
text-align: left
}
article.pic > img:first-child { text-align: center }
article.pic nav { flex-wrap: wrap }
#pics.card-grid article { max-width: 24rem }
@media only screen and (max-width:895px) {
#pics.card-grid article {
max-width: calc(100% - 1rem)
}
}
article.paste code {
overflow-x: scroll;
display: block;
}
iframe {
width: 100%;
flex-grow: 1;
border: 0;
margin: 0;
padding: 0
}
.page-container, .page-container > .page.active, main {
flex-grow: 1;
display: flex;
flex-direction: column
}
#info :is(ul,ol) { margin-left: var(--spacing) }
#info :is(p,ul,ol) { margin-bottom: var(--spacing) }
#directory { column-width: 14rem }
article#directory ul { list-style: none }
nav label:is(.checkbox,.radio,.switch) {
white-space: break-spaces; white-space: break-spaces;
flex: 1 1 100%; flex: 1 1 100%
} }
menu > details .row, menu > li > details .row { menu > details .row, menu > li > details .row {
padding: .5rem 1rem; padding: .5rem 1rem;
min-block-size: 3rem; min-block-size: 3rem;
flex: 1
}
.row p {
white-space: normal;
max-width: 100%
}
a.row.indent { margin-left: 1rem }
: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
}
@media only screen and (max-width:600px) {
.l:not(.s), .m.l:not(.s), .m:not(.s) {
display: none !important
}
}
@media only screen and (min-width:601px) and (max-width:992px) {
.l:not(.m), .s.l:not(.m), .s:not(.m) {
display: none !important
}
}
@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
}
}
.markdown-editor {
flex: auto;
display: flex;
flex-direction: column
}
.markdown-editor + nav, .markdown-editor > .EasyMDEContainer > .editor-toolbar {
flex: none
}
.markdown-editor > .EasyMDEContainer {
flex: 1; flex: 1;
display: flex;
flex-direction: column
} }
menu > details > a:is(:hover,:focus,.active), menu > details > summary:is(:hover,:focus,.active) { .markdown-editor > .EasyMDEContainer > .CodeMirror {
background-color: var(--active); flex: auto
}
article {
width: 100%;
max-width: var(--max-article-size);
margin-inline: auto;
}
article.now {
margin: 0;
width:auto;
}
#advanced textarea, #advanced .field.textarea {
background-color: #212121;
color: #EEFFFF;
font-family: monospace;
}
article.theme {
height: 10rem;
width: 15rem;
} }

18
wwwroot/css/type.css Normal file
View file

@ -0,0 +1,18 @@
/* typography and fonts */
:root { --font: 'Lato', 'Helvetica Neue', Helvetica, Arial, sans-serif, var(--emoji-font) }
body, html { font-family: var(--font) }
body { font-size: 1.2em }
.address, .author, .honey, .page-heading { font-family: "VC Honey Deck",var(--font) }
.mono, #advanced :is(.field.textarea, textarea), .EasyMDEContainer, article.paste code {
font-family: 'MD IO 0.4', monospace, var(--emoji-font)
}
li, p { line-height: 160% }
.author { font-size: 1.2em }
nav.bottom.s :is(small,.label) { font-size: .75rem }
#directory a { font-size: 1.1rem }
#directoryIndex a { font-size: 1.5rem }

13
wwwroot/css/z.css Normal file
View file

@ -0,0 +1,13 @@
/* Anything related to z-indexes */
nav header { z-index: 101 }
nav.bottom.s:not(.drawer) :is(button,.button) > menu { z-index: 100 }
.fab {
z-index: 1
}
.avatar::after { z-index: 1 }
.hover { z-index: 1 }

View file

@ -18,7 +18,7 @@
<link href="/vendor/cm-material.css" rel="stylesheet" /> <link href="/vendor/cm-material.css" rel="stylesheet" />
<script src="/_content/PSC.Blazor.Components.MarkdownEditor/js/easymde.min.js"></script> <script src="/_content/PSC.Blazor.Components.MarkdownEditor/js/easymde.min.js"></script>
<script src="/_content/PSC.Blazor.Components.MarkdownEditor/js/markdownEditor.js"></script> <script src="/_content/PSC.Blazor.Components.MarkdownEditor/js/markdownEditor.js"></script>
<script src="/js/csharp.js"></script> <script src="/js/script.js"></script>
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
</head> </head>

View file

@ -1,47 +0,0 @@
window.injectCSharp = async function (helper) {
window.CSHARP = helper
}
async function delay(t) {
return new Promise((resolve) => {
setTimeout(resolve, t);
});
}
async function removeElementById(id) {
document.getElementById(id)?.remove()
}
function scrollToId(id) {
const element = document.getElementById(id);
if (element instanceof HTMLElement) {
element.scrollIntoView({
behavior: "smooth",
block: "start",
inline: "nearest"
});
}
}
function toggleDetails(id) {
const element = document.getElementById(id)
if (element instanceof HTMLDetailsElement)
element.open = !element.open
}
function cacheBust(url) {
fetch(new Request(url), {
headers: new Headers({
"pragma": "no-cache",
"cache-control": "no-cache"
}),
mode: 'no-cors',
cache: 'no-cache',
})
.finally(() => {
let els = document.querySelectorAll(`[src="${url}"]`)
els.forEach(el => el.removeAttribute('src'))
els.forEach(el => el.src = url)
})
}

32
wwwroot/js/script.js Normal file
View file

@ -0,0 +1,32 @@
window.injectCSharp = async function (helper) { window.CSHARP = helper }
async function delay(t) { return new Promise((resolve) => { setTimeout(resolve, t) }) }
async function removeElementById(id) { document.getElementById(id)?.remove() }
function scrollToId(id) {
const element = document.getElementById(id)
if (element instanceof HTMLElement)
element.scrollIntoView({behavior: "smooth",block: "start",inline: "nearest"})
}
function toggleDetails(id) {
const element = document.getElementById(id)
if (element instanceof HTMLDetailsElement) element.open = !element.open
}
function cacheBust(url) {
fetch(new Request(url), {
headers: new Headers({
"pragma": "no-cache",
"cache-control": "no-cache"
}),
mode: 'no-cors',
cache: 'no-cache',
})
.finally(() => {
let els = document.querySelectorAll(`[src="${url}"]`)
els.forEach(el => el.removeAttribute('src'))
els.forEach(el => el.src = url)
})
}