Compare commits

...

34 commits
0.9.3 ... 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
640cd73271 made the single animated emoji I'm using local 2024-07-12 15:16:46 +10:00
bdea7870ea Bumped version
I put the patch version in line with the single digit version for simplicity's sake.
It does mean I missed 0.9.5-0.9.6, but whatever.
2024-07-12 15:11:01 +10:00
7b462e3cfc Trimming some fat 2024-07-12 15:08:12 +10:00
2dc66abcd7 Fixed iframe resizer version @5.1.5 2024-07-12 14:44:19 +10:00
3d8047c01c Substituted the TossFace emoji font for Twemoji
Not only that, but a subset of Twemoji covering just the flags, bringing font size down dramatically.
2024-07-11 17:44:58 +10:00
f24ef392f7 Edit profile / profile pic 2024-07-11 15:57:53 +10:00
0fcda98b9f Customizing the markdown editor toolbar 2024-07-05 17:18:46 +10:00
25f362bfc5 Added a way to edit the profile page
Including custom css and head content, but not custom themes or metadata (yet)
2024-07-05 15:47:05 +10:00
b42bf2023d Fixed feed icon not appearing
+version bump
2024-07-04 10:21:25 +10:00
134 changed files with 2278 additions and 12140 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 {
public class NavigatorService {
internal NavigationManager? NavigationManager { get; set; }
internal Page? Page { get; set; }
}
}

View file

@ -1,276 +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 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?.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?> 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<List<MarkupString>> EphemeralScrape() {
List<string> notes = new List<string>();
Uri Uri = new Uri($"https://eph.emer.al/");
try {
var response = await _client.GetAsync(Uri);
var str = await response.Content.ReadAsStringAsync();
string pattern = @"<p class=""post"">(.*?)<\/p>";
var matches = Regex.Matches(str, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
foreach (Match match in matches) {
notes.Add(match.Groups[1].Value);
}
}
catch (Exception ex) {
Debug.WriteLine(ex);
}
return notes.Select(s => (MarkupString)s).ToList();
}
public async Task<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,10 +24,19 @@ namespace Neighbourhood.omg.lol {
public List<MarkupString>? EphemeralMessages { 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; }
// Account data
public AccountResponseData? AccountInfo { get; set; }
private AccountResponseData? _accountInfo;
public AccountResponseData? AccountInfo {
get => _accountInfo;
set {
_accountInfo = value;
OnPropertyChanged(nameof(AccountInfo));
}
}
public AddressResponseList? AddressList { get; set; }
public bool IsAuthorized { get => AccountInfo != null; }
@ -64,6 +73,7 @@ namespace Neighbourhood.omg.lol {
// data for selected address
public List<Status>? CachedAddressStatuses { get; set; }
public List<Pic>? CachedAddressPics { get; set; }
public List<Paste>? CachedAddressPastes { get; set; }
public MarkupString? CachedAddressBio { get; set; }
private string? _cachedAddress;
public string? CachedAddress {
@ -73,6 +83,7 @@ namespace Neighbourhood.omg.lol {
_cachedAddress = value;
CachedAddressStatuses = new List<Status>();
CachedAddressPics = new List<Pic>();
CachedAddressPastes = new List<Paste>();
CachedAddressBio = null;
}
}
@ -146,9 +157,9 @@ namespace Neighbourhood.omg.lol {
}
// api service
private RestService api { get; set; }
private ApiService api { get; set; }
public State(RestService restService) {
public State(ApiService restService) {
api = restService;
}
@ -215,7 +226,7 @@ namespace Neighbourhood.omg.lol {
public async Task<MarkupString?> GetBio(string address, bool forceRefresh = false) {
CachedAddress = address;
if (forceRefresh || CachedAddressBio == null) {
CachedAddressBio = await api.StatuslogBio(address);
CachedAddressBio = Utilities.MdToHtmlMarkup(await api.StatuslogBio(address));
}
return CachedAddressBio;
}
@ -275,6 +286,19 @@ namespace Neighbourhood.omg.lol {
return CachedAddressPics;
}
public async Task<List<Paste>?> GetPastes(string address, bool forceRefresh = false) {
CachedAddress = address;
if (forceRefresh || this.CachedAddressPastes == null || this.CachedAddressPastes.Count == 0) {
if (AddressNames?.Contains(address) ?? false) {
CachedAddressPastes = (await api.GetMyPastes(address)) ?? new List<Paste>();
}
else {
CachedAddressPastes = (await api.GetPastes(address)) ?? new List<Paste>();
}
}
return CachedAddressPastes;
}
public async Task RefreshStatuses() {
await GetStatuses(forceRefresh: true);
if(SelectedAddressName != null)
@ -287,16 +311,29 @@ namespace Neighbourhood.omg.lol {
}
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) {
Feed = new List<StatusOrPic>();
Feed = new List<FeedItem>();
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 GetPics(address, forceRefresh))?.Select(p => new StatusOrPic { Pic = p }) ?? 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 FeedItem { Pic = p }) ?? new List<FeedItem>());
Feed.AddRange((await GetPastes(address, forceRefresh))?.Select(p => new FeedItem { Paste = p }) ?? new List<FeedItem>());
}
}
return Feed.OrderByDescending(s => s.CreatedTime);
}
public async Task<Dictionary<string, Theme>?> GetThemes(bool forceRefresh = false) {
if (forceRefresh || this.Themes == null || this.Themes.Count == 0) {
this.Themes = await api.GetThemes();
}
return this.Themes;
}
}
}

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 State State
@inject RestService api
@inject ApiService api
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">

