Private/LayoutExport.ps1

function ConvertTo-SldStartPin {
    param([Parameter(Mandatory)]$App)

    $sourceType = if ($App.StartSourceType) { $App.StartSourceType } else { $App.SourceType }
    $value = if ($App.StartValue) { $App.StartValue } else { $App.Value }

    switch ($sourceType) {
        'DesktopAppLink' { [ordered]@{ desktopAppLink = $value } }
        'PackagedAppId' { [ordered]@{ packagedAppId = $value } }
        'DesktopApplicationId' { [ordered]@{ desktopAppId = $value } }
        default { throw "Unsupported Start pin type: $sourceType" }
    }
}

function New-SldImportedAppCandidate {
    param(
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][ValidateSet('DesktopAppLink', 'PackagedAppId', 'DesktopApplicationId')][string]$SourceType,
        [Parameter(Mandatory)][string]$Value,
        [Parameter(Mandatory)][ValidateSet('Start', 'Taskbar')][string]$Mode
    )

    $startSourceType = if ($Mode -eq 'Start') { $SourceType } else { $null }
    $startValue = if ($Mode -eq 'Start') { $Value } else { $null }
    $taskbarSourceType = if ($Mode -eq 'Taskbar') { $SourceType } else { $null }
    $taskbarValue = if ($Mode -eq 'Taskbar') { $Value } else { $null }

    [pscustomobject]@{
        Name              = $Name
        SourceType        = $SourceType
        Value             = $Value
        IconPath          = $null
        Category          = 'Imported'
        PinSummary        = "Imported $SourceType"
        StartSourceType   = $startSourceType
        StartValue        = $startValue
        TaskbarSourceType = $taskbarSourceType
        TaskbarValue      = $taskbarValue
        AlternatePins     = @()
    }
}

function Get-SldImportedPinName {
    param(
        [Parameter(Mandatory)][string]$SourceType,
        [Parameter(Mandatory)][string]$Value
    )

    if ($SourceType -eq 'DesktopAppLink') {
        $fileName = [System.IO.Path]::GetFileNameWithoutExtension($Value)
        if ($fileName) {
            return $fileName
        }
    }

    if ($Value -match '^([^_!]+)') {
        return $matches[1]
    }

    $Value
}

function Find-SldAppByPin {
    param(
        [Parameter(Mandatory)][object[]]$AvailableApp,
        [Parameter(Mandatory)][ValidateSet('Start', 'Taskbar')][string]$Mode,
        [Parameter(Mandatory)][string]$SourceType,
        [Parameter(Mandatory)][string]$Value
    )

    foreach ($app in $AvailableApp) {
        $modeSourceType = if ($Mode -eq 'Start') { $app.StartSourceType } else { $app.TaskbarSourceType }
        $modeValue = if ($Mode -eq 'Start') { $app.StartValue } else { $app.TaskbarValue }

        if ($modeSourceType -eq $SourceType -and $modeValue -eq $Value) {
            return $app
        }

        foreach ($pin in @($app.AlternatePins)) {
            if ($pin.SourceType -eq $SourceType -and $pin.Value -eq $Value) {
                return $app
            }
        }
    }

    $null
}

function Copy-SldAppWithImportedPin {
    param(
        [Parameter(Mandatory)]$App,
        [Parameter(Mandatory)][ValidateSet('Start', 'Taskbar')][string]$Mode,
        [Parameter(Mandatory)][string]$SourceType,
        [Parameter(Mandatory)][string]$Value
    )

    [pscustomobject]@{
        Name              = $App.Name
        SourceType        = $App.SourceType
        Value             = $App.Value
        IconPath          = $App.IconPath
        IconSource        = $App.IconSource
        Category          = $App.Category
        PinSummary        = "Imported $SourceType"
        StartSourceType   = if ($Mode -eq 'Start') { $SourceType } else { $App.StartSourceType }
        StartValue        = if ($Mode -eq 'Start') { $Value } else { $App.StartValue }
        TaskbarSourceType = if ($Mode -eq 'Taskbar') { $SourceType } else { $App.TaskbarSourceType }
        TaskbarValue      = if ($Mode -eq 'Taskbar') { $Value } else { $App.TaskbarValue }
        AlternatePins     = @($App.AlternatePins)
    }
}

