443 lines
15 KiB
C#
443 lines
15 KiB
C#
using MainShell.Common;
|
||
using MainShell.Models;
|
||
using MainShell.Recipe.Models;
|
||
using Newtonsoft.Json.Linq;
|
||
using Stylet;
|
||
using System;
|
||
using System.Collections.ObjectModel;
|
||
using System.IO;
|
||
using System.Linq;
|
||
|
||
namespace MainShell.Recipe.ViewModel
|
||
{
|
||
public abstract class RecipeViewModelBase<TRecipe> : BaseScreen, IRecipeViewModel
|
||
where TRecipe : RecipeBase, new()
|
||
{
|
||
private string _lastSavedSnapshot = string.Empty;
|
||
protected string LastSavedSnapshot => _lastSavedSnapshot;
|
||
private bool _skipLeaveCheck;
|
||
private RecipeWrap _selectedRecipeWrap;
|
||
|
||
public ObservableCollection<RecipeWrap> RecipeWraps { get; protected set; } = new ObservableCollection<RecipeWrap>();
|
||
|
||
public RecipeWrap SelectedRecipeWrap
|
||
{
|
||
get { return _selectedRecipeWrap; }
|
||
set
|
||
{
|
||
if (_selectedRecipeWrap == value)
|
||
return;
|
||
|
||
if (!_skipLeaveCheck && _selectedRecipeWrap != null && value != null && !TryLeaveCurrentRecipeContext())
|
||
{
|
||
NotifyOfPropertyChange(() => SelectedRecipeWrap);
|
||
return;
|
||
}
|
||
|
||
if (SetAndNotify(ref _selectedRecipeWrap, value))
|
||
{
|
||
LoadRecipeFromWrap(value);
|
||
}
|
||
}
|
||
}
|
||
|
||
public RecipeButtonGroupViewModel RecipeButtonGroupViewModel { get; protected set; }
|
||
public virtual bool CanCopyRecipe => true;
|
||
public virtual bool CanImportRecipe => false;
|
||
public virtual bool CanExportRecipe => false;
|
||
public bool HasUnsavedChanges => IsRecipeDirty();
|
||
public string CurrentRecipeDisplayName => CurrentRecipe != null ? CurrentRecipe.RecipeName : EmptyRecipeDisplayName;
|
||
public RecipeManager RecipeManager { get; }
|
||
|
||
protected RecipeViewModelBase(RecipeManager recipeManager)
|
||
{
|
||
RecipeManager = recipeManager;
|
||
}
|
||
|
||
protected abstract TRecipe CurrentRecipe { get; set; }
|
||
protected abstract string RecipeFolderPath { get; }
|
||
protected abstract string EmptyRecipeDisplayName { get; }
|
||
protected abstract string GetActiveRecipeName();
|
||
protected abstract void SwitchActiveRecipe(string recipeName);
|
||
protected abstract void ClearActiveRecipe();
|
||
protected virtual void OnRecipeLoaded(TRecipe recipe, RecipeWrap recipeWrap) { }
|
||
protected virtual string GetUnsavedChangesMessage() => $"<22>䷽<EFBFBD><E4B7BD>{CurrentRecipeDisplayName}<7D><><EFBFBD><EFBFBD>δ<EFBFBD><CEB4><EFBFBD><EFBFBD><EFBFBD>ģ<DEB8><C4A3>Ƿ<EFBFBD><C7B7>ȱ<EFBFBD><C8B1>棿";
|
||
protected virtual void AfterDeleteCurrentRecipe(string deletedRecipeName) { }
|
||
protected virtual void AfterRecipeRenamed(string oldRecipeName, string newRecipeName) { }
|
||
|
||
public virtual bool TryLeaveCurrentRecipeContext()
|
||
{
|
||
if (!HasUnsavedChanges)
|
||
return true;
|
||
|
||
var result = MwMessageBox.Show(GetUnsavedChangesMessage(), "δ<><CEB4><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʾ", System.Windows.MessageBoxButton.YesNoCancel, System.Windows.MessageBoxImage.Warning);
|
||
if (result == System.Windows.MessageBoxResult.Cancel)
|
||
return false;
|
||
|
||
if (result == System.Windows.MessageBoxResult.Yes)
|
||
SaveCurrentRecipe(false);
|
||
|
||
return true;
|
||
}
|
||
|
||
public void SaveCurrentRecipeSilently()
|
||
{
|
||
SaveCurrentRecipe(false);
|
||
}
|
||
|
||
protected void RegisterButtonGroupEvents()
|
||
{
|
||
RecipeButtonGroupViewModel = new RecipeButtonGroupViewModel();
|
||
RecipeButtonGroupViewModel.CreateNewRequested -= OnCreateNewRecipe;
|
||
RecipeButtonGroupViewModel.ApplyRequested -= OnApplyRecipe;
|
||
RecipeButtonGroupViewModel.DeleteRequested -= OnDeleteRecipe;
|
||
RecipeButtonGroupViewModel.CopyRequested -= OnCopyRecipe;
|
||
RecipeButtonGroupViewModel.ImportRequested -= OnImportRecipe;
|
||
RecipeButtonGroupViewModel.SaveRequested -= OnSaveRecipe;
|
||
RecipeButtonGroupViewModel.ExportRequested -= OnExportRecipe;
|
||
RecipeButtonGroupViewModel.ReNameRequested -= OnReNameRecipe;
|
||
RecipeButtonGroupViewModel.ClearRequested -= OnClearRecipe;
|
||
RecipeButtonGroupViewModel.CreateNewRequested += OnCreateNewRecipe;
|
||
RecipeButtonGroupViewModel.ApplyRequested += OnApplyRecipe;
|
||
RecipeButtonGroupViewModel.DeleteRequested += OnDeleteRecipe;
|
||
RecipeButtonGroupViewModel.CopyRequested += OnCopyRecipe;
|
||
RecipeButtonGroupViewModel.ImportRequested += OnImportRecipe;
|
||
RecipeButtonGroupViewModel.SaveRequested += OnSaveRecipe;
|
||
RecipeButtonGroupViewModel.ExportRequested += OnExportRecipe;
|
||
RecipeButtonGroupViewModel.ReNameRequested += OnReNameRecipe;
|
||
RecipeButtonGroupViewModel.ClearRequested += OnClearRecipe;
|
||
}
|
||
|
||
public virtual void OnCreateNewRecipe(object sender, EventArgs eventArgs)
|
||
{
|
||
if (eventArgs is RecipeEventArgs args)
|
||
{
|
||
if (!TryLeaveCurrentRecipeContext())
|
||
return;
|
||
|
||
var recipeWrap = new RecipeWrap
|
||
{
|
||
RecipeName = args.RecipeName,
|
||
CreateTime = DateTime.UtcNow,
|
||
ModifiedTime = DateTime.UtcNow,
|
||
};
|
||
|
||
RecipeWraps.Add(recipeWrap);
|
||
SelectRecipeWrapInternal(recipeWrap);
|
||
}
|
||
}
|
||
|
||
public virtual void OnDeleteRecipe(object sender, EventArgs eventArgs)
|
||
{
|
||
if (SelectedRecipeWrap == null)
|
||
return;
|
||
|
||
if (!TryLeaveCurrentRecipeContext())
|
||
return;
|
||
|
||
var deletedRecipeName = SelectedRecipeWrap.RecipeName;
|
||
if (!RecipeManager.DeleteRecipeFolder(RecipeFolderPath, deletedRecipeName, out var error))
|
||
{
|
||
MwMessageBox.Show(error);
|
||
return;
|
||
}
|
||
|
||
RecipeWraps.Remove(SelectedRecipeWrap);
|
||
AfterDeleteCurrentRecipe(deletedRecipeName);
|
||
|
||
if (RecipeWraps.Count > 0)
|
||
{
|
||
SelectRecipeWrapInternal(RecipeWraps[0]);
|
||
}
|
||
else
|
||
{
|
||
SelectedRecipeWrap = null;
|
||
CurrentRecipe = null;
|
||
ClearActiveRecipe();
|
||
CaptureSavedSnapshot();
|
||
}
|
||
}
|
||
|
||
public virtual void OnCopyRecipe(object sender, EventArgs eventArgs)
|
||
{
|
||
if (!(eventArgs is RecipeEventArgs args) || SelectedRecipeWrap == null)
|
||
return;
|
||
|
||
var sourceRecipeName = SelectedRecipeWrap.RecipeName;
|
||
if (string.IsNullOrWhiteSpace(sourceRecipeName) || string.IsNullOrWhiteSpace(args.RecipeName))
|
||
return;
|
||
|
||
if (CurrentRecipe != null &&
|
||
string.Equals(CurrentRecipe.RecipeName, sourceRecipeName, StringComparison.Ordinal) &&
|
||
HasUnsavedChanges)
|
||
{
|
||
SaveCurrentRecipe(false);
|
||
}
|
||
|
||
if (!RecipeManager.CopyRecipeFolder(RecipeFolderPath, sourceRecipeName, args.RecipeName, out var error))
|
||
{
|
||
MwMessageBox.Show(error);
|
||
return;
|
||
}
|
||
|
||
var copiedRecipeWrap = new RecipeWrap
|
||
{
|
||
RecipeName = args.RecipeName,
|
||
CreateTime = DateTime.UtcNow,
|
||
ModifiedTime = DateTime.UtcNow,
|
||
};
|
||
|
||
RecipeWraps.Add(copiedRecipeWrap);
|
||
SelectRecipeWrapInternal(copiedRecipeWrap);
|
||
}
|
||
|
||
public virtual void OnApplyRecipe(object sender, EventArgs eventArgs)
|
||
{
|
||
if (eventArgs is RecipeEventArgs args)
|
||
{
|
||
var recipeWrap = RecipeWraps.FirstOrDefault(p => p.RecipeName == args.RecipeName);
|
||
if (recipeWrap == null)
|
||
return;
|
||
|
||
if (!ReferenceEquals(SelectedRecipeWrap, recipeWrap))
|
||
{
|
||
if (!TryLeaveCurrentRecipeContext())
|
||
return;
|
||
|
||
SelectRecipeWrapInternal(recipeWrap);
|
||
}
|
||
|
||
if (SelectedRecipeWrap == null)
|
||
return;
|
||
|
||
if (CurrentRecipe != null && IsRecipeDirty())
|
||
{
|
||
SaveCurrentRecipe(false);
|
||
}
|
||
|
||
SwitchActiveRecipe(SelectedRecipeWrap.RecipeName);
|
||
UpdateInUseState(SelectedRecipeWrap);
|
||
}
|
||
}
|
||
|
||
public abstract void OnImportRecipe(object sender, EventArgs eventArgs);
|
||
|
||
public virtual void OnSaveRecipe(object sender, EventArgs eventArgs)
|
||
{
|
||
CommonUti.RunOnUi(() =>
|
||
{
|
||
if (eventArgs is RecipeEventArgs)
|
||
{
|
||
SaveCurrentRecipe(true);
|
||
}
|
||
});
|
||
}
|
||
|
||
public abstract void OnExportRecipe(object sender, EventArgs eventArgs);
|
||
public abstract void OnReNameRecipe(object sender, EventArgs eventArgs);
|
||
|
||
public virtual void OnClearRecipe(object sender, EventArgs eventArgs)
|
||
{
|
||
if (!TryLeaveCurrentRecipeContext())
|
||
return;
|
||
|
||
if (SelectedRecipeWrap == null)
|
||
return;
|
||
|
||
var clearedRecipe = new TRecipe
|
||
{
|
||
RecipeName = SelectedRecipeWrap.RecipeName
|
||
};
|
||
|
||
CurrentRecipe = clearedRecipe;
|
||
OnRecipeLoaded(clearedRecipe, SelectedRecipeWrap);
|
||
NotifyOfPropertyChange(() => HasUnsavedChanges);
|
||
}
|
||
|
||
protected bool IsRecipeDirty()
|
||
{
|
||
if (CurrentRecipe == null)
|
||
return false;
|
||
|
||
var path = Path.Combine(CurrentRecipe.Dir ?? string.Empty, CurrentRecipe.FileName ?? string.Empty);
|
||
if (!File.Exists(path))
|
||
return true;
|
||
|
||
return !AreSnapshotsEquivalent(_lastSavedSnapshot, CurrentRecipe.CreateSnapshot());
|
||
}
|
||
|
||
protected void CaptureSavedSnapshot()
|
||
{
|
||
_lastSavedSnapshot = CurrentRecipe != null ? CurrentRecipe.CreateSnapshot() : string.Empty;
|
||
NotifyOfPropertyChange(() => HasUnsavedChanges);
|
||
NotifyOfPropertyChange(() => CurrentRecipeDisplayName);
|
||
}
|
||
|
||
protected bool TryRenameRecipe(RecipeRenameEventArgs args)
|
||
{
|
||
if (args == null)
|
||
return false;
|
||
|
||
var recipeWrap = RecipeWraps.FirstOrDefault(p => p.RecipeName == args.OldRecipeName);
|
||
if (recipeWrap == null)
|
||
return false;
|
||
|
||
if (!RecipeManager.RenameFolder(RecipeFolderPath, args.OldRecipeName, args.NewRecipeName, out var error))
|
||
{
|
||
MwMessageBox.Show(error);
|
||
return false;
|
||
}
|
||
|
||
recipeWrap.RecipeName = args.NewRecipeName;
|
||
recipeWrap.ModifiedTime = DateTime.UtcNow;
|
||
|
||
if (CurrentRecipe != null && string.Equals(CurrentRecipe.RecipeName, args.OldRecipeName, StringComparison.Ordinal))
|
||
{
|
||
CurrentRecipe.RecipeName = args.NewRecipeName;
|
||
}
|
||
|
||
if (ReferenceEquals(recipeWrap, SelectedRecipeWrap))
|
||
{
|
||
SwitchActiveRecipe(args.NewRecipeName);
|
||
}
|
||
|
||
CaptureSavedSnapshot();
|
||
AfterRecipeRenamed(args.OldRecipeName, args.NewRecipeName);
|
||
return true;
|
||
}
|
||
|
||
protected void SaveCurrentRecipe(bool showSuccessMessage)
|
||
{
|
||
if (CurrentRecipe == null)
|
||
return;
|
||
|
||
CurrentRecipe.Write();
|
||
CaptureSavedSnapshot();
|
||
|
||
if (SelectedRecipeWrap != null)
|
||
{
|
||
SelectedRecipeWrap.ModifiedTime = DateTime.UtcNow;
|
||
}
|
||
|
||
if (showSuccessMessage)
|
||
{
|
||
MwMessageBox.Show("<22><><EFBFBD><EFBFBD><EFBFBD>ɹ<EFBFBD>");
|
||
}
|
||
}
|
||
|
||
private static bool AreSnapshotsEquivalent(string leftSnapshot, string rightSnapshot)
|
||
{
|
||
var left = string.IsNullOrWhiteSpace(leftSnapshot) ? null : ParseSnapshotToken(leftSnapshot);
|
||
var right = string.IsNullOrWhiteSpace(rightSnapshot) ? null : ParseSnapshotToken(rightSnapshot);
|
||
|
||
if (left == null || right == null)
|
||
return false;
|
||
|
||
return JToken.DeepEquals(left, right);
|
||
}
|
||
|
||
private static JToken ParseSnapshotToken(string snapshot)
|
||
{
|
||
try
|
||
{
|
||
return JToken.Parse(snapshot);
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
protected void SelectRecipeWrapInternal(RecipeWrap recipeWrap)
|
||
{
|
||
try
|
||
{
|
||
_skipLeaveCheck = true;
|
||
SelectedRecipeWrap = recipeWrap;
|
||
}
|
||
finally
|
||
{
|
||
_skipLeaveCheck = false;
|
||
}
|
||
}
|
||
|
||
protected void InitializeSelection(string activeRecipeName)
|
||
{
|
||
SyncSelection(activeRecipeName, true);
|
||
}
|
||
|
||
protected virtual void UpdateInUseState(RecipeWrap activeRecipeWrap)
|
||
{
|
||
foreach (var wrap in RecipeWraps)
|
||
{
|
||
wrap.IsInUse = ReferenceEquals(wrap, activeRecipeWrap);
|
||
}
|
||
}
|
||
|
||
public void SyncSelectionFromActiveRecipe()
|
||
{
|
||
SyncSelection(GetActiveRecipeName(), false);
|
||
}
|
||
|
||
private void SyncSelection(string recipeName, bool fallbackToFirst)
|
||
{
|
||
if (RecipeWraps == null || RecipeWraps.Count == 0)
|
||
{
|
||
UpdateInUseState(null);
|
||
return;
|
||
}
|
||
|
||
var target = string.IsNullOrWhiteSpace(recipeName)
|
||
? null
|
||
: RecipeWraps.FirstOrDefault(p => string.Equals(p.RecipeName, recipeName, StringComparison.Ordinal));
|
||
|
||
if (target == null)
|
||
{
|
||
UpdateInUseState(null);
|
||
if (!fallbackToFirst)
|
||
return;
|
||
|
||
target = RecipeWraps[0];
|
||
}
|
||
|
||
if (ReferenceEquals(SelectedRecipeWrap, target))
|
||
{
|
||
UpdateInUseState(target);
|
||
return;
|
||
}
|
||
|
||
SelectRecipeWrapInternal(target);
|
||
}
|
||
|
||
private void LoadRecipeFromWrap(RecipeWrap recipeWrap)
|
||
{
|
||
if (recipeWrap == null)
|
||
{
|
||
UpdateInUseStateByActiveRecipe();
|
||
return;
|
||
}
|
||
|
||
var newRecipe = new TRecipe
|
||
{
|
||
RecipeName = recipeWrap.RecipeName
|
||
};
|
||
newRecipe.Read();
|
||
|
||
CurrentRecipe = newRecipe;
|
||
CaptureSavedSnapshot();
|
||
UpdateInUseStateByActiveRecipe();
|
||
OnRecipeLoaded(newRecipe, recipeWrap);
|
||
}
|
||
|
||
private void UpdateInUseStateByActiveRecipe()
|
||
{
|
||
var activeRecipeName = GetActiveRecipeName();
|
||
var activeRecipeWrap = RecipeWraps == null || string.IsNullOrWhiteSpace(activeRecipeName)
|
||
? null
|
||
: RecipeWraps.FirstOrDefault(p => string.Equals(p.RecipeName, activeRecipeName, StringComparison.Ordinal));
|
||
|
||
UpdateInUseState(activeRecipeWrap);
|
||
}
|
||
}
|
||
}
|