View file

@ -0,0 +1,114 @@
@inject IJSRuntime JS
@inject State State
@inject ApiService api
@inject NavigationManager navigationManager
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active">
<h5>Update your profile picture</h5>
<div class="padding center-align">
<img src="@(Base64Url ?? ExistingUrl)" class="small-height square" />
</div>
<div class="row">
<button @onclick="PicFromMedia"><i class="fa-solid fa-image"></i> Select a picture</button>
<button @onclick="PicFromPhoto"><i class="fa-solid fa-camera"></i> Take a photo</button>
</div>
<nav class="right-align no-space">
<button class="transparent link" data-ui="#@id" disabled="@loading">Cancel</button>
<button @onclick="PostPic" disabled="@loading">
@if (loading) {
<span>Uploading...</span>
}
else {
<i class="fa-solid fa-cloud-arrow-up"></i> <span>Upload</span>
}
</button>
</nav>
</dialog>
@code {
[Parameter]
public string? Address { get; set; }
public string ExistingUrl { get => $"https://profiles.cache.lol/{Address ?? ""}/picture"; }
// private IBrowserFile? File { get; set; }
[Parameter]
public string? Base64File { get; set; }
[Parameter]
public long? FileSize { get; set; }
[Parameter]
public string? FileContentType { get; set; }
[Parameter]
public string? id { get; set; }
[Parameter]
public bool Active { get; set; }
private bool loading = false;
private FileResult? File { get; set; }
private string? Base64Url {
get {
if (FileContentType == null || Base64File == null) return null;
return $"data:{FileContentType};base64,{Base64File}";
}
}
public async Task PostPic() {
loading = true;
await InvokeAsync(StateHasChanged);
if (Base64File != null && File != null)
{
BasicResponseData? response = await api.PostProfilePic(Address!, File);
if (response != null)
{
await JS.InvokeVoidAsync("ui", "#" + id);
// clear input
File = null;
Base64File = null;
FileSize = null;
FileContentType = null;
await JS.InvokeVoidAsync("cacheBust", ExistingUrl);
}
loading = false;
await InvokeAsync(StateHasChanged);
}
}
private string formatSizeUnits(long? bytes) {
if (bytes == null) return "?? bytes";
string formatted = "0 bytes";
if (bytes >= 1073741824) { formatted = $"{(bytes / 1073741824):.##} GB"; }
else if (bytes >= 1048576) { formatted = $"{(bytes / 1048576):.##} MB"; }
else if (bytes >= 1024) { formatted = $"{(bytes / 1024):.##} KB"; }
else if (bytes > 1) { formatted = $"{bytes} bytes"; }
else if (bytes == 1) { formatted = $"{bytes} byte"; }
return formatted;
}
private async Task PicFromMedia(EventArgs e) {
File = await MediaPicker.Default.PickPhotoAsync();
await PopulateFileDetails();
}
private async Task PicFromPhoto(EventArgs e) {
File = await MediaPicker.Default.CapturePhotoAsync();
await PopulateFileDetails();
}
private async Task PopulateFileDetails() {
if (File == null) {
FileContentType = null;
FileSize = null;
Base64File = null;
}
else {
FileContentType = File.ContentType;
FileSize = await Utilities.FileSize(File);
Base64File = await Utilities.Base64FromFile(File);
}
}
}

