@@ -39,14 +39,17 @@ param(
3939 [string ]$Language ,
4040 [ValidateSet (' User' , ' Developer' )]
4141 [string ]$Role ,
42- [ValidateSet (' scan' , ' clean' , ' downloads' , ' devscan' , ' install-devradar' , ' install-claudefy' , ' restore' , ' temp' , ' schedule' )]
42+ [ValidateSet (' scan' , ' clean' , ' downloads' , ' devscan' , ' install-devradar' , ' install-claudefy' , ' restore' , ' temp' , ' schedule' , ' clean-resume ' )]
4343 [string ]$Action
4444)
4545
4646$ErrorActionPreference = ' Continue'
47+ # Tắt progress bar mặc định của các cmdlet hệ thống (Get/Remove-NetFirewallRule,
48+ # Invoke-WebRequest...) - chúng vẽ khối xanh to chèn lên giao diện của mình
49+ $ProgressPreference = ' SilentlyContinue'
4750try { [Console ]::OutputEncoding = [System.Text.Encoding ]::UTF8 } catch {}
4851
49- $script :WinTrashVersion = [version ]' 1.1.1 '
52+ $script :WinTrashVersion = [version ]' 1.1.2 '
5053$script :UpdateRawBase = ' https://raw.githubusercontent.com/hasoftware/WinTrash/main'
5154
5255# ════════════════════════════ I18N ════════════════════════════
@@ -92,6 +95,10 @@ $i18n = @{
9295 UpdateFound = ' Có phiên bản mới {0} (bạn đang dùng {1}). Cập nhật ngay? [y/N]'
9396 UpdateDone = ' Cập nhật thành công - đang khởi động lại...'
9497 UpdateFail = ' Không cập nhật được: {0} - tiếp tục dùng phiên bản hiện tại.'
98+ ElevateAsk = ' {0}/{1} mục cần quyền Administrator. Mở cửa sổ Admin để dọn TOÀN BỘ? [y = mở Admin / n = chỉ dọn phần làm được]'
99+ ElevateLaunched = ' Đã mở cửa sổ Administrator - phần dọn tiếp tục ở đó (quét lại nhanh rồi tự dọn đúng các mục bạn đã chọn).'
100+ SkippedAdmin = ' Đã bỏ qua {0} mục cần Administrator.'
101+ ResumeNothing = ' Không còn mục nào khớp danh sách đã chọn (có thể đã được dọn).'
95102 PickerTitle = ' CHỌN CÁC MỤC MUỐN DỌN (chưa xóa gì cho tới khi bạn xác nhận)'
96103 NothingFound = ' Không phát hiện mục nào có thể dọn. Máy sạch!'
97104 NothingSel = ' Không chọn mục nào - không làm gì cả.'
@@ -151,6 +158,10 @@ $i18n = @{
151158 UpdateFound = ' New version {0} available (you have {1}). Update now? [y/N]'
152159 UpdateDone = ' Updated successfully - restarting...'
153160 UpdateFail = ' Update failed: {0} - continuing with current version.'
161+ ElevateAsk = ' {0}/{1} items require Administrator. Open an Admin window to clean EVERYTHING? [y = elevate / n = clean what is possible now]'
162+ ElevateLaunched = ' Administrator window opened - cleanup continues there (quick re-scan, then cleans exactly what you picked).'
163+ SkippedAdmin = ' Skipped {0} items that require Administrator.'
164+ ResumeNothing = ' No items match the saved selection (they may already be cleaned).'
154165 PickerTitle = ' SELECT ITEMS TO CLEAN (nothing is deleted until you confirm)'
155166 NothingFound = ' Nothing cleanable found. Your machine is clean!'
156167 NothingSel = ' Nothing selected - no action taken.'
@@ -210,6 +221,10 @@ $i18n = @{
210221 UpdateFound = ' 发现新版本 {0}(当前 {1})。立即更新?[y/N]'
211222 UpdateDone = ' 更新成功 - 正在重新启动...'
212223 UpdateFail = ' 更新失败:{0} - 继续使用当前版本。'
224+ ElevateAsk = ' {0}/{1} 项需要管理员权限。打开管理员窗口清理全部?[y = 提权 / n = 仅清理当前可行的]'
225+ ElevateLaunched = ' 已打开管理员窗口 - 清理将在那里继续(快速重扫后清理您选择的项目)。'
226+ SkippedAdmin = ' 已跳过 {0} 个需要管理员权限的项目。'
227+ ResumeNothing = ' 没有与已保存选择匹配的项目(可能已被清理)。'
213228 PickerTitle = ' 选择要清理的项目(确认前不会删除任何内容)'
214229 NothingFound = ' 未发现可清理的项目。您的电脑很干净!'
215230 NothingSel = ' 未选择任何项目 - 不执行任何操作。'
@@ -269,6 +284,10 @@ $i18n = @{
269284 UpdateFound = ' Доступна новая версия {0} (у вас {1}). Обновить сейчас? [y/N]'
270285 UpdateDone = ' Обновление выполнено - перезапуск...'
271286 UpdateFail = ' Ошибка обновления: {0} - продолжаем с текущей версией.'
287+ ElevateAsk = ' {0}/{1} пунктов требуют прав администратора. Открыть окно администратора для полной очистки? [y / n = очистить возможное]'
288+ ElevateLaunched = ' Окно администратора открыто - очистка продолжится там (пересканирование, затем очистка выбранного).'
289+ SkippedAdmin = ' Пропущено пунктов, требующих администратора: {0}.'
290+ ResumeNothing = ' Нет пунктов, соответствующих сохранённому выбору (возможно, уже очищены).'
272291 PickerTitle = ' ВЫБЕРИТЕ ПУНКТЫ ДЛЯ ОЧИСТКИ (ничего не удаляется до подтверждения)'
273292 NothingFound = ' Ничего для очистки не найдено. Ваш компьютер чист!'
274293 NothingSel = ' Ничего не выбрано - действий не выполнено.'
@@ -1283,9 +1302,11 @@ function Invoke-ScanProtocols {
12831302 if ([string ]::IsNullOrWhiteSpace($command )) { continue }
12841303 $exe = Resolve-CommandPath - CommandLine $command
12851304 if ($exe -and $exe -match ' ^[A-Za-z]:\\' -and (Test-ExeMissing - ExePath $exe )) {
1305+ # HKCR là VIEW GỘP của HKCU+HKLM Classes - phải xóa ở hive thật,
1306+ # xóa qua HKCR sẽ vỡ giữa chừng ("subkey does not exist")
12861307 Add-Finding - Category ' Protocols' - Name " $name `://" - Target $exe `
12871308 - Detail ' Protocol handler trỏ exe đã mất' `
1288- - RemoveKind ' RegKey ' - RemoveData @ { PSPath = " Registry::HKEY_CLASSES_ROOT\ $name " }
1309+ - RemoveKind ' ProtocolKey ' - RemoveData @ { Name = $name }
12891310 }
12901311 } finally { $cmdKey.Close () }
12911312 } finally { $key.Close () }
@@ -1377,6 +1398,25 @@ function Invoke-ScanDevTrash {
13771398 }
13781399}
13791400
1401+ function Test-FindingNeedsAdmin {
1402+ # Ước lượng mục này có cần Administrator để xóa không (để cảnh báo TRƯỚC khi làm)
1403+ param ($Finding )
1404+ switch ($Finding.RemoveKind ) {
1405+ ' Service' { return $true }
1406+ ' Firewall' { return $true }
1407+ ' DefenderPath' { return $true }
1408+ ' DefenderProcess' { return $true }
1409+ ' Task' { return ($Finding.RemoveData.TaskPath -eq ' \' ) } # task gốc thường của hệ thống
1410+ ' PathEntry' { return ($Finding.RemoveData.Scope -eq ' Machine' ) }
1411+ ' RegValue' { return ([string ]$Finding.RemoveData.PSPath -match ' ^(HKLM:|Registry::HKEY_LOCAL_MACHINE)' ) }
1412+ ' RegKey' { return ([string ]$Finding.RemoveData.PSPath -match ' ^(HKLM:|Registry::HKEY_LOCAL_MACHINE)' ) }
1413+ ' ProtocolKey' { return (Test-Path (" HKLM:\Software\Classes\{0}" -f $Finding.RemoveData.Name )) }
1414+ ' RecycleDir' { return ([string ]$Finding.RemoveData.Path -match ' ^[A-Za-z]:\\(ProgramData|Program Files)' ) }
1415+ ' RecycleFile' { return ([string ]$Finding.RemoveData.Path -match ' ^[A-Za-z]:\\(ProgramData|Program Files)' ) }
1416+ default { return $false }
1417+ }
1418+ }
1419+
13801420# ════════════════════════ REMOVAL ENGINE ════════════════════════
13811421
13821422function Remove-SelectedFindings {
@@ -1406,11 +1446,47 @@ function Remove-SelectedFindings {
14061446 $deferOrSkip = $true # xử lý gộp ở cuối
14071447 }
14081448 ' RecycleDir' {
1409- [Microsoft.VisualBasic.FileIO.FileSystem ]::DeleteDirectory($f.RemoveData.Path , ' OnlyErrorDialogs' , ' SendToRecycleBin' )
1449+ try {
1450+ [Microsoft.VisualBasic.FileIO.FileSystem ]::DeleteDirectory($f.RemoveData.Path , ' OnlyErrorDialogs' , ' SendToRecycleBin' )
1451+ } catch {
1452+ # API Recycle Bin dở chứng (vd: "not supported") -> chuyển cả thư mục
1453+ # vào thư mục backup của lần dọn này (vẫn hoàn tác được)
1454+ $destDir = Join-Path $backupDir (" dir_{0}_{1}" -f $idx , (Split-Path $f.RemoveData.Path - Leaf))
1455+ Move-Item - LiteralPath $f.RemoveData.Path - Destination $destDir - Force - ErrorAction Stop
1456+ }
14101457 if (Test-Path - LiteralPath $f.RemoveData.Path ) { throw ' Thư mục vẫn còn (bị khóa?)' }
14111458 }
14121459 ' RecycleFile' {
1413- [Microsoft.VisualBasic.FileIO.FileSystem ]::DeleteFile($f.RemoveData.Path , ' OnlyErrorDialogs' , ' SendToRecycleBin' )
1460+ try {
1461+ [Microsoft.VisualBasic.FileIO.FileSystem ]::DeleteFile($f.RemoveData.Path , ' OnlyErrorDialogs' , ' SendToRecycleBin' )
1462+ } catch {
1463+ # Fallback: copy file vào backup rồi xóa thẳng (fix "not supported" ở ProgramData)
1464+ $destFile = Join-Path $backupDir (" file_{0}_{1}" -f $idx , (Split-Path $f.RemoveData.Path - Leaf))
1465+ Copy-Item - LiteralPath $f.RemoveData.Path - Destination $destFile - Force - ErrorAction Stop
1466+ Remove-Item - LiteralPath $f.RemoveData.Path - Force - ErrorAction Stop
1467+ }
1468+ }
1469+ ' ProtocolKey' {
1470+ # Xóa ở CẢ HAI hive thật (HKCU + HKLM Software\Classes), backup từng cái
1471+ $protoName = $f.RemoveData.Name
1472+ $safeProto = ($protoName -replace ' [^\w\.-]' , ' _' )
1473+ $deletedAny = $false
1474+ $protoErrors = [System.Collections.Generic.List [string ]]::new()
1475+ foreach ($hivePair in @ (
1476+ @ { PS = " HKCU:\Software\Classes\$protoName " ; Reg = " HKEY_CURRENT_USER\Software\Classes\$protoName " },
1477+ @ { PS = " HKLM:\Software\Classes\$protoName " ; Reg = " HKEY_LOCAL_MACHINE\Software\Classes\$protoName " })) {
1478+ if (-not (Test-Path - Path $hivePair.PS )) { continue }
1479+ & reg.exe export $hivePair.Reg (Join-Path $backupDir " protocol_$idx `_$safeProto .reg" ) / y | Out-Null
1480+ try {
1481+ Remove-Item - Path $hivePair.PS - Recurse - Force - ErrorAction Stop
1482+ $deletedAny = $true
1483+ } catch { $protoErrors.Add ($_.Exception.Message ) }
1484+ }
1485+ if ($protoErrors.Count -gt 0 ) {
1486+ if ($deletedAny ) { throw (' đã xóa 1 hive, hive còn lại lỗi: ' + ($protoErrors -join ' ; ' )) }
1487+ throw ($protoErrors -join ' ; ' )
1488+ }
1489+ if (-not $deletedAny ) { throw ' không tìm thấy key ở hive nào (đã bị xóa trước đó?)' }
14141490 }
14151491 ' RegValue' {
14161492 $regExe = ConvertTo-RegExePath - PSPath $f.RemoveData.PSPath
@@ -1664,6 +1740,48 @@ function Invoke-FlowClean {
16641740 $selected = @ ($selectedIdx | ForEach-Object { $removable [$_ ] })
16651741 $answer = Read-Host ($L.ConfirmDel -f $selected.Count )
16661742 if ($answer -notmatch ' ^[yY]' ) { Write-Host $L.NothingSel - ForegroundColor Yellow; return }
1743+
1744+ # Chưa phải admin mà có mục cần admin -> đề nghị mở cửa sổ Administrator
1745+ # (tránh cảnh cả trăm dòng lỗi "Access is denied" như trước)
1746+ if (-not (Test-IsAdmin )) {
1747+ $adminNeeded = @ ($selected | Where-Object { Test-FindingNeedsAdmin - Finding $_ })
1748+ if ($adminNeeded.Count -gt 0 -and (Test-Interactive )) {
1749+ $elevAnswer = Read-Host ($L.ElevateAsk -f $adminNeeded.Count , $selected.Count )
1750+ if ($elevAnswer -match ' ^[yY]' ) {
1751+ # Lưu danh sách ID đã chọn -> cửa sổ admin quét lại và dọn đúng các mục này
1752+ $pendingDir = Join-Path $PSScriptRoot ' WinTrashBackups'
1753+ if (-not (Test-Path - LiteralPath $pendingDir )) { New-Item - ItemType Directory - Path $pendingDir - Force | Out-Null }
1754+ $ids = @ ($selected | ForEach-Object { Get-FindingId $_ })
1755+ ConvertTo-Json $ids | Set-Content - LiteralPath (Join-Path $pendingDir ' pending-clean.json' ) - Encoding UTF8
1756+ $argStr = ' -NoProfile -ExecutionPolicy Bypass -File "{0}" -Language {1} -Action clean-resume' -f $PSCommandPath , $script :Language
1757+ Start-Process - FilePath ' powershell.exe' - Verb RunAs - ArgumentList $argStr
1758+ Write-Host $L.ElevateLaunched - ForegroundColor Green
1759+ return
1760+ }
1761+ # Chọn n: chỉ dọn phần làm được ở quyền thường
1762+ $selected = @ ($selected | Where-Object { -not (Test-FindingNeedsAdmin - Finding $_ ) })
1763+ Write-Host ($L.SkippedAdmin -f $adminNeeded.Count ) - ForegroundColor Yellow
1764+ if ($selected.Count -eq 0 ) { return }
1765+ }
1766+ }
1767+ Remove-SelectedFindings - Selected $selected - L $L
1768+ }
1769+
1770+ function Invoke-FlowCleanResume {
1771+ # Chạy trong cửa sổ Administrator: đọc danh sách ID đã chọn, quét lại, dọn đúng các mục đó
1772+ param ([hashtable ]$L )
1773+ $pendingFile = Join-Path $PSScriptRoot ' WinTrashBackups\pending-clean.json'
1774+ if (-not (Test-Path - LiteralPath $pendingFile )) { Write-Host $L.ResumeNothing - ForegroundColor Yellow; return }
1775+ $ids = @ (Get-Content - LiteralPath $pendingFile - Raw | ConvertFrom-Json )
1776+ Remove-Item - LiteralPath $pendingFile - Force - ErrorAction SilentlyContinue
1777+ Invoke-AllScans - L $L
1778+ if (@ ($ids | Where-Object { $_ -like ' DevTrash|*' }).Count -gt 0 ) {
1779+ $spinnerHandle = Start-ScanSpinner - Text ' DevTrash...'
1780+ Invoke-ScanDevTrash
1781+ Stop-ScanSpinner - Handle $spinnerHandle
1782+ }
1783+ $selected = @ ($script :findings | Where-Object { $_.RemoveKind -ne ' None' -and $ids -contains (Get-FindingId $_ ) })
1784+ if ($selected.Count -eq 0 ) { Write-Host $L.ResumeNothing - ForegroundColor Yellow; return }
16671785 Remove-SelectedFindings - Selected $selected - L $L
16681786}
16691787
@@ -1983,6 +2101,7 @@ function Invoke-OneAction {
19832101 ' devscan' { Invoke-FlowClean - L $L - DevOnly }
19842102 ' install-devradar' { Invoke-FlowInstall - L $L - Package ' devradar' }
19852103 ' install-claudefy' { Invoke-FlowInstall - L $L - Package ' claudefy' }
2104+ ' clean-resume' { Invoke-FlowCleanResume - L $L }
19862105 }
19872106}
19882107
@@ -1995,6 +2114,8 @@ if ($Action) {
19952114 Show-Banner - Tagline $tagline
19962115 Show-Spinner - Label $L.Init
19972116 Invoke-OneAction - L $L - Key $Action
2117+ # Cửa sổ admin mở riêng cho clean-resume: giữ lại để người dùng đọc kết quả
2118+ if ($Action -eq ' clean-resume' -and (Test-Interactive )) { Read-Host $L.PressEnter | Out-Null }
19982119 return
19992120}
20002121
0 commit comments