thenightfury commited on
Commit
493404f
·
verified ·
1 Parent(s): 835e018

Upload 3 files

Browse files
Files changed (3) hide show
  1. .gitignore +2 -0
  2. FileOrganizer.ps1 +1673 -0
  3. config.json +99 -0
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.log
2
+ lastused.json
FileOrganizer.ps1 ADDED
@@ -0,0 +1,1673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FileOrganizer.ps1 - Modern File Organizer with GUI & CLI
2
+ # A professional-grade Windows utility for organizing files by type
3
+ #
4
+ # Default config.json (auto-created if missing):
5
+ # <#
6
+ # {
7
+ # "categories": {
8
+ # "Word Files": ["doc", "docx", "docm", "dotx"],
9
+ # "PowerPoint Files": ["ppt", "pptx", "pptm"],
10
+ # "Excel Files": ["xls", "xlsx", "xlsm", "csv"],
11
+ # "PDF Files": ["pdf"],
12
+ # "Images": ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "tiff"],
13
+ # "Videos": ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
14
+ # "Executables": ["exe", "msi", "bat", "cmd", "ps1"],
15
+ # "Archives": ["zip", "rar", "7z", "tar", "gz"],
16
+ # "Audio": ["mp3", "wav", "flac", "ogg", "aac", "wma"],
17
+ # "Code": ["ps1", "py", "js", "ts", "cpp", "c", "h", "cs", "java", "rb", "go", "rs"],
18
+ # "Text": ["txt", "md", "log", "rtf", "json", "xml", "yaml", "yml", "ini", "cfg"]
19
+ # },
20
+ # "excludePatterns": ["Thumbs.db", ".DS_Store", "desktop.ini", "*.tmp", "~$*"],
21
+ # "createTypeSubfolder": true,
22
+ # "defaultAction": "Move",
23
+ # "defaultDepth": 100
24
+ # }
25
+ # #>
26
+ #
27
+ # Usage:
28
+ # .\FileOrganizer.ps1 # Interactive GUI mode
29
+ # .\FileOrganizer.ps1 -SourcePath "C:\Downloads" -DestPath "D:\Organized" -DryRun
30
+ # .\FileOrganizer.ps1 -SourcePath "C:\Src" -DestPath "D:\Dest" -Action Copy -Depth 2
31
+ # .\FileOrganizer.ps1 -CategoriesFile "custom.json"
32
+ # Get-Help .\FileOrganizer.ps1 -Detailed
33
+
34
+ #Requires -Version 5.1
35
+
36
+ # ============================================================
37
+ # IMPORTANT: param() MUST come first before any executable code
38
+ # ============================================================
39
+
40
+ [CmdletBinding()]
41
+ param (
42
+ [Parameter(HelpMessage = "Source folder path to scan")]
43
+ [string]$SourcePath,
44
+
45
+ [Parameter(HelpMessage = "Destination folder path")]
46
+ [string]$DestPath,
47
+
48
+ [Parameter(HelpMessage = "Scan depth: 0=Main only, 1=Subfolders, 2=SubSubfolders, 100=All")]
49
+ [ValidateRange(0, 100)]
50
+ [int]$Depth = 100,
51
+
52
+ [Parameter(HelpMessage = "Action to perform: Move or Copy")]
53
+ [ValidateSet("Move", "Copy")]
54
+ [string]$Action = "Move",
55
+
56
+ [Parameter(HelpMessage = "Simulate operation without moving/copying files")]
57
+ [switch]$DryRun,
58
+
59
+ [Parameter(HelpMessage = "Path to custom categories JSON file")]
60
+ [string]$CategoriesFile,
61
+
62
+ [Parameter(HelpMessage = "Run without looping for additional tasks")]
63
+ [switch]$NoLoop,
64
+
65
+ [Parameter(HelpMessage = "Use separate destination folder per file type")]
66
+ [switch]$SeparateDestinations,
67
+
68
+ [Parameter(HelpMessage = "Show this help message")]
69
+ [switch]$Help
70
+ )
71
+
72
+ # ============================================================
73
+ # Now we can have executable code after param()
74
+ # ============================================================
75
+
76
+ # Early error handling - don't let script silently fail
77
+ $ErrorActionPreference = "Continue"
78
+ $Script:StartupError = $null
79
+
80
+ # Check execution policy
81
+ try {
82
+ $execPolicy = Get-ExecutionPolicy -ErrorAction SilentlyContinue
83
+ if ($execPolicy -eq "Restricted") {
84
+ Write-Host "WARNING: Execution Policy is Restricted." -ForegroundColor Yellow
85
+ Write-Host "Run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser" -ForegroundColor Yellow
86
+ }
87
+ }
88
+ catch {
89
+ Write-Verbose "Could not get ExecutionPolicy"
90
+ }
91
+
92
+ # Fix for $PSScriptRoot being empty when run via right-click "Run with PowerShell"
93
+ $Script:ScriptPath = if ($PSScriptRoot) { $PSScriptRoot } elseif ($PSCommandPath) { Split-Path -Parent $PSCommandPath } else { Split-Path -Parent $MyInvocation.MyCommand.Path }
94
+ if (-not $Script:ScriptPath) { $Script:ScriptPath = Get-Location }
95
+
96
+ # Pre-load Windows Forms (needed for GUI dialogs)
97
+ try {
98
+ Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop
99
+ }
100
+ catch {
101
+ $errMsg = $_.Exception.Message
102
+ $Script:StartupError = "Failed to load Windows Forms - $errMsg"
103
+ }
104
+
105
+ #region Global Variables
106
+ $Script:Config = $null
107
+ $Script:LastUsed = $null
108
+ $Script:ScriptName = "FileOrganizer"
109
+ $Script:LogFile = $null
110
+ $Script:IsElevated = $false
111
+ $Script:PSVersion = $PSVersionTable.PSVersion.Major
112
+ #endregion
113
+
114
+ # Check for startup errors
115
+ if ($Script:StartupError) {
116
+ Write-Host "ERROR: $Script:StartupError" -ForegroundColor Red
117
+ Write-Host "Press Enter to exit..." -ForegroundColor Yellow
118
+ Read-Host
119
+ exit 1
120
+ }
121
+
122
+ # Show startup banner
123
+ Write-Host ""
124
+ Write-Host "======================================" -ForegroundColor Cyan
125
+ Write-Host " FileOrganizer v2.0 - Loading..." -ForegroundColor Cyan
126
+ Write-Host "======================================" -ForegroundColor Cyan
127
+ Write-Host "Script Path: $Script:ScriptPath" -ForegroundColor Gray
128
+ Write-Host "PowerShell: $($Script:PSVersion)" -ForegroundColor Gray
129
+ Write-Host ""
130
+
131
+ #region Comment-Based Help
132
+ <#
133
+ .SYNOPSIS
134
+ Organizes files by type into destination folders.
135
+
136
+ .DESCRIPTION
137
+ FileOrganizer is a professional-grade Windows utility that scans a source folder,
138
+ categorizes files by type (Word, PDF, Images, etc.), and moves or copies them to
139
+ destination folders. Supports both CLI and interactive GUI modes.
140
+
141
+ Features:
142
+ - CLI and Interactive GUI hybrid operation
143
+ - Configurable file categories via JSON
144
+ - Move or Copy operations
145
+ - Dry-Run mode for safe testing
146
+ - Modern Out-GridView selection
147
+ - Remembers last used folders
148
+ - Comprehensive logging
149
+
150
+ .PARAMETER SourcePath
151
+ Source folder to scan. If not provided, launches interactive folder browser.
152
+
153
+ .PARAMETER DestPath
154
+ Destination folder for organized files. If not provided, launches interactive browser.
155
+
156
+ .PARAMETER Depth
157
+ Scan depth: 0=Main folder only, 1=One level deep, 2=Two levels, 100=All (default).
158
+
159
+ .PARAMETER Action
160
+ Operation type: "Move" (default) or "Copy". Use Copy for safer testing.
161
+
162
+ .PARAMETER DryRun
163
+ Simulates the operation without actually moving/copying files. Shows what would happen.
164
+
165
+ .PARAMETER CategoriesFile
166
+ Path to custom JSON file with file type categories.
167
+
168
+ .PARAMETER NoLoop
169
+ Runs single operation without prompting for additional tasks.
170
+
171
+ .PARAMETER SeparateDestinations
172
+ Use separate destination folder per file type (prompts for each).
173
+
174
+ .EXAMPLE
175
+ .\FileOrganizer.ps1
176
+
177
+ Launches interactive GUI mode with folder browsers and selections.
178
+
179
+ .EXAMPLE
180
+ .\FileOrganizer.ps1 -SourcePath "C:\Downloads" -DestPath "D:\Organized" -DryRun
181
+
182
+ Scans Downloads, shows what would be organized to D:\Organized without making changes.
183
+
184
+ .EXAMPLE
185
+ .\FileOrganizer.ps1 -SourcePath "C:\Files" -Action Copy -Depth 2
186
+
187
+ Copies files from C:\Files (including subfolders 2 levels deep) with interactive destination.
188
+
189
+ .NOTES
190
+ Author: FileOrganizer Team
191
+ Version: 2.0.0
192
+ Requires: PowerShell 5.1+
193
+ #>
194
+ #endregion
195
+
196
+ #region Initialization Functions
197
+
198
+ function Initialize-Environment {
199
+ <#
200
+ .SYNOPSIS
201
+ Initializes the script environment - logging, config, paths
202
+ #>
203
+ [CmdletBinding()]
204
+ param()
205
+
206
+ # Set up log file path
207
+ $logDate = Get-Date -Format "yyyy-MM-dd"
208
+ $Script:LogFile = Join-Path $Script:ScriptPath "$($Script:ScriptName)_$logDate.log"
209
+
210
+ # Create script path if it doesn't exist (for when running from other locations)
211
+ if (-not $Script:ScriptPath) {
212
+ $Script:ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
213
+ }
214
+
215
+ # Load configuration
216
+ Load-Config -CategoriesFile $CategoriesFile
217
+
218
+ # Load last used paths
219
+ Load-LastUsed
220
+
221
+ # Check PowerShell version for feature compatibility
222
+ if ($Script:PSVersion -lt 5) {
223
+ Write-Warning "PowerShell version 5.1 or higher recommended. Current: $($Script:PSVersion)"
224
+ }
225
+
226
+ Write-Log "INFO" "FileOrganizer started - PS Version: $($Script:PSVersion)"
227
+ }
228
+
229
+ function Load-Config {
230
+ <#
231
+ .SYNOPSIS
232
+ Loads configuration from JSON file or creates default
233
+ #>
234
+ [CmdletBinding()]
235
+ param(
236
+ [string]$CategoriesFile
237
+ )
238
+
239
+ # Determine config file path
240
+ if ($CategoriesFile) {
241
+ $configPath = $CategoriesFile
242
+ }
243
+ else {
244
+ $configPath = Join-Path $Script:ScriptPath "config.json"
245
+ }
246
+
247
+ # Default configuration
248
+ $defaultConfig = @{
249
+ categories = @{
250
+ "Word Files" = @("doc", "docx", "docm", "dotx")
251
+ "PowerPoint Files" = @("ppt", "pptx", "pptm")
252
+ "Excel Files" = @("xls", "xlsx", "xlsm", "csv")
253
+ "PDF Files" = @("pdf")
254
+ "Images" = @("jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "tiff")
255
+ "Videos" = @("mp4", "avi", "mkv", "mov", "wmv", "flv")
256
+ "Executables" = @("exe", "msi", "bat", "cmd")
257
+ "Archives" = @("zip", "rar", "7z", "tar", "gz")
258
+ "Audio" = @("mp3", "wav", "flac", "ogg", "aac", "wma")
259
+ "Code" = @("ps1", "py", "js", "ts", "cpp", "c", "h", "cs", "java", "rb", "go", "rs")
260
+ "Text" = @("txt", "md", "log", "rtf", "json", "xml", "yaml", "yml", "ini", "cfg")
261
+ }
262
+ excludePatterns = @("Thumbs.db", ".DS_Store", "desktop.ini", "*.tmp", "~$*")
263
+ createTypeSubfolder = $true
264
+ defaultAction = "Move"
265
+ defaultDepth = 100
266
+ }
267
+
268
+ # Load existing or create default config
269
+ if (Test-Path $configPath) {
270
+ try {
271
+ $configContent = Get-Content $configPath -Raw -ErrorAction Stop
272
+ # Handle PS 5.1 vs PS 6+ compatibility
273
+ if ($Script:PSVersion -ge 6) {
274
+ $Script:Config = $configContent | ConvertFrom-Json -AsHashtable -ErrorAction Stop
275
+ }
276
+ else {
277
+ $loadedConfig = $configContent | ConvertFrom-Json -ErrorAction Stop
278
+ # Convert PSObject to Hashtable for PS 5.1 compatibility
279
+ $Script:Config = @{}
280
+ $loadedConfig.PSObject.Properties | ForEach-Object {
281
+ $Script:Config[$_.Name] = $_.Value
282
+ }
283
+ }
284
+ Write-Log "INFO" "Configuration loaded from: $configPath"
285
+ }
286
+ catch {
287
+ $errMsg = $_.Exception.Message
288
+ Write-Warning "Failed to load config, using defaults - $errMsg"
289
+ $Script:Config = $defaultConfig
290
+ Save-Config -Config $Script:Config -Path $configPath
291
+ }
292
+ }
293
+ else {
294
+ # Create default config file
295
+ $Script:Config = $defaultConfig
296
+ try {
297
+ Save-Config -Config $Script:Config -Path $configPath
298
+ Write-Host "Created default config: $configPath" -ForegroundColor Cyan
299
+ }
300
+ catch {
301
+ $errMsg = $_.Exception.Message
302
+ Write-Warning "Could not create config file - $errMsg"
303
+ }
304
+ }
305
+
306
+ # Ensure required keys exist
307
+ if (-not $Script:Config.categories) { $Script:Config.categories = $defaultConfig.categories }
308
+ if (-not $Script:Config.excludePatterns) { $Script:Config.excludePatterns = $defaultConfig.excludePatterns }
309
+ if ($null -eq $Script:Config.createTypeSubfolder) { $Script:Config.createTypeSubfolder = $true }
310
+ }
311
+
312
+ function Save-Config {
313
+ <#
314
+ .SYNOPSIS
315
+ Saves configuration to JSON file
316
+ #>
317
+ [CmdletBinding()]
318
+ param(
319
+ [hashtable]$Config,
320
+ [string]$Path
321
+ )
322
+
323
+ try {
324
+ $Config | ConvertTo-Json -Depth 10 | Set-Content $Path -Force -ErrorAction Stop
325
+ }
326
+ catch {
327
+ $errMsg = $_.Exception.Message
328
+ Write-Warning "Failed to save config - $errMsg"
329
+ }
330
+ }
331
+
332
+ function Load-LastUsed {
333
+ <#
334
+ .SYNOPSIS
335
+ Loads last used source/destination paths
336
+ #>
337
+ [CmdletBinding()]
338
+ param()
339
+
340
+ $lastUsedPath = Join-Path $Script:ScriptPath "lastused.json"
341
+
342
+ if (Test-Path $lastUsedPath) {
343
+ try {
344
+ $content = Get-Content $lastUsedPath -Raw
345
+ # Handle PS 5.1 vs PS 6+ compatibility
346
+ if ($Script:PSVersion -ge 6) {
347
+ $Script:LastUsed = $content | ConvertFrom-Json -AsHashtable -ErrorAction Stop
348
+ }
349
+ else {
350
+ $loaded = $content | ConvertFrom-Json -ErrorAction Stop
351
+ # Convert PSObject to Hashtable for PS 5.1 compatibility
352
+ $Script:LastUsed = @{}
353
+ $loaded.PSObject.Properties | ForEach-Object {
354
+ $Script:LastUsed[$_.Name] = $_.Value
355
+ }
356
+ }
357
+ }
358
+ catch {
359
+ $Script:LastUsed = @{ sourcePath = ""; destPath = "" }
360
+ }
361
+ }
362
+ else {
363
+ $Script:LastUsed = @{ sourcePath = ""; destPath = "" }
364
+ }
365
+ }
366
+
367
+ function Save-LastUsed {
368
+ <#
369
+ .SYNOPSIS
370
+ Saves last used paths for future sessions
371
+ #>
372
+ [CmdletBinding()]
373
+ param(
374
+ [string]$SourcePath,
375
+ [string]$DestPath
376
+ )
377
+
378
+ $lastUsedPath = Join-Path $Script:ScriptPath "lastused.json"
379
+
380
+ $Script:LastUsed.sourcePath = $SourcePath
381
+ $Script:LastUsed.destPath = $DestPath
382
+
383
+ try {
384
+ $Script:LastUsed | ConvertTo-Json | Set-Content $lastUsedPath -Force
385
+ }
386
+ catch {
387
+ $errMsg = $_.Exception.Message
388
+ Write-Warning "Could not save last used paths - $errMsg"
389
+ }
390
+ }
391
+
392
+ function Write-Log {
393
+ <#
394
+ .SYNOPSIS
395
+ Writes timestamped log entry to file
396
+ #>
397
+ [CmdletBinding()]
398
+ param(
399
+ [Parameter(Mandatory)]
400
+ [string]$Level,
401
+
402
+ [Parameter(Mandatory)]
403
+ [string]$Message
404
+ )
405
+
406
+ $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
407
+ $logEntry = "[$timestamp] [$Level] $Message"
408
+
409
+ # Write to log file
410
+ try {
411
+ Add-Content -Path $Script:LogFile -Value $logEntry -ErrorAction SilentlyContinue
412
+ }
413
+ catch {
414
+ Write-Verbose "Logging silently failed"
415
+ }
416
+
417
+ # Also output to console based on level
418
+ switch ($Level) {
419
+ "ERROR" { Write-Host $logEntry -ForegroundColor Red }
420
+ "WARN" { Write-Host $logEntry -ForegroundColor Yellow }
421
+ "INFO" { Write-Verbose $logEntry }
422
+ "DRYRUN" { Write-Host $logEntry -ForegroundColor Cyan }
423
+ }
424
+ }
425
+
426
+ #endregion
427
+
428
+ #region GUI Helper Functions
429
+
430
+ function Test-OutGridViewAvailable {
431
+ <#
432
+ .SYNOPSIS
433
+ Checks if Out-GridView is available (Windows only)
434
+ #>
435
+ if ($IsWindows -or (-not $IsLinux -and -not $IsMacOS)) {
436
+ return $true
437
+ }
438
+ return $false
439
+ }
440
+
441
+ function Select-SourceFolder {
442
+ <#
443
+ .SYNOPSIS
444
+ Shows folder browser dialog for source selection
445
+ #>
446
+ [CmdletBinding()]
447
+ param(
448
+ [string]$InitialPath
449
+ )
450
+
451
+ # Use last used path if available and no initial path provided
452
+ if (-not $InitialPath -and $Script:LastUsed.sourcePath) {
453
+ if (Test-Path $Script:LastUsed.sourcePath) {
454
+ $InitialPath = $Script:LastUsed.sourcePath
455
+ }
456
+ }
457
+
458
+ # Use Windows Forms folder browser
459
+ Add-Type -AssemblyName System.Windows.Forms
460
+ $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
461
+ $dialog.Description = "Select Source Folder to Scan"
462
+ $dialog.ShowNewFolderButton = $false
463
+
464
+ if ($InitialPath -and (Test-Path $InitialPath)) {
465
+ $dialog.SelectedPath = $InitialPath
466
+ }
467
+
468
+ $result = $dialog.ShowDialog()
469
+
470
+ if ($result -eq "OK") {
471
+ Write-Log "INFO" "Source folder selected: $($dialog.SelectedPath)"
472
+ return $dialog.SelectedPath
473
+ }
474
+
475
+ return $null
476
+ }
477
+
478
+ function Select-DestinationFolder {
479
+ <#
480
+ .SYNOPSIS
481
+ Shows folder browser dialog for destination selection
482
+ #>
483
+ [CmdletBinding()]
484
+ param(
485
+ [string]$Description = "Select Destination Folder",
486
+ [string]$InitialPath
487
+ )
488
+
489
+ # Use last used path if available
490
+ if (-not $InitialPath -and $Script:LastUsed.destPath) {
491
+ if (Test-Path $Script:LastUsed.destPath) {
492
+ $InitialPath = $Script:LastUsed.destPath
493
+ }
494
+ }
495
+
496
+ Add-Type -AssemblyName System.Windows.Forms
497
+ $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
498
+ $dialog.Description = $Description
499
+ $dialog.ShowNewFolderButton = $true
500
+
501
+ if ($InitialPath -and (Test-Path $InitialPath)) {
502
+ $dialog.SelectedPath = $InitialPath
503
+ }
504
+
505
+ $result = $dialog.ShowDialog()
506
+
507
+ if ($result -eq "OK") {
508
+ Write-Log "INFO" "Destination folder selected: $($dialog.SelectedPath)"
509
+ return $dialog.SelectedPath
510
+ }
511
+
512
+ return $null
513
+ }
514
+
515
+ function Select-DepthGUI {
516
+ <#
517
+ .SYNOPSIS
518
+ Shows depth selection via modern Windows Forms
519
+ #>
520
+ [CmdletBinding()]
521
+ param()
522
+
523
+ Add-Type -AssemblyName System.Windows.Forms
524
+ Add-Type -AssemblyName System.Drawing
525
+
526
+ $form = New-Object System.Windows.Forms.Form
527
+ $form.Text = "FileOrganizer - Scan Depth"
528
+ $form.Size = New-Object System.Drawing.Size(460, 320)
529
+ $form.StartPosition = "CenterScreen"
530
+ $form.FormBorderStyle = "FixedDialog"
531
+ $form.MaximizeBox = $false
532
+ $form.MinimizeBox = $false
533
+ $form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250)
534
+ $form.Font = New-Object System.Drawing.Font("Segoe UI", 10)
535
+
536
+ $headerPanel = New-Object System.Windows.Forms.Panel
537
+ $headerPanel.Dock = "Top"
538
+ $headerPanel.Height = 50
539
+ $headerPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
540
+
541
+ $headerLabel = New-Object System.Windows.Forms.Label
542
+ $headerLabel.Text = " Select Scan Depth"
543
+ $headerLabel.ForeColor = [System.Drawing.Color]::White
544
+ $headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14)
545
+ $headerLabel.Dock = "Fill"
546
+ $headerLabel.TextAlign = "MiddleLeft"
547
+ $headerPanel.Controls.Add($headerLabel)
548
+
549
+ $radio0 = New-Object System.Windows.Forms.RadioButton
550
+ $radio0.Location = New-Object System.Drawing.Point(40, 70)
551
+ $radio0.Size = New-Object System.Drawing.Size(380, 28)
552
+ $radio0.Text = "Main Folder Only (Current folder only)"
553
+ $radio0.FlatStyle = "Flat"
554
+
555
+ $radio1 = New-Object System.Windows.Forms.RadioButton
556
+ $radio1.Location = New-Object System.Drawing.Point(40, 105)
557
+ $radio1.Size = New-Object System.Drawing.Size(380, 28)
558
+ $radio1.Text = "Subfolders (1 level deep)"
559
+ $radio1.FlatStyle = "Flat"
560
+
561
+ $radio2 = New-Object System.Windows.Forms.RadioButton
562
+ $radio2.Location = New-Object System.Drawing.Point(40, 140)
563
+ $radio2.Size = New-Object System.Drawing.Size(380, 28)
564
+ $radio2.Text = "Sub-Subfolders (2 levels deep)"
565
+ $radio2.FlatStyle = "Flat"
566
+
567
+ $radio100 = New-Object System.Windows.Forms.RadioButton
568
+ $radio100.Location = New-Object System.Drawing.Point(40, 175)
569
+ $radio100.Size = New-Object System.Drawing.Size(380, 28)
570
+ $radio100.Text = "All Subfolders (Recursive)"
571
+ $radio100.Checked = $true
572
+ $radio100.FlatStyle = "Flat"
573
+
574
+ $okButton = New-Object System.Windows.Forms.Button
575
+ $okButton.Location = New-Object System.Drawing.Point(170, 225)
576
+ $okButton.Size = New-Object System.Drawing.Size(100, 36)
577
+ $okButton.Text = "OK"
578
+ $okButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
579
+ $okButton.FlatStyle = "Flat"
580
+ $okButton.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
581
+ $okButton.ForeColor = [System.Drawing.Color]::White
582
+ $okButton.Cursor = "Hand"
583
+ $okButton.DialogResult = "OK"
584
+
585
+ $form.Controls.AddRange(@($headerPanel, $radio0, $radio1, $radio2, $radio100, $okButton))
586
+ $form.AcceptButton = $okButton
587
+
588
+ $result = $form.ShowDialog()
589
+ $form.Dispose()
590
+
591
+ if ($result -eq "OK") {
592
+ if ($radio0.Checked) { return 0 }
593
+ if ($radio1.Checked) { return 1 }
594
+ if ($radio2.Checked) { return 2 }
595
+ if ($radio100.Checked) { return 100 }
596
+ }
597
+
598
+ return 100 # default
599
+ }
600
+
601
+ function Select-FileType {
602
+ <#
603
+ .SYNOPSIS
604
+ Shows type selection using custom modern Windows Forms CheckedListBox
605
+ #>
606
+ [CmdletBinding()]
607
+ param(
608
+ [hashtable]$TypeCounts
609
+ )
610
+
611
+ Add-Type -AssemblyName System.Windows.Forms
612
+ Add-Type -AssemblyName System.Drawing
613
+
614
+ $form = New-Object System.Windows.Forms.Form
615
+ $form.Text = "FileOrganizer - File Types"
616
+ $form.Size = New-Object System.Drawing.Size(420, 500)
617
+ $form.StartPosition = "CenterScreen"
618
+ $form.FormBorderStyle = "FixedDialog"
619
+ $form.MaximizeBox = $false
620
+ $form.MinimizeBox = $false
621
+ $form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250)
622
+ $form.Font = New-Object System.Drawing.Font("Segoe UI", 10)
623
+
624
+ $headerPanel = New-Object System.Windows.Forms.Panel
625
+ $headerPanel.Dock = "Top"
626
+ $headerPanel.Height = 50
627
+ $headerPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
628
+
629
+ $headerLabel = New-Object System.Windows.Forms.Label
630
+ $headerLabel.Text = " Select File Types"
631
+ $headerLabel.ForeColor = [System.Drawing.Color]::White
632
+ $headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14)
633
+ $headerLabel.Dock = "Fill"
634
+ $headerLabel.TextAlign = "MiddleLeft"
635
+ $headerPanel.Controls.Add($headerLabel)
636
+
637
+ $listBox = New-Object System.Windows.Forms.CheckedListBox
638
+ $listBox.Location = New-Object System.Drawing.Point(20, 95)
639
+ $listBox.Size = New-Object System.Drawing.Size(360, 280)
640
+ $listBox.Font = New-Object System.Drawing.Font("Segoe UI", 10.5)
641
+ $listBox.CheckOnClick = $true
642
+
643
+ # Store plain string items so we can easily map them back
644
+ $itemList = @()
645
+ foreach ($type in ($TypeCounts.Keys | Sort-Object)) {
646
+ $count = $TypeCounts[$type]
647
+ $display = "$type ($count files)"
648
+ $listBox.Items.Add($display, $true) | Out-Null
649
+ $itemList += [PSCustomObject]@{ Display = $display; Type = $type }
650
+ }
651
+
652
+ $selectAllBtn = New-Object System.Windows.Forms.Button
653
+ $selectAllBtn.Location = New-Object System.Drawing.Point(20, 395)
654
+ $selectAllBtn.Size = New-Object System.Drawing.Size(100, 32)
655
+ $selectAllBtn.Text = "Select All"
656
+ $selectAllBtn.FlatStyle = "Flat"
657
+ $selectAllBtn.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225)
658
+ $selectAllBtn.Cursor = "Hand"
659
+ $selectAllBtn.Add_Click({
660
+ for ($i = 0; $i -lt $listBox.Items.Count; $i++) {
661
+ $listBox.SetItemChecked($i, $true)
662
+ }
663
+ })
664
+
665
+ $clearAllBtn = New-Object System.Windows.Forms.Button
666
+ $clearAllBtn.Location = New-Object System.Drawing.Point(130, 395)
667
+ $clearAllBtn.Size = New-Object System.Drawing.Size(100, 32)
668
+ $clearAllBtn.Text = "Clear All"
669
+ $clearAllBtn.FlatStyle = "Flat"
670
+ $clearAllBtn.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225)
671
+ $clearAllBtn.Cursor = "Hand"
672
+ $clearAllBtn.Add_Click({
673
+ for ($i = 0; $i -lt $listBox.Items.Count; $i++) {
674
+ $listBox.SetItemChecked($i, $false)
675
+ }
676
+ })
677
+
678
+ $okButton = New-Object System.Windows.Forms.Button
679
+ $okButton.Location = New-Object System.Drawing.Point(280, 390)
680
+ $okButton.Size = New-Object System.Drawing.Size(100, 40)
681
+ $okButton.Text = "OK"
682
+ $okButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10.5)
683
+ $okButton.FlatStyle = "Flat"
684
+ $okButton.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
685
+ $okButton.ForeColor = [System.Drawing.Color]::White
686
+ $okButton.Cursor = "Hand"
687
+ $okButton.DialogResult = "OK"
688
+
689
+ $form.Controls.AddRange(@($headerPanel, $listBox, $selectAllBtn, $clearAllBtn, $okButton))
690
+ $form.AcceptButton = $okButton
691
+
692
+ # Loop until valid selection or cancelled
693
+ $selectedTypes = @()
694
+ while ($true) {
695
+ $result = $form.ShowDialog()
696
+
697
+ if ($result -eq "OK") {
698
+ foreach ($item in $listBox.CheckedItems) {
699
+ # Map display back to type name
700
+ $mappedType = ($itemList | Where-Object { $_.Display -eq $item }).Type
701
+ $selectedTypes += $mappedType
702
+ }
703
+
704
+ if ($selectedTypes.Count -gt 0) {
705
+ break
706
+ }
707
+ else {
708
+ [System.Windows.Forms.MessageBox]::Show("Please select at least one file type.", "No Selection", "OK", "Warning")
709
+ }
710
+ }
711
+ else {
712
+ break # Cancelled/closed
713
+ }
714
+ }
715
+
716
+ $form.Dispose()
717
+
718
+ if ($selectedTypes.Count -eq 0) { return @() }
719
+ return $selectedTypes
720
+ }
721
+
722
+ function Select-ActionMode {
723
+ <#
724
+ .SYNOPSIS
725
+ Shows Move/Copy selection dialog (modern styled)
726
+ #>
727
+ [CmdletBinding()]
728
+ param()
729
+
730
+ Add-Type -AssemblyName System.Windows.Forms
731
+ Add-Type -AssemblyName System.Drawing
732
+
733
+ $form = New-Object System.Windows.Forms.Form
734
+ $form.Text = "FileOrganizer - Select Operation"
735
+ $form.Size = New-Object System.Drawing.Size(440, 220)
736
+ $form.StartPosition = "CenterScreen"
737
+ $form.FormBorderStyle = "FixedDialog"
738
+ $form.MaximizeBox = $false
739
+ $form.MinimizeBox = $false
740
+ $form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250)
741
+ $form.Font = New-Object System.Drawing.Font("Segoe UI", 10)
742
+
743
+ $headerPanel = New-Object System.Windows.Forms.Panel
744
+ $headerPanel.Dock = "Top"
745
+ $headerPanel.Height = 50
746
+ $headerPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
747
+
748
+ $headerLabel = New-Object System.Windows.Forms.Label
749
+ $headerLabel.Text = " Select Operation Mode"
750
+ $headerLabel.ForeColor = [System.Drawing.Color]::White
751
+ $headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14)
752
+ $headerLabel.Dock = "Fill"
753
+ $headerLabel.TextAlign = "MiddleLeft"
754
+ $headerPanel.Controls.Add($headerLabel)
755
+
756
+ $moveRadio = New-Object System.Windows.Forms.RadioButton
757
+ $moveRadio.Location = New-Object System.Drawing.Point(40, 70)
758
+ $moveRadio.Size = New-Object System.Drawing.Size(160, 30)
759
+ $moveRadio.Text = "Move (default)"
760
+ $moveRadio.Font = New-Object System.Drawing.Font("Segoe UI", 10)
761
+ $moveRadio.Checked = $true
762
+ $moveRadio.FlatStyle = "Flat"
763
+
764
+ $copyRadio = New-Object System.Windows.Forms.RadioButton
765
+ $copyRadio.Location = New-Object System.Drawing.Point(230, 70)
766
+ $copyRadio.Size = New-Object System.Drawing.Size(160, 30)
767
+ $copyRadio.Text = "Copy (safer)"
768
+ $copyRadio.Font = New-Object System.Drawing.Font("Segoe UI", 10)
769
+ $copyRadio.FlatStyle = "Flat"
770
+
771
+ $okButton = New-Object System.Windows.Forms.Button
772
+ $okButton.Location = New-Object System.Drawing.Point(110, 125)
773
+ $okButton.Size = New-Object System.Drawing.Size(100, 36)
774
+ $okButton.Text = "OK"
775
+ $okButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
776
+ $okButton.FlatStyle = "Flat"
777
+ $okButton.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
778
+ $okButton.ForeColor = [System.Drawing.Color]::White
779
+ $okButton.Cursor = "Hand"
780
+ $okButton.DialogResult = "OK"
781
+
782
+ $cancelButton = New-Object System.Windows.Forms.Button
783
+ $cancelButton.Location = New-Object System.Drawing.Point(225, 125)
784
+ $cancelButton.Size = New-Object System.Drawing.Size(100, 36)
785
+ $cancelButton.Text = "Cancel"
786
+ $cancelButton.Font = New-Object System.Drawing.Font("Segoe UI", 10)
787
+ $cancelButton.FlatStyle = "Flat"
788
+ $cancelButton.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225)
789
+ $cancelButton.Cursor = "Hand"
790
+ $cancelButton.DialogResult = "Cancel"
791
+
792
+ $form.Controls.AddRange(@($headerPanel, $moveRadio, $copyRadio, $okButton, $cancelButton))
793
+ $form.AcceptButton = $okButton
794
+ $form.CancelButton = $cancelButton
795
+
796
+ $result = $form.ShowDialog()
797
+
798
+ if ($result -eq "OK") {
799
+ if ($copyRadio.Checked) {
800
+ return "Copy"
801
+ }
802
+ }
803
+
804
+ return "Move"
805
+ }
806
+
807
+ function Confirm-Summary {
808
+ <#
809
+ .SYNOPSIS
810
+ Shows modern confirmation dialog with operation summary
811
+ #>
812
+ [CmdletBinding()]
813
+ param(
814
+ [string]$SourcePath,
815
+ [string[]]$SelectedTypes,
816
+ [hashtable]$TypeCounts,
817
+ [string]$Action,
818
+ [hashtable]$Destinations,
819
+ [bool]$DryRun
820
+ )
821
+
822
+ Add-Type -AssemblyName System.Windows.Forms
823
+ Add-Type -AssemblyName System.Drawing
824
+
825
+ # Calculate total files
826
+ $totalFiles = 0
827
+ foreach ($type in $SelectedTypes) {
828
+ $totalFiles += $TypeCounts[$type]
829
+ }
830
+
831
+ # Build summary lines using ASCII-safe characters
832
+ $summaryLines = @()
833
+ $summaryLines += "Source Folder: $SourcePath"
834
+ $summaryLines += ""
835
+ $summaryLines += "Selected Types ($($SelectedTypes.Count) categories):"
836
+
837
+ foreach ($type in $SelectedTypes | Sort-Object) {
838
+ $count = $TypeCounts[$type]
839
+ $dest = $Destinations[$type]
840
+ $summaryLines += " - $type : $count files"
841
+ $summaryLines += " -> $dest"
842
+ }
843
+
844
+ $dryRunText = if ($DryRun) { " (DRY RUN - No changes)" } else { "" }
845
+ $summaryLines += ""
846
+ $summaryLines += "Operation: $Action$dryRunText"
847
+ $summaryLines += "Total Files: $totalFiles"
848
+
849
+ $summaryText = $summaryLines -join "`r`n"
850
+
851
+ # Create modern styled confirmation form
852
+ $form = New-Object System.Windows.Forms.Form
853
+ $form.Text = "FileOrganizer - Confirm Operation"
854
+ $form.Size = New-Object System.Drawing.Size(520, 420)
855
+ $form.StartPosition = "CenterScreen"
856
+ $form.FormBorderStyle = "FixedDialog"
857
+ $form.MaximizeBox = $false
858
+ $form.MinimizeBox = $false
859
+ $form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250)
860
+ $form.Font = New-Object System.Drawing.Font("Segoe UI", 10)
861
+
862
+ # Header panel
863
+ $headerPanel = New-Object System.Windows.Forms.Panel
864
+ $headerPanel.Dock = "Top"
865
+ $headerPanel.Height = 50
866
+ $accentColor = if ($DryRun) { [System.Drawing.Color]::FromArgb(0, 120, 180) } else { [System.Drawing.Color]::FromArgb(55, 71, 133) }
867
+ $headerPanel.BackColor = $accentColor
868
+
869
+ $headerLabel = New-Object System.Windows.Forms.Label
870
+ $headerText = if ($DryRun) { " Confirm Operation (Dry Run)" } else { " Confirm Operation" }
871
+ $headerLabel.Text = $headerText
872
+ $headerLabel.ForeColor = [System.Drawing.Color]::White
873
+ $headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14)
874
+ $headerLabel.Dock = "Fill"
875
+ $headerLabel.TextAlign = "MiddleLeft"
876
+ $headerPanel.Controls.Add($headerLabel)
877
+
878
+ # Summary text box (read-only)
879
+ $textBox = New-Object System.Windows.Forms.TextBox
880
+ $textBox.Location = New-Object System.Drawing.Point(20, 65)
881
+ $textBox.Size = New-Object System.Drawing.Size(462, 240)
882
+ $textBox.Multiline = $true
883
+ $textBox.ReadOnly = $true
884
+ $textBox.ScrollBars = "Vertical"
885
+ $textBox.Font = New-Object System.Drawing.Font("Segoe UI", 9.5)
886
+ $textBox.BackColor = [System.Drawing.Color]::White
887
+ $textBox.BorderStyle = "FixedSingle"
888
+ $textBox.Text = $summaryText
889
+
890
+ # Question label
891
+ $questionLabel = New-Object System.Windows.Forms.Label
892
+ $questionLabel.Location = New-Object System.Drawing.Point(20, 315)
893
+ $questionLabel.Size = New-Object System.Drawing.Size(460, 25)
894
+ $questionLabel.Text = "Proceed with this operation?"
895
+ $questionLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
896
+
897
+ # Yes button
898
+ $yesButton = New-Object System.Windows.Forms.Button
899
+ $yesButton.Location = New-Object System.Drawing.Point(140, 345)
900
+ $yesButton.Size = New-Object System.Drawing.Size(110, 36)
901
+ $yesButton.Text = "Yes, Proceed"
902
+ $yesButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
903
+ $yesButton.FlatStyle = "Flat"
904
+ $yesButton.BackColor = $accentColor
905
+ $yesButton.ForeColor = [System.Drawing.Color]::White
906
+ $yesButton.Cursor = "Hand"
907
+ $yesButton.DialogResult = "Yes"
908
+
909
+ # No button
910
+ $noButton = New-Object System.Windows.Forms.Button
911
+ $noButton.Location = New-Object System.Drawing.Point(265, 345)
912
+ $noButton.Size = New-Object System.Drawing.Size(110, 36)
913
+ $noButton.Text = "Cancel"
914
+ $noButton.Font = New-Object System.Drawing.Font("Segoe UI", 10)
915
+ $noButton.FlatStyle = "Flat"
916
+ $noButton.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225)
917
+ $noButton.Cursor = "Hand"
918
+ $noButton.DialogResult = "No"
919
+
920
+ $form.Controls.AddRange(@($headerPanel, $textBox, $questionLabel, $yesButton, $noButton))
921
+ $form.AcceptButton = $yesButton
922
+ $form.CancelButton = $noButton
923
+
924
+ $result = $form.ShowDialog()
925
+ $form.Dispose()
926
+
927
+ return ($result -eq "Yes")
928
+ }
929
+
930
+ function Select-DestinationMode {
931
+ <#
932
+ .SYNOPSIS
933
+ Shows destination mode selection (modern styled)
934
+ #>
935
+ [CmdletBinding()]
936
+ param()
937
+
938
+ Add-Type -AssemblyName System.Windows.Forms
939
+ Add-Type -AssemblyName System.Drawing
940
+
941
+ $form = New-Object System.Windows.Forms.Form
942
+ $form.Text = "FileOrganizer - Destination Mode"
943
+ $form.Size = New-Object System.Drawing.Size(480, 240)
944
+ $form.StartPosition = "CenterScreen"
945
+ $form.FormBorderStyle = "FixedDialog"
946
+ $form.MaximizeBox = $false
947
+ $form.MinimizeBox = $false
948
+ $form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250)
949
+ $form.Font = New-Object System.Drawing.Font("Segoe UI", 10)
950
+
951
+ $headerPanel = New-Object System.Windows.Forms.Panel
952
+ $headerPanel.Dock = "Top"
953
+ $headerPanel.Height = 50
954
+ $headerPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
955
+
956
+ $headerLabel = New-Object System.Windows.Forms.Label
957
+ $headerLabel.Text = " Select Destination Mode"
958
+ $headerLabel.ForeColor = [System.Drawing.Color]::White
959
+ $headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14)
960
+ $headerLabel.Dock = "Fill"
961
+ $headerLabel.TextAlign = "MiddleLeft"
962
+ $headerPanel.Controls.Add($headerLabel)
963
+
964
+ $singleRadio = New-Object System.Windows.Forms.RadioButton
965
+ $singleRadio.Location = New-Object System.Drawing.Point(40, 70)
966
+ $singleRadio.Size = New-Object System.Drawing.Size(400, 28)
967
+ $singleRadio.Text = "Single Destination (all types to one folder)"
968
+ $singleRadio.Font = New-Object System.Drawing.Font("Segoe UI", 10)
969
+ $singleRadio.Checked = $true
970
+ $singleRadio.FlatStyle = "Flat"
971
+
972
+ $separateRadio = New-Object System.Windows.Forms.RadioButton
973
+ $separateRadio.Location = New-Object System.Drawing.Point(40, 105)
974
+ $separateRadio.Size = New-Object System.Drawing.Size(400, 28)
975
+ $separateRadio.Text = "Separate Destination (pick folder per file type)"
976
+ $separateRadio.Font = New-Object System.Drawing.Font("Segoe UI", 10)
977
+ $separateRadio.FlatStyle = "Flat"
978
+
979
+ $okButton = New-Object System.Windows.Forms.Button
980
+ $okButton.Location = New-Object System.Drawing.Point(130, 150)
981
+ $okButton.Size = New-Object System.Drawing.Size(100, 36)
982
+ $okButton.Text = "OK"
983
+ $okButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
984
+ $okButton.FlatStyle = "Flat"
985
+ $okButton.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
986
+ $okButton.ForeColor = [System.Drawing.Color]::White
987
+ $okButton.Cursor = "Hand"
988
+ $okButton.DialogResult = "OK"
989
+
990
+ $cancelButton = New-Object System.Windows.Forms.Button
991
+ $cancelButton.Location = New-Object System.Drawing.Point(245, 150)
992
+ $cancelButton.Size = New-Object System.Drawing.Size(100, 36)
993
+ $cancelButton.Text = "Cancel"
994
+ $cancelButton.Font = New-Object System.Drawing.Font("Segoe UI", 10)
995
+ $cancelButton.FlatStyle = "Flat"
996
+ $cancelButton.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225)
997
+ $cancelButton.Cursor = "Hand"
998
+ $cancelButton.DialogResult = "Cancel"
999
+
1000
+ $form.Controls.AddRange(@($headerPanel, $singleRadio, $separateRadio, $okButton, $cancelButton))
1001
+ $form.AcceptButton = $okButton
1002
+ $form.CancelButton = $cancelButton
1003
+
1004
+ $result = $form.ShowDialog()
1005
+ $form.Dispose()
1006
+
1007
+ if ($result -eq "OK") {
1008
+ if ($separateRadio.Checked) {
1009
+ return "Separate"
1010
+ }
1011
+ }
1012
+
1013
+ return "Single"
1014
+ }
1015
+
1016
+ #endregion
1017
+
1018
+ #region Core Processing Functions
1019
+
1020
+ function Get-FileType {
1021
+ <#
1022
+ .SYNOPSIS
1023
+ Determines file category based on extension
1024
+ #>
1025
+ [CmdletBinding()]
1026
+ param(
1027
+ [Parameter(Mandatory)]
1028
+ [string]$Extension
1029
+ )
1030
+
1031
+ $ext = $Extension.TrimStart('.').ToLower()
1032
+
1033
+ # Handle PS 5.1 (PSCustomObject) vs PS 6+ (Hashtable)
1034
+ $categories = $Script:Config.categories
1035
+ if ($categories -is [System.Collections.Hashtable]) {
1036
+ $categoryList = $categories.Keys
1037
+ }
1038
+ else {
1039
+ # PS 5.1 - convert PSObject to collection
1040
+ $categoryList = $categories.PSObject.Properties.Name
1041
+ }
1042
+
1043
+ foreach ($category in $categoryList) {
1044
+ if ($categories -is [System.Collections.Hashtable]) {
1045
+ $extensions = $categories[$category]
1046
+ }
1047
+ else {
1048
+ $extensions = $categories.$category
1049
+ }
1050
+ if ($extensions -contains $ext) {
1051
+ return $category
1052
+ }
1053
+ }
1054
+
1055
+ return "Others"
1056
+ }
1057
+
1058
+ function Test-ShouldExclude {
1059
+ <#
1060
+ .SYNOPSIS
1061
+ Checks if file matches any exclude patterns
1062
+ #>
1063
+ [CmdletBinding()]
1064
+ param(
1065
+ [Parameter(Mandatory)]
1066
+ [string]$FileName
1067
+ )
1068
+
1069
+ # Get exclude patterns - handle PS 5.1 PSObject vs array
1070
+ $patterns = $Script:Config.excludePatterns
1071
+ if ($patterns -is [PSObject] -and $patterns -isnot [Array]) {
1072
+ $patterns = @($patterns)
1073
+ }
1074
+ if (-not $patterns) { return $false }
1075
+
1076
+ foreach ($pattern in $patterns) {
1077
+ # Handle wildcard patterns
1078
+ if ($pattern.Contains("*")) {
1079
+ if ($FileName -like $pattern) {
1080
+ return $true
1081
+ }
1082
+ }
1083
+ # Handle prefix patterns like ~$ (temporary Office files)
1084
+ elseif ($pattern.StartsWith("~")) {
1085
+ if ($FileName.StartsWith($pattern)) {
1086
+ return $true
1087
+ }
1088
+ }
1089
+ # Exact match
1090
+ else {
1091
+ if ($FileName -eq $pattern) {
1092
+ return $true
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ return $false
1098
+ }
1099
+
1100
+ function Scan-SourceFiles {
1101
+ <#
1102
+ .SYNOPSIS
1103
+ Scans source folder and returns file list with metadata
1104
+ #>
1105
+ [CmdletBinding()]
1106
+ param(
1107
+ [Parameter(Mandatory)]
1108
+ [string]$SourcePath,
1109
+
1110
+ [Parameter(Mandatory)]
1111
+ [int]$Depth
1112
+ )
1113
+
1114
+ Write-Host "`nScanning files..." -ForegroundColor Cyan
1115
+ Write-Log "INFO" "Scanning source: $SourcePath with depth: $Depth"
1116
+
1117
+ $files = @()
1118
+
1119
+ try {
1120
+ # PowerShell 7+ has -Depth parameter
1121
+ if ($Script:PSVersion -ge 7) {
1122
+ $files = @(Get-ChildItem -Path $SourcePath -File -Recurse -Depth $Depth -ErrorAction SilentlyContinue)
1123
+ }
1124
+ else {
1125
+ # PowerShell 5.1 - simulate depth limit
1126
+ if ($Depth -ge 100) {
1127
+ $files = @(Get-ChildItem -Path $SourcePath -File -Recurse -ErrorAction SilentlyContinue)
1128
+ }
1129
+ else {
1130
+ # Manual depth filtering for PS 5.1
1131
+ # Normalize source path to remove trailing backslash for consistent counting
1132
+ $normalizedSource = $SourcePath.TrimEnd([IO.Path]::DirectorySeparatorChar)
1133
+ $sourceDepth = $normalizedSource.Split([IO.Path]::DirectorySeparatorChar).Count
1134
+ $files = @(Get-ChildItem -Path $SourcePath -File -Recurse -ErrorAction SilentlyContinue |
1135
+ Where-Object {
1136
+ $fileDepth = $_.DirectoryName.TrimEnd([IO.Path]::DirectorySeparatorChar).Split([IO.Path]::DirectorySeparatorChar).Count
1137
+ ($fileDepth - $sourceDepth) -le $Depth
1138
+ })
1139
+ }
1140
+ }
1141
+ }
1142
+ catch {
1143
+ $errMsg = $_.Exception.Message
1144
+ Write-Log "ERROR" "Error scanning source - $errMsg"
1145
+ throw "Failed to scan source folder - $errMsg"
1146
+ }
1147
+
1148
+ # Filter out excluded files
1149
+ $filteredFiles = @()
1150
+ foreach ($file in $files) {
1151
+ if (-not (Test-ShouldExclude -FileName $file.Name)) {
1152
+ $filteredFiles += $file
1153
+ }
1154
+ }
1155
+
1156
+ Write-Host "Found $($filteredFiles.Count) files (excluding $($files.Count - $filteredFiles.Count) matches)" -ForegroundColor Green
1157
+
1158
+ return $filteredFiles
1159
+ }
1160
+
1161
+ function Get-TypeSummary {
1162
+ <#
1163
+ .SYNOPSIS
1164
+ Counts files by category
1165
+ #>
1166
+ [CmdletBinding()]
1167
+ param(
1168
+ [Parameter(Mandatory)]
1169
+ [array]$Files
1170
+ )
1171
+
1172
+ $typeCount = @{}
1173
+
1174
+ foreach ($file in $Files) {
1175
+ $typeName = Get-FileType -Extension $file.Extension
1176
+
1177
+ if (-not $typeCount.ContainsKey($typeName)) {
1178
+ $typeCount[$typeName] = 0
1179
+ }
1180
+ $typeCount[$typeName]++
1181
+ }
1182
+
1183
+ return $typeCount
1184
+ }
1185
+
1186
+ function Get-UniqueFileName {
1187
+ <#
1188
+ .SYNOPSIS
1189
+ Generates unique filename to avoid conflicts
1190
+ #>
1191
+ [CmdletBinding()]
1192
+ param(
1193
+ [Parameter(Mandatory)]
1194
+ [string]$DestinationPath,
1195
+
1196
+ [Parameter(Mandatory)]
1197
+ [string]$FileName
1198
+ )
1199
+
1200
+ $fullPath = Join-Path $DestinationPath $FileName
1201
+
1202
+ if (-not (Test-Path $fullPath)) {
1203
+ return $FileName
1204
+ }
1205
+
1206
+ # File exists, generate unique name
1207
+ $baseName = [IO.Path]::GetFileNameWithoutExtension($FileName)
1208
+ $extension = [IO.Path]::GetExtension($FileName)
1209
+
1210
+ $counter = 1
1211
+ do {
1212
+ $newName = "${baseName}_${counter}${extension}"
1213
+ $fullPath = Join-Path $DestinationPath $newName
1214
+ $counter++
1215
+ } while (Test-Path $fullPath)
1216
+
1217
+ return $newName
1218
+ }
1219
+
1220
+ function Test-Cancel {
1221
+ <#
1222
+ .SYNOPSIS
1223
+ Checks if user pressed Escape to cancel operation
1224
+ #>
1225
+ [CmdletBinding()]
1226
+ param()
1227
+
1228
+ if ($Script:PSVersion -ge 7 -and [Console]::IsInputRedirected) {
1229
+ return $false
1230
+ }
1231
+
1232
+ if ([Console]::KeyAvailable) {
1233
+ $key = [Console]::ReadKey($true)
1234
+ if ($key.Key -eq "Escape") {
1235
+ Write-Host "`nOperation Cancelled by user." -ForegroundColor Yellow
1236
+ Write-Log "WARN" "Operation cancelled by user"
1237
+ return $true
1238
+ }
1239
+ }
1240
+ return $false
1241
+ }
1242
+
1243
+ #endregion
1244
+
1245
+ #region File Operation Functions
1246
+
1247
+ function Process-Files {
1248
+ <#
1249
+ .SYNOPSIS
1250
+ Main file processing loop with progress and cancellation
1251
+ #>
1252
+ [CmdletBinding()]
1253
+ param(
1254
+ [Parameter(Mandatory)]
1255
+ [array]$Files,
1256
+
1257
+ [Parameter(Mandatory)]
1258
+ [string[]]$SelectedTypes,
1259
+
1260
+ [Parameter(Mandatory)]
1261
+ [hashtable]$TypeDestinationMap,
1262
+
1263
+ [Parameter(Mandatory)]
1264
+ [string]$Action,
1265
+
1266
+ [bool]$DryRun,
1267
+
1268
+ [bool]$CreateTypeSubfolder
1269
+ )
1270
+
1271
+ $processedCount = 0
1272
+ $errorCount = 0
1273
+ $total = $Files.Count
1274
+ $index = 0
1275
+
1276
+ $operationVerb = if ($Action -eq "Move") { "Moving" } else { "Copying" }
1277
+ $logAction = if ($DryRun) { "DRYRUN" } else { "INFO" }
1278
+
1279
+ Write-Host "`n$operationVerb files..." -ForegroundColor $(if ($DryRun) { "Cyan" }else { "Yellow" })
1280
+
1281
+ foreach ($file in $Files) {
1282
+ # Check for cancellation
1283
+ if (Test-Cancel) {
1284
+ Write-Host "`nOperation cancelled. Stopping..." -ForegroundColor Yellow
1285
+ break
1286
+ }
1287
+
1288
+ $index++
1289
+ $percent = [math]::Round(($index / $total) * 100)
1290
+ Write-Progress -Activity "$operationVerb Files ($Action)" -Status "$index of $total files" -PercentComplete $percent
1291
+
1292
+ # Determine file type
1293
+ $typeName = Get-FileType -Extension $file.Extension
1294
+
1295
+ # Get destination path
1296
+ $baseDest = $TypeDestinationMap[$typeName]
1297
+
1298
+ # Create type subfolder if enabled
1299
+ if ($CreateTypeSubfolder) {
1300
+ $destPath = Join-Path $baseDest $typeName
1301
+ }
1302
+ else {
1303
+ $destPath = $baseDest
1304
+ }
1305
+
1306
+ # Create destination if it doesn't exist
1307
+ if (-not (Test-Path $destPath)) {
1308
+ try {
1309
+ New-Item -ItemType Directory -Path $destPath -Force | Out-Null
1310
+ Write-Log "INFO" "Created directory: $destPath"
1311
+ }
1312
+ catch {
1313
+ $errMsg = $_.Exception.Message
1314
+ Write-Log "ERROR" "Failed to create directory $destPath - $errMsg"
1315
+ $errorCount++
1316
+ continue
1317
+ }
1318
+ }
1319
+
1320
+ # Get unique filename
1321
+ $newFileName = Get-UniqueFileName -DestinationPath $destPath -FileName $file.Name
1322
+ $newFullPath = Join-Path $destPath $newFileName
1323
+
1324
+ # Perform the operation
1325
+ if ($DryRun) {
1326
+ # Dry run - just log what would happen
1327
+ Write-Log "DRYRUN" "Would $Action '$($file.FullName)' -> '$newFullPath'"
1328
+ $processedCount++
1329
+ }
1330
+ else {
1331
+ # Actual move or copy
1332
+ try {
1333
+ if ($Action -eq "Move") {
1334
+ Move-Item -Path $file.FullName -Destination $newFullPath -Force -ErrorAction Stop
1335
+ }
1336
+ else {
1337
+ Copy-Item -Path $file.FullName -Destination $newFullPath -Force -ErrorAction Stop
1338
+ }
1339
+
1340
+ Write-Log "INFO" "$Action completed: $($file.Name) -> $newFullPath"
1341
+ $processedCount++
1342
+ }
1343
+ catch {
1344
+ $errMsg = $_.Exception.Message
1345
+ $fileName = $file.Name
1346
+ Write-Log "ERROR" "Failed to $Action '$fileName' - $errMsg"
1347
+ $errorCount++
1348
+ }
1349
+ }
1350
+ }
1351
+
1352
+ Write-Progress -Activity "$operationVerb Files" -Completed
1353
+
1354
+ # Return summary
1355
+ return @{
1356
+ Processed = $processedCount
1357
+ Errors = $errorCount
1358
+ }
1359
+ }
1360
+
1361
+ #endregion
1362
+
1363
+ #region Interactive Mode
1364
+
1365
+ function Start-InteractiveMode {
1366
+ <#
1367
+ .SYNOPSIS
1368
+ Runs the interactive GUI-based workflow
1369
+ #>
1370
+ [CmdletBinding()]
1371
+ param()
1372
+
1373
+ Write-Host "`n=== FileOrganizer - Interactive Mode ===" -ForegroundColor Cyan
1374
+ Write-Host "Press ESC anytime to cancel operation`n" -ForegroundColor Gray
1375
+
1376
+ # Give user a moment to see the startup
1377
+ Start-Sleep -Milliseconds 500
1378
+
1379
+ # Step 1: Select source
1380
+ Write-Host "Step 1: Select Source Folder (a dialog will open)" -ForegroundColor Yellow
1381
+ Write-Host ">>> Click Browse and select a folder <<<" -ForegroundColor Magenta
1382
+ $sourceFolder = Select-SourceFolder
1383
+
1384
+ if (-not $sourceFolder) {
1385
+ Write-Host "No source folder selected. Exiting." -ForegroundColor Yellow
1386
+ return
1387
+ }
1388
+
1389
+ # Step 2: Select depth
1390
+ Write-Host "`nStep 2: Select Scan Depth" -ForegroundColor Yellow
1391
+ $depth = Select-DepthGUI
1392
+ Write-Host "Using depth: $depth" -ForegroundColor Gray
1393
+
1394
+ # Step 3: Scan files
1395
+ $files = Scan-SourcePath -SourcePath $sourceFolder -Depth $depth
1396
+
1397
+ if ($files.Count -eq 0) {
1398
+ Write-Host "No files found in source folder." -ForegroundColor Yellow
1399
+ return
1400
+ }
1401
+
1402
+ # Step 4: Get type summary
1403
+ $typeCounts = Get-TypeSummary -Files $files
1404
+
1405
+ # Step 5: Select types
1406
+ Write-Host "`nStep 3: Select File Types" -ForegroundColor Yellow
1407
+ $selectedTypes = Select-FileType -TypeCounts $typeCounts
1408
+
1409
+ if ($selectedTypes.Count -eq 0) {
1410
+ Write-Host "No file types selected. Exiting." -ForegroundColor Yellow
1411
+ return
1412
+ }
1413
+
1414
+ # Step 6: Select action (Move/Copy)
1415
+ Write-Host "`nStep 4: Select Operation" -ForegroundColor Yellow
1416
+ $action = Select-ActionMode
1417
+ Write-Host "Operation: $action" -ForegroundColor Gray
1418
+
1419
+ # Step 7: Destination mode
1420
+ Write-Host "`nStep 5: Select Destination Mode" -ForegroundColor Yellow
1421
+ $destMode = Select-DestinationMode
1422
+
1423
+ $typeDestinationMap = @{}
1424
+
1425
+ if ($destMode -eq "Separate") {
1426
+ # Separate destination per type
1427
+ foreach ($type in $selectedTypes) {
1428
+ Write-Host "`nSelect destination for: $type" -ForegroundColor Cyan
1429
+ $dest = Select-DestinationFolder -Description "Select destination for $type"
1430
+
1431
+ if (-not $dest) {
1432
+ Write-Host "Destination not selected. Using source folder." -ForegroundColor Yellow
1433
+ $dest = $sourceFolder
1434
+ }
1435
+
1436
+ $typeDestinationMap[$type] = $dest
1437
+ }
1438
+ }
1439
+ else {
1440
+ # Single destination
1441
+ Write-Host "`nSelect single destination folder" -ForegroundColor Cyan
1442
+ $singleDest = Select-DestinationFolder -Description "Select destination for all files"
1443
+
1444
+ if (-not $singleDest) {
1445
+ Write-Host "No destination selected. Using source folder." -ForegroundColor Yellow
1446
+ $singleDest = $sourceFolder
1447
+ }
1448
+
1449
+ foreach ($type in $selectedTypes) {
1450
+ $typeDestinationMap[$type] = $singleDest
1451
+ }
1452
+ }
1453
+
1454
+ # Step 8: DryRun option
1455
+ Write-Host "`nStep 6: Dry Run Mode?" -ForegroundColor Yellow
1456
+ $dryRunChoice = Read-Host "Run as Dry Run (simulate only)? y/n (default: n)"
1457
+ $dryRun = ($dryRunChoice -eq "y")
1458
+
1459
+ if ($dryRun) {
1460
+ Write-Host "`n*** DRY RUN MODE - No files will be modified ***" -ForegroundColor Cyan
1461
+ }
1462
+
1463
+ # Step 9: Confirm summary
1464
+ Write-Host "`nStep 7: Confirm Operation" -ForegroundColor Yellow
1465
+ $confirmed = Confirm-Summary -SourcePath $sourceFolder -SelectedTypes $selectedTypes -TypeCounts $typeCounts -Action $action -Destinations $typeDestinationMap -DryRun $dryRun
1466
+
1467
+ if (-not $confirmed) {
1468
+ Write-Host "Operation cancelled by user." -ForegroundColor Yellow
1469
+ return
1470
+ }
1471
+
1472
+ # Step 10: Filter files to only selected types, then process
1473
+ $filteredFiles = @($files | Where-Object {
1474
+ $typeName = Get-FileType -Extension $_.Extension
1475
+ @($selectedTypes) -contains $typeName
1476
+ })
1477
+
1478
+ Write-Host "`nFiles to process: $($filteredFiles.Count) (of $($files.Count) total scanned)" -ForegroundColor Gray
1479
+
1480
+ $createSubfolder = [bool]$Script:Config.createTypeSubfolder
1481
+ $result = Process-Files -Files $filteredFiles -SelectedTypes @($selectedTypes) -TypeDestinationMap $typeDestinationMap -Action $action -DryRun $dryRun -CreateTypeSubfolder $createSubfolder
1482
+
1483
+ # Step 11: Show results
1484
+ Write-Host "`n=== Operation Complete ===" -ForegroundColor Green
1485
+ Write-Host "Files processed: $($result.Processed)" -ForegroundColor White
1486
+
1487
+ if ($result.Errors -gt 0) {
1488
+ Write-Host "Errors: $($result.Errors)" -ForegroundColor Red
1489
+ }
1490
+
1491
+ Write-Log "INFO" "Operation completed - Processed: $($result.Processed), Errors: $($result.Errors)"
1492
+
1493
+ # Save last used paths on success
1494
+ if ($result.Processed -gt 0 -and -not $dryRun) {
1495
+ $firstDest = ($typeDestinationMap.Values | Select-Object -First 1)
1496
+ Save-LastUsed -SourcePath $sourceFolder -DestPath $firstDest
1497
+ }
1498
+ }
1499
+
1500
+ function Scan-SourcePath {
1501
+ <#
1502
+ .SYNOPSIS
1503
+ Wrapper for scanning with user feedback
1504
+ #>
1505
+ [CmdletBinding()]
1506
+ param(
1507
+ [string]$SourcePath,
1508
+ [int]$Depth
1509
+ )
1510
+
1511
+ return Scan-SourceFiles -SourcePath $SourcePath -Depth $Depth
1512
+ }
1513
+
1514
+ function Start-CLIMode {
1515
+ <#
1516
+ .SYNOPSIS
1517
+ Runs the command-line interface mode with provided parameters
1518
+ #>
1519
+ [CmdletBinding()]
1520
+ param()
1521
+
1522
+ Write-Host "`n=== FileOrganizer - CLI Mode ===" -ForegroundColor Cyan
1523
+
1524
+ # Validate source path
1525
+ if (-not $SourcePath) {
1526
+ Write-Error "SourcePath is required in CLI mode. Use -SourcePath parameter."
1527
+ return
1528
+ }
1529
+
1530
+ if (-not (Test-Path $SourcePath)) {
1531
+ Write-Error "Source path does not exist: $SourcePath"
1532
+ return
1533
+ }
1534
+
1535
+ # Set default destination if not provided
1536
+ if (-not $DestPath) {
1537
+ $DestPath = $SourcePath
1538
+ Write-Host "No destination specified, using source path: $DestPath" -ForegroundColor Yellow
1539
+ }
1540
+
1541
+ # Scan files
1542
+ $files = Scan-SourceFiles -SourcePath $SourcePath -Depth $Depth
1543
+
1544
+ if ($files.Count -eq 0) {
1545
+ Write-Host "No files found." -ForegroundColor Yellow
1546
+ return
1547
+ }
1548
+
1549
+ # Get type summary
1550
+ $typeCounts = Get-TypeSummary -Files $files
1551
+
1552
+ # Show summary
1553
+ Write-Host "`nFile Type Summary:" -ForegroundColor Green
1554
+ foreach ($type in ($typeCounts.Keys | Sort-Object)) {
1555
+ Write-Host " $type : $($typeCounts[$type]) files" -ForegroundColor White
1556
+ }
1557
+
1558
+ # Determine selected types (all by default in CLI)
1559
+ $selectedTypes = $typeCounts.Keys
1560
+
1561
+ # Set up destination map
1562
+ $typeDestinationMap = @{}
1563
+
1564
+ if ($SeparateDestinations) {
1565
+ Write-Host "`nSeparate destinations requested but not implemented in CLI mode." -ForegroundColor Yellow
1566
+ Write-Host "Using single destination: $DestPath" -ForegroundColor Yellow
1567
+ }
1568
+
1569
+ foreach ($type in $selectedTypes) {
1570
+ $typeDestinationMap[$type] = $DestPath
1571
+ }
1572
+
1573
+ # Show dry run status
1574
+ if ($DryRun) {
1575
+ Write-Host "`n*** DRY RUN MODE - No files will be modified ***" -ForegroundColor Cyan
1576
+ }
1577
+
1578
+ # Filter files to selected types, then process
1579
+ $filteredFiles = @($files | Where-Object {
1580
+ $typeName = Get-FileType -Extension $_.Extension
1581
+ @($selectedTypes) -contains $typeName
1582
+ })
1583
+
1584
+ $createSubfolder = [bool]$Script:Config.createTypeSubfolder
1585
+ $result = Process-Files -Files $filteredFiles -TypeDestinationMap $typeDestinationMap -Action $Action -DryRun $DryRun -CreateTypeSubfolder $createSubfolder
1586
+
1587
+ # Show results
1588
+ Write-Host "`n=== Operation Complete ===" -ForegroundColor Green
1589
+ Write-Host "Files processed: $($result.Processed)" -ForegroundColor White
1590
+
1591
+ if ($result.Errors -gt 0) {
1592
+ Write-Host "Errors: $($result.Errors)" -ForegroundColor Red
1593
+ }
1594
+
1595
+ Write-Log "INFO" "CLI operation completed - Processed: $($result.Processed), Errors: $($result.Errors)"
1596
+
1597
+ # Save last used paths
1598
+ if ($result.Processed -gt 0 -and -not $DryRun) {
1599
+ Save-LastUsed -SourcePath $SourcePath -DestPath $DestPath
1600
+ }
1601
+ }
1602
+
1603
+ #endregion
1604
+
1605
+ #region Main Entry Point
1606
+
1607
+ function Main {
1608
+ <#
1609
+ .SYNOPSIS
1610
+ Main entry point for the script
1611
+ #>
1612
+ [CmdletBinding()]
1613
+ param()
1614
+
1615
+ # Initialize environment
1616
+ Initialize-Environment
1617
+
1618
+ # Check if Help requested
1619
+ if ($Help) {
1620
+ Get-Help $MyInvocation.MyCommand.Path -Detailed
1621
+ return
1622
+ }
1623
+
1624
+ # Determine mode: CLI or Interactive
1625
+ $isCLIMode = $SourcePath -and $DestPath
1626
+
1627
+ if ($isCLIMode) {
1628
+ # Run in CLI mode
1629
+ Start-CLIMode
1630
+ }
1631
+ else {
1632
+ # Run in Interactive mode
1633
+ Start-InteractiveMode
1634
+
1635
+ # Loop for additional tasks unless -NoLoop specified
1636
+ if (-not $NoLoop) {
1637
+ while ($true) {
1638
+ Write-Host "`n" -NoNewline
1639
+ $again = Read-Host "Do another task? y/n"
1640
+
1641
+ if ($again -ne "y") {
1642
+ break
1643
+ }
1644
+
1645
+ Start-InteractiveMode
1646
+ }
1647
+ }
1648
+ }
1649
+
1650
+ Write-Host "`nGoodbye!" -ForegroundColor Cyan
1651
+ Write-Log "INFO" "FileOrganizer ended"
1652
+
1653
+ # Pause to keep window open
1654
+ Write-Host "`nPress Enter to exit..." -ForegroundColor Gray
1655
+ try { [void](Read-Host) } catch { Start-Sleep -Seconds 5 }
1656
+ }
1657
+
1658
+ # Run main function
1659
+ try {
1660
+ Main
1661
+ }
1662
+ catch {
1663
+ $errMsg = $_.Exception.Message
1664
+ $stackTrace = $_.ScriptStackTrace
1665
+ Write-Host "`n" -ForegroundColor Red
1666
+ Write-Host "FATAL ERROR: $errMsg" -ForegroundColor Red
1667
+ Write-Host "Stack Trace: $stackTrace" -ForegroundColor Red
1668
+ Write-Host "`nPress Enter to exit..." -ForegroundColor Yellow
1669
+ try { Read-Host } catch { Start-Sleep -Seconds 5 }
1670
+ exit 1
1671
+ }
1672
+
1673
+ #endregion
config.json ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "categories": {
3
+ "PowerPoint Files": [
4
+ "ppt",
5
+ "pptx",
6
+ "pptm"
7
+ ],
8
+ "Archives": [
9
+ "zip",
10
+ "rar",
11
+ "7z",
12
+ "tar",
13
+ "gz"
14
+ ],
15
+ "Images": [
16
+ "jpg",
17
+ "jpeg",
18
+ "png",
19
+ "gif",
20
+ "bmp",
21
+ "svg",
22
+ "webp",
23
+ "tiff"
24
+ ],
25
+ "Videos": [
26
+ "mp4",
27
+ "avi",
28
+ "mkv",
29
+ "mov",
30
+ "wmv",
31
+ "flv"
32
+ ],
33
+ "Text": [
34
+ "txt",
35
+ "md",
36
+ "log",
37
+ "rtf",
38
+ "json",
39
+ "xml",
40
+ "yaml",
41
+ "yml",
42
+ "ini",
43
+ "cfg"
44
+ ],
45
+ "PDF Files": [
46
+ "pdf"
47
+ ],
48
+ "Executables": [
49
+ "exe",
50
+ "msi",
51
+ "bat",
52
+ "cmd"
53
+ ],
54
+ "Code": [
55
+ "ps1",
56
+ "py",
57
+ "js",
58
+ "ts",
59
+ "cpp",
60
+ "c",
61
+ "h",
62
+ "cs",
63
+ "java",
64
+ "rb",
65
+ "go",
66
+ "rs"
67
+ ],
68
+ "Word Files": [
69
+ "doc",
70
+ "docx",
71
+ "docm",
72
+ "dotx"
73
+ ],
74
+ "Excel Files": [
75
+ "xls",
76
+ "xlsx",
77
+ "xlsm",
78
+ "csv"
79
+ ],
80
+ "Audio": [
81
+ "mp3",
82
+ "wav",
83
+ "flac",
84
+ "ogg",
85
+ "aac",
86
+ "wma"
87
+ ]
88
+ },
89
+ "defaultDepth": 100,
90
+ "defaultAction": "Move",
91
+ "excludePatterns": [
92
+ "Thumbs.db",
93
+ ".DS_Store",
94
+ "desktop.ini",
95
+ "*.tmp",
96
+ "~$*"
97
+ ],
98
+ "createTypeSubfolder": true
99
+ }