1/** 2 * Play Store install banner â Android detection and dismissal. 3 * 4 * The markup ships hidden for every mobile visitor so page caching can't pin an 5 * Android-only banner into a shared cache entry; this decides whether to reveal 6 * it on the actual device. 7 */ 8(function () { 9 'use strict'; 10 11 var banner = document.getElementById('grv-android-banner'); 12 if (!banner) return; 13 14 var ua = navigator.userAgent || ''; 15 16 // Android phones and tablets, but not Windows Phone, which used to put 17 // "Android" in its UA string for compatibility. 18 var isAndroid = /Android/i.test(ua) && !/Windows Phone/i.test(ua); 19 if (!isAndroid) return; 20 21 // Already installed and launched from the home screen â don't advertise it. 22 var standalone = window.matchMedia && window.matchMedia('(display-mode: standalone)').matches; 23 if (standalone || navigator.standalone) return; 24 25 var key = banner.getAttribute('data-dismiss-key') || 'grv_android_banner_dismissed_v1'; 26 var days = parseInt(banner.getAttribute('data-dismiss-days'), 10); 27 if (!days || days < 0) days = 30; 28 29 // localStorage throws in some privacy modes, so every access is guarded and 30 // failure just means the banner shows. 31 function stored(name) { 32 try { return window.localStorage.getItem(name); } catch (e) { return null; } 33 } 34 35 var until = parseInt(stored(key), 10); 36 if (until && Date.now() < until) return; 37 38 var root = document.documentElement; 39 40 /** 41 * Publish the measured height so the stylesheet can make room for the banner. 42 * Everything that moves reads --grv-android-banner-h, so setting it here is 43 * enough â we never touch padding or top values directly from JS. 44 * 45 * Measuring beats the 56px CSS fallback because the title and subtext wrap 46 * differently across devices and font sizes. 47 */ 48 function measure() { 49 var h = banner.offsetHeight; 50 if (h) { 51 root.style.setProperty('--grv-android-banner-h', h + 'px'); 52 } 53 } 54 55 /** 56 * True while a modal owns the screen. The stylesheet hides the banner in this 57 * state; this mirrors the same condition so measurement can be skipped. 58 */ 59 function modalOpen() { 60 if (root.classList.contains('grv-modal-open')) return true; 61 62 // The stylesheet also hides the banner for overlays that never set that 63 // class, matched structurally with :has(). Mirror the same selectors so the 64 // reserved space collapses for them too â otherwise the inline height set 65 // by measure() would keep a blank strip above a full-screen overlay. 66 try { 67 return !!document.querySelector( 68 '.grv-modal-overlay[style*="display: flex"],' + 69 '.grv-tp-overlay:not([hidden]),' + 70 '.search-form.open,' + 71 '.grv-hides-app-banner' 72 ); 73 } catch (e) { 74 return false; 75 } 76 } 77 78 /** 79 * The stylesheet hides the banner while a modal is open and zeroes 80 * --grv-android-banner-h with it. An inline height set here would win over 81 * that (inline style beats a stylesheet rule), so the reserved space would 82 * survive the banner being hidden and push the modal down by an empty strip. 83 * 84 * Dropping the inline value while a modal is open hands control back to the 85 * stylesheet; reapplying on close restores the measured height. 86 */ 87 /* A subtree observer fires for unrelated attribute churn too (lazy images, 88 ad slots, the theme's own scripts), and each call runs a querySelector. 89 Coalescing to one check per frame keeps that off the critical path â the 90 banner only needs to settle by the next paint, not on every mutation. */ 91 var syncQueued = false; 92 93 function queueSync() { 94 if (syncQueued) return; 95 syncQueued = true; 96 97 var run = window.requestAnimationFrame || function (fn) { return setTimeout(fn, 16); }; 98 run(function () { 99 syncQueued = false; 100 syncForModal(); 101 }); 102 } 103 104 function syncForModal() { 105 if (banner.hidden) return; 106 107 if (modalOpen()) { 108 root.style.removeProperty('--grv-android-banner-h'); 109 } else { 110 measure(); 111 } 112 } 113 114 /** 115 * Watch the class modal/core.js toggles on <html>. A class change carries no 116 * event of its own, so this observes the attribute directly rather than 117 * relying on every modal to announce itself. 118 */ 119 function watchModal() { 120 if (typeof MutationObserver !== 'function') return; 121 122 // Watch the whole document rather than just <html>: the class lands on 123 // <html> for the grv-identity modals, but on the overlay element itself for 124 // the theme's search panel, and on <body> for twentytwenty. Subtree covers 125 // all three without needing to know which fires. 126 new MutationObserver(queueSync).observe(document.documentElement, { 127 attributes: true, 128 attributeFilter: ['class', 'style', 'hidden'], 129 subtree: true 130 }); 131 } 132 133 function show() { 134 banner.hidden = false;
135 root.classList.add('grv-android-banner-open'); 136 document.body.classList.add('grv-android-banner-open'); 137 measure(); 138 } 139 140 function hide() { 141 banner.hidden = true; 142 root.classList.remove('grv-android-banner-open'); 143 document.body.classList.remove('grv-android-banner-open'); 144 // Drop the inline override so the property falls back to the 0px in the 145 // stylesheet and every offset collapses with it. 146 root.style.removeProperty('--grv-android-banner-h'); 147 } 148 149 /** 150 * The banner can change height after the first measurement â rotating the 151 * device changes how the text wraps, and a late webfont shifts the line box. 152 * Re-measure so the reserved space keeps matching the banner. 153 */ 154 function watchHeight() { 155 if (typeof ResizeObserver === 'function') { 156 new ResizeObserver(function () { 157 if (!banner.hidden && !modalOpen()) measure(); 158 }).observe(banner); 159 return; 160 } 161 162 // Older Chrome on Android â orientationchange is the case that matters. 163 window.addEventListener('resize', function () { 164 if (!banner.hidden && !modalOpen()) measure(); 165 }); 166 } 167 168 banner.querySelector('.grv-android-banner__close').addEventListener('click', function () { 169 hide(); 170 try { 171 window.localStorage.setItem(key, String(Date.now() + days * 86400000)); 172 } catch (e) { /* private mode â banner returns next visit */ } 173 }); 174 175 // Tapping through to the Play Store counts as dealt with. 176 banner.querySelector('.grv-android-banner__cta').addEventListener('click', function () { 177 try { 178 window.localStorage.setItem(key, String(Date.now() + days * 86400000)); 179 } catch (e) {} 180 }); 181 182 show(); 183 watchHeight(); 184 watchModal(); 185})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.