Skip to content

Commit

Permalink
Merge pull request #316 from LiamMorrow/plaintext-export
Browse files Browse the repository at this point in the history
  • Loading branch information
LiamMorrow authored Nov 15, 2024
2 parents 7967738 + 5f2a33b commit e520f03
Show file tree
Hide file tree
Showing 23 changed files with 297 additions and 24 deletions.
22 changes: 22 additions & 0 deletions Docs/PlaintextExport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Plaintext Export

LiftLog supports exporting your data as plaintext. Currently it supports CSV files.
These exports must NOT be used as a backup mechanism, as LiftLog cannot read these CSVs.

To create an export, simply navigate to `Settings -> Export, Backup, and Restore -> Plaintext Export`.
Here, you can export a CSV file which contains your workout data. Sets which have not been completed are not included in the export.
![Excluded Fields](./img/includedexcludedplaintext.png)

An example of the produced CSV:

```csv
SessionId,Timestamp,Exercise,Weight,WeightUnit,Reps,TargetReps,Notes
b59ab47f-8955-4a23-afa3-5d472dacc575,2024-10-16T10:45:06,Incline Dumbbell Bench Press,60,kg,8,8,
8b929966-2426-4cbf-b678-ac8e811c8b2b,2024-10-16T10:44:39,Bench Press,67.5,kg,12,12,
8b929966-2426-4cbf-b678-ac8e811c8b2b,2024-10-16T10:44:38,Bench Press,67.5,kg,12,12,
8b929966-2426-4cbf-b678-ac8e811c8b2b,2024-10-16T10:44:38,Bench Press,67.5,kg,12,12,
95bf84cb-1f11-4d51-ba6e-a4c3f1e40b08,2024-10-16T10:43:50,Pull-ups,84,kg,8,8,
95bf84cb-1f11-4d51-ba6e-a4c3f1e40b08,2024-10-16T10:43:50,Pull-ups,84,kg,7,8,
9c67f65a-3099-4d6b-b04d-428ce386f61d,2024-10-16T10:43:41,Squats,97.5,kg,8,8,
9c67f65a-3099-4d6b-b04d-428ce386f61d,2024-10-16T10:43:41,Squats,97.5,kg,8,8,
```
2 changes: 1 addition & 1 deletion Docs/RemoteBackup.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Mobile devices require that HTTPS is used for connections, which means that user

## Setting up the app

Within the LiftLog app, navigate to `Settings -> Backup and Restore -> Automatic Remote Backup`. On this screen, two values can be set:
Within the LiftLog app, navigate to `Settings -> Export, Backup, and Restore -> Automatic Remote Backup`. On this screen, two values can be set:

```
Endpoint - Required. This is the https endpoint which will receive the backups
Expand Down
Binary file added Docs/img/includedexcludedplaintext.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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" Options="Formats" Value="@Format.ToString()" 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.SelectOption> Formats = Enum.GetValues<PlaintextExportFormat>().Select(x=>new SelectField.SelectOption(x.ToString(), x.ToString())).ToList();

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

private void SelectFormat(string format)
{
Format = Enum.Parse<PlaintextExportFormat>(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);
}
83 changes: 83 additions & 0 deletions LiftLog.Ui/Services/PlaintextExportService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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(),
// s=sortable, ISO 8601 format without milliseconds or timezone
session
.Date.ToDateTime(set.Set!.CompletionTime!, DateTimeKind.Local)
.ToString("s"),
exercise.Blueprint.Name,
set.Weight,
unit,
set.Set.RepsCompleted,
exercise.Blueprint.RepsPerSet,
exercise.Notes ?? ""
));
}
}
Loading

0 comments on commit e520f03

Please sign in to comment.