Skip to content

Commit

Permalink
Add plaintext export
Browse files Browse the repository at this point in the history
This commit adds a plaintext export, in CSV format.  It exports the users sessions as '1 row per set'.
  • Loading branch information
LiamMorrow committed Nov 15, 2024
1 parent 7967738 commit 21eb066
Show file tree
Hide file tree
Showing 19 changed files with 271 additions and 21 deletions.
4 changes: 2 additions & 2 deletions LiftLog.Cypress/cypress/e2e/settings.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ describe('Settings', () => {
cy.containsA('Show tips').click()

cy.navigate('Settings')
cy.containsA('Backup and restore').click()
cy.containsA('Import data').click()
cy.containsA('restore').click()
cy.containsA('Restore data').click()
cy.get('input[type=file]').selectFile('export.liftlogbackup.gz', { force: true })
})

Expand Down
5 changes: 3 additions & 2 deletions LiftLog.Maui/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,15 @@ public static MauiApp CreateMauiApp()
AppDataFileStorageKeyValueStore,
SecureStoragePreferenceStore,
MauiNotificationService,
MauiShareExporter,
MauiBackupRestoreService,
AppThemeProvider,
MauiStringSharer,
AppPurchaseService,
OsEncryptionService,
AppHapticFeedbackService,
AppDeviceService,
AppBuiltInExerciseLoader
AppBuiltInExerciseLoader,
MauiFileExportService
>(typeof(ThemeEffects).Assembly);

