📄 app/src/Pages/RecordingPage.axaml.cs
using System.Threading.Tasks;
using System.Web;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using SkiaSharp;

namespace BookScanner.App.Pages;

public partial class RecordingPage : ContentPage
{
    private readonly IStorageFolder projectFolder;
    private int page = 0;
    private int attempt = 0;

    public RecordingPage(IStorageFolder projectFolder)
    {
        InitializeComponent();
        DataContext = this;

        this.projectFolder = projectFolder;
        projectName.Text = HttpUtility.UrlDecode(projectFolder.Path.Segments[^1].Trim('/'));
    }

    private async void OnCaptureClick(object? sender, RoutedEventArgs args)
    {
        pageCounter.Text = (++page).ToString("D3");
        attempt = 0;
        var images = await projectFolder.GetFolderAsync("images");
        await CaptureAttempt(images!);
    }

    private async void OnRecaptureClick(object? sender, RoutedEventArgs args)
    {
        attempt++;
        var images = await projectFolder.GetFolderAsync("images");
        await CaptureAttempt(images!);
    }

    private async Task CaptureAttempt(IStorageFolder images)
    {
        var leftTask = SaveLeft(images);
        await SaveRight(images);
        await leftTask;
    }

    private async Task SaveLeft(IStorageFolder images)
    {
        using var image = SKImage.FromBitmap(leftCameraPage.Image);
        using var encoded = image.Encode(SKEncodedImageFormat.Png, 100);
        var file = await images!.CreateFileAsync($"{page:D3}-{attempt:D1}L.png");
        using var stream = await file!.OpenWriteAsync();
        encoded.SaveTo(stream);
    }

    private async Task SaveRight(IStorageFolder images)
    {
        using var image = SKImage.FromBitmap(rightCameraPage.Image);
        using var encoded = image.Encode(SKEncodedImageFormat.Png, 100);
        var file = await images!.CreateFileAsync($"{page:D3}-{attempt:D1}R.png");
        using var stream = await file!.OpenWriteAsync();
        encoded.SaveTo(stream);
    }
}