Skip to content

Commit

Permalink
Allow saving with default folder & naming
Browse files Browse the repository at this point in the history
  • Loading branch information
nguyenquyhy committed Nov 14, 2022
1 parent 57d68a5 commit dd7930a
Show file tree
Hide file tree
Showing 17 changed files with 288 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="SharpZipLib" Version="1.4.1" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta11" />
</ItemGroup>

Expand Down
8 changes: 5 additions & 3 deletions FlightRecorder.Client.Logics/IDialogLogic.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System.Threading.Tasks;
using System.IO;
using System.Threading.Tasks;

namespace FlightRecorder.Client.Logics;

public interface IDialogLogic
{
bool Confirm(string message);
void Error(string error);
Task<string?> SaveAsync(SavedData data);
Task<(string? fileName, SavedData? data)> LoadAsync();
Task<string?> PickSaveFileAsync();
Task<(string filePath, Stream fileStream)?> PickOpenFileAsync();
Task<string?> PickSaveFolderAsync();
}
2 changes: 2 additions & 0 deletions FlightRecorder.Client.Logics/ISettingsLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ public interface ISettingsLogic
{
Task<bool> IsShortcutKeysEnabledAsync();
Task SetShortcutKeysEnabledAsync(bool value);
Task<string?> GetDefaultSaveFolderAsync();
Task SetDefaultSaveFolderAsync(string? folderPath);
}
10 changes: 10 additions & 0 deletions FlightRecorder.Client.Logics/IStorageLogic.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.IO;
using System.Threading.Tasks;

namespace FlightRecorder.Client.Logics;

public interface IStorageLogic
{
Task<SavedData?> LoadAsync(Stream file);
Task SaveAsync(string filePath, SavedData data);
}
12 changes: 12 additions & 0 deletions FlightRecorder.Client.Logics/SettingsLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ public Task SetShortcutKeysEnabledAsync(bool value)
return SaveAsync(settings => settings.ShortcutKeysEnabled = value);
}

public async Task<string?> GetDefaultSaveFolderAsync()
{
var settings = await LoadAsync();
return settings.DefaultSaveFolder;
}

public Task SetDefaultSaveFolderAsync(string? folderPath)
{
return SaveAsync(settings => settings.DefaultSaveFolder = folderPath);
}

private static SemaphoreSlim sm = new SemaphoreSlim(1);

private Settings? settings = null;
Expand Down Expand Up @@ -87,4 +98,5 @@ private async Task<Settings> LoadNoLockAsync()
public class Settings
{
public bool ShortcutKeysEnabled { get; set; } = false;
public string? DefaultSaveFolder { get; set; } = null;
}
63 changes: 63 additions & 0 deletions FlightRecorder.Client.Logics/StorageLogic.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;

namespace FlightRecorder.Client.Logics;

public class StorageLogic : IStorageLogic
{
private readonly ILogger<StorageLogic> logger;

public StorageLogic(ILogger<StorageLogic> logger)
{
this.logger = logger;
}

/// <param name="filePath">This must not be null or empty</param>
public async Task SaveAsync(string filePath, SavedData data)
{
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
using var outStream = new ZipOutputStream(fileStream);

outStream.SetLevel(9);

var entry = new ZipEntry("data.json")
{
DateTime = DateTime.Now
};
outStream.PutNextEntry(entry);

await JsonSerializer.SerializeAsync(outStream, data);
outStream.Finish();

logger.LogDebug("Saved file into {fileName}", filePath);
}
}

