Add Satsuma scripts

This commit is contained in:
2026-10-22 23:04:57 +00:00
commit def171a0d9
5 changed files with 666 additions and 0 deletions
+356
View File
@@ -0,0 +1,356 @@
param(
[string]$Target,
[string]$Source
)
# Satsuma converter. Called by Run.vbs from the right-click menu.
# Target is the format wanted (jpg, png, mp4, pdf ...). Source is the full path of the file.
$ErrorActionPreference = 'Continue'
$root = $PSScriptRoot
$logFile = Join-Path $root 'log.txt'
function Write-Log([string]$text) {
try {
if ((Test-Path -LiteralPath $logFile) -and ((Get-Item -LiteralPath $logFile).Length -gt 1048576)) {
Remove-Item -LiteralPath $logFile -Force
}
Add-Content -LiteralPath $logFile -Value ('[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $text)
} catch { }
}
function Limit-Text([string]$s, [int]$max) {
if ($null -eq $s) { return '' }
$s = ($s -replace '\s+', ' ').Trim()
if ($s.Length -gt $max) { $s = $s.Substring(0, $max - 3) + '...' }
return $s
}
# ---------- tray notifications ----------
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$tray = New-Object System.Windows.Forms.NotifyIcon
$icoPath = Join-Path $root 'satsuma.ico'
if (Test-Path -LiteralPath $icoPath) {
$tray.Icon = New-Object System.Drawing.Icon($icoPath)
} else {
$tray.Icon = [System.Drawing.SystemIcons]::Information
}
$tray.Visible = $true
function Send-Note([string]$title, [string]$text, [string]$kind) {
try {
$tray.BalloonTipTitle = (Limit-Text $title 60)
$tray.BalloonTipText = (Limit-Text $text 250)
$tray.BalloonTipIcon = $kind
$tray.ShowBalloonTip(5000)
} catch { }
}
# ---------- helpers ----------
function Test-Nonempty([string]$p) {
return ((Test-Path -LiteralPath $p) -and ((Get-Item -LiteralPath $p).Length -gt 0))
}
function Remove-IfExists([string]$p) {
if (Test-Path -LiteralPath $p) {
Remove-Item -LiteralPath $p -Force -Recurse -ErrorAction SilentlyContinue
}
}
function Get-OutPath([string]$src, [string]$ext) {
$dir = [System.IO.Path]::GetDirectoryName($src)
$base = [System.IO.Path]::GetFileNameWithoutExtension($src)
$candidate = Join-Path $dir ($base + '.' + $ext)
$n = 1
while (Test-Path -LiteralPath $candidate) {
$candidate = Join-Path $dir ('{0} ({1}).{2}' -f $base, $n, $ext)
$n++
}
return $candidate
}
function Invoke-Tool([string]$exe, [string[]]$arguments) {
# Turn every output line (including error lines) into plain text so messages stay readable.
$text = & $exe @arguments 2>&1 | ForEach-Object { $_.ToString() } | Out-String
$code = $LASTEXITCODE
return @{ Code = $code; Output = $text }
}
function Get-Ffmpeg {
$p = Join-Path $root 'bin\ffmpeg.exe'
if (Test-Path -LiteralPath $p) { return $p }
$c = Get-Command ffmpeg.exe -ErrorAction SilentlyContinue
if ($c) { return $c.Source }
throw 'ffmpeg was not found. Run Install.bat again.'
}
function Get-Magick {
$dir = Join-Path $root 'bin\magick'
if (Test-Path -LiteralPath $dir) {
$f = Get-ChildItem -LiteralPath $dir -Filter 'magick.exe' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($f) { return $f.FullName }
}
foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)})) {
if ($base -and (Test-Path -LiteralPath $base)) {
$d = Get-ChildItem -LiteralPath $base -Filter 'ImageMagick*' -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1
if ($d) {
$m = Join-Path $d.FullName 'magick.exe'
if (Test-Path -LiteralPath $m) { return $m }
}
}
}
$c = Get-Command magick.exe -ErrorAction SilentlyContinue
if ($c) { return $c.Source }
throw 'ImageMagick was not found. Run Install.bat again.'
}
function Find-Soffice {
$candidates = @(
(Join-Path $env:ProgramFiles 'LibreOffice\program\soffice.exe'),
(Join-Path ${env:ProgramFiles(x86)} 'LibreOffice\program\soffice.exe')
)
foreach ($c in $candidates) {
if ($c -and (Test-Path -LiteralPath $c)) { return $c }
}
return $null
}
function Free-Com($obj) {
if ($null -ne $obj) {
try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($obj) } catch { }
}
}
# ---------- video ----------
function Convert-Video([string]$src, [string]$out, [string]$t) {
$ff = Get-Ffmpeg
$pre = @('-hide_banner', '-loglevel', 'error', '-nostdin', '-y', '-i', $src, '-map', '0:v:0', '-map', '0:a?')
# Several encoders refuse odd sizes (321x241 for example), so round every size down to even.
$evenSize = @('-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2')
$x264 = @('-c:v', 'libx264', '-preset', 'fast', '-crf', '20', '-pix_fmt', 'yuv420p')
$aac = @('-c:a', 'aac', '-b:a', '192k')
switch ($t) {
'mp4' { $enc = $x264 + $aac + @('-movflags', '+faststart') }
'mov' { $enc = $x264 + $aac }
'mkv' { $enc = $x264 + $aac }
'flv' { $enc = $x264 + $aac + @('-ar', '44100') }
'webm' { $enc = @('-c:v', 'libvpx-vp9', '-crf', '30', '-b:v', '0', '-row-mt', '1', '-cpu-used', '4', '-pix_fmt', 'yuv420p', '-c:a', 'libopus', '-b:a', '128k') }
'avi' { $enc = @('-c:v', 'mpeg4', '-q:v', '3', '-pix_fmt', 'yuv420p', '-c:a', 'libmp3lame', '-q:a', '3', '-ac', '2') }
'wmv' { $enc = @('-c:v', 'wmv2', '-b:v', '5M', '-pix_fmt', 'yuv420p', '-c:a', 'wmav2', '-b:a', '192k', '-ac', '2') }
'mpg' { $enc = @('-c:v', 'mpeg2video', '-q:v', '3', '-pix_fmt', 'yuv420p', '-c:a', 'mp2', '-b:a', '192k', '-ar', '48000', '-ac', '2') }
default { throw ('Unsupported video format: ' + $t) }
}
# MKV can hold nearly anything, so try a fast lossless copy first.
if ($t -eq 'mkv') {
$r = Invoke-Tool $ff ($pre + @('-c', 'copy', $out))
if (($r.Code -eq 0) -and (Test-Nonempty $out)) { return }
Remove-IfExists $out
}
$r = Invoke-Tool $ff ($pre + $evenSize + $enc + @($out))
if (($r.Code -ne 0) -or -not (Test-Nonempty $out)) {
Remove-IfExists $out
throw ('ffmpeg failed: ' + (Limit-Text $r.Output 200))
}
}
# ---------- audio (also pulls the sound out of a video) ----------
function Convert-Audio([string]$src, [string]$out, [string]$t) {
$ff = Get-Ffmpeg
$pre = @('-hide_banner', '-loglevel', 'error', '-nostdin', '-y', '-i', $src, '-vn', '-map', '0:a:0')
switch ($t) {
'mp3' { $enc = @('-c:a', 'libmp3lame', '-q:a', '2') }
'wav' { $enc = @('-c:a', 'pcm_s16le') }
'flac' { $enc = @('-c:a', 'flac') }
'ogg' { $enc = @('-c:a', 'libvorbis', '-q:a', '5') }
'm4a' { $enc = @('-c:a', 'aac', '-b:a', '192k') }
'opus' { $enc = @('-c:a', 'libopus', '-b:a', '128k') }
default { throw ('Unsupported audio format: ' + $t) }
}
$r = Invoke-Tool $ff ($pre + $enc + @($out))
if (($r.Code -ne 0) -or -not (Test-Nonempty $out)) {
Remove-IfExists $out
throw ('ffmpeg failed: ' + (Limit-Text $r.Output 200))
}
}
# ---------- images ----------
function Convert-Image([string]$src, [string]$out, [string]$srcExt, [string]$t) {
$mg = Get-Magick
$stamp = [guid]::NewGuid().ToString('N')
# Work on safe temp names so odd characters in file names cannot confuse ImageMagick.
$tmpIn = Join-Path $env:TEMP ('satsuma_{0}_in.{1}' -f $stamp, $srcExt)
$tmpOut = Join-Path $env:TEMP ('satsuma_{0}_out.{1}' -f $stamp, $t)
try {
Copy-Item -LiteralPath $src -Destination $tmpIn -Force
$mArgs = @(($tmpIn + '[0]'), '-auto-orient')
switch ($t) {
'jpg' { $mArgs += @('-background', 'white', '-alpha', 'remove', '-alpha', 'off', '-quality', '92') }
'bmp' { $mArgs += @('-background', 'white', '-alpha', 'remove', '-alpha', 'off') }
'pdf' { $mArgs += @('-background', 'white', '-alpha', 'remove', '-alpha', 'off', '-quality', '92') }
'webp' { $mArgs += @('-quality', '90') }
'tiff' { $mArgs += @('-compress', 'lzw') }
'png' { }
'gif' { }
default { throw ('Unsupported image format: ' + $t) }
}
$mArgs += $tmpOut
$r = Invoke-Tool $mg $mArgs
if (($r.Code -ne 0) -or -not (Test-Nonempty $tmpOut)) {
throw ('ImageMagick failed: ' + (Limit-Text $r.Output 200))
}
Move-Item -LiteralPath $tmpOut -Destination $out -Force
} finally {
Remove-IfExists $tmpIn
Remove-IfExists $tmpOut
}
}
# ---------- documents to PDF ----------
function Convert-WithWord([string]$src, [string]$out) {
$app = $null
$doc = $null
try {
$app = New-Object -ComObject Word.Application
$app.Visible = $false
$app.DisplayAlerts = 0
# The odd password stops Word from showing a password box nobody can see.
$doc = $app.Documents.Open($src, $false, $true, $false, 'satsuma-no-password')
$doc.ExportAsFixedFormat($out, 17)
} finally {
if ($null -ne $doc) { try { $doc.Close(0) } catch { } }
if ($null -ne $app) { try { $app.Quit(0) } catch { } }
Free-Com $doc
Free-Com $app
}
}
function Convert-WithExcel([string]$src, [string]$out) {
$app = $null
$book = $null
try {
$app = New-Object -ComObject Excel.Application
$app.Visible = $false
$app.DisplayAlerts = $false
$missing = [System.Reflection.Missing]::Value
$book = $app.Workbooks.Open($src, 0, $true, $missing, 'satsuma-no-password')
$book.ExportAsFixedFormat(0, $out)
} finally {
if ($null -ne $book) { try { $book.Close($false) } catch { } }
if ($null -ne $app) { try { $app.Quit() } catch { } }
Free-Com $book
Free-Com $app
}
}
function Convert-WithPowerPoint([string]$src, [string]$out) {
$app = $null
$pres = $null
try {
$app = New-Object -ComObject PowerPoint.Application
$pres = $app.Presentations.Open($src, -1, 0, 0)
$pres.SaveAs($out, 32)
} finally {
if ($null -ne $pres) { try { $pres.Close() } catch { } }
if ($null -ne $app) { try { $app.Quit() } catch { } }
Free-Com $pres
Free-Com $app
}
}
function Convert-Document([string]$src, [string]$out, [string]$ext) {
$wordExts = @('doc', 'docx', 'rtf', 'txt', 'odt')
$excelExts = @('xls', 'xlsx', 'ods')
$slideExts = @('ppt', 'pptx', 'odp')
$problems = @()
try {
if ($wordExts -contains $ext) { Convert-WithWord $src $out }
elseif ($excelExts -contains $ext) { Convert-WithExcel $src $out }
elseif ($slideExts -contains $ext) { Convert-WithPowerPoint $src $out }
else { throw ('Unsupported document type: ' + $ext) }
} catch {
$problems += $_.Exception.Message
}
if (Test-Nonempty $out) { return }
Remove-IfExists $out
$soffice = Find-Soffice
if ($soffice) {
$tmpDir = Join-Path $env:TEMP ('satsuma_' + [guid]::NewGuid().ToString('N'))
[void](New-Item -ItemType Directory -Force -Path $tmpDir)
try {
$r = Invoke-Tool $soffice @('--headless', '--convert-to', 'pdf', '--outdir', $tmpDir, $src)
$made = Join-Path $tmpDir ([System.IO.Path]::GetFileNameWithoutExtension($src) + '.pdf')
if (Test-Nonempty $made) {
Move-Item -LiteralPath $made -Destination $out -Force
return
}
$problems += ('LibreOffice: ' + $r.Output)
} finally {
Remove-IfExists $tmpDir
}
} else {
$problems += 'LibreOffice not installed'
}
throw ('Could not make a PDF. This needs Microsoft Office or LibreOffice installed. ' + (Limit-Text ($problems -join ' | ') 160))
}
# ---------- main ----------
$videoTargets = @('mp4', 'mkv', 'webm', 'avi', 'mov', 'wmv', 'flv', 'mpg')
$audioTargets = @('mp3', 'wav', 'flac', 'ogg', 'm4a', 'opus')
$imageTargets = @('jpg', 'png', 'webp', 'bmp', 'tiff', 'gif')
$docExts = @('doc', 'docx', 'rtf', 'txt', 'odt', 'xls', 'xlsx', 'ods', 'ppt', 'pptx', 'odp')
$mutex = New-Object System.Threading.Mutex($false, 'Local\SatsumaQueue')
$haveLock = $false
$exitCode = 0
$name = ''
try {
if ([string]::IsNullOrWhiteSpace($Target) -or [string]::IsNullOrWhiteSpace($Source)) {
throw 'Missing format or file name.'
}
$Target = $Target.ToLower()
$name = [System.IO.Path]::GetFileName($Source)
$srcExt = [System.IO.Path]::GetExtension($Source).TrimStart('.').ToLower()
# One conversion at a time, so picking many files does not overload the machine.
try { $haveLock = $mutex.WaitOne() } catch [System.Threading.AbandonedMutexException] { $haveLock = $true }
if (-not (Test-Path -LiteralPath $Source)) { throw ('File not found: ' + $Source) }
Send-Note 'Satsuma' ('Converting {0} to {1}' -f $name, $Target.ToUpper()) 'Info'
$out = Get-OutPath $Source $Target
if ($Target -eq 'pdf') {
if ($docExts -contains $srcExt) { Convert-Document $Source $out $srcExt }
else { Convert-Image $Source $out $srcExt 'pdf' }
}
elseif ($videoTargets -contains $Target) { Convert-Video $Source $out $Target }
elseif ($audioTargets -contains $Target) { Convert-Audio $Source $out $Target }
elseif ($imageTargets -contains $Target) { Convert-Image $Source $out $srcExt $Target }
else { throw ('Unknown format: ' + $Target) }
$outName = [System.IO.Path]::GetFileName($out)
Write-Log ('OK {0} -> {1}' -f $Source, $out)
Send-Note 'Satsuma: done' $outName 'Info'
}
catch {
$msg = $_.Exception.Message
Write-Log ('FAILED {0} -> {1}: {2}' -f $Source, $Target, $msg)
Send-Note 'Satsuma could not convert' ($name + ': ' + $msg) 'Error'
$exitCode = 1
}
finally {
if ($haveLock) { try { $mutex.ReleaseMutex() } catch { } }
Start-Sleep -Seconds 5
try { $tray.Visible = $false; $tray.Dispose() } catch { }
}
exit $exitCode
+5
View File
@@ -0,0 +1,5 @@
@echo off
title Satsuma setup
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Install.ps1"
echo.
pause
+247
View File
@@ -0,0 +1,247 @@
# Satsuma installer. Run it with Install.bat.
# Adds a "Convert to (Satsuma)" entry to the right-click menu. Nothing needs admin rights.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$root = Join-Path $env:LOCALAPPDATA 'Satsuma'
$bin = Join-Path $root 'bin'
$tmp = Join-Path $env:TEMP ('satsuma_setup_' + [guid]::NewGuid().ToString('N'))
function Say([string]$text) { Write-Host $text }
function Good([string]$text) { Write-Host $text -ForegroundColor Green }
function Warn([string]$text) { Write-Host $text -ForegroundColor Yellow }
function Set-Reg([string]$subKey, [string]$name, [string]$value) {
$k = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($subKey)
$k.SetValue($name, $value)
$k.Close()
}
function Remove-Reg([string]$subKey) {
try { [Microsoft.Win32.Registry]::CurrentUser.DeleteSubKeyTree($subKey, $false) } catch { }
}
function Find-Magick {
$dir = Join-Path $bin 'magick'
if (Test-Path -LiteralPath $dir) {
$f = Get-ChildItem -LiteralPath $dir -Filter 'magick.exe' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($f) { return $f.FullName }
}
foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)})) {
if ($base -and (Test-Path -LiteralPath $base)) {
$d = Get-ChildItem -LiteralPath $base -Filter 'ImageMagick*' -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1
if ($d) {
$m = Join-Path $d.FullName 'magick.exe'
if (Test-Path -LiteralPath $m) { return $m }
}
}
}
return $null
}
Write-Host ''
Write-Host ' Satsuma setup' -ForegroundColor Yellow
Write-Host ' Right-click file converter for Windows'
Write-Host ''
try {
[void](New-Item -ItemType Directory -Force -Path $root)
[void](New-Item -ItemType Directory -Force -Path $bin)
[void](New-Item -ItemType Directory -Force -Path $tmp)
# ---- 1. copy the program files ----
Say 'Step 1 of 4: copying Satsuma files...'
foreach ($f in @('Convert.ps1', 'Run.vbs', 'satsuma.ico')) {
$from = Join-Path $here $f
if (-not (Test-Path -LiteralPath $from)) { throw ('A file is missing next to the installer: ' + $f + '. Unzip the whole folder first.') }
$to = Join-Path $root $f
Copy-Item -LiteralPath $from -Destination $to -Force
try { Unblock-File -LiteralPath $to } catch { }
}
Good ' done'
# ---- 2. ffmpeg (video, audio, webp) ----
$ffmpegExe = Join-Path $bin 'ffmpeg.exe'
if (Test-Path -LiteralPath $ffmpegExe) {
Say 'Step 2 of 4: ffmpeg is already here.'
} else {
Say 'Step 2 of 4: downloading ffmpeg (about 90 MB, this can take a minute)...'
$zip = Join-Path $tmp 'ffmpeg.zip'
$urls = @(
'https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip',
'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip'
)
$got = $false
foreach ($u in $urls) {
try {
Invoke-WebRequest -Uri $u -OutFile $zip -UseBasicParsing
$got = $true
break
} catch {
Warn (' that source failed, trying the next one: ' + $_.Exception.Message)
}
}
if (-not $got) { throw 'Could not download ffmpeg. Check the internet connection and run Install.bat again.' }
$x = Join-Path $tmp 'ffmpeg_x'
Expand-Archive -LiteralPath $zip -DestinationPath $x -Force
$found = Get-ChildItem -LiteralPath $x -Filter 'ffmpeg.exe' -Recurse | Select-Object -First 1
if (-not $found) { throw 'ffmpeg.exe was not inside the download.' }
Copy-Item -LiteralPath $found.FullName -Destination $ffmpegExe -Force
Good ' done'
}
try {
$v = (& $ffmpegExe -version 2>&1 | Select-Object -First 1) | Out-String
Say (' ' + $v.Trim())
} catch {
Warn ' ffmpeg was installed but did not start. Video and audio may not work.'
}
# ---- 3. ImageMagick (HEIC, WebP, PNG, JPG) ----
$magickPath = Find-Magick
if ($magickPath) {
Say 'Step 3 of 4: ImageMagick is already here.'
} else {
Say 'Step 3 of 4: downloading ImageMagick (for HEIC and other pictures)...'
$base = 'https://imagemagick.org/archive/binaries/'
$name = $null
try {
$html = (Invoke-WebRequest -Uri $base -UseBasicParsing).Content
$best = $null
$bestKey = $null
foreach ($m in [regex]::Matches($html, 'ImageMagick-(\d+)\.(\d+)\.(\d+)-(\d+)-portable-Q16-HDRI-x64\.7z')) {
$key = [version]('{0}.{1}.{2}.{3}' -f $m.Groups[1].Value, $m.Groups[2].Value, $m.Groups[3].Value, $m.Groups[4].Value)
if (($null -eq $bestKey) -or ($key -gt $bestKey)) { $bestKey = $key; $best = $m.Value }
}
$name = $best
} catch { }
if (-not $name) { $name = 'ImageMagick-7.1.2-25-portable-Q16-HDRI-x64.7z' }
$ok = $false
try {
$arc = Join-Path $tmp 'magick.7z'
$z = Join-Path $tmp '7zr.exe'
Invoke-WebRequest -Uri ($base + $name) -OutFile $arc -UseBasicParsing
Invoke-WebRequest -Uri 'https://www.7-zip.org/a/7zr.exe' -OutFile $z -UseBasicParsing
$dest = Join-Path $bin 'magick'
[void](New-Item -ItemType Directory -Force -Path $dest)
& $z x $arc ('-o' + $dest) -y | Out-Null
if ($LASTEXITCODE -ne 0) { throw 'The 7-Zip extractor reported an error.' }
$ok = $true
} catch {
Warn (' portable download did not work: ' + $_.Exception.Message)
}
if (-not $ok) {
Say ' trying winget instead (Windows may ask for permission)...'
try {
$wg = Get-Command winget.exe -ErrorAction SilentlyContinue
if ($wg) {
& $wg.Source install --id ImageMagick.ImageMagick -e --silent --accept-package-agreements --accept-source-agreements
}
} catch { }
}
$magickPath = Find-Magick
if ($magickPath) { Good ' done' }
else { Warn ' ImageMagick could not be installed. Pictures will not convert, but video, audio and documents still can.' }
}
if ($magickPath) {
try {
$fmt = & $magickPath -list format 2>&1 | Out-String
if ($fmt -match '(?m)^\s*HEIC\b') { Good ' HEIC pictures are supported.' }
else { Warn ' This ImageMagick build does not list HEIC support, so iPhone pictures may fail.' }
} catch {
Warn ' Could not check HEIC support.'
}
}
# ---- 4. the right-click menu ----
Say 'Step 4 of 4: adding the right-click menu...'
$iconPath = Join-Path $root 'satsuma.ico'
$runVbs = Join-Path $root 'Run.vbs'
$video = @(
@('mp4', 'MP4'), @('mkv', 'MKV'), @('webm', 'WebM'), @('avi', 'AVI'),
@('mov', 'MOV'), @('wmv', 'WMV'), @('flv', 'FLV'), @('mpg', 'MPG')
)
$audio = @(
@('mp3', 'MP3'), @('wav', 'WAV'), @('flac', 'FLAC'),
@('ogg', 'OGG'), @('m4a', 'M4A'), @('opus', 'Opus')
)
$image = @(
@('jpg', 'JPG'), @('png', 'PNG'), @('webp', 'WebP'),
@('bmp', 'BMP'), @('tiff', 'TIFF'), @('pdf', 'PDF')
)
$same = @{ 'm4v' = 'mp4'; 'mpeg' = 'mpg'; 'jpeg' = 'jpg'; 'tif' = 'tiff'; 'heif' = 'heic' }
$groups = @(
@{ Exts = @('mp4', 'm4v', 'mkv', 'webm', 'avi', 'mov', 'wmv', 'flv', 'mpg', 'mpeg', '3gp', 'ogv'); Items = ($video + @(, @('mp3', 'MP3 (sound only)'))) },
@{ Exts = @('mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'opus', 'wma'); Items = $audio },
@{ Exts = @('heic', 'heif', 'webp', 'png', 'jpg', 'jpeg', 'bmp', 'tif', 'tiff'); Items = $image },
@{ Exts = @('gif'); Items = ($image + @(, @('mp4', 'MP4 video'))) },
@{ Exts = @('doc', 'docx', 'rtf', 'txt', 'odt', 'xls', 'xlsx', 'ods', 'ppt', 'pptx', 'odp'); Items = @(, @('pdf', 'PDF')) }
)
$count = 0
foreach ($g in $groups) {
foreach ($ext in $g.Exts) {
$menuKey = 'Software\Classes\SystemFileAssociations\.' + $ext + '\shell\Satsuma'
Remove-Reg $menuKey
$mine = $ext
if ($same.ContainsKey($ext)) { $mine = $same[$ext] }
Set-Reg $menuKey 'MUIVerb' 'Convert to (Satsuma)'
Set-Reg $menuKey 'SubCommands' ''
Set-Reg $menuKey 'Icon' $iconPath
$n = 0
foreach ($item in $g.Items) {
$t = $item[0]
$label = $item[1]
if ($t -eq $mine) { continue }
$n++
$verbKey = '{0}\shell\{1:D2}_{2}' -f $menuKey, $n, $t
Set-Reg $verbKey '' $label
Set-Reg ($verbKey + '\command') '' ('wscript.exe "{0}" {1} "%1"' -f $runVbs, $t)
}
$count++
}
}
Good (' done (' + $count + ' file types)')
# ---- Windows 11 note ----
$build = 0
try { $build = [int](Get-CimInstance Win32_OperatingSystem).BuildNumber } catch { }
if ($build -ge 22000) {
$clsid = 'Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32'
$existing = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($clsid)
if ($null -eq $existing) {
Write-Host ''
Warn 'Windows 11 hides this menu under "Show more options" in the right-click menu.'
$a = Read-Host 'Turn on the classic full right-click menu so Satsuma shows right away? (Y/N)'
if ($a -match '^[Yy]') {
Set-Reg $clsid '' ''
Set-Content -LiteralPath (Join-Path $root 'classic-menu.txt') -Value 'set by Satsuma'
Good ' classic menu turned on. Sign out and back in (or restart) to see it.'
}
} else {
$existing.Close()
}
}
Write-Host ''
Good 'All set.'
Say 'Right-click a picture, video, sound or document file and look for "Convert to (Satsuma)".'
Say 'The converted file lands next to the original. The original is never touched.'
}
catch {
Write-Host ''
Write-Host ('Setup stopped: ' + $_.Exception.Message) -ForegroundColor Red
}
finally {
if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue }
}
+15
View File
@@ -0,0 +1,15 @@
' Satsuma launcher. Starts the converter with no visible window.
Option Explicit
Dim sh, fso, here, target, src, cmd
Set sh = CreateObject("WScript.Shell")
Set fso = CreateObject("Scripting.FileSystemObject")
If WScript.Arguments.Count < 2 Then WScript.Quit 1
here = fso.GetParentFolderName(WScript.ScriptFullName)
target = WScript.Arguments(0)
src = WScript.Arguments(1)
cmd = "powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File """ & here & "\Convert.ps1"" -Target """ & target & """ -Source """ & src & """"
sh.Run cmd, 0, False
+43
View File
@@ -0,0 +1,43 @@
# Satsuma uninstaller. Run it with Uninstall.bat.
$ErrorActionPreference = 'Continue'
$root = Join-Path $env:LOCALAPPDATA 'Satsuma'
$classes = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Software\Classes\SystemFileAssociations', $true)
$removed = 0
if ($null -ne $classes) {
foreach ($name in $classes.GetSubKeyNames()) {
$shell = $classes.OpenSubKey($name + '\shell', $true)
if ($null -ne $shell) {
if ($shell.GetSubKeyNames() -contains 'Satsuma') {
try { $shell.DeleteSubKeyTree('Satsuma', $false); $removed++ } catch { }
}
$shell.Close()
}
}
$classes.Close()
}
Write-Host ('Removed the right-click menu from ' + $removed + ' file types.')
$marker = Join-Path $root 'classic-menu.txt'
if (Test-Path -LiteralPath $marker) {
$a = Read-Host 'Satsuma turned on the classic Windows 11 right-click menu. Put the new Windows 11 menu back? (Y/N)'
if ($a -match '^[Yy]') {
try {
[Microsoft.Win32.Registry]::CurrentUser.DeleteSubKeyTree('Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}', $false)
Write-Host 'Done. Sign out and back in (or restart) to see the change.'
} catch { }
}
}
if (Test-Path -LiteralPath $root) {
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
if (Test-Path -LiteralPath $root) {
Write-Host 'Some files in the Satsuma folder could not be deleted (they may be in use). Delete this folder by hand:'
Write-Host $root
} else {
Write-Host 'Deleted the Satsuma program folder, including the downloaded tools.'
}
}
Write-Host ''
Write-Host 'Satsuma is removed.'