Skip to content

Commit a17346d

Browse files
committed
feat: add profile bundle import export
1 parent c807f5e commit a17346d

4 files changed

Lines changed: 145 additions & 18 deletions

File tree

MonitorControlPro.ps1

Lines changed: 141 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
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

1111
param([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 = @"
1616
using 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

13651372
function 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+
13661380
function 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+
14381552
function Normalize-AppExeName {
14391553
param([string]$ExeName)
14401554
$name = $ExeName.Trim()
@@ -2035,10 +2149,7 @@ function Hide-MainWindowToTray {
20352149
}
20362150

20372151
function 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 }

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# MonitorControl Pro
22

33
<p align="center">
4-
<img src="https://img.shields.io/badge/Version-v3.16.0-brightgreen" alt="Version v3.16.0">
4+
<img src="https://img.shields.io/badge/Version-v3.17.0-brightgreen" alt="Version v3.17.0">
55
<img src="https://img.shields.io/badge/PowerShell-5.1+-blue?logo=powershell" alt="PowerShell 5.1+">
66
<img src="https://img.shields.io/badge/Windows-10%2F11-0078D6?logo=windows" alt="Windows 10/11">
77
<img src="https://img.shields.io/badge/DDC%2FCI-Supported-green" alt="DDC/CI">
@@ -229,6 +229,9 @@ Profiles are saved as JSON files in `%APPDATA%\MonitorControlPro\`
229229

230230
Profiles without `SchemaVersion` are treated as v1 and migrated to the current schema on load.
231231

232+
Use **Export Bundle** on the Profiles tab to create a timestamped ZIP in `%APPDATA%\MonitorControlPro\exports`.
233+
Use **Import Bundle** to restore every profile JSON from a bundle; corrupt entries are skipped without blocking valid profiles.
234+
232235
### Example Profiles
233236

234237
**Night Mode** — Reduced brightness and blue light

ROADMAP.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ PowerShell/WPF utility for DDC/CI monitor control — brightness, contrast,
55
## Planned Features
66

77
### Data & Profiles
8-
- Export/import profile bundle (all profiles at once)
98
- Cloud-sync profiles via OneDrive/Dropbox folder pointer
109
- Profile scheduling UI (graphical timeline, not just trigger rules)
1110

screenshot.png

1.3 KB
Loading

0 commit comments

Comments
 (0)