v0.14.18
This commit is contained in:
+6
-6
@@ -191,7 +191,7 @@ function setChartSeriesHidden(chartId, item, index, hidden) {
|
||||
|
||||
function updateChartZoomControls(id) {
|
||||
const canvas = document.getElementById(id);
|
||||
const card = canvas?.closest('.history-chart-card');
|
||||
const card = canvas?.closest('.chart-card');
|
||||
if (!card) return;
|
||||
const zoom = clamp(Number(app.chartZooms[id] || 1), 1, MAX_CHART_ZOOM);
|
||||
const reset = card.querySelector('[data-chart-zoom="reset"]');
|
||||
@@ -319,11 +319,11 @@ function closeChartPreview(card) {
|
||||
function toggleChartFullscreen(id) {
|
||||
if (!window.matchMedia('(min-width: 761px)').matches) return;
|
||||
const canvas = document.getElementById(id);
|
||||
const card = canvas?.closest('.history-chart-card');
|
||||
const card = canvas?.closest('.chart-card');
|
||||
if (!card) return;
|
||||
|
||||
const active = chartCardIsFullscreen(card);
|
||||
const opened = document.querySelector('.history-chart-card.chart-fullscreen-fallback');
|
||||
const opened = document.querySelector('.chart-card.chart-fullscreen-fallback');
|
||||
if (opened && opened !== card) closeChartPreview(opened);
|
||||
|
||||
if (active) {
|
||||
@@ -356,7 +356,7 @@ function prepareCanvas(canvas, height) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const wrap = canvas.parentElement;
|
||||
const wrapWidth = Math.floor(wrap?.clientWidth || rect.width || 720);
|
||||
const fullscreen = chartCardIsFullscreen(canvas.closest('.history-chart-card'));
|
||||
const fullscreen = chartCardIsFullscreen(canvas.closest('.chart-card'));
|
||||
const wrapHeight = fullscreen ? Math.floor(wrap?.clientHeight || 0) : 0;
|
||||
const actualHeight = wrapHeight || height;
|
||||
const zoom = clamp(Number(canvas.dataset.chartZoom || 1), 1, MAX_CHART_ZOOM);
|
||||
@@ -724,7 +724,7 @@ function renderLegend(host, series) {
|
||||
function historyChartMarkup(id, title, hint, compact = false) {
|
||||
const zoom = clamp(Number(app.chartZooms[id] || 1), 1, MAX_CHART_ZOOM);
|
||||
const percent = Math.round(zoom * 100);
|
||||
return `<div class="panel chart-panel history-chart-card"><div class="chart-title-row"><div><h3>${esc(title)}</h3><p>${esc(hint || '')}</p><small class="chart-hover-hint">${esc(tr('history.hoverHint'))}</small></div><div class="chart-title-actions"><div class="chart-zoom-controls" aria-label="${esc(tr('history.zoomControls'))}"><button type="button" data-chart-zoom="out" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomOut'))}" ${zoom <= 1 ? 'disabled' : ''}>${uiIcon('minus')}</button><button type="button" class="chart-zoom-reset" data-chart-zoom="reset" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomReset'))}">${percent}%</button><button type="button" data-chart-zoom="in" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomIn'))}" ${zoom >= MAX_CHART_ZOOM ? 'disabled' : ''}>${uiIcon('plus')}</button></div><button type="button" class="chart-fullscreen-button" data-chart-fullscreen="${esc(id)}" title="${esc(tr('history.fullscreen'))}" aria-label="${esc(tr('history.fullscreen'))}">${uiIcon('fullscreen')}</button></div></div><div class="chart-wrap ${compact ? 'compact-chart' : ''}"><canvas id="${esc(id)}" data-chart-zoom="${zoom}" width="1000" height="${compact ? 300 : 420}" tabindex="0" aria-label="${esc(title)}"></canvas><div class="chart-hover-line" hidden></div><div class="chart-selection" hidden></div><div class="chart-tooltip" hidden></div></div><div class="legend" id="${esc(id)}Legend"></div></div>`;
|
||||
return `<div class="panel chart-panel chart-card"><div class="chart-title-row"><div><h3>${esc(title)}</h3><p>${esc(hint || '')}</p><small class="chart-hover-hint">${esc(tr('history.hoverHint'))}</small></div><div class="chart-title-actions"><div class="chart-zoom-controls" aria-label="${esc(tr('history.zoomControls'))}"><button type="button" data-chart-zoom="out" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomOut'))}" ${zoom <= 1 ? 'disabled' : ''}>${uiIcon('minus')}</button><button type="button" class="chart-zoom-reset" data-chart-zoom="reset" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomReset'))}">${percent}%</button><button type="button" data-chart-zoom="in" data-chart-id="${esc(id)}" title="${esc(tr('history.zoomIn'))}" ${zoom >= MAX_CHART_ZOOM ? 'disabled' : ''}>${uiIcon('plus')}</button></div><button type="button" class="chart-fullscreen-button" data-chart-fullscreen="${esc(id)}" title="${esc(tr('history.fullscreen'))}" aria-label="${esc(tr('history.fullscreen'))}">${uiIcon('fullscreen')}</button></div></div><div class="chart-wrap ${compact ? 'compact-chart' : ''}"><canvas id="${esc(id)}" data-chart-zoom="${zoom}" width="1000" height="${compact ? 300 : 420}" tabindex="0" aria-label="${esc(title)}"></canvas><div class="chart-hover-line" hidden></div><div class="chart-selection" hidden></div><div class="chart-tooltip" hidden></div></div><div class="legend" id="${esc(id)}Legend"></div></div>`;
|
||||
}
|
||||
|
||||
function historySeriesColor(index) {
|
||||
@@ -733,6 +733,6 @@ function historySeriesColor(index) {
|
||||
|
||||
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape') closeChartPreview(document.querySelector('.history-chart-card.chart-fullscreen-fallback'));
|
||||
if (event.key === 'Escape') closeChartPreview(document.querySelector('.chart-card.chart-fullscreen-fallback'));
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ const app = {
|
||||
languages: [], translations: {}, locales: {},
|
||||
historyTab: 'overview', historyData: { zones: [], devices: [], sensors: [] }, historyCounts: {},
|
||||
historyZone: 'all', historyDevice: 'all', historySensor: 'all', historyNetworkTarget: 'all', historyNetworkShowJitter: true, historyNetwork: [], historyNetworkTargets: [], historyEnergyDevice: '', historyEnergyTargets: [], historyEnergyInterval: 'daily', historyEnergyCompare: 'none', historyEnergy: [], historyLoading: false, historyReloadPending: false,
|
||||
customChartSeries: [], savedCharts: [], chartZooms: {}, chartHiddenSeries: {}, zoneControlSeq: {}, groupControlSeq: {}, zoneControlQueue: {}, groupControlQueue: {}, deviceControlQueue: {}, houseControlQueue: null, climateControlQueue: null, groupCustomDrafts: {}, zoneTemperatureTimers: {}, deviceTemperatureDrafts: {},
|
||||
customChartSeries: [], savedCharts: [], customChartEditingId: null, customChartNameDraft: null, chartZooms: {}, chartHiddenSeries: {}, zoneControlSeq: {}, groupControlSeq: {}, zoneControlQueue: {}, groupControlQueue: {}, deviceControlQueue: {}, houseControlQueue: null, climateControlQueue: null, groupCustomDrafts: {}, zoneTemperatureTimers: {}, deviceTemperatureDrafts: {},
|
||||
controlPlan: null, controlPlanRevision: null, controlPlanPushReady: false, controlPlanTimer: null, debugLines: [], debugBacklogLoaded: false, debugFilter: 'all', sensorAliases: {}, flowSharedInputs: [], flowSharedInputValueCache: {}, flowSharedInputValueRequests: {}, flowSharedInputValueTimer: null,
|
||||
haManualFallbackVisible: false, haSupervisorTestState: null, haEntityCatalog: [], haEntityCatalogConfigured: false, haEntityCatalogLoadedAt: 0, haEntityCatalogLoading: false,
|
||||
simulationScope: 'units', simulationTarget: 'all', standaloneSimulation: false, dashboardTab: 'main', settingsTab: 'app', systemSnapshotAt: Date.now(),
|
||||
|
||||
+1
-1
@@ -377,7 +377,7 @@ function renderSimulationPage() {
|
||||
return `<path class="flow-link ${cls}" d="M ${ax} ${ay} C ${ax + bend} ${ay}, ${bx - bend} ${by}, ${bx} ${by}"/>`;
|
||||
};
|
||||
const verticalLink = (ax, ay, bx, by, cls = 'bus') => `<path class="flow-link ${cls}" d="M ${ax} ${ay} C ${ax} ${ay + 35}, ${bx} ${by - 35}, ${bx} ${by}"/>`;
|
||||
const node = ({ left, top, kind, eyebrow, title, value, meta = '', badge = '', badgeIcon = '', badgeClass = '' }) => `<article class="flow-node ${kind}" style="left:${left}px;top:${top}px;width:${nodeW}px;min-height:${nodeH}px"><div class="flow-node-top"><span>${esc(eyebrow)}</span>${badge || badgeIcon ? `<b class="flow-node-badge ${badgeClass}">${badgeIcon ? uiIcon(badgeIcon) : esc(badge)}</b>` : ''}</div><h3>${esc(title)}</h3><strong>${esc(value)}</strong>${meta ? `<p>${esc(meta)}</p>` : ''}<i class="flow-port in"></i><i class="flow-port out"></i></article>`;
|
||||
const node = ({ left, top, kind, eyebrow, title, value, meta = '', badge = '', badgeIcon = '', badgeClass = '' }) => `<article class="diagram-node ${kind}" style="left:${left}px;top:${top}px;width:${nodeW}px;min-height:${nodeH}px"><div class="flow-node-top"><span>${esc(eyebrow)}</span>${badge || badgeIcon ? `<b class="flow-node-badge ${badgeClass}">${badgeIcon ? uiIcon(badgeIcon) : esc(badge)}</b>` : ''}</div><h3>${esc(title)}</h3><strong>${esc(value)}</strong>${meta ? `<p>${esc(meta)}</p>` : ''}<i class="diagram-port in"></i><i class="diagram-port out"></i></article>`;
|
||||
|
||||
const globalHouseLeft = 305, globalNightLeft = 565, globalTop = 28;
|
||||
nodes.push(node({ left: globalHouseLeft, top: globalTop, kind: 'logic global', eyebrow: tr('simulation.globalInput'), title: tr('simulation.houseMode'), value: houseModeLabel(plan.house_mode || 'off'), meta: tr('simulation.strategy', { strategy: (plan.control_strategy || 'setpoint') === 'setpoint' ? tr('plan.strategySetpoint') : (plan.control_strategy || 'setpoint') }), badge: tr('simulation.house') }));
|
||||
|
||||
+1
-1
@@ -537,7 +537,7 @@ function zoneCard(zone, detailed = true) {
|
||||
const groupText = groupNames.length ? groupNames.join(' · ') : tr('zones.noGroup');
|
||||
const policy = zone.inherit_house_mode ? tr('zones.followHouse') : tr(zone.mode === 'heat' ? 'zones.heatOnly' : 'zones.coolOnly');
|
||||
return `<article class="list-card zone-config-card ${workingDemand ? 'demanding' : ''} ${waitingDemand ? 'lockout-waiting' : ''} ${zone.enabled ? '' : 'zone-disabled'} ${deviceManualOverride ? 'manual-takeover' : ''} ${controlGroup ? 'group-controlled' : ''}" data-zone-config="${esc(zone.id)}"${groupControlStyle(visualGroup)}>
|
||||
<div class="list-card-head"><div><span class="eyebrow">${esc(tr('zones.configuration'))}</span><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(tr('groups.group'))}: ${esc(groupText)}</p></div><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'))}" title="${esc(tr('zones.automationToggleHint'))}"><span>${uiIcon(zone.enabled ? 'check' : 'circle')}</span>${esc(state)}</button></div>
|
||||
<div class="list-card-head"><div><span class="eyebrow">${esc(tr('zones.configuration'))}</span><h3>${esc(zone.name)}</h3><p>${esc(device?.name || tr('common.noDevice'))} · ${esc(tr('groups.group'))}: ${esc(groupText)}</p></div><button type="button" class="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'))}" title="${esc(tr('zones.automationToggleHint'))}"><span>${uiIcon(zone.enabled ? 'check' : 'circle')}</span>${esc(state)}</button></div>
|
||||
<div class="zone-config-status">
|
||||
<div class="zone-config-temperature"><small>${esc(tr('zones.currentStatus'))}</small><div><span>${fmtTemp(roomTemperature)}</span><b>→</b><strong>${Number.isFinite(target) ? `${target.toFixed(1)}°C` : '—'}</strong></div></div>
|
||||
<div class="zone-config-runtime"><span class="badge ${workingDemand ? 'active' : ''}">${esc(zoneRuntimeStatusLabel(zone, effectiveMode, device))}</span><span>${esc(houseModeLabel(effectiveMode))} · ${esc(zonePresetLabel(displayPreset))}</span><small>${esc(override)}</small><small><strong>${esc(tr('zones.controlOwner'))}:</strong> ${esc(zoneControlOwnerLabel(zone))}${zoneControlOwnerMeta(zone) ? ` · ${esc(zoneControlOwnerMeta(zone))}` : ''}</small></div>
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ document.addEventListener('click', async event => {
|
||||
if (hours && $('#historyHours')) $('#historyHours').value = hours;
|
||||
else if ($('#historyHours')) $('#historyHours').value = '24';
|
||||
const dialog = historyRoute.closest('dialog');
|
||||
closeChartPreview(dialog?.querySelector('.history-chart-card.chart-fullscreen-fallback'));
|
||||
closeChartPreview(dialog?.querySelector('.chart-card.chart-fullscreen-fallback'));
|
||||
if (dialog?.open) dialog.close();
|
||||
app.historyTab = 'overview';
|
||||
showView('history');
|
||||
|
||||
+5
-5
@@ -98,7 +98,7 @@ function renderFlows() {
|
||||
const host = $('#flowList'); if (!host) return;
|
||||
const count = $('#flowListCount'); if (count) count.textContent = tr('flow.listCount', { count: app.flows.length });
|
||||
host.innerHTML = app.flows.length ? app.flows.map(flow => `<article class="list-card flow-card ${flow.enabled && !flow.draft ? '' : 'is-disabled'} ${flow.draft ? 'is-draft' : ''}" data-flow-card-id="${esc(flow.id)}" tabindex="0" aria-label="${esc(`${tr('flow.openEditor')}: ${flow.name}`)}">
|
||||
<div class="list-card-head"><div><h3>${esc(flow.name)}${flow.draft ? ` <span class="badge flow-draft-badge">${esc(tr('flow.draft'))}</span>` : ''}</h3><p>${esc(flow.description || tr('flow.defaultDescription'))}</p></div>${flow.draft ? `<button type="button" class="zone-enable-toggle" disabled title="${esc(tr('flow.draftDisabledHint'))}"><span>${uiIcon('edit')}</span>${esc(tr('flow.disabledState'))}</button>` : `<button type="button" class="zone-enable-toggle ${flow.enabled ? 'active' : ''}" data-action="toggle-flow-enabled" data-id="${esc(flow.id)}" data-value="${flow.enabled ? 'false' : 'true'}" aria-label="${esc(tr(flow.enabled ? 'flow.disable' : 'flow.enable'))}" title="${esc(tr('flow.quickToggleHint'))}"><span>${uiIcon(flow.enabled ? 'check' : 'circle')}</span>${esc(flow.enabled ? tr('flow.enabledState') : tr('flow.disabledState'))}</button>`}</div>
|
||||
<div class="list-card-head"><div><h3>${esc(flow.name)}${flow.draft ? ` <span class="badge flow-draft-badge">${esc(tr('flow.draft'))}</span>` : ''}</h3><p>${esc(flow.description || tr('flow.defaultDescription'))}</p></div>${flow.draft ? `<button type="button" class="enable-toggle" disabled title="${esc(tr('flow.draftDisabledHint'))}"><span>${uiIcon('edit')}</span>${esc(tr('flow.disabledState'))}</button>` : `<button type="button" class="enable-toggle ${flow.enabled ? 'active' : ''}" data-action="toggle-flow-enabled" data-id="${esc(flow.id)}" data-value="${flow.enabled ? 'false' : 'true'}" aria-label="${esc(tr(flow.enabled ? 'flow.disable' : 'flow.enable'))}" title="${esc(tr('flow.quickToggleHint'))}"><span>${uiIcon(flow.enabled ? 'check' : 'circle')}</span>${esc(flow.enabled ? tr('flow.enabledState') : tr('flow.disabledState'))}</button>`}</div>
|
||||
<div class="card-stats"><div class="card-stat"><small>${esc(tr('flow.blocks'))}</small><strong>${flow.nodes?.length || 0}</strong></div><div class="card-stat"><small>${esc(tr('nav.schedules'))}</small><strong>${flow.compiled_schedule_ids?.length || 0}</strong></div><div class="card-stat"><small>${esc(tr('nav.automations'))}</small><strong>${flow.compiled_automation_ids?.length || 0}</strong></div></div>
|
||||
<div class="card-footer"><small>${esc(flow.draft ? tr('flow.draftNoExecution') : tr('flow.compileCount', { schedules: flow.compiled_schedule_ids?.length || 0, automations: flow.compiled_automation_ids?.length || 0 }))}</small><div class="card-menu"><button class="card-primary-action" data-action="edit-flow" data-id="${esc(flow.id)}">${esc(tr('flow.openEditor'))}</button><details class="card-overflow"><summary aria-label="${esc(tr('actions.more'))}" title="${esc(tr('actions.more'))}">${uiIcon('more-horizontal')}</summary><div class="card-overflow-menu"><button class="danger" data-action="delete-flow" data-id="${esc(flow.id)}">${esc(tr('actions.delete'))}</button></div></details></div></div>
|
||||
</article>`).join('') : `<div class="empty"><strong>${esc(tr('flow.emptyTitle'))}</strong>${esc(tr('flow.emptyText'))}</div>`;
|
||||
@@ -426,7 +426,7 @@ function renderFlowBlockLibrary(filter = '') {
|
||||
const title = group.querySelector(':scope > span')?.textContent?.trim() || '';
|
||||
const buttons = $$('[data-flow-add]', group).filter(button => !query || `${title} ${button.textContent}`.toLocaleLowerCase(locale()).includes(query));
|
||||
if (!buttons.length) return '';
|
||||
const categoryClass = [...group.classList].find(name => name.startsWith('flow-palette-') && name !== 'flow-palette-group') || '';
|
||||
const categoryClass = [...group.classList].find(name => name.startsWith('block-category-')) || '';
|
||||
return `<section class="flow-block-library-group ${esc(categoryClass)}"><h3>${esc(title)}</h3><div>${buttons.map(button => `<button type="button" class="secondary" data-flow-add="${esc(button.dataset.flowAdd)}">${esc(button.textContent.trim())}</button>`).join('')}</div></section>`;
|
||||
}).join('') || `<div class="empty compact"><strong>${esc(tr('flow.noBlocksFound'))}</strong><span>${esc(tr('flow.noBlocksFoundHint'))}</span></div>`;
|
||||
}
|
||||
@@ -443,11 +443,11 @@ function renderFlowEditor() {
|
||||
const nodesHost = $('#flowNodes');
|
||||
nodesHost.innerHTML = draft.nodes.map(node => {
|
||||
const meta = FLOW_NODE_META[node.kind] || { title: node.kind, category: 'logic' };
|
||||
return `<article class="flow-node flow-node-${esc(meta.category)} ${(app.flowSelectedNodeIds || []).includes(node.id) ? 'selected' : ''}" data-flow-node="${esc(node.id)}" style="left:${Number(node.x || 0)}px;top:${Number(node.y || 0)}px">
|
||||
<button class="flow-port flow-port-in" type="button" data-flow-input="${esc(node.id)}" title="${esc(tr('flow.connectHere'))}"></button>
|
||||
return `<article class="diagram-node flow-node-${esc(meta.category)} ${(app.flowSelectedNodeIds || []).includes(node.id) ? 'selected' : ''}" data-flow-node="${esc(node.id)}" style="left:${Number(node.x || 0)}px;top:${Number(node.y || 0)}px">
|
||||
<button class="diagram-port flow-port-in" type="button" data-flow-input="${esc(node.id)}" title="${esc(tr('flow.connectHere'))}"></button>
|
||||
<div class="flow-node-head"><span>${esc(flowNodeTitle(meta))}</span><button type="button" data-flow-remove="${esc(node.id)}" aria-label="${esc(tr('actions.delete'))}">${uiIcon('close')}</button></div>
|
||||
<div class="flow-node-body">${esc(flowNodeSummary(node))}${node.kind === 'shared_input' && node.config?.input_id ? `<div class="flow-node-current" data-flow-shared-current="${esc(node.config.input_id)}"><span>${esc(tr('flow.sharedInputTestCurrent'))}</span><strong>—</strong></div>` : ''}</div>
|
||||
<button class="flow-port flow-port-out ${app.flowConnectFrom === node.id ? 'armed' : ''}" type="button" data-flow-output="${esc(node.id)}" title="${esc(tr('flow.startConnection'))}"></button>
|
||||
<button class="diagram-port flow-port-out ${app.flowConnectFrom === node.id ? 'armed' : ''}" type="button" data-flow-output="${esc(node.id)}" title="${esc(tr('flow.startConnection'))}"></button>
|
||||
</article>`;
|
||||
}).join('');
|
||||
$('#flowEmptyHint').hidden = draft.nodes.length > 0;
|
||||
|
||||
+3
-3
@@ -66,7 +66,7 @@ function showFormError(form, message) {
|
||||
summary.className = 'inline-alert error form-error-summary';
|
||||
summary.setAttribute('role', 'alert');
|
||||
summary.innerHTML = `<span>${esc(tr('devices.lastError'))}</span><strong>${esc(message)}</strong>`;
|
||||
const anchor = $('.settings-save-bar', form) || $('.form-actions', form);
|
||||
const anchor = $('.sticky-form-actions', form) || $('.form-actions', form);
|
||||
if (anchor?.parentElement) anchor.parentElement.insertBefore(summary, anchor);
|
||||
else form.appendChild(summary);
|
||||
}
|
||||
@@ -259,7 +259,7 @@ function confirmDiscardForm(form) {
|
||||
function activeDirtySettingsForm() {
|
||||
const active = $('.view.active');
|
||||
if (!active) return null;
|
||||
const form = $('form.settings-form', active);
|
||||
const form = $('form.config-form', active);
|
||||
return form && isFormDirty(form) ? form : null;
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ function requestDialogClose(dialog) {
|
||||
if (!dialog) return true;
|
||||
const form = $('form', dialog);
|
||||
if (form && !confirmDiscardForm(form)) return false;
|
||||
closeChartPreview(dialog.querySelector?.('.history-chart-card.chart-fullscreen-fallback'));
|
||||
closeChartPreview(dialog.querySelector?.('.chart-card.chart-fullscreen-fallback'));
|
||||
dialog.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
+38
-4
@@ -272,17 +272,31 @@ function persistSavedCharts() {
|
||||
localStorage.setItem('gree_controller_saved_charts', JSON.stringify(app.savedCharts.slice(0, 30)));
|
||||
}
|
||||
|
||||
function savedChartRangeLabel(hours) {
|
||||
const value = Number(hours || 24);
|
||||
if (value === 168) return tr('history.range7d');
|
||||
if (value === 720) return tr('history.range30d');
|
||||
if (value === 2160) return tr('history.range90d');
|
||||
if (value === 8760) return tr('history.range1y');
|
||||
return `${value} h`;
|
||||
}
|
||||
|
||||
function renderCustomBuilder() {
|
||||
const host = $('#historyCustomBuilder'); if (!host) return;
|
||||
if (app.historyTab !== 'custom') { host.innerHTML = ''; host.classList.remove('active'); return; }
|
||||
host.classList.add('active');
|
||||
const options = customSeriesOptions();
|
||||
const selected = app.customChartSeries.map((key, index) => customSeriesDefinition(key, index)).filter(Boolean);
|
||||
const editing = app.savedCharts.find(item => item.id === app.customChartEditingId) || null;
|
||||
const draftName = app.customChartNameDraft !== null ? app.customChartNameDraft : (editing?.name || '');
|
||||
const editNote = editing ? `<span class="custom-chart-edit-note">${esc(tr('history.editingChart'))}: <strong>${esc(editing.name)}</strong></span>` : '';
|
||||
const cancelEdit = editing ? `<button class="secondary" data-history-action="cancel-chart-edit">${esc(tr('actions.cancel'))}</button>` : '';
|
||||
host.innerHTML = `<div class="panel custom-chart-panel"><div class="chart-title-row"><div><h3>${esc(tr('history.customTitle'))}</h3><p>${esc(tr('history.customDescription'))}</p></div></div>
|
||||
<div class="custom-chart-add"><select id="customSeriesSelect">${options.map(([value, label]) => `<option value="${esc(value)}">${esc(label)}</option>`).join('')}</select><button class="secondary" data-history-action="add-series">${esc(tr('history.addSeries'))}</button></div>
|
||||
<div class="custom-series-list">${selected.length ? selected.map((item, index) => `<span class="custom-series-chip"><i style="--chip-color:${esc(item.color)}"></i>${esc(item.label)}<button data-history-action="remove-series" data-index="${index}" aria-label="${esc(tr('actions.remove'))}">${uiIcon('close')}</button></span>`).join('') : `<span class="field-note">${esc(tr('history.noCustomSeries'))}</span>`}</div>
|
||||
<div class="custom-chart-save"><input id="customChartName" placeholder="${esc(tr('history.chartName'))}"><button class="secondary" data-history-action="save-chart">${esc(tr('actions.save'))}</button><button class="primary" data-history-action="copy-chart-link">${esc(tr('history.copyLink'))}</button></div>
|
||||
<div class="saved-chart-list">${app.savedCharts.length ? app.savedCharts.map(item => `<div class="saved-chart-row"><button data-history-action="load-chart" data-id="${esc(item.id)}"><strong>${esc(item.name)}</strong><small>${esc(item.series.length)} ${esc(tr('history.series'))}</small></button><button class="danger" data-history-action="delete-chart" data-id="${esc(item.id)}">${uiIcon('close')}</button></div>`).join('') : `<span class="field-note">${esc(tr('history.noSavedCharts'))}</span>`}</div>
|
||||
${editNote}
|
||||
<div class="custom-chart-save"><input id="customChartName" value="${esc(draftName)}" placeholder="${esc(tr('history.chartName'))}"><button class="secondary" data-history-action="save-chart">${esc(editing ? tr('history.saveChanges') : tr('actions.save'))}</button>${cancelEdit}<button class="primary" data-history-action="copy-chart-link">${esc(tr('history.copyLink'))}</button></div>
|
||||
<div class="saved-chart-section"><div class="saved-chart-heading"><strong>${esc(tr('history.savedCharts'))}</strong><small>${app.savedCharts.length}</small></div><div class="saved-chart-list">${app.savedCharts.length ? app.savedCharts.map(item => `<div class="saved-chart-row${item.id === app.customChartEditingId ? ' is-editing' : ''}"><button class="saved-chart-open" data-history-action="load-chart" data-id="${esc(item.id)}"><strong>${esc(item.name)}</strong><small>${esc(item.series.length)} ${esc(tr('history.series'))} · ${esc(savedChartRangeLabel(item.hours))}</small></button><div class="saved-chart-actions"><button class="secondary" data-history-action="edit-chart" data-id="${esc(item.id)}" title="${esc(tr('actions.edit'))}" aria-label="${esc(tr('actions.edit'))}">${uiIcon('edit')}</button><button class="danger" data-history-action="delete-chart" data-id="${esc(item.id)}" title="${esc(tr('actions.delete'))}" aria-label="${esc(tr('actions.delete'))}">${uiIcon('close')}</button></div></div>`).join('') : `<span class="field-note">${esc(tr('history.noSavedCharts'))}</span>`}</div></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -327,26 +341,46 @@ function publicCustomChartUrl(path) {
|
||||
async function handleHistoryAction(button) {
|
||||
const action = button.dataset.historyAction;
|
||||
if (action === 'add-series') {
|
||||
if ($('#customChartName')) app.customChartNameDraft = $('#customChartName').value;
|
||||
const value = $('#customSeriesSelect')?.value;
|
||||
if (value && !app.customChartSeries.includes(value)) app.customChartSeries.push(value);
|
||||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath(), true); return;
|
||||
}
|
||||
if (action === 'remove-series') {
|
||||
if ($('#customChartName')) app.customChartNameDraft = $('#customChartName').value;
|
||||
app.customChartSeries.splice(Number(button.dataset.index), 1);
|
||||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath(), true); return;
|
||||
}
|
||||
if (action === 'save-chart') {
|
||||
if (!app.customChartSeries.length) return toast(tr('history.noCustomSeries'), true);
|
||||
const name = $('#customChartName')?.value.trim() || tr('history.customChart');
|
||||
const item = { id: `chart-${Date.now()}`, name, series: [...app.customChartSeries], hours: $('#historyHours')?.value || '24' };
|
||||
app.savedCharts.unshift(item); persistSavedCharts(); renderCustomHistory(); toast(tr('history.chartSaved')); return;
|
||||
const hours = $('#historyHours')?.value || '24';
|
||||
const editingIndex = app.savedCharts.findIndex(entry => entry.id === app.customChartEditingId);
|
||||
if (editingIndex >= 0) {
|
||||
app.savedCharts[editingIndex] = { ...app.savedCharts[editingIndex], name, series: [...app.customChartSeries], hours };
|
||||
app.customChartEditingId = null; app.customChartNameDraft = null;
|
||||
persistSavedCharts(); renderCustomHistory(); toast(tr('history.chartUpdated')); return;
|
||||
}
|
||||
const item = { id: `chart-${Date.now()}`, name, series: [...app.customChartSeries], hours };
|
||||
app.savedCharts.unshift(item); app.customChartNameDraft = null; persistSavedCharts(); renderCustomHistory(); toast(tr('history.chartSaved')); return;
|
||||
}
|
||||
if (action === 'load-chart') {
|
||||
const item = app.savedCharts.find(entry => entry.id === button.dataset.id); if (!item) return;
|
||||
app.customChartEditingId = null; app.customChartNameDraft = item.name || '';
|
||||
app.customChartSeries = [...item.series]; if ($('#historyHours') && item.hours) $('#historyHours').value = String(item.hours);
|
||||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath()); return;
|
||||
}
|
||||
if (action === 'edit-chart') {
|
||||
const item = app.savedCharts.find(entry => entry.id === button.dataset.id); if (!item) return;
|
||||
app.customChartEditingId = item.id; app.customChartNameDraft = item.name || ''; app.customChartSeries = [...item.series];
|
||||
if ($('#historyHours') && item.hours) $('#historyHours').value = String(item.hours);
|
||||
renderCustomHistory(); updateBrowserUrl(currentHistoryPath()); $('#customChartName')?.focus(); return;
|
||||
}
|
||||
if (action === 'cancel-chart-edit') {
|
||||
app.customChartEditingId = null; app.customChartNameDraft = null; renderCustomHistory(); return;
|
||||
}
|
||||
if (action === 'delete-chart') {
|
||||
if (app.customChartEditingId === button.dataset.id) { app.customChartEditingId = null; app.customChartNameDraft = null; }
|
||||
app.savedCharts = app.savedCharts.filter(entry => entry.id !== button.dataset.id); persistSavedCharts(); renderCustomHistory(); return;
|
||||
}
|
||||
if (action === 'copy-chart-link') {
|
||||
|
||||
Reference in New Issue
Block a user