This commit is contained in:
Mateusz Gruszczyński
2026-08-25 19:05:18 +02:00
parent 3ee93b7225
commit dce9ef7270
14 changed files with 132 additions and 93 deletions
+11 -7
View File
@@ -621,7 +621,9 @@ function deviceCard(device, detailed = false) {
const responseTime = hasResponseTime ? `${Math.max(0, Math.round(Number(device.response_time_ms)))} ms` : '— ms';
const protocol = device.simulated ? tr('devices.simulator') : device.protocol_version === 2 ? 'V2 GCM' : device.protocol_version === 1 ? 'V1 ECB' : tr('devices.protocolAuto');
const networkInfo = `<div class="device-network-info"><small>${esc(device.ip)} · ${esc(responseTime)} · ${esc(protocol)}</small>${device.last_error ? `<small class="device-network-error" title="${esc(device.last_error)}">${esc(device.last_error)}</small>` : ''}</div>`;
return `<article class="device-card ${device.power ? '' : 'off'}" data-device-card="${esc(device.id)}">
const quickClass = detailed ? '' : ' quick-control-card quick-device-control';
const quickRow = detailed ? '' : ' quick-control-row';
return `<article class="device-card${quickClass} ${device.power ? '' : 'off'}" data-device-card="${esc(device.id)}">
<div class="device-head">
<div class="device-title"><h3>${esc(device.name)}</h3><p><span class="status ${device.online ? 'online' : ''}">${esc(tr(device.online ? 'status.online' : 'status.offline'))}</span> · ${esc(device.model || device.mac)}</p></div>
<button class="power-button ${device.power ? 'on' : ''}" data-action="power" data-device="${esc(device.id)}" aria-label="${esc(tr('devices.power'))}">⏻</button>
@@ -632,9 +634,9 @@ function deviceCard(device, detailed = false) {
<button data-action="temperature" data-delta="1" data-device="${esc(device.id)}">+</button>
</div>
<div class="current-line">${esc(tr('devices.currentTemperature'))}: <strong>${fmtTemp(device.current_temperature)}</strong>${device.outdoor_temperature == null ? '' : ` · ${esc(tr('devices.outdoor'))} ${fmtTemp(device.outdoor_temperature)}`}</div>
<div class="mode-row">${modes.map(mode => `<button class="${device.mode === mode ? 'active' : ''}" data-action="mode" data-value="${mode}" data-device="${esc(device.id)}">${esc(modeLabel(mode))}</button>`).join('')}</div>
<div class="fan-row">${fans.map(fan => `<button class="${Number(device.fan_speed) === fan ? 'active' : ''}" data-action="fan" data-value="${fan}" data-device="${esc(device.id)}">${esc(fanLabel(fan))}</button>`).join('')}</div>
<div class="device-toggles">
<div class="mode-row${quickRow}${detailed ? '' : ' quick-control-row-5'}">${modes.map(mode => `<button class="${device.mode === mode ? 'active' : ''}" data-action="mode" data-value="${mode}" data-device="${esc(device.id)}">${esc(modeLabel(mode))}</button>`).join('')}</div>
<div class="fan-row${quickRow}${detailed ? '' : ' quick-control-row-4'}">${fans.map(fan => `<button class="${Number(device.fan_speed) === fan ? 'active' : ''}" data-action="fan" data-value="${fan}" data-device="${esc(device.id)}">${esc(fanLabel(fan))}</button>`).join('')}</div>
<div class="device-toggles${quickRow}${detailed ? '' : ' quick-control-row-3'}">
<button class="${device.swing_vertical ? 'active' : ''}" data-action="toggle" data-field="swing_vertical" data-device="${esc(device.id)}">${esc(tr('devices.swing'))}</button>
${device.supports_quiet === false ? '' : `<button class="${device.quiet ? 'active' : ''}" data-action="toggle" data-field="quiet" data-device="${esc(device.id)}">${esc(tr('devices.quiet'))}</button>`}
${device.supports_turbo === false ? '' : `<button class="${device.turbo ? 'active' : ''}" data-action="toggle" data-field="turbo" data-device="${esc(device.id)}">${esc(tr('devices.turbo'))}</button>`}
@@ -691,13 +693,15 @@ function zoneCard(zone, detailed = true) {
const enabledControl = detailed
? `<span class="badge ${zone.enabled ? 'active' : ''}">${esc(state)}</span>`
: `<button type="button" class="zone-enable-toggle ${zone.enabled ? 'active' : ''}" data-action="zone-enabled" data-id="${esc(zone.id)}" data-value="${zone.enabled ? 'false' : 'true'}" aria-label="${esc(tr(zone.enabled ? 'zones.disable' : 'zones.enable'))}"><span>${zone.enabled ? '✓' : '○'}</span>${esc(state)}</button>`;
return `<article class="list-card zone-thermostat ${zone.demand ? 'demanding' : ''} ${zone.enabled ? '' : 'zone-disabled'}">
const quickClass = detailed ? '' : ' quick-control-card quick-thermostat-control';
const quickRow = detailed ? '' : ' quick-control-row';
return `<article class="list-card zone-thermostat${quickClass} ${zone.demand ? 'demanding' : ''} ${zone.enabled ? '' : 'zone-disabled'}">
<div class="list-card-head"><div><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(zonePresetLabel(zone.active_preset))}</p></div>${enabledControl}</div>
<div class="thermostat-main"><div><small>${esc(tr('zones.measurement'))}</small><strong>${fmtTemp(roomTemperature)}</strong></div><div class="temperature-control compact"><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="-0.5"></button><div class="target-temp compact">${Number.isFinite(target)?target.toFixed(1):'--'}<small>°C</small></div><button data-action="zone-temperature" data-id="${esc(zone.id)}" data-delta="0.5">+</button></div><div><small>${esc(tr('zones.deviceTarget'))}</small><strong>${fmtTemp(zone.device_setpoint)}</strong></div></div>
<div class="preset-row">
<div class="preset-row${quickRow}${detailed ? '' : ' quick-control-row-4'}">
${['auto','comfort','sleep','away'].map(preset=>`<button class="${manual===preset?'active':''}" data-action="zone-preset" data-id="${esc(zone.id)}" data-value="${preset}">${esc(preset==='sleep'?tr('zones.sleepNow'):zonePresetLabel(preset))}</button>`).join('')}
</div>
<div class="mode-row zone-mode-row"><button class="${mode==='house'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="house">${esc(tr('zones.followHouseShort'))}</button><button class="${mode==='heat'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="heat">${esc(modeLabel('heat'))}</button><button class="${mode==='cool'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="cool">${esc(modeLabel('cool'))}</button></div>
<div class="mode-row zone-mode-row${quickRow}${detailed ? '' : ' quick-control-row-3'}"><button class="${mode==='house'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="house">${esc(tr('zones.followHouseShort'))}</button><button class="${mode==='heat'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="heat">${esc(modeLabel('heat'))}</button><button class="${mode==='cool'?'active':''}" data-action="zone-mode" data-id="${esc(zone.id)}" data-value="cool">${esc(modeLabel('cool'))}</button></div>
<div class="zone-state-line"><span>${zone.demand ? esc(tr('zones.requesting')) : esc(tr('zones.satisfied'))}</span><span>${esc(override)}</span></div>
${detailed ? `<div class="sensor-detail">${esc(sensorDetails)}</div><div class="card-footer"><small>${esc(tr('zones.quickHintSmart'))}</small><div class="card-menu"><button data-action="edit-zone" data-id="${esc(zone.id)}">${esc(tr('actions.edit'))}</button><button class="danger" data-action="delete-zone" data-id="${esc(zone.id)}">${esc(tr('actions.delete'))}</button></div></div>` : ''}
</article>`;
+2 -2
View File
@@ -375,8 +375,8 @@
<label><span data-i18n="common.name">Name</span><input name="name" required data-i18n-placeholder="placeholder.livingRoom" placeholder="Living room"></label>
<label><span data-i18n="common.device">Device</span><select name="device_id" required></select></label>
<div class="copy-settings-row"><select id="copyZoneSource"><option value="" data-i18n="zones.copyFrom">Copy thermostat settings from…</option></select><button type="button" class="secondary" id="copyZoneSettings" data-i18n="zones.copySettings">Copy settings</button></div>
<div class="two"><label><span data-i18n="zones.modePolicy">Mode policy</span><select name="mode_policy"><option value="house" data-i18n="zones.followHouse">Follow house mode</option><option value="cool" data-i18n="mode.cool">Cooling only</option><option value="heat" data-i18n="mode.heat">Heating only</option></select></label><label><span data-i18n="common.targetC">Manual target °C</span><input type="text" inputmode="decimal" step="0.5" min="8" max="30" name="setpoint" value="23"></label></div>
<p class="field-note" data-i18n="zones.modePolicyHint">If this zone should inherit the mode selected globally in the app, leave “Follow house mode” unchanged. Cooling only / Heating only forces a fixed mode for this zone.</p>
<div class="two"><label><span data-i18n="zones.modePolicy">Mode policy</span><select name="mode_policy"><option value="house" data-i18n="zones.followHouse">Follow global mode</option><option value="cool" data-i18n="mode.cool">Cooling only</option><option value="heat" data-i18n="mode.heat">Heating only</option></select></label><label><span data-i18n="common.targetC">Manual target °C</span><input type="text" inputmode="decimal" step="0.5" min="8" max="30" name="setpoint" value="23"></label></div>
<p class="field-note" data-i18n="zones.modePolicyHint">If this zone should use the global Heating/Cooling mode selected in GREE Controller, leave “Follow global mode” unchanged. Cooling only / Heating only forces a fixed mode for this zone.</p>
<details class="profile-editor"><summary data-i18n="zones.profileTemperatures">Comfort / sleep / away temperatures</summary>
<div class="profile-grid"><strong data-i18n="mode.cool">Cooling</strong><label><span data-i18n="preset.comfort">Comfort</span><input type="text" inputmode="decimal" step="0.5" name="cool_comfort_setpoint" value="23"></label><label><span data-i18n="preset.sleep">Sleep</span><input type="text" inputmode="decimal" step="0.5" name="cool_sleep_setpoint" value="24.5"></label><label><span data-i18n="preset.away">Away</span><input type="text" inputmode="decimal" step="0.5" name="cool_away_setpoint" value="27"></label></div>
<div class="profile-grid"><strong data-i18n="mode.heat">Heating</strong><label><span data-i18n="preset.comfort">Comfort</span><input type="text" inputmode="decimal" step="0.5" name="heat_comfort_setpoint" value="21"></label><label><span data-i18n="preset.sleep">Sleep</span><input type="text" inputmode="decimal" step="0.5" name="heat_sleep_setpoint" value="19"></label><label><span data-i18n="preset.away">Away</span><input type="text" inputmode="decimal" step="0.5" name="heat_away_setpoint" value="17"></label></div>
+51 -46
View File
@@ -732,19 +732,41 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
.dashboard-disclosure-body .device-grid,
.dashboard-disclosure-body .list-grid,
.dashboard-disclosure-body .automation-plan-grid { gap:10px; grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr)); }
#dashboardZones .list-card, #dashboardDevices .device-card { padding:15px; }
#dashboardZones .preset-row,
#dashboardZones .mode-row,
#dashboardDevices .mode-row,
#dashboardDevices .fan-row { display:grid; grid-template-columns:repeat(auto-fit,minmax(70px,1fr)); gap:5px; overflow:visible; padding:2px 0 6px; }
#dashboardZones .preset-row button,
#dashboardZones .mode-row button,
#dashboardDevices .mode-row button,
#dashboardDevices .fan-row button { min-width:0; min-height:32px; padding:6px 7px; font-size:10px; }
#dashboardDevices .temperature-control { gap:16px; padding:18px 0 14px; }
#dashboardDevices .target-temp { min-width:92px; font-size:40px; }
#dashboardDevices .current-line { margin:-6px 0 13px; }
#dashboardZones .thermostat-main { margin:12px 0 8px; }
/* Dashboard quick controls share one compact visual language. */
.quick-control-card {
--quick-control-height:28px;
--quick-control-gap:4px;
padding:14px;
}
.quick-control-card .quick-control-row {
display:grid;
gap:var(--quick-control-gap);
overflow:visible;
padding:2px 0 5px;
}
.quick-control-card .quick-control-row-3 { grid-template-columns:repeat(3,minmax(0,1fr)); }
.quick-control-card .quick-control-row-4 { grid-template-columns:repeat(4,minmax(0,1fr)); }
.quick-control-card .quick-control-row-5 { grid-template-columns:repeat(5,minmax(0,1fr)); }
.quick-control-card .quick-control-row button {
min-width:0;
min-height:var(--quick-control-height);
padding:4px 5px;
font-size:9px;
line-height:1.05;
white-space:nowrap;
}
.quick-control-card .power-button { width:38px; height:38px; font-size:18px; }
.quick-control-card .temperature-control button { width:34px; height:34px; font-size:18px; }
.quick-control-card .target-temp { min-width:80px; font-size:32px; }
.quick-control-card .device-title h3,
.quick-control-card .list-card-head h3 { font-size:16px; }
.quick-control-card .device-title p,
.quick-control-card .list-card-head p { font-size:11px; }
.quick-device-control .temperature-control { gap:12px; padding:12px 0 10px; }
.quick-device-control .current-line { margin:-3px 0 10px; font-size:11px; }
.quick-device-control .device-toggles.quick-control-row { margin-top:1px; }
.quick-thermostat-control .thermostat-main { margin:8px 0 7px; }
.quick-thermostat-control .zone-enable-toggle { min-height:28px; padding:4px 8px; font-size:10px; }
@media (max-width: 700px) {
[data-view="dashboard"] .hero { min-height:145px; padding:20px; }
@@ -785,26 +807,26 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
/* Quick thermostat: target control gets its own row, readings no longer collide with it. */
#dashboardZones { grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr)); }
#dashboardZones .zone-thermostat { min-width:0; }
#dashboardZones .thermostat-main {
.quick-thermostat-control { min-width:0; }
.quick-thermostat-control .thermostat-main {
grid-template-columns:minmax(0,1fr) minmax(0,1fr);
grid-template-areas:"control control" "room device";
gap:5px 10px;
margin:8px 0 7px;
}
#dashboardZones .thermostat-main > div:first-child { grid-area:room; min-width:0; text-align:left; }
#dashboardZones .thermostat-main > .temperature-control { grid-area:control; }
#dashboardZones .thermostat-main > div:last-child { grid-area:device; min-width:0; text-align:right; }
#dashboardZones .thermostat-main small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
#dashboardZones .thermostat-main strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:14px; }
#dashboardZones .thermostat-main .temperature-control.compact { gap:12px; padding:0 0 2px; }
#dashboardZones .thermostat-main .temperature-control.compact button { width:36px; height:36px; font-size:18px; }
#dashboardZones .thermostat-main .target-temp.compact { min-width:80px; font-size:30px; }
#dashboardZones .list-card-head h3 { font-size:16px; }
#dashboardZones .list-card-head p { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:11px; }
#dashboardZones .zone-enable-toggle { flex:0 0 auto; }
#dashboardZones .zone-state-line { gap:8px; font-size:10px; }
#dashboardZones .zone-state-line span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.quick-thermostat-control .thermostat-main > div:first-child { grid-area:room; min-width:0; text-align:left; }
.quick-thermostat-control .thermostat-main > .temperature-control { grid-area:control; }
.quick-thermostat-control .thermostat-main > div:last-child { grid-area:device; min-width:0; text-align:right; }
.quick-thermostat-control .thermostat-main small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.quick-thermostat-control .thermostat-main strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:14px; }
.quick-thermostat-control .thermostat-main .temperature-control.compact { gap:12px; padding:0 0 2px; }
.quick-thermostat-control .thermostat-main .temperature-control.compact button { width:34px; height:34px; font-size:18px; }
.quick-thermostat-control .thermostat-main .target-temp.compact { min-width:80px; font-size:32px; }
.quick-thermostat-control .list-card-head h3 { font-size:16px; }
.quick-thermostat-control .list-card-head p { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:11px; }
.quick-thermostat-control .zone-enable-toggle { flex:0 0 auto; }
.quick-thermostat-control .zone-state-line { gap:8px; font-size:10px; }
.quick-thermostat-control .zone-state-line span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* Smaller Node-RED/simulator-like plan blocks with explicit group context. */
#controlPlan.automation-plan-grid { grid-template-columns:repeat(auto-fit,minmax(min(100%,210px),1fr)); gap:8px; }
@@ -854,21 +876,4 @@ body.simulation-standalone [data-view="simulation"] { min-height:calc(100vh - 36
}
#zoneList .zone-thermostat .card-menu { flex:0 0 auto; }
@media (min-width: 701px) {
#dashboardDevices .mode-row {
grid-template-columns:repeat(5,minmax(0,1fr));
gap:4px;
}
#dashboardDevices .fan-row {
grid-template-columns:repeat(4,minmax(0,1fr));
gap:4px;
}
#dashboardDevices .mode-row button,
#dashboardDevices .fan-row button {
min-height:26px;
padding:4px 3px;
font-size:9px;
line-height:1.05;
white-space:nowrap;
}
}
/* 0.7.1: quick thermostats and quick manual control now use the shared quick-control classes above. */
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'gree-controller-v070-schedule-audit-ui';
const CACHE = 'gree-controller-v071-quick-controls';
const SCOPE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
const path = value => `${SCOPE}${value.startsWith('/') ? value : `/${value}`}` || '/';
const ASSETS = [path('/'), path('/styles.css'), path('/app.js'), path('/theme-init.js'), path('/favicon.svg'), path('/manifest.webmanifest'), path('/lang/index.json'), path('/lang/en.json')];