From df05e8a819bea37f398276b752999f938a8c15b3 Mon Sep 17 00:00:00 2001 From: Gordon Pedersen Date: Fri, 14 Jun 2024 17:20:04 +1000 Subject: [PATCH] Made the api a singleton and got a share intent working The result of the share intent needs more work, though. --- Components/EditPicDialog.razor | 2 +- Components/EditStatusDialog.razor | 2 +- Components/ExternalPageComponent.razor | 7 +- Components/Layout/MainLayout.razor | 20 +++++ Components/NewPicDialog.razor | 6 +- Components/NewStatusDialog.razor | 3 +- Components/Pages/ShareText.razor | 10 +++ Components/Routes.razor | 13 +++ LoginWebViewPage.xaml.cs | 6 +- MauiProgram.cs | 4 +- Models/NowData.cs | 2 +- Models/Pic.cs | 2 +- Models/State.cs | 109 ++++++++++--------------- Models/Status.cs | 7 +- Models/Utilities.cs | 51 ++++++++++++ Platforms/Android/MainActivity.cs | 40 ++++++++- RestService.cs | 10 ++- 17 files changed, 200 insertions(+), 94 deletions(-) create mode 100644 Components/Pages/ShareText.razor create mode 100644 Models/Utilities.cs diff --git a/Components/EditPicDialog.razor b/Components/EditPicDialog.razor index 7bc5639..84d4fdf 100644 --- a/Components/EditPicDialog.razor +++ b/Components/EditPicDialog.razor @@ -1,5 +1,6 @@ @inject IJSRuntime JS @inject State State +@inject RestService api
@@ -46,7 +47,6 @@ loading = true; await InvokeAsync(StateHasChanged); - RestService api = new RestService(); if(!string.IsNullOrEmpty(Pic.Id)) { await api.PostPicDescription(State.SelectedAddressName, Pic.Id, Description); await State.RefreshPics(); diff --git a/Components/EditStatusDialog.razor b/Components/EditStatusDialog.razor index 5f1d270..32e9fcd 100644 --- a/Components/EditStatusDialog.razor +++ b/Components/EditStatusDialog.razor @@ -1,5 +1,6 @@ @inject IJSRuntime JS @inject State State +@inject RestService api
@@ -69,7 +70,6 @@ loading = true; await InvokeAsync(StateHasChanged); - RestService api = new RestService(); if (!string.IsNullOrEmpty(Status?.Id)) { await api.PatchStatus(State.SelectedAddressName, Status.Id, Content, Emoji); await State.RefreshStatuses(); diff --git a/Components/ExternalPageComponent.razor b/Components/ExternalPageComponent.razor index 9e54d9b..2450e14 100644 --- a/Components/ExternalPageComponent.razor +++ b/Components/ExternalPageComponent.razor @@ -1,5 +1,6 @@ @inject IJSRuntime JS @inject State State +@inject RestService api @if(Html != null) { } @@ -11,11 +12,6 @@ public string id { get; set; } public MarkupString? Html { get; set; } - protected override void OnInitialized() { - base.OnInitialized(); - State.CurrentPage = Page.Other; - } - protected override async Task OnAfterRenderAsync(bool firstRender) { if(firstRender){ await Reload(); @@ -24,7 +20,6 @@ public async Task Reload() { if (Html == null){ - RestService api = new RestService(); Html = await api.GetHtml(Url); string? HtmlString = Html?.ToString(); HtmlString = HtmlString?.Replace("", ""); diff --git a/Components/Layout/MainLayout.razor b/Components/Layout/MainLayout.razor index 55fe33c..dcc75dd 100644 --- a/Components/Layout/MainLayout.razor +++ b/Components/Layout/MainLayout.razor @@ -1,4 +1,5 @@ @inherits LayoutComponentBase +@implements IDisposable @inject NavigatorService NavigatorService @inject NavigationManager NavigationManager @inject State State @@ -11,5 +12,24 @@ protected override void OnInitialized() { base.OnInitialized(); NavigatorService.NavigationManager = NavigationManager; + State.IntentReceived += IntentRecieved; + if (!string.IsNullOrEmpty(State.ShareString) || !string.IsNullOrEmpty(State.SharePhoto)) { + IntentRecieved(); + } + } + + private void IntentRecieved(object? sender = null, EventArgs? e = null) { + if (!string.IsNullOrEmpty(State.ShareString)) { + NavigationManager.NavigateTo($"/sharetext/{State.ShareString}"); + State.ShareString = null; + } + else if (!string.IsNullOrEmpty(State.SharePhoto)) { + NavigationManager.NavigateTo($"/sharepic/{State.SharePhoto}"); + State.ShareString = null; + } + } + + void IDisposable.Dispose() { + State.IntentReceived -= IntentRecieved; } } diff --git a/Components/NewPicDialog.razor b/Components/NewPicDialog.razor index 1f41c03..b344518 100644 --- a/Components/NewPicDialog.razor +++ b/Components/NewPicDialog.razor @@ -1,5 +1,6 @@ @inject IJSRuntime JS @inject State State +@inject RestService api
@@ -61,7 +62,6 @@ loading = true; await InvokeAsync(StateHasChanged); - RestService api = new RestService(); PutPicResponseData? response = await api.PutPic(State.SelectedAddressName, File); if(!string.IsNullOrEmpty(Description) && response != null && !string.IsNullOrEmpty(response.Id)) { await api.PostPicDescription(State.SelectedAddressName, response.Id, Description); @@ -107,7 +107,7 @@ } private async Task PopulateFileDetails() { - FileSize = await State.FileSize(File); - Base64File = await State.Base64FromFile(File); + FileSize = await Utilities.FileSize(File); + Base64File = await Utilities.Base64FromFile(File); } } diff --git a/Components/NewStatusDialog.razor b/Components/NewStatusDialog.razor index 19142a6..7d8c07f 100644 --- a/Components/NewStatusDialog.razor +++ b/Components/NewStatusDialog.razor @@ -1,5 +1,6 @@ @inject IJSRuntime JS @inject State State +@inject RestService api
@@ -71,8 +72,6 @@ loading = true; InvokeAsync(StateHasChanged); - - RestService api = new RestService(); var result = await api.StatusPost(State.SelectedAddressName, post); if(result != null){ State.RefreshStatuses().ContinueWith(t => InvokeAsync(StateHasChanged)); diff --git a/Components/Pages/ShareText.razor b/Components/Pages/ShareText.razor new file mode 100644 index 0000000..6dc7747 --- /dev/null +++ b/Components/Pages/ShareText.razor @@ -0,0 +1,10 @@ +@page "/sharetext/{Text}" + +

Sharing

+ +

@Text

+ +@code { + [Parameter] + public string Text { get; set; } +} diff --git a/Components/Routes.razor b/Components/Routes.razor index 9bbebfe..7e96f2a 100644 --- a/Components/Routes.razor +++ b/Components/Routes.razor @@ -1,4 +1,5 @@ @using Microsoft.AspNetCore.Components.Authorization +@inject NavigationManager navigationManager @@ -16,3 +17,15 @@ + + +@code +{ + protected override void OnAfterRender(bool firstRender) { + string? shareString = Preferences.Get("shareString", null); + if (!string.IsNullOrWhiteSpace(shareString)) { + Preferences.Remove("shareString"); + navigationManager.NavigateTo($"/sharetext/{shareString}"); + } + } +} \ No newline at end of file diff --git a/LoginWebViewPage.xaml.cs b/LoginWebViewPage.xaml.cs index d74fe13..62e3cb0 100644 --- a/LoginWebViewPage.xaml.cs +++ b/LoginWebViewPage.xaml.cs @@ -12,16 +12,18 @@ public partial class LoginWebViewPage : ContentPage private AuthenticationStateProvider AuthStateProvider { get; set; } private NavigatorService NavigatorService { get; set; } private IConfiguration Configuration { get; set; } + private RestService api { get; set; } private string? client_id; private string? client_secret; private string? redirect_uri; - public LoginWebViewPage(AuthenticationStateProvider authStateProvider, NavigatorService navigatorService, IConfiguration configuration) + public LoginWebViewPage(AuthenticationStateProvider authStateProvider, NavigatorService navigatorService, IConfiguration configuration, RestService restService) { this.AuthStateProvider = authStateProvider; this.NavigatorService = navigatorService; this.Configuration = configuration; + this.api = restService; InitializeComponent(); client_id = configuration.GetValue("client_id"); client_secret = configuration.GetValue("client_secret"); @@ -45,10 +47,8 @@ public partial class LoginWebViewPage : ContentPage var query = HttpUtility.ParseQueryString(uri.Query); string? code = query.Get("code"); if (!string.IsNullOrEmpty(code)) { - RestService api = new RestService(); string? token = await api.OAuth(code, client_id, client_secret, redirect_uri); if (!string.IsNullOrEmpty(token)) { - Debug.WriteLine($"Fuck yeah, a token! {token}"); await ((CustomAuthenticationStateProvider)this.AuthStateProvider).Login(token); NavigatorService.NavigationManager.NavigateTo(NavigatorService.NavigationManager.Uri, forceLoad: true); await Shell.Current.GoToAsync(".."); diff --git a/MauiProgram.cs b/MauiProgram.cs index f17e53c..3f05c8f 100644 --- a/MauiProgram.cs +++ b/MauiProgram.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Components.Authorization; +using Markdig; +using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Neighbourhood.omg.lol.Models; @@ -17,6 +18,7 @@ namespace Neighbourhood.omg.lol { builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/Models/NowData.cs b/Models/NowData.cs index e1ac089..3975760 100644 --- a/Models/NowData.cs +++ b/Models/NowData.cs @@ -11,7 +11,7 @@ namespace Neighbourhood.omg.lol.Models { public TimeData Updated { get; set; } public string UpdatedRelative { - get => State.RelativeTimeFromUnix(Convert.ToInt64(Updated.UnixEpochTime)); + get => Utilities.RelativeTimeFromUnix(Convert.ToInt64(Updated.UnixEpochTime)); } } } diff --git a/Models/Pic.cs b/Models/Pic.cs index c0554ce..559d0c3 100644 --- a/Models/Pic.cs +++ b/Models/Pic.cs @@ -17,7 +17,7 @@ namespace Neighbourhood.omg.lol.Models { public long Size { get; set; } public string Mime { get; set; } public string Description { get; set; } - public string DescriptionHtml { get => Description == null ? string.Empty : Markdown.ToHtml(Description); } + public string DescriptionHtml { get => Description == null ? string.Empty : Utilities.MdToHtml(Description); } [JsonPropertyName("exif")] public JsonElement ExifJson { get; set; } diff --git a/Models/State.cs b/Models/State.cs index 97b520a..18b9601 100644 --- a/Models/State.cs +++ b/Models/State.cs @@ -1,32 +1,27 @@ using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Web.Virtualization; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net; -using System.Text; using System.Text.Json; -using System.Threading.Tasks; namespace Neighbourhood.omg.lol.Models { public class State { - - public Page CurrentPage { get; set; } - public AccountResponseData? AccountInfo { get; set; } - public AddressResponseList? AddressList { get; set; } - - public string? Name { get => AccountInfo?.Name; } - public string? Email { get => AccountInfo?.Email; } - public IEnumerable? AddressNames { get => AddressList?.Select(a => a.Address); } - public AddressResponseData? SelectedAddress { get; set; } - public string? SelectedAddressName { get => SelectedAddress?.Address; } - + // Main data lists public List? Statuses { get; set; } public List? Pics { get; set; } public List? NowGarden { get; set; } public List? EphemeralMessages { get; set; } + // Account data + public AccountResponseData? AccountInfo { get; set; } + public AddressResponseList? AddressList { get; set; } + + public string? Name { get => AccountInfo?.Name; } + public string? Email { get => AccountInfo?.Email; } + public IEnumerable? AddressNames { get => AddressList?.Select(a => a.Address); } + + // Selected Address + public AddressResponseData? SelectedAddress { get; set; } + public string? SelectedAddressName { get => SelectedAddress?.Address; } + + // data for selected address public List? CachedAddressStatuses { get; set; } public List? CachedAddressPics { get; set; } public MarkupString? CachedAddressBio { get; set; } @@ -43,8 +38,36 @@ namespace Neighbourhood.omg.lol.Models { } } + // share intent stuff + public event EventHandler? IntentReceived; + private string? _shareString; + public string? ShareString { + get => _shareString; + set { + _shareString = value; + IntentReceived?.Invoke(this, EventArgs.Empty); + } + } + private string? _sharePhoto; + public string? SharePhoto { + get => _sharePhoto; + set { + _sharePhoto = value; + IntentReceived?.Invoke(this, EventArgs.Empty); + } + } + + + + // api service + private RestService api { get; set; } + + public State(RestService restService) { + api = restService; + } + public async Task PopulateAccountDetails(string token) { - RestService api = new RestService(token); + api.AddToken(token); string accountJson = Preferences.Default.Get("accountdetails", string.Empty); string addressJson = Preferences.Default.Get("accountaddresses", string.Empty); @@ -78,19 +101,18 @@ namespace Neighbourhood.omg.lol.Models { AccountInfo = null; AddressList = null; SelectedAddress = null; + api.RemoveToken(); } public async Task GetBio(string address, bool forceRefresh = false) { CachedAddress = address; if (forceRefresh || CachedAddressBio == null) { - RestService api = new RestService(); CachedAddressBio = await api.StatuslogBio(address); } return CachedAddressBio; } public async Task?> GetEphemeralMessages(bool forceRefresh = false) { - RestService api = new RestService(); if(forceRefresh || this.EphemeralMessages == null || this.EphemeralMessages.Count == 0) { this.EphemeralMessages = await api.Ephemeral(); } @@ -98,7 +120,6 @@ namespace Neighbourhood.omg.lol.Models { } public async Task?> GetStatuses(bool forceRefresh = false) { - RestService api = new RestService(); if (forceRefresh || this.Statuses == null || this.Statuses.Count == 0) { this.Statuses = await api.StatuslogLatest(); } @@ -107,7 +128,6 @@ namespace Neighbourhood.omg.lol.Models { public async Task?> GetStatuses(string address, bool forceRefresh = false) { this.CachedAddress = address; - RestService api = new RestService(); if (forceRefresh || this.CachedAddressStatuses == null || this.CachedAddressStatuses.Count == 0) { this.CachedAddressStatuses = await api.Statuslog(address); } @@ -115,7 +135,6 @@ namespace Neighbourhood.omg.lol.Models { } public async Task?> GetNowGarden(bool forceRefresh = false) { - RestService api = new RestService(); if (forceRefresh || this.NowGarden == null || this.NowGarden.Count == 0) { this.NowGarden = await api.NowGarden(); } @@ -124,7 +143,6 @@ namespace Neighbourhood.omg.lol.Models { public async Task?> GetPics(bool forceRefresh = false) { if(forceRefresh || this.Pics == null || this.Pics.Count == 0) { - RestService api = new RestService(); this.Pics = await api.SomePics(); } return this.Pics; @@ -133,53 +151,14 @@ namespace Neighbourhood.omg.lol.Models { public async Task?> GetPics(string address, bool forceRefresh = false) { CachedAddress = address; if (forceRefresh || this.CachedAddressPics == null || this.CachedAddressPics.Count == 0) { - RestService api = new RestService(); CachedAddressPics = (await api.SomePics(address)) ?? new List(); } return CachedAddressPics; } - public async Task FileSize(FileResult file) { - using var fileStream = await file.OpenReadAsync(); - return fileStream.Length; - } - - public async Task Base64FromFile(FileResult file) { - using var memoryStream = new MemoryStream(); - using var fileStream = await file.OpenReadAsync(); - await fileStream.CopyToAsync(memoryStream); - byte[] bytes = memoryStream.ToArray(); - return Convert.ToBase64String(bytes); - } - public async Task RefreshStatuses() => await GetStatuses(forceRefresh: true); public async Task RefreshPics() => await GetPics(forceRefresh: true); public async Task RefreshNow() => await GetNowGarden(forceRefresh: true); - public static string RelativeTimeFromUnix(long unix) { - DateTimeOffset createdTime = DateTimeOffset.UnixEpoch.AddSeconds(unix); - TimeSpan offset = DateTimeOffset.UtcNow - createdTime; - - var offsetString = string.Empty; - if (Math.Floor(offset.TotalDays) == 1) offsetString = $"{Math.Floor(offset.TotalDays)} day ago"; - else if (offset.TotalDays > 1) offsetString = $"{Math.Floor(offset.TotalDays)} days ago"; - else if (Math.Floor(offset.TotalHours) == 1) offsetString = $"{Math.Floor(offset.TotalHours)} hour ago"; - else if (offset.TotalHours > 1) offsetString = $"{Math.Floor(offset.TotalHours)} hours ago"; - else if (Math.Floor(offset.TotalMinutes) == 1) offsetString = $"{Math.Floor(offset.TotalMinutes)} minute ago"; - else if (offset.TotalMinutes > 1) offsetString = $"{Math.Floor(offset.TotalMinutes)} minutes ago"; - else if (Math.Floor(offset.TotalSeconds) == 1) offsetString = $"{Math.Floor(offset.TotalSeconds)} second ago"; - else offsetString = $"{Math.Floor(offset.TotalSeconds)} seconds ago"; - - return offsetString; - } - } - - public enum Page { - None = 0, - Status, - Pics, - Ephemeral, - NowGarden, - Other } } diff --git a/Models/Status.cs b/Models/Status.cs index a42ff8e..5dca643 100644 --- a/Models/Status.cs +++ b/Models/Status.cs @@ -20,11 +20,8 @@ namespace Neighbourhood.omg.lol.Models { } } - public MarkupString HtmlContent { - get { - if(!string.IsNullOrEmpty(RenderedMarkdown)) return (MarkupString)RenderedMarkdown; - else return (MarkupString)Markdown.ToHtml(Content); - } + public MarkupString HtmlContent { + get => Utilities.MdToHtmlMarkup(Content); } public string Url { diff --git a/Models/Utilities.cs b/Models/Utilities.cs new file mode 100644 index 0000000..a5e10e8 --- /dev/null +++ b/Models/Utilities.cs @@ -0,0 +1,51 @@ +using Markdig; +using Microsoft.AspNetCore.Components; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Neighbourhood.omg.lol { + public static class Utilities { + + private static MarkdownPipeline markdownPipeline { get; } + = new MarkdownPipelineBuilder().UseAutoLinks().Build(); + + public static string MdToHtml(string markdown) => + Markdown.ToHtml(markdown, markdownPipeline); + + public static MarkupString MdToHtmlMarkup(string markdown) => + (MarkupString)MdToHtml(markdown); + + public static async Task FileSize(FileResult file) { + using var fileStream = await file.OpenReadAsync(); + return fileStream.Length; + } + + public static async Task Base64FromFile(FileResult file) { + using var memoryStream = new MemoryStream(); + using var fileStream = await file.OpenReadAsync(); + await fileStream.CopyToAsync(memoryStream); + byte[] bytes = memoryStream.ToArray(); + return Convert.ToBase64String(bytes); + } + + public static string RelativeTimeFromUnix(long unix) { + DateTimeOffset createdTime = DateTimeOffset.UnixEpoch.AddSeconds(unix); + TimeSpan offset = DateTimeOffset.UtcNow - createdTime; + + var offsetString = string.Empty; + if (Math.Floor(offset.TotalDays) == 1) offsetString = $"{Math.Floor(offset.TotalDays)} day ago"; + else if (offset.TotalDays > 1) offsetString = $"{Math.Floor(offset.TotalDays)} days ago"; + else if (Math.Floor(offset.TotalHours) == 1) offsetString = $"{Math.Floor(offset.TotalHours)} hour ago"; + else if (offset.TotalHours > 1) offsetString = $"{Math.Floor(offset.TotalHours)} hours ago"; + else if (Math.Floor(offset.TotalMinutes) == 1) offsetString = $"{Math.Floor(offset.TotalMinutes)} minute ago"; + else if (offset.TotalMinutes > 1) offsetString = $"{Math.Floor(offset.TotalMinutes)} minutes ago"; + else if (Math.Floor(offset.TotalSeconds) == 1) offsetString = $"{Math.Floor(offset.TotalSeconds)} second ago"; + else offsetString = $"{Math.Floor(offset.TotalSeconds)} seconds ago"; + + return offsetString; + } + } +} diff --git a/Platforms/Android/MainActivity.cs b/Platforms/Android/MainActivity.cs index 1e3838e..0fcc2b4 100644 --- a/Platforms/Android/MainActivity.cs +++ b/Platforms/Android/MainActivity.cs @@ -1,9 +1,45 @@ -using Android.App; + +using Android.App; +using Android.Content; using Android.Content.PM; using Android.OS; +using Microsoft.Extensions.DependencyInjection; +using Neighbourhood.omg.lol.Models; namespace Neighbourhood.omg.lol { - [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] + [Activity(Theme = "@style/Maui.SplashTheme", LaunchMode = LaunchMode.SingleTop, MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] + [IntentFilter([Intent.ActionSend], Categories = [Intent.CategoryDefault], DataMimeType = "text/plain")] + [IntentFilter([Intent.ActionSend], Categories = [Intent.CategoryDefault], DataMimeType = "*/*")] public class MainActivity : MauiAppCompatActivity { + + protected override void OnCreate(Bundle savedInstanceState) { + base.OnCreate(savedInstanceState); + + // In case the app was opened (on first load) with an `ActionView` intent + OnNewIntent(this.Intent); + } + + + protected override void OnNewIntent(Intent? intent) { + base.OnNewIntent(intent); + if (intent != null && intent.Type != null) { + if (intent.Type.StartsWith("text/")) //string + { + string? shareString = intent.GetStringExtra(Intent.ExtraText); + if (!string.IsNullOrWhiteSpace(shareString)) { + State state = IPlatformApplication.Current!.Services.GetService()!; + state.ShareString = shareString; + } + } + else if (intent.Type.StartsWith("image/")) //image + { + var uri = intent.GetParcelableExtra(Intent.ExtraStream); + } + else if (intent.Type.Equals(Intent.ActionSendMultiple)) //Multiple file + { + var uriList = intent.GetParcelableArrayListExtra(Intent.ExtraStream); + } + } + } } } diff --git a/RestService.cs b/RestService.cs index 33deb3a..4702dda 100644 --- a/RestService.cs +++ b/RestService.cs @@ -21,14 +21,18 @@ namespace Neighbourhood.omg.lol { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, WriteIndented = true }; - addToken(token); + AddToken(token); } - private void addToken(string? token = null) { + public void AddToken(string? token = null) { if (token == null) token = Task.Run(() => SecureStorage.GetAsync("accounttoken")).GetAwaiter().GetResult(); if (token != null) _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); } + public void RemoveToken() { + _client.DefaultRequestHeaders.Remove("Authorization"); + } + private async Task Get(string uri, CancellationToken cancellationToken = default) where T : IOmgLolResponseData { T? responseData = default(T); try { @@ -121,7 +125,7 @@ namespace Neighbourhood.omg.lol { public async Task StatuslogBio(string address) { StatusBioResponseData? responseData = await Get($"/address/{address}/statuses/bio"); - return (MarkupString)Markdown.ToHtml(responseData?.Bio ?? ""); + return Utilities.MdToHtmlMarkup(responseData?.Bio ?? ""); } public async Task AccountInfo() =>