function ConvertFrom-SldStartLayoutJson {
    param(
        [Parameter(Mandatory)][string]$Json,
        [object[]]$AvailableApp = @()
    )

    $layout = $Json | ConvertFrom-Json
    $selected = [System.Collections.ObjectModel.ObservableCollection[object]]::new()

    foreach ($pin in @($layout.pinnedList)) {
        $sourceType = $null
        $value = $null

        if ($pin.desktopAppLink) {
            $sourceType = 'DesktopAppLink'
            $value = [string]$pin.desktopAppLink
        }
        elseif ($pin.packagedAppId) {
            $sourceType = 'PackagedAppId'
            $value = [string]$pin.packagedAppId
        }
        elseif ($pin.desktopAppId) {
            $sourceType = 'DesktopApplicationId'
            $value = [string]$pin.desktopAppId
        }
        else {
            continue
        }

        $match = Find-SldAppByPin -AvailableApp $AvailableApp -Mode Start -SourceType $sourceType -Value $value
        if ($match) {
            [void]$selected.Add((Copy-SldAppWithImportedPin -App $match -Mode Start -SourceType $sourceType -Value $value))
        }
        else {
            $name = Get-SldImportedPinName -SourceType $sourceType -Value $value
            [void]$selected.Add((New-SldImportedAppCandidate -Name $name -SourceType $sourceType -Value $value -Mode Start))
        }
    }

    [pscustomobject]@{
        ApplyOnce = [bool]$layout.applyOnce
        Apps      = $selected
    }
}

function ConvertTo-SldStartLayoutJson {
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]]$App,
        [bool]$ApplyOnce = $false
    )

    $pins = foreach ($entry in $App) {
        ConvertTo-SldStartPin -App $entry
    }

    [ordered]@{
        applyOnce  = $ApplyOnce
        pinnedList = @($pins)
    } | ConvertTo-Json -Depth 10
}

function New-SldXmlAttribute {
    param(
        [Parameter(Mandatory)][xml]$Document,
        [Parameter(Mandatory)][System.Xml.XmlElement]$Element,
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][string]$Value
    )

    $attribute = $Document.CreateAttribute($Name)
    $attribute.Value = $Value
    [void]$Element.Attributes.Append($attribute)
}

function New-SldTaskbarPinElement {
    param(
        [Parameter(Mandatory)][xml]$Document,
        [Parameter(Mandatory)]$App
    )

    $taskbarNamespace = 'http://schemas.microsoft.com/Start/2014/TaskbarLayout'

    $sourceType = if ($App.TaskbarSourceType) { $App.TaskbarSourceType } else { $App.SourceType }
    $value = if ($App.TaskbarValue) { $App.TaskbarValue } else { $App.Value }

    switch ($sourceType) {
        'PackagedAppId' {
            $element = $Document.CreateElement('taskbar', 'UWA', $taskbarNamespace)
            New-SldXmlAttribute -Document $Document -Element $element -Name 'AppUserModelID' -Value $value
            $element
        }
        'DesktopApplicationId' {
            $element = $Document.CreateElement('taskbar', 'DesktopApp', $taskbarNamespace)
            New-SldXmlAttribute -Document $Document -Element $element -Name 'DesktopApplicationID' -Value $value
            $element
        }
        'DesktopAppLink' {
            $element = $Document.CreateElement('taskbar', 'DesktopApp', $taskbarNamespace)
            New-SldXmlAttribute -Document $Document -Element $element -Name 'DesktopApplicationLinkPath' -Value $value
            $element
        }
        default {
            throw "Unsupported taskbar pin type: $sourceType"
        }
    }
}

