Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/NoteBookmark.Domain/SyncProgressEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;

namespace NoteBookmark.Domain;

public class SyncProgressEventArgs : EventArgs
{
public int Current { get; }
public int Total { get; }
public string Status { get; }
public double Percentage => Total > 0 ? (double)Current / Total * 100 : 0;

public SyncProgressEventArgs(int current, int total, string status)
{
Current = current;
Total = total;
Status = status;
}
}
25 changes: 25 additions & 0 deletions src/NoteBookmark.MauiApp.Tests/SyncServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -444,5 +444,30 @@ public async Task PushPhase_ShouldSyncNoteDirectlyAndClearCreatedOfflineFlag_Whe
_localDataServiceMock.Verify(c => c.SaveNoteAsync(It.Is<Note>(n => !n.CreatedOffline), false), Times.Once);
_localDataServiceMock.Verify(c => c.MarkSyncedAsync("note1", false), Times.Once);
}

[Fact]
public async Task SyncAsync_ShouldRaiseSyncProgressChanged_WhenDownloadingPostHtml()
{
var post1 = new Post { Id = "post1", RowKey = "post1", PartitionKey = "pk", Title = "Post 1", is_read = false };
var post2 = new Post { Id = "post2", RowKey = "post2", PartitionKey = "pk", Title = "Post 2", is_read = false };

_localDataServiceMock.Setup(c => c.GetPendingSyncNotesAsync()).ReturnsAsync(new List<Note>());
_localDataServiceMock.Setup(c => c.GetPostsAsync()).ReturnsAsync(new List<Post> { post1, post2 });
_apiClientMock.Setup(c => c.GetPostsModifiedAfter(It.IsAny<DateTime>())).ReturnsAsync(new List<PostL>());
_apiClientMock.Setup(c => c.GetNotesModifiedAfter(It.IsAny<DateTime>())).ReturnsAsync(new List<Note>());
_apiClientMock.Setup(c => c.GetPostHtmlAsync(It.IsAny<string>())).ReturnsAsync("<html>Post content</html>");

_localHtmlStorageServiceMock.Setup(s => s.GetCachedPostIds()).Returns(new List<string>());
_localHtmlStorageServiceMock.Setup(s => s.IsPostHtmlCached(It.IsAny<string>())).Returns(false);

var progressEvents = new List<SyncProgressEventArgs>();
_sut.SyncProgressChanged += (sender, args) => progressEvents.Add(args);

await _sut.SyncAsync();

progressEvents.Should().NotBeEmpty();
progressEvents.Should().Contain(e => e.Status.Contains("Downloading offline text"));
progressEvents.Last().Status.Should().Be("Synchronization complete!");
}
}

5 changes: 5 additions & 0 deletions src/NoteBookmark.MauiApp/Data/OfflineDataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,11 @@ public async Task<bool> DeletePost(string id)
=> localHtmlStorageService.GetPostHtmlAsync(postId);

public Task SyncAsync() => syncService.SyncAsync();
public event EventHandler<SyncProgressEventArgs>? SyncProgressChanged
{
add => syncService.SyncProgressChanged += value;
remove => syncService.SyncProgressChanged -= value;
}
public bool IsOffline => connectivity.NetworkAccess != NetworkAccess.Internet;
public bool CanSync => true;

Expand Down
41 changes: 30 additions & 11 deletions src/NoteBookmark.MauiApp/Data/SyncService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public interface ISyncService
Task SyncAsync();
bool IsSyncing { get; }
event EventHandler<SyncConflictEventArgs>? ConflictDetected;
event EventHandler<SyncProgressEventArgs>? SyncProgressChanged;
}

