/** * !! DOCUMENTATION: https://kirbindustries.gitbook.io/polygol/developers/developer-materials/gurapp-api !! * * Gurasuraisu API for Gurapps * This helper script allows an iframe (Gurapp) to safely communicate * with the parent Gurasuraisu (Polygol) window and use its core functions. */ const isInsideGurasuraisu = window.self !== window.top; let _mediaControlActions = {}; let _actionRequestHandlers = {}; const _dialogCallbacks = {}; // For handling dialog responses let _dialogRequestId = 0; // For tracking dialog requests const _myActiveActivities = new Set(); // Tracks this app's active activities // Gurasuraisu Font and Cursor Injection // This block runs as soon as the script is loaded by the Gurapp. (function() { const style = document.createElement('style'); let css = ` /* Inject Font Faces */ /* Inter */ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); /* Material Symbols */ @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,0'); /* Roboto */ @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100..900&display=swap'); /* Bricolage Grotesque */ @import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap'); /* DynaPuff */ @import url('https://fonts.googleapis.com/css2?family=DynaPuff:wght@400..700&display=swap'); /* Domine */ @import url('https://fonts.googleapis.com/css2?family=Domine:wght@400..700&display=swap'); /* Climate Crisis */ @import url('https://fonts.googleapis.com/css2?family=Climate+Crisis&display=swap'); /* JetBrains Mono */ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@100..800&display=swap'); /* DotGothic16 (400) */ @import url('https://fonts.googleapis.com/css2?family=DotGothic16&display=swap'); /* Playpen Sans */ @import url('https://fonts.googleapis.com/css2?family=Playpen+Sans:wght@100..800&display=swap'); /* Jaro */ @import url('https://fonts.googleapis.com/css2?family=Jaro:opsz@6..72&display=swap'); /* Doto */ @import url('https://fonts.googleapis.com/css2?family=Doto:wght@400;700&display=swap'); /* Nunito */ @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@200..900&display=swap'); @font-face { font-family: 'Open Runde'; font-style: normal; font-weight: 400; src: url('https://cdn.jsdelivr.net/gh/lauridskern/open-runde@main/src/web/OpenRunde-Regular.woff2') format('woff2'); } @font-face { font-family: 'Open Runde'; font-style: normal; font-weight: 500; src: url('https://cdn.jsdelivr.net/gh/lauridskern/open-runde@main/src/web/OpenRunde-Medium.woff2') format('woff2'); } @font-face { font-family: 'Open Runde'; font-style: normal; font-weight: 700; src: url('https://cdn.jsdelivr.net/gh/lauridskern/open-runde@main/src/web/OpenRunde-Semibold.woff2') format('woff2'); } @font-face { font-family: 'Open Runde'; font-style: normal; font-weight: 800; src: url('https://cdn.jsdelivr.net/gh/lauridskern/open-runde@main/src/web/OpenRunde-Bold.woff2') format('woff2'); } @font-face { font-family: 'Inter Numeric'; src: url('/assets/fonts/InterNumeric.ttf') format('truetype-variations'); font-weight: 100 900; /* Define the supported variable weight range */ font-style: normal; } * { -webkit-tap-highlight-color: transparent; } *::-webkit-scrollbar { width: 8px; /* Thin scrollbar */ } *::-webkit-scrollbar-track { background: transparent; } *::-webkit-scrollbar-thumb { background-color: var(--search-background); border-radius: 50px; } /* Set Font Faces */ h1, h2, h3, h4, h5, h6 { font-family: 'Open Runde', sans-serif; } .material-symbols-rounded { font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 24; vertical-align: middle; } :root { --edge-refraction-filter: url('#edge-refraction-only'); --sun-shadow: 0 0 0 0 transparent; /* Dark Theme (Default) Variables */ --background-color-dark: #1c1c1c; --background-color-dark-tr: rgba(28, 28, 28, 0.7); --text-color-dark: #f9f9f9; --secondary-text-color-dark: rgba(255, 255, 255, 0.7); --modal-background-dark: rgba(51, 51, 51, 0.8); --modal-transparent-dark: rgba(51, 51, 51, 0.7); --search-background-dark: rgba(51, 51, 51, 0.5); --dark-overlay: rgba(51, 51, 51, 0.2); --dark-transparent: rgba(255, 255, 255, 0.1); --glass-border-dark: rgba(100, 100, 100, 0.2); /* Light Theme Variables */ --background-color-light: #f0f0f0; --background-color-light-tr: rgba(240, 240, 240, 0.7); --text-color-light: #333333; --secondary-text-color-light: rgba(0, 0, 0, 0.7); --modal-background-light: rgba(220, 220, 220, 0.8); --modal-transparent-light: rgba(240, 240, 240, 0.7); --search-background-light: rgba(220, 220, 220, 0.5); --light-overlay: rgba(220, 220, 220, 0.2); --light-transparent: rgba(255, 255, 255, 0.1); --glass-border-light: rgba(200, 200, 200, 0.2); /* High Contrast Dark Theme Variables */ --background-color-dark-highcontrast: #1c1c1c; --background-color-dark-tr-highcontrast: #1c1c1c; --text-color-dark-highcontrast: #f9f9f9; --secondary-text-color-dark-highcontrast: #b3b3b3; --modal-background-dark-highcontrast: #333333; --modal-transparent-dark-highcontrast: #333333; --search-background-dark-highcontrast: #333333; --dark-overlay-highcontrast: #1c1c1c; --dark-transparent-highcontrast: #000000; /* High Contrast Light Theme Variables */ --background-color-light-highcontrast: #f0f0f0; --background-color-light-tr-highcontrast: #f0f0f0; --text-color-light-highcontrast: #333333; --secondary-text-color-light-highcontrast: #4d4d4d; --modal-background-light-highcontrast: #dcdcdc; --modal-transparent-light-highcontrast: #f0f0f0; --search-background-light-highcontrast: #dcdcdc; --light-overlay-highcontrast: #f0f0f0; --light-transparent-highcontrast: #ffffff; /* Base Variables */ --base-font-size: clamp(16px, 2vw + 1rem, 24px); /* Default to Dark Theme */ --background-color: var(--background-color-dark); --background-color-tr: var(--background-color-dark-tr); --background-color-tr-op: var(--background-color-light-tr); --text-color: var(--text-color-dark); --secondary-text-color: var(--secondary-text-color-dark); --modal-background: var(--modal-background-dark); --modal-transparent: var(--modal-transparent-dark); --search-background: var(--search-background-dark); --search-background-op: var(--search-background-light); --overlay-color: var(--dark-overlay); --overlay-color-op: var(--light-overlay); --transparent-color: var(--dark-transparent); --glass-border: var(--glass-border-dark); } body.light-theme { --background-color: var(--background-color-light); --background-color-tr: var(--background-color-light-tr); --background-color-tr-op: var(--background-color-dark-tr); --text-color: var(--text-color-light); --secondary-text-color: var(--secondary-text-color-light); --modal-background: var(--modal-background-light); --modal-transparent: var(--modal-transparent-light); --search-background: var(--search-background-light); --search-background-op: var(--search-background-dark); --overlay-color: var(--light-overlay); --overlay-color-op: var(--dark-overlay); --transparent-color: var(--light-transparent); --glass-border: var(--glass-border-light); --polygol-cursor-visible: var(--polygol-cursor-light); } /* For dark theme (default) with high contrast */ html.gurasuraisu-high-contrast body:not(.light-theme) { --background-color-tr: var(--background-color-dark-tr-highcontrast); --background-color-tr-op: var(--background-color-light-tr-highcontrast); --secondary-text-color: var(--secondary-text-color-dark-highcontrast); --modal-background: var(--modal-background-dark-highcontrast); --modal-transparent: var(--modal-transparent-dark-highcontrast); --search-background: var(--search-background-dark-highcontrast); --search-background-op: var(--search-background-light-highcontrast); --overlay-color: var(--dark-overlay-highcontrast); --overlay-color-op: var(--light-overlay-highcontrast); --transparent-color: var(--dark-transparent-highcontrast); --glass-border: var(--secondary-text-color-dark-highcontrast); } /* For light theme with high contrast */ html.gurasuraisu-high-contrast body.light-theme { --background-color-tr: var(--background-color-light-tr-highcontrast); --background-color-tr-op: var(--background-color-dark-tr-highcontrast); --secondary-text-color: var(--secondary-text-color-light-highcontrast); --modal-background: var(--modal-background-light-highcontrast); --modal-transparent: var(--modal-transparent-light-highcontrast); --search-background: var(--search-background-light-highcontrast); --search-background-op: var(--search-background-dark-highcontrast); --overlay-color: var(--light-overlay-highcontrast); --overlay-color-op: var(--dark-overlay-highcontrast); --transparent-color: var(--light-transparent-highcontrast); --glass-border: var(--secondary-text-color-light-highcontrast); } /* Universal backdrop-filter removal for high contrast */ html.gurasuraisu-high-contrast * { backdrop-filter: none !important; } html.gurasuraisu-glass-disabled { --edge-refraction-filter: blur(17.5px); /* Frosted glass appearance */ } :root.standalone { --background-color-dark-tr: var(--background-color-dark); --background-color-light-tr: var(--background-color-light); --edge-refraction-filter: blur(17.5px); /* Frosted glass appearance */ } /* When animations are disabled */ .reduce-animations * { /* Disable all animations */ animation: none !important; /* Disable all transitions except opacity */ transition: opacity 0.3s ease !important; transition-property: opacity !important; } /* Special handling for clickable elements */ .reduce-animations [onclick], .reduce-animations button, .reduce-animations a, .reduce-animations input[type="button"], .reduce-animations input[type="submit"], .reduce-animations .clickable { /* Keep initial state but remove transition */ transform: scale(1) !important; transition: opacity 0.3s ease !important; } /* Keep active state functional but without animation */ .reduce-animations [onclick]:active, .reduce-animations button:active, .reduce-animations a:active, .reduce-animations input[type="button"]:active, .reduce-animations input[type="submit"]:active, .reduce-animations .clickable:active { /* Apply scale instantly without transition */ transform: scale(0.98) !important; transition: none !important; } /* For all clickable elements */ [onclick], button, a, input[type="button"], input[type="submit"], .clickable { cursor: pointer; transform: scale(1); transition: transform 0.15s cubic-bezier(0.2, 0, 0.38, 0.9); } /* Active effect (when clicking down) */ [onclick]:active, button:active, a:active, input[type="button"]:active, input[type="submit"]:active, .clickable:active { transform: scale(0.96); transition: transform 0.1s cubic-bezier(0.2, 0, 0.38, 0.9); filter: brightness(1.5); } input[type="color"] { -webkit-appearance: none; appearance: none; border: 1px solid var(--glass-border); width: 30px; height: 30px; padding: 0; background: none; border-radius: 999px; cursor: pointer; overflow: hidden; } input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; } input[type="color"]::-webkit-color-swatch { border: none; border-radius: 999px; } input[type="color"]::-moz-color-swatch { border: 1px solid var(--glass-border); border-radius: 999px; } input[type="checkbox"] { appearance: none; width: 56px; height: 32px; background-color: var(--search-background); border-radius: 16px; position: relative; cursor: pointer; transition: all 0.3s cubic-bezier(.3, 1.2, .64, 1); border: 1px solid var(--glass-border); cursor: pointer; box-shadow: var(--sun-shadow); } input[type="checkbox"]::before { content: ''; position: absolute; width: 20px; height: 20px; background-color: var(--secondary-text-color); border-radius: 50%; top: 50%; left: 6.5px; transform: translateY(-50%); border: 1px solid var(--glass-border); box-sizing: border-box; box-shadow: var(--sun-shadow); transition: all 0.3s cubic-bezier(.3, 1.2, .64, 1); } input[type="checkbox"]:checked { background-color: var(--secondary-text-color); } input[type="checkbox"]:checked::before { background-color: var(--overlay-color-op); transform: translateY(-50%); width: 46px; height: 40px; top: 50%; left: 14px; backdrop-filter: saturate(2) blur(2.5px); border-radius: 36px; } select option { background-color: var(--background-color); color: var(--text-color); transition: background-color 0.2s, transform 0.1s; } .toolbar { display: flex; justify-content: center; align-content: center; flex-direction: row; gap: 14px; padding: 15px 20px; background-color: transparent; border: none; position: fixed; top: 0px; left: 50%; transform: translateX(-50%); z-index: 1000; transition: top 0.3s ease; width: 100%; flex-wrap: wrap; height: 80px; } .toolbar.hidden { display: none; } .toolbar::before { content: ""; position: absolute; inset: 0; z-index: -1; } .toolbar::after { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: -2; backdrop-filter: blur(2.5px); mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 50%, rgba(0, 0, 0, 0) 100%); -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 50%, rgba(0, 0, 0, 0) 100%); } .tab-btn { background-color: var(--search-background); color: transparent; border-radius: 50%; padding: 14px 14px; font-size: 0; cursor: pointer; transition: all 0.3s cubic-bezier(.3, 1.2, .64, 1) ! IMPORTANT; display: flex; align-items: center; backdrop-filter: var(--edge-refraction-filter) saturate(2) blur(2.5px); box-shadow: var(--sun-shadow); border: 1px solid var(--glass-border); } .tab-btn.active { background-color: var(--secondary-text-color); color: var(--background-color); border-radius: 35px; corner-shape: superellipse(1.5); font-family: 'Open Runde'; font-weight: 500; padding: 14px 22px 14px 18px; font-size: revert; gap: 12px; } .toolbar .tab-btn .material-symbols-rounded { transition: color 0.3s; color: var(--text-color); font-size: 20px; } .toolbar .tab-btn.active .material-symbols-rounded { color: var(--background-color) !important; } @media (max-width: 800px) { .toolbar { justify-content: flex-start; } .tab-btn { font-size: 0; gap: 0 !important; display: flex; flex-direction: column; justify-content: center; } .tab-btn.active { font-size: 12px; padding: 5px 14px; } .toolbar .tab-btn.active .material-symbols-rounded { font-size: 18px !important; } } @media (max-width: 500px) { .toolbar { justify-content: flex-start; gap: 10px; } .tab-btn { font-size: 0; gap: 0 !important; display: flex; flex-direction: column; justify-content: center; padding: 10px; } .tab-btn.active { font-size: 0; padding: 10px; } .toolbar .tab-btn.active .material-symbols-rounded { font-size: 20px !important; } } `; // Conditionally add Gurasuraisu-specific styles. if (isInsideGurasuraisu) { css += ` /* Add CSS later */ `; } style.textContent = css; document.head.appendChild(style); // Inject SVG Filter for glass effects, overriding if one already exists document.addEventListener('DOMContentLoaded', () => { const existingFilterSvg = document.querySelector('svg > filter#edge-refraction-only'); if (existingFilterSvg) { const parentSvg = existingFilterSvg.closest('svg'); if (parentSvg) parentSvg.remove(); } const svgFilterHtml = ` `; document.body.insertAdjacentHTML('afterbegin', svgFilterHtml); }); })(); // Native JS solutions for when the app is running outside of Polygol const _fallbacks = { showPopup: function(message) { // A simple, non-blocking "toast" notification fallback const toast = document.createElement('div'); toast.textContent = message; toast.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: #333; color: white; padding: 10px 20px; border-radius: 20px; z-index: 9999; transition: opacity 0.5s; font-family: sans-serif; `; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 500); }, 3000); }, showConfirm: function(message) { return window.confirm(message); }, showPrompt: function(message, defaultValue) { return window.prompt(message, defaultValue); }, // For functions that have no standalone equivalent, we can just log a warning. default: function(functionName) { console.warn(`Gurasuraisu API: '${functionName}' is only available inside the Polygol environment.`); } }; // Internal State for Sound let _autoSoundEnabled = true; // App dev override let _systemSoundsAllowed = true; // User preference from System const Gurasuraisu = { /** * Internal helper to send a structured message to the parent window. * @param {string} functionName - The name of the Gurasuraisu function to call. * @param {Array} args - An array of arguments to pass to the function. */ _call: function(functionName, args = []) { if (isInsideGurasuraisu) { window.parent.postMessage({ action: 'callGurasuraisuFunc', functionName: functionName, args: args }, '*'); } else { // Use the fallback if it exists, otherwise use the default fallback const fallback = _fallbacks[functionName] || (() => _fallbacks.default(functionName)); fallback.apply(this, args); } }, // --- Public API Functions --- /** * Shows a temporary popup message at the bottom of the screen. * @param {string} message - The text to display in the popup. */ showPopup: function(message) { this._call('showPopup', [message]); }, /** * Shows a more advanced notification on-screen and in the notification shade. * @param {string} message - The text to display. * @param {object} [options] - Optional parameters like icon and button text. */ showNotification: function(message, options = {}) { // Note: 'buttonAction' functions cannot be passed from the iframe. // The parent window handles all actions. this._call('showNotification', [message, options]); }, /** * Plays a system UI sound. * @param {string} type - 'select', 'toggle', 'check', 'error', 'success', 'open', 'close', 'type' */ playSound: function(type) { if (_systemSoundsAllowed) { this._call('playUiSound', [type]); } }, /** * Configures the automatic UI sound detection for this Gurapp. * Use this to disable automatic click sounds if you want to handle them manually. * @param {object} config * @param {boolean} config.auto - Set to false to disable auto-click detection. */ configureSounds: function(config) { if (config && typeof config.auto === 'boolean') { _autoSoundEnabled = config.auto; } }, /** * Sets the UI on the connected Waves remote (Second Screen). * @param {Array} components - Array of widget definitions. * Example: [{ type: 'button', id: 'btn1', label: 'Next Slide', icon: 'skip_next' }] */ setRemoteUI: function(components) { this._call('setRemoteUI', [components]); }, /** * Sends a partial update for specific components on the remote UI. * @param {object} updates - Key-value pairs where Key is the component ID and Value is the new value. */ sendRemoteUpdate: function(updates) { this._call('sendRemoteUpdate', [updates]); }, /** * Listen for actions triggered from the Waves remote. * @param {function} callback - Function to handle the event (id, value). */ onRemoteAction: function(callback) { window.addEventListener('message', (event) => { if (event.source !== window.parent) return; if (event.data.type === 'remote-action') { callback(event.data.id, event.data.value); } }); }, /** * Listen for a request to refresh/send the remote UI. * Triggered when the app is launched via Mini Apps but is already running. * @param {function} callback - Function to execute (usually calling setRemoteUI). */ onRequestRemoteUI: function(callback) { window.addEventListener('message', (event) => { if (event.source !== window.parent) return; if (event.data.type === 'requestRemoteUI') { callback(); } }); }, /** * Requests a file upload from the user. * In Polygol, this triggers both the local file picker and a request to the Waves Remote. * @param {object} options * @param {string} options.accept - MIME types (e.g., 'image/*', '.png'). * @param {boolean} options.multiple - Allow multiple files. * @returns {Promise} - Resolves with an array of File objects. */ requestFile: function(options = {}) { const { accept = '*/*', multiple = false } = options; return new Promise((resolve, reject) => { if (!isInsideGurasuraisu) { // Standalone Fallback: Create temporary input const input = document.createElement('input'); input.type = 'file'; input.accept = accept; input.multiple = multiple; input.style.display = 'none'; input.onchange = (e) => { if (e.target.files.length > 0) { resolve(Array.from(e.target.files)); } else { // User cancelled (hard to detect reliably, but we leave promise pending or handle via focus hack if needed) } input.remove(); }; document.body.appendChild(input); input.click(); } else { // Polygol Mode: Request from Parent const requestId = `file_req_${++_dialogRequestId}`; _dialogCallbacks[requestId] = (filesData) => { // Reconstruct File objects from data sent by parent // filesData = [{ name, type, data (base64/blob), size }] if (!filesData) return; const files = filesData.map(f => { // Convert Base64 to Blob if necessary, or use existing blob // Parent should send Blob/File if possible, but postMessage clones it. if (f instanceof File) return f; // If data is base64 string if (typeof f.data === 'string') { const arr = f.data.split(','); const bstr = atob(arr[1] || arr[0]); let n = bstr.length; const u8arr = new Uint8Array(n); while (n--) u8arr[n] = bstr.charCodeAt(n); return new File([u8arr], f.name, { type: f.type }); } return null; }).filter(f => f !== null); resolve(files); }; this._call('requestFileUpload', [{ accept, multiple, requestId }]); } }); }, /** * Namespace for Live Activity functions. */ liveActivity: { /** * Starts a Live Activity. * @param {object} options - Configuration object. * @param {string} options.activityId - A unique ID for this activity within your app. * @param {string} options.url - The URL of the HTML page for the activity's iframe. * @param {boolean} [options.homescreen=false] - Set to true if this activity should appear on the homescreen. * @param {string} [options.height='120px'] - The desired height of the activity in the notification shade. */ start: function(options) { if (options && options.activityId) { _myActiveActivities.add(options.activityId); // Add to local tracker } const appName = document.body.dataset.appName || 'UnknownApp'; Gurasuraisu._call('startLiveActivity', [appName, options]); }, /** * Checks if a live activity with the given ID is currently active for this app. * @param {string} activityId - The unique ID of the activity. * @returns {boolean} - True if the activity is active, false otherwise. */ isActive: function(activityId) { return _myActiveActivities.has(activityId); }, /** * Pushes updated data to a running Live Activity. * The parent OS will forward this data to the correct iframe. * @param {string} activityId - The unique ID of the activity. * @param {object} data - The data payload to send (e.g., { timeLeft: 120 }). */ update: function(activityId, data) { Gurasuraisu._call('updateLiveActivity', [activityId, data]); }, /** * Stops a running Live Activity. * @param {string} activityId - The unique ID of the activity you want to stop. */ stop: function(activityId) { if (activityId) { _myActiveActivities.delete(activityId); // Remove from local tracker } Gurasuraisu._call('stopLiveActivity', [activityId]); }, /** * (For use inside a Live Activity iframe) Pushes updated summary data to the parent homescreen. * @param {object} data - The data to display. * @param {string} data.icon - The Material Symbols icon name. * @param {string} data.text - The text to display. */ pushHomescreenUpdate: function(data) { if (isInsideGurasuraisu) { window.parent.postMessage({ type: 'live-activity-homescreen-update', icon: data.icon, text: data.text }, '*'); } } }, /** * Requests the parent window to minimize the current Gurapp. */ minimize: function() { this._call('minimizeFullscreenEmbed'); }, /** * Requests the parent to open another Gurapp. * @param {string} url - The URL of the Gurapp to open (e.g., "/chronos/index.html"). */ openApp: function(url) { this._call('createFullscreenEmbed', [url]); }, /** * Turns the screen black for power-saving or privacy. */ blackout: function() { this._call('blackoutScreen'); }, /** * Asks the parent Gurasuraisu to send back the list of currently installed apps. * The parent will respond with a 'installed-apps-list' message. */ requestInstalledApps: function() { this._call('requestInstalledApps', []); }, /** * Requests the parent Gurasuraisu to install a new Gurapp. * @param {object} appObject - The complete app object with id, url, iconUrl, etc. */ installApp: function(appObject) { this._call('installApp', [appObject]); }, deleteApp: function(appObject) { this._call('deleteApp', [appObject]); }, /** * Requests the parent Gurasuraisu to install a new App-Link. * @param {object} appLinkObject - The app-link object with name, url, iconUrl, etc. */ installAppLink: function(appLinkObject) { this._call('installAppLink', [appLinkObject]); }, /** * Registers a widget with the Polygol dashboard. * Apps should call this for each widget they provide. * @param {object} widgetData - An object describing the widget. * @param {string} widgetData.appName - The name of the app providing the widget. * @param {string} widgetData.widgetId - A unique ID for the widget (e.g., 'weather-current'). * @param {string} widgetData.title - A user-friendly title (e.g., 'Current Weather'). * @param {string} widgetData.url - The URL of the widget's content. * @param {Array} widgetData.defaultSize - The default [width, height] in grid units (e.g., [1, 1]). * @param {string} [widgetData.openUrl] - Optional. The URL to open when the widget is tapped. Defaults to the app's main URL. */ registerWidget: function(widgetData) { if (!widgetData || !widgetData.appName || !widgetData.widgetId || !widgetData.url || !widgetData.title) { console.error('[Gurasuraisu API] registerWidget requires appName, widgetId, url, and title.'); return; } this._call('registerWidget', [widgetData]); }, /** * Registers a new media session with the parent. * This will show the media widget in the Gurasu UI. * @param {object} metadata - An object with { title, artist, artwork: [{src}] }. * @param {string[]} [supportedActions] - An array of supported actions, e.g., ['playPause', 'next', 'prev']. */ registerMediaSession: function(metadata, supportedActions = ['playPause']) { const appName = document.body.dataset.appName || 'UnknownApp'; // Pass the new 'supportedActions' array to the parent this._call('registerMediaSession', [appName, metadata, supportedActions]); }, /** * Updates the parent Gurasu with the current playback state. * @param {object} state - An object, e.g., { playbackState: 'playing' | 'paused', metadata: (optional) }. */ updatePlaybackState: function(state) { const appName = document.body.dataset.appName || 'UnknownApp'; this._call('updateMediaPlaybackState', [appName, state]); }, /** * Tells the parent to clear/hide the media widget. */ clearMediaSession: function() { const appName = document.body.dataset.appName || 'UnknownApp'; this._call('clearMediaSession', [appName]); }, updateMediaProgress: function(progressState) { const appName = document.body.dataset.appName || 'UnknownApp'; this._call('updateMediaProgress', [appName, progressState]); }, /** * Sets up listeners for media control actions sent FROM the parent. * @param {object} actions - An object with functions, e.g., { playPause: () => {...}, next: () => {...} } */ onMediaControl: function(actions) { window.addEventListener('message', (event) => { if (event.source !== window.parent) return; if (event.data.type === 'media-control' && actions[event.data.action]) { actions[event.data.action](); } }); }, // --- NEW IndexedDB Functions --- listIDBDatabases: function() { this._call('listIDBDatabases'); }, listIDBStores: function(dbName) { this._call('listIDBStores', [dbName]); }, getIDBRecord: function(dbName, storeName, key) { this._call('getIDBRecord', [dbName, storeName, key]); }, setIDBRecord: function(dbName, storeName, jsonData) { this._call('setIDBRecord', [dbName, storeName, jsonData]); }, removeIDBRecord: function(dbName, storeName, key) { this._call('removeIDBRecord', [dbName, storeName, key]); }, clearIDBStore: function(dbName, storeName) { this._call('clearIDBStore', [dbName, storeName]); }, getLocalStorageItem: function(key) { this._call('getLocalStorageItem', [key]); }, setLocalStorageItem: function(key, value) { this._call('setLocalStorageItem', [key, value]); }, /** * Asks the parent Polygol to change a specific setting value. * This is the correct way for the settings app to apply changes. * @param {string} key - The localStorage key of the setting. * @param {string} value - The new value for the setting. */ setSettingValue: function(key, value) { this._call('setLocalStorageItem', [key, value]); }, /** * Asks the parent Polygol to check for a new service worker version * and trigger the update flow. */ forceUpdate: function() { this._call('forceUpdatePolygol', []); }, /** * Asks the parent to trigger a file download. * @param {string} filename - The desired name of the file. * @param {string} dataUrl - The content of the file as a data URL. */ downloadFile: function(filename, dataUrl) { this._call('downloadFile', [filename, dataUrl]); }, showAlert: function(message, title = 'Alert') { this._call('showDialog', [{ type: 'alert', message, title }]); }, showConfirm: function(message, title = 'Confirm') { return new Promise((resolve) => { if (!isInsideGurasuraisu) { return resolve(window.confirm(message)); } const requestId = `confirm_${++_dialogRequestId}`; _dialogCallbacks[requestId] = resolve; this._call('showDialog', [{ type: 'confirm', message, title, requestId }]); }); }, showPrompt: function(message, title = 'Prompt', defaultValue = '') { return new Promise((resolve) => { if (!isInsideGurasuraisu) { return resolve(window.prompt(message, defaultValue)); } const requestId = `prompt_${++_dialogRequestId}`; _dialogCallbacks[requestId] = resolve; this._call('showDialog', [{ type: 'prompt', message, title, defaultValue, requestId }]); }); } }; // --- Event Listener for Messages FROM Gurasuraisu --- /** * Listens for messages from the parent window, such as theme * or animation setting changes, and applies them to the Gurapp. */ window.addEventListener('message', async (event) => { if (event.source !== window.parent) { return; } const data = event.data; if (data && data.type) { switch (data.type) { case 'themeUpdate': document.body.classList.toggle('light-theme', data.theme === 'light'); // Update Filter const feBlend = document.querySelector('#edge-refraction-only feBlend'); if (feBlend) { feBlend.setAttribute('mode', data.theme === 'light' ? 'lighten' : 'darken'); } break; case 'animationsUpdate': document.body.classList.toggle('reduce-animations', !data.enabled); break; case 'contrastUpdate': document.documentElement.classList.toggle('gurasuraisu-high-contrast', data.enabled); break; case 'sunUpdate': document.documentElement.style.setProperty('--sun-shadow', data.shadow); document.documentElement.style.setProperty('--sun-shadow-strong', data.shadowStrong); break; case 'glassEffectsUpdate': document.documentElement.classList.toggle('gurasuraisu-glass-disabled', !data.enabled); break; case 'settingUpdate': if (data.key === 'gurappSoundsEnabled') { _systemSoundsAllowed = (data.value === 'true'); } break; case 'dialog-response': if (data.requestId && _dialogCallbacks[data.requestId]) { _dialogCallbacks[data.requestId](data.value); delete _dialogCallbacks[data.requestId]; } break; // --- Handles screenshot requests from the parent --- case 'request-screenshot': // Helper function to perform the capture const doCapture = async () => { try { // Determine theme-based background color // Gurapps sync the 'light-theme' class from the parent const isLight = document.body.classList.contains('light-theme'); const bgColor = isLight ? '#ffffff' : '#000000'; // Generate the screenshot of the app's content const canvas = await html2canvas(document.body, { useCORS: true, logging: false, backgroundColor: bgColor // Explicitly set background }); const screenshotDataUrl = canvas.toDataURL('image/jpeg', 0.5); // Send the generated screenshot data back to the parent window.parent.postMessage({ type: 'screenshot-response', screenshotDataUrl: screenshotDataUrl }, '*'); } catch (e) { console.error("Gurapp screenshot failed:", e); } }; // Check if html2canvas is loaded if (typeof html2canvas !== 'function') { // Inject it dynamically const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js'; script.onload = doCapture; script.onerror = () => console.error("Failed to load html2canvas for Gurapp"); document.head.appendChild(script); } else { doCapture(); } break; } } }); /** * On initial load, apply settings that might have been set by Gurasuraisu * in localStorage for a seamless appearance. */ document.addEventListener('DOMContentLoaded', () => { // NEW: Automatically request persistent storage for the Gurapp if (navigator.storage && navigator.storage.persist) { navigator.storage.persist().then(granted => { if (granted) { console.log("[Gurapp API] Persistent storage automatically granted."); } else { console.log("[Gurapp API] Persistent storage not granted (Browser may manage eviction)."); } }).catch(e => console.warn("[Gurapp API] Storage persistence request failed:", e)); } // Apply the 'standalone' class to the element if not in Gurasuraisu if (!isInsideGurasuraisu) { document.documentElement.classList.add('standalone'); // Standalone: Polygol Advertisement/Promotional Modal // Developers, please do not try to bypass this! try { const hidePromo = localStorage.getItem('gurappapi_polygol_ad_openenviroment_hide_user') === 'true'; if (!hidePromo) { const promoHtml = `

