/ src / modules / imageresizer / ui / ViewModels / InputViewModel.cs
InputViewModel.cs
  1  #pragma warning disable IDE0073
  2  // Copyright (c) Brice Lambson
  3  // The Brice Lambson licenses this file to you under the MIT license.
  4  // See the LICENSE file in the project root for more information.  Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
  5  #pragma warning restore IDE0073
  6  
  7  using System;
  8  using System.Collections.Generic;
  9  using System.ComponentModel;
 10  using System.Globalization;
 11  using System.IO;
 12  using System.Linq;
 13  using System.Runtime.InteropServices;
 14  using System.Threading.Tasks;
 15  using System.Windows.Input;
 16  using System.Windows.Media.Imaging;
 17  using Common.UI;
 18  using ImageResizer.Helpers;
 19  using ImageResizer.Models;
 20  using ImageResizer.Properties;
 21  using ImageResizer.Services;
 22  using ImageResizer.Views;
 23  
 24  namespace ImageResizer.ViewModels
 25  {
 26      public class InputViewModel : Observable
 27      {
 28          public const int DefaultAiScale = 2;
 29          private const int MinAiScale = 1;
 30          private const int MaxAiScale = 8;
 31  
 32          private readonly ResizeBatch _batch;
 33          private readonly MainViewModel _mainViewModel;
 34          private readonly IMainView _mainView;
 35          private readonly bool _hasMultipleFiles;
 36          private bool _originalDimensionsLoaded;
 37          private int? _originalWidth;
 38          private int? _originalHeight;
 39          private string _currentResolutionDescription;
 40          private string _newResolutionDescription;
 41          private bool _isDownloadingModel;
 42          private string _modelStatusMessage;
 43          private double _modelDownloadProgress;
 44  
 45          public enum Dimension
 46          {
 47              Width,
 48              Height,
 49          }
 50  
 51          public class KeyPressParams
 52          {
 53              public double Value { get; set; }
 54  
 55              public Dimension Dimension { get; set; }
 56          }
 57  
 58          public InputViewModel(
 59              Settings settings,
 60              MainViewModel mainViewModel,
 61              IMainView mainView,
 62              ResizeBatch batch)
 63          {
 64              _batch = batch;
 65              _mainViewModel = mainViewModel;
 66              _mainView = mainView;
 67              _hasMultipleFiles = _batch?.Files.Count > 1;
 68  
 69              Settings = settings;
 70              if (settings != null)
 71              {
 72                  settings.CustomSize.PropertyChanged += (sender, e) => settings.SelectedSize = (CustomSize)sender;
 73                  settings.AiSize.PropertyChanged += (sender, e) =>
 74                  {
 75                      if (e.PropertyName == nameof(AiSize.Scale))
 76                      {
 77                          NotifyAiScaleChanged();
 78                      }
 79                  };
 80                  settings.PropertyChanged += HandleSettingsPropertyChanged;
 81              }
 82  
 83              ResizeCommand = new RelayCommand(Resize, () => CanResize);
 84              CancelCommand = new RelayCommand(Cancel);
 85              OpenSettingsCommand = new RelayCommand(OpenSettings);
 86              EnterKeyPressedCommand = new RelayCommand<KeyPressParams>(HandleEnterKeyPress);
 87              DownloadModelCommand = new RelayCommand(async () => await DownloadModelAsync());
 88  
 89              // Initialize AI UI state based on Settings availability
 90              InitializeAiState();
 91          }
 92  
 93          public Settings Settings { get; }
 94  
 95          public IEnumerable<ResizeFit> ResizeFitValues => Enum.GetValues<ResizeFit>();
 96  
 97          public IEnumerable<ResizeUnit> ResizeUnitValues => Enum.GetValues<ResizeUnit>();
 98  
 99          public int AiSuperResolutionScale
100          {
101              get => Settings?.AiSize?.Scale ?? DefaultAiScale;
102              set
103              {
104                  if (Settings?.AiSize != null && Settings.AiSize.Scale != value)
105                  {
106                      Settings.AiSize.Scale = value;
107                      NotifyAiScaleChanged();
108                  }
109              }
110          }
111  
112          public string AiScaleDisplay => Settings?.AiSize?.ScaleDisplay ?? string.Empty;
113  
114          public string CurrentResolutionDescription
115          {
116              get => _currentResolutionDescription;
117              private set => Set(ref _currentResolutionDescription, value);
118          }
119  
120          public string NewResolutionDescription
121          {
122              get => _newResolutionDescription;
123              private set => Set(ref _newResolutionDescription, value);
124          }
125  
126          // ==================== UI State Properties ====================
127  
128          // Show AI size descriptions only when AI size is selected and not multiple files
129          public bool ShowAiSizeDescriptions => Settings?.SelectedSize is AiSize && !_hasMultipleFiles;
130  
131          // Helper property: Is model currently being downloaded?
132          public bool IsModelDownloading => _isDownloadingModel;
133  
134          public string ModelStatusMessage
135          {
136              get => _modelStatusMessage;
137              private set => Set(ref _modelStatusMessage, value);
138          }
139  
140          public double ModelDownloadProgress
141          {
142              get => _modelDownloadProgress;
143              private set => Set(ref _modelDownloadProgress, value);
144          }
145  
146          // Show download prompt when: AI size is selected and model is not ready (including downloading)
147          public bool ShowModelDownloadPrompt =>
148              Settings?.SelectedSize is AiSize &&
149              (App.AiAvailabilityState == Properties.AiAvailabilityState.ModelNotReady || _isDownloadingModel);
150  
151          // Show AI controls when: AI size is selected and AI is ready
152          public bool ShowAiControls =>
153              Settings?.SelectedSize is AiSize &&
154              App.AiAvailabilityState == Properties.AiAvailabilityState.Ready;
155  
156          /// <summary>
157          /// Gets a value indicating whether the resize operation can proceed.
158          /// For AI resize: only enabled when AI is fully ready.
159          /// For non-AI resize: always enabled.
160          /// </summary>
161          public bool CanResize
162          {
163              get
164              {
165                  // If AI size is selected, only allow resize when AI is fully ready
166                  if (Settings?.SelectedSize is AiSize)
167                  {
168                      return App.AiAvailabilityState == Properties.AiAvailabilityState.Ready;
169                  }
170  
171                  // Non-AI resize can always proceed
172                  return true;
173              }
174          }
175  
176          public ICommand ResizeCommand { get; }
177  
178          public ICommand CancelCommand { get; }
179  
180          public ICommand OpenSettingsCommand { get; }
181  
182          public ICommand EnterKeyPressedCommand { get; private set; }
183  
184          public ICommand DownloadModelCommand { get; private set; }
185  
186          // Any of the files is a gif
187          public bool TryingToResizeGifFiles =>
188                  _batch?.Files.Any(filename => filename.EndsWith(".gif", System.StringComparison.InvariantCultureIgnoreCase)) == true;
189  
190          public void Resize()
191          {
192              Settings.Save();
193              _mainViewModel.CurrentPage = new ProgressViewModel(_batch, _mainViewModel, _mainView);
194          }
195  
196          public static void OpenSettings()
197          {
198              SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.ImageResizer);
199          }
200  
201          private void HandleEnterKeyPress(KeyPressParams parameters)
202          {
203              switch (parameters.Dimension)
204              {
205                  case Dimension.Width:
206                      Settings.CustomSize.Width = parameters.Value;
207                      break;
208                  case Dimension.Height:
209                      Settings.CustomSize.Height = parameters.Value;
210                      break;
211              }
212          }
213  
214          public void Cancel()
215              => _mainView.Close();
216  
217          private void HandleSettingsPropertyChanged(object sender, PropertyChangedEventArgs e)
218          {
219              switch (e.PropertyName)
220              {
221                  case nameof(Settings.SelectedSizeIndex):
222                  case nameof(Settings.SelectedSize):
223                      // Notify UI state properties that depend on SelectedSize
224                      NotifyAiStateChanged();
225                      UpdateAiDetails();
226  
227                      // Trigger CanExecuteChanged for ResizeCommand
228                      if (ResizeCommand is RelayCommand cmd)
229                      {
230                          cmd.OnCanExecuteChanged();
231                      }
232  
233                      break;
234              }
235          }
236  
237          private void EnsureAiScaleWithinRange()
238          {
239              if (Settings?.AiSize != null)
240              {
241                  Settings.AiSize.Scale = Math.Clamp(
242                      Settings.AiSize.Scale,
243                      MinAiScale,
244                      MaxAiScale);
245              }
246          }
247  
248          private void UpdateAiDetails()
249          {
250              // Clear AI details if AI size not selected
251              if (Settings == null || Settings.SelectedSize is not AiSize)
252              {
253                  CurrentResolutionDescription = string.Empty;
254                  NewResolutionDescription = string.Empty;
255                  return;
256              }
257  
258              EnsureAiScaleWithinRange();
259  
260              if (_hasMultipleFiles)
261              {
262                  CurrentResolutionDescription = string.Empty;
263                  NewResolutionDescription = string.Empty;
264                  return;
265              }
266  
267              EnsureOriginalDimensionsLoaded();
268  
269              var hasConcreteSize = _originalWidth.HasValue && _originalHeight.HasValue;
270              CurrentResolutionDescription = hasConcreteSize
271                  ? FormatDimensions(_originalWidth!.Value, _originalHeight!.Value)
272                  : Resources.Input_AiUnknownSize;
273  
274              var scale = Settings.AiSize.Scale;
275              NewResolutionDescription = hasConcreteSize
276                  ? FormatDimensions((long)_originalWidth!.Value * scale, (long)_originalHeight!.Value * scale)
277                  : Resources.Input_AiUnknownSize;
278          }
279  
280          private static string FormatDimensions(long width, long height)
281          {
282              return string.Format(CultureInfo.CurrentCulture, "{0} × {1}", width, height);
283          }
284  
285          private void EnsureOriginalDimensionsLoaded()
286          {
287              if (_originalDimensionsLoaded)
288              {
289                  return;
290              }
291  
292              var file = _batch?.Files.FirstOrDefault();
293              if (string.IsNullOrEmpty(file))
294              {
295                  _originalDimensionsLoaded = true;
296                  return;
297              }
298  
299              try
300              {
301                  using var stream = File.OpenRead(file);
302                  var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
303                  var frame = decoder.Frames.FirstOrDefault();
304                  if (frame != null)
305                  {
306                      _originalWidth = frame.PixelWidth;
307                      _originalHeight = frame.PixelHeight;
308                  }
309              }
310              catch (Exception)
311              {
312                  // Failed to load image dimensions - clear values
313                  _originalWidth = null;
314                  _originalHeight = null;
315              }
316              finally
317              {
318                  _originalDimensionsLoaded = true;
319              }
320          }
321  
322          /// <summary>
323          /// Initializes AI UI state based on App's cached availability state.
324          /// Subscribe to state change event to update UI when background initialization completes.
325          /// </summary>
326          private void InitializeAiState()
327          {
328              // Subscribe to initialization completion event to refresh UI
329              App.AiInitializationCompleted += OnAiInitializationCompleted;
330  
331              // Set initial status message based on current state
332              UpdateStatusMessage();
333          }
334  
335          /// <summary>
336          /// Handles AI initialization completion event from App.
337          /// Refreshes UI when background initialization finishes.
338          /// </summary>
339          private void OnAiInitializationCompleted(object sender, Properties.AiAvailabilityState finalState)
340          {
341              UpdateStatusMessage();
342              NotifyAiStateChanged();
343          }
344  
345          /// <summary>
346          /// Updates status message based on current App availability state.
347          /// </summary>
348          private void UpdateStatusMessage()
349          {
350              ModelStatusMessage = App.AiAvailabilityState switch
351              {
352                  Properties.AiAvailabilityState.Ready => string.Empty,
353                  Properties.AiAvailabilityState.ModelNotReady => Resources.Input_AiModelNotAvailable,
354                  Properties.AiAvailabilityState.NotSupported => Resources.Input_AiModelNotSupported,
355                  _ => string.Empty,
356              };
357          }
358  
359          /// <summary>
360          /// Notifies UI when AI state changes (model availability, download status).
361          /// </summary>
362          private void NotifyAiStateChanged()
363          {
364              OnPropertyChanged(nameof(IsModelDownloading));
365              OnPropertyChanged(nameof(ShowModelDownloadPrompt));
366              OnPropertyChanged(nameof(ShowAiControls));
367              OnPropertyChanged(nameof(ShowAiSizeDescriptions));
368              OnPropertyChanged(nameof(CanResize));
369  
370              // Trigger CanExecuteChanged for ResizeCommand
371              if (ResizeCommand is RelayCommand resizeCommand)
372              {
373                  resizeCommand.OnCanExecuteChanged();
374              }
375          }
376  
377          /// <summary>
378          /// Notifies UI when AI scale changes (slider value).
379          /// </summary>
380          private void NotifyAiScaleChanged()
381          {
382              OnPropertyChanged(nameof(AiSuperResolutionScale));
383              OnPropertyChanged(nameof(AiScaleDisplay));
384              UpdateAiDetails();
385          }
386  
387          private async Task DownloadModelAsync()
388          {
389              try
390              {
391                  // Set downloading flag and show progress
392                  _isDownloadingModel = true;
393                  ModelStatusMessage = Resources.Input_AiModelDownloading;
394                  ModelDownloadProgress = 0;
395                  NotifyAiStateChanged();
396  
397                  // Create progress reporter to update UI
398                  var progress = new Progress<double>(value =>
399                  {
400                      // progressValue could be 0-1 or 0-100, normalize to 0-100
401                      ModelDownloadProgress = value > 1 ? value : value * 100;
402                  });
403  
404                  // Call EnsureReadyAsync to download and prepare the AI model
405                  var result = await WinAiSuperResolutionService.EnsureModelReadyAsync(progress);
406  
407                  if (result?.Status == Microsoft.Windows.AI.AIFeatureReadyResultState.Success)
408                  {
409                      // Model successfully downloaded and ready
410                      ModelDownloadProgress = 100;
411  
412                      // Update App's cached state
413                      App.AiAvailabilityState = Properties.AiAvailabilityState.Ready;
414                      UpdateStatusMessage();
415  
416                      // Initialize the AI service now that model is ready
417                      var aiService = await WinAiSuperResolutionService.CreateAsync();
418                      ResizeBatch.SetAiSuperResolutionService(aiService ?? (Services.IAISuperResolutionService)NoOpAiSuperResolutionService.Instance);
419                  }
420                  else
421                  {
422                      // Download failed
423                      ModelStatusMessage = Resources.Input_AiModelDownloadFailed;
424                  }
425              }
426              catch (Exception)
427              {
428                  // Exception during download
429                  ModelStatusMessage = Resources.Input_AiModelDownloadFailed;
430              }
431              finally
432              {
433                  // Clear downloading flag
434                  _isDownloadingModel = false;
435  
436                  // Reset progress if not successful
437                  if (App.AiAvailabilityState != Properties.AiAvailabilityState.Ready)
438                  {
439                      ModelDownloadProgress = 0;
440                  }
441  
442                  NotifyAiStateChanged();
443              }
444          }
445      }
446  }