Add primary display indicator, custom accent color picker, restart script
- Detect primary monitor via Windows EnumDisplayMonitors API and show badge - Expand accent color picker with 9 presets and custom color input - Auto-generate hover color for custom accent colors - Re-render accent swatches on locale change for proper i18n - Replace restart-server.bat with PowerShell restart-server.ps1 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
"""Display brightness and power control service."""
|
"""Display brightness and power control service."""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import ctypes.wintypes
|
||||||
import logging
|
import logging
|
||||||
import platform
|
import platform
|
||||||
import struct
|
import struct
|
||||||
@@ -61,6 +63,7 @@ class MonitorInfo:
|
|||||||
model: str = ""
|
model: str = ""
|
||||||
manufacturer: str = ""
|
manufacturer: str = ""
|
||||||
resolution: str | None = None
|
resolution: str | None = None
|
||||||
|
is_primary: bool = False
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
@@ -72,9 +75,68 @@ class MonitorInfo:
|
|||||||
"model": self.model,
|
"model": self.model,
|
||||||
"manufacturer": self.manufacturer,
|
"manufacturer": self.manufacturer,
|
||||||
"resolution": self.resolution,
|
"resolution": self.resolution,
|
||||||
|
"is_primary": self.is_primary,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_primary_resolution() -> str | None:
|
||||||
|
"""Detect the primary display resolution via Windows API."""
|
||||||
|
if platform.system() != "Windows":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
class MONITORINFO(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("cbSize", ctypes.wintypes.DWORD),
|
||||||
|
("rcMonitor", ctypes.wintypes.RECT),
|
||||||
|
("rcWork", ctypes.wintypes.RECT),
|
||||||
|
("dwFlags", ctypes.wintypes.DWORD),
|
||||||
|
]
|
||||||
|
|
||||||
|
MONITORINFOF_PRIMARY = 1
|
||||||
|
primary_res = None
|
||||||
|
|
||||||
|
def callback(hmon, hdc, rect, data):
|
||||||
|
nonlocal primary_res
|
||||||
|
mi = MONITORINFO()
|
||||||
|
mi.cbSize = ctypes.sizeof(mi)
|
||||||
|
ctypes.windll.user32.GetMonitorInfoW(hmon, ctypes.byref(mi))
|
||||||
|
if mi.dwFlags & MONITORINFOF_PRIMARY:
|
||||||
|
w = mi.rcMonitor.right - mi.rcMonitor.left
|
||||||
|
h = mi.rcMonitor.bottom - mi.rcMonitor.top
|
||||||
|
primary_res = f"{w}x{h}"
|
||||||
|
return True
|
||||||
|
|
||||||
|
MONITORENUMPROC = ctypes.WINFUNCTYPE(
|
||||||
|
ctypes.c_int,
|
||||||
|
ctypes.wintypes.HMONITOR,
|
||||||
|
ctypes.wintypes.HDC,
|
||||||
|
ctypes.POINTER(ctypes.wintypes.RECT),
|
||||||
|
ctypes.wintypes.LPARAM,
|
||||||
|
)
|
||||||
|
ctypes.windll.user32.EnumDisplayMonitors(
|
||||||
|
None, None, MONITORENUMPROC(callback), 0
|
||||||
|
)
|
||||||
|
return primary_res
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _mark_primary(monitors: list[MonitorInfo]) -> None:
|
||||||
|
"""Mark the primary display in the monitor list."""
|
||||||
|
if not monitors:
|
||||||
|
return
|
||||||
|
|
||||||
|
primary_res = _detect_primary_resolution()
|
||||||
|
if primary_res:
|
||||||
|
for m in monitors:
|
||||||
|
if m.resolution == primary_res:
|
||||||
|
m.is_primary = True
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fallback: mark first monitor as primary
|
||||||
|
monitors[0].is_primary = True
|
||||||
|
|
||||||
|
|
||||||
# Cache for monitor list
|
# Cache for monitor list
|
||||||
_monitor_cache: list[MonitorInfo] | None = None
|
_monitor_cache: list[MonitorInfo] | None = None
|
||||||
_cache_time: float = 0
|
_cache_time: float = 0
|
||||||
@@ -142,6 +204,7 @@ def list_monitors(force_refresh: bool = False) -> list[MonitorInfo]:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Failed to enumerate monitors: %s", e)
|
logger.error("Failed to enumerate monitors: %s", e)
|
||||||
|
|
||||||
|
_mark_primary(monitors)
|
||||||
_monitor_cache = monitors
|
_monitor_cache = monitors
|
||||||
_cache_time = time.time()
|
_cache_time = time.time()
|
||||||
return monitors
|
return monitors
|
||||||
|
|||||||
@@ -303,12 +303,12 @@ h1 {
|
|||||||
top: calc(100% + 4px);
|
top: calc(100% + 4px);
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 8px;
|
padding: 10px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
grid-template-columns: repeat(3, 24px);
|
grid-template-columns: repeat(3, 28px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.accent-picker-dropdown.open {
|
.accent-picker-dropdown.open {
|
||||||
@@ -316,8 +316,8 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.accent-swatch {
|
.accent-swatch {
|
||||||
width: 24px;
|
width: 28px;
|
||||||
height: 24px;
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -325,13 +325,57 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.accent-swatch:hover {
|
.accent-swatch:hover {
|
||||||
transform: scale(1.2);
|
transform: scale(1.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.accent-swatch.active {
|
.accent-swatch.active {
|
||||||
border-color: var(--text-primary);
|
border-color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accent-custom-row {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 2px 2px;
|
||||||
|
margin-top: 4px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-custom-row:hover .accent-custom-label {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-custom-row.active .accent-custom-swatch {
|
||||||
|
outline: 2px solid var(--text-primary);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-custom-swatch {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-custom-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-custom-row input[type="color"] {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
#locale-select {
|
#locale-select {
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -1115,6 +1159,20 @@ button:disabled {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.display-primary-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-left: 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
.display-monitor-details {
|
.display-monitor-details {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|||||||
@@ -182,11 +182,23 @@
|
|||||||
{ name: 'Yellow', color: '#eab308', hover: '#facc15' },
|
{ name: 'Yellow', color: '#eab308', hover: '#facc15' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function lightenColor(hex, percent) {
|
||||||
|
const num = parseInt(hex.replace('#', ''), 16);
|
||||||
|
const r = Math.min(255, (num >> 16) + Math.round(255 * percent / 100));
|
||||||
|
const g = Math.min(255, ((num >> 8) & 0xff) + Math.round(255 * percent / 100));
|
||||||
|
const b = Math.min(255, (num & 0xff) + Math.round(255 * percent / 100));
|
||||||
|
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function initAccentColor() {
|
function initAccentColor() {
|
||||||
const saved = localStorage.getItem('accentColor');
|
const saved = localStorage.getItem('accentColor');
|
||||||
if (saved) {
|
if (saved) {
|
||||||
const preset = accentPresets.find(p => p.color === saved);
|
const preset = accentPresets.find(p => p.color === saved);
|
||||||
if (preset) applyAccentColor(preset.color, preset.hover);
|
if (preset) {
|
||||||
|
applyAccentColor(preset.color, preset.hover);
|
||||||
|
} else {
|
||||||
|
applyAccentColor(saved, lightenColor(saved, 15));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
renderAccentSwatches();
|
renderAccentSwatches();
|
||||||
}
|
}
|
||||||
@@ -195,18 +207,33 @@
|
|||||||
document.documentElement.style.setProperty('--accent', color);
|
document.documentElement.style.setProperty('--accent', color);
|
||||||
document.documentElement.style.setProperty('--accent-hover', hover);
|
document.documentElement.style.setProperty('--accent-hover', hover);
|
||||||
localStorage.setItem('accentColor', color);
|
localStorage.setItem('accentColor', color);
|
||||||
|
const dot = document.getElementById('accentDot');
|
||||||
|
if (dot) dot.style.background = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAccentSwatches() {
|
function renderAccentSwatches() {
|
||||||
const dropdown = document.getElementById('accentDropdown');
|
const dropdown = document.getElementById('accentDropdown');
|
||||||
if (!dropdown) return;
|
if (!dropdown) return;
|
||||||
const current = localStorage.getItem('accentColor') || '#1db954';
|
const current = localStorage.getItem('accentColor') || '#1db954';
|
||||||
dropdown.innerHTML = accentPresets.map(p =>
|
const isCustom = !accentPresets.some(p => p.color === current);
|
||||||
|
|
||||||
|
const swatches = accentPresets.map(p =>
|
||||||
`<div class="accent-swatch ${p.color === current ? 'active' : ''}"
|
`<div class="accent-swatch ${p.color === current ? 'active' : ''}"
|
||||||
style="background: ${p.color}"
|
style="background: ${p.color}"
|
||||||
onclick="selectAccentColor('${p.color}', '${p.hover}')"
|
onclick="selectAccentColor('${p.color}', '${p.hover}')"
|
||||||
title="${p.name}"></div>`
|
title="${p.name}"></div>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
|
const customRow = `
|
||||||
|
<div class="accent-custom-row ${isCustom ? 'active' : ''}" onclick="document.getElementById('accentCustomInput').click()">
|
||||||
|
<span class="accent-custom-swatch" style="background: ${isCustom ? current : '#888'}"></span>
|
||||||
|
<span class="accent-custom-label">${t('accent.custom')}</span>
|
||||||
|
<input type="color" id="accentCustomInput" value="${current}"
|
||||||
|
onclick="event.stopPropagation()"
|
||||||
|
onchange="selectAccentColor(this.value, lightenColor(this.value, 15))">
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
dropdown.innerHTML = swatches + customRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAccentColor(color, hover) {
|
function selectAccentColor(color, hover) {
|
||||||
@@ -408,6 +435,7 @@
|
|||||||
loadLinksTable();
|
loadLinksTable();
|
||||||
displayQuickAccess();
|
displayQuickAccess();
|
||||||
}
|
}
|
||||||
|
renderAccentSwatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchVersion() {
|
async function fetchVersion() {
|
||||||
@@ -2956,6 +2984,7 @@ async function loadDisplayMonitors() {
|
|||||||
|
|
||||||
const details = [monitor.resolution, monitor.manufacturer].filter(Boolean).join(' · ');
|
const details = [monitor.resolution, monitor.manufacturer].filter(Boolean).join(' · ');
|
||||||
const detailsHtml = details ? `<span class="display-monitor-details">${details}</span>` : '';
|
const detailsHtml = details ? `<span class="display-monitor-details">${details}</span>` : '';
|
||||||
|
const primaryBadge = monitor.is_primary ? `<span class="display-primary-badge">${t('display.primary')}</span>` : '';
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="display-monitor-header">
|
<div class="display-monitor-header">
|
||||||
@@ -2963,7 +2992,7 @@ async function loadDisplayMonitors() {
|
|||||||
<path fill="currentColor" d="M20 3H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h6v2H8v2h8v-2h-2v-2h6c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H4V5h16v10z"/>
|
<path fill="currentColor" d="M20 3H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h6v2H8v2h8v-2h-2v-2h6c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H4V5h16v10z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="display-monitor-info">
|
<div class="display-monitor-info">
|
||||||
<span class="display-monitor-name">${monitor.name}</span>
|
<span class="display-monitor-name">${monitor.name}${primaryBadge}</span>
|
||||||
${detailsHtml}
|
${detailsHtml}
|
||||||
</div>
|
</div>
|
||||||
${powerBtn}
|
${powerBtn}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"auth.cleared": "Token cleared. Please enter a new token.",
|
"auth.cleared": "Token cleared. Please enter a new token.",
|
||||||
"auth.required": "Please enter a token",
|
"auth.required": "Please enter a token",
|
||||||
"player.theme": "Toggle theme",
|
"player.theme": "Toggle theme",
|
||||||
|
"accent.custom": "Custom",
|
||||||
"player.locale": "Change language",
|
"player.locale": "Change language",
|
||||||
"player.previous": "Previous",
|
"player.previous": "Previous",
|
||||||
"player.play": "Play/Pause",
|
"player.play": "Play/Pause",
|
||||||
@@ -128,6 +129,7 @@
|
|||||||
"display.no_monitors": "No monitors detected",
|
"display.no_monitors": "No monitors detected",
|
||||||
"display.power_on": "Turn on",
|
"display.power_on": "Turn on",
|
||||||
"display.power_off": "Turn off",
|
"display.power_off": "Turn off",
|
||||||
|
"display.primary": "Primary",
|
||||||
"browser.title": "Media Browser",
|
"browser.title": "Media Browser",
|
||||||
"browser.home": "Home",
|
"browser.home": "Home",
|
||||||
"browser.manage_folders": "Manage Folders",
|
"browser.manage_folders": "Manage Folders",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"auth.cleared": "Токен очищен. Пожалуйста, введите новый токен.",
|
"auth.cleared": "Токен очищен. Пожалуйста, введите новый токен.",
|
||||||
"auth.required": "Пожалуйста, введите токен",
|
"auth.required": "Пожалуйста, введите токен",
|
||||||
"player.theme": "Переключить тему",
|
"player.theme": "Переключить тему",
|
||||||
|
"accent.custom": "Свой цвет",
|
||||||
"player.locale": "Изменить язык",
|
"player.locale": "Изменить язык",
|
||||||
"player.previous": "Предыдущий",
|
"player.previous": "Предыдущий",
|
||||||
"player.play": "Воспроизвести/Пауза",
|
"player.play": "Воспроизвести/Пауза",
|
||||||
@@ -128,6 +129,7 @@
|
|||||||
"display.no_monitors": "Мониторы не обнаружены",
|
"display.no_monitors": "Мониторы не обнаружены",
|
||||||
"display.power_on": "Включить",
|
"display.power_on": "Включить",
|
||||||
"display.power_off": "Выключить",
|
"display.power_off": "Выключить",
|
||||||
|
"display.primary": "Основной",
|
||||||
"browser.title": "Медиа Браузер",
|
"browser.title": "Медиа Браузер",
|
||||||
"browser.home": "Главная",
|
"browser.home": "Главная",
|
||||||
"browser.manage_folders": "Управление папками",
|
"browser.manage_folders": "Управление папками",
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
@echo off
|
|
||||||
REM Media Server Restart Script
|
|
||||||
REM This script restarts the media server
|
|
||||||
|
|
||||||
echo Restarting Media Server...
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM Stop the server first
|
|
||||||
echo [1/2] Stopping server...
|
|
||||||
call "%~dp0\stop-server.bat"
|
|
||||||
|
|
||||||
REM Wait a moment
|
|
||||||
timeout /t 2 /nobreak >nul
|
|
||||||
|
|
||||||
REM Change to parent directory (media-server root)
|
|
||||||
cd /d "%~dp0\.."
|
|
||||||
|
|
||||||
REM Start the server
|
|
||||||
echo.
|
|
||||||
echo [2/2] Starting server...
|
|
||||||
python -m media_server.main
|
|
||||||
|
|
||||||
REM If the server exits, pause to show any error messages
|
|
||||||
pause
|
|
||||||
35
scripts/restart-server.ps1
Normal file
35
scripts/restart-server.ps1
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Restart the Media Server
|
||||||
|
# Stop any running instance
|
||||||
|
$procs = Get-Process -Name 'media-server' -ErrorAction SilentlyContinue
|
||||||
|
foreach ($p in $procs) {
|
||||||
|
Write-Host "Stopping server (PID $($p.Id))..."
|
||||||
|
Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
if ($procs) { Start-Sleep -Seconds 2 }
|
||||||
|
|
||||||
|
# Merge registry PATH with current PATH so newly-installed tools are visible
|
||||||
|
$regUser = [Environment]::GetEnvironmentVariable('PATH', 'User')
|
||||||
|
if ($regUser) {
|
||||||
|
$currentDirs = $env:PATH -split ';' | ForEach-Object { $_.TrimEnd('\') }
|
||||||
|
foreach ($dir in ($regUser -split ';')) {
|
||||||
|
if ($dir -and ($currentDirs -notcontains $dir.TrimEnd('\'))) {
|
||||||
|
$env:PATH = "$env:PATH;$dir"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start server detached
|
||||||
|
Write-Host "Starting server..."
|
||||||
|
Start-Process -FilePath 'media-server' `
|
||||||
|
-WorkingDirectory 'c:\Users\Alexei\Documents\haos-integration-media-player\media-server' `
|
||||||
|
-WindowStyle Hidden
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
|
||||||
|
# Verify it's running
|
||||||
|
$check = Get-Process -Name 'media-server' -ErrorAction SilentlyContinue
|
||||||
|
if ($check) {
|
||||||
|
Write-Host "Server started (PID $($check[0].Id))"
|
||||||
|
} else {
|
||||||
|
Write-Host "WARNING: Server does not appear to be running!"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user