Skip to content

Commit a44d6dc

Browse files
feat: Added UWP Discovery, Created Custom App from existing one, and Launch it from Context Menu (#373)
* UWP Discover System - Ability to fetch UWP programs and their logo. * Fixed - able to obtain apps as LOCAL_SYSTEM Before, it was only able to get "global" apps, but now it is able to get from all User * Fixed - Able to launch from the shortcuts now. It was unreliable to search for Executable file. Therefore I decided to utilize `explorer shell:AppsFolder`. This also required me to change how launching work in `winboat.ts`. by default `/app:program` cannot run `explorer.exe ...` method due to the argument are not allowed. instead we needed to pass to `,cmd:` * Made the launching application more robust. Before, I simply split the command by " ". Now we will have dedicated arg field. Figured it'll be useful in the future like giving custom launching arg to an application * Context Menu * editing, saving custom app * clean up * Update Apps.vue * Update Apps.vue * More Filter Option
1 parent c84a277 commit a44d6dc

5 files changed

Lines changed: 394 additions & 140 deletions

File tree

guest_server/scripts/apps.ps1

Lines changed: 130 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -131,83 +131,112 @@ function Get-ApplicationName {
131131
return $appName
132132
}
133133

134+
function Get-PrettifyName {
135+
param (
136+
[string]$Name
137+
)
138+
139+
if ($Name -match '\.([^\.]+)$') {
140+
$ProductName = $Matches[1]
141+
} else {
142+
# If no period is found, use the whole name.
143+
$ProductName = $Name
144+
}
145+
146+
$PrettyName = ($ProductName -creplace '([A-Z\W_]|\d+)(?<![a-z])',' $&').trim()
147+
148+
return $PrettyName
149+
}
150+
134151
# Gets the display name for a UWP app.
135152
function Get-UWPApplicationName {
136153
param (
137-
[string]$exePath, # The resolved executable path
138154
$app # The AppxPackage object
139155
)
140-
141156
# UWP properties are usually the best source
142-
if ($app.DisplayName) { return $app.DisplayName.Trim() }
143-
if ($app.Name) { return $app.Name.Trim() } # Often the package name, less ideal but a fallback
144-
145-
# If UWP properties fail, try standard name extraction on the EXE
146-
if (Test-Path $exePath -PathType Leaf) {
147-
return Get-ApplicationName -targetPath $exePath
157+
if ($app.DisplayName) {
158+
return $app.DisplayName.Trim()
159+
#Write-Host($app)
148160
}
161+
if ($app.Name) {
162+
return Get-PrettifyName -Name $app.Name
163+
} # Often the package name, less ideal but a fallback
149164

150165
return $null # Failed to get a name
151166
}
152167

153-
# Parses the AppxManifest.xml to find the primary executable path for a UWP app.
154-
function Get-UWPExecutablePath {
168+
function Get-ParseUWP {
155169
param ([string]$instLoc) # InstallLocation from Get-AppxPackage
156170

157171
$manifestPath = Join-Path -Path $instLoc -ChildPath "AppxManifest.xml"
158172
if (-not (Test-Path $manifestPath -PathType Leaf)) {
159173
return $null # Manifest doesn't exist or isn't a file
160174
}
161175

162-
try {
163-
# Read manifest content, default encoding often works
164-
$xmlContent = Get-Content $manifestPath -Raw -Encoding Default -ErrorAction Stop
165-
166-
# Remove known namespace prefixes and xmlns attributes to simplify XML parsing
167-
$prefixesToRemove = 'uap10', 'uap', 'desktop', 'rescap', 'com' # Common prefixes
168-
$cleanedXml = $xmlContent
169-
foreach ($prefix in $prefixesToRemove) {
170-
# Regex to remove 'prefix:' from start/end tags and self-closing tags
171-
$cleanedXml = $cleanedXml -replace "(</?)$prefix`:", '$1' `
172-
-replace "<$prefix`:([^>\s]+?)\s*/>", "<$1 />"
173-
}
174-
# Remove the xmlns declarations themselves
175-
$cleanedXml = $cleanedXml -replace 'xmlns(:\w+)?="[^"]+"', ''
176+
$xmlContent = Get-Content $manifestPath -Raw -Encoding Default -ErrorAction Stop
177+
178+
$appId = "App" # Default, as it works for most packages.
179+
if ($xmlContent -match '<Application[^>]*Id\s*=\s*"([^"]+)"') {
180+
$appId = $Matches[1]
181+
}
176182

177-
# Attempt to parse the cleaned XML
178-
[xml]$manifest = $cleanedXml
183+
$logoMatch = [regex]::Match($xmlContent, '<Properties.*?>.*?<Logo>(.*?)</Logo>.*?</Properties>', [System.Text.RegularExpressions.RegexOptions]::Singleline)
184+
if ($logoMatch.Success) {
185+
$logo = $logoMatch.Groups[1].Value
186+
}
187+
188+
return $logo, $appId
189+
}
179190

180-
# Find the first <Application> node
181-
$appNode = $manifest.Package.Applications.Application | Select-Object -First 1
182-
if (-not $appNode) { return $null } # No application defined
191+
function Get-UWPBase64Logo {
192+
param([string]$logo, [string]$instLoc)
183193

184-
# Get the 'Executable' attribute value
185-
$exeRelPath = $appNode.Executable
186-
if (-not $exeRelPath) { return $null } # No executable attribute
194+
$logoPath = Join-Path -Path $instLoc -ChildPath $logo
187195

188-
# Handle special $targetnametoken$.exe case by finding the first EXE in the root
189-
if ($exeRelPath -like '*$targetnametoken$.exe*') {
190-
$candidateExe = Get-ChildItem -Path $instLoc -Filter *.exe -File -ErrorAction SilentlyContinue | Select-Object -First 1
191-
if ($candidateExe -and (Test-Path $candidateExe.FullName -PathType Leaf)) {
192-
return $candidateExe.FullName
193-
}
194-
} else {
195-
# Handle regular relative path
196-
$fullPath = Join-Path -Path $instLoc -ChildPath $exeRelPath
197-
if (Test-Path $fullPath -PathType Leaf) {
198-
return $fullPath
196+
if (-not (Test-Path $logoPath)) {
197+
# if base file not exist, attempt to find scaled version.
198+
$scaledVersions = @("scale-100", "scale-200", "scale-400")
199+
foreach ($scale in $scaledVersions) {
200+
$scaledLogoPath = $logoPath -replace '\.png$', ".$scale.png"
201+
if (Test-Path $scaledLogoPath) {
202+
$logoPath = $scaledLogoPath
203+
break
199204
}
200205
}
206+
207+
# null if no valid file found.
208+
if (-not (Test-Path $logoPath)) {
209+
return $null
210+
}
211+
}
212+
213+
try {
214+
$image = [System.Drawing.Image]::FromFile($logoPath)
215+
216+
# resize to 32x32
217+
$resizedBmp = New-Object System.Drawing.Bitmap(32, 32)
218+
$graphics = [System.Drawing.Graphics]::FromImage($resizedBmp)
219+
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
220+
$graphics.DrawImage($image, 0, 0, 32, 32)
221+
222+
# Save as PNG to memory stream
223+
$stream = New-Object System.IO.MemoryStream
224+
$resizedBmp.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
225+
226+
$base64 = [Convert]::ToBase64String($stream.ToArray())
227+
228+
# Clean up
229+
$stream.Dispose()
230+
$graphics.Dispose()
231+
$resizedBmp.Dispose()
232+
$image.Dispose()
233+
234+
return $base64
201235
} catch {
202-
# Ignore any parsing errors and return null
203236
return $null
204237
}
205-
206-
# Default return if no valid path found
207-
return $null
208238
}
209239

210-
211240
# --- Main Application Logic ---
212241

213242
# Use efficient List and HashSet for collection and deduplication
@@ -220,14 +249,45 @@ function Add-AppToListIfValid {
220249
param(
221250
[string]$Name,
222251
[string]$InputPath, # The path discovered (can be relative or contain variables)
223-
[string]$Source # Source type (e.g., 'system', 'winreg', 'startmenu')
252+
[string]$Source, # Source type (e.g., 'system', 'winreg', 'startmenu')
253+
254+
[string]$logoBase64, # Optional base64 logo, mainly for UWP logo discover system
255+
[string]$launchArg # optional arg
224256
)
225257

226258
$resolved = $null
227259
$fullPath = $null
228260
$normalizedPathKey = $null
229261

230-
# 1. Resolve and Validate Path
262+
# Step 1, handle UWP app.
263+
if ($Source -eq "uwp") {
264+
if (-not $launchArg) {
265+
return # launching UWP app requires Arg
266+
}
267+
268+
$normalizedPathKey = $InputPath.ToLowerInvariant() + $launchArg.ToLowerInvariant()
269+
if ($addedPaths.Contains($normalizedPathKey)) {
270+
return # Already added this app
271+
}
272+
273+
$icon = $null
274+
if ($logoBase64) {
275+
$icon = $logoBase64
276+
}
277+
278+
# UWP object
279+
$apps.Add([PSCustomObject]@{
280+
Name = $Name
281+
Path = $InputPath
282+
Args = $launchArg
283+
Icon = $icon
284+
Source = 'uwp'
285+
})
286+
$addedPaths.Add($normalizedPathKey) | Out-Null
287+
return
288+
}
289+
290+
# 2. Resolve and Validate Path Other
231291
try {
232292
$resolved = Resolve-Path -Path $InputPath -ErrorAction SilentlyContinue
233293
} catch { return } # Ignore if path resolution throws error
@@ -241,28 +301,29 @@ function Add-AppToListIfValid {
241301
return # Skip if path doesn't resolve to a valid file
242302
}
243303

244-
# 2. Validate Name (Basic Check)
304+
# 3. Validate Name (Basic Check)
245305
if (-not $Name -or $Name.Trim().Length -eq 0 -or $Name -like 'Microsoft? Windows? Operating System*') {
246306
return # Skip if name is empty, invalid, or generic OS name
247307
}
248308

249-
# 3. Check for Duplicates using normalized path
309+
# 4. Check for Duplicates using normalized path
250310
if ($addedPaths.Contains($normalizedPathKey)) {
251311
return # Skip if this exact executable path has already been added
252312
}
253313

254-
# 4. Get Icon
314+
# 5. Get Icon
255315
$icon = Get-ApplicationIcon -targetPath $fullPath
256316

257-
# 5. Add the Application Object (matching WinApp type)
317+
# 6. Add the Application Object (matching WinApp type)
258318
$apps.Add([PSCustomObject]@{
259319
Name = $Name
260320
Path = $fullPath # Use the resolved, non-normalized path for output
321+
Args = ""
261322
Icon = $icon
262323
Source = $Source
263324
})
264325

265-
# 6. Mark Path as Added
326+
# 7. Mark Path as Added
266327
$addedPaths.Add($normalizedPathKey) | Out-Null
267328
}
268329

@@ -452,9 +513,9 @@ if ($lnkFiles) {
452513

453514

454515
# 4. UWP Apps
455-
if (Get-Command Get-AppxPackage -ErrorAction SilentlyContinue) {
516+
if (Get-Command Get-AppxPackage -AllUsers -ErrorAction SilentlyContinue) {
456517
try {
457-
Get-AppxPackage -ErrorAction SilentlyContinue |
518+
Get-AppxPackage -AllUsers -ErrorAction SilentlyContinue |
458519
Where-Object {
459520
$_.IsFramework -eq $false -and
460521
$_.IsResourcePackage -eq $false -and
@@ -463,14 +524,18 @@ if (Get-Command Get-AppxPackage -ErrorAction SilentlyContinue) {
463524
} |
464525
ForEach-Object {
465526
$app = $_
466-
# Attempt to find the executable path using the manifest
467-
$exePath = Get-UWPExecutablePath -instLoc $app.InstallLocation
527+
$pathValue = "explorer.exe"
528+
529+
# Attempt to find logo and executable using Appxmanifest.xml
530+
$logo, $appId = Get-ParseUWP -instLoc $app.InstallLocation
531+
$launchArgs = "shell:AppsFolder\" + $app.PackageFamilyName + '!' + $appId
468532

469-
if ($exePath) { # Function already validates path is a file
533+
if ($appId) { # check if we have appId
470534
# Get the best display name (UWP properties preferred)
471535
$name = Get-UWPApplicationName -exePath $exePath -app $app
472536
if ($name) {
473-
Add-AppToListIfValid -Name $name -InputPath $exePath -Source "uwp"
537+
$base64 = Get-UWPBase64Logo -logo $logo -instLoc $app.InstallLocation
538+
Add-AppToListIfValid -Name $name -InputPath $pathValue -Source "uwp" -logoBase64 $base64 -launchArg $launchArgs
474539
}
475540
}
476541
}
@@ -567,4 +632,7 @@ if ($scoopDir) {
567632

568633
# Convert the final list of application objects to a compressed JSON string.
569634
# This is the only output sent to the standard output stream.
570-
$apps | ConvertTo-Json -Depth 5 -Compress
635+
636+
$apps | ConvertTo-Json -Depth 5 -Compress
637+
638+

src/renderer/components/WBContextMenu.vue

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
-->
66
<template>
77
<!-- Invisible trigger area that covers the parent element -->
8-
<div v-if="hasTrigger" ref="triggerRef" class="wb-contextmenu-trigger" @contextmenu.prevent="showMenu"></div>
8+
<!-- div v-if="hasTrigger" ref="triggerRef" class="wb-contextmenu-trigger" @contextmenu.prevent="showMenu"></div> -->
99

1010
<!-- Context menu popup -->
1111
<teleport to="body">
@@ -65,13 +65,15 @@ const showMenu = (event: MouseEvent) => {
6565
emit("show");
6666
6767
// Close on next tick to allow menu to render
68+
adjustPosition();
69+
document.addEventListener("click", hideMenu);
70+
document.addEventListener("contextmenu", hideMenu);
71+
window.addEventListener("scroll", hideMenu);
72+
window.addEventListener("resize", hideMenu);
73+
6874
nextTick(() => {
6975
adjustPosition();
70-
document.addEventListener("click", hideMenu);
71-
document.addEventListener("contextmenu", hideMenu);
72-
window.addEventListener("scroll", hideMenu);
73-
window.addEventListener("resize", hideMenu);
74-
});
76+
})
7577
};
7678
7779
const handleMenuClick = (event: MouseEvent) => {

0 commit comments

Comments
 (0)