public async Task<SavedData?> LoadAsync(Stream file)
{
using (file)
{
using var zipFile = new ZipFile(file);

foreach (ZipEntry entry in zipFile)
{
if (entry.IsFile && entry.Name == "data.json")
{
using var stream = zipFile.GetInputStream(entry);

var result = await JsonSerializer.DeserializeAsync<SavedData>(stream);

logger.LogDebug("Loaded file data from file stream");

return result;
}
}
}
return null;
}
}
16 changes: 13 additions & 3 deletions FlightRecorder.Client.ViewModels.Tests/TestStateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Moq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace FlightRecorder.Client.ViewModels.Tests
Expand All @@ -14,6 +15,8 @@ namespace FlightRecorder.Client.ViewModels.Tests
public class TestStateMachine
{
private Mock<IThreadLogic> mockThreadLogic = null!;
private Mock<ISettingsLogic> mockSettingsLogic = null!;
private Mock<IStorageLogic> mockStorageLogic = null!;
private Mock<IRecorderLogic> mockRecorderLogic = null!;
private Mock<IReplayLogic> mockReplayLogic = null!;
private Mock<IConnector> mockConnector = null!;
Expand All @@ -32,6 +35,8 @@ public void Setup()
var factory = serviceProvider.GetService<ILoggerFactory>()!;

mockThreadLogic = new Mock<IThreadLogic>();
mockSettingsLogic = new Mock<ISettingsLogic>();
mockStorageLogic = new Mock<IStorageLogic>();
mockRecorderLogic = new Mock<IRecorderLogic>();
mockReplayLogic = new Mock<IReplayLogic>();
mockConnector = new Mock<IConnector>();
Expand All @@ -47,6 +52,8 @@ public void Setup()
stateMachine = new StateMachine(
factory.CreateLogger<StateMachine>(),
viewModel,
mockSettingsLogic.Object,
mockStorageLogic.Object,
mockRecorderLogic.Object,
mockReplayLogic.Object,
mockDialogLogic.Object,
Expand All @@ -62,14 +69,17 @@ public async Task TestAllEvents()

Assert.AreEqual(StateMachine.State.DisconnectedEmpty, viewModel.State);

mockDialogLogic.Setup(logic => logic.LoadAsync())
.Returns(Task.FromResult<(string?, SavedData?)>(("test", new SavedData(
mockDialogLogic.Setup(logic => logic.PickOpenFileAsync())
.Returns(Task.FromResult<(string, Stream)?>(("test", new MemoryStream())));
mockStorageLogic.Setup(logic => logic.LoadAsync(It.IsAny<Stream>()))
.Returns(Task.FromResult<SavedData?>(new SavedData(
"TEST_VERSION",
0,
1,
null,
new List<(long milliseconds, AircraftPositionStruct position)>()
))));
)));

await stateMachine.TransitAsync(StateMachine.Event.Load);
Assert.AreEqual(StateMachine.State.DisconnectedSaved, viewModel.State);
}
Expand Down
17 changes: 11 additions & 6 deletions FlightRecorder.Client.ViewModels/ShortcutKeyLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class ShortcutKeyLogic
private const uint KeyPause = 0xBC; // ,
private const uint KeyResume = 0xBE; // .
private const uint KeyStopReplaying = 'S';
private const uint KeySave = 'C';

private readonly ILogger<ShortcutKeyLogic> logger;
private readonly StateMachine stateMachine;
Expand Down Expand Up @@ -56,6 +57,7 @@ public async Task<bool> RegisterAsync(IntPtr handle)
RegisterHotKey(handle, 3, MOD_CONTROL | MOD_ALT | MOD_SHIFT, KeyPause);
RegisterHotKey(handle, 4, MOD_CONTROL | MOD_ALT | MOD_SHIFT, KeyResume);
RegisterHotKey(handle, 5, MOD_CONTROL | MOD_ALT | MOD_SHIFT, KeyStopReplaying);
RegisterHotKey(handle, 5, MOD_CONTROL | MOD_ALT | MOD_SHIFT, KeySave);
return true;
}
}
Expand Down Expand Up @@ -94,22 +96,25 @@ public bool HandleWindowsEvent(int message, IntPtr wParam, IntPtr lParam)
switch (key)
{
case KeyRecord:
await stateMachine.TransitAsync(StateMachine.Event.Record);
await stateMachine.TransitFromShortcutAsync(StateMachine.Event.Record);
break;
case KeyStopRecording:
await stateMachine.TransitAsync(StateMachine.Event.Stop);
await stateMachine.TransitFromShortcutAsync(StateMachine.Event.Stop);
break;
case KeyReplay:
await stateMachine.TransitAsync(StateMachine.Event.Replay);
await stateMachine.TransitFromShortcutAsync(StateMachine.Event.Replay);
break;
case KeyPause:
await stateMachine.TransitAsync(StateMachine.Event.Pause);
await stateMachine.TransitFromShortcutAsync(StateMachine.Event.Pause);
break;
case KeyResume:
await stateMachine.TransitAsync(StateMachine.Event.Resume);
await stateMachine.TransitFromShortcutAsync(StateMachine.Event.Resume);
break;
case KeyStopReplaying:
await stateMachine.TransitAsync(StateMachine.Event.RequestStopping);
await stateMachine.TransitFromShortcutAsync(StateMachine.Event.RequestStopping);
break;
case KeySave:
await stateMachine.TransitFromShortcutAsync(StateMachine.Event.Save);
break;
}
}
Expand Down
59 changes: 47 additions & 12 deletions FlightRecorder.Client.ViewModels/StateMachine.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using FlightRecorder.Client.Logics;
using FlightRecorder.Client.ViewModels.States;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Threading.Tasks;

