Public/Show-IntuneWinAppUtilGui.ps1

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

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

    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")
    $DownloadTool = $window.FindName("DownloadTool")
    
    $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 -SetupFileControl $SetupFile -FinalFilenameControl $FinalFilename }
    })

    # 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 -Target $ToolVersionText
            }
        } 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 -SetupFileControl $SetupFile -FinalFilenameControl $FinalFilename
        }
    })

    # 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 = Get-RelativePath -BasePath $sourceRoot -TargetPath $selectedPath
                    if (-not ($relativePath.StartsWith(".."))) {
                        $SetupFile.Text = $relativePath # File is inside source folder or subdir
                    } else {
                        $SetupFile.Text = $selectedPath # Outside of source folder
                    }
                } catch {
                    $SetupFile.Text = $selectedPath # If relative path fails (e.g. bad format), fallback
                }
            # } else {
            # $SetupFile.Text = $selectedPath # Source folder not set or invalid, fallback
            # }
            } else {
                $SourceFolder.Text = Split-Path $selectedPath -Parent # Source folder not set or invalid -> infer it from the selected setup path
                $SetupFile.Text = [System.IO.Path]::GetFileName($selectedPath) # Store only the file name in SetupFile so it is relative to SourceFolder
            }
        }
    })

    # 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 -Target $ToolVersionText
        }
    })

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

        try {
            $newPath = Invoke-DownloadIntuneTool
            $ToolPathBox.Text = $newPath
            Show-ToolVersion -Path $newPath -Target $ToolVersionText

            [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(
                "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 -Target $ToolVersionText } else { Show-ToolVersion -Path $null -Target $ToolVersionText }
    })

    # If the user typed/pasted an absolute setup path before setting SourceFolder,
    # infer SourceFolder from that path and convert SetupFile to a relative file name.
    $SetupFile.Add_LostFocus({
        $sText = $SetupFile.Text.Trim()
        # Only act if SourceFolder is empty and SetupFile looks like an absolute existing path
        if ([string]::IsNullOrWhiteSpace($SourceFolder.Text) -and
            -not [string]::IsNullOrWhiteSpace($sText) -and
            [System.IO.Path]::IsPathRooted($sText) -and
            (Test-Path $sText)) {

            $SourceFolder.Text = Split-Path $sText -Parent
            $SetupFile.Text = [System.IO.Path]::GetFileName($sText)
            # Note: SourceFolder.Text change will NOT override SetupFile because Set-SetupFromSource
            # early-returns if SetupFile already points to an existing file (absolute or relative).
        }
    })

    # Run button: validate inputs, run IntuneWinAppUtil.exe, rename output if needed
    $RunButton.Add_Click({
        $c = $SourceFolder.Text.Trim() # Source folder
        $s = $SetupFile.Text.Trim() # Setup file (relative or absolute)
        $o = $OutputFolder.Text.Trim() # Output folder
        $f = $FinalFilename.Text.Trim() # Final filename

        # Clean FinalFilename from invalid chars
        $f = -join ($f.ToCharArray() | Where-Object { [System.IO.Path]::GetInvalidFileNameChars() -notcontains $_ })

        # Validate source folder
        if (-not (Test-Path $c)) { [System.Windows.MessageBox]::Show("Invalid source folder path.", "Error", "OK", "Error"); return }
        
        # Validate setup file
        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 }
        }

        # Validate extension before running the tool
        $extSetup = [System.IO.Path]::GetExtension($s).ToLowerInvariant()
        if ($extSetup -notin @(".exe", ".msi")) {
            [System.Windows.MessageBox]::Show(
                "Setup file must be .exe or .msi (got '$extSetup').",
                "Invalid setup type", "OK", "Error"
            )
            return
        }

        # Validate output folder
        if (-not (Test-Path $o)) { 
            try {
                New-Item -Path $o -ItemType Directory -Force | Out-Null
            } catch {
                [System.Windows.MessageBox]::Show("Output folder path is invalid and could not be created.", "Error", "OK", "Error")
                return
            }
        }

        # Normalize all paths to absolute
        try {
            $c = [System.IO.Path]::GetFullPath($c)
            $s = [System.IO.Path]::GetFullPath($s)
            $o = [System.IO.Path]::GetFullPath($o)
        } catch {
            [System.Windows.MessageBox]::Show("Invalid path format: $($_.Exception.Message)", "Error", "OK", "Error")
            return
        }

        # IntuneWinAppUtil.exe path check (or initialize/download if not set)
        $toolPath = Initialize-IntuneWinAppUtil -UiToolPath ($ToolPathBox.Text.Trim())

        if (-not $toolPath -or -not (Test-Path $toolPath)) {
            [System.Windows.MessageBox]::Show(
                "IntuneWinAppUtil.exe not found and could not be initialized.",
                "Error", "OK", "Error"
            )
            return
        }

        # Keep UI in sync and show version
        $ToolPathBox.Text = $toolPath
        Show-ToolVersion -Path $toolPath -Target $ToolVersionText
        
        # Build a single, properly-quoted argument string
        # -c = source folder, -s = setup file (EXE/MSI), -o = output folder.
        $iwaArgs = ('-c "{0}" -s "{1}" -o "{2}"' -f $c, $s, $o)

        # Launch IntuneWinAppUtil.exe, wait, and capture exit code (WorkingDirectory is set to the tool's folder to avoid relative path issues)
        try {
            $proc = Start-Process -FilePath $toolPath `
                -ArgumentList $iwaArgs `
                -WorkingDirectory (Split-Path $toolPath) `
                -WindowStyle Normal `
                -PassThru
        } catch {
            [System.Windows.MessageBox]::Show(
                "Failed to start IntuneWinAppUtil.exe:`n$($_.Exception.Message)",
                "Execution error", "OK", "Error"
            )
            return
        }
        $proc.WaitForExit()

        # Fail early if tool returned non-zero
        if ($proc.ExitCode -ne 0) {
            [System.Windows.MessageBox]::Show(
                "IntuneWinAppUtil exited with code $($proc.ExitCode).",
                "Packaging failed", "OK", "Error"
            )
            return
        }

        # Wait a bit for the output file to appear (up to 10 seconds, checking every 250ms)
        # Compute the default output filename that IntuneWinAppUtil generates. By default it matches the setup's base name + ".intunewin".
        $defaultName = [System.IO.Path]::GetFileNameWithoutExtension($s) + ".intunewin"
        $defaultPath = Join-Path $o $defaultName

        $timeoutSec = 10
        $elapsed = 0
        while (-not (Test-Path $defaultPath) -and $elapsed -lt $timeoutSec) {
            Start-Sleep -Milliseconds 250
            $elapsed += 0.25
        }

        if (Test-Path $defaultPath) {
            # Build desired name from $f (if any), ensuring exactly one ".intunewin":
            # - If FinalFilename ($f) is blank, fallback to using the source folder name.
            # - Otherwise use the provided FinalFilename.
            if ([string]::IsNullOrWhiteSpace($f)) {
                $desiredName = (Split-Path $c -Leaf) + ".intunewin"
            } else {
                $extF = [System.IO.Path]::GetExtension($f).ToLowerInvariant()
                $baseF = if ($extF -eq ".intunewin") { [System.IO.Path]::GetFileNameWithoutExtension($f) } else { $f }
                $desiredName = $baseF + ".intunewin"
            }

            $newName = $desiredName

            try {
                # Prepare collision-safe rename:
                # If a file with the desired name already exists, append _1, _2, ... until unique.
                $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++
                }

                # Perform the rename operation from the tool's default output to our final target name.
                Rename-Item -Path $defaultPath -NewName $finalName -Force
                $fullPath = Join-Path $o $finalName

                # Inform the user and optionally offer to open File Explorer with the file selected.
                $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`"" # Open Explorer with the new file pre-selected.
                }

            } catch {
                [System.Windows.MessageBox]::Show("Renaming failed: $($_.Exception.Message)", "Warning", "OK", "Warning") # If anything goes wrong during the rename, show a warning.
            }

        } else {
            [System.Windows.MessageBox]::Show(
                "Output file not found:`n$defaultPath",
                "Warning", "OK", "Warning"
            ) # The expected output was not found; warn the user (the tool may have failed).
        }
    })

    # 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()
    })

    # Keyboard shortcuts: Esc to exit (with confirmation), Enter to run packaging
    $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)))
            }
        }
    })

    # When the window is closed, save the ToolPath to config.json
    $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
    })

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

    # Find the Image control in XAML and load the PNG from disk and assign it to the Image.Source
    $HeaderIcon = $window.FindName('HeaderIcon')
    if ($HeaderIcon -and (Test-Path $iconPngPath)) {
        # Use BitmapImage with OnLoad so the file is not locked after loading
        $bmp = New-Object System.Windows.Media.Imaging.BitmapImage
        $bmp.BeginInit()
        $bmp.CacheOption = [System.Windows.Media.Imaging.BitmapCacheOption]::OnLoad
        $bmp.UriSource = [Uri]::new($iconPngPath, [UriKind]::Absolute)
        $bmp.EndInit()

        $HeaderIcon.Source = $bmp
    }

    # Hyperlink in the ToolVersionText to open the GitHub version history page (and other links if needed)
    $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