Skip to content

Commit cbc00c0

Browse files
committed
feat: add profile sync folder storage
1 parent a17346d commit cbc00c0

4 files changed

Lines changed: 147 additions & 10 deletions

File tree

MonitorControlPro.ps1

Lines changed: 145 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<#
22
.SYNOPSIS
3-
MonitorControl Pro v3.17.0 - Advanced Display & GPU Settings Utility
3+
MonitorControl Pro v3.18.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.17.0 - Enhanced with profile bundle export/import
8+
Version: 3.18.0 - Enhanced with cloud-sync profile storage
99
#>
1010

1111
param([switch]$StartMinimized, [string]$LoadProfile)
@@ -414,15 +414,32 @@ $script:FpsOverlayText = $null
414414
$script:FpsOverlayTimer = $null
415415
$script:GpuTimer = $null
416416
$script:AutoModeTimer = $null
417-
$script:ProfilesPath = "$env:APPDATA\MonitorControlPro"
417+
$script:DefaultProfilesPath = "$env:APPDATA\MonitorControlPro"
418+
$script:ProfileStorageSettingsPath = Join-Path $script:DefaultProfilesPath "profile-storage.json"
419+
$script:ProfilesPath = $script:DefaultProfilesPath
420+
$script:ProfileStorageMode = "Local"
421+
if (-not (Test-Path $script:DefaultProfilesPath)) { New-Item -ItemType Directory -Path $script:DefaultProfilesPath -Force | Out-Null }
422+
if (Test-Path $script:ProfileStorageSettingsPath) {
423+
try {
424+
$profileStorage = Get-Content -Path $script:ProfileStorageSettingsPath -Raw | ConvertFrom-Json
425+
$configuredPath = [string]$profileStorage.ProfilePath
426+
if (-not [string]::IsNullOrWhiteSpace($configuredPath)) {
427+
$script:ProfilesPath = [System.IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($configuredPath))
428+
$script:ProfileStorageMode = if ($profileStorage.Mode) { [string]$profileStorage.Mode } else { "Sync" }
429+
}
430+
} catch {
431+
$script:ProfilesPath = $script:DefaultProfilesPath
432+
$script:ProfileStorageMode = "Local"
433+
}
434+
}
418435
$script:AppProfileRulesPath = Join-Path $script:ProfilesPath "app-profile-rules.json"
419436
$script:ProfileScheduleRulesPath = Join-Path $script:ProfilesPath "profile-schedules.json"
420437
$script:IdleDimSettingsPath = Join-Path $script:ProfilesPath "idle-dim.json"
421438
$script:BatteryProfileSettingsPath = Join-Path $script:ProfilesPath "battery-profile.json"
422439
$script:ProfileSchemaVersion = 2
423440
$script:ProfileBundleSchemaVersion = 1
424441
$script:ProfileExportsPath = Join-Path $script:ProfilesPath "exports"
425-
$script:ProfileMetadataFiles = @("app-profile-rules.json", "profile-schedules.json", "idle-dim.json", "battery-profile.json")
442+
$script:ProfileMetadataFiles = @("app-profile-rules.json", "profile-schedules.json", "idle-dim.json", "battery-profile.json", "profile-storage.json")
426443
$script:UpdatingUI = $false
427444
$script:ApplyToAll = $false
428445
$script:AutoModeEnabled = $false
@@ -786,11 +803,22 @@ function Get-CpuTemperature {
786803
return $null
787804
}
788805

789-
if (-not (Test-Path $script:ProfilesPath)) { New-Item -ItemType Directory -Path $script:ProfilesPath -Force | Out-Null }
806+
try {
807+
if (-not (Test-Path $script:ProfilesPath)) { New-Item -ItemType Directory -Path $script:ProfilesPath -Force | Out-Null }
808+
} catch {
809+
$script:ProfilesPath = $script:DefaultProfilesPath
810+
$script:ProfileStorageMode = "Local"
811+
$script:AppProfileRulesPath = Join-Path $script:ProfilesPath "app-profile-rules.json"
812+
$script:ProfileScheduleRulesPath = Join-Path $script:ProfilesPath "profile-schedules.json"
813+
$script:IdleDimSettingsPath = Join-Path $script:ProfilesPath "idle-dim.json"
814+
$script:BatteryProfileSettingsPath = Join-Path $script:ProfilesPath "battery-profile.json"
815+
$script:ProfileExportsPath = Join-Path $script:ProfilesPath "exports"
816+
if (-not (Test-Path $script:ProfilesPath)) { New-Item -ItemType Directory -Path $script:ProfilesPath -Force | Out-Null }
817+
}
790818

