Public/Show-IntuneWinAppUtilGui.ps1

# Show-Gui.ps1
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.IO.Compression.FileSystem

# Returns a relative path from BasePath to TargetPath when possible; otherwise returns the absolute path.
function Get-RelativePath {
    param(
        [Parameter(Mandatory)] [string]$BasePath,
        [Parameter(Mandatory)] [string]$TargetPath
    )
    
    try {
        $baseFull = [System.IO.Path]::GetFullPath(($BasePath.TrimEnd('\') + '\'))
        $targetFull = [System.IO.Path]::GetFullPath($TargetPath)
        $uriBase   = [Uri]$baseFull
        $uriTarget = [Uri]$targetFull
        return $uriBase.MakeRelativeUri($uriTarget).ToString().Replace('/','\')
    } catch {
        return $TargetPath
    }
}

# If Invoke-AppDeployToolkit.exe exists under SourcePath, suggest it into the Setup textbox
# and optionally populate FinalFilename if AppName/AppVersion are found in Invoke-AppDeployToolkit.ps1
function Set-SetupFromSource {
    param([string]$SourcePath)

    if ([string]::IsNullOrWhiteSpace($SourcePath) -or -not (Test-Path $SourcePath)) { return }

    # If current SetupFile value already points to an existing file (absolute or relative to source), do not override.
    $current = $SetupFile.Text.Trim()
    if ($current) {
        if (Test-Path $current) { return }
        $maybeRelative = Join-Path $SourcePath $current
        if (Test-Path $maybeRelative) { return }
    }

    # Search for Invoke-AppDeployToolkit.exe
    $exeHit = Get-ChildItem -Path $SourcePath -Filter 'Invoke-AppDeployToolkit.exe' -File -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($exeHit) {
        # Prefer a relative path when the file is inside the source folder
        $relativeExe = Get-RelativePath -BasePath $SourcePath -TargetPath $exeHit.FullName
        $SetupFile.Text = $relativeExe

        # Look for Invoke-AppDeployToolkit.ps1 in the same folder
        $ps1Path = Join-Path $exeHit.Directory.FullName 'Invoke-AppDeployToolkit.ps1'
        if (Test-Path $ps1Path) {
            try {
                $content = Get-Content $ps1Path -Raw

                $appName = if ($content -match "AppName\s*=\s*'([^']+)'") { $matches[1] } else { $null }
                $appVersion = if ($content -match "AppVersion\s*=\s*'([^']+)'") { $matches[1] } else { $null }

                if ($appName -and $appVersion) {
                    # Clean filename: remove spaces and invalid chars
                    $cleanName = ($appName -replace '\s+', '' -replace '[\\/:*?"<>|]', '-')
                    $cleanVer = ($appVersion -replace '\s+', '' -replace '[\\/:*?"<>|]', '-')
                    $FinalFilename.Text = "${cleanName}_${cleanVer}"
                }
            } catch {
                # Fail silently if parsing goes wrong
            }
        }
    }
}

# Returns file version (FileVersion preferred, then ProductVersion); $null if not available.
function Get-ExeVersion {
    param([Parameter(Mandatory)][string]$Path)

    try {
        if (-not (Test-Path $Path)) { return $null }
        $vi = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($Path)
        if ($vi.FileVersion -and $vi.FileVersion.Trim()) { return $vi.FileVersion.Trim() }
        if ($vi.ProductVersion -and $vi.ProductVersion.Trim()) { return $vi.ProductVersion.Trim() }
        return $null
    } catch {
        return $null
    }
}

# Updates the ToolVersion TextBlock with current version or a default message.
function Show-ToolVersion {
    param([string]$Path)

    if (-not $ToolVersionText) { return }
    $ver = if ($Path) { Get-ExeVersion -Path $Path } else { $null }
    $ToolVersionText.Text = if ($ver) {
        "IntuneWinAppUtil version: $ver"
    } else {
        "IntuneWinAppUtil version: (not detected)"
    }
}

# Downloads the latest IntuneWinAppUtil.exe by fetching the master zip from GitHub,
# extracting it, locating the EXE, and copying it into %APPDATA%\IntuneWinAppUtilGUI\bin.
function Invoke-RedownloadIntuneTool {
    param()

    $appRoot   = Join-Path $env:APPDATA 'IntuneWinAppUtilGUI'
    $binDir    = Join-Path $appRoot 'bin'
    $exePath   = Join-Path $binDir 'IntuneWinAppUtil.exe'
    $zipUrl    = 'https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool/archive/refs/heads/master.zip'
    $zipPath   = Join-Path $env:TEMP 'IntuneWinAppUtil-master.zip'
    $extractTo = Join-Path $env:TEMP 'IntuneExtract'

    try {
        # Clean previous temp
        if (Test-Path $extractTo) { Remove-Item $extractTo -Recurse -Force }

        # Ensure bin dir exists (and clear old exe to avoid stale versions)
        New-Item -ItemType Directory -Path $binDir -Force | Out-Null
        if (Test-Path $exePath) { Remove-Item $exePath -Force -ErrorAction SilentlyContinue }

        # Download ZIP
        Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing

        # Extract ZIP
        [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $extractTo, $true)

        # Find EXE in extracted content
        $found = Get-ChildItem -Path $extractTo -Recurse -Filter 'IntuneWinAppUtil.exe' -File -ErrorAction SilentlyContinue | Select-Object -First 1
        if (-not $found) {
            throw "IntuneWinAppUtil.exe not found in extracted archive."
        }

        # Copy to bin
        Copy-Item -Path $found.FullName -Destination $exePath -Force

        # Cleanup temp
        if (Test-Path $zipPath)   { Remove-Item $zipPath -Force }
        if (Test-Path $extractTo) { Remove-Item $extractTo -Recurse -Force }

        return $exePath
    } catch {
        throw $_
    }
}

# Show the main GUI window and handle all events.
function Show-IntuneWinAppUtilGui {
    [CmdletBinding()]
    param ()

    $configPath = Join-Path -Path $env:APPDATA -ChildPath "IntuneWinAppUtilGUI\config.json"
    $xamlPath = Join-Path -Path $PSScriptRoot -ChildPath "..\UI\UI.xaml"
    $iconPath = Join-Path -Path $PSScriptRoot -ChildPath "..\Assets\Intune.ico"

    if (-not (Test-Path $xamlPath)) {
        Write-Error "XAML file not found: $xamlPath"
        return
    }

    $xaml = Get-Content $xamlPath -Raw
    $window = [Windows.Markup.XamlReader]::Parse($xaml)

    $SourceFolder = $window.FindName("SourceFolder")
    $SetupFile = $window.FindName("SetupFile")
    $OutputFolder = $window.FindName("OutputFolder")

    $ToolPathBox = $window.FindName("ToolPathBox")
    $ToolVersion = $window.FindName("ToolVersion")
    $ToolVersionText = $window.FindName("ToolVersionText")
    $ToolVersionLink = $window.FindName("ToolVersionLink")
    $RedownloadTool = $window.FindName("RedownloadTool")
    
    $FinalFilename = $window.FindName("FinalFilename")
    
    $BrowseSource = $window.FindName("BrowseSource")
    $BrowseSetup = $window.FindName("BrowseSetup")
    $BrowseOutput = $window.FindName("BrowseOutput")
    $BrowseTool = $window.FindName("BrowseTool")
    
    $RunButton = $window.FindName("RunButton")
    $ClearButton = $window.FindName("ClearButton")
    $ExitButton = $window.FindName("ExitButton")

    # When user types/pastes the source path manually, try to auto-suggest the setup file if found.
    $SourceFolder.Add_TextChanged({
        param($sender, $e)
        $src = $SourceFolder.Text.Trim()
        if ($src) { Set-SetupFromSource -SourcePath $src }
    })

    # Preload config.json if it exists
    if (Test-Path $configPath) {
        try {
            $cfg = Get-Content $configPath -Raw | ConvertFrom-Json
            if ($cfg.ToolPath -and (Test-Path $cfg.ToolPath)) {
                $ToolPathBox.Text = $cfg.ToolPath
                Show-ToolVersion -Path $cfg.ToolPath
            }
        } catch {}
    }

    # Browse for Source Folder
    $BrowseSource.Add_Click({
        $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
        if ($dialog.ShowDialog() -eq 'OK') {
            $SourceFolder.Text = $dialog.SelectedPath
            # Auto-suggest Invoke-AppDeployToolkit.exe when present in the selected source
            Set-SetupFromSource -SourcePath $dialog.SelectedPath
        }
    })

    # Browse for Setup File
    $BrowseSetup.Add_Click({
        $dialog = New-Object System.Windows.Forms.OpenFileDialog
        $dialog.Filter = "Executable or MSI (*.exe;*.msi)|*.exe;*.msi"
        if ($dialog.ShowDialog() -eq 'OK') {
            $selectedPath = $dialog.FileName
            $sourceRoot = $SourceFolder.Text.Trim()

            if (-not [string]::IsNullOrWhiteSpace($sourceRoot) -and (Test-Path $sourceRoot)) {
                try {
                    $relativePath = [System.IO.Path]::GetRelativePath($sourceRoot, $selectedPath)
                    if (-not ($relativePath.StartsWith(".."))) {
                        # File is inside source folder or subdir
                        $SetupFile.Text = $relativePath
                    } else {
                        # Outside of source folder
                        $SetupFile.Text = $selectedPath
                    }
                } catch {
                    # If relative path fails (e.g. bad format), fallback
                    $SetupFile.Text = $selectedPath
                }
            } else {
                # Source folder not set or invalid, fallback
                $SetupFile.Text = $selectedPath
            }
        }
    })

    # Browse for Output Folder
    $BrowseOutput.Add_Click({
        $dlg = New-Object System.Windows.Forms.FolderBrowserDialog
        if ($dlg.ShowDialog() -eq 'OK') { $OutputFolder.Text = $dlg.SelectedPath }
    })

    # Browse for IntuneWinAppUtil.exe
    $BrowseTool.Add_Click({
        $dlg = New-Object System.Windows.Forms.OpenFileDialog
        $dlg.Filter = "IntuneWinAppUtil.exe|IntuneWinAppUtil.exe"
        if ($dlg.ShowDialog() -eq 'OK') {
            $ToolPathBox.Text = $dlg.FileName
            Show-ToolVersion -Path $dlg.FileName
        }
    })

    # Force download the IntuneWinAppUtil.exe tool
    $RedownloadTool.Add_Click({
        $confirm = [System.Windows.MessageBox]::Show(
            "This will re-download the latest IntuneWinAppUtil.exe and replace the one in your bin folder.`n`nProceed?",
            "Confirm re-download",
            [System.Windows.MessageBoxButton]::YesNo,
            [System.Windows.MessageBoxImage]::Question
        )
        if ($confirm -ne [System.Windows.MessageBoxResult]::Yes) { return }

        try {
            $newPath = Invoke-RedownloadIntuneTool
            $ToolPathBox.Text = $newPath
            Show-ToolVersion -Path $newPath

            [System.Windows.MessageBox]::Show(
                "IntuneWinAppUtil.exe has been refreshed.`n`nPath:`n$newPath",
                "Download complete",
                [System.Windows.MessageBoxButton]::OK,
                [System.Windows.MessageBoxImage]::Information
            )
        } catch {
            [System.Windows.MessageBox]::Show(
                "Re-download failed:`n$($_.Exception.Message)",
                "Error",
                [System.Windows.MessageBoxButton]::OK,
                [System.Windows.MessageBoxImage]::Error
            )
        }
    })

    # When user types/pastes the tool path manually, show version if valid
    $ToolPathBox.Add_TextChanged({
        param($sender, $e)
        $p = $ToolPathBox.Text.Trim()
        if ($p) { Show-ToolVersion -Path $p } else { Show-ToolVersion -Path $null }
    })

    $RunButton.Add_Click({
        $c = $SourceFolder.Text.Trim()
        $s = $SetupFile.Text.Trim()
        $o = $OutputFolder.Text.Trim()
        $f = $FinalFilename.Text.Trim()

        $f = -join ($f.ToCharArray() | Where-Object { [System.IO.Path]::GetInvalidFileNameChars() -notcontains $_ })

        if (-not (Test-Path $c)) { [System.Windows.MessageBox]::Show("Invalid source folder path.", "Error", "OK", "Error"); return }
        if (-not (Test-Path $s)) {
            $s = Join-Path $c $s
            if (-not (Test-Path $s)) { [System.Windows.MessageBox]::Show("Setup file not found.", "Error", "OK", "Error"); return }
        }
        if (-not (Test-Path $o)) { [System.Windows.MessageBox]::Show("Invalid output folder path.", "Error", "OK", "Error"); return }

        # IntuneWinAppUtil.exe path check (or download if not set)
        $toolPath = $ToolPathBox.Text.Trim()
        $downloadDir = Join-Path $env:APPDATA "IntuneWinAppUtilGUI\bin"
        $exePath = Join-Path $downloadDir "IntuneWinAppUtil.exe"

        if ([string]::IsNullOrWhiteSpace($toolPath) -or -not (Test-Path $toolPath)) {
            if (-not (Test-Path $exePath)) {
                try {
                    $url = "https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool/archive/refs/heads/master.zip"
                    $zipPath = Join-Path $env:TEMP "IntuneWinAppUtil-master.zip"
                    $extractPath = Join-Path $env:TEMP "IntuneExtract"

                    if (Test-Path $extractPath) { Remove-Item $extractPath -Recurse -Force }

                    Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing
                    [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $extractPath, $true)
                    Remove-Item $zipPath -Force

                    # Find the executable in the extracted structure
                    $sourceExe = Get-ChildItem -Path $extractPath -Recurse -Filter "IntuneWinAppUtil.exe" | Select-Object -First 1

                    if (-not $sourceExe) {
                        throw "IntuneWinAppUtil.exe not found in extracted archive."
                    }

                    # Copy to destination
                    New-Item -ItemType Directory -Force -Path $downloadDir | Out-Null
                    Copy-Item -Path $sourceExe.FullName -Destination $exePath -Force

                    [System.Windows.MessageBox]::Show("Tool downloaded and extracted to:`n$exePath", "Download Complete", "OK", "Info")
                } catch {
                    [System.Windows.MessageBox]::Show("Failed to download or extract the archive:`n$($_.Exception.Message)", "Download Error", "OK", "Error")
                    return
                }
            }

            if (Test-Path $exePath) {
                $toolPath = $exePath
                $ToolPathBox.Text = $toolPath
                Show-ToolVersion -Path $toolPath  # equivalent -Path $exePath
            }
        }

        if (-not (Test-Path $toolPath)) {
            [System.Windows.MessageBox]::Show("IntuneWinAppUtil.exe not found at:`n$toolPath", "Error", "OK", "Error")
            return
        }

        $IWAUtilargs = "-c `"$c`" -s `"$s`" -o `"$o`""
        Start-Process -FilePath $toolPath -ArgumentList $IWAUtilargs -WorkingDirectory (Split-Path $toolPath) -WindowStyle Normal -Wait

        Start-Sleep -Seconds 1
        $defaultName = [System.IO.Path]::GetFileNameWithoutExtension($s) + ".intunewin"
        $defaultPath = Join-Path $o $defaultName

        if (Test-Path $defaultPath) {
            $newName = if ([string]::IsNullOrWhiteSpace($f)) {
            (Split-Path $c -Leaf) + ".intunewin"
            } else {
                $f + ".intunewin"
            }

            try {
                $baseName = [System.IO.Path]::GetFileNameWithoutExtension($newName)
                $ext = [System.IO.Path]::GetExtension($newName)
                $finalName = $newName
                $counter = 1
        
                while (Test-Path (Join-Path $o $finalName)) {
                    $finalName = "$baseName" + "_$counter" + "$ext"
                    $counter++
                }
        
                Rename-Item -Path $defaultPath -NewName $finalName -Force
                $fullPath = Join-Path $o $finalName
        
                $msg = "Package created and renamed to:`n$finalName"
                if ($finalName -ne $newName) {
                    $msg += "`n(Note: original name '$newName' already existed.)"
                }
                $msg += "`n`nOpen folder?"
        
                $resp = [System.Windows.MessageBox]::Show(
                    $msg,
                    "Success",
                    [System.Windows.MessageBoxButton]::YesNo,
                    [System.Windows.MessageBoxImage]::Information
                )
                if ($resp -eq "Yes") {
                    Start-Process explorer.exe "/select,`"$fullPath`""
                }
        
            } catch {
                [System.Windows.MessageBox]::Show("Renaming failed: $($_.Exception.Message)", "Warning", "OK", "Warning")
            }
            
        } else {
            [System.Windows.MessageBox]::Show("Output file not found.", "Warning", "OK", "Warning")
        }
    })

    # Clear button: reset all except ToolPath if loaded from config
    $ClearButton.Add_Click({
        $SourceFolder.Clear()
        $SetupFile.Clear()
        $OutputFolder.Clear()
        $FinalFilename.Clear()
    })

    # Exit button: close the window
    $ExitButton.Add_Click({
        $window.Close()
    })

    $window.Add_KeyDown({
        param($sender, $e)
        switch ($e.Key) {
            'Escape' {
                if ([System.Windows.MessageBox]::Show("Exit the tool?", "Confirm", "YesNo", "Question") -eq "Yes") {
                    $window.Close()
                }
            }
            'Return' {
                $RunButton.RaiseEvent((New-Object System.Windows.RoutedEventArgs ([System.Windows.Controls.Button]::ClickEvent)))
            }
        }
    })

    $window.Add_Closed({
        if (-not (Test-Path (Split-Path $configPath))) {
            New-Item -Path (Split-Path $configPath) -ItemType Directory -Force | Out-Null
        }
        $cfg = @{ ToolPath = $ToolPathBox.Text.Trim() }
        $cfg | ConvertTo-Json | Set-Content $configPath -Encoding UTF8
    })

    if (Test-Path $iconPath) {
        $window.Icon = [System.Windows.Media.Imaging.BitmapFrame]::Create((New-Object System.Uri $iconPath, [System.UriKind]::Absolute))
    }

    $window.AddHandler([ System.Windows.Documents.Hyperlink]::RequestNavigateEvent,
        [System.Windows.Navigation.RequestNavigateEventHandler] {
            param($sender, $e)
            Start-Process $e.Uri.AbsoluteUri
            $e.Handled = $true
        })
    
    $window.ShowDialog() | Out-Null
}

Export-ModuleMember -Function Show-IntuneWinAppUtilGui