Skip to content

Commit a8a9d55

Browse files
authored
Merge pull request #80 from leotrinh/feature/capture-history
feat(library): add screenshot history library with tray integration
2 parents f9fe73b + 4c4aa5a commit a8a9d55

19 files changed

Lines changed: 1130 additions & 18 deletions

File tree

app.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"golang.org/x/image/webp"
2020
"winshot/internal/config"
2121
"winshot/internal/hotkeys"
22+
"winshot/internal/library"
2223
"winshot/internal/overlay"
2324
"winshot/internal/screenshot"
2425
"winshot/internal/tray"
@@ -155,6 +156,12 @@ func (a *App) onTrayMenu(menuID int) {
155156
runtime.EventsEmit(a.ctx, "hotkey:region")
156157
case tray.MenuWindow:
157158
runtime.EventsEmit(a.ctx, "hotkey:window")
159+
case tray.MenuLibrary:
160+
// Show main window first so library modal has context
161+
runtime.WindowShow(a.ctx)
162+
a.isWindowHidden = false
163+
// Emit event to open library window
164+
runtime.EventsEmit(a.ctx, "tray:library")
158165
case tray.MenuQuit:
159166
// Quit the application - use goroutine to avoid blocking tray menu
160167
go func() {
@@ -933,3 +940,111 @@ func (a *App) ClearGDriveCredentials() error {
933940
a.credManager.Delete(upload.CredGDriveToken)
934941
return nil
935942
}
943+
944+
// ==================== Screenshot Library ====================
945+
946+
// GetLibraryImages returns all screenshots from QuickSave folder
947+
func (a *App) GetLibraryImages() ([]library.LibraryImage, error) {
948+
folder := a.config.QuickSave.Folder
949+
if folder == "" {
950+
// Fallback to default location
951+
homeDir, err := os.UserHomeDir()
952+
if err != nil {
953+
return nil, fmt.Errorf("failed to get home directory: %w", err)
954+
}
955+
folder = filepath.Join(homeDir, "Pictures", "WinShot")
956+
}
957+
958+
opts := library.DefaultScanOptions()
959+
return library.ScanFolder(folder, opts)
960+
}
961+
962+
// OpenInEditor loads an image file into the editor
963+
// Security: validates path is within QuickSave folder
964+
func (a *App) OpenInEditor(imagePath string) (*screenshot.CaptureResult, error) {
965+
// Validate path is within QuickSave folder (prevent directory traversal)
966+
folder := a.config.QuickSave.Folder
967+
if folder == "" {
968+
homeDir, _ := os.UserHomeDir()
969+
folder = filepath.Join(homeDir, "Pictures", "WinShot")
970+
}
971+
972+
absPath, err := filepath.Abs(imagePath)
973+
if err != nil {
974+
return nil, fmt.Errorf("invalid path: %w", err)
975+
}
976+
977+
absFolder, err := filepath.Abs(folder)
978+
if err != nil {
979+
return nil, fmt.Errorf("invalid folder path: %w", err)
980+
}
981+
982+
// Security check: ensure file is within QuickSave folder
983+
if !strings.HasPrefix(absPath, absFolder+string(filepath.Separator)) {
984+
return nil, fmt.Errorf("access denied: file outside QuickSave folder")
985+
}
986+
987+
// Read file
988+
data, err := os.ReadFile(absPath)
989+
if err != nil {
990+
return nil, fmt.Errorf("failed to read file: %w", err)
991+
}
992+
993+
// Decode image
994+
var img image.Image
995+
ext := strings.ToLower(filepath.Ext(absPath))
996+
997+
switch ext {
998+
case ".png":
999+
img, err = png.Decode(bytes.NewReader(data))
1000+
case ".jpg", ".jpeg":
1001+
img, err = jpeg.Decode(bytes.NewReader(data))
1002+
default:
1003+
img, _, err = image.Decode(bytes.NewReader(data))
1004+
}
1005+
if err != nil {
1006+
return nil, fmt.Errorf("failed to decode image: %w", err)
1007+
}
1008+
1009+
bounds := img.Bounds()
1010+
1011+
// Re-encode as PNG for consistent handling in frontend
1012+
var buf bytes.Buffer
1013+
if err := png.Encode(&buf, img); err != nil {
1014+
return nil, fmt.Errorf("failed to encode image: %w", err)
1015+
}
1016+
1017+
return &screenshot.CaptureResult{
1018+
Width: bounds.Dx(),
1019+
Height: bounds.Dy(),
1020+
Data: base64.StdEncoding.EncodeToString(buf.Bytes()),
1021+
}, nil
1022+
}
1023+
1024+
// DeleteScreenshot removes a screenshot file from disk
1025+
// Security: validates path is within QuickSave folder
1026+
func (a *App) DeleteScreenshot(imagePath string) error {
1027+
// Validate path is within QuickSave folder (prevent directory traversal)
1028+
folder := a.config.QuickSave.Folder
1029+
if folder == "" {
1030+
homeDir, _ := os.UserHomeDir()
1031+
folder = filepath.Join(homeDir, "Pictures", "WinShot")
1032+
}
1033+
1034+
absPath, err := filepath.Abs(imagePath)
1035+
if err != nil {
1036+
return fmt.Errorf("invalid path: %w", err)
1037+
}
1038+
1039+
absFolder, err := filepath.Abs(folder)
1040+
if err != nil {
1041+
return fmt.Errorf("invalid folder path: %w", err)
1042+
}
1043+
1044+
// Security check: ensure file is within QuickSave folder
1045+
if !strings.HasPrefix(absPath, absFolder+string(filepath.Separator)) {
1046+
return fmt.Errorf("access denied: file outside QuickSave folder")
1047+
}
1048+
1049+
return os.Remove(absPath)
1050+
}

docs/codebase-summary.md

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ D:\www\winshot/
2727
│ │ ├── types/index.ts # TypeScript interfaces
2828
│ │ ├── utils/ # Utility functions (Phase 2: Color extraction)
2929
│ │ │ └── extract-edge-color.ts
30-
│ │ ├── components/ # 14 React components
30+
│ │ ├── components/ # 15 React components
3131
│ │ │ ├── title-bar.tsx
3232
│ │ │ ├── capture-toolbar.tsx
3333
│ │ │ ├── annotation-toolbar.tsx
34-
│ │ │ ├── export-toolbar.tsx
34+
│ │ │ ├── export-toolbar.tsx # Includes Library button
3535
│ │ │ ├── settings-panel.tsx
3636
│ │ │ ├── settings-modal.tsx # App config dialog (16KB)
3737
│ │ │ ├── editor-canvas.tsx # Konva Stage wrapper (15KB)
@@ -40,6 +40,7 @@ D:\www\winshot/
4040
│ │ │ ├── crop-overlay.tsx
4141
│ │ │ ├── region-selector.tsx # Region capture UI
4242
│ │ │ ├── window-picker.tsx # Window enumeration UI
43+
│ │ │ ├── library-window.tsx # Screenshot history library modal
4344
│ │ │ ├── status-bar.tsx
4445
│ │ │ └── hotkey-input.tsx # Custom hotkey binding
4546
│ │ ├── assets/
@@ -60,6 +61,9 @@ D:\www\winshot/
6061
│ │ └── startup.go # Windows startup registry
6162
│ ├── hotkeys/
6263
│ │ └── hotkeys.go # RegisterHotKey() implementation
64+
│ ├── library/
65+
│ │ ├── library.go # Screenshot library scanning + management
66+
│ │ └── thumbnail.go # Thumbnail generation with CatmullRom scaling
6367
│ ├── overlay/
6468
│ │ ├── types.go # Win32 constants + GDI structures
6569
│ │ ├── overlay.go # Native overlay manager + message loop
@@ -69,7 +73,7 @@ D:\www\winshot/
6973
│ │ ├── window.go # Window capture + DPI handling
7074
│ │ └── clipboard.go # Win32 clipboard DIB image reader
7175
│ ├── tray/
72-
│ │ └── tray.go # System tray icon + menu
76+
│ │ └── tray.go # System tray icon + menu (+ left-click library)
7377
│ └── windows/
7478
│ └── enum.go # Window enumeration (EnumWindows)
7579
├── docs/
@@ -282,7 +286,7 @@ Wraps kbinani/screenshot library with DPI-awareness, multi-display support, and
282286
- `GetClipboardImage()` → CaptureResult (new)
283287

284288
### Package: `internal/tray`
285-
**File:** tray.go (180 LOC)
289+
**File:** tray.go (200 LOC)
286290

287291
Implements system tray icon and context menu using Windows APIs.
288292

@@ -291,12 +295,56 @@ Implements system tray icon and context menu using Windows APIs.
291295
- Context menu with 3 capture modes
292296
- Show/minimize window toggle
293297
- Exit action
298+
- **Left-click opens Screenshot Library** (Jan 2026)
299+
300+
**Menu Constants:**
301+
```go
302+
const (
303+
MenuFullscreen = 1001
304+
MenuRegion = 1002
305+
MenuWindow = 1003
306+
MenuShow = 1004
307+
MenuExit = 1005
308+
MenuSettings = 1006
309+
MenuLibrary = 1007 // NEW: Left-click trigger
310+
)
311+
```
294312

295313
**Entry Points:**
296314
- `NewTrayIcon(title)` - Create tray icon
297315
- `Start()` - Show icon
298316
- `Stop()` - Hide and cleanup
299317

318+
### Package: `internal/library`
319+
**Files:** library.go (150 LOC), thumbnail.go (100 LOC)
320+
321+
Screenshot history library - scans QuickSave folder and generates thumbnails.
322+
323+
**Key Structures:**
324+
```go
325+
type LibraryImage struct {
326+
Filepath string // Full path to image file
327+
Filename string // Filename only
328+
ModifiedDate time.Time // Last modified timestamp
329+
Thumbnail string // Base64 PNG thumbnail (150px max dimension)
330+
Width int // Original image width
331+
Height int // Original image height
332+
}
333+
```
334+
335+
**Features:**
336+
- `ScanFolder(folderPath)` - Returns []LibraryImage sorted by date descending
337+
- `DeleteImage(filepath)` - Remove screenshot with path validation
338+
- `GenerateThumbnail(imagePath)` - High-quality CatmullRom scaling to 150px max
339+
- Supports PNG and JPEG formats
340+
- Auto-creates QuickSave folder if missing
341+
- Directory traversal protection (validates paths within QuickSave folder)
342+
343+
**Entry Points:**
344+
- `ScanFolder(path)`[]LibraryImage
345+
- `DeleteImage(path)` → error
346+
- `GenerateThumbnail(path)` → (string, int, int, error)
347+
300348
### Package: `internal/windows`
301349
**File:** enum.go (80 LOC)
302350

@@ -331,7 +379,7 @@ type App struct {
331379
}
332380
```
333381

334-
**Key Methods (~30 total):**
382+
**Key Methods (~35 total):**
335383
```go
336384
// Capture operations
337385
CaptureFullscreen()
@@ -355,6 +403,11 @@ GetConfig()
355403
SaveConfig(config)
356404
SetHotkey(mode, combo)
357405

406+
// Library operations (NEW - Jan 2026)
407+
GetLibraryImages() // Scan QuickSave folder, return thumbnails
408+
OpenInEditor(imagePath) // Load image into editor with path validation
409+
DeleteScreenshot(imagePath) // Remove file with path validation
410+
358411
// Utility
359412
MinimizeToTray() // Hide window to tray
360413
UpdateWindowSize(width, height)
@@ -538,6 +591,15 @@ interface CropArea {
538591
height: number
539592
}
540593

594+
interface LibraryImage {
595+
filepath: string // Full path to image file
596+
filename: string // Filename only
597+
modifiedDate: string // ISO date string
598+
thumbnail: string // Base64 PNG (150px max)
599+
width: number // Original image width
600+
height: number // Original image height
601+
}
602+
541603
type CropAspectRatio = 'free' | '16:9' | '4:3' | '1:1' | '9:16' | '3:4'
542604

543605
type EditorTool = 'select' | 'crop' | AnnotationType
@@ -954,3 +1016,85 @@ App.tsx startup
9541016
- AutoBackground flag enables/disables color extraction pipeline
9551017
- No breaking changes to existing config files (new fields use defaults if missing)
9561018
- Full backward compatibility with older config versions
1019+
1020+
---
1021+
1022+
## Screenshot Library Feature (Jan 12, 2026)
1023+
1024+
**Overview:** Added Screenshot History Library accessible via tray icon left-click or export toolbar button. Displays grid of captured screenshots with thumbnails for quick browsing, editing, and deletion.
1025+
1026+
**Backend Changes:**
1027+
1028+
1. **internal/library/library.go** (New file)
1029+
- `LibraryImage` struct: filepath, filename, modifiedDate, thumbnail (base64), width, height
1030+
- `ScanFolder(path)` - Scans QuickSave folder for PNG/JPEG files, returns sorted by date descending
1031+
- `DeleteImage(filepath)` - Removes file with directory traversal protection
1032+
- Auto-creates QuickSave folder if it doesn't exist
1033+
1034+
2. **internal/library/thumbnail.go** (New file)
1035+
- `GenerateThumbnail(imagePath)` - High-quality thumbnail generation
1036+
- Uses `golang.org/x/image/draw` with CatmullRom scaling
1037+
- Max dimension 150px while preserving aspect ratio
1038+
- Returns base64 PNG + original dimensions
1039+
1040+
3. **internal/tray/tray.go** (Modified)
1041+
- Added `MenuLibrary = 1007` constant
1042+
- Added `WM_LBUTTONUP` handler for left-click detection
1043+
- Left-click triggers `MenuLibrary` callback to open library window
1044+
1045+
4. **app.go** (Modified)
1046+
- Added `GetLibraryImages()` - Returns all screenshots from QuickSave folder
1047+
- Added `OpenInEditor(imagePath)` - Loads image into editor with path validation
1048+
- Added `DeleteScreenshot(imagePath)` - Removes screenshot with path validation
1049+
- Added `MenuLibrary` case to `onTrayMenu` handler
1050+
1051+
**Frontend Changes:**
1052+
1053+
1. **frontend/src/components/library-window.tsx** (New file)
1054+
- Full modal component with glassmorphism design
1055+
- Grid layout with responsive thumbnail cards
1056+
- Selection state with highlight border
1057+
- Action bar: Capture, Edit, Delete, Close buttons
1058+
- Keyboard navigation: Arrow keys, Enter to edit, Delete to remove, Escape to close
1059+
- Uses Wails bindings: `GetLibraryImages()`, `DeleteScreenshot()`
1060+
1061+
2. **frontend/src/components/export-toolbar.tsx** (Modified)
1062+
- Added `onOpenLibrary` prop to interface
1063+
- Added Library button between Quick Save and Cloud
1064+
- Amber/orange gradient styling to match WinShot theme
1065+
1066+
3. **frontend/src/App.tsx** (Modified)
1067+
- Added `showLibrary` state
1068+
- Added handlers: `handleLibraryClose`, `handleLibraryEdit`, `handleLibraryCapture`
1069+
- Added `tray:library` event listener for tray left-click
1070+
- Added `LibraryWindow` component rendering
1071+
- Added `onOpenLibrary` prop to ExportToolbar
1072+
1073+
4. **frontend/src/types/index.ts** (Modified)
1074+
- Added `LibraryImage` interface
1075+
1076+
**User Flow:**
1077+
1078+
```
1079+
Left-click on TrayIcon
1080+
→ Go: WM_LBUTTONUP triggers MenuLibrary callback
1081+
→ App.onTrayMenu(MenuLibrary)
1082+
→ Wails EventsEmit("tray:library")
1083+
→ Frontend: EventsOn handler sets showLibrary=true
1084+
→ LibraryWindow modal renders
1085+
→ GetLibraryImages() fetches thumbnails
1086+
→ Grid displays with selection and keyboard nav
1087+
1088+
Or: Click Library button in export toolbar
1089+
→ onOpenLibrary callback
1090+
→ setShowLibrary(true)
1091+
→ Same modal rendering flow
1092+
```
1093+
1094+
**Design Decisions:**
1095+
- Single-select only for v1 (multi-select deferred)
1096+
- Thumbnails regenerated each open (no disk cache)
1097+
- Default sort: date descending (newest first)
1098+
- Empty state: Show empty list, auto-create folder
1099+
- Path validation: Prevents directory traversal attacks
1100+
- Thumbnail quality: CatmullRom scaling for crisp previews

0 commit comments

Comments
 (0)