791819
[xml]$xaml = @"
792820
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
793-
Title="MonitorControl Pro v3.17.0" Width="640" Height="680" MinWidth="560" MinHeight="560"
821+
Title="MonitorControl Pro v3.18.0" Width="640" Height="680" MinWidth="560" MinHeight="560"
794822
Background="#0a0a0a" WindowStartupLocation="CenterScreen" ResizeMode="CanResizeWithGrip">
795823
<Window.Resources>
796824
<ControlTemplate x:Key="ComboBoxToggleButton" TargetType="ToggleButton">
@@ -924,7 +952,7 @@ if (-not (Test-Path $script:ProfilesPath)) { New-Item -ItemType Directory -Path
924952
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
925953
<StackPanel VerticalAlignment="Center">
926954
<TextBlock Text="MonitorControl Pro" FontSize="16" FontWeight="SemiBold" Foreground="#fff" FontFamily="Segoe UI"/>
927-
<TextBlock Text="v3.17.0 - Click monitor to select" FontSize="9" Foreground="#505050" Margin="0,1,0,0"/>
955+
<TextBlock Text="v3.18.0 - Click monitor to select" FontSize="9" Foreground="#505050" Margin="0,1,0,0"/>
928956
</StackPanel>
929957
<StackPanel Grid.Column="2" Orientation="Horizontal">
930958
<CheckBox x:Name="ApplyAllCheckbox" Content="All Monitors" VerticalAlignment="Center" Margin="0,0,10,0"/>
@@ -1117,7 +1145,7 @@ if (-not (Test-Path $script:ProfilesPath)) { New-Item -ItemType Directory -Path
11171145
</TabItem>
11181146
<TabItem Header="Profiles">
11191147
<Border Background="#151515" CornerRadius="0,5,5,5" Padding="10"><Grid>
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>
1148+
<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"/><RowDefinition Height="8"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
11211149
<Border Background="#1a1a1a" CornerRadius="5" Padding="10"><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="6"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
11221150
<TextBox x:Name="ProfileNameBox" Text="My Profile"/>
11231151
<Button x:Name="SaveProfileBtn" Grid.Column="2" Content="Save" Style="{StaticResource GreenBtn}" Padding="10,4"/>
@@ -1135,6 +1163,13 @@ if (-not (Test-Path $script:ProfilesPath)) { New-Item -ItemType Directory -Path
11351163
<Button x:Name="ImportProfilesBtn" Grid.Column="2" Content="Import Bundle" Style="{StaticResource Btn}"/>
11361164
</Grid>
11371165
<Border Grid.Row="8" Background="#1a1a1a" CornerRadius="5" Padding="10"><Grid>
1166+
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="6"/><ColumnDefinition Width="Auto"/><ColumnDefinition Width="6"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
1167+
<StackPanel><TextBlock Text="Profile Storage" FontSize="10" Foreground="#909090"/>
1168+
<TextBlock x:Name="ProfileStorageStatusText" Text="Local" FontSize="8" Foreground="#707070" Margin="0,2,0,0" TextTrimming="CharacterEllipsis"/></StackPanel>
1169+
<Button x:Name="ProfileSyncFolderBtn" Grid.Column="2" Content="Sync Folder" Style="{StaticResource Btn}" Padding="8,4" FontSize="9"/>
1170+
<Button x:Name="ProfileLocalFolderBtn" Grid.Column="4" Content="Use Local" Style="{StaticResource Btn}" Padding="8,4" FontSize="9"/>
1171+
</Grid></Border>
1172+
<Border Grid.Row="10" Background="#1a1a1a" CornerRadius="5" Padding="10"><Grid>
11381173
<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>
11391174
<Grid><CheckBox x:Name="AppProfileEnabledCheckbox" Content="Per-application profiles" VerticalAlignment="Center"/>
11401175
<TextBlock x:Name="AppProfileStatusText" Text="Off" FontSize="9" Foreground="#707070" HorizontalAlignment="Right" VerticalAlignment="Center"/></Grid>
@@ -1274,6 +1309,7 @@ $vcpResultBox = $window.FindName("VCPResultBox"); $vcpSetValueBox = $window.Find
12741309
$profileNameBox = $window.FindName("ProfileNameBox"); $profilesList = $window.FindName("ProfilesList")
12751310
$saveProfileBtn = $window.FindName("SaveProfileBtn"); $loadProfileBtn = $window.FindName("LoadProfileBtn"); $deleteProfileBtn = $window.FindName("DeleteProfileBtn")
12761311
$exportProfilesBtn = $window.FindName("ExportProfilesBtn"); $importProfilesBtn = $window.FindName("ImportProfilesBtn")
1312+
$profileStorageStatusText = $window.FindName("ProfileStorageStatusText"); $profileSyncFolderBtn = $window.FindName("ProfileSyncFolderBtn"); $profileLocalFolderBtn = $window.FindName("ProfileLocalFolderBtn")
12771313
$appProfileEnabledCheckbox = $window.FindName("AppProfileEnabledCheckbox"); $appProfileStatusText = $window.FindName("AppProfileStatusText")
12781314
$appProfileExeBox = $window.FindName("AppProfileExeBox"); $appProfileCaptureBtn = $window.FindName("AppProfileCaptureBtn")
12791315
$appProfileProfileCombo = $window.FindName("AppProfileProfileCombo"); $appProfileAddBtn = $window.FindName("AppProfileAddBtn")
@@ -1549,6 +1585,86 @@ function Import-ProfileBundle {
15491585
}
15501586
}
15511587

1588+
function Set-ProfileStorageRoot {
1589+
param([string]$Path, [string]$Mode = "Sync")
1590+
if ([string]::IsNullOrWhiteSpace($Path)) {
1591+
Update-Status "Profile storage path is empty"
1592+
return $false
1593+
}
1594+
try {
1595+
$expandedPath = [Environment]::ExpandEnvironmentVariables($Path)
1596+
$fullPath = [System.IO.Path]::GetFullPath($expandedPath)
1597+
if (-not (Test-Path $fullPath)) { New-Item -ItemType Directory -Path $fullPath -Force | Out-Null }
1598+
$script:ProfilesPath = $fullPath
1599+
$script:ProfileStorageMode = $Mode
1600+
$script:AppProfileRulesPath = Join-Path $script:ProfilesPath "app-profile-rules.json"
1601+
$script:ProfileScheduleRulesPath = Join-Path $script:ProfilesPath "profile-schedules.json"
1602+
$script:IdleDimSettingsPath = Join-Path $script:ProfilesPath "idle-dim.json"
1603+
$script:BatteryProfileSettingsPath = Join-Path $script:ProfilesPath "battery-profile.json"
1604+
$script:ProfileExportsPath = Join-Path $script:ProfilesPath "exports"
1605+
return $true
1606+
} catch {
1607+
Update-Status "Profile storage failed: $($_.Exception.Message)"
1608+
return $false
1609+
}
1610+
}
1611+
1612+
function Save-ProfileStorageSettings {
1613+
if (-not (Test-Path $script:DefaultProfilesPath)) { New-Item -ItemType Directory -Path $script:DefaultProfilesPath -Force | Out-Null }
1614+
$payload = [PSCustomObject]@{
1615+
Mode = $script:ProfileStorageMode
1616+
ProfilePath = $script:ProfilesPath
1617+
UpdatedAt = (Get-Date).ToString("o")
1618+
}
1619+
$payload | ConvertTo-Json | Set-Content -Path $script:ProfileStorageSettingsPath -Encoding UTF8
1620+
}
1621+
1622+
function Reset-ProfileBackedAutomationState {
1623+
$script:AppProfileEnabled = $false
1624+
$script:AppProfileRules = @()
1625+
$script:LastForegroundExe = $null
1626+
$script:LastAppliedAppProfileKey = $null
1627+
$script:ProfileScheduleEnabled = $false
1628+
$script:ProfileSchedules = @()
1629+
$script:LastAppliedScheduleKey = $null
1630+
$script:IdleDimEnabled = $false
1631+
$script:IdleDimMinutes = 10
1632+
$script:IdleDimBrightness = 20
1633+
$script:IdleDimRestoreOnActivity = $true
1634+
$script:IdleDimActive = $false
1635+
$script:IdleDimPreviousBrightness = $null
1636+
$script:BatteryProfileEnabled = $false
1637+
$script:BatteryBrightness = 35
1638+
$script:AcBrightness = 75
1639+
$script:LastPowerLineStatus = $null
1640+
$script:ProfileCycleIndex = -1
1641+
}
1642+
1643+
function Update-ProfileStorageControls {
1644+
if ($null -eq $profileStorageStatusText) { return }
1645+
$mode = if ($script:ProfileStorageMode -eq "Local") { "Local" } else { "Sync" }
1646+
$profileStorageStatusText.Text = "$mode - $script:ProfilesPath"
1647+
$profileStorageStatusText.ToolTip = $script:ProfilesPath
1648+
}
1649+
1650+
function Reload-ProfileStorageState {
1651+
Reset-ProfileBackedAutomationState
1652+
Load-AppProfileRules
1653+
Load-ProfileSchedules
1654+
Load-IdleDimSettings
1655+
Load-BatteryProfileSettings
1656+
Update-ProfilesList
1657+
Update-AppProfileControls
1658+
Update-ScheduleControls
1659+
Update-IdleDimControls
1660+
Update-BatteryProfileControls
1661+
Start-AppProfileWatcher
1662+
Start-ProfileScheduleWatcher
1663+
Start-IdleDimWatcher
1664+
Start-BatteryProfileWatcher
1665+
Update-ProfileStorageControls
1666+
}
1667+
15521668
function Normalize-AppExeName {
15531669
param([string]$ExeName)
15541670
$name = $ExeName.Trim()
@@ -2470,6 +2586,26 @@ $importProfilesBtn.Add_Click({
24702586
Import-ProfileBundle -BundlePath $dialog.FileName | Out-Null
24712587
}
24722588
})
2589+
$profileSyncFolderBtn.Add_Click({
2590+
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
2591+
$dialog.Description = "Select a OneDrive or Dropbox folder for MonitorControl profiles"
2592+
$dialog.ShowNewFolderButton = $true
2593+
$dialog.SelectedPath = if (Test-Path $script:ProfilesPath) { $script:ProfilesPath } else { $script:DefaultProfilesPath }
2594+
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
2595+
if (Set-ProfileStorageRoot -Path $dialog.SelectedPath -Mode "Sync") {
2596+
Save-ProfileStorageSettings
2597+
Reload-ProfileStorageState
2598+
Update-Status "Profile sync folder: $(Split-Path -Path $script:ProfilesPath -Leaf)"
2599+
}
2600+
}
2601+
})
2602+
$profileLocalFolderBtn.Add_Click({
2603+
if (Set-ProfileStorageRoot -Path $script:DefaultProfilesPath -Mode "Local") {
2604+
Save-ProfileStorageSettings
2605+
Reload-ProfileStorageState
2606+
Update-Status "Profile storage: local"
2607+
}
2608+
})
24732609

24742610
$appProfileEnabledCheckbox.Add_Checked({
24752611
if ($script:UpdatingAppProfileUI) { return }
@@ -2650,6 +2786,7 @@ Load-AppProfileRules; Update-AppProfileControls; Start-AppProfileWatcher
26502786
Load-ProfileSchedules; Update-ScheduleControls; Start-ProfileScheduleWatcher
26512787
Load-IdleDimSettings; Update-IdleDimControls; Start-IdleDimWatcher
26522788
Load-BatteryProfileSettings; Update-BatteryProfileControls; Start-BatteryProfileWatcher
2789+
Update-ProfileStorageControls
26532790
if (-not ($script:HasNvidia -or $script:HasAmd -or $script:HasCpuTempMonitor)) { $gpuTab.Visibility = "Collapsed" } else {
26542791
$script:GpuTimer = New-Object System.Windows.Threading.DispatcherTimer; $script:GpuTimer.Interval = [TimeSpan]::FromSeconds(2)
26552792
$script:GpuTimer.Add_Tick({ Update-GpuStats }); $script:GpuTimer.Start(); Update-GpuStats

README.md

Lines changed: 2 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.17.0-brightgreen" alt="Version v3.17.0">
4+
<img src="https://img.shields.io/badge/Version-v3.18.0-brightgreen" alt="Version v3.18.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">
@@ -231,6 +231,7 @@ Profiles without `SchemaVersion` are treated as v1 and migrated to the current s
231231

232232
Use **Export Bundle** on the Profiles tab to create a timestamped ZIP in `%APPDATA%\MonitorControlPro\exports`.
233233
Use **Import Bundle** to restore every profile JSON from a bundle; corrupt entries are skipped without blocking valid profiles.
234+
Use **Sync Folder** to point profile storage at a OneDrive or Dropbox folder. The pointer is stored in `%APPDATA%\MonitorControlPro\profile-storage.json`; **Use Local** switches back to `%APPDATA%\MonitorControlPro`.
234235

235236
### Example Profiles
236237

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-
- Cloud-sync profiles via OneDrive/Dropbox folder pointer
98
- Profile scheduling UI (graphical timeline, not just trigger rules)
109

1110
## Competitive Research

screenshot.png

5.1 KB
Loading

0 commit comments

Comments
 (0)