View file

@ -1,6 +1,6 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@inject ApiService api
<div class="overlay" data-ui="#@id"></div>
<dialog id="@id">

View file

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

View file

@ -24,7 +24,7 @@
<AvatarMenuLinks></AvatarMenuLinks>
</menu>
</button>
<small class="s m address">Omg.lol</small>
<small class="s m honey">Omg.lol</small>
</NavLink>
<div class="l">
Hey there. <br />

View file

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

View file

@ -27,10 +27,22 @@ else {
<div class="label">Now.garden</div>
</NavLink>
}
<NavLink class="l m nav-link" href="/directory">
<i class="square fa-duotone fa-address-book"></i>
<div class="label">Directory</div>
</NavLink>
</NavLink>
@code {
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
State.PropertyChanged += StateChanged;
}
private async void StateChanged(object? sender, PropertyChangedEventArgs e) {
if (e.PropertyName == nameof(State.AccountInfo)) await InvokeAsync(StateHasChanged);
}
public void Dispose() {
State.PropertyChanged -= StateChanged;
}
}

View file

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

View file

@ -1,6 +1,6 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@inject ApiService api
@inject NavigationManager navigationManager
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
@ -9,12 +9,6 @@
<div class="row">
<button @onclick="PicFromMedia"><i class="fa-solid fa-image"></i> Select a picture</button>
<button @onclick="PicFromPhoto"><i class="fa-solid fa-camera"></i> Take a photo</button>
@* <div class="field label prefix border">
<i class="fa-solid fa-image"></i>
<InputFile OnChange="@ChangeFile" accept="image/gif, image/heic, image/heif, image/jpeg, image/png, image/svg+xml, image/webp"></InputFile>
<input type="text">
<label>Select a picture</label>
</div> *@
</div>
<div class="row">
@if(Base64File != null && FileSize != null){

View file

@ -1,7 +1,8 @@
@inject IJSRuntime JS
@inject State State
@inject RestService api
@inject ApiService api
@inject NavigationManager navigationManager
@inject NavigatorService navigatorService
<div class="overlay @(Active ? "active" : string.Empty)" data-ui="#@id"></div>
<dialog id="@id" class="@(Active ? "active" : string.Empty)" open="@Active">
@ -27,7 +28,8 @@
</button>
</div>
<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>
<nav class="right-align no-space">
@ -66,16 +68,26 @@
public async Task PostStatus() {
StatusPost post = new StatusPost
{
StatusPost post = new StatusPost {
Emoji = Emoji,
Content = Content
};
if (State?.SelectedAddress?.Preferences?.Statuslog?.MastodonPosting ?? false){
if (State?.SelectedAddress?.Preferences?.Statuslog?.MastodonPosting ?? false) {
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;
await InvokeAsync(StateHasChanged);
var result = await api.StatusPost(State!.SelectedAddressName!, post);

View file

@ -16,13 +16,13 @@
<article id="directoryIndex" class="responsive">
<nav class="wrap">
@foreach (var group in groupedAddresses) {
<a @onclick='()=>{JS.InvokeVoidAsync("scrollToId", $"index-{group.Key}");}' class="button circle transparent address">@group.Key</a>
<a @onclick='()=>{JS.InvokeVoidAsync("scrollToId", $"index-{group.Key}");}' class="button circle transparent honey">@group.Key</a>
}
</nav>
</article>
<article id="directory" class="responsive">
@foreach(var group in groupedAddresses) {
<h3 class="address" id="index-@group.Key">— @group.Key —</h3>
<h3 class="honey" id="index-@group.Key">— @group.Key —</h3>
<ul>
@foreach(string address in group) {
string displayAddress = address;
@ -35,7 +35,7 @@
catch (Exception) { }
}
<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">
<span>@displayAddress</span>
</a>

View file

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

View file

@ -0,0 +1,142 @@
@page "/editProfile"
@inject NavigationManager Nav
@inject ApiService api
@inject State State
@inject IJSRuntime JS
<div class="max markdown-editor">
@if (markdownValue != null)
{
<MarkdownEditor @ref="Editor"
@bind-Value="@markdownValue"
Theme="material-darker"
MaxHeight="100%"
CustomButtonClicked="@OnCustomButtonClicked"
AutoDownloadFontAwesome="false"
>
<Toolbar>
<MarkdownToolbarButton Action="MarkdownAction.Bold" Icon="fa-solid fa-bold" Title="Bold" />
<MarkdownToolbarButton Action="MarkdownAction.Italic" Icon="fa-solid fa-italic" Title="Italic" />
<MarkdownToolbarButton Action="MarkdownAction.Heading" Icon="fa-solid fa-heading" Title="Heading" />
<MarkdownToolbarButton Action="MarkdownAction.Code" Icon="fa-solid fa-code" Title="Code" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Quote" Icon="fa-solid fa-quote-left" Title="Quote" />
<MarkdownToolbarButton Action="MarkdownAction.UnorderedList" Icon="fa-solid fa-list-ul" Title="Unordered List" />
<MarkdownToolbarButton Action="MarkdownAction.OrderedList" Icon="fa-solid fa-list-ol" Title="Ordered List" />
<MarkdownToolbarButton Action="MarkdownAction.Link" Icon="fa-solid fa-link" Title="Link" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Image" Icon="fa-solid fa-image" Title="Image" />
<MarkdownToolbarButton Action="MarkdownAction.HorizontalRule" Icon="fa-solid fa-horizontal-rule" Title="Horizontal Rule" />
<MarkdownToolbarButton Action="MarkdownAction.Guide" Icon="fa-solid fa-circle-question" Title="Guide" Separator="true" />
<MarkdownToolbarButton Action="MarkdownAction.Custom" Icon="omg-icon omg-prami" Title="Editor Information" Name="Help" />
</Toolbar>
</MarkdownEditor>
}
</div>
@if (markdownValue != null)
{
<details id="advanced">
<summary> Advanced </summary>
<h5>Theme:</h5>
<div class="row bottom-margin">
<ThemeDialog id="theme-modal" onthemechanged="ThemeChanged"></ThemeDialog>
<a data-ui="#theme-modal" class="row min" style="text-decoration:none;">
@if(selectedTheme != null) {
<ThemeCard theme="selectedTheme"></ThemeCard>
}
else {
<button>Choose a theme</button>
}
</a>
</div>
<small>Style you include here will be places in a &lt;style&gt; element in your pages &lt;head&gt;.</small>
<div class="field textarea label border max">
<InputTextArea @bind-Value="css"></InputTextArea>
<label>Custom CSS</label>
</div>
<small>Anything you put here will be included in your pages &lt;head&gt; element.</small>
<div class="field textarea label border max">
<InputTextArea @bind-Value="head"></InputTextArea>
<label>Additional &lt;head&gt; Content</label>
</div>
</details>
}
<nav>
<div class="max"></div>
<button class="transparent link" onclick="history.back();" disabled="@loading">Cancel</button>
<button @onclick="Save" disabled="@loading">
@if (loading) {
<span>Saving...</span>
}
else {
<i class="fa-solid fa-floppy-disk"></i> <span>Save & Publish</span>
}
</button>
</nav>
@code {
private MarkdownEditor? Editor;
private string? markdownValue;
private string? css;
private string? head;
private string? theme;
private Theme? selectedTheme;
private Dictionary<string, Theme>? themes;
private bool loading = true;
protected override async Task OnInitializedAsync() {
await base.OnInitializedAsync();
ProfileResponseData? data = await api.GetProfile(State.SelectedAddressName!);
if (data != null) {
markdownValue = data.Content;
css = data.Css;
head = data.Head;
theme = data.Theme;
themes = await State.GetThemes();
selectedTheme = themes?[theme];
loading = false;
await InvokeAsync(StateHasChanged);
await Editor!.SetValueAsync(markdownValue);
}
loading = false;
await InvokeAsync(StateHasChanged);
}
Task OnMarkdownValueChanged(string value) {
return Task.CompletedTask;
}
public async Task Save() {
loading = true;
await InvokeAsync(StateHasChanged);
var result = await api.PostProfile(State.SelectedAddressName!,
new PostProfile() {
Content = markdownValue ?? string.Empty,
Css = string.IsNullOrEmpty(css) ? null : css,
Head = string.IsNullOrEmpty(head) ? null : head,
Theme = string.IsNullOrEmpty(theme) ? null : theme
});
if (result != null) {
await State.RefreshNow();
await InvokeAsync(StateHasChanged);
Nav.NavigateTo($"/person/{State.SelectedAddressName}#profile");
}
loading = false;
await InvokeAsync(StateHasChanged);
}
public async Task OnCustomButtonClicked(MarkdownButtonEventArgs eventArgs) {
if (eventArgs.Name == "Help") {
await JS.InvokeVoidAsync("open", "https://home.omg.lol/info/editor", "_blank");
}
}
public void ThemeChanged(Theme? _theme) {
theme = _theme?.Id;
selectedTheme = _theme;
InvokeAsync(StateHasChanged);
}
}

View file

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

View file

@ -16,7 +16,7 @@
<article class="now">
<nav>
<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>
</nav>
<nav>

View file

@ -9,7 +9,13 @@
<h3 class="page-heading"><i class="fa-solid fa-fw fa-at"></i>@Address</h3>
</div>
<div class="row center-align">
<img class="profile avatar" src="https://profiles.cache.lol/@Address/picture" alt="@Address" />
<div class="min">
@if (IsMe) {
<EditProfilePicDialog id="profile-pic" Address="@Address"></EditProfilePicDialog>
<button data-ui="#profile-pic" class="small circle small-elevate absolute top right no-margin" style="z-index:1;"><i class="fa-solid fa-pencil"></i></button>
}
<img class="profile avatar" src="https://profiles.cache.lol/@Address/picture" alt="@Address" />
</div>
</div>
@if (FeatureFlags.Following) {
<div class="row center-align">
@ -45,6 +51,10 @@
<span>/Now</span>
</a>
}
<a data-ui="#pastebin">
<i class="fa-solid fa-clipboard"></i>
<span>Paste.lol</span>
</a>
</div>
</div>
@ -52,6 +62,12 @@
<div id="profile" class="page no-padding">
<a href="@ProfileUrl" target="_blank" class="hover absolute top right chip fill large-elevate">Open in browser <i class="fa-solid fa-arrow-up-right-from-square tiny"></i></a>
<ExternalPageComponent id="profile_page" @ref="ProfilePage" Url="@ProfileUrl"></ExternalPageComponent>
@if (IsMe) {
<a href="/editProfile" class="button fab circle extra large-elevate center-align middle-align">
<i class="square fa-solid fa-file-pen" style="line-height:56px;"></i>
<span>Edit</span>
</a>
}
</div>
<div id="statuses" class="page padding active">
@ -67,10 +83,16 @@
</article>
</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>
@if(IsMe) {
<button class="fab circle extra large-elevate" data-ui="#post-modal">
<i class="fa-solid fa-pen-to-square"></i>
<i class="fa-solid fa-message-plus square"></i>
</button>
<NewStatusDialog id="post-modal"></NewStatusDialog>
}
@ -97,6 +119,15 @@
}
</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>
@code {
@ -108,6 +139,7 @@
_address = value;
if (StatusList != null) StatusList.StatusFunc = async (refresh) => await State.GetStatuses(_address, refresh);
if (PicList != null) PicList.PicsFunc = async (refresh) => await State.GetPics(_address, refresh);
if (PasteList != null) PasteList.PastesFunc = async (refresh) => await State.GetPastes(_address, refresh);
}
}
public string ProfileUrl {
@ -123,9 +155,10 @@
private StatusList? StatusList { get; set; }
private PicList? PicList { get; set; }
private PasteList? PasteList { get; set; }
private bool IsMe {
get => Address == State.SelectedAddressName;
get => State.AddressList?.Any(a => a.Address == Address) ?? false;
}
private MarkupString? bio;

View file

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

View file

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

View file

@ -0,0 +1,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;
// 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() {
await base.OnInitializedAsync();
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

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

View file

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

View file

@ -14,3 +14,4 @@
@using Markdig
@using PSC.Blazor.Components.MarkdownEditor
@using PSC.Blazor.Components.MarkdownEditor.EventsArgs
@using PSC.Blazor.Components.MarkdownEditor.Enums

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.Logging;
@ -8,14 +9,14 @@ namespace Neighbourhood.omg.lol {
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts => {
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
.UseMauiCommunityToolkit(options => {
options.SetShouldEnableSnackbarOnWindows(true);
});
builder.Services.AddMauiBlazorWebView();
builder.Services.AddTransient<LoginWebViewPage>();
builder.Services.AddSingleton<RestService>();
builder.Services.AddSingleton<ApiService>();
builder.Services.AddSingleton<State>();
builder.Services.AddSingleton<NavigatorService>();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
namespace Neighbourhood.omg.lol.Models {
public interface IOmgLolResponseData {
public string Message { get; set; }
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

View file

@ -0,0 +1,9 @@
namespace Neighbourhood.omg.lol.Models {
public class PostProfile {
public string Content { get; set; } = string.Empty;
public bool Publish { get; set; } = true;
public string? Theme { get; set; }
public string? Css { get; set; }
public string? Head { get; set; }
}
}

View file

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

View file

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

View file

@ -0,0 +1,16 @@
namespace Neighbourhood.omg.lol.Models {
public class ProfileResponseData: IOmgLolResponseData {
public string Message { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string Theme { get; set; } = string.Empty;
public string? Css { get; set; }
public string? Head { get; set; }
public short Verified { get; set; }
public string Pfp { get; set; } = string.Empty;
public string Metadata { get; set; } = string.Empty;
public string Branding { get; set; } = string.Empty;
public string? Modified { get; set; }
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Neighbourhood.omg.lol.Models {
public class StatusOrPic {
namespace Neighbourhood.omg.lol.Models {
public class FeedItem {
public Status? Status { get; set; }
public Pic? Pic { get; set; }
public Paste? Paste { get; set; }
public bool IsStatus { get => Status != null; }
public bool IsPic { get => Pic != null; }
public bool IsPaste { get => Paste != null; }
public DateTimeOffset? CreatedTime { get => Status?.CreatedTime ?? Pic?.CreatedTime; }
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;
}
}
}
}

19
Models/Theme.cs Normal file
View file

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

View file

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

View file

@ -43,22 +43,22 @@
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-ios|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.3</ApplicationDisplayVersion>
<ApplicationVersion>5</ApplicationVersion>
<ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>9</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-maccatalyst|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.3</ApplicationDisplayVersion>
<ApplicationVersion>5</ApplicationVersion>
<ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>9</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android34.0|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.3</ApplicationDisplayVersion>
<ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<AndroidKeyStore>True</AndroidKeyStore>
<AndroidSigningKeyStore>D:\Neighbourhood.omg.lol\neighbourhood.omg.lol.keystore</AndroidSigningKeyStore>
<ApplicationVersion>5</ApplicationVersion>
<AndroidSigningKeyStore>D:\_assets\neighbourhood.omg.lol\neighbourhood.omg.lol.keystore</AndroidSigningKeyStore>
<ApplicationVersion>9</ApplicationVersion>
<AndroidSigningStorePass>a!zobzizl</AndroidSigningStorePass>
<AndroidSigningKeyAlias>neighbourhood.omg.lol</AndroidSigningKeyAlias>
<AndroidSigningKeyPass>a!zobzizl</AndroidSigningKeyPass>
@ -66,36 +66,37 @@
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.3</ApplicationDisplayVersion>
<ApplicationVersion>5</ApplicationVersion>
<ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>9</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-ios|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.3</ApplicationDisplayVersion>
<ApplicationVersion>5</ApplicationVersion>
<ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>9</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-maccatalyst|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.3</ApplicationDisplayVersion>
<ApplicationVersion>5</ApplicationVersion>
<ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>9</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-android34.0|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.3</ApplicationDisplayVersion>
<ApplicationVersion>5</ApplicationVersion>
<ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>9</ApplicationVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationId>au.death.lol.omg.neighbourhood</ApplicationId>
<ApplicationDisplayVersion>0.9.3</ApplicationDisplayVersion>
<ApplicationVersion>5</ApplicationVersion>
<ApplicationDisplayVersion>0.9.9</ApplicationDisplayVersion>
<ApplicationVersion>9</ApplicationVersion>
</PropertyGroup>
<ItemGroup>
<!-- 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" />
<!-- Splash Screen -->
@ -103,7 +104,6 @@
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
@ -117,108 +117,7 @@
</ItemGroup>
<ItemGroup>
<MauiFont Remove="Resources\Fonts\fa-brands-400.ttf" />
<MauiFont Remove="Resources\Fonts\fa-brands-400.woff2" />
<MauiFont Remove="Resources\Fonts\fa-duotone-900.ttf" />
<MauiFont Remove="Resources\Fonts\fa-duotone-900.woff2" />
<MauiFont Remove="Resources\Fonts\fa-light-300.ttf" />
<MauiFont Remove="Resources\Fonts\fa-light-300.woff2" />
<MauiFont Remove="Resources\Fonts\fa-regular-400.ttf" />
<MauiFont Remove="Resources\Fonts\fa-regular-400.woff2" />
<MauiFont Remove="Resources\Fonts\fa-solid-900.ttf" />
<MauiFont Remove="Resources\Fonts\fa-solid-900.woff2" />
<MauiFont Remove="Resources\Fonts\fa-thin-100.ttf" />
<MauiFont Remove="Resources\Fonts\fa-thin-100.woff2" />
<MauiFont Remove="Resources\Fonts\fa-v4compatibility.ttf" />
<MauiFont Remove="Resources\Fonts\fa-v4compatibility.woff2" />
<MauiFont Remove="Resources\Fonts\omg.lol-icons.woff2" />
<MauiFont Remove="Resources\Fonts\seguiemj.ttf" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\Fonts\fa-brands-400.ttf" />
<None Remove="Resources\Fonts\fa-brands-400.woff2" />
<None Remove="Resources\Fonts\fa-duotone-900.ttf" />
<None Remove="Resources\Fonts\fa-duotone-900.woff2" />
<None Remove="Resources\Fonts\fa-light-300.ttf" />
<None Remove="Resources\Fonts\fa-light-300.woff2" />
<None Remove="Resources\Fonts\fa-regular-400.ttf" />
<None Remove="Resources\Fonts\fa-regular-400.woff2" />
<None Remove="Resources\Fonts\fa-solid-900.ttf" />
<None Remove="Resources\Fonts\fa-solid-900.woff2" />
<None Remove="Resources\Fonts\fa-thin-100.ttf" />
<None Remove="Resources\Fonts\fa-thin-100.woff2" />
<None Remove="Resources\Fonts\fa-v4compatibility.ttf" />
<None Remove="Resources\Fonts\fa-v4compatibility.woff2" />
<None Remove="Resources\Fonts\omg.lol-icons.woff2" />
<None Remove="Resources\Fonts\seguiemj.ttf" />
</ItemGroup>
<ItemGroup>
<Content Include="Resources\Fonts\fa-brands-400.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-brands-400.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-duotone-900.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-duotone-900.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-light-300.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-light-300.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-regular-400.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-regular-400.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-solid-900.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-solid-900.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-thin-100.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-thin-100.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-v4compatibility.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\fa-v4compatibility.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\omg.lol-icons.woff2">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="Resources\Fonts\seguiemj.ttf">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<None Remove="Resources\AppIcon\icon.svg" />
</ItemGroup>
<ItemGroup>
@ -226,18 +125,20 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Maui" Version="9.0.2" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="8.0.70" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.70" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="8.0.70" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.Maui.Essentials" Version="8.0.40" />
<PackageReference Include="PSC.Blazor.Components.MarkdownEditor" Version="8.0.0" />
<PackageReference Include="Microsoft.Maui.Essentials" Version="8.0.70" />
<PackageReference Include="PSC.Blazor.Components.MarkdownEditor" Version="8.0.4" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
<ItemGroup>
@ -249,4 +150,10 @@
</MauiXaml>
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\Fonts\" />
<Folder Include="Resources\Images\" />
<Folder Include="Resources\Raw\" />
</ItemGroup>
</Project>

View file

@ -1,5 +1,5 @@
<?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="5" android:versionName="0.9.3">
<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>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<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
{
// TODO: we don't really support this at the moment.
//System.Collections.IList? uriList;
//if (OperatingSystem.IsAndroidVersionAtLeast(33))
//NOTE we don't really support recieving multiple files from a share request at the moment.
// <!--
// order:0
// -->
// System.Collections.IList? uriList;
// if (OperatingSystem.IsAndroidVersionAtLeast(33))
// 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:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
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" />
@ -36,6 +38,26 @@
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</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>
</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">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/icon.appiconset</string>
<key>CFBundleShortVersionString</key>
<string>0.9.9</string>
<key>MinimumOSVersion</key>
<string>14.2</string>
</dict>
</plist>

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
</svg>

Before

Width:  |  Height:  |  Size: 228 B

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more