From 096aace964493b0fa65f198d1497e3e111bdc2f4 Mon Sep 17 00:00:00 2001 From: Leafus Date: Tue, 18 Nov 2025 05:48:11 +0100 Subject: [PATCH] feat: [frontend] improve watermark --- .../src/components/global/userIdWatermark.vue | 270 +++++++++++------- 1 file changed, 169 insertions(+), 101 deletions(-) diff --git a/packages/frontend/src/components/global/userIdWatermark.vue b/packages/frontend/src/components/global/userIdWatermark.vue index c0bf285c31..dbdb88cd75 100644 --- a/packages/frontend/src/components/global/userIdWatermark.vue +++ b/packages/frontend/src/components/global/userIdWatermark.vue @@ -11,30 +11,17 @@ const generateRandomClass = () => { return result; }; -const gridClass = ref(generateRandomClass()); -const itemClasses = ref([]); +const containerClass = ref(generateRandomClass()); const recreationKey = ref(0); -const gridItemCount = ref(600); let regenerationInterval: ReturnType | null = null; +let checkInterval: ReturnType | null = null; let styleElement: HTMLStyleElement | null = null; +let watermarkElement: HTMLDivElement | null = null; +let mutationObserver: MutationObserver | null = null; +let headObserver: MutationObserver | null = null; +let attributeObserver: MutationObserver | null = null; let allCreatedClasses: string[] = []; -// Calculate grid items based on screen resolution -const calculateGridItemCount = () => { - const width = window.innerWidth; - const height = window.innerHeight; - - // Grid cell size: 80px width + 10px gap, 16px height + 10px gap (more concise) - const itemWidth = 80 + 10; - const itemHeight = 16 + 10; - - const columns = Math.ceil(width / itemWidth); - const rows = Math.ceil(height / itemHeight); - - // Add some buffer (20%) to ensure coverage during animations/scrolling - return Math.ceil(columns * rows * 1.2); -}; - const encodeTextToPath = (text: string) => { const charPaths: { [key: string]: string } = { a: "M2,8 L4,2 L6,2 L8,8 M3,6 L7,6", @@ -94,31 +81,17 @@ const encodeTextToPath = (text: string) => { ); path += translatedPath + " "; } - xOffset += 7; // Reduced from 10 to 7 for tighter spacing + xOffset += 7; }); return path.trim(); }; -const generateItemClasses = (count: number) => { - const classes: string[] = []; - for (let i = 0; i < count; i++) { - classes.push(generateRandomClass()); - } - return classes; -}; - const regenerateWatermark = () => { cleanupOldElements(); - - gridClass.value = generateRandomClass(); - const count = calculateGridItemCount(); - gridItemCount.value = count; - itemClasses.value = generateItemClasses(count); + containerClass.value = generateRandomClass(); recreationKey.value++; - - allCreatedClasses.push(gridClass.value, ...itemClasses.value); - + allCreatedClasses.push(containerClass.value); createStyleElement(); }; @@ -128,16 +101,14 @@ const cleanupOldElements = () => { const content = style.textContent || ""; const hasOldClasses = allCreatedClasses.some( (className) => - content.includes(className) && - className !== gridClass.value && - !itemClasses.value.includes(className), + content.includes(className) && className !== containerClass.value, ); if (hasOldClasses) { style.remove(); } }); - allCreatedClasses = [gridClass.value, ...itemClasses.value]; + allCreatedClasses = [containerClass.value]; }; const createStyleElement = () => { @@ -147,103 +118,200 @@ const createStyleElement = () => { styleElement = document.createElement("style"); - const itemStyles = itemClasses.value - .map( - (className) => ` - .${className} { - display: flex !important; - align-items: center !important; - justify-content: center !important; - } + const fgColor = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg').trim() || '#000000'; - .${className} svg { - opacity: 0.02 !important; - width: auto !important; - height: 10px !important; - } - - .${className} svg path { - stroke: var(--MI_THEME-fg) !important; - fill: none !important; - stroke-width: 0.8 !important; - } - `, - ) - .join(""); + const svgContent = ``; + const encodedSvg = encodeURIComponent(svgContent); const styles = ` - .${gridClass.value} { + .${containerClass.value} { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; - display: grid !important; - grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)) !important; - grid-template-rows: repeat(auto-fill, minmax(16px, 1fr)) !important; - gap: 10px !important; - padding: 10px !important; pointer-events: none !important; z-index: 999999999 !important; overflow: hidden !important; + opacity: 0.02 !important; + background-image: url("data:image/svg+xml,${encodedSvg}") !important; + background-repeat: repeat !important; + background-size: 80px 16px !important; + background-position: 10px 10px !important; } - ${itemStyles} `; styleElement.textContent = styles; + + Object.defineProperty(styleElement, 'remove', { + value: () => { + setTimeout(() => { + if (!styleElement?.parentNode) { + document.head.appendChild(styleElement!); + } + }, 0); + }, + writable: false, + configurable: false, + }); + document.head.appendChild(styleElement); }; +const ensureWatermarkExists = () => { + if (!watermarkElement || !document.body.contains(watermarkElement)) { + const newElement = document.createElement('div'); + newElement.className = containerClass.value; + newElement.setAttribute('data-watermark', 'true'); + + Object.defineProperty(newElement, 'remove', { + value: () => { + setTimeout(() => { + if (!document.body.contains(newElement)) { + document.body.appendChild(newElement); + } + }, 0); + }, + writable: false, + configurable: false, + }); + + const originalStyleSetter = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'style')?.set; + if (originalStyleSetter) { + Object.defineProperty(newElement, 'style', { + get: () => { + return newElement.getAttribute('style') || ''; + }, + set: (value) => { + return; + }, + }); + } + + document.body.appendChild(newElement); + watermarkElement = newElement; + + startAttributeObserver(); + } else if (watermarkElement.className !== containerClass.value) { + watermarkElement.className = containerClass.value; + } + + if (!styleElement || !document.head.contains(styleElement)) { + createStyleElement(); + } +}; + +const startAttributeObserver = () => { + if (attributeObserver) { + attributeObserver.disconnect(); + } + + if (!watermarkElement) return; + + attributeObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + if (watermarkElement && watermarkElement.className !== containerClass.value) { + watermarkElement.className = containerClass.value; + } + } else if (mutation.type === 'attributes' && mutation.attributeName === 'style') { + if (watermarkElement) { + const style = watermarkElement.getAttribute('style'); + if (style && (style.includes('display') || style.includes('visibility') || style.includes('opacity'))) { + watermarkElement.removeAttribute('style'); + } + } + } + } + }); + + attributeObserver.observe(watermarkElement, { + attributes: true, + attributeOldValue: true, + }); +}; + +const startMutationObserver = () => { + mutationObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type === 'childList') { + mutation.removedNodes.forEach((node) => { + if (node === watermarkElement || node === styleElement) { + ensureWatermarkExists(); + } + }); + } + } + }); + + mutationObserver.observe(document.body, { + childList: true, + subtree: true, + }); + + headObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type === 'childList') { + mutation.removedNodes.forEach((node) => { + if (node === styleElement) { + ensureWatermarkExists(); + } + }); + } + } + }); + + headObserver.observe(document.head, { + childList: true, + subtree: true, + }); +}; + const watermarkText = computed(() => $i?.id || "not signed in"); const pathData = computed(() => encodeTextToPath(watermarkText.value)); -const textWidth = computed(() => watermarkText.value.length * 7 + 10); // Adjusted for tighter spacing - -let resizeTimeout: ReturnType | null = null; - -const handleResize = () => { - if (resizeTimeout) clearTimeout(resizeTimeout); - - // Debounce resize to avoid excessive recalculations - resizeTimeout = setTimeout(() => { - regenerateWatermark(); - }, 250); -}; +const textWidth = computed(() => watermarkText.value.length * 7 + 10); onMounted(() => { - const count = calculateGridItemCount(); - gridItemCount.value = count; - itemClasses.value = generateItemClasses(count); - allCreatedClasses.push(gridClass.value, ...itemClasses.value); + allCreatedClasses.push(containerClass.value); createStyleElement(); + ensureWatermarkExists(); + startMutationObserver(); + + checkInterval = setInterval(() => { + ensureWatermarkExists(); + }, 100); regenerationInterval = setInterval(() => { - const count = calculateGridItemCount(); - gridItemCount.value = count; - itemClasses.value = generateItemClasses(count); - allCreatedClasses.push(gridClass.value, ...itemClasses.value); regenerateWatermark(); }, 5000); - - window.addEventListener("resize", handleResize); }); onUnmounted(() => { - if (regenerationInterval) clearInterval(regenerationInterval); - if (resizeTimeout) clearTimeout(resizeTimeout); - window.removeEventListener("resize", handleResize); + if (checkInterval) { + clearInterval(checkInterval); + } + if (regenerationInterval) { + clearInterval(regenerationInterval); + } + if (mutationObserver) { + mutationObserver.disconnect(); + } + if (headObserver) { + headObserver.disconnect(); + } + if (attributeObserver) { + attributeObserver.disconnect(); + } cleanupOldElements(); if (styleElement && styleElement.parentNode) { styleElement.parentNode.removeChild(styleElement); } + if (watermarkElement && watermarkElement.parentNode) { + watermarkElement.parentNode.removeChild(watermarkElement); + } });