public class SyncService(
Expand All @@ -30,6 +31,7 @@ public class SyncService(

public bool IsSyncing => _isSyncing;
public event EventHandler<SyncConflictEventArgs>? ConflictDetected;
public event EventHandler<SyncProgressEventArgs>? SyncProgressChanged;

public async Task SyncAsync()
{
Expand All @@ -38,18 +40,24 @@ public async Task SyncAsync()
_isSyncing = true;
try
{
SyncProgressChanged?.Invoke(this, new SyncProgressEventArgs(0, 0, "Starting synchronization..."));
var lastSyncStr = await GetPreferenceAsync(LastSyncTimestampKey);
DateTime? lastSync = null;
if (!string.IsNullOrEmpty(lastSyncStr) && DateTime.TryParse(lastSyncStr, out var parsed))
{
lastSync = parsed.ToUniversalTime();
}

SyncProgressChanged?.Invoke(this, new SyncProgressEventArgs(0, 0, "Pushing local changes..."));
await PushAsync(lastSync);

SyncProgressChanged?.Invoke(this, new SyncProgressEventArgs(0, 0, "Pulling remote changes..."));
await PullAsync(lastSync);

await SyncHtmlAsync();

await SetPreferenceAsync(LastSyncTimestampKey, DateTime.UtcNow.ToString("O"));
SyncProgressChanged?.Invoke(this, new SyncProgressEventArgs(0, 0, "Synchronization complete!"));
}
finally
{
Expand Down Expand Up @@ -245,22 +253,33 @@ private async Task SyncHtmlAsync()
}

// Download HTML for unread posts not yet cached
foreach (var post in posts.Where(p => p.is_read != true))
var unreadToDownload = posts.Where(p => p.is_read != true && !localHtmlStorageService.IsPostHtmlCached(p.Id ?? p.RowKey)).ToList();
int total = unreadToDownload.Count;

if (total > 0)
{
var id = post.Id ?? post.RowKey;
if (localHtmlStorageService.IsPostHtmlCached(id)) continue;
SyncProgressChanged?.Invoke(this, new SyncProgressEventArgs(0, total, $"Downloading offline text (0/{total})..."));

try
for (int i = 0; i < unreadToDownload.Count; i++)
{
var html = await apiClient.GetPostHtmlAsync(id);
if (html != null)
var post = unreadToDownload[i];
var id = post.Id ?? post.RowKey;

try
{
await localHtmlStorageService.SavePostHtmlAsync(id, html);
var html = await apiClient.GetPostHtmlAsync(id);
if (html != null)
{
await localHtmlStorageService.SavePostHtmlAsync(id, html);
}
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to download HTML for post {PostId}", id);
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to download HTML for post {PostId}", id);
}

int current = i + 1;
SyncProgressChanged?.Invoke(this, new SyncProgressEventArgs(current, total, $"Downloading offline text ({current}/{total})..."));
}
}
}
Expand Down
31 changes: 31 additions & 0 deletions src/NoteBookmark.SharedUI/Components/Pages/Posts.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
@inject IDialogService DialogService
@inject NavigationManager Navigation
@inject ILocalHtmlCache localHtmlCache
@implements IDisposable

<PageTitle>Posts</PageTitle>

Expand All @@ -23,6 +24,16 @@
<FluentButton OnClick="SyncNow" IconStart="@(new Icons.Regular.Size20.ArrowSync())" Loading="isSyncing" Title="Sync posts and comments">Sync</FluentButton>
}
</FluentStack>
@if (isSyncing || !string.IsNullOrEmpty(syncProgressStatus))
{
<FluentStack Orientation="Orientation.Vertical" Style="gap: 0.25rem; margin: 0.25rem 0;">
<FluentProgress Min="0" Max="@(syncProgressTotal > 0 ? syncProgressTotal : 100)" Value="@(syncProgressTotal > 0 ? syncProgressCurrent : null)" />
@if (!string.IsNullOrEmpty(syncProgressStatus))
{
<span style="font-size: 0.85rem; color: var(--neutral-foreground-rest);">@syncProgressStatus</span>
}
</FluentStack>
}
<FluentSwitch ValueChanged="OnShowReadChanged" Label="Show">
<span slot="checked-message">Read Only</span>
<span slot="unchecked-message">UnRead Only</span>
Expand Down Expand Up @@ -84,9 +95,13 @@
private PaginationState pagination = new PaginationState { ItemsPerPage = 20 };
private string titleFilter = string.Empty;
private bool showPublishedDate = false;
private int syncProgressCurrent = 0;
private int syncProgressTotal = 0;
private string syncProgressStatus = string.Empty;

protected override async Task OnInitializedAsync()
{
client.SyncProgressChanged += OnSyncProgressChanged;
await LoadPosts();
_ = StartBackgroundSync();
}
Expand Down Expand Up @@ -284,4 +299,20 @@
}

private void ReadPost(string postId) => Navigation.NavigateTo($"postreader/{postId}");

private void OnSyncProgressChanged(object? sender, SyncProgressEventArgs e)
{
InvokeAsync(() =>
{
syncProgressCurrent = e.Current;
syncProgressTotal = e.Total;
syncProgressStatus = e.Status;
StateHasChanged();
});
}

public void Dispose()
{
client.SyncProgressChanged -= OnSyncProgressChanged;
}
}
1 change: 1 addition & 0 deletions src/NoteBookmark.SharedUI/IDataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public interface IDataService
Task<bool> SaveReadingNotesMarkdown(string markdown, string number);
Task<string?> GetPostHtmlAsync(string postId);
Task SyncAsync();
event System.EventHandler<SyncProgressEventArgs>? SyncProgressChanged;
bool IsOffline { get; }
bool CanSync { get; }
}
1 change: 1 addition & 0 deletions src/NoteBookmark.SharedUI/PostNoteClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ public async Task<List<Note>> GetNotesModifiedAfter(DateTime modifiedAfter)
}

public Task SyncAsync() => Task.CompletedTask;
public event EventHandler<SyncProgressEventArgs>? SyncProgressChanged { add { } remove { } }
public bool IsOffline => false;
public bool CanSync => false;
}
Loading