Skip to content

Commit

Permalink
feat: make the user data storable (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
BoBoBaSs84 authored Jul 1, 2024
2 parents 84d6f3f + 8794db8 commit c502077
Show file tree
Hide file tree
Showing 20 changed files with 343 additions and 53 deletions.
1 change: 0 additions & 1 deletion src/BB84.SAU.Application/BB84.SAU.Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
<ProjectReference Include="..\BB84.SAU.Domain\BB84.SAU.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Interfaces\Infrastructure\Persistence\" />
<Folder Include="Interfaces\Presentation\Services\" />
</ItemGroup>
<ItemGroup>
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using BB84.SAU.Domain.Models;

namespace BB84.SAU.Application.Interfaces.Infrastructure.Persistence;

/// <summary>
/// The user data service interface.
/// </summary>
public interface IUserDataService
{
/// <summary>
/// Loads the persisted state of the user data.
/// </summary>
/// <param name="cancellationToken">The cancellation token to cancel the request.</param>
/// <returns><see cref="Task{TResult}"/></returns>
Task<UserDataModel> LoadUserDataAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Saves the current user data state.
/// </summary>
/// <param name="userData">The user data to persist.</param>
/// <param name="cancellationToken">The cancellation token to cancel the request.</param>
/// <returns><see cref="Task{TResult}"/></returns>
Task<bool> SaveUserDataAsync(UserDataModel userData, CancellationToken cancellationToken = default);
}
18 changes: 10 additions & 8 deletions src/BB84.SAU.Application/ViewModels/AchievementsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public IActionCommand<AchievementModel?> UnlockAchievementCommand
=> new ActionCommand<AchievementModel?>(UnlockAchievement, CanUnlockAchievement);

private bool CanLoadAchievements()
=> Model.LastUpdate is not null && AchievementsAreLoading.IsFalse();
=> AchievementsAreLoading.IsFalse();

private bool CanLockAchievement(AchievementModel? model)
=> model is not null && model.Unlocked.IsTrue();
Expand All @@ -136,6 +136,8 @@ private async Task LoadAchievements()
{
AchievementsAreLoading = true;

Model.Achievements.Clear();

IEnumerable<AchievementModel> achievementsData = await _steamWebService.GetAchievementsAsync(Model.Id, _steamSettings.ApiKey)
.ConfigureAwait(true);

Expand Down Expand Up @@ -163,8 +165,8 @@ private void LockAchievement(AchievementModel? model)
if (model is null)
return;

_steamApiService.ResetAchievement(model.Id);
_steamApiService.StoreStats();
_ = _steamApiService.ResetAchievement(model.Id);
_ = _steamApiService.StoreStats();

SetAchievement(model, false, null);
}
Expand All @@ -174,8 +176,8 @@ private void UnlockAchievement(AchievementModel? model)
if (model is null)
return;

_steamApiService.UnlockAchievement(model.Id);
_steamApiService.StoreStats();
_ = _steamApiService.UnlockAchievement(model.Id);
_ = _steamApiService.StoreStats();

SetAchievement(model, true, _dateTimeProvider.Now);
}
Expand All @@ -188,7 +190,7 @@ private void OnPropertyChanging(string? propertyName)
if (propertyName == nameof(Model))
{
if (_steamApiService.StatsRequested.IsTrue())
_steamApiService.StoreStats();
_ = _steamApiService.StoreStats();

if (_steamApiService.Initialized.IsTrue())
_steamApiService.Shutdown();
Expand All @@ -203,10 +205,10 @@ private void OnPropertyChanged(string? propertyName)
if (propertyName == nameof(Model))
{
if (_steamApiService.Initialized.IsFalse())
_steamApiService.Initialize(Model.Id);
_ = _steamApiService.Initialize(Model.Id);

if (_steamApiService.StatsRequested.IsFalse())
_steamApiService.RequestStats();
_ = _steamApiService.RequestStats();
}

if (propertyName == nameof(SelectedAchievement) && SelectedAchievement is not null)
Expand Down
4 changes: 3 additions & 1 deletion src/BB84.SAU.Application/ViewModels/GamesViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public bool IsSelectButtonVisible
=> SelectedGame is not null && SelectedGame.LastUpdate is not null;

private bool CanLoadGames()
=> Model.LastUpdate is not null && GamesAreLoading.IsFalse();
=> GamesAreLoading.IsFalse();

private bool CanSelectGame(GameDetailModel model)
=> model is not null && model.LastUpdate is not null;
Expand All @@ -120,6 +120,8 @@ private async Task LoadGames()
{
GamesAreLoading = true;

Model.Games.Clear();

IEnumerable<GameModel> gamesData = await _steamWebService.GetGamesAsync(_steamSettings.Id, _steamSettings.ApiKey)
.ConfigureAwait(true);

Expand Down
4 changes: 4 additions & 0 deletions src/BB84.SAU.Application/ViewModels/UserDataViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public UserDataViewModel(ISteamWebService steamWebService, IOptions<SteamSetting
_steamSettings = options.Value;

Model = model;

if (Model.LastUpdate is not null)
Image = CreateImageFromUri(new(Model.ImageUrl));

Model.PropertyChanged += (s, e) => OnModelPropertyChanged(e.PropertyName);
}

Expand Down
25 changes: 0 additions & 25 deletions src/BB84.SAU.Domain/Converters/JsonUnixTimeToDateTime.cs

This file was deleted.

8 changes: 7 additions & 1 deletion src/BB84.SAU.Domain/Models/GameDetailModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ namespace BB84.SAU.Domain.Models;
/// <param name="lastUpdate">Indicates when the game data was last updated.</param>
public sealed class GameDetailModel(int id, string title, string? description = null, string? imageUrl = null, DateTime? lastUpdate = null) : ModelBase, IUpdatable
{
private ObservableCollection<AchievementModel> _achievements = [];

/// <summary>
/// Initializes a new instance of the <see cref="GameDetailModel"/> class.
/// </summary>
Expand Down Expand Up @@ -69,5 +71,9 @@ public DateTime? LastUpdate
/// <summary>
/// The achievements of the game.
/// </summary>
public ObservableCollection<AchievementModel> Achievements { get; } = [];
public ObservableCollection<AchievementModel> Achievements
{
get => _achievements;
set => SetProperty(ref _achievements, value);
}
}
12 changes: 9 additions & 3 deletions src/BB84.SAU.Domain/Models/UserDataModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ namespace BB84.SAU.Domain.Models;
/// <param name="lastUpdate">Indicates when the user data was last updated.</param>
public sealed class UserDataModel(string name, string imageUrl, string? profileUrl, DateTime created, DateTime lastLogOff, DateTime? lastUpdate = null) : ModelBase, IUpdatable
{
private ObservableCollection<GameDetailModel> _games = [];

/// <summary>
///
/// Initializes a new instance of the <see cref="UserDataModel"/> class.
/// </summary>
public UserDataModel() : this(string.Empty, string.Empty, string.Empty, default, default)
{ }
{ }

/// <summary>
/// The name of the user.
Expand Down Expand Up @@ -79,5 +81,9 @@ public DateTime? LastUpdate
/// <summary>
/// The games of the user.
/// </summary>
public ObservableCollection<GameDetailModel> Games { get; } = [];
public ObservableCollection<GameDetailModel> Games
{
get => _games;
set => SetProperty(ref _games, value);
}
}
3 changes: 3 additions & 0 deletions src/BB84.SAU.Infrastructure/Installer/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;

using BB84.SAU.Application.Interfaces.Infrastructure.Persistence;
using BB84.SAU.Application.Interfaces.Infrastructure.Services;
using BB84.SAU.Infrastructure.Extensions;
using BB84.SAU.Infrastructure.Interfaces.Provider;
using BB84.SAU.Infrastructure.Persistence;
using BB84.SAU.Infrastructure.Provider;
using BB84.SAU.Infrastructure.Services;

Expand Down Expand Up @@ -31,6 +33,7 @@ public static IServiceCollection RegisterInfrastructureServices(this IServiceCol
services.TryAddSingleton<IFileProvider, FileProvider>();
services.TryAddSingleton<ISteamWorksProvider, SteamWorksProvider>();
services.TryAddSingleton<ISteamApiService, SteamApiService>();
services.TryAddSingleton<IUserDataService, UserDataService>();

return services;
}
Expand Down
13 changes: 11 additions & 2 deletions src/BB84.SAU.Infrastructure/Interfaces/Provider/IFileProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@
/// </summary>
internal interface IFileProvider
{
/// <inheritdoc cref="File.Delete(string)"/>
void Delete(string path);

/// <inheritdoc cref="File.Exists(string?)"/>
bool Exists(string? path);

/// <inheritdoc cref="File.ReadAllTextAsync(string, CancellationToken)"/>
Task<string> ReadAllTextAsync(string path, CancellationToken cancellationToken = default);

/// <inheritdoc cref="File.WriteAllText(string, string?)"/>
void WriteAllText(string path, string? contents);

/// <inheritdoc cref="File.Delete(string)"/>
void Delete(string path);
/// <inheritdoc cref="File.WriteAllTextAsync(string, string?, CancellationToken)"/>
Task WriteAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default);
}
74 changes: 74 additions & 0 deletions src/BB84.SAU.Infrastructure/Persistence/UserDataService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Text.Json;
using System.Text.Json.Serialization;

using BB84.Extensions.Serialization;
using BB84.SAU.Application.Interfaces.Infrastructure.Persistence;
using BB84.SAU.Application.Interfaces.Infrastructure.Services;
using BB84.SAU.Domain.Models;
using BB84.SAU.Infrastructure.Interfaces.Provider;

using Microsoft.Extensions.Logging;

namespace BB84.SAU.Infrastructure.Persistence;

/// <summary>
/// The user data service class.
/// </summary>
/// <param name="loggerService">The logger service instance to use.</param>
/// <param name="fileProvider">The file provider instance to use.</param>
internal sealed class UserDataService(ILoggerService<UserDataService> loggerService, IFileProvider fileProvider) : IUserDataService
{
private static readonly Action<ILogger, Exception?> LogException =
LoggerMessage.Define(LogLevel.Error, 0, "Exception occured.");

private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private static readonly string AppName = "BB84.SAU";
private static readonly string DataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
private static readonly string DataFile = "UserData.json";

public async Task<UserDataModel> LoadUserDataAsync(CancellationToken cancellationToken = default)
{
try
{
string filePath = Path.Combine(DataFolder, AppName, DataFile);

if (!fileProvider.Exists(filePath))
return new();

string fileContent = await fileProvider.ReadAllTextAsync(filePath, cancellationToken);

UserDataModel userData = fileContent.FromJson<UserDataModel>(SerializerOptions);

return userData;
}
catch (Exception ex)
{
loggerService.Log(LogException, ex);
return new();
}
}

public async Task<bool> SaveUserDataAsync(UserDataModel userData, CancellationToken cancellationToken = default)
{
try
{
_ = Directory.CreateDirectory(Path.Combine(DataFolder, AppName));

string filePath = Path.Combine(DataFolder, AppName, DataFile);

string fileContent = userData.ToJson(SerializerOptions);

await fileProvider.WriteAllTextAsync(filePath, fileContent, cancellationToken);

return true;
}
catch (Exception ex)
{
loggerService.Log(LogException, ex);
return false;
}
}
}
9 changes: 9 additions & 0 deletions src/BB84.SAU.Infrastructure/Provider/FileProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ internal sealed class FileProvider : IFileProvider
public void Delete(string path)
=> File.Delete(path);

public bool Exists(string? path)
=> File.Exists(path);

public Task<string> ReadAllTextAsync(string path, CancellationToken cancellationToken = default)
=> File.ReadAllTextAsync(path, cancellationToken);

public void WriteAllText(string path, string? contents)
=> File.WriteAllText(path, contents);

public Task WriteAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default)
=> File.WriteAllTextAsync(path, contents, cancellationToken);
}
39 changes: 35 additions & 4 deletions src/BB84.SAU/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
using System.Windows;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using BB84.SAU.Application.Interfaces.Infrastructure.Persistence;
using BB84.SAU.Application.Interfaces.Infrastructure.Services;
using BB84.SAU.Domain.Models;
using BB84.SAU.Extensions;
using BB84.SAU.Presentation.Windows;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using WinApplication = System.Windows.Application;

