Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a970e5a
fix(#173): use Id ?? RowKey for checking and opening offline post HTM…
fboucher-os Aug 8, 2026
a5d25a2
feat(#176): add sync progress bar and status updates for downloading …
fboucher-os Aug 8, 2026
2c97bd1
should have done that long ago
fboucher-os Aug 11, 2026
2677eb1
fix: resolve Android MAUI app crash when opening URLs (issue #177) (#…
fboucher-os Aug 13, 2026
1acc7d6
feat: add up/down reordering for categories and notes in summary edit…
fboucher-os Aug 15, 2026
007a4d4
feat: add text size slider and fix text wrapping on offline reader pa…
fboucher-os Aug 15, 2026
5077f44
bumb version
fboucher-os Aug 15, 2026
ff8ecd1
feat: implement async background post content extraction to save HTML…
fboucher Aug 15, 2026
aaa3880
feat: improve sync html download, always display read post button, an…
fboucher-os Aug 15, 2026
369139c
feat: configure protected Azure Function Parser API and key propagati…
fboucher Aug 15, 2026
5a65b09
Merge branch 'main' into feature/resolve-pr-188-conflicts
fboucher-os Aug 15, 2026
c8afa57
update missing env variable
fboucher-os Aug 16, 2026
7726e97
fix
fboucher-os Aug 16, 2026
6018801
chore: bump version to 1.4.1
fboucher-os Aug 16, 2026
1b28a01
Merge branch 'main' into v-next
fboucher Aug 16, 2026
be83109
fix(#190): update actions/cache to v5 for Node.js 24 compatibility in…
fboucher-os Sep 5, 2026
ae568c8
Update version to 1.4.2, add sync progress tracking, and reader page …
fboucher-os Sep 5, 2026
322848d
ci: include version in android apk filename (#193)
fboucher Sep 5, 2026
6c944fa
Merge branch 'main' into v-next
fboucher Sep 5, 2026
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
27 changes: 27 additions & 0 deletions .github/workflows/build-android-apk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,33 @@ jobs:
ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_KEYSTORE_ALIAS }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}

- name: Rename APK with version
run: |
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
VERSION="${{ github.ref_name }}"
else
VERSION_NUM=$(grep -oPm1 "(?<=<ApplicationDisplayVersion>)[^<]+" src/NoteBookmark.MauiApp/NoteBookmark.MauiApp.csproj || true)
if [ -z "$VERSION_NUM" ]; then
VERSION_NUM=$(grep -oPm1 "(?<=<Version>)[^<]+" Directory.Build.props)
fi
VERSION="v${VERSION_NUM#v}"
fi
echo "Using version: $VERSION"

PUBLISH_DIR="src/NoteBookmark.MauiApp/bin/Release/net10.0-android/publish"
for apk in "$PUBLISH_DIR"/*.apk; do
if [ -f "$apk" ]; then
filename=$(basename "$apk")
if [[ "$filename" == *"-Signed.apk" ]]; then
new_filename="${filename%-Signed.apk}-${VERSION}-Signed.apk"
else
new_filename="${filename%.apk}-${VERSION}.apk"
fi
echo "Renaming $apk to $PUBLISH_DIR/$new_filename"
mv "$apk" "$PUBLISH_DIR/$new_filename"
fi
done

- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
Expand Down
124 changes: 72 additions & 52 deletions .github/workflows/opencode-triage.yml
Original file line number Diff line number Diff line change
@@ -1,52 +1,72 @@
name: OpenCode Daily Triage

on:
schedule:
- cron: "0 6 * * *" # 1:00 AM EST (6:00 AM UTC) daily
workflow_dispatch:

jobs:
triage:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
issues: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- uses: anomalyco/opencode/github@latest
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
model: opencode/big-pickle
use_github_token: true
prompt: |
You are a triage agent. Triaging open GitHub issues in this repo.

1. Use `gh issue list --state open --json number,title,body,labels,createdAt` to list open issues.
2. Skip any issue that already has a state label (ready-for-agent, ready-for-human, go:needs-research, go:no, wontfix).
3. For each unlabeled issue, read it with `gh issue view <number>`.
4. Classify as `type:bug` or `type:feature`.
5. Assign a state:
- `go:needs-research` — needs more info or investigation
- `ready-for-agent` — well-specified, agent-implementable
- `ready-for-human` — needs human judgment or design decisions
- `wontfix` — out of scope or not actionable (close it)
6. Apply labels with `gh issue edit <number> --add-label`.
7. Post a triage comment with `gh issue comment <number>`. Every comment MUST start with:
> *This was generated by AI during triage.*

For `go:needs-research`: post "Triage Notes" with what you established and specific questions for the reporter.
For `ready-for-agent`: post an "Agent Brief" with category, summary, current/desired behavior, key interfaces, acceptance criteria, and out-of-scope.
For `ready-for-human`: same as agent brief but note why it can't be delegated.
For `wontfix`: post a polite explanation and close with `gh issue close <number> --reason not-planned`.

Rules:
- Never re-triage an issue that already has a state label.
- Never modify closed issues.
- When in doubt, use `go:needs-research`.
- Keep briefs behavioral (what, not how). No file paths or line numbers.
name: OpenCode Daily Triage

on:
schedule:
- cron: "0 6 * * *" # 1:00 AM EST (6:00 AM UTC) daily
workflow_dispatch:

jobs:
triage:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
issues: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Get opencode version
id: version
run: |
VERSION=$(curl -sf https://api.github.com/repos/anomalyco/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
echo "version=${VERSION:-latest}" >> $GITHUB_OUTPUT

- name: Cache opencode
id: cache
uses: actions/cache@v5
with:
path: ~/.opencode/bin
key: opencode-${{ runner.os }}-${{ runner.arch }}-${{ steps.version.outputs.version }}

- name: Install opencode
if: steps.cache.outputs.cache-hit != 'true'
run: curl -fsSL https://opencode.ai/install | bash

- name: Add opencode to PATH
run: echo "$HOME/.opencode/bin" >> $GITHUB_PATH

- name: Run opencode
run: opencode github run
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MODEL: opencode/big-pickle
USE_GITHUB_TOKEN: "true"
PROMPT: |
You are a triage agent. Triaging open GitHub issues in this repo.

1. Use `gh issue list --state open --json number,title,body,labels,createdAt` to list open issues.
2. Skip any issue that already has a state label (ready-for-agent, ready-for-human, go:needs-research, go:no, wontfix).
3. For each unlabeled issue, read it with `gh issue view <number>`.
4. Classify as `type:bug` or `type:feature`.
5. Assign a state:
- `go:needs-research` — needs more info or investigation
- `ready-for-agent` — well-specified, agent-implementable
- `ready-for-human` — needs human judgment or design decisions
- `wontfix` — out of scope or not actionable (close it)
6. Apply labels with `gh issue edit <number> --add-label`.
7. Post a triage comment with `gh issue comment <number>`. Every comment MUST start with:
> *This was generated by AI during triage.*

For `go:needs-research`: post "Triage Notes" with what you established and specific questions for the reporter.
For `ready-for-agent`: post an "Agent Brief" with category, summary, current/desired behavior, key interfaces, acceptance criteria, and out-of-scope.
For `ready-for-human`: same as agent brief but note why it can't be delegated.
For `wontfix`: post a polite explanation and close with `gh issue close <number> --reason not-planned`.

Rules:
- Never re-triage an issue that already has a state label.
- Never modify closed issues.
- When in doubt, use `go:needs-research`.
- Keep briefs behavioral (what, not how). No file paths or line numbers.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>1.4.1</Version>
<Version>1.4.2</Version>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand Down
2 changes: 1 addition & 1 deletion docs/maui-android-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This guide walks you through installing (often called "sideloading") the NoteBoo
2. Tap on the **Actions** tab at the top.
3. Select the latest successful **Build Android APK** run.
4. Scroll down to the **Artifacts** section and download the **`notebookmark-android-apk`** ZIP file.
5. Extract the ZIP file to retrieve the **`.apk`** installer file (e.g., `NoteBookmark.MauiApp.apk`).
5. Extract the ZIP file to retrieve the **`.apk`** installer file (e.g., `c5m.notebookmark.mauiapp-v1.4.2-Signed.apk`).

---

Expand Down
34 changes: 27 additions & 7 deletions src/NoteBookmark.BlazorApp.Tests/Tests/PostReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public PostReaderTests()
}

[Fact]
public void PostReader_RendersTitleAndContentAndSlider()
public void PostReader_RendersTitleAndContentAndSlidersAndBackButtonsAtTopAndBottom()
{
var cut = Render<PostReader>(ps => ps.Add(p => p.PostId, "p1"));

Expand All @@ -44,23 +44,43 @@ public void PostReader_RendersTitleAndContentAndSlider()
cut.Markup.Should().Contain("reader-content");
cut.Markup.Should().Contain("Text size:");

var slider = cut.FindComponent<FluentSlider<int>>();
slider.Instance.Min.Should().Be(8);
slider.Instance.Max.Should().Be(56);
var sliders = cut.FindComponents<FluentSlider<int>>();
sliders.Should().HaveCount(2);
sliders[0].Instance.Min.Should().Be(8);
sliders[0].Instance.Max.Should().Be(56);
sliders[1].Instance.Min.Should().Be(8);
sliders[1].Instance.Max.Should().Be(56);

var backButtons = cut.FindComponents<FluentButton>()
.Where(b => b.Instance.Title == "Back to posts")
.ToList();
backButtons.Should().HaveCount(2);
}

[Fact]
public void PostReader_SliderValueChange_UpdatesContentFontSize()
public void PostReader_TopSliderValueChange_UpdatesContentFontSize()
{
var cut = Render<PostReader>(ps => ps.Add(p => p.PostId, "p1"));

var contentDivBefore = cut.Find("div.reader-content");
contentDivBefore.GetAttribute("style").Should().Contain("font-size: 16px;");

var slider = cut.FindComponent<FluentSlider<int>>();
cut.InvokeAsync(() => slider.Instance.ValueChanged.InvokeAsync(24));
var sliders = cut.FindComponents<FluentSlider<int>>();
cut.InvokeAsync(() => sliders[0].Instance.ValueChanged.InvokeAsync(24));

var contentDivAfter = cut.Find("div.reader-content");
contentDivAfter.GetAttribute("style").Should().Contain("font-size: 24px;");
}

[Fact]
public void PostReader_BottomSliderValueChange_UpdatesContentFontSize()
{
var cut = Render<PostReader>(ps => ps.Add(p => p.PostId, "p1"));

var sliders = cut.FindComponents<FluentSlider<int>>();
cut.InvokeAsync(() => sliders[1].Instance.ValueChanged.InvokeAsync(20));

var contentDivAfter = cut.Find("div.reader-content");
contentDivAfter.GetAttribute("style").Should().Contain("font-size: 20px;");
}
}
29 changes: 29 additions & 0 deletions src/NoteBookmark.BlazorApp.Tests/Tests/PostsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,34 @@ public void Posts_ReadPostButton_IsAlwaysRendered()

cut.Markup.Should().Contain("Read post");
}

[Fact]
public void Posts_DisplaysSyncProgress_WhenSyncProgressChangedFired()
{
var cut = Render<Posts>();

cut.InvokeAsync(() =>
{
_dataServiceMock.Raise(s => s.SyncProgressChanged += null, new SyncProgressEventArgs(1, 6, "Downloading 1 of 6 posts..."));
});

cut.Markup.Should().Contain("Downloading 1 of 6 posts...");
var progress = cut.FindComponent<FluentProgress>();
progress.Instance.Value.Should().Be(1);
progress.Instance.Max.Should().Be(6);
}

[Fact]
public void Posts_DisplaysCleaningStatus_WhenSyncProgressChangedFired()
{
var cut = Render<Posts>();

cut.InvokeAsync(() =>
{
_dataServiceMock.Raise(s => s.SyncProgressChanged += null, new SyncProgressEventArgs(0, 0, "Cleaning..."));
});

cut.Markup.Should().Contain("Cleaning...");
}
}

4 changes: 3 additions & 1 deletion src/NoteBookmark.MauiApp.Tests/SyncServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,9 @@ public async Task SyncAsync_ShouldRaiseSyncProgressChanged_WhenDownloadingPostHt
await _sut.SyncAsync();

progressEvents.Should().NotBeEmpty();
progressEvents.Should().Contain(e => e.Status.Contains("Downloading offline text"));
progressEvents.Should().Contain(e => e.Status == "Cleaning...");
progressEvents.Should().Contain(e => e.Status == "Downloading 1 of 2 posts..." && e.Current == 1 && e.Total == 2);
progressEvents.Should().Contain(e => e.Status == "Downloading 2 of 2 posts..." && e.Current == 2 && e.Total == 2);
progressEvents.Last().Status.Should().Be("Synchronization complete!");
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/NoteBookmark.MauiApp/Data/SyncService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ private async Task SyncHtmlAsync()
var postMap = posts.ToDictionary(p => p.Id ?? p.RowKey);
var cachedIds = localHtmlStorageService.GetCachedPostIds().ToHashSet();

SyncProgressChanged?.Invoke(this, new SyncProgressEventArgs(0, 0, "Cleaning..."));

// Prune cached HTML for posts that are read or no longer exist
foreach (var cachedId in cachedIds)
{
Expand All @@ -258,7 +260,7 @@ private async Task SyncHtmlAsync()

if (total > 0)
{
SyncProgressChanged?.Invoke(this, new SyncProgressEventArgs(0, total, $"Downloading offline text (0/{total})..."));
SyncProgressChanged?.Invoke(this, new SyncProgressEventArgs(0, total, $"Downloading 0 of {total} posts..."));

for (int i = 0; i < unreadToDownload.Count; i++)
{
Expand All @@ -279,7 +281,7 @@ private async Task SyncHtmlAsync()
}

int current = i + 1;
SyncProgressChanged?.Invoke(this, new SyncProgressEventArgs(current, total, $"Downloading offline text ({current}/{total})..."));
SyncProgressChanged?.Invoke(this, new SyncProgressEventArgs(current, total, $"Downloading {current} of {total} posts..."));
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/NoteBookmark.MauiApp/NoteBookmark.MauiApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@
<ApplicationId>c5m.notebookmark.mauiapp</ApplicationId>

<!-- Versions -->
<ApplicationDisplayVersion>1.3.3</ApplicationDisplayVersion>
<ApplicationVersion>6</ApplicationVersion>
<Version>1.3.3</Version>
<ApplicationDisplayVersion>1.4.2</ApplicationDisplayVersion>
<ApplicationVersion>7</ApplicationVersion>
<Version>1.4.2</Version>

<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType>
Expand Down
18 changes: 14 additions & 4 deletions src/NoteBookmark.SharedUI/Components/Pages/PostReader.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@
<PageTitle>@(post?.Title ?? "Reading...")</PageTitle>

<FluentStack Orientation="Orientation.Vertical" Style="width: 100%; max-width: 800px; margin: 0 auto; padding: 1rem; box-sizing: border-box;">
<FluentStack Orientation="Orientation.Horizontal">
<FluentButton OnClick="@(() => Navigation.NavigateTo("/posts"))"
IconStart="@(new Icons.Regular.Size20.ArrowLeft())"
Title="Back to posts">Back</FluentButton>
<FluentStack Orientation="Orientation.Vertical" Style="width: 100%; gap: 0.5rem; margin-bottom: 1rem;">
<FluentStack Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
<FluentButton OnClick="@(() => Navigation.NavigateTo("/posts"))"
IconStart="@(new Icons.Regular.Size20.ArrowLeft())"
Title="Back to posts">Back</FluentButton>
<FluentSpacer />
<span style="font-weight: 600;">Text size:</span>
<span>@(textSize)px</span>
</FluentStack>
<FluentSlider Min="8" Max="56" Step="1" @bind-Value="textSize" Style="width: 100%;" aria-label="Text size slider" />
</FluentStack>

@if (isLoading)
Expand Down Expand Up @@ -51,6 +57,10 @@

<FluentStack Orientation="Orientation.Vertical" Style="width: 100%; gap: 0.5rem; margin-top: 1rem;">
<FluentStack Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
<FluentButton OnClick="@(() => Navigation.NavigateTo("/posts"))"
IconStart="@(new Icons.Regular.Size20.ArrowLeft())"
Title="Back to posts">Back</FluentButton>
<FluentSpacer />
<span style="font-weight: 600;">Text size:</span>
<span>@(textSize)px</span>
</FluentStack>
Expand Down
14 changes: 13 additions & 1 deletion src/NoteBookmark.SharedUI/Components/Pages/Posts.razor
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,22 @@

try
{
isSyncing = true;
await client.SyncAsync();
await LoadPosts();
StateHasChanged();
}
catch (Exception)
{
// Ignore background sync errors
}
finally
{
isSyncing = false;
syncProgressStatus = string.Empty;
syncProgressCurrent = 0;
syncProgressTotal = 0;
StateHasChanged();
}
}

private async Task OpenUrlInNewWindow(string? url)
Expand Down Expand Up @@ -287,6 +295,10 @@
finally
{
isSyncing = false;
syncProgressStatus = string.Empty;
syncProgressCurrent = 0;
syncProgressTotal = 0;
StateHasChanged();
}
}

Expand Down
Loading