Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ Examples:
- Comments are only allowed when strictly necessary to explain complex or non-obvious logic.
- Prefer clear and self-explanatory code over adding comments.

### Testing Style & Conventions
- **Assertions**: use FluentAssertions (`.Should()`). **Do not use** `NUnit.Framework.Legacy` / `ClassicAssert`.
- **Usings**: remove `using NUnit.Framework.Legacy;` from tests.
- **Structure**: follow Arrange–Act–Assert. Use clear, English names and messages.

### Coverage Requirements
- **Target per new unit test class**: ≥ 85% coverage.
- **PR Quality (Sonar)**: each PR must maintain a minimum coverage of 80%.

## Build and Test Guidelines
- Always run build and test as two separate commands to avoid blocking issues.
- Use `dotnet build --verbosity quiet /property:WarningLevel=0` to build the solution.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls;
using ByteSync.Assets.Resources;
using ByteSync.Business.Sessions;
using ByteSync.Common.Business.Sessions.Cloud;
using ByteSync.Interfaces.Controls.Synchronizations;
using ByteSync.Interfaces.Repositories;
using ByteSync.Interfaces.Services.Localizations;
using ByteSync.Interfaces.Services.Sessions;
using ByteSync.ViewModels.Misc;
using DynamicData;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;

namespace ByteSync.ViewModels.Sessions.Synchronizations;

public class SynchronizationBeforeStartViewModel : ActivatableViewModelBase
{
private readonly ISessionService _sessionService = null!;
private readonly ILocalizationService _localizationService = null!;
private readonly ISynchronizationService _synchronizationService = null!;
private readonly ISynchronizationStarter _synchronizationStarter = null!;
private readonly IAtomicActionRepository _atomicActionRepository = null!;
private readonly ISessionMemberRepository _sessionMemberRepository = null!;
private readonly ILogger<SynchronizationBeforeStartViewModel> _logger = null!;

public SynchronizationBeforeStartViewModel()
{
}

public SynchronizationBeforeStartViewModel(ISessionService sessionService, ILocalizationService localizationService,
ISynchronizationService synchronizationService, ISynchronizationStarter synchronizationStarter,
IAtomicActionRepository atomicActionRepository, ISessionMemberRepository sessionMemberRepository,
ILogger<SynchronizationBeforeStartViewModel> logger, ErrorViewModel errorViewModel)
{
_sessionService = sessionService;
_localizationService = localizationService;
_synchronizationService = synchronizationService;
_synchronizationStarter = synchronizationStarter;
_atomicActionRepository = atomicActionRepository;
_sessionMemberRepository = sessionMemberRepository;
_logger = logger;

StartSynchronizationError = errorViewModel;

var canStartSynchronization = _sessionService.SessionObservable
.CombineLatest(
_atomicActionRepository.ObservableCache.Connect().ToCollection(),
_sessionMemberRepository.IsCurrentUserFirstSessionMemberObservable,
(session, atomicActions, isCurrentUserFirstSessionMember) =>
(Session: session, AtomicActions: atomicActions, IsCurrentUserFirstSessionMember: isCurrentUserFirstSessionMember))
.DistinctUntilChanged()
.ObserveOn(RxApp.MainThreadScheduler)
.Select(tuple => tuple.Session is CloudSession && tuple.IsCurrentUserFirstSessionMember && tuple.AtomicActions.Count > 0);

StartSynchronizationCommand = ReactiveCommand.CreateFromTask(StartSynchronization, canStartSynchronization);

this.WhenActivated(disposables =>
{
_synchronizationService.SynchronizationProcessData.SynchronizationStart
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ => IsSynchronizationRunning = true)
.DisposeWith(disposables);

_synchronizationService.SynchronizationProcessData.SynchronizationEnd
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ => IsSynchronizationRunning = false)
.DisposeWith(disposables);

_sessionService.SessionStatusObservable
.CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationStart)
.Select(tuple =>
!tuple.First.In(SessionStatus.None, SessionStatus.Preparation, SessionStatus.Comparison,
SessionStatus.CloudSessionCreation, SessionStatus.CloudSessionJunction, SessionStatus.Inventory)
&& tuple.Second != null)
.ObserveOn(RxApp.MainThreadScheduler)
.ToPropertyEx(this, x => x.HasSynchronizationStarted)
.DisposeWith(disposables);

this.WhenAnyValue(
x => x.IsSynchronizationRunning, x => x.IsCloudSession, x => x.IsSessionCreatedByMe, x => x.HasSynchronizationStarted,
x => x.IsProfileSessionSynchronization, x => x.HasSessionBeenRestarted,
ComputeShowStartSynchronizationObservable)
.ObserveOn(RxApp.MainThreadScheduler)
.ToPropertyEx(this, x => x.ShowStartSynchronizationObservable)
.DisposeWith(disposables);

this.WhenAnyValue(
x => x.IsSynchronizationRunning, x => x.IsCloudSession, x => x.IsSessionCreatedByMe, x => x.HasSynchronizationStarted,
x => x.IsProfileSessionSynchronization, x => x.HasSessionBeenRestarted,
ComputeShowWaitingForSynchronizationStartObservable)
.ObserveOn(RxApp.MainThreadScheduler)
.ToPropertyEx(this, x => x.ShowWaitingForSynchronizationStartObservable)
.DisposeWith(disposables);
});