namespace BB84.SAU;
Expand Down Expand Up @@ -45,6 +47,7 @@ private async void Application_Startup(object sender, StartupEventArgs e)
_loggerService.Log(LogInformation, "Application starting...");

await _host.StartAsync().ConfigureAwait(false);
await LoadUserDataAsync();

MainWindow mainWindow = _host.Services.GetRequiredService<MainWindow>();
mainWindow.Show();
Expand All @@ -57,6 +60,8 @@ private async void Application_Exit(object sender, ExitEventArgs e)
if (_steamApiService.AppId is not null)
_steamApiService.Shutdown();

await SaveUserDataAsync();

using (_host)
await _host.StopAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
}
Expand All @@ -67,4 +72,30 @@ private void OnUnhandledException(Exception exception)
private static IHostBuilder CreateHostBuilder()
=> Host.CreateDefaultBuilder().AddApplicationSettings().ConfigureServices((context, services)
=> services.RegisterServices(context.HostingEnvironment, context.Configuration));

private async Task LoadUserDataAsync()
{
IUserDataService userDataService = _host.Services.GetRequiredService<IUserDataService>();
UserDataModel currentUserData = _host.Services.GetRequiredService<UserDataModel>();

UserDataModel loadedUserData = await userDataService.LoadUserDataAsync()
.ConfigureAwait(false);

currentUserData.Created = loadedUserData.Created;
currentUserData.Name = loadedUserData.Name;
currentUserData.ProfileUrl = loadedUserData.ProfileUrl;
currentUserData.ImageUrl = loadedUserData.ImageUrl;
currentUserData.LastLogOff = loadedUserData.LastLogOff;
currentUserData.LastUpdate = loadedUserData.LastUpdate;
currentUserData.Games = loadedUserData.Games;
}

private async Task SaveUserDataAsync()
{
IUserDataService userDataService = _host.Services.GetRequiredService<IUserDataService>();
UserDataModel currentUserData = _host.Services.GetRequiredService<UserDataModel>();

_ = await userDataService.SaveUserDataAsync(currentUserData)
.ConfigureAwait(false);
}
}
Loading

0 comments on commit c502077

Please sign in to comment.