builder.UseMaterialColors<AppColorService>(opts =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace LiftLog.Maui.Services;

public class MauiShareExporter(IShare share, IFilePicker filePicker) : IExporter
public class MauiBackupRestoreService(IShare share, IFilePicker filePicker) : IBackupRestoreService
{
public async Task ExportBytesAsync(byte[] bytes)
{
Expand All @@ -15,7 +15,7 @@ public async Task ExportBytesAsync(byte[] bytes)
await gzip.WriteAsync(bytes);

await share.RequestAsync(
new ShareFileRequest { Title = "Export Data", File = new ShareFile(file) }
new ShareFileRequest { Title = "Backup Data", File = new ShareFile(file) }
);
}

Expand Down
18 changes: 18 additions & 0 deletions LiftLog.Maui/Services/MauiFileExportService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using LiftLog.Ui.Services;

namespace LiftLog.Maui.Services;

public class MauiFileExportService(IShare share) : IFileExportService
{
public async Task ExportBytesAsync(string fileName, byte[] bytes, string contentType)
{
string file = Path.Combine(FileSystem.CacheDirectory, fileName);

using FileStream stream = File.Create(file);
await stream.WriteAsync(bytes);

await share.RequestAsync(
new ShareFileRequest { Title = "Export Data", File = new ShareFile(file) }
);
}
}
1 change: 1 addition & 0 deletions LiftLog.Ui/LiftLog.Ui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
</PackageReference>
<PackageReference Include="Blazor-ApexCharts" Version="4.0.0" />
<PackageReference Include="BlazorTransitionableRoute" Version="4.0.0" />
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="FuzzySharp" Version="2.0.2" />
<PackageReference Include="MaterialColorUtilities" Version="0.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.10" />
Expand Down
6 changes: 6 additions & 0 deletions LiftLog.Ui/Models/PlaintextExportFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace LiftLog.Ui.Models;

public enum PlaintextExportFormat
{
CSV,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@page "/settings/backup-and-restore/plain-text-export"
@inject IDispatcher Dispatcher
@inject IState<SettingsState> SettingsState

@inherits Fluxor.Blazor.Web.Components.FluxorComponent

<Card Type="Card.CardType.Filled" class="mx-6 mb-4">
<div>
This feature allows you to export your data in a plain text format such as CSV for use in other applications.
It is not intended for backup purposes. For backups, please use the backup feature.
LiftLog cannot restore data from plain text exports.
</div>
<AppButton OnClick="OpenDocumentation" Type="AppButtonType.Text">Read Documentation</AppButton>
</Card>

<LabelledForm>
<LabelledFormRow Label="Format" Icon="description">
<SelectField data-cy="export-format-selector" T="PlaintextExportFormat" Options="Formats" Value="Format" ValueChanged="SelectFormat"/>
</LabelledFormRow>
</LabelledForm>

<div class="flex justify-end gap-4 m-6">
<AppButton Type="AppButtonType.Primary" OnClick="Export">Export</AppButton>
</div>


@code {

private PlaintextExportFormat Format = PlaintextExportFormat.CSV;
private List<SelectField<PlaintextExportFormat>.SelectOption> Formats = [new("CSV", PlaintextExportFormat.CSV)];

protected override void OnInitialized()
{
Dispatcher.Dispatch(new SetPageTitleAction("Plain Text Export"));
Dispatcher.Dispatch(new SetBackNavigationUrlAction("/settings/backup-and-restore"));
base.OnInitialized();
}

private void SelectFormat(PlaintextExportFormat format)
{
Format = format;
StateHasChanged();
}

private void Export(MouseEventArgs _)
{
Dispatcher.Dispatch(new ExportPlainTextAction(Format));
}

private void OpenDocumentation(MouseEventArgs _)
{
Dispatcher.Dispatch(new NavigateAction("https://github.com/LiamMorrow/LiftLog/blob/main/Docs/PlaintextExport.md"));
}
}
15 changes: 11 additions & 4 deletions LiftLog.Ui/Pages/Settings/BackupAndRestorePage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@
<md-list>
<md-list-item type="button" class="text-left" multi-line-supporting-text @onclick="()=>exportFeedDialog?.Open()">
<md-icon slot="start">backup</md-icon>
<span slot="headline">Export data</span>
<span slot="headline">Backup data</span>
<span slot="supporting-text">Export your data to a file for backup or transfer</span>
</md-list-item>
<md-list-item type="button" class="text-left" multi-line-supporting-text @onclick="ImportData">
<md-icon slot="start">restore</md-icon>
<span slot="headline">Import data</span>
<span slot="headline">Restore data</span>
<span slot="supporting-text">Import data from a file to restore</span>
</md-list-item>
<md-list-item type="button" class="text-left" multi-line-supporting-text @onclick="RemoteBackup">
<md-icon slot="start">cloud_upload</md-icon>
<span slot="headline">Automatic remote backup</span>
<span slot="supporting-text">Send backups to a remote server. Advanced users only</span>
</md-list-item>
<md-list-item type="button" class="text-left" multi-line-supporting-text @onclick="PlainTextExport">
<md-icon slot="start">description</md-icon>
<span slot="headline">Plain text export</span>
<span slot="supporting-text">Export your data in a plain text format such as CSV for use in other applications</span>
</md-list-item>
</md-list>


Expand Down Expand Up @@ -55,7 +60,7 @@
private InputFile? fileInput;
protected override void OnInitialized()
{
Dispatcher.Dispatch(new SetPageTitleAction("Backup and Restore"));
Dispatcher.Dispatch(new SetPageTitleAction("Export, Backup and Restore"));
Dispatcher.Dispatch(new SetBackNavigationUrlAction("/settings"));
SubscribeToAction<BeginFeedImportAction>(OnBeginFeedImport);
base.OnInitialized();
Expand All @@ -82,7 +87,9 @@

private void RemoteBackup() => Dispatcher.Dispatch(new NavigateAction("/settings/backup-and-restore/remote-backup"));

private void ExportData(bool includeFeed) => Dispatcher.Dispatch(new ExportDataAction(includeFeed));
private void PlainTextExport() => Dispatcher.Dispatch(new NavigateAction("/settings/backup-and-restore/plain-text-export"));

private void ExportData(bool includeFeed) => Dispatcher.Dispatch(new ExportBackupDataAction(includeFeed));

private void OnBeginFeedImport(BeginFeedImportAction action)
{
Expand Down
2 changes: 1 addition & 1 deletion LiftLog.Ui/Pages/Settings/SettingsPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

<md-list-item type="button" multi-line-supporting-text @onclick="NavigateToBackupAndRestore">
<md-icon slot="start">settings_backup_restore</md-icon>
<span slot="headline" >Backup and restore</span>
<span slot="headline" >Export, backup, and restore</span>
<span slot="supporting-text">Create and export backups for transfering between devices</span>
</md-list-item>
</md-list>
Expand Down
11 changes: 8 additions & 3 deletions LiftLog.Ui/ServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,21 @@ public static IServiceCollection RegisterUiServices<
TEncryptionService,
TVibrationService,
TDeviceService,
TBuiltInExerciseService
TBuiltInExerciseService,
TFileExportService
>(this IServiceCollection services, Assembly fluxorScanAssembly)
where TKeyValueStore : class, IKeyValueStore
where TPreferenceStore : class, IPreferenceStore
where TNotificationService : class, INotificationService
where TExporter : class, IExporter
where TExporter : class, IBackupRestoreService
where TThemeProvider : class, IThemeProvider
where TStringSharer : class, IStringSharer
where TPurchaseService : class, IAppPurchaseService
where TEncryptionService : class, IEncryptionService
where TVibrationService : class, IHapticFeedbackService
where TDeviceService : class, IDeviceService
where TBuiltInExerciseService : class, IBuiltInExerciseLoader
where TFileExportService : class, IFileExportService
{
var lifetime = ServiceLifetime.Singleton;
services.AddFluxor(o =>
Expand Down Expand Up @@ -82,7 +84,7 @@ public static IServiceCollection RegisterUiServices<
services.Add<IKeyValueStore, TKeyValueStore>(lifetime);
services.Add<IPreferenceStore, TPreferenceStore>(lifetime);
services.Add<INotificationService, TNotificationService>(lifetime);
services.Add<IExporter, TExporter>(lifetime);
services.Add<IBackupRestoreService, TExporter>(lifetime);

services.Add<IAiWorkoutPlanner, ApiBasedAiWorkoutPlanner>(lifetime);

Expand All @@ -100,6 +102,9 @@ public static IServiceCollection RegisterUiServices<

services.Add<IBuiltInExerciseLoader, TBuiltInExerciseService>(lifetime);

services.Add<IFileExportService, TFileExportService>(lifetime);
services.Add<PlaintextExportService>(lifetime);

services.Add<IFeedApiService, FeedApiService>(lifetime);
services.Add<FeedIdentityService>(lifetime);
services.Add<FeedFollowService>(lifetime);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace LiftLog.Ui.Services;

public interface IExporter
public interface IBackupRestoreService
{
Task ExportBytesAsync(byte[] bytes);
Task<byte[]> ImportBytesAsync();
Expand Down
6 changes: 6 additions & 0 deletions LiftLog.Ui/Services/IFileExportService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace LiftLog.Ui.Services;

public interface IFileExportService
{
Task ExportBytesAsync(string fileName, byte[] bytes, string contentType);
}
80 changes: 80 additions & 0 deletions LiftLog.Ui/Services/PlaintextExportService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Globalization;
using CsvHelper;
using Fluxor;
using LiftLog.Lib.Models;
using LiftLog.Ui.Models;
using LiftLog.Ui.Store.Settings;

namespace LiftLog.Ui.Services;

public class PlaintextExportService(
IFileExportService fileExportService,
ProgressRepository progressRepository,
IState<SettingsState> settingsState
)
{
public async Task ExportAsync(PlaintextExportFormat format)
{
var unit = settingsState.Value.UseImperialUnits ? "lbs" : "kg";
var sessions = progressRepository.GetOrderedSessions();

var (fileName, bytes, contentType) = format switch
{
PlaintextExportFormat.CSV => (
"liftlog-export.csv",
await ExportToCsv(sessions, unit),
"text/csv"
),
};

await fileExportService.ExportBytesAsync(fileName, bytes, contentType);
}

private static async Task<byte[]> ExportToCsv(IAsyncEnumerable<Session> sessions, string unit)
{
var exportedSets = await sessions
.SelectMany(s => s.RecordedExercises.Select(ex => (s, ex)).ToAsyncEnumerable())
.SelectMany(
(val) => ExportedSetCsvRow.FromModel(val.s, val.ex, unit).ToAsyncEnumerable()
)
.ToListAsync();
using var memoryStream = new MemoryStream();
using var writer = new StreamWriter(memoryStream);
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.WriteRecords(exportedSets);
csv.Flush();
return memoryStream.ToArray();
}
}

public record ExportedSetCsvRow(
string SessionId,
string Timestamp,
string Exercise,
decimal Weight,
string WeightUnit,
int Reps,
int TargetReps,
string Notes
)
{
public static IEnumerable<ExportedSetCsvRow> FromModel(
Session session,
RecordedExercise exercise,
string unit
)
{
return exercise
.PotentialSets.Where(x => x.Set is not null)
.Select(set => new ExportedSetCsvRow(
session.Id.ToString(),
session.Date.ToDateTime(set.Set!.CompletionTime!).ToString("o"),
exercise.Blueprint.Name,
set.Weight,
unit,
set.Set.RepsCompleted,
exercise.Blueprint.RepsPerSet,
exercise.Notes ?? ""
));
}
}
43 changes: 43 additions & 0 deletions LiftLog.Ui/Shared/Presentation/SelectField.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
@inject IJSRuntime JsRuntime
@typeparam T

@switch(TextFieldType){
case TextFieldType.Outlined:
<md-outlined-select value=@Value @attributes="AdditionalAttributes">
@foreach (var option in Options)
{
<md-select-option value="@option.Value">
<div slot="headline">@option.Title</div>
</md-select-option>
}
</md-outlined-select>
break;
case TextFieldType.Filled:
<md-filled-select value=@Value @attributes="AdditionalAttributes" >
@foreach (var option in Options)
{
<md-select-option value="@option.Value">
<div slot="headline">@option.Title</div>
</md-select-option>
}
</md-filled-select>
break;
default:
throw new ArgumentOutOfRangeException(nameof(TextFieldType), TextFieldType, null);

}


@code {
[Parameter] [EditorRequired] public T Value { get; set; } = default!;

[Parameter] [EditorRequired] public List<SelectOption> Options { get; set; } = null!;

[Parameter] [EditorRequired] public EventCallback<T> ValueChanged { get; set; }

[Parameter] public TextFieldType TextFieldType { get; set; } = TextFieldType.Outlined;

[Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }

public record SelectOption(string Title, T Value);
}
5 changes: 4 additions & 1 deletion LiftLog.Ui/Store/Settings/SettingsActions.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using LiftLog.Lib;
using LiftLog.Lib.Models;
using LiftLog.Ui.Models;
using LiftLog.Ui.Services;
using LiftLog.Ui.Store.Feed;

namespace LiftLog.Ui.Store.Settings;

public record ExportDataAction(bool ExportFeed);
public record ExportBackupDataAction(bool ExportFeed);

public record ImportDataAction();

Expand Down Expand Up @@ -48,3 +49,5 @@ public record RemoteBackupSucceededEvent();
public record UpdateRemoteBackupSettingsAction(RemoteBackupSettings Settings);

public record SetLastSuccessfulRemoteBackupHashAction(string Hash);

public record ExportPlainTextAction(PlaintextExportFormat Format);
Loading

0 comments on commit 21eb066

Please sign in to comment.