diff --git a/App.xaml.cs b/App.xaml.cs index fb489ea..0dac48c 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -3,7 +3,11 @@ public App(NavigatorService navigatorService) { InitializeComponent(); +#if WINDOWS + MainPage = new WindowsAppShell(); +#else MainPage = new AppShell(); +#endif NavigatorService = navigatorService; } diff --git a/Components/Layout/MainLayout.razor b/Components/Layout/MainLayout.razor index dcc75dd..2e3ac0b 100644 --- a/Components/Layout/MainLayout.razor +++ b/Components/Layout/MainLayout.razor @@ -2,13 +2,83 @@ @implements IDisposable @inject NavigatorService NavigatorService @inject NavigationManager NavigationManager +@inject IJSRuntime JS @inject State State +
@Body
+ + @code { + + private DotNetObjectReference? DotNetRef { get; set; } + protected override void OnAfterRender(bool firstRender) { + base.OnAfterRender(firstRender); + if (firstRender) { + // See warning about memory above in the article + DotNetRef = DotNetObjectReference.Create(State); + JS.InvokeVoidAsync("injectCSharp", DotNetRef); + } + } + protected override void OnInitialized() { base.OnInitialized(); NavigatorService.NavigationManager = NavigationManager; @@ -25,11 +95,12 @@ } else if (!string.IsNullOrEmpty(State.SharePhoto)) { NavigationManager.NavigateTo($"/sharepic/{State.SharePhoto}"); - State.ShareString = null; + State.SharePhoto = null; } } void IDisposable.Dispose() { State.IntentReceived -= IntentRecieved; + DotNetRef?.Dispose(); } } diff --git a/Components/Pages/Person.razor b/Components/Pages/Person.razor index 9c46b7f..d39e021 100644 --- a/Components/Pages/Person.razor +++ b/Components/Pages/Person.razor @@ -3,6 +3,7 @@ @inject IJSRuntime JS @inject NavigationManager Nav +

@Address

@@ -51,7 +52,7 @@ }
- + @if(Address == State.SelectedAddressName) { + +@code { + protected override async Task OnInitializedAsync() { + await base.OnInitializedAsync(); + State.PropertyChanged += StateChanged; + } + + private async void StateChanged(object? sender, PropertyChangedEventArgs e) { + if(e.PropertyName == nameof(State.IsRefreshing)) { + await InvokeAsync(StateHasChanged); + } + } +} diff --git a/Components/StatusList.razor b/Components/StatusList.razor index e18035f..68efaf5 100644 --- a/Components/StatusList.razor +++ b/Components/StatusList.razor @@ -1,6 +1,8 @@ @inject IJSRuntime JS @inject State State +@implements IDisposable + @if (Editable) { } @@ -13,18 +15,60 @@ @code { [Parameter] - public Func?>> StatusFunc { get; set; } + public Func?>> StatusFunc { get; set; } [Parameter] public bool Editable { get; set; } = false; public EditStatusDialog? Dialog { get; set; } - + private List? statuses; + private int count { get; set; } = 0; + private int pageSize { get; } = 50; protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - if (statuses == null || statuses.Count == 0) statuses = await StatusFunc(); + State.PropertyChanged += StateChanged; + State.CanRefresh = true; + + if (statuses == null || statuses.Count == 0) await LoadNext(); await InvokeAsync(StateHasChanged); await JS.InvokeVoidAsync("removeElementById", "statusLoading"); } + + public async Task PullUp() { + int countBefore = count; + await LoadNext(); + return (count != countBefore); + } + + public async Task LoadNext(bool forceRefresh = false) { + if (statuses == null) statuses = new List(); + if (forceRefresh) { + count = 0; + statuses.Clear(); + } + + var allStatuses = await StatusFunc(forceRefresh); + if (allStatuses != null && count < allStatuses.Count) { + statuses.AddRange(allStatuses.Skip(count).Take(pageSize)); + count = statuses.Count; + } + await InvokeAsync(StateHasChanged); + } + + private async void StateChanged(object? sender, PropertyChangedEventArgs e) { + if (e.PropertyName == nameof(State.IsRefreshing) && State.IsRefreshing) { + await LoadNext(true); + State.IsRefreshing = false; + } + + if(e.PropertyName == nameof(State.AtBottom) && State.AtBottom) { + await LoadNext(); + } + } + + public void Dispose() { + State.PropertyChanged -= StateChanged; + State.CanRefresh = false; + } } diff --git a/Components/_Imports.razor b/Components/_Imports.razor index 8ecaf89..f83b24c 100644 --- a/Components/_Imports.razor +++ b/Components/_Imports.razor @@ -1,4 +1,5 @@ -@using System.Net.Http +@using System.ComponentModel +@using System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @@ -11,3 +12,4 @@ @using Neighbourhood.omg.lol.Components @using Neighbourhood.omg.lol.Models @using Markdig +@using BcdLib.Components \ No newline at end of file diff --git a/MainPage.xaml b/MainPage.xaml index 9cfbe95..2ab4ac7 100644 --- a/MainPage.xaml +++ b/MainPage.xaml @@ -2,13 +2,17 @@ - - - - + + + + + + diff --git a/MainPage.xaml.cs b/MainPage.xaml.cs index 5df0b19..eec331f 100644 --- a/MainPage.xaml.cs +++ b/MainPage.xaml.cs @@ -1,13 +1,26 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.WebView; using Microsoft.AspNetCore.Components.WebView.Maui; +using Neighbourhood.omg.lol.Models; +using System.ComponentModel; using System.Diagnostics; namespace Neighbourhood.omg.lol { public partial class MainPage : ContentPage { + private State State { get; set; } + public MainPage() { InitializeComponent(); + State = IPlatformApplication.Current!.Services.GetService()!; + BindingContext = State; + State.PropertyChanged += State_PropertyChanged; + } + + private void State_PropertyChanged(object? sender, PropertyChangedEventArgs e) { + if (e.PropertyName == nameof(State.CanRefresh) || e.PropertyName == nameof(State.AtTop)) { + refreshView.IsEnabled = State.CanRefresh && State.AtTop; + } } private void BlazorUrlLoading(object? sender, UrlLoadingEventArgs e) { diff --git a/MauiProgram.cs b/MauiProgram.cs index 3f05c8f..a5cc734 100644 --- a/MauiProgram.cs +++ b/MauiProgram.cs @@ -1,4 +1,5 @@ -using Markdig; +using BcdLib.Components; +using Markdig; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -15,6 +16,7 @@ namespace Neighbourhood.omg.lol { }); builder.Services.AddMauiBlazorWebView(); + builder.Services.AddBcdLibPullComponent(); builder.Services.AddTransient(); builder.Services.AddTransient(); diff --git a/Models/State.cs b/Models/State.cs index 18b9601..0cae968 100644 --- a/Models/State.cs +++ b/Models/State.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using System.ComponentModel; using System.Text.Json; namespace Neighbourhood.omg.lol.Models { - public class State { + public class State : INotifyPropertyChanged { // Main data lists public List? Statuses { get; set; } public List? Pics { get; set; } @@ -57,7 +59,47 @@ namespace Neighbourhood.omg.lol.Models { } } + // refreshing + public event PropertyChangedEventHandler? PropertyChanged; + private bool _isRefreshing; + public bool IsRefreshing { + get => _isRefreshing; + set { + _isRefreshing = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRefreshing))); + } + } + private bool _canRefresh; + public bool CanRefresh { + get => _canRefresh; + set { + _canRefresh = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanRefresh))); + } + } + + private bool _atTop; + public bool AtTop { + get => _atTop; + set { + if (_atTop != value) { + _atTop = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AtTop))); + } + } + } + + private bool _atBottom; + public bool AtBottom { + get => _atBottom; + set { + if (_atBottom != value) { + _atBottom = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AtBottom))); + } + } + } // api service private RestService api { get; set; } @@ -160,5 +202,18 @@ namespace Neighbourhood.omg.lol.Models { public async Task RefreshPics() => await GetPics(forceRefresh: true); public async Task RefreshNow() => await GetNowGarden(forceRefresh: true); + + + [JSInvokable("SetAtTop")] + public void SetAtTop(bool atTop) { + if(AtTop != atTop) AtTop = atTop; + //if(CanRefresh != atTop) CanRefresh = atTop; + } + + [JSInvokable("SetAtBottom")] + public void SetAtBottom(bool atBottom) { + if(AtBottom != atBottom) AtBottom = atBottom; + } + } } diff --git a/Neighbourhood.omg.lol.csproj b/Neighbourhood.omg.lol.csproj index 223b2c0..3e23625 100644 --- a/Neighbourhood.omg.lol.csproj +++ b/Neighbourhood.omg.lol.csproj @@ -196,6 +196,7 @@ + @@ -219,6 +220,12 @@ MSBuild:Compile + + MSBuild:Compile + + + MSBuild:Compile + diff --git a/WindowsAppShell.xaml b/WindowsAppShell.xaml new file mode 100644 index 0000000..2c71b7a --- /dev/null +++ b/WindowsAppShell.xaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/WindowsAppShell.xaml.cs b/WindowsAppShell.xaml.cs new file mode 100644 index 0000000..7f4b9f8 --- /dev/null +++ b/WindowsAppShell.xaml.cs @@ -0,0 +1,10 @@ +namespace Neighbourhood.omg.lol; + +public partial class WindowsAppShell : Shell { + public WindowsAppShell() { + InitializeComponent(); + + Routing.RegisterRoute(nameof(LoginWebViewPage), typeof(LoginWebViewPage)); + Routing.RegisterRoute(nameof(EphemeralWebPage), typeof(EphemeralWebPage)); + } +} \ No newline at end of file diff --git a/WindowsMainPage.xaml b/WindowsMainPage.xaml new file mode 100644 index 0000000..4b30dc1 --- /dev/null +++ b/WindowsMainPage.xaml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/WindowsMainPage.xaml.cs b/WindowsMainPage.xaml.cs new file mode 100644 index 0000000..1cafc7e --- /dev/null +++ b/WindowsMainPage.xaml.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.WebView; +using Microsoft.AspNetCore.Components.WebView.Maui; +using Neighbourhood.omg.lol.Models; +using System.ComponentModel; +using System.Diagnostics; + +namespace Neighbourhood.omg.lol { + public partial class WindowsMainPage : ContentPage { + + private State State { get; set; } + + public WindowsMainPage() { + InitializeComponent(); + } + + private void BlazorUrlLoading(object? sender, UrlLoadingEventArgs e) { + if (e.Url.Host == "home.omg.lol" && e.Url.AbsolutePath == "/oauth/authorize") { + e.UrlLoadingStrategy = UrlLoadingStrategy.CancelLoad; + Shell.Current.GoToAsync(nameof(LoginWebViewPage)); + } + } + } +} diff --git a/wwwroot/css/style.css b/wwwroot/css/style.css index 4f3a5c9..8406b9f 100644 --- a/wwwroot/css/style.css +++ b/wwwroot/css/style.css @@ -395,4 +395,45 @@ a.row.indent { :is(button,.button,.chip).large.small > .responsive { inline-size: 3rem; } +} + +[class|="fa"].animated { + animation: rotating 1s ease-in-out infinite; +} + +@keyframes rotating { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +.pull-container { + display: flex; + flex-direction: column; + flex-grow: 1; +} +.pull-container > .pull-wrapper +{ + position: relative; +} + +.pull-tip { + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + height: var(--pull-refresh-head-height, 50px); + width: 100%; +} + +.pull-down-tip { + position: absolute; + transform: translateY(-100%); +} + +.pull-up-tip { } \ No newline at end of file diff --git a/wwwroot/js/csharp.js b/wwwroot/js/csharp.js index 544181e..c371ad2 100644 --- a/wwwroot/js/csharp.js +++ b/wwwroot/js/csharp.js @@ -1,5 +1,6 @@ window.injectCSharp = async function (helper) { window.CSHARP = helper + console.info("Recieved DotNetReference", window.CSHARP) } @@ -11,4 +12,8 @@ async function delay(t) { async function removeElementById(id) { document.getElementById(id)?.remove() +} + +function Dispose(obj) { + if (obj && typeof obj.Dispose === "function") obj.Dispose() } \ No newline at end of file