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 }