asdf98 commited on
Commit
43d57e8
·
verified ·
1 Parent(s): eceaf26

fix: hover overlay ADD uses reliable image beacon and best image URL extraction

Browse files
src-tauri/resources/scripts/hover_overlay.js CHANGED
@@ -1,144 +1,154 @@
1
- /* Refstudio hover overlay: + ADD button. Prevents click-through to underlying links. */
2
- (function () {
3
- if (window.__muse_hover_v7) return;
4
- window.__muse_hover_v7 = true;
5
-
6
- var MIN = 90;
7
- var activeImg = null;
8
- var badge = null;
9
- var showTimer = 0;
10
- var hideTimer = 0;
11
-
12
- function injectCSS() {
13
- if (document.getElementById('__muse_badge_css')) return;
14
- var s = document.createElement('style'); s.id = '__muse_badge_css';
15
- s.textContent = '#__muse_add_badge{all:initial;position:fixed;z-index:2147483647;display:flex;align-items:center;gap:4px;padding:7px 14px;background:rgba(255,255,255,0.97);color:#111;border-radius:999px;box-shadow:0 4px 20px rgba(0,0,0,0.3),0 0 0 1px rgba(0,0,0,0.05);font:700 11px/1 -apple-system,BlinkMacSystemFont,"Inter",sans-serif;cursor:pointer;opacity:0;transform:translateY(4px) scale(0.92);transition:opacity .15s,transform .15s;pointer-events:none;letter-spacing:0.02em;user-select:none}#__muse_add_badge.v{opacity:1;transform:none;pointer-events:auto}#__muse_add_badge:hover{background:#fff;box-shadow:0 6px 28px rgba(0,0,0,0.35),0 0 0 1px rgba(0,0,0,0.08);transform:scale(1.05)}#__muse_add_badge:active{transform:scale(0.94)}#__muse_add_badge svg{width:14px;height:14px;stroke-width:2.5}#__mt7{all:initial;position:fixed;left:50%;bottom:24px;transform:translateX(-50%) translateY(8px);z-index:2147483647;padding:9px 16px;border-radius:999px;background:rgba(20,20,20,.97);border:1px solid rgba(255,255,255,.1);box-shadow:0 10px 36px rgba(0,0,0,.5);font:600 12px/1 -apple-system,sans-serif;color:#fff;opacity:0;transition:opacity .16s,transform .16s;pointer-events:none}#__mt7.s{opacity:1;transform:translateX(-50%) translateY(0)}';
16
- (document.head || document.documentElement).appendChild(s);
17
- }
18
-
19
- function enc(v) { return encodeURIComponent(v == null ? '' : String(v)); }
20
- function absUrl(s) { try { return new URL(s || '', location.href).href; } catch (_) { return s || ''; } }
21
-
22
- function toast(m) {
23
- var t = document.getElementById('__mt7');
24
- if (!t) { t = document.createElement('div'); t.id = '__mt7'; document.documentElement.appendChild(t); }
25
- t.textContent = m; t.classList.add('s');
26
- clearTimeout(t._t); t._t = setTimeout(function () { t.classList.remove('s'); }, 1500);
27
- }
28
-
29
- function addToBoard(url, title, w, h) {
30
- var u = 'muse-action://board?url=' + enc(url) + '&source=' + enc(location.href) + '&title=' + enc(title) + '&w=' + enc(w) + '&h=' + enc(h);
31
- // Use an iframe to trigger navigation without changing current page
32
- var frame = document.createElement('iframe');
33
- frame.style.cssText = 'display:none;width:0;height:0;border:none;position:absolute;';
34
- document.documentElement.appendChild(frame);
35
- try { frame.contentWindow.location.href = u; } catch (_) {
36
- // Fallback: direct navigation (Rust will block it via on_navigation returning false)
37
- try { window.location.href = u; } catch (_2) {}
38
- }
39
- setTimeout(function () { if (frame.parentNode) frame.parentNode.removeChild(frame); }, 200);
40
- }
41
-
42
- function pos(el, img) {
43
- var r = img.getBoundingClientRect();
44
- // Position at top-right corner of the image (where user expects action buttons)
45
- var x = r.right - 70;
46
- var y = r.top + 8;
47
- if (x < r.left + 8) x = r.left + 8;
48
- if (x > innerWidth - 80) x = innerWidth - 80;
49
- if (y < 4) y = 4;
50
- if (y > innerHeight - 40) y = innerHeight - 40;
51
- el.style.left = x + 'px';
52
- el.style.top = y + 'px';
53
- }
54
-
55
- function hide() {
56
- if (!badge) return;
57
- badge.classList.remove('v');
58
- activeImg = null;
59
- setTimeout(function () { if (badge && !badge.classList.contains('v')) badge.style.pointerEvents = 'none'; }, 160);
60
- }
61
-
62
- function show(img) {
63
- if (img === activeImg && badge && badge.classList.contains('v')) return;
64
- injectCSS();
65
- activeImg = img;
66
-
67
- if (!badge) {
68
- badge = document.createElement('div');
69
- badge.id = '__muse_add_badge';
70
- badge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> ADD';
71
-
72
- // Use mousedown + prevent everything to ensure no link/image click passes through
73
- badge.addEventListener('mousedown', function (e) {
74
- e.preventDefault();
75
- e.stopPropagation();
76
- e.stopImmediatePropagation();
77
- }, true);
78
-
79
- badge.addEventListener('click', function (e) {
80
- e.preventDefault();
81
- e.stopPropagation();
82
- e.stopImmediatePropagation();
83
- if (!activeImg) return;
84
- var url = absUrl(activeImg.currentSrc || activeImg.src);
85
- var title = activeImg.alt || activeImg.title || document.title || 'Reference';
86
- var w = activeImg.naturalWidth || Math.round(activeImg.getBoundingClientRect().width);
87
- var h = activeImg.naturalHeight || Math.round(activeImg.getBoundingClientRect().height);
88
- addToBoard(url, title, w, h);
89
- toast('✓ Added to Board');
90
- }, true);
91
-
92
- // Prevent any pointer event from bubbling to parent links
93
- badge.addEventListener('pointerdown', function (e) {
94
- e.stopPropagation();
95
- e.stopImmediatePropagation();
96
- }, true);
97
-
98
- badge.addEventListener('pointerup', function (e) {
99
- e.stopPropagation();
100
- e.stopImmediatePropagation();
101
- }, true);
102
-
103
- badge.onmouseenter = function () { clearTimeout(hideTimer); };
104
- badge.onmouseleave = function () { hideTimer = setTimeout(hide, 300); };
105
-
106
- document.documentElement.appendChild(badge);
107
- }
108
-
109
- pos(badge, activeImg);
110
- badge.offsetHeight;
111
- badge.classList.add('v');
112
- }
113
-
114
- // Only show on IMG elements, not on elements inside links that happen to be images
115
- function isValidTarget(el) {
116
- if (!el || el.tagName !== 'IMG') return false;
117
- var r = el.getBoundingClientRect();
118
- return r.width >= MIN && r.height >= MIN;
119
- }
120
-
121
- document.addEventListener('mouseover', function (e) {
122
- var t = e.target;
123
- if (!isValidTarget(t)) return;
124
- if (t === activeImg) { clearTimeout(hideTimer); return; }
125
- clearTimeout(hideTimer); clearTimeout(showTimer);
126
- showTimer = setTimeout(function () { show(t); }, 250);
127
- }, true);
128
-
129
- document.addEventListener('mouseout', function (e) {
130
- var t = e.target;
131
- if (!t || t.tagName !== 'IMG' || t !== activeImg) return;
132
- var rel = e.relatedTarget;
133
- if (badge && (badge === rel || badge.contains && badge.contains(rel))) return;
134
- clearTimeout(showTimer);
135
- hideTimer = setTimeout(hide, 350);
136
- }, true);
137
-
138
- window.addEventListener('scroll', function () {
139
- if (!badge || !activeImg) return;
140
- var r = activeImg.getBoundingClientRect();
141
- if (r.bottom < -20 || r.top > innerHeight + 20) hide();
142
- else pos(badge, activeImg);
143
- }, { passive: true });
144
- })();
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Refstudio hover overlay: + ADD button on web images. */
2
+ (function () {
3
+ if (window.__muse_hover_v8) return;
4
+ window.__muse_hover_v8 = true;
5
+
6
+ var MIN = 90;
7
+ var activeImg = null;
8
+ var badge = null;
9
+ var showTimer = 0;
10
+ var hideTimer = 0;
11
+ var beaconKeepalive = [];
12
+
13
+ function injectCSS() {
14
+ if (document.getElementById('__muse_badge_css')) return;
15
+ var s = document.createElement('style'); s.id = '__muse_badge_css';
16
+ s.textContent = '#__muse_add_badge{all:initial;position:fixed;z-index:2147483647;display:flex;align-items:center;gap:4px;padding:7px 14px;background:rgba(255,255,255,0.97);color:#111;border-radius:999px;box-shadow:0 4px 20px rgba(0,0,0,0.3),0 0 0 1px rgba(0,0,0,0.05);font:700 11px/1 -apple-system,BlinkMacSystemFont,"Inter",sans-serif;cursor:pointer;opacity:0;transform:translateY(4px) scale(0.92);transition:opacity .15s,transform .15s;pointer-events:none;letter-spacing:0.02em;user-select:none}#__muse_add_badge.v{opacity:1;transform:none;pointer-events:auto}#__muse_add_badge:hover{background:#fff;box-shadow:0 6px 28px rgba(0,0,0,0.35),0 0 0 1px rgba(0,0,0,0.08);transform:scale(1.05)}#__muse_add_badge:active{transform:scale(0.94)}#__muse_add_badge svg{width:14px;height:14px;stroke-width:2.5}#__mt7{all:initial;position:fixed;left:50%;bottom:24px;transform:translateX(-50%) translateY(8px);z-index:2147483647;padding:9px 16px;border-radius:999px;background:rgba(20,20,20,.97);border:1px solid rgba(255,255,255,.1);box-shadow:0 10px 36px rgba(0,0,0,.5);font:600 12px/1 -apple-system,sans-serif;color:#fff;opacity:0;transition:opacity .16s,transform .16s;pointer-events:none}#__mt7.s{opacity:1;transform:translateX(-50%) translateY(0)}';
17
+ (document.head || document.documentElement).appendChild(s);
18
+ }
19
+
20
+ function enc(v) { return encodeURIComponent(v == null ? '' : String(v)); }
21
+ function absUrl(s) { try { return new URL(s || '', location.href).href; } catch (_) { return s || ''; } }
22
+
23
+ function bestFromSrcset(srcset) {
24
+ if (!srcset) return '';
25
+ try {
26
+ var best = '', bestScore = 0;
27
+ srcset.split(',').forEach(function(part) {
28
+ var bits = part.trim().split(/\s+/);
29
+ var u = bits[0];
30
+ var score = 1;
31
+ if (bits[1]) {
32
+ if (bits[1].endsWith('w')) score = parseInt(bits[1], 10) || 1;
33
+ else if (bits[1].endsWith('x')) score = (parseFloat(bits[1]) || 1) * 1000;
34
+ }
35
+ if (score > bestScore) { bestScore = score; best = u; }
36
+ });
37
+ return best ? absUrl(best) : '';
38
+ } catch (_) { return ''; }
39
+ }
40
+
41
+ function bestImageUrl(img) {
42
+ // Prefer the highest available URL. This catches srcset/lazy-loaded images.
43
+ var u = bestFromSrcset(img.getAttribute('srcset') || img.srcset);
44
+ if (u) return u;
45
+ u = img.getAttribute('data-full') || img.getAttribute('data-fullsrc') || img.getAttribute('data-original') || img.getAttribute('data-src') || img.getAttribute('data-lazy-src') || img.currentSrc || img.src;
46
+ u = absUrl(u);
47
+ // Site adapter tweaks for common thumb URL patterns.
48
+ try {
49
+ if (u.includes('pinimg.com') && u.match(/\/\d+x\//)) u = u.replace(/\/\d+x\//, '/originals/');
50
+ if (u.includes('artstation.com') && u.includes('/medium/')) u = u.replace('/medium/', '/large/');
51
+ if (u.includes('behance.net') && u.includes('/max_1200/')) u = u.replace('/max_1200/', '/max_3840/');
52
+ } catch (_) {}
53
+ return u;
54
+ }
55
+
56
+ function toast(m) {
57
+ var t = document.getElementById('__mt7');
58
+ if (!t) { t = document.createElement('div'); t.id = '__mt7'; document.documentElement.appendChild(t); }
59
+ t.textContent = m; t.classList.add('s');
60
+ clearTimeout(t._t); t._t = setTimeout(function () { t.classList.remove('s'); }, 1500);
61
+ }
62
+
63
+ function addToBoard(url, title, w, h) {
64
+ var u = 'muse-action://board?url=' + enc(url) + '&source=' + enc(location.href) + '&title=' + enc(title) + '&w=' + enc(w) + '&h=' + enc(h);
65
+ // Most reliable cross-origin child-webview bridge: image beacon to registered URI scheme.
66
+ var b = new Image();
67
+ b.onload = b.onerror = function () {
68
+ setTimeout(function () {
69
+ var i = beaconKeepalive.indexOf(b);
70
+ if (i >= 0) beaconKeepalive.splice(i, 1);
71
+ }, 500);
72
+ };
73
+ beaconKeepalive.push(b);
74
+ b.src = u;
75
+ }
76
+
77
+ function pos(el, img) {
78
+ var r = img.getBoundingClientRect();
79
+ var x = r.right - 78;
80
+ var y = r.top + 8;
81
+ if (x < r.left + 8) x = r.left + 8;
82
+ if (x > innerWidth - 90) x = innerWidth - 90;
83
+ if (y < 4) y = 4;
84
+ if (y > innerHeight - 42) y = innerHeight - 42;
85
+ el.style.left = x + 'px';
86
+ el.style.top = y + 'px';
87
+ }
88
+
89
+ function hide() {
90
+ if (!badge) return;
91
+ badge.classList.remove('v');
92
+ activeImg = null;
93
+ setTimeout(function () { if (badge && !badge.classList.contains('v')) badge.style.pointerEvents = 'none'; }, 160);
94
+ }
95
+
96
+ function show(img) {
97
+ if (img === activeImg && badge && badge.classList.contains('v')) return;
98
+ injectCSS();
99
+ activeImg = img;
100
+ if (!badge) {
101
+ badge = document.createElement('div');
102
+ badge.id = '__muse_add_badge';
103
+ badge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> ADD';
104
+ ['mousedown','pointerdown','pointerup'].forEach(function(ev){ badge.addEventListener(ev, function(e){ e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }, true); });
105
+ badge.addEventListener('click', function (e) {
106
+ e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
107
+ if (!activeImg) return;
108
+ var url = bestImageUrl(activeImg);
109
+ var title = activeImg.alt || activeImg.title || document.title || 'Reference';
110
+ var w = activeImg.naturalWidth || Math.round(activeImg.getBoundingClientRect().width);
111
+ var h = activeImg.naturalHeight || Math.round(activeImg.getBoundingClientRect().height);
112
+ if (!url || url.startsWith('data:') || url.startsWith('blob:')) { toast('Cannot capture this image'); return; }
113
+ addToBoard(url, title, w, h);
114
+ toast('✓ Added to Board');
115
+ }, true);
116
+ badge.onmouseenter = function () { clearTimeout(hideTimer); };
117
+ badge.onmouseleave = function () { hideTimer = setTimeout(hide, 300); };
118
+ document.documentElement.appendChild(badge);
119
+ }
120
+ pos(badge, activeImg);
121
+ badge.offsetHeight;
122
+ badge.classList.add('v');
123
+ }
124
+
125
+ function isValidTarget(el) {
126
+ if (!el || el.tagName !== 'IMG') return false;
127
+ var r = el.getBoundingClientRect();
128
+ return r.width >= MIN && r.height >= MIN;
129
+ }
130
+
131
+ document.addEventListener('mouseover', function (e) {
132
+ var t = e.target;
133
+ if (!isValidTarget(t)) return;
134
+ if (t === activeImg) { clearTimeout(hideTimer); return; }
135
+ clearTimeout(hideTimer); clearTimeout(showTimer);
136
+ showTimer = setTimeout(function () { show(t); }, 180);
137
+ }, true);
138
+
139
+ document.addEventListener('mouseout', function (e) {
140
+ var t = e.target;
141
+ if (!t || t.tagName !== 'IMG' || t !== activeImg) return;
142
+ var rel = e.relatedTarget;
143
+ if (badge && (badge === rel || badge.contains && badge.contains(rel))) return;
144
+ clearTimeout(showTimer);
145
+ hideTimer = setTimeout(hide, 300);
146
+ }, true);
147
+
148
+ window.addEventListener('scroll', function () {
149
+ if (!badge || !activeImg) return;
150
+ var r = activeImg.getBoundingClientRect();
151
+ if (r.bottom < -20 || r.top > innerHeight + 20) hide();
152
+ else pos(badge, activeImg);
153
+ }, { passive: true, capture: true });
154
+ })();