diff --git a/media_server/services/display_service.py b/media_server/services/display_service.py index 3d23193..dbc9bbf 100644 --- a/media_server/services/display_service.py +++ b/media_server/services/display_service.py @@ -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 diff --git a/media_server/static/css/styles.css b/media_server/static/css/styles.css index 19c1a9e..b77dff8 100644 --- a/media_server/static/css/styles.css +++ b/media_server/static/css/styles.css @@ -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); diff --git a/media_server/static/js/app.js b/media_server/static/js/app.js index 1ab196e..4b238ce 100644 --- a/media_server/static/js/app.js +++ b/media_server/static/js/app.js @@ -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 => `
` ).join(''); + + const customRow = ` +
+ + ${t('accent.custom')} + +
`; + + 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 ? `${details}` : ''; + const primaryBadge = monitor.is_primary ? `${t('display.primary')}` : ''; card.innerHTML = `
@@ -2963,7 +2992,7 @@ async function loadDisplayMonitors() {
- ${monitor.name} + ${monitor.name}${primaryBadge} ${detailsHtml}
${powerBtn} diff --git a/media_server/static/locales/en.json b/media_server/static/locales/en.json index ffeb521..8e756e3 100644 --- a/media_server/static/locales/en.json +++ b/media_server/static/locales/en.json @@ -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", diff --git a/media_server/static/locales/ru.json b/media_server/static/locales/ru.json index 22733c2..385f16b 100644 --- a/media_server/static/locales/ru.json +++ b/media_server/static/locales/ru.json @@ -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": "Управление папками", diff --git a/scripts/restart-server.bat b/scripts/restart-server.bat deleted file mode 100644 index 26fb44b..0000000 --- a/scripts/restart-server.bat +++ /dev/null @@ -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 diff --git a/scripts/restart-server.ps1 b/scripts/restart-server.ps1 new file mode 100644 index 0000000..b1add28 --- /dev/null +++ b/scripts/restart-server.ps1 @@ -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!" +}