function ConvertTo-SldTaskbarLayoutXml {
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]]$App,
        [switch]$Replace,
        [string]$Region
    )

    $layoutNamespace = 'http://schemas.microsoft.com/Start/2014/LayoutModification'
    $defaultLayoutNamespace = 'http://schemas.microsoft.com/Start/2014/FullDefaultLayout'
    $startNamespace = 'http://schemas.microsoft.com/Start/2014/StartLayout'
    $taskbarNamespace = 'http://schemas.microsoft.com/Start/2014/TaskbarLayout'

    $doc = New-Object System.Xml.XmlDocument
    $declaration = $doc.CreateXmlDeclaration('1.0', 'utf-8', $null)
    [void]$doc.AppendChild($declaration)

    $root = $doc.CreateElement('LayoutModificationTemplate', $layoutNamespace)
    New-SldXmlAttribute -Document $doc -Element $root -Name 'xmlns:defaultlayout' -Value $defaultLayoutNamespace
    New-SldXmlAttribute -Document $doc -Element $root -Name 'xmlns:start' -Value $startNamespace
    New-SldXmlAttribute -Document $doc -Element $root -Name 'xmlns:taskbar' -Value $taskbarNamespace
    New-SldXmlAttribute -Document $doc -Element $root -Name 'Version' -Value '1'
    [void]$doc.AppendChild($root)

    $collection = $doc.CreateElement('CustomTaskbarLayoutCollection', $layoutNamespace)
    if ($Replace) {
        New-SldXmlAttribute -Document $doc -Element $collection -Name 'PinListPlacement' -Value 'Replace'
    }
    [void]$root.AppendChild($collection)

    $layout = $doc.CreateElement('defaultlayout', 'TaskbarLayout', $defaultLayoutNamespace)
    if ($Region) {
        New-SldXmlAttribute -Document $doc -Element $layout -Name 'Region' -Value $Region
    }
    [void]$collection.AppendChild($layout)

    $pinList = $doc.CreateElement('taskbar', 'TaskbarPinList', $taskbarNamespace)
    [void]$layout.AppendChild($pinList)

    foreach ($entry in $App) {
        [void]$pinList.AppendChild((New-SldTaskbarPinElement -Document $doc -App $entry))
    }

    $memoryStream = New-Object System.IO.MemoryStream
    $settings = New-Object System.Xml.XmlWriterSettings
    $settings.Indent = $true
    $settings.Encoding = [System.Text.UTF8Encoding]::new($false)
    $writer = [System.Xml.XmlWriter]::Create($memoryStream, $settings)
    $doc.Save($writer)
    $writer.Close()
    $xmlString = [System.Text.Encoding]::UTF8.GetString($memoryStream.ToArray())
    $memoryStream.Dispose()
    return $xmlString
}

function ConvertFrom-SldTaskbarLayoutXml {
    param(
        [Parameter(Mandatory)][string]$Xml,
        [object[]]$AvailableApp = @()
    )

    [xml]$document = $Xml
    $selected = [System.Collections.ObjectModel.ObservableCollection[object]]::new()

    $collection = $document.SelectNodes("//*[local-name()='CustomTaskbarLayoutCollection']") | Select-Object -First 1
    $replace = $false
    if ($collection -and $collection.PinListPlacement -eq 'Replace') {
        $replace = $true
    }

    foreach ($node in $document.SelectNodes("//*[local-name()='DesktopApp']")) {
        $sourceType = $null
        $value = $null

        if ($node.DesktopApplicationID) {
            $sourceType = 'DesktopApplicationId'
            $value = [string]$node.DesktopApplicationID
        }
        elseif ($node.DesktopApplicationLinkPath) {
            $sourceType = 'DesktopAppLink'
            $value = [string]$node.DesktopApplicationLinkPath
        }

        if (-not $sourceType) {
            continue
        }

        $match = Find-SldAppByPin -AvailableApp $AvailableApp -Mode Taskbar -SourceType $sourceType -Value $value
        if ($match) {
            [void]$selected.Add((Copy-SldAppWithImportedPin -App $match -Mode Taskbar -SourceType $sourceType -Value $value))
        }
        else {
            $name = Get-SldImportedPinName -SourceType $sourceType -Value $value
            [void]$selected.Add((New-SldImportedAppCandidate -Name $name -SourceType $sourceType -Value $value -Mode Taskbar))
        }
    }

    foreach ($node in $document.SelectNodes("//*[local-name()='UWA']")) {
        if (-not $node.AppUserModelID) {
            continue
        }

        $sourceType = 'PackagedAppId'
        $value = [string]$node.AppUserModelID
        $match = Find-SldAppByPin -AvailableApp $AvailableApp -Mode Taskbar -SourceType $sourceType -Value $value
        if ($match) {
            [void]$selected.Add((Copy-SldAppWithImportedPin -App $match -Mode Taskbar -SourceType $sourceType -Value $value))
        }
        else {
            $name = Get-SldImportedPinName -SourceType $sourceType -Value $value
            [void]$selected.Add((New-SldImportedAppCandidate -Name $name -SourceType $sourceType -Value $value -Mode Taskbar))
        }
    }

    [pscustomobject]@{
        Replace = $replace
        Apps    = $selected
    }
}