Skip to content

Commit eaeb460

Browse files
authored
Add external countdown banner javascript and instructions for use (#113)
* Add external countdown banner javascript and instructions for use * Fix build pages
1 parent 2257702 commit eaeb460

File tree

8 files changed

+427
-161
lines changed

8 files changed

+427
-161
lines changed

public/banner.js

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
/**
2+
* Keep Android Open – Countdown Banner
3+
* Licensed under the GNU General Public License v3.0
4+
* SPDX-License-Identifier: GPL-3.0-only
5+
*
6+
* A self-contained, embeddable script that injects a countdown banner into any
7+
* web page. No external dependencies.
8+
*
9+
* Usage:
10+
* <script src="https://keepandroidopen.org/banner.js"></script>
11+
*
12+
* Query parameters (appended to the script src URL):
13+
* lang=fr Override the browser language (default: auto-detected)
14+
* id=myDiv Insert the banner inside the element with this id
15+
* (default: prepend to <body>)
16+
* size=normal Banner size: "normal" (default) or "mini"
17+
* link=URL Make the banner text a link (default: https://keepandroidopen.org)
18+
* Set link=none to disable the link
19+
* hidebutton=on Show an X close button (default: on)
20+
* Set hidebutton=off to hide the close button
21+
*/
22+
(function () {
23+
"use strict";
24+
25+
// ── Localized banner strings ──────────────────────────────────────────
26+
var messages = {
27+
en: "Android will become a locked-down platform",
28+
ca: "Android es convertir\u00E0 en una plataforma tancada",
29+
cs: "Android will become a locked-down platform in",
30+
de: "Android wird eine geschlossene Plattform werden.",
31+
el: "\u03A4\u03BF Android \u03B8\u03B1 \u03B3\u03AF\u03BD\u03B5\u03B9 \u03BC\u03AF\u03B1 \u03BA\u03BB\u03B5\u03B9\u03C3\u03C4\u03AE \u03C0\u03BB\u03B1\u03C4\u03C6\u03CC\u03C1\u03BC\u03B1",
32+
es: "Android se convertir\u00E1 en una plataforma cerrada",
33+
fr: "Android deviendra une plateforme verrouill\u00E9e",
34+
id: "Android akan menjadi platform yang terkunci.",
35+
it: "Android diventer\u00E0 una piattaforma bloccata",
36+
ko: "Android\uAC00 \uD3D0\uC1C7\uB41C \uD50C\uB7AB\uD3FC\uC774 \uB418\uAE30 \uAE4C\uC9C0 \uB0A8\uC740 \uC2DC\uAC04:",
37+
pl: "Android stanie si\u0119 platform\u0105 zamkni\u0119t\u0105",
38+
"pt-BR": "O Android se tornar\u00E1 uma plataforma fechada",
39+
ru: "Android \u0441\u0442\u0430\u043D\u0435\u0442 \u0437\u0430\u043A\u0440\u044B\u0442\u043E\u0439 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u043E\u0439 \u0447\u0435\u0440\u0435\u0437",
40+
sk: "Android sa stane uzamknutou platformou",
41+
th: "Android\u0E08\u0E30\u0E40\u0E1B\u0E47\u0E19\u0E41\u0E1E\u0E25\u0E15\u0E1F\u0E2D\u0E23\u0E4C\u0E21\u0E17\u0E35\u0E48\u0E16\u0E39\u0E01\u0E25\u0E47\u0E2D\u0E01",
42+
tr: "Android k\u0131s\u0131tl\u0131 bir platform haline gelecek.",
43+
uk: "Android \u0441\u0442\u0430\u043D\u0435 \u0437\u0430\u043A\u0440\u0438\u0442\u043E\u044E \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u043E\u044E",
44+
"zh-CN": "\u5B89\u5353\u5C06\u6210\u4E3A\u4E00\u4E2A\u5C01\u95ED\u5E73\u53F0",
45+
"zh-TW": "\u5012\u6578 Android \u5373\u5C07\u6DEA\u70BA\u756B\u5730\u70BA\u7262\u3001\u687C\u68B0\u6EFF\u76C8\u7684\u5C01\u9589\u5E73\u81FA"
46+
};
47+
48+
// ── Parse query parameters from the script's own src URL ──────────────
49+
function getScriptParams() {
50+
var params = {};
51+
try {
52+
var src = document.currentScript && document.currentScript.src;
53+
if (!src) return params;
54+
var q = src.indexOf("?");
55+
if (q === -1) return params;
56+
var pairs = src.substring(q + 1).split("&");
57+
for (var i = 0; i < pairs.length; i++) {
58+
var kv = pairs[i].split("=");
59+
params[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || "");
60+
}
61+
} catch (e) {}
62+
return params;
63+
}
64+
65+
var params = getScriptParams();
66+
67+
// ── Determine locale ──────────────────────────────────────────────────
68+
function resolveLocale(tag) {
69+
if (!tag) return "en";
70+
if (messages[tag]) return tag;
71+
var lower = tag.toLowerCase();
72+
for (var key in messages) {
73+
if (key.toLowerCase() === lower) return key;
74+
}
75+
var base = tag.split("-")[0].toLowerCase();
76+
if (messages[base]) return base;
77+
for (var key2 in messages) {
78+
if (key2.toLowerCase().split("-")[0] === base) return key2;
79+
}
80+
return "en";
81+
}
82+
83+
var locale = resolveLocale(
84+
params.lang ||
85+
document.documentElement.lang ||
86+
navigator.language ||
87+
navigator.userLanguage
88+
);
89+
90+
// ── Size variant ──────────────────────────────────────────────────────
91+
var size = params.size === "mini" ? "mini" : "normal";
92+
93+
// ── Link ────────────────────────────────────────────────────────────
94+
var linkParam = params.link;
95+
var linkUrl = linkParam === "none" ? null : (linkParam || "https://keepandroidopen.org");
96+
97+
// ── Close button ────────────────────────────────────────────────────
98+
var showClose = params.hidebutton !== "off";
99+
var storageKey = "kao-banner-hidden";
100+
101+
// ── Inject CSS ────────────────────────────────────────────────────────
102+
var cssNormal =
103+
".kao-banner{" +
104+
"position:relative;" +
105+
"font-variant-numeric:tabular-nums;" +
106+
"background:linear-gradient(180deg,#d32f2f 0%,#b71c1c 100%);" +
107+
"border-bottom:4px solid #801313;" +
108+
"color:#fff;" +
109+
"font-family:'Arial Black',sans-serif;" +
110+
"font-weight:900;" +
111+
"text-transform:uppercase;" +
112+
"letter-spacing:2px;" +
113+
"font-size:1.5rem;" +
114+
"text-align:center;" +
115+
"text-shadow:" +
116+
"0px 1px 0px #9e1a1a," +
117+
"0px 2px 0px #8a1515," +
118+
"0px 3px 0px #751111," +
119+
"0px 4px 0px #5e0d0d," +
120+
"0px 6px 10px rgba(0,0,0,0.5);" +
121+
"animation:kao-pulse 2s infinite;" +
122+
"padding:0.5rem 2.5rem;" +
123+
"line-height:1.6;" +
124+
"box-sizing:border-box;" +
125+
"}";
126+
127+
var cssMini =
128+
".kao-banner{" +
129+
"position:relative;" +
130+
"font-variant-numeric:tabular-nums;" +
131+
"background:linear-gradient(180deg,#d32f2f 0%,#b71c1c 100%);" +
132+
"border-bottom:2px solid #801313;" +
133+
"color:#fff;" +
134+
"font-family:'Arial Black',sans-serif;" +
135+
"font-weight:900;" +
136+
"text-transform:uppercase;" +
137+
"letter-spacing:1px;" +
138+
"font-size:0.75rem;" +
139+
"text-align:center;" +
140+
"text-shadow:" +
141+
"0px 1px 0px #9e1a1a," +
142+
"0px 2px 0px #8a1515," +
143+
"0px 3px 5px rgba(0,0,0,0.4);" +
144+
"animation:kao-pulse 2s infinite;" +
145+
"padding:0.25rem 1.5rem;" +
146+
"line-height:1.4;" +
147+
"box-sizing:border-box;" +
148+
"}";
149+
150+
var cssCommon =
151+
".kao-banner a{color:#fff;text-decoration:none;}" +
152+
".kao-banner a:hover{text-decoration:underline;}" +
153+
".kao-banner-close{" +
154+
"position:absolute;" +
155+
"right:0.5rem;" +
156+
"top:50%;" +
157+
"transform:translateY(-50%);" +
158+
"background:none;" +
159+
"border:none;" +
160+
"color:#fff;" +
161+
"font-size:0.8em;" +
162+
"cursor:pointer;" +
163+
"opacity:0.7;" +
164+
"padding:0.25rem 0.5rem;" +
165+
"line-height:1;" +
166+
"text-shadow:none;" +
167+
"}" +
168+
".kao-banner-close:hover{opacity:1;}" +
169+
"@keyframes kao-pulse{" +
170+
"0%{box-shadow:0 0 0 0 rgba(211,47,47,0.7)}" +
171+
"70%{box-shadow:0 0 0 15px rgba(211,47,47,0)}" +
172+
"100%{box-shadow:0 0 0 0 rgba(211,47,47,0)}" +
173+
"}";
174+
175+
var style = document.createElement("style");
176+
style.textContent = (size === "mini" ? cssMini : cssNormal) + cssCommon;
177+
document.head.appendChild(style);
178+
179+
// ── Check if previously dismissed ────────────────────────────────────
180+
try {
181+
if (localStorage.getItem(storageKey)) return;
182+
} catch (e) {}
183+
184+
// ── Create banner DOM ─────────────────────────────────────────────────
185+
var banner = document.createElement("div");
186+
banner.className = "kao-banner";
187+
188+
var messageText = messages[locale] || messages.en;
189+
190+
if (linkUrl) {
191+
var link = document.createElement("a");
192+
link.href = linkUrl;
193+
link.target = "_blank";
194+
link.rel = "noopener";
195+
link.textContent = messageText;
196+
banner.appendChild(link);
197+
} else {
198+
banner.appendChild(document.createTextNode(messageText));
199+
}
200+
201+
banner.appendChild(document.createElement("br"));
202+
203+
var countdownSpan = document.createElement("span");
204+
countdownSpan.textContent = "\u00A0";
205+
banner.appendChild(countdownSpan);
206+
207+
// Close button
208+
if (showClose) {
209+
var closeBtn = document.createElement("button");
210+
closeBtn.className = "kao-banner-close";
211+
closeBtn.setAttribute("aria-label", "Close");
212+
closeBtn.textContent = "\u2715";
213+
closeBtn.addEventListener("click", function () {
214+
banner.style.display = "none";
215+
try { localStorage.setItem(storageKey, "1"); } catch (e) {}
216+
});
217+
banner.appendChild(closeBtn);
218+
}
219+
220+
// Insert into target element (by id) or prepend to <body>
221+
var targetId = params.id;
222+
if (targetId) {
223+
var target = document.getElementById(targetId);
224+
if (target) {
225+
target.appendChild(banner);
226+
} else {
227+
document.body.insertBefore(banner, document.body.firstChild);
228+
}
229+
} else {
230+
document.body.insertBefore(banner, document.body.firstChild);
231+
}
232+
233+
// ── Countdown logic ───────────────────────────────────────────────────
234+
var countDownDate = new Date("Sep 1, 2026 00:00:00").getTime();
235+
236+
var formatter = new Intl.RelativeTimeFormat(locale, { style: "narrow" });
237+
238+
var pfx = new Array(4);
239+
var sfx = new Array(4);
240+
241+
function getOffset(unit) {
242+
switch (unit) {
243+
case "day": return 0;
244+
case "hour": return 1;
245+
case "minute": return 2;
246+
case "second": return 3;
247+
}
248+
}
249+
250+
function extractCommon(p, c, reverse) {
251+
var s = 0;
252+
var w = 0;
253+
var i = reverse ? p.length - 1 : 0;
254+
var j = reverse ? c.length - 1 : 0;
255+
var pEnd = reverse ? 0 : p.length;
256+
var cEnd = reverse ? 0 : c.length;
257+
var chr;
258+
while (
259+
(reverse ? i >= pEnd : i < pEnd) &&
260+
(reverse ? j >= cEnd : j < cEnd) &&
261+
(chr = p[reverse ? i-- : i++]) === c[reverse ? j-- : j++]
262+
) {
263+
w = chr === " " ? w + 1 : 0;
264+
s++;
265+
}
266+
return s - w;
267+
}
268+
269+
function cacheFormattingInfo(value, unit) {
270+
var p = formatter.formatToParts(value, unit);
271+
if (!p.length) return;
272+
var c = formatter.formatToParts(-value, unit);
273+
274+
var offset = getOffset(unit);
275+
if (p[0].type === "literal" && (!c.length || c[0].type !== "literal" || !c[0].value.endsWith(p[0].value))) {
276+
pfx[offset] = p[0].value.length;
277+
}
278+
if (p[p.length - 1].type === "literal") {
279+
if (!c.length || c[c.length - 1].type !== "literal") {
280+
sfx[offset] = p[p.length - 1].value.length;
281+
} else if (!c[c.length - 1].value.startsWith(p[p.length - 1].value)) {
282+
sfx[offset] =
283+
p[p.length - 1].value.length -
284+
extractCommon(p[p.length - 1].value, c[c.length - 1].value, false);
285+
}
286+
}
287+
}
288+
289+
cacheFormattingInfo(1, "day");
290+
cacheFormattingInfo(2, "hour");
291+
cacheFormattingInfo(3, "minute");
292+
cacheFormattingInfo(4, "second");
293+
294+
function getLocalizedUnit(value, unit, trimConjunction, trimSuffix) {
295+
var offset = getOffset(unit);
296+
var string = formatter.format(value, unit);
297+
var p = pfx[offset];
298+
var s = sfx[offset];
299+
return string.slice(
300+
trimConjunction && p || (p == 1 && string[0] === "+") ? pfx[offset] : 0,
301+
trimSuffix && s ? -sfx[offset] : string.length
302+
);
303+
}
304+
305+
var remaining = new Array(7);
306+
var separator = " ";
307+
var timer = null;
308+
309+
function updateBanner() {
310+
var now = new Date().getTime();
311+
var distance = countDownDate - now;
312+
313+
var days = Math.floor(distance / (1000 * 60 * 60 * 24));
314+
var hours = Math.floor(
315+
(distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
316+
);
317+
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
318+
var seconds = Math.floor((distance % (1000 * 60)) / 1000);
319+
320+
var parts = 0;
321+
remaining[0] = days > 0 ? getLocalizedUnit(days, "day", parts++, true) : null;
322+
remaining[1] = parts ? separator : null;
323+
remaining[2] =
324+
parts || hours > 0
325+
? getLocalizedUnit(hours, "hour", parts++, true)
326+
: null;
327+
remaining[3] = parts ? separator : null;
328+
remaining[4] =
329+
parts || minutes > 0
330+
? getLocalizedUnit(minutes, "minute", parts++, true)
331+
: null;
332+
remaining[5] = parts ? separator : null;
333+
remaining[6] = getLocalizedUnit(seconds, "second", parts++, false);
334+
335+
countdownSpan.textContent = remaining.join("");
336+
337+
if (distance < 0) {
338+
clearInterval(timer);
339+
}
340+
}
341+
342+
timer = setInterval(updateBanner, 1000);
343+
updateBanner();
344+
})();

0 commit comments

Comments
 (0)