Open in Polygol

This app is part of the Polygol ecosystem. Experience the full environment, with smart AI across your apps, multitasking tools and extensive customization options.

Open Polygol
`; document.body.insertAdjacentHTML('beforeend', promoHtml); document.getElementById('polygol-promo-close').addEventListener('click', () => { if (document.getElementById('polygol-promo-dontshow').checked) { localStorage.setItem('gurappapi_polygol_ad_openenviroment_hide_user', 'true'); } document.getElementById('polygol-promo-overlay').remove(); }); } } catch (e) { console.error("Please do not block Gurasuraisu API promotions! Failed to show Polygol promo:", e); } } try { // --- Theme Logic --- if (!isInsideGurasuraisu) { // Standalone: Priority System Preference with Live Switching const mediaQuery = window.matchMedia('(prefers-color-scheme: light)'); const updateTheme = (e) => { const isLight = e.matches; document.body.classList.toggle('light-theme', isLight); }; // Apply immediately and listen for changes updateTheme(mediaQuery); mediaQuery.addEventListener('change', updateTheme); } else { // Polygol: Use LocalStorage or Default let theme = localStorage.getItem('theme'); if (theme === null) theme = 'dark'; document.body.classList.toggle('light-theme', theme === 'light'); } // --- Animation Logic --- let animValue = localStorage.getItem('animationsEnabled'); let animationsEnabled = true; if (!isInsideGurasuraisu && animValue === null) { // Standalone Default: System Preference (prefers-reduced-motion) animationsEnabled = !window.matchMedia('(prefers-reduced-motion: reduce)').matches; } else { animationsEnabled = animValue !== 'false'; // Default true if null in Polygol } document.body.classList.toggle('reduce-animations', !animationsEnabled); // --- High Contrast Logic --- let contrastValue = localStorage.getItem('highContrast'); let highContrastEnabled = false; if (!isInsideGurasuraisu && contrastValue === null) { // Standalone Default: System Preference highContrastEnabled = window.matchMedia('(prefers-contrast: more)').matches; } else { highContrastEnabled = contrastValue === 'true'; } // FIX: Target the element for the initial high contrast check document.documentElement.classList.toggle('gurasuraisu-high-contrast', highContrastEnabled); // We use setTimeout to ensure the SVG injection (which happens in another listener) has completed setTimeout(() => { const feBlend = document.querySelector('#edge-refraction-only feBlend'); if (feBlend) { const isLight = document.body.classList.contains('light-theme'); feBlend.setAttribute('mode', isLight ? 'lighten' : 'darken'); } }, 0); } catch (e) { console.error("Gurapp: Could not access localStorage. Settings may not apply.", e); } if (isInsideGurasuraisu) { let lastActivitySignal = 0; const throttleInterval = 500; // Throttle messages to the parent const handleLocalActivity = () => { // Step 2: Notify the parent to reset the global hide timer (throttled). const now = Date.now(); if (now - lastActivitySignal > throttleInterval) { window.parent.postMessage({ action: 'userActivity' }, '*'); lastActivitySignal = now; } }; // Listen for any user activity within this iframe. window.addEventListener('mousemove', handleLocalActivity); window.addEventListener('click', handleLocalActivity); window.addEventListener('keydown', handleLocalActivity); function getLocalSoundContext(element) { if (!element) return null; const tag = element.tagName; // Ignore Labels to prevent double sounds if (tag === 'LABEL') return null; const type = element.getAttribute('type'); const role = element.getAttribute('role'); if (tag === 'INPUT') { if (type === 'checkbox' || type === 'radio') return (role === 'switch') ? 'toggle' : 'check'; if (type === 'range') return null; if (['text', 'password', 'email', 'number', 'search'].includes(type)) return 'type'; return 'select'; } if (tag === 'TEXTAREA') return 'type'; if (tag === 'SELECT') return 'expand'; if (tag === 'BUTTON' || tag === 'A' || role === 'button') return 'select'; // Heuristic: Computed Pointer Cursor const style = window.getComputedStyle(element); if (style.cursor === 'pointer') return 'select'; return null; } document.addEventListener('click', (e) => { if (!_autoSoundEnabled || !_systemSoundsAllowed) return; let target = e.target; let soundType = null; // Traverse up 5 levels to find interactive parent (e.g. span inside button) for (let i = 0; i < 5; i++) { if (!target || target === document) break; soundType = getLocalSoundContext(target); if (soundType) break; target = target.parentElement; } if (soundType) { Gurasuraisu.playSound(soundType); } }, { capture: true }); // Input Focus Sounds document.addEventListener('focus', (e) => { if (!_autoSoundEnabled || !_systemSoundsAllowed) return; const type = getLocalSoundContext(e.target); if (type === 'type') { Gurasuraisu.playSound('type'); } }, { capture: true }); } // Announce API presence to enable full-screen mode and readiness for settings. if (isInsideGurasuraisu) { window.parent.postMessage({ type: 'gurasuraisu-api-present' }, '*'); window.parent.postMessage({ type: 'gurapp-ready' }, '*'); } }); // Announce that the API is ready window.GURASURAISU_API_READY = true; const readyEvent = new CustomEvent('GurasuraisuReady'); window.dispatchEvent(readyEvent);