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:
2026-02-27 16:18:18 +03:00
parent adf2d936da
commit 397d38ac12
7 changed files with 198 additions and 33 deletions

View File

@@ -1,5 +1,7 @@
"""Display brightness and power control service."""
import ctypes
import ctypes.wintypes
import logging
import platform
import struct
@@ -61,6 +63,7 @@ class MonitorInfo:
model: str = ""
manufacturer: str = ""
resolution: str | None = None
is_primary: bool = False
def to_dict(self) -> dict:
return {
@@ -72,9 +75,68 @@ class MonitorInfo:
"model": self.model,
"manufacturer": self.manufacturer,
"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
_monitor_cache: list[MonitorInfo] | None = None
_cache_time: float = 0
@@ -142,6 +204,7 @@ def list_monitors(force_refresh: bool = False) -> list[MonitorInfo]:
except Exception as e:
logger.error("Failed to enumerate monitors: %s", e)
_mark_primary(monitors)
_monitor_cache = monitors
_cache_time = time.time()
return monitors

View File

@@ -303,12 +303,12 @@ h1 {
top: calc(100% + 4px);
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px;
border-radius: 12px;
padding: 10px;
gap: 6px;
z-index: 100;
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 {
@@ -316,8 +316,8 @@ h1 {
}
.accent-swatch {
width: 24px;
height: 24px;
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
@@ -325,13 +325,57 @@ h1 {
}
.accent-swatch:hover {
transform: scale(1.2);
transform: scale(1.15);
}
.accent-swatch.active {
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 {
background: var(--bg-tertiary);
border: 1px solid var(--border);
@@ -1115,6 +1159,20 @@ button:disabled {
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 {
font-size: 0.75rem;
color: var(--text-muted);

View File

@@ -182,11 +182,23 @@
{ 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() {
const saved = localStorage.getItem('accentColor');
if (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();
}
@@ -195,18 +207,33 @@
document.documentElement.style.setProperty('--accent', color);
document.documentElement.style.setProperty('--accent-hover', hover);
localStorage.setItem('accentColor', color);
const dot = document.getElementById('accentDot');
if (dot) dot.style.background = color;
}
function renderAccentSwatches() {
const dropdown = document.getElementById('accentDropdown');
if (!dropdown) return;
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' : ''}"
style="background: ${p.color}"
onclick="selectAccentColor('${p.color}', '${p.hover}')"
title="${p.name}"></div>`
).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) {
@@ -408,6 +435,7 @@
loadLinksTable();
displayQuickAccess();
}
renderAccentSwatches();
}
async function fetchVersion() {
@@ -2956,6 +2984,7 @@ async function loadDisplayMonitors() {
const details = [monitor.resolution, monitor.manufacturer].filter(Boolean).join(' · ');
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 = `
<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"/>
</svg>
<div class="display-monitor-info">
<span class="display-monitor-name">${monitor.name}</span>
<span class="display-monitor-name">${monitor.name}${primaryBadge}</span>
${detailsHtml}
</div>
${powerBtn}

View File

@@ -10,6 +10,7 @@
"auth.cleared": "Token cleared. Please enter a new token.",
"auth.required": "Please enter a token",
"player.theme": "Toggle theme",
"accent.custom": "Custom",
"player.locale": "Change language",
"player.previous": "Previous",
"player.play": "Play/Pause",
@@ -128,6 +129,7 @@
"display.no_monitors": "No monitors detected",
"display.power_on": "Turn on",
"display.power_off": "Turn off",
"display.primary": "Primary",
"browser.title": "Media Browser",
"browser.home": "Home",
"browser.manage_folders": "Manage Folders",

View File

@@ -10,6 +10,7 @@
"auth.cleared": "Токен очищен. Пожалуйста, введите новый токен.",
"auth.required": "Пожалуйста, введите токен",
"player.theme": "Переключить тему",
"accent.custom": "Свой цвет",
"player.locale": "Изменить язык",
"player.previous": "Предыдущий",
"player.play": "Воспроизвести/Пауза",
@@ -128,6 +129,7 @@
"display.no_monitors": "Мониторы не обнаружены",
"display.power_on": "Включить",
"display.power_off": "Выключить",
"display.primary": "Основной",
"browser.title": "Медиа Браузер",
"browser.home": "Главная",
"browser.manage_folders": "Управление папками",

View File

@@ -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

View 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!"
}