namespace FlightRecorder.Client
Expand All @@ -11,6 +13,8 @@ public class StateMachine : StateMachineCore
public const string SaveErrorMessage = "Flight Recorder cannot write the file to disk.\nPlease make sure the folder is accessible by Flight Recorder, and you are not overwriting a locked file.";

private readonly MainViewModel viewModel;
private readonly ISettingsLogic settingsLogic;
private readonly IStorageLogic storageLogic;
private readonly IRecorderLogic recorderLogic;
private readonly IReplayLogic replayLogic;
private readonly string currentVersion;
Expand Down Expand Up @@ -56,11 +60,21 @@ public enum State
End
}

public StateMachine(ILogger<StateMachine> logger, MainViewModel viewModel, IRecorderLogic recorderLogic, IReplayLogic replayLogic, IDialogLogic dialogLogic, VersionLogic versionLogic)
: base(logger, dialogLogic, viewModel)
public StateMachine(
ILogger<StateMachine> logger,
MainViewModel viewModel,
ISettingsLogic settingsLogic,
IStorageLogic storageLogic,
IRecorderLogic recorderLogic,
IReplayLogic replayLogic,
IDialogLogic dialogLogic,
VersionLogic versionLogic
) : base(logger, dialogLogic, viewModel)
{
logger.LogDebug("Creating instance of {class}", nameof(StateMachine));
this.viewModel = viewModel;
this.settingsLogic = settingsLogic;
this.storageLogic = storageLogic;
this.recorderLogic = recorderLogic;
this.replayLogic = replayLogic;
this.currentVersion = versionLogic.GetVersion();
Expand Down Expand Up @@ -209,25 +223,46 @@ private bool StopRecording()
return true;
}

private async Task<bool> SaveRecordingAsync()
private async Task<bool> SaveRecordingAsync(ActionContext actionContext)
{
var data = replayLogic.ToData(currentVersion);
var fileName = await dialogLogic.SaveAsync(data);
if (!string.IsNullOrEmpty(fileName))

async Task<string?> GetSavePath()
{
replayLogic.FromData(fileName, data);
return true;
if (actionContext.FromShortcut)
{
var folder = await settingsLogic.GetDefaultSaveFolderAsync();
if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder))
{
return Path.Combine(folder, $"{DateTime.Now:yyyy-MM-dd-HH-mm-ss}.fltrec");
}
}
return await dialogLogic.PickSaveFileAsync();
}
return false;

var filePath = await GetSavePath();

if (string.IsNullOrEmpty(filePath))
{
return false;
}

await storageLogic.SaveAsync(filePath, data);
replayLogic.FromData(Path.GetFileName(filePath), data);
return true;
}

private async Task<bool> LoadRecordingAsync()
{
var (fileName, data) = await dialogLogic.LoadAsync();
if (data != null)
var selectedFile = await dialogLogic.PickOpenFileAsync();
if (selectedFile != null)
{
replayLogic.FromData(fileName, data);
return true;
var data = await storageLogic.LoadAsync(selectedFile.Value.fileStream);
if (data != null)
{
replayLogic.FromData(Path.GetFileName(selectedFile.Value.filePath), data);
return true;
}
}
return false;
}
Expand Down

0 comments on commit dd7930a

Please sign in to comment.