if (Design.IsDesignMode)
{
return;
}

IsCloudSession = _sessionService.IsCloudSession;
IsSessionCreatedByMe = _sessionMemberRepository.IsCurrentUserFirstSessionMemberCurrentValue;
IsProfileSessionSynchronization = _sessionService.CurrentRunSessionProfileInfo is { AutoStartsSynchronization: true };

if (IsCloudSession)
{
if (IsProfileSessionSynchronization)
{
WaitingForSynchronizationStartMessage = _localizationService[nameof(Resources.SynchronizationMain_WaitingForAutomaticStart_CloudSession)];
}
else if (!IsCloudSessionCreatedByMe)
{
var creatorMachineName = _sessionMemberRepository.Elements.First().MachineName;
WaitingForSynchronizationStartMessage = string.Format(_localizationService[nameof(Resources.SynchronizationMain_WaitingForClientATemplate)], creatorMachineName);
}
}
else
{
if (IsProfileSessionSynchronization)
{
WaitingForSynchronizationStartMessage = _localizationService[nameof(Resources.SynchronizationMain_WaitingForAutomaticStart_LocalSession)];
}
}
}

public ReactiveCommand<Unit, Unit> StartSynchronizationCommand { get; }

[Reactive]
public bool IsSynchronizationRunning { get; set; }

[Reactive]
public bool IsCloudSession { get; set; }

[Reactive]
public bool IsSessionCreatedByMe { get; set; }

[Reactive]
public bool IsProfileSessionSynchronization { get; set; }

[Reactive]
public bool HasSessionBeenRestarted { get; set; }

[Reactive]
public string WaitingForSynchronizationStartMessage { get; set; } = string.Empty;

[Reactive]
public ErrorViewModel StartSynchronizationError { get; set; }

public extern bool ShowStartSynchronizationObservable { [ObservableAsProperty] get; }

public extern bool ShowWaitingForSynchronizationStartObservable { [ObservableAsProperty] get; }

public extern bool HasSynchronizationStarted { [ObservableAsProperty] get; }

private bool IsCloudSessionCreatedByMe => IsCloudSession && IsSessionCreatedByMe;

private bool ComputeShowStartSynchronizationObservable(bool isSynchronizationRunning, bool isCloudSession, bool isSessionCreatedByMe,
bool hasSynchronizationStarted, bool isProfileSessionSynchronization, bool hasSessionBeenRestarted)
{
var result = (!isProfileSessionSynchronization || (!isCloudSession && hasSessionBeenRestarted))
&& !isSynchronizationRunning
&& !hasSynchronizationStarted
&& (!isCloudSession || isSessionCreatedByMe);

return result;
}

private bool ComputeShowWaitingForSynchronizationStartObservable(bool isSynchronizationRunning, bool isCloudSession, bool isSessionCreatedByMe,
bool hasSynchronizationStarted, bool isProfileSessionSynchronization, bool hasSessionBeenRestarted)
{
var result = !isSynchronizationRunning
&& !hasSynchronizationStarted
&& ((isProfileSessionSynchronization && !hasSessionBeenRestarted) || (!isSessionCreatedByMe && isCloudSession));

return result;
}

