1using System; 2using System.Collections.Generic; 3using System.Drawing; 4using System.Drawing.Drawing2D; 5using System.Drawing.Imaging; 6using System.Drawing.Text; 7using System.IO; 8using System.Linq; 9using System.Runtime.InteropServices; 10using System.Windows.Forms; 11using WindowCapture.App; 12using WindowCapture.Detection; 13using WindowCapture.Effects; 14using WindowCapture.Helpers; 15using WindowCapture.Integration; 16using WindowCapture.Models; 17using WindowCapture.Native; 18 19namespace WindowCapture.UI 20{ 21 public partial class EditorForm 22 { 23 // ===== Viewer mode (image/file browsing) ===== 24 25 /// <summary>Load GIF without cloning — preserves animation frames for ImageAnimator.</summary> 26 private Image LoadGifImage(string path) 27 { 28 // Must copy to MemoryStream — Image.FromFile locks the file 29 var data = File.ReadAllBytes(path); 30 var ms = new MemoryStream(data); 31 return Image.FromStream(ms); // ms must stay alive while Image is alive 32 } 33 34 private Bitmap LoadImageFromFile(string path) 35 { 36 try 37 { 38 using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) 39 { 40 // Read entire file into memory first to avoid stream contention 41 var ms = new MemoryStream(); 42 fs.CopyTo(ms); 43 ms.Position = 0; 44 using (var temp = new Bitmap(ms)) 45 return BitmapHelper.Clone32(temp); 46 } 47 } 48 catch 49 { 50 // Return a small placeholder on failure (corrupted file, race condition, etc.) 51 var placeholder = new Bitmap(1, 1, System.Drawing.Imaging.PixelFormat.Format32bppArgb); 52 return placeholder; 53 } 54 } 55 56 private void ScanFolderFiles(string filePath) 57 { 58 string dir = Path.GetDirectoryName(filePath); 59 string ext = Path.GetExtension(filePath).ToLowerInvariant(); 60 61 // Only show files of the same type as the opened file 62 string[] allowedExts; 63 if (VideoExtensions.Contains(ext)) 64 allowedExts = VideoExtensions; 65 else if (AudioExtensions.Contains(ext)) 66 allowedExts = AudioExtensions; 67 else 68 allowedExts = ImageExtensions; 69 70 var files = Directory.GetFiles(dir) 71 .Where(f => allowedExts.Contains(Path.GetExtension(f).ToLowerInvariant())) 72 .OrderBy(f => f, StringComparer.OrdinalIgnoreCase) 73 .ToArray(); 74 folderFiles = files; 75 currentFileIndex = Array.FindIndex(folderFiles, f => 76 string.Equals(f, filePath, StringComparison.OrdinalIgnoreCase)); 77 if (currentFileIndex < 0) currentFileIndex = 0; 78 } 79 80 private void SetupFileNameOverlay() 81 { 82 // No more Label — file name is now painted in glass style via PaintFileNameIndicator 83 fileNameFadeTimer = new System.Windows.Forms.Timer(); 84 fileNameFadeTimer.Interval = 16; 85 fileNameFadeTimer.Tick += FileNameFadeTimer_Tick; 86 } 87 88 private DateTime fileNameShowTime; 89 90 private void ShowFileName() 91 { 92 if (currentFilePath == null) return; 93 fileNameAlpha = 1f; 94 fileNameShowTime = DateTime.Now; 95 fileNameFadeTimer.Start(); 96 // Trigger repaint on the appropriate surface 97 if (videoOverlay != null) videoOverlay.Invalidate(new Rectangle(0, 0, 400, 80)); 98 else if (scrollContainer != null && scrollContainer.IsHandleCreated) scrollContainer.Invalidate(new Rectangle(0, 0, 400, 80)); 99 if (canvas != null && canvas.IsHandleCreated) canvas.Invalidate(new Rectangle(0, 0, 400, 80)); 100 } 101 102 private void FileNameFadeTimer_Tick(object sender, EventArgs e) 103 { 104 double elapsed = (DateTime.Now - fileNameShowTime).TotalMilliseconds; 105 if (elapsed < 2000) return; 106 double fadeMs = elapsed - 2000; 107 float alpha = 1f - (float)(fadeMs / 1000.0); 108 if (alpha <= 0) 109 { 110 fileNameAlpha = 0; 111 fileNameFadeTimer.Stop(); 112 } 113 else 114 fileNameAlpha = alpha; 115 // Repaint 116 if (videoOverlay != null) videoOverlay.Invalidate(new Rectangle(0, 0, 400, 80)); 117 else if (scrollContainer != null && scrollContainer.IsHandleCreated) scrollContainer.Invalidate(new Rectangle(0, 0, 400, 80)); 118 if (canvas != null && canvas.IsHandleCreated) canvas.Invalidate(new Rectangle(0, 0, 400, 80)); 119 } 120 121 // Viewer state 122 private DateTime lastBrowseTime; 123 private System.Windows.Forms.Timer delayedNameTimer; 124 125 private bool browsing; // guard against reentrant calls 126 127 public void BrowseFile(int direction) 128 { 129 if (folderFiles == null || folderFiles.Length <= 1) return; 130 if (browsing) return; 131 browsing = true; 132 try { BrowseFileInternal(direction); } finally { browsing = false; } 133 } 134 135 private void BrowseFileInternal(int direction) 136 { 137 138 int newIndex = currentFileIndex + direction; 139 if (newIndex < 0) newIndex = folderFiles.Length - 1; 140 if (newIndex >= folderFiles.Length) newIndex = 0; 141 142 if (newIndex == currentFileIndex) return; 143 currentFileIndex = newIndex; 144 currentFilePath = folderFiles[currentFileIndex]; 145 146 PerformanceLogger.Log("BrowseFile", -1, string.Format("Index: {0}, Path: {1}", currentFileIndex, currentFilePath)); 147 148 string ext = Path.GetExtension(currentFilePath).ToLowerInvariant(); 149 bool nowVideo = VideoExtensions.Contains(ext); 150 bool nowAudio = AudioExtensions.Contains(ext); 151 152 bool nowGif = !nowVideo && !nowAudio && MediaTypes.IsAnimatedGif(currentFilePath); 153 154 if (nowGif) 155 { 156 isAnimatedGif = true; 157 isVideoFile = false; 158 isAudioFile = false; 159 160 // Clean up video player if it was active 161 if (dsMedia != null || dsPanel != null || videoBrowser != null) 162 { 163 DsCleanup(); 164 if (videoUpdateTimer != null) { videoUpdateTimer.Stop(); videoUpdateTimer.Dispose(); videoUpdateTimer = null; } 165 if (videoControlsFadeTimer != null) { videoControlsFadeTimer.Stop(); videoControlsFadeTimer.Dispose(); videoControlsFadeTimer = null; } 166 if (videoOverlayForm != null) { videoOverlayForm.Close(); videoOverlayForm.Dispose(); videoOverlayForm = null; } 167 if (videoOverlay != null) { scrollContainer.Controls.Remove(videoOverlay); videoOverlay.Dispose(); videoOverlay = null; } 168 if (dsPanel != null) { scrollContainer.Controls.Remove(dsPanel); dsPanel.Dispose(); dsPanel = null; } 169 if (canvas != null) canvas.Visible = true; 170 } 171 172 // Load and animate GIF on canvas 173 if (gifImage != null) { gifImage.Dispose(); gifImage = null; } 174 gifImage = LoadGifImage(currentFilePath); 175 bounds = new Rectangle(0, 0, gifImage.Width, gifImage.Height); 176 cropRect = bounds; 177 if (clipPath != null) clipPath.Dispose(); 178 clipPath = new GraphicsPath(); 179 clipPath.AddRectangle(bounds); 180 if (captured != null) captured.Dispose(); 181 captured = new Bitmap(gifImage.Width, gifImage.Height, PixelFormat.Format32bppArgb); 182 using (var g = Graphics.FromImage(captured)) g.DrawImage(gifImage, 0, 0); 183 184 canvas.SetAnimatedGif(gifImage); 185 D2DRenderer.UploadBitmap(captured); 186 FitZoomToViewport(); 187 CenterImage(); 188 ShowFileName(); 189 return; 190 } 191 192 isAnimatedGif = false; 193 if (gifImage != null) { canvas.StopGifAnimation(); gifImage.Dispose(); gifImage = null; } 194 195 if (nowVideo || nowAudio) 196 { 197 isVideoFile = nowVideo; 198 isAudioFile = nowAudio; 199 // isAnimatedGif already set above 200 201 // Switch to new video/audio 202 if (dsMedia != null) 203 { 204 NavigateVideo(currentFilePath); // DsCleanup inside 205 } 206 else 207 { 208 // First time switching from image to video 209 if (canvas != null) canvas.Visible = false; 210 SetupVideoPlayer(currentFilePath); 211 } 212 ShowFileName(); 213 return; 214 } 215 else 216 { 217 // Switch to image 218 isVideoFile = false; 219 isAudioFile = false; 220 isAnimatedGif = false; 221 222 // Clean up video player if it was active 223 if (dsMedia != null || dsPanel != null || videoBrowser != null) 224 { 225 DsCleanup(); 226 if (videoUpdateTimer != null) { videoUpdateTimer.Stop(); videoUpdateTimer.Dispose(); videoUpdateTimer = null; } 227 if (videoControlsFadeTimer != null) { videoControlsFadeTimer.Stop(); videoControlsFadeTimer.Dispose(); videoControlsFadeTimer = null; } 228 if (videoOverlayForm != null) { videoOverlayForm.Close(); videoOverlayForm.Dispose(); videoOverlayForm = null; } 229 if (videoOverlay != null) { scrollContainer.Controls.Remove(videoOverlay); videoOverlay.Dispose(); videoOverlay = null; } 230 if (dsPanel != null) { scrollContainer.Controls.Remove(dsPanel); dsPanel.Dispose(); dsPanel = null; } 231 if (canvas != null) canvas.Visible = true; 232 } 233 234 Bitmap img = null; 235 lock (imageCache) 236 { 237 if (imageCache.ContainsKey(currentFileIndex)) 238 img = (Bitmap)imageCache[currentFileIndex].Clone(); 239 } 240 241 if (img == null) 242 img = LoadImageFromFile(currentFilePath); 243 244 UpdateImageContent(img); 245 246 // Preload neighbors in background 247 PreloadNeighborImages(); 248 } 249 250 // Delayed name show 251 if (delayedNameTimer == null) 252 { 253 delayedNameTimer = new System.Windows.Forms.Timer { Interval = 400 }; 254 delayedNameTimer.Tick += (s, e) => { delayedNameTimer.Stop(); ShowFileName(); }; 255 } 256 delayedNameTimer.Stop(); 257 delayedNameTimer.Start(); 258 } 259 260 private void UpdateImageContent(Bitmap img) 261 { 262 using (PerformanceLogger.Measure("UpdateImageContent")) 263 { 264 if (captured != null) captured.Dispose(); 265 captured = img; 266 bounds = new Rectangle(0, 0, img.Width, img.Height); 267 cropRect = bounds; 268 269 if (clipPath != null) clipPath.Dispose(); 270 clipPath = new GraphicsPath(); 271 clipPath.AddRectangle(bounds); 272 273 // Optimization: Don't recreate blur bitmap during fast browsing 274 if (DateTime.Now.Subtract(lastBrowseTime).TotalMilliseconds > 200) 275 CreateBlurredBitmap(); 276 277 if (canvas != null) 278 canvas.SetOriginalBitmap(captured); 279 280 D2DRenderer.UploadBitmap(captured); 281 282 // Fit zoom to current viewport (preserves aspect ratio) 283 FitZoomToViewport(); 284 CenterImage(); 285 lastBrowseTime = DateTime.Now; 286 } 287 } 288 289 /// <summary>Calculate zoom so the image fits entirely within the current viewport, preserving aspect ratio.</summary> 290 private void FitZoomToViewport() 291 { 292 int vw, vh; 293 if (scrollContainer != null && scrollContainer.IsHandleCreated) 294 { 295 vw = scrollContainer.ClientSize.Width; 296 vh = scrollContainer.ClientSize.Height; 297 } 298 else 299 { 300 vw = Width - 40; 301 vh = Height - 40; 302 } 303 if (vw < 10) vw = Width; 304 if (vh < 10) vh = Height; 305 306 float scaleW = (float)vw / bounds.Width; 307 float scaleH = (float)vh / bounds.Height; 308 float fit = Math.Min(scaleW, scaleH); 309 if (fit > 1.0f) fit = 1.0f; // Don't upscale small images 310 311 zoomLevel = fit; 312 targetZoom = fit; 313 } 314 315 // Background preloading of neighboring images for instant browsing 316 private void PreloadNeighborImages() 317 { 318 if (folderFiles == null || imageCache == null) return; 319 int curIdx = currentFileIndex; 320 System.Threading.ThreadPool.QueueUserWorkItem(delegate { 321 for (int offset = 1; offset <= CacheRadius; offset++) 322 { 323 foreach (int dir in new[] { -1, 1 }) 324 { 325 int idx = (curIdx + offset * dir + folderFiles.Length) % folderFiles.Length; 326 lock (imageCache) { if (imageCache.ContainsKey(idx)) continue; } 327 string ext2 = Path.GetExtension(folderFiles[idx]).ToLowerInvariant(); 328 if (VideoExtensions.Contains(ext2) || AudioExtensions.Contains(ext2)) continue; 329 try 330 { 331 var bmp = LoadImageFromFile(folderFiles[idx]); 332 lock (imageCache) { imageCache[idx] = bmp; } 333 } 334 catch { } 335 } 336 } 337 // Evict distant entries 338 lock (imageCache) 339 { 340 var keys = new List<int>(imageCache.Keys); 341 foreach (var k in keys) 342 { 343 int dist = Math.Abs(k - curIdx); 344 dist = Math.Min(dist, folderFiles.Length - dist); 345 if (dist > CacheRadius + 2) 346 { 347 imageCache[k].Dispose(); 348 imageCache.Remove(k); 349 } 350 } 351 } 352 }); 353 } 354 355 // Static helper to open a file in viewer mode 356 public static void OpenFile(string path) 357 { 358 var form = new EditorForm(path); 359 form.Show(); 360 } 361 } 362}