diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index 3df85f1d..63a5cd7a 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -4349,6 +4349,48 @@ body.objectKanbanColumnResizing * { --live-activity-flash-to: 0.45; } +.live-activity-favicon-source { + position: fixed; + top: 0; + left: 0; + margin: 0; + padding: 0; + border: 0; + border-radius: 8px; + overflow: hidden; + pointer-events: none; + transform: translateX(-120vw); + z-index: -1; +} + +.live-activity-favicon-source .live-activity { + margin: 0; + padding: 0; + border: 1px solid var(--live-activity-color); + border-radius: 8px; + box-sizing: border-box; + box-shadow: none; + cursor: default; + background: color-mix( + in srgb, + var(--live-activity-color) 45%, + var(--color-background) + ); +} + +.live-activity-favicon-source .live-activity-progress-fill { + opacity: 0.92; +} + +.live-activity-favicon-source .live-activity-flash { + --live-activity-flash-from: 0.55; + --live-activity-flash-to: 0.92; +} + +.live-activity-favicon-source .live-activity-progress-wave { + display: none; +} + .printerQueue.ant-steps.ant-steps-vertical > .ant-steps-item:last-child .ant-steps-item-content { diff --git a/src/components/Dashboard/common/LiveActivities.jsx b/src/components/Dashboard/common/LiveActivities.jsx index 9a5c98a1..ee49df54 100644 --- a/src/components/Dashboard/common/LiveActivities.jsx +++ b/src/components/Dashboard/common/LiveActivities.jsx @@ -7,6 +7,8 @@ import { useRef, useState } from 'react' +import { createPortal } from 'react-dom' +import { snapdom } from '@zumer/snapdom' import { theme, Popover } from 'antd' import { useNavigate } from 'react-router-dom' import { ApiServerContext } from '../context/ApiServerContext' @@ -61,6 +63,127 @@ const STATUS_COLOR_VAR = { } const LIVE_ACTIVITY_TRANSITION_MS = 300 +const FAVICON_PX = 32 + +let faviconOriginalHref +let faviconOriginalType +let faviconAppliedHref +let faviconObjectUrl +let faviconCaptureChain = Promise.resolve() + +const faviconLink = () => document.querySelector('link[rel="icon"]') + +const tightenFaviconSvg = (src) => { + const comma = src.indexOf(',') + if (comma < 0 || !src.slice(0, comma).includes('svg')) return src + let svg = decodeURIComponent(src.slice(comma + 1)) + svg = svg.replace(/viewBox="0 0 ([\d.]+) ([\d.]+)"/, (match, w, h) => { + const width = Number(w) + const height = Number(h) + if (!Number.isFinite(width) || width < 4 || width !== height) return match + return `viewBox="1 1 ${width - 2} ${height - 2}"` + }) + return `${src.slice(0, comma)},${encodeURIComponent(svg)}` +} + +const applyLiveFavicon = (href) => { + const tightened = href ? tightenFaviconSvg(href) : href + if (!tightened || tightened === faviconAppliedHref) return + const current = faviconLink() + if (!current) return + if (faviconOriginalHref === undefined) { + faviconOriginalHref = current.getAttribute('href') + faviconOriginalType = current.getAttribute('type') + } + const next = document.createElement('link') + next.rel = 'icon' + next.type = 'image/svg+xml' + next.href = tightened + current.replaceWith(next) + if (faviconObjectUrl && faviconObjectUrl !== tightened) { + URL.revokeObjectURL(faviconObjectUrl) + } + faviconObjectUrl = tightened.startsWith('blob:') ? tightened : null + faviconAppliedHref = tightened +} + +const restoreLiveFavicon = () => { + if (faviconAppliedHref == null) return + const current = faviconLink() + faviconAppliedHref = null + if (faviconObjectUrl) { + URL.revokeObjectURL(faviconObjectUrl) + faviconObjectUrl = null + } + if (!current || faviconOriginalHref == null) return + const next = document.createElement('link') + next.rel = 'icon' + if (faviconOriginalType) next.type = faviconOriginalType + next.href = faviconOriginalHref + current.replaceWith(next) +} + +const waitForPaint = () => + new Promise((resolve) => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(resolve) + }) + }) + +const renderLiveActivityFace = ({ + activity, + size, + iconSize, + label, + tabIndex = 0, + onClick +}) => { + const colorVar = STATUS_COLOR_VAR[activity.status] || '--color-info' + const color = `var(${colorVar})` + + return ( + + ) +} const readState = (object) => { const raw = object?.state ?? object?.status @@ -340,110 +463,172 @@ const LiveActivities = () => { const size = token.controlHeight const iconSize = Math.max(12, Math.round(size * 0.5)) + const faviconIconSize = Math.round(size * 0.66) const openCount = slots.filter((slot) => slot.open).length const activitiesOpen = openCount > 0 - return ( -