private async Task StartSynchronization()
{
try
{
StartSynchronizationError.Clear();

await _synchronizationStarter.StartSynchronization(true);
}
catch (Exception ex)
{
_logger.LogError(ex, "SynchronizationBeforeStartViewModel.StartSynchronization");

StartSynchronizationError.SetException(ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using ByteSync.Assets.Resources;
using Avalonia.Controls;
using ByteSync.Business;
using ByteSync.Business.Sessions;
using ByteSync.Common.Business.Synchronizations;
using ByteSync.Interfaces.Controls.Synchronizations;
using ByteSync.Interfaces.Dialogs;
using ByteSync.Interfaces.Services.Sessions;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;

namespace ByteSync.ViewModels.Sessions.Synchronizations;

public class SynchronizationMainStatusViewModel : ActivatableViewModelBase
{
private readonly ISessionService _sessionService = null!;
private readonly ISynchronizationService _synchronizationService = null!;
private readonly IDialogService _dialogService = null!;
private readonly ILogger<SynchronizationMainStatusViewModel> _logger = null!;

public SynchronizationMainStatusViewModel()
{
#if DEBUG
MainStatus = "MainStatus";
IsMainProgressRingVisible = false;
IsMainCheckVisible = false;
#endif
if (Design.IsDesignMode)
{
IsSynchronizationRunning = true;
IsMainCheckVisible = true;
}

MainStatus = Resources.SynchronizationMain_SynchronizationRunning;
IsSynchronizationRunning = false;

AbortSynchronizationCommand = ReactiveCommand.CreateFromTask(AbortSynchronization);

this.WhenActivated(disposables =>
{
_synchronizationService.SynchronizationProcessData.SynchronizationStart
.CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationAbortRequest,
_synchronizationService.SynchronizationProcessData.SynchronizationEnd)
.Where(tuple => tuple.First != null && tuple.Second == null && tuple.Third == null)
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(tuple => OnSynchronizationStarted(tuple.First!))
.DisposeWith(disposables);

_synchronizationService.SynchronizationProcessData.SynchronizationAbortRequest.DistinctUntilChanged()
.Where(synchronizationAbortRequest => synchronizationAbortRequest != null)
.Subscribe(synchronizationAbortRequest => OnSynchronizationAbortRequested(synchronizationAbortRequest!))
.DisposeWith(disposables);

_synchronizationService.SynchronizationProcessData.SynchronizationEnd.DistinctUntilChanged()
.Where(synchronizationEnd => synchronizationEnd != null)
.Subscribe(synchronizationEnd => OnSynchronizationEnded(synchronizationEnd!))
.DisposeWith(disposables);

_sessionService.SessionStatusObservable
.CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationStart)
.Select(tuple =>
!tuple.First.In(SessionStatus.None, SessionStatus.Preparation, SessionStatus.Comparison,
SessionStatus.CloudSessionCreation, SessionStatus.CloudSessionJunction, SessionStatus.Inventory)
&& tuple.Second != null)
.ObserveOn(RxApp.MainThreadScheduler)
.ToPropertyEx(this, x => x.HasSynchronizationStarted)
.DisposeWith(disposables);
});
}

public SynchronizationMainStatusViewModel(ISessionService sessionService, ISynchronizationService synchronizationService,
IDialogService dialogService, ILogger<SynchronizationMainStatusViewModel> logger) : this()
{
_sessionService = sessionService;
_synchronizationService = synchronizationService;
_dialogService = dialogService;
_logger = logger;
}

public ReactiveCommand<Unit, Unit> AbortSynchronizationCommand { get; }

[Reactive]
public bool IsSynchronizationRunning { get; set; }

[Reactive]
public bool IsMainProgressRingVisible { get; set; }

[Reactive]
public bool IsMainCheckVisible { get; set; }

[Reactive]
public string MainIcon { get; set; }

[Reactive]
public string MainStatus { get; set; }

public extern bool HasSynchronizationStarted { [ObservableAsProperty] get; }

private async Task AbortSynchronization()
{
try
{
var messageBoxViewModel = _dialogService.CreateMessageBoxViewModel(
nameof(Resources.SynchronizationMain_AbortSynchronization_Title), nameof(Resources.SynchronizationMain_AbortSynchronization_Message));
messageBoxViewModel.ShowYesNo = true;
var result = await _dialogService.ShowMessageBoxAsync(messageBoxViewModel);

if (result == MessageBoxResult.Yes)
{
await _synchronizationService.AbortSynchronization();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "SynchronizationMainStatusViewModel.AbortSynchronization");
}
}

private void OnSynchronizationStarted(Synchronization _)
{
IsSynchronizationRunning = true;
HandledActionsReset();
IsMainCheckVisible = false;
IsMainProgressRingVisible = true;
MainStatus = Resources.SynchronizationMain_SynchronizationRunning;
}

private void OnSynchronizationAbortRequested(SynchronizationAbortRequest _)
{
if (IsSynchronizationRunning)
{
MainStatus = Resources.SynchronizationMain_SynchronizationAbortRequested;
}
}

private void OnSynchronizationEnded(SynchronizationEnd synchronizationEnd)
{
IsSynchronizationRunning = false;

if (synchronizationEnd.Status == SynchronizationEndStatuses.Abortion)
{
MainStatus = Resources.SynchronizationMain_SynchronizationAborted;
MainIcon = "SolidXCircle";
}
else if (synchronizationEnd.Status == SynchronizationEndStatuses.Error)
{
MainStatus = Resources.SynchronizationMain_SynchronizationError;
MainIcon = "SolidXCircle";
}
else
{
MainStatus = Resources.SynchronizationMain_SynchronizationDone;
MainIcon = "SolidCheckCircle";
}

IsMainProgressRingVisible = false;
IsMainCheckVisible = true;
}

private void HandledActionsReset()
{
}
}
Loading
Loading