@@ -1035,12 +1035,11 @@ const _myActiveActivities = new Set(); // Tracks this app's active activities
10351035// Native JS solutions for when the app is running outside of Polygol
10361036const _fallbacks = {
10371037 showPopup : function ( message ) {
1038- // A simple, non-blocking "toast" notification fallback
10391038 const toast = document . createElement ( 'div' ) ;
10401039 toast . textContent = message ;
10411040 toast . style . cssText = `
10421041 position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
1043- background-color: #333; color: white; padding: 10px 20px; border-radius: 20px;
1042+ border:1px solid var(--glass-border); color:var(--text-color);background:var(--search-background);backdrop-filter:var(--edge-refraction-filter) saturate(2) blur(2.5px); padding: 10px 20px; border-radius: 20px;
10441043 z-index: 9999; transition: opacity 0.5s; font-family: sans-serif;
10451044 ` ;
10461045 document . body . appendChild ( toast ) ;
@@ -1049,11 +1048,116 @@ const _fallbacks = {
10491048 setTimeout ( ( ) => toast . remove ( ) , 500 ) ;
10501049 } , 3000 ) ;
10511050 } ,
1052- showConfirm : function ( message ) {
1053- return window . confirm ( message ) ;
1051+ showNotification : function ( message , options = { } ) {
1052+ const title = options . heading || options . header || "Notification" ;
1053+ if ( "Notification" in window ) {
1054+ if ( Notification . permission === "granted" ) {
1055+ new Notification ( title , { body : message , icon : options . iconUrl || options . icon } ) ;
1056+ } else if ( Notification . permission !== "denied" ) {
1057+ Notification . requestPermission ( ) . then ( permission => {
1058+ if ( permission === "granted" ) {
1059+ new Notification ( title , { body : message , icon : options . iconUrl || options . icon } ) ;
1060+ }
1061+ } ) ;
1062+ }
1063+ } else {
1064+ alert ( title + "\n\n" + message ) ;
1065+ }
1066+ } ,
1067+ speakText : function ( text ) {
1068+ if ( 'speechSynthesis' in window ) {
1069+ window . speechSynthesis . speak ( new SpeechSynthesisUtterance ( text ) ) ;
1070+ }
1071+ } ,
1072+ playUiSound : function ( type ) {
1073+ try {
1074+ const ctx = new ( window . AudioContext || window . webkitAudioContext ) ( ) ;
1075+ const osc = ctx . createOscillator ( ) ;
1076+ osc . type = 'sine' ;
1077+ osc . frequency . setValueAtTime ( 100 , ctx . currentTime ) ;
1078+ const gain = ctx . createGain ( ) ;
1079+ gain . connect ( ctx . destination ) ;
1080+ gain . gain . setValueAtTime ( 0.5 , ctx . currentTime ) ;
1081+ gain . gain . linearRampToValueAtTime ( 0 , ctx . currentTime + 0.05 ) ;
1082+ osc . connect ( gain ) ;
1083+ osc . start ( ) ;
1084+ osc . stop ( ctx . currentTime + 0.05 ) ;
1085+ } catch ( e ) {
1086+ console . error ( 'Audio error:' , e ) ;
1087+ }
1088+ } ,
1089+ createFullscreenEmbed : function ( url ) {
1090+ window . location . href = url ;
1091+ } ,
1092+ closeFullscreenEmbed : function ( ) {
1093+ window . close ( ) ;
1094+ } ,
1095+ setImmersiveMode : function ( enabled ) {
1096+ if ( enabled && document . documentElement . requestFullscreen ) {
1097+ document . documentElement . requestFullscreen ( ) . catch ( ( ) => { } ) ;
1098+ } else if ( ! enabled && document . exitFullscreen ) {
1099+ document . exitFullscreen ( ) . catch ( ( ) => { } ) ;
1100+ }
1101+ } ,
1102+ downloadFile : function ( filename , dataUrl ) {
1103+ const a = document . createElement ( 'a' ) ;
1104+ a . href = dataUrl ;
1105+ a . download = filename ;
1106+ document . body . appendChild ( a ) ;
1107+ a . click ( ) ;
1108+ document . body . removeChild ( a ) ;
1109+ } ,
1110+ showDialog : function ( options ) {
1111+ let res ;
1112+ if ( options . type === 'confirm' ) res = window . confirm ( options . message ) ;
1113+ else if ( options . type === 'prompt' ) res = window . prompt ( options . message , options . defaultValue ) ;
1114+ else { window . alert ( options . message ) ; res = true ; }
1115+
1116+ if ( options . requestId && _dialogCallbacks [ options . requestId ] ) {
1117+ _dialogCallbacks [ options . requestId ] ( res ) ;
1118+ delete _dialogCallbacks [ options . requestId ] ;
1119+ }
1120+ } ,
1121+ setLocalStorageItem : function ( key , value ) {
1122+ localStorage . setItem ( key , value ) ;
1123+ } ,
1124+ getLocalStorageItem : function ( key ) {
1125+ const val = localStorage . getItem ( key ) ;
1126+ window . postMessage ( { type : 'localStorageItemValue' , key : key , value : val } , '*' ) ;
1127+ } ,
1128+ listLocalStorageKeys : function ( ) {
1129+ const keys = Object . keys ( localStorage ) ;
1130+ window . postMessage ( { type : 'localStorageKeysList' , keys : keys } , '*' ) ;
1131+ } ,
1132+ showSheet : function ( options = { } ) {
1133+ const overlay = document . createElement ( 'div' ) ;
1134+ overlay . id = 'gura-fallback-sheet' ;
1135+ overlay . style . cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:var(--overlay-color);backdrop-filter: blur(10px);z-index:99999;display:flex;justify-content:center;align-items:flex-end;' ;
1136+ const container = document . createElement ( 'div' ) ;
1137+ container . style . cssText = 'width:100%;max-width:800px;height:' + ( options . height || '60%' ) + ';max-height:calc(100% - 80px);background:var(--background-color);corner-shape: superellipse(1.5);border-radius:35px 35px 0 0;overflow:hidden;position:relative;border:1px solid var(--glass-border);box-shadow: var(--sun-shadow), 0 10px 30px rgba(0, 0, 0, 0.2);' ;
1138+
1139+ const closeBtn = document . createElement ( 'button' ) ;
1140+ closeBtn . textContent = 'Done' ;
1141+ closeBtn . style . cssText = 'position:absolute;top:10px;right:10px;z-index:10;padding:10px 15px;border-radius:50px;border:1px solid var(--glass-border);color:var(--text-color);background:var(--search-background);backdrop-filter:var(--edge-refraction-filter) saturate(2) blur(2.5px);cursor:pointer;' ;
1142+ closeBtn . onclick = ( ) => overlay . remove ( ) ;
1143+
1144+ const iframe = document . createElement ( 'iframe' ) ;
1145+ iframe . style . cssText = 'width:100%;height:100%;border:none;corner-shape:inherit;border-radius:inherit;' ;
1146+
1147+ if ( options . url ) {
1148+ iframe . src = options . url ;
1149+ } else if ( options . html ) {
1150+ iframe . srcdoc = options . html ;
1151+ }
1152+
1153+ container . appendChild ( closeBtn ) ;
1154+ container . appendChild ( iframe ) ;
1155+ overlay . appendChild ( container ) ;
1156+ document . body . appendChild ( overlay ) ;
10541157 } ,
1055- showPrompt : function ( message , defaultValue ) {
1056- return window . prompt ( message , defaultValue ) ;
1158+ closeSheet : function ( ) {
1159+ const sheet = document . getElementById ( 'gura-fallback-sheet' ) ;
1160+ if ( sheet ) sheet . remove ( ) ;
10571161 } ,
10581162 // For functions that have no standalone equivalent, we can just log a warning.
10591163 default : function ( functionName ) {
@@ -1527,6 +1631,14 @@ const Gurasuraisu = {
15271631 } ) ;
15281632 } ,
15291633
1634+ /**
1635+ * Listen for changes to the app's visibility (minimized vs restored).
1636+ * @param {function } callback - Function that receives (isVisible: boolean)
1637+ */
1638+ onVisibilityChange : function ( callback ) {
1639+ _onVisibilityChangeHandler = callback ;
1640+ } ,
1641+
15301642 /**
15311643 * Opens a bottom sheet with custom content.
15321644 * @param {object } options
@@ -1665,6 +1777,19 @@ window.addEventListener('message', async (event) => {
16651777 KeyboardNavigationManager . setEnabled ( data . value ) ;
16661778 }
16671779 break ;
1780+ case 'visibilityUpdate' :
1781+ _isSuspended = ! data . visible ;
1782+ if ( ! _isSuspended && ! _perfInterval ) {
1783+ _perfInterval = setInterval ( reportPerformance , 30000 ) ;
1784+ } else if ( _isSuspended && _perfInterval ) {
1785+ clearInterval ( _perfInterval ) ;
1786+ _perfInterval = null ;
1787+ }
1788+ // Trigger user-defined handler
1789+ if ( typeof _onVisibilityChangeHandler === 'function' ) {
1790+ _onVisibilityChangeHandler ( data . visible ) ;
1791+ }
1792+ break ;
16681793 case 'switch-control-enter' :
16691794 KeyboardNavigationManager . startNavigation ( data . direction ) ;
16701795 break ;
0 commit comments