diff --git a/beango/index.html b/beango/index.html index 532ec06..2a2fc01 100644 --- a/beango/index.html +++ b/beango/index.html @@ -17,6 +17,21 @@
+ +
+ + +
@@ -51,12 +66,6 @@ -
@@ -441,7 +450,7 @@ diff --git a/beango/js/beango.js b/beango/js/beango.js index a9f432a..44ef7bc 100644 --- a/beango/js/beango.js +++ b/beango/js/beango.js @@ -1392,6 +1392,9 @@ export function explodeBeans(event) { // --- Consolidated Event Listeners --- document.addEventListener('DOMContentLoaded', () => { + // Apply shared settings from URL if present before loading + tryApplySettingsFromUrl(); + // Load state first thing loadFromLocalStorage(); @@ -1600,7 +1603,550 @@ function checkWinConditions() { } -// --- Settings Import/Export --- +// --- Settings Import/Export & Sharing --- + +// v1 fallback: base64url helpers (kept for backward compatibility) +function base64UrlEncode(text) { + const utf8 = new TextEncoder().encode(text); + let binary = ''; + utf8.forEach(byte => binary += String.fromCharCode(byte)); + const base64 = btoa(binary); + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); +} + +function base64UrlDecode(text) { + let base64 = text.replace(/-/g, '+').replace(/_/g, '/'); + while (base64.length % 4) base64 += '='; + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return new TextDecoder().decode(bytes); +} + +// v2: Lightweight LZ-based compression for URI component +// Adapted from LZ-String (MIT) compressToEncodedURIComponent/decompressFromEncodedURIComponent +const LZ = (function() { + const f = String.fromCharCode; + const keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"; + const baseReverseDic = {}; + + function getBaseValue(alphabet, character) { + if (!baseReverseDic[alphabet]) { + baseReverseDic[alphabet] = {}; + for (let i = 0; i < alphabet.length; i++) { + baseReverseDic[alphabet][alphabet.charAt(i)] = i; + } + } + return baseReverseDic[alphabet][character]; + } + + function compressToEncodedURIComponent(input) { + if (input == null) return ''; + return _compress(input, 6, function(a){ return keyStrUriSafe.charAt(a); }); + } + + function _compress(uncompressed, bitsPerChar, getCharFromInt) { + if (uncompressed == null) return ''; + let i, value, + context_dictionary = {}, + context_dictionaryToCreate = {}, + context_c = '', + context_wc = '', + context_w = '', + context_enlargeIn = 2, // Compensate for the first entry which should not count + context_dictSize = 3, + context_numBits = 2, + context_data = [], + context_data_val = 0, + context_data_position = 0, + ii; + + for (ii = 0; ii < uncompressed.length; ii += 1) { + context_c = uncompressed.charAt(ii); + if (!Object.prototype.hasOwnProperty.call(context_dictionary, context_c)) { + context_dictionary[context_c] = context_dictSize++; + context_dictionaryToCreate[context_c] = true; + } + + context_wc = context_w + context_c; + if (Object.prototype.hasOwnProperty.call(context_dictionary, context_wc)) { + context_w = context_wc; + } else { + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) { + if (context_w.charCodeAt(0) < 256) { + for (i = 0; i < context_numBits; i++) { + context_data_val = (context_data_val << 1); + if (context_data_position == bitsPerChar - 1) { + context_data_position = 0; + context_data.push(getCharFromInt(context_data_val)); + context_data_val = 0; + } else { + context_data_position++; + } + } + value = context_w.charCodeAt(0); + for (i = 0; i < 8; i++) { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) { + context_data_position = 0; + context_data.push(getCharFromInt(context_data_val)); + context_data_val = 0; + } else { + context_data_position++; + } + value = value >> 1; + } + } else { + value = 1; + for (i = 0; i < context_numBits; i++) { + context_data_val = (context_data_val << 1) | value; + if (context_data_position == bitsPerChar - 1) { + context_data_position = 0; + context_data.push(getCharFromInt(context_data_val)); + context_data_val = 0; + } else { + context_data_position++; + } + value = 0; + } + value = context_w.charCodeAt(0); + for (i = 0; i < 16; i++) { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) { + context_data_position = 0; + context_data.push(getCharFromInt(context_data_val)); + context_data_val = 0; + } else { + context_data_position++; + } + value = value >> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + delete context_dictionaryToCreate[context_w]; + } else { + value = context_dictionary[context_w]; + for (i = 0; i < context_numBits; i++) { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) { + context_data_position = 0; + context_data.push(getCharFromInt(context_data_val)); + context_data_val = 0; + } else { + context_data_position++; + } + value = value >> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + // Add wc to the dictionary. + context_dictionary[context_wc] = context_dictSize++; + context_w = String(context_c); + } + } + + // Output the code for w. + if (context_w !== '') { + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) { + if (context_w.charCodeAt(0) < 256) { + for (i = 0; i < context_numBits; i++) { + context_data_val = (context_data_val << 1); + if (context_data_position == bitsPerChar - 1) { + context_data_position = 0; + context_data.push(getCharFromInt(context_data_val)); + context_data_val = 0; + } else { + context_data_position++; + } + } + value = context_w.charCodeAt(0); + for (i = 0; i < 8; i++) { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) { + context_data_position = 0; + context_data.push(getCharFromInt(context_data_val)); + context_data_val = 0; + } else { + context_data_position++; + } + value = value >> 1; + } + } else { + value = 1; + for (i = 0; i < context_numBits; i++) { + context_data_val = (context_data_val << 1) | value; + if (context_data_position == bitsPerChar - 1) { + context_data_position = 0; + context_data.push(getCharFromInt(context_data_val)); + context_data_val = 0; + } else { + context_data_position++; + } + value = 0; + } + value = context_w.charCodeAt(0); + for (i = 0; i < 16; i++) { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) { + context_data_position = 0; + context_data.push(getCharFromInt(context_data_val)); + context_data_val = 0; + } else { + context_data_position++; + } + value = value >> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + delete context_dictionaryToCreate[context_w]; + } else { + value = context_dictionary[context_w]; + for (i = 0; i < context_numBits; i++) { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) { + context_data_position = 0; + context_data.push(getCharFromInt(context_data_val)); + context_data_val = 0; + } else { + context_data_position++; + } + value = value >> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + } + + // Mark the end of the stream + value = 2; + for (i = 0; i < context_numBits; i++) { + context_data_val = (context_data_val << 1) | (value & 1); + if (context_data_position == bitsPerChar - 1) { + context_data_position = 0; + context_data.push(getCharFromInt(context_data_val)); + context_data_val = 0; + } else { + context_data_position++; + } + value = value >> 1; + } + + // Flush the last char + while (true) { + context_data_val = (context_data_val << 1); + if (context_data_position == bitsPerChar - 1) { + context_data.push(getCharFromInt(context_data_val)); + break; + } else { + context_data_position++; + } + } + return context_data.join(''); + } + + function decompressFromEncodedURIComponent(input) { + if (input == null) return ''; + if (input === '') return null; + input = input.replace(/ /g, '+'); + return _decompress(input.length, 32, function(index) { return getBaseValue(keyStrUriSafe, input.charAt(index)); }); + } + + function _decompress(length, resetValue, getNextValue) { + let dictionary = [] + , next + , enlargeIn = 4 + , dictSize = 4 + , numBits = 3 + , entry = '' + , result = [] + , i + , w + , bits, resb, maxpower, power + , c + , data = {val:getNextValue(0), position:resetValue, index:1}; + + for (i = 0; i < 3; i += 1) { + dictionary[i] = i; + } + + bits = 0; + maxpower = Math.pow(2, 2); + power = 1; + while (power != maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + switch (next = bits) { + case 0: + bits = 0; + maxpower = Math.pow(2, 8); + power = 1; + while (power != maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + case 1: + bits = 0; + maxpower = Math.pow(2, 16); + power = 1; + while (power != maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + case 2: + return ''; + } + dictionary[3] = c; + w = c; + result.push(c); + while (true) { + if (data.index > length) { + return ''; + } + + bits = 0; + maxpower = Math.pow(2, numBits); + power = 1; + while (power != maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + switch (c = bits) { + case 0: + bits = 0; + maxpower = Math.pow(2, 8); + power = 1; + while (power != maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + dictionary[dictSize++] = f(bits); + c = dictSize - 1; + enlargeIn--; + break; + case 1: + bits = 0; + maxpower = Math.pow(2, 16); + power = 1; + while (power != maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = resetValue; + data.val = getNextValue(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + dictionary[dictSize++] = f(bits); + c = dictSize - 1; + enlargeIn--; + break; + case 2: + return result.join(''); + } + + if (enlargeIn == 0) { + enlargeIn = Math.pow(2, numBits); + numBits++; + } + + if (dictionary[c]) { + entry = dictionary[c]; + } else { + if (c === dictSize) { + entry = w + w.charAt(0); + } else { + return ''; + } + } + result.push(entry); + + // Add w+entry[0] + dictionary[dictSize++] = w + entry.charAt(0); + enlargeIn--; + w = entry; + + if (enlargeIn == 0) { + enlargeIn = Math.pow(2, numBits); + numBits++; + } + } + } + + return { + compressToEncodedURIComponent, + decompressFromEncodedURIComponent, + }; +})(); + +function collectAllBeangoSettings() { + const settings = {}; + const prefix = 'beango_'; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(prefix)) { + settings[key] = localStorage.getItem(key); + } + } + return settings; +} + +export async function copyShareLink() { + try { + const data = collectAllBeangoSettings(); + const json = JSON.stringify(data); + // v2 compressed, URL-safe + const encoded = LZ.compressToEncodedURIComponent(json); + const shareUrl = `${location.origin}${location.pathname}#s=v2:${encoded}`; + + let copySucceeded = false; + try { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(shareUrl); + copySucceeded = true; + } + } catch { + // Fall through to legacy copy + } + + if (!copySucceeded) { + const tempInput = document.createElement('input'); + tempInput.value = shareUrl; + tempInput.style.position = 'fixed'; + tempInput.style.top = '-1000px'; + document.body.appendChild(tempInput); + tempInput.focus(); + tempInput.select(); + try { + copySucceeded = document.execCommand('copy'); + } catch { + copySucceeded = false; + } + document.body.removeChild(tempInput); + } + + if (copySucceeded) { + showNotification('Share link copied to clipboard!', 'success'); + } else { + try { window.prompt('Copy this share link:', shareUrl); } catch { /* ignore */ } + showNotification('Could not copy automatically. Link shown for manual copy.', 'warning'); + } + } catch (e) { + console.error('Failed to create share link', e); + showNotification('Failed to create share link.', 'error'); + } +} + +let _sharedSettingsAppliedOnce = false; + +function tryApplySettingsFromUrl() { + try { + const hash = window.location.hash || ''; + const match = hash.match(/^#(?:s|settings)=(.+)$/); + if (!match) return; + + const payload = match[1]; + let json = ''; + if (payload.startsWith('v2:')) { + const body = payload.slice(3); + json = LZ.decompressFromEncodedURIComponent(body) || ''; + } else { + // v1 fallback (base64url) + json = base64UrlDecode(payload); + } + const parsed = JSON.parse(json); + if (typeof parsed !== 'object' || parsed === null) return; + + // Clear existing beango_ keys to avoid stale settings + const prefix = 'beango_'; + const toRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(prefix)) toRemove.push(key); + } + toRemove.forEach(k => localStorage.removeItem(k)); + + let imported = 0; + Object.keys(parsed).forEach(key => { + if (key.startsWith(prefix)) { + const value = parsed[key]; + if (value !== undefined && value !== null) { + localStorage.setItem(key, String(value)); + imported++; + } + } + }); + + if (imported > 0) { + _sharedSettingsAppliedOnce = true; + // Clean the URL hash and reload once to ensure all UI reflects new settings + if (history && history.replaceState) { + history.replaceState(null, '', `${location.pathname}${location.search}`); + } else { + window.location.hash = ''; + } + // Reload synchronously; after reload the hash is clean, so this runs only once + location.reload(); + return; + } + } catch (e) { + console.warn('Failed to apply settings from URL', e); + showNotification('Invalid share link.', 'warning'); + } +} export function exportSettings() { const settingsToExport = {}; const prefix = 'beango_';