11<#
22. SYNOPSIS
3- MonitorControl Pro v3.16 .0 - Advanced Display & GPU Settings Utility
3+ MonitorControl Pro v3.17 .0 - Advanced Display & GPU Settings Utility
44. DESCRIPTION
55 Comprehensive GUI for monitor DDC/CI control with VCP explorer, input switching,
66 color temperature presets, sync across monitors, and time-based automation.
77. NOTES
8- Version: 3.16 .0 - Enhanced with profile schema migration
8+ Version: 3.17 .0 - Enhanced with profile bundle export/import
99#>
1010
1111param ([switch ]$StartMinimized , [string ]$LoadProfile )
1212
13- Add-Type - AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Windows.Forms, System.Drawing
13+ Add-Type - AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Windows.Forms, System.Drawing, System.IO.Compression.FileSystem
1414
1515$nativeCode = @"
1616using System;
@@ -420,6 +420,8 @@ $script:ProfileScheduleRulesPath = Join-Path $script:ProfilesPath "profile-sched
420420$script :IdleDimSettingsPath = Join-Path $script :ProfilesPath " idle-dim.json"
421421$script :BatteryProfileSettingsPath = Join-Path $script :ProfilesPath " battery-profile.json"
422422$script :ProfileSchemaVersion = 2
423+ $script :ProfileBundleSchemaVersion = 1
424+ $script :ProfileExportsPath = Join-Path $script :ProfilesPath " exports"
423425$script :ProfileMetadataFiles = @ (" app-profile-rules.json" , " profile-schedules.json" , " idle-dim.json" , " battery-profile.json" )
424426$script :UpdatingUI = $false
425427$script :ApplyToAll = $false
@@ -788,7 +790,7 @@ if (-not (Test-Path $script:ProfilesPath)) { New-Item -ItemType Directory -Path
788790
789791[xml ]$xaml = @"
790792<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
791- Title="MonitorControl Pro v3.16 .0" Width="640" Height="680" MinWidth="560" MinHeight="560"
793+ Title="MonitorControl Pro v3.17 .0" Width="640" Height="680" MinWidth="560" MinHeight="560"
792794 Background="#0a0a0a" WindowStartupLocation="CenterScreen" ResizeMode="CanResizeWithGrip">
793795<Window.Resources>
794796 <ControlTemplate x:Key="ComboBoxToggleButton" TargetType="ToggleButton">
@@ -922,7 +924,7 @@ if (-not (Test-Path $script:ProfilesPath)) { New-Item -ItemType Directory -Path
922924 <Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
923925 <StackPanel VerticalAlignment="Center">
924926 <TextBlock Text="MonitorControl Pro" FontSize="16" FontWeight="SemiBold" Foreground="#fff" FontFamily="Segoe UI"/>
925- <TextBlock Text="v3.16 .0 - Click monitor to select" FontSize="9" Foreground="#505050" Margin="0,1,0,0"/>
927+ <TextBlock Text="v3.17 .0 - Click monitor to select" FontSize="9" Foreground="#505050" Margin="0,1,0,0"/>
926928 </StackPanel>
927929 <StackPanel Grid.Column="2" Orientation="Horizontal">
928930 <CheckBox x:Name="ApplyAllCheckbox" Content="All Monitors" VerticalAlignment="Center" Margin="0,0,10,0"/>
@@ -1115,7 +1117,7 @@ if (-not (Test-Path $script:ProfilesPath)) { New-Item -ItemType Directory -Path
11151117 </TabItem>
11161118 <TabItem Header="Profiles">
11171119 <Border Background="#151515" CornerRadius="0,5,5,5" Padding="10"><Grid>
1118- <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="8"/><RowDefinition Height="*"/><RowDefinition Height="8"/><RowDefinition Height="Auto"/><RowDefinition Height="8"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
1120+ <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="8"/><RowDefinition Height="*"/><RowDefinition Height="8"/><RowDefinition Height="Auto"/><RowDefinition Height="6"/><RowDefinition Height="Auto"/><RowDefinition Height=" 8"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
11191121 <Border Background="#1a1a1a" CornerRadius="5" Padding="10"><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="6"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
11201122 <TextBox x:Name="ProfileNameBox" Text="My Profile"/>
11211123 <Button x:Name="SaveProfileBtn" Grid.Column="2" Content="Save" Style="{StaticResource GreenBtn}" Padding="10,4"/>
@@ -1128,7 +1130,11 @@ if (-not (Test-Path $script:ProfilesPath)) { New-Item -ItemType Directory -Path
11281130 <Button x:Name="LoadProfileBtn" Content="Load" Style="{StaticResource AccBtn}"/>
11291131 <Button x:Name="DeleteProfileBtn" Grid.Column="2" Content="Delete" Style="{StaticResource WarnBtn}"/>
11301132 </Grid>
1131- <Border Grid.Row="6" Background="#1a1a1a" CornerRadius="5" Padding="10"><Grid>
1133+ <Grid Grid.Row="6"><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="6"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
1134+ <Button x:Name="ExportProfilesBtn" Content="Export Bundle" Style="{StaticResource Btn}"/>
1135+ <Button x:Name="ImportProfilesBtn" Grid.Column="2" Content="Import Bundle" Style="{StaticResource Btn}"/>
1136+ </Grid>
1137+ <Border Grid.Row="8" Background="#1a1a1a" CornerRadius="5" Padding="10"><Grid>
11321138 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="6"/><RowDefinition Height="Auto"/><RowDefinition Height="6"/><RowDefinition Height="Auto"/><RowDefinition Height="6"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
11331139 <Grid><CheckBox x:Name="AppProfileEnabledCheckbox" Content="Per-application profiles" VerticalAlignment="Center"/>
11341140 <TextBlock x:Name="AppProfileStatusText" Text="Off" FontSize="9" Foreground="#707070" HorizontalAlignment="Right" VerticalAlignment="Center"/></Grid>
@@ -1267,6 +1273,7 @@ $vcpCodeBox = $window.FindName("VCPCodeBox"); $vcpPresetCombo = $window.FindName
12671273$vcpResultBox = $window.FindName (" VCPResultBox" ); $vcpSetValueBox = $window.FindName (" VCPSetValueBox" ); $vcpSetBtn = $window.FindName (" VCPSetBtn" ); $vcpScanBtn = $window.FindName (" VCPScanBtn" )
12681274$profileNameBox = $window.FindName (" ProfileNameBox" ); $profilesList = $window.FindName (" ProfilesList" )
12691275$saveProfileBtn = $window.FindName (" SaveProfileBtn" ); $loadProfileBtn = $window.FindName (" LoadProfileBtn" ); $deleteProfileBtn = $window.FindName (" DeleteProfileBtn" )
1276+ $exportProfilesBtn = $window.FindName (" ExportProfilesBtn" ); $importProfilesBtn = $window.FindName (" ImportProfilesBtn" )
12701277$appProfileEnabledCheckbox = $window.FindName (" AppProfileEnabledCheckbox" ); $appProfileStatusText = $window.FindName (" AppProfileStatusText" )
12711278$appProfileExeBox = $window.FindName (" AppProfileExeBox" ); $appProfileCaptureBtn = $window.FindName (" AppProfileCaptureBtn" )
12721279$appProfileProfileCombo = $window.FindName (" AppProfileProfileCombo" ); $appProfileAddBtn = $window.FindName (" AppProfileAddBtn" )
@@ -1363,13 +1370,16 @@ function Load-MonitorSettings {
13631370}
13641371
13651372function Refresh-Monitors { Get-Monitors ; if ($script :CurrentMonitorIndex -ge $script :PhysicalMonitors.Count ) { $script :CurrentMonitorIndex = 0 }; Draw- MonitorLayout; Load- MonitorSettings; Update-ProfilesList }
1373+ function Get-UserProfileFiles {
1374+ if (-not (Test-Path $script :ProfilesPath )) { return @ () }
1375+ return @ (Get-ChildItem - Path $script :ProfilesPath - Filter " *.json" - File |
1376+ Where-Object { $script :ProfileMetadataFiles -notcontains $_.Name } |
1377+ Sort-Object - Property BaseName)
1378+ }
1379+
13661380function Update-ProfilesList {
13671381 $profilesList.Items.Clear ()
1368- if (Test-Path $script :ProfilesPath ) {
1369- Get-ChildItem - Path $script :ProfilesPath - Filter " *.json" |
1370- Where-Object { $script :ProfileMetadataFiles -notcontains $_.Name } |
1371- ForEach-Object { $profilesList.Items.Add ($_.BaseName ) | Out-Null }
1372- }
1382+ Get-UserProfileFiles | ForEach-Object { $profilesList.Items.Add ($_.BaseName ) | Out-Null }
13731383 Update-AppProfileProfileCombo
13741384}
13751385
@@ -1435,6 +1445,110 @@ function Read-ProfileObject {
14351445 return $converted
14361446}
14371447
1448+ function Export-ProfileBundle {
1449+ param ([string ]$OutputPath )
1450+ $profileFiles = @ (Get-UserProfileFiles )
1451+ if ($profileFiles.Count -eq 0 ) {
1452+ Update-Status " No profiles to export"
1453+ return $null
1454+ }
1455+
1456+ if ([string ]::IsNullOrWhiteSpace($OutputPath )) {
1457+ if (-not (Test-Path $script :ProfileExportsPath )) { New-Item - ItemType Directory - Path $script :ProfileExportsPath - Force | Out-Null }
1458+ $timestamp = Get-Date - Format " yyyyMMdd-HHmmss"
1459+ $OutputPath = Join-Path $script :ProfileExportsPath " monitorcontrol-profiles-$timestamp .zip"
1460+ } else {
1461+ $parent = Split-Path - Path $OutputPath - Parent
1462+ if (-not [string ]::IsNullOrWhiteSpace($parent ) -and -not (Test-Path $parent )) { New-Item - ItemType Directory - Path $parent - Force | Out-Null }
1463+ }
1464+
1465+ $tempRoot = Join-Path ([System.IO.Path ]::GetTempPath()) (" MonitorControlProfileExport-" + [guid ]::NewGuid())
1466+ $tempProfiles = Join-Path $tempRoot " profiles"
1467+ try {
1468+ New-Item - ItemType Directory - Path $tempProfiles - Force | Out-Null
1469+ $exportedProfiles = @ ()
1470+ foreach ($profileFile in $profileFiles ) {
1471+ $profile = Read-ProfileObject - Name $profileFile.BaseName
1472+ if ($null -eq $profile ) { continue }
1473+ $safeName = $profileFile.BaseName
1474+ $profile | ConvertTo-Json - Depth 4 | Set-Content - Path (Join-Path $tempProfiles " $safeName .json" ) - Encoding UTF8
1475+ $exportedProfiles += $safeName
1476+ }
1477+
1478+ if ($exportedProfiles.Count -eq 0 ) {
1479+ Update-Status " No profiles to export"
1480+ return $null
1481+ }
1482+
1483+ $manifest = [PSCustomObject ]@ {
1484+ BundleSchemaVersion = $script :ProfileBundleSchemaVersion
1485+ AppVersion = " 3.17.0"
1486+ ProfileSchemaVersion = $script :ProfileSchemaVersion
1487+ ExportedAt = (Get-Date ).ToString(" o" )
1488+ ProfileCount = $exportedProfiles.Count
1489+ Profiles = $exportedProfiles
1490+ }
1491+ $manifest | ConvertTo-Json - Depth 4 | Set-Content - Path (Join-Path $tempRoot " manifest.json" ) - Encoding UTF8
1492+ if (Test-Path $OutputPath ) { Remove-Item - Path $OutputPath - Force }
1493+ [System.IO.Compression.ZipFile ]::CreateFromDirectory($tempRoot , $OutputPath )
1494+ Update-Status " Exported $ ( $exportedProfiles.Count ) profiles to $ ( Split-Path - Path $OutputPath - Leaf) "
1495+ return $OutputPath
1496+ } catch {
1497+ Update-Status " Profile export failed: $ ( $_.Exception.Message ) "
1498+ return $null
1499+ } finally {
1500+ if (Test-Path $tempRoot ) { Remove-Item - Path $tempRoot - Recurse - Force - ErrorAction SilentlyContinue }
1501+ }
1502+ }
1503+
1504+ function Import-ProfileBundle {
1505+ param ([string ]$BundlePath )
1506+ if ([string ]::IsNullOrWhiteSpace($BundlePath ) -or -not (Test-Path $BundlePath )) {
1507+ Update-Status " Profile bundle not found"
1508+ return 0
1509+ }
1510+
1511+ $archive = $null
1512+ $imported = 0
1513+ $skipped = 0
1514+ try {
1515+ $archive = [System.IO.Compression.ZipFile ]::OpenRead($BundlePath )
1516+ $entries = @ ($archive.Entries | Where-Object {
1517+ -not [string ]::IsNullOrWhiteSpace($_.Name ) -and
1518+ $_.Name.ToLowerInvariant ().EndsWith(" .json" ) -and
1519+ $_.Name -ne " manifest.json" -and
1520+ $script :ProfileMetadataFiles -notcontains $_.Name
1521+ })
1522+
1523+ foreach ($entry in $entries ) {
1524+ $fallbackName = [System.IO.Path ]::GetFileNameWithoutExtension($entry.Name )
1525+ if ([string ]::IsNullOrWhiteSpace($fallbackName )) { $skipped ++ ; continue }
1526+ $reader = $null
1527+ try {
1528+ $reader = New-Object System.IO.StreamReader($entry.Open ())
1529+ $rawProfile = $reader.ReadToEnd () | ConvertFrom-Json
1530+ $profile = ConvertTo-CurrentProfileSchema - Profile $rawProfile - FallbackName $fallbackName
1531+ if (Save-ProfileObject - Profile $profile ) { $imported ++ } else { $skipped ++ }
1532+ } catch {
1533+ $skipped ++
1534+ } finally {
1535+ if ($reader ) { $reader.Dispose () }
1536+ }
1537+ }
1538+
1539+ Update-ProfilesList
1540+ $status = if ($imported -gt 0 ) { " Imported $imported profiles from $ ( Split-Path - Path $BundlePath - Leaf) " } else { " No profiles imported" }
1541+ if ($skipped -gt 0 ) { $status = " $status ($skipped skipped)" }
1542+ Update-Status $status
1543+ return $imported
1544+ } catch {
1545+ Update-Status " Profile import failed: $ ( $_.Exception.Message ) "
1546+ return 0
1547+ } finally {
1548+ if ($archive ) { $archive.Dispose () }
1549+ }
1550+ }
1551+
14381552function Normalize-AppExeName {
14391553 param ([string ]$ExeName )
14401554 $name = $ExeName.Trim ()
@@ -2035,10 +2149,7 @@ function Hide-MainWindowToTray {
20352149}
20362150
20372151function Invoke-NextProfile {
2038- $profiles = @ ()
2039- if (Test-Path $script :ProfilesPath ) {
2040- $profiles = @ (Get-ChildItem - Path $script :ProfilesPath - Filter " *.json" | Where-Object { $script :ProfileMetadataFiles -notcontains $_.Name } | Sort-Object - Property BaseName)
2041- }
2152+ $profiles = @ (Get-UserProfileFiles )
20422153 if ($profiles.Count -eq 0 ) {
20432154 Update-Status " No profiles saved"
20442155 Show-TrayNotification - Message " No saved profiles are available to cycle."
@@ -2345,6 +2456,20 @@ $deleteProfileBtn.Add_Click({
23452456 Update-ScheduleControls
23462457 }
23472458})
2459+ $exportProfilesBtn.Add_Click ({
2460+ Export-ProfileBundle | Out-Null
2461+ })
2462+ $importProfilesBtn.Add_Click ({
2463+ $dialog = New-Object System.Windows.Forms.OpenFileDialog
2464+ $dialog.Title = " Import profile bundle"
2465+ $dialog.Filter = " MonitorControl profile bundles (*.zip)|*.zip|All files (*.*)|*.*"
2466+ $dialog.CheckFileExists = $true
2467+ $dialog.Multiselect = $false
2468+ $dialog.InitialDirectory = if (Test-Path $script :ProfileExportsPath ) { $script :ProfileExportsPath } else { $script :ProfilesPath }
2469+ if ($dialog.ShowDialog () -eq [System.Windows.Forms.DialogResult ]::OK) {
2470+ Import-ProfileBundle - BundlePath $dialog.FileName | Out-Null
2471+ }
2472+ })
23482473
23492474$appProfileEnabledCheckbox.Add_Checked ({
23502475 if ($script :UpdatingAppProfileUI ) { return }
0 commit comments