Przeglądaj źródła

Fixed typo on info page

Toby Chui 2 lat temu
rodzic
commit
39ca92e91c
1 zmienionych plików z 1148 dodań i 1148 usunięć
  1. 1148 1148
      web/script/ao_module.js

+ 1148 - 1148
web/script/ao_module.js

@@ -1,1148 +1,1148 @@
-/*
-    ArOZ Online Module Javascript Wrapper
-
-    This is a wrapper for module developers to access system API easier and need not to dig through the source code.
-    Basically: Write less do more (?)
-    WARNING! SOME FUNCTION ARE NOT COMPATIBILE WITH PREVIOUS VERSION OF AO_MODULE.JS.
-    PLEASE REFER TO THE SYSTEM DOCUMENTATION FOR MORE INFORMATION.
-
-    *** Please include this javascript file with relative path instead of absolute path.
-    E.g. ../script/ao_module.js (OK)
-           /script/ao_module.js (NOT OK)
-*/
-var ao_module_virtualDesktop = false;
-try{
-    ao_module_virtualDesktop = !(!parent.isDesktopMode);
-}catch(ex){
-    //Running ArozOS inside iframe for some reason
-    console.log("CORS Access Error. Entering compatibility mode with virtual desktop mode disabled.");
-}
-
-var ao_root = null;
-//Get the current windowID if in Virtual Desktop Mode, return false if VDI is not detected
-var ao_module_windowID = false;
-var ao_module_parentID = false;
-var ao_module_callback = false;
-var ao_module_ime = false;
-if (ao_module_virtualDesktop)ao_module_windowID = $(window.frameElement).parent().parent().attr("windowId");
-if (ao_module_virtualDesktop)ao_module_parentID = $(window.frameElement).parent().parent().attr("parent");
-if (ao_module_virtualDesktop)ao_module_callback = $(window.frameElement).parent().parent().attr("callback");
-if (ao_module_virtualDesktop)ao_module_parentURL = $(window.frameElement).parent().find("iframe").attr("src");
-ao_root = ao_module_getAORootFromScriptPath();
-
-/*
-    Event bindings
-    The following events are required for ao_module to operate normally 
-    under Web Desktop Mode. 
-*/
-
-document.addEventListener("DOMContentLoaded", function() {
-    if (ao_module_virtualDesktop){
-        if (parent.window.ime == null){
-            return;
-        }
-        //Add window focus handler
-        document.addEventListener("mousedown", function(event) {
-            //When click on this document, focus this
-            ao_module_focus();
-
-            if (event.target.tagName == "INPUT" || event.target.tagName == "TEXTAREA"){
-
-            }else{
-                if (parent.window.ime.focus != null){
-                    if (ao_module_ime){
-                        //This is clicking on ime windows. Do not change focus
-                    }else{
-                        parent.window.ime.focus = null;
-                    }
-                }
-            }
-        }, true);
-    
-        //Add IME registration handler
-        var inputFields = document.querySelectorAll("input,textarea");
-        for (var i = 0; i < inputFields.length; i++){
-            if ($(inputFields[i]).attr("type") != undefined){
-                var thisType = $(inputFields[i]).attr("type");
-                if ((thisType == "text" || thisType =="search" || thisType =="url")){
-                    //Supported types of input
-                    ao_module_bindCustomIMEEvents(inputFields[i]);
-                }else{
-                    //Not supported type of inputs
-
-                }
-            }else{
-                //text area
-                ao_module_bindCustomIMEEvents(inputFields[i]);
-            }
-        }
-    }
-    /*
-     //Load html2canvas
-     if ($){
-        $.getScript(ao_root + "script/html2canvas.min.js", function() {
-            console.log("Html2canvas loaded")
-        });
-    }
-    */
-});
-
-/*
-    Startup Section Script
-    
-    These functions handle the startup of an ao_module and adapt them into the
-    standard arozos desktop eco-system api
-*/
-
-//Function handle to bind custom IME events
-function ao_module_bindCustomIMEEvents(object){
-    parent.bindObjectToIMEEvents(object);
-}
-
-function ao_module_screenshot(callback){
-    html2canvas(document.querySelector("body")).then(screenshot => {
-        return callback(screenshot);
-    });
-}
-
-//Get the ao_root from script includsion path
-function ao_module_getAORootFromScriptPath(){
-    var possibleRoot = "";
-    $("script").each(function(){
-        if (this.hasAttribute("src") && $(this).attr("src").includes("ao_module.js")){
-            var tmp = $(this).attr("src");
-            tmp = tmp.split("script/ao_module.js");
-            possibleRoot = tmp[0];
-        }
-    });
-    return possibleRoot;
-}
-
-//Get the input filename and filepath from the window hash paramter
-function ao_module_loadInputFiles(){
-    try{
-        if (window.location.hash.length == 0){
-            return null;
-        }
-        var inputFileInfo = window.location.hash.substring(1,window.location.hash.length);
-        inputFileInfo = JSON.parse(decodeURIComponent(inputFileInfo));
-        return inputFileInfo
-    }catch{
-        return null;
-    }
-}
-
-//Set the ao_module window to fixed size (not allowing resize)
-function ao_module_setFixedWindowSize(){
-    if (!ao_module_virtualDesktop){
-        return;
-    }
-    parent.setFloatWindowResizePolicy(ao_module_windowID, false);
-}
-
-//Restore a float window to be resizble
-function ao_module_setResizableWindowSize(){
-    if (!ao_module_virtualDesktop){
-        return;
-    }
-    parent.setFloatWindowResizePolicy(ao_module_windowID, true);
-}
-
-//Update the window size of the given float window object
-function ao_module_setWindowSize(width, height){
-    if (!ao_module_virtualDesktop){
-        return;
-    }
-    parent.setFloatWindowSize(ao_module_windowID, width, height)
-}
-
-//Update the floatWindow title
-function ao_module_setWindowTitle(newTitle){
-    if (!ao_module_virtualDesktop){
-        document.title = newTitle;
-        return;
-    }
-    parent.setFloatWindowTitle(ao_module_windowID, newTitle);
-}   
-
-//Set new window theme, default dark, support {dark/white}
-function ao_module_setWindowTheme(newtheme="dark"){
-    if (!ao_module_virtualDesktop){
-        return;
-    }
-    parent.setFloatWindowTheme(ao_module_windowID, newtheme);
-}   
-
-//Check if there are any windows with the same path. 
-//If yes, replace its hash content and reload to the new one and close the current floatWindow
-function ao_module_makeSingleInstance(){
-    $(window.parent.document).find(".floatWindow").each(function(){
-        if ($(this).attr("windowid") == ao_module_windowID){
-            return
-        }
-        var currentPath = window.location.pathname;
-        if ("/" + $(this).find("iframe").attr('src').split("#").shift() == currentPath){
-            //Another instance already running. Replace it with the current path
-            $(this).find("iframe").attr('src', window.location.pathname.substring(1) + window.location.hash);
-            $(this).find("iframe")[0].contentWindow.location.reload();
-            //Move the other instant to top
-            var targetfw = parent.getFloatWindowByID($(this).attr("windowid"))
-            parent.MoveFloatWindowToTop(targetfw);
-            //Close the instance
-            ao_module_close();
-            return true
-        }
-    });
-    return false
-}
-
-//Use for cross frame communication, example: 
-//let targetOpeningInstances = ao_module_getInstanceByPath("NotepadA/index.html")
-function ao_module_getInstanceByPath(matchingPath){
-    let targetInstance = "";
-    $(window.parent.document).find(".floatWindow").each(function(){
-        if ($(this).attr("windowid") == ao_module_windowID){
-            return
-        }
-        let thisfwPath = $(this).find("iframe").attr('src').split("#").shift();
-        if (thisfwPath == matchingPath){
-            targetInstance = $(this).attr("windowid");
-        }
-    });
-
-    if (targetInstance == ""){
-        return null;
-    }
-        
-    return parent.getFloatWindowByID(targetInstance);
-}
-
-
-//Close the current window
-function ao_module_close(){
-    if (!ao_module_virtualDesktop){
-        window.close('','_parent','');
-        window.location.href = ao_root + "SystemAO/closeTabInsturction.html";
-        return;
-    }
-    parent.closeFwProcess(ao_module_windowID);
-}
-
-//Focus this floatWindow
-function ao_module_focus(){
-    parent.MoveFloatWindowToTop(parent.getFloatWindowByID(ao_module_windowID));
-}
-
-//Set the floatWindow to top most mode
-function ao_module_setTopMost(){
-    parent.PinFloatWindowToTopMostMode(parent.getFloatWindowByID(ao_module_windowID));
-}
-
-//Unset the floatWindow top most mode
-function ao_module_unsetTopMost(){
-    parent.UnpinFloatWindowFromTopMostMode(parent.getFloatWindowByID(ao_module_windowID));
-}
-
-//Popup a file selection window for uplaod
-function ao_module_selectFiles(callback, fileType="file", accept="*", allowMultiple=false){
-    var input = document.createElement('input');
-    input.type = fileType;
-    input.multiple = allowMultiple;
-    input.accept = accept;
-    input.onchange = e => { 
-        var files = e.target.files; 
-        callback(files);
-    }
-    input.click();
-}
-
-//Open a path with File Manager, optional highligh filename
-function ao_module_openPath(path, filename=undefined){
-    //Trim away the last / if exists
-    if (path.substr(path.length - 1, 1) == "/"){
-        path = path.substr(0, path.length - 1);
-    }
-
-    if (filename == undefined){
-        if (ao_module_virtualDesktop){
-            parent.newFloatWindow({
-                url: "SystemAO/file_system/file_explorer.html#" + encodeURIComponent(path),
-                appicon: "SystemAO/file_system/img/small_icon.png",
-                width:1080,
-                height:580,
-                title: "File Manager"
-            });
-        }else{
-            window.open(ao_root + "SystemAO/file_system/file_explorer.html#" + encodeURIComponent(path))
-        }
-    }else{
-        var fileObject = [{
-            filepath: path + "/" + filename,
-            filename: filename,
-        }];
-        if (ao_module_virtualDesktop){
-            parent.newFloatWindow({
-                url: "SystemAO/file_system/file_explorer.html#" + encodeURIComponent(JSON.stringify(fileObject)),
-                appicon: "SystemAO/file_system/img/small_icon.png",
-                width:1080,
-                height:580,
-                title: "File Manager"
-            });
-        }else{
-            window.open(ao_root + "SystemAO/file_system/file_explorer.html#" + encodeURIComponent(JSON.stringify(fileObject)))
-        }
-    }
-
-   
-}
-
-//Open a particular tab using System Setting module. Require 
-//1) Setting Group
-//2) Setting Name
-function ao_module_openSetting(group, name){
-    var requestObject = {
-        group: group,
-        name: name
-    }
-
-    requestObject = encodeURIComponent(JSON.stringify(requestObject));
-    var openURL = "SystemAO/system_setting/index.html#" + requestObject;
-    if (ao_module_virtualDesktop){
-        ao_module_newfw({
-            url: openURL,
-            width: 1080,
-            height: 580,
-            appicon: "SystemAO/system_setting/img/small_icon.png",
-            title: "System Setting"
-        });
-    }else{
-        window.open(ao_root + openURL)
-    }
-}
-
-
-/*
-    ao_module_newfw(launchConfig) => Create a new floatWindow object from the given paramters
-
-    Most basic usage: (With auto assign UID, size and location)
-    ao_module_newfw({
-        url: "Dummy/index.html",
-        title: "Dummy Module",
-        appicon: "Dummy/img/icon.png"
-    });
-
-    Example usage that involve all configs:
-    ao_module_newfw({
-        url: "Dummy/index.html",
-        uid: "CustomUUID",
-        width: 1024,
-        height: 768,
-        appicon: "Dummy/img/icon.png",
-        title: "Dummy Module",
-        left: 100,
-        top: 100,
-        parent: ao_module_windowID,
-        callback: "childCallbackHandler"
-    });
-*/
-function ao_module_newfw(launchConfig){
-    if (launchConfig["parent"] == undefined){
-        launchConfig["parent"] = ao_module_windowID;
-    }
-    if (ao_module_virtualDesktop){
-        parent.newFloatWindow(launchConfig);
-    }else{
-        window.open(ao_root + launchConfig.url);
-    }
-}
-
-/*
-    File Selector
-
-    Open a file selector and return selected item back to the current window
-    Tips: Unlike the beta version, you can use this function in both Virtual Desktop Mode and normal mode.
-
-    Possible selection type:
-    type => {file / folder / all / new}
-
-    Example usage: 
-    ao_module_openFileSelector(fileSelected, "user:/Desktop", "file",true);
-
-    function fileSelected(filedata){
-        for (var i=0; i < filedata.length; i++){
-            var filename = filedata[i].filename;
-            var filepath = filedata[i].filepath;
-            //Do something here
-        }
-    }
-
-    If you want to create a new file or folder object, you can use the following options paramters
-    option = {
-        defaultName: "newfile.txt",            //Default filename used in new operation
-        fnameOverride: "myfunction",           //For those defined with window.myfunction
-        filter: ["mp3","aac","ogg","flac","wav"] //File extension filter
-    }
-*/
-let ao_module_fileSelectionListener;
-let ao_module_fileSelectorWindow;
-function ao_module_openFileSelector(callback,root="user:/", type="file",allowMultiple=false, options=undefined){
-    var initInfo = {
-        root: root,
-        type: type,
-        allowMultiple: allowMultiple,
-        listenerUUID: "",
-        options: options
-    }
-    var initInfoEncoded = encodeURIComponent(JSON.stringify(initInfo))
-    if (ao_module_virtualDesktop){
-        var callbackname = callback.name;
-        if (typeof(options) != "undefined" && typeof(options.fnameOverride) != "undefined"){
-            callbackname = options.fnameOverride;
-        }
-        console.log(callbackname);
-        parent.newFloatWindow({
-            url: "SystemAO/file_system/file_selector.html#" + initInfoEncoded,
-            width: 700,
-            height: 440,
-            appicon: "SystemAO/file_system/img/selector.png",
-            title: "Open",
-            parent: ao_module_windowID,
-            callback: callbackname
-        });
-    }else{
-        //Create a return listener base on localStorage
-        let listenerUUID = "fileSelector_" + new Date().getTime();
-        ao_module_fileSelectionListener = setInterval(function(){
-            if (localStorage.getItem(listenerUUID) === undefined || localStorage.getItem(listenerUUID)=== null){
-                //Not ready
-            }else{
-                //File ready!
-                var selectedFiles = JSON.parse(localStorage.getItem(listenerUUID));
-                console.log("Removing Localstorage Item " + listenerUUID);
-                
-                localStorage.removeItem(listenerUUID); 
-                setTimeout(function(){
-                    localStorage.removeItem(listenerUUID); 
-                },500);
-                if(selectedFiles == "&&selection_canceled&&"){
-                    //Selection canceled. Returm empty array
-                    callback([]);
-                }else{
-                    //Files Selected
-                    callback(selectedFiles);
-                }
-                
-                clearInterval(ao_module_fileSelectionListener);
-                ao_module_fileSelectorWindow.close();
-            }
-        },1000);
-
-        //Open the file selector in a new tab
-        initInfo.listenerUUID = listenerUUID;
-        initInfoEncoded = encodeURIComponent(JSON.stringify(initInfo))
-        ao_module_fileSelectorWindow = window.open(ao_root + "SystemAO/file_system/file_selector.html#" + initInfoEncoded,);
-    }
-}
-
-//Check if there is parent to callback
-function ao_module_hasParentCallback(){
-    if (ao_module_virtualDesktop){
-        //Check if parent callback exists
-        var thisFw;
-        $(parent.window.document.body).find(".floatWindow").each(function(){
-            if ($(this).attr('windowid') == ao_module_windowID){
-                thisFw = $(this);
-            }
-        });
-        var parentWindowID = thisFw.attr("parent");
-        var parentCallback = thisFw.attr("callback");
-        if (parentWindowID == "" || parentCallback == ""){
-            //No parent window defined
-            return false;
-        }
-
-        //Check if parent windows is alive
-        var parentWindow = undefined;
-        $(parent.window.document.body).find(".floatWindow").each(function(){
-            if ($(this).attr('windowid') == parentWindowID){
-                parentWindow = $(this);
-            }
-        });
-        if (parentWindow == undefined){
-            //parent window not exists
-            return false;
-        }
-
-        //Parent callback is set and ready to callback
-        return true;
-    }else{
-        return false
-    }
-}
-
-//Callback to parent with results
-function ao_module_parentCallback(data=""){
-    if (ao_module_virtualDesktop){
-        var thisFw;
-        $(parent.window.document.body).find(".floatWindow").each(function(){
-            if ($(this).attr('windowid') == ao_module_windowID){
-                thisFw = $(this);
-            }
-        });
-        var parentWindowID = thisFw.attr("parent");
-        var parentCallback = thisFw.attr("callback");
-        if (parentWindowID == "" || parentCallback == ""){
-            //No parent window defined
-            console.log("Undefined parent window ID or callback name");
-            return false;
-        }
-        var parentWindow = undefined;
-        $(parent.window.document.body).find(".floatWindow").each(function(){
-            if ($(this).attr('windowid') == parentWindowID){
-                parentWindow = $(this);
-            }
-        });
-        if (parentWindow == undefined){
-            //parent window not exists
-            console.log("Parent Window not exists!")
-            return false;
-        }
-        $(parentWindow).find('iframe')[0].contentWindow.eval(parentCallback + "(" + JSON.stringify(data) + ");")
-
-        //Focus the parent windows
-        parent.MoveFloatWindowToTop(parentWindow);
-        return true;
-    }else{
-        console.log("[ao_module] WARNING! Invalid call to parentCallback under non-virtualDesktop mode");
-        return false;
-    }
-}
-
-
-function ao_module_agirun(scriptpath, data, callback, failedcallback = undefined, timeout=0){
-    $.ajax({
-        url: ao_root + "system/ajgi/interface?script=" + scriptpath,
-        method: "POST",
-        data: data,
-        success: function(data){
-            if (typeof(callback) != "undefined"){
-                callback(data);
-            }
-        },
-        error: function(){
-            if (typeof(failedcallback) != "undefined"){
-                failedcallback();
-            }
-        },
-        timeout: timeout
-    });
-}
-
-function ao_module_uploadFile(file, targetPath, callback=undefined, progressCallback=undefined, failedcallback=undefined) {
-    let url = ao_root + 'system/file_system/upload'
-    let formData = new FormData()
-    let xhr = new XMLHttpRequest()
-    formData.append('file', file);
-    formData.append('path', targetPath);
-
-    xhr.open('POST', url, true);
-
-    xhr.upload.addEventListener("progress", function(e) {
-        if (progressCallback !== undefined){
-            progressCallback((e.loaded * 100.0 / e.total) || 100);
-        }
-    });
-
-    xhr.addEventListener('readystatechange', function(e) {
-        if (xhr.readyState == 4 && xhr.status == 200) {
-            if (callback !== undefined){
-                callback(e.target.response);
-            }
-        }
-        else if (xhr.readyState == 4 && xhr.status != 200) {
-            if (failedcallback !== undefined){
-                failedcallback(xhr.status);
-            }
-        }
-    })
-
-    xhr.send(formData);
-}
-
-
-/*
-    ao_module_storage, allow key-value storage per module settings. 
-    WARNING: NOT CROSS USER READ-WRITABLE
-    
-    ao_module_storage.setStorage(moduleName, configName,configValue);
-    ao_module_storage.loadStorage(moduleName, configName);
-*/
-class ao_module_storage {
-    static setStorage(moduleName, configName,configValue){
-    	$.ajax({
-    	  type: 'GET',
-    	  url: ao_root + "system/file_system/preference",
-    	  data: {key: moduleName + "/" + configName,value:configValue},
-    	  success: function(data){},
-    	  async:true
-    	});
-    	return true;
-    }
-    
-    static loadStorage(moduleName, configName, callback=undefined){
-        var result = "";
-        if (callback == undefined){
-            //Do not use async
-            $.ajax({
-                type: 'GET',
-                url: ao_root + "system/file_system/preference",
-                data: {key: moduleName + "/" + configName},
-                success: function(data){
-                        if (data.error !== undefined){
-                            result = "";
-                        }else{
-                            result = data;
-                        }
-                    },
-                error: function(data){result = "";},
-                async:false,
-                timeout: 3000
-            });
-              return result;
-        }else{
-            //Use sync method
-            $.ajax({
-                type: 'GET',
-                url: ao_root + "system/file_system/preference",
-                data: {key: moduleName + "/" + configName},
-                success: function(data){
-                        if (data.error !== undefined){
-                            callback("");
-                        }else{
-                            callback(data);
-                        }
-                    },
-                error: function(evt){
-                    callback("");
-                },
-                timeout: 30000
-            });
-        }
-    	
-    	
-    }
-}
-
-class ao_module_codec{
-	//Decode umfilename into standard filename in utf-8, which umfilename usually start with "inith"
-	//Example: ao_module_codec.decodeUmFilename(umfilename_here);
-    static decodeUmFilename(umfilename){
-		if (umfilename.includes("inith")){
-			var data = umfilename.split(".");
-			if (data.length == 1){
-				//This is a filename without extension
-				data = data[0].replace("inith","");
-				var decodedname = ao_module_codec.decode_utf8(ao_module_codec.hex2bin(data));
-				if (decodedname != "false"){
-					//This is a umfilename
-					return decodedname;
-				}else{
-					//This is not a umfilename
-					return umfilename;
-				}
-			}else{
-				//This is a filename with extension
-				var extension = data.pop();
-				var filename = data[0];
-				filename = filename.replace("inith",""); //Javascript replace only remove the first instances (i.e. the first inith in filename)
-				var decodedname = ao_module_codec.decode_utf8(ao_module_codec.hex2bin(filename));
-				if (decodedname != "false"){
-					//This is a umfilename
-					return decodedname + "." + extension;
-				}else{
-					//This is not a umfilename
-					return umfilename;
-				}
-			}
-			
-		}else{
-			//This is not umfilename as it doesn't have the inith prefix
-			return umfilename;
-		}
-	}
-	
-	//Encode filename to UMfilename
-	//Example: ao_module_codec.encodeUMFilename("test.stl");
-	static encodeUMFilename(filename){
-	    if (filename.substring(0,5) != "inith"){
-	        //Check if the filename include extension. 
-	        if (filename.includes(".")){
-	            //Filename with extension. pop it out first.
-	            var info = filename.split(".");
-	            var ext = info.pop();
-	            var filenameOnly = info.join(".");
-	            var encodedFilename = "inith" + ao_module_codec.decode_utf8(ao_module_codec.bin2hex(filenameOnly)) + "." + ext;
-	            return encodedFilename;
-	        }else{
-	            //Filename with no extension. Convert the whole name into UMfilename
-	            var encodedFilename = "inith" + ao_module_codec.decode_utf8(ao_module_codec.bin2hex(filename));
-	            return encodedFilename;
-	        }
-	    }else{
-	        //This is already a UMfilename. return the raw filename.
-	        return filename;
-	    }
-	}
-	
-	//Decode hexFoldername into standard foldername in utf-8, return the original name if it is not a hex foldername
-	//Example: ao_module_codec.decodeHexFoldername(hexFolderName_here);
-	static decodeHexFoldername(folderName, prefix=true){
-	    var decodedFoldername = ao_module_codec.decode_utf8(ao_module_codec.hex2bin(folderName));
-		if (decodedFoldername == "false"){
-			//This is not a hex encoded foldername
-			decodedFoldername = folderName;
-		}else{
-			//This is a hex encoded foldername
-			if (prefix){
-			    	decodedFoldername = "*" + decodedFoldername;
-			}else{
-			    	decodedFoldername =decodedFoldername;
-			}
-		}
-		return decodedFoldername;
-	}
-    
-    //Encode foldername into hexfoldername
-    //Example: ao_module_codec.encodeHexFoldername("test");
-    static encodeHexFoldername(folderName){
-        var encodedFilename = "";
-        if (ao_module_codec.decodeHexFoldername(folderName) == folderName){
-            //This is not hex foldername. Encode it
-            encodedFilename = ao_module_codec.decode_utf8(ao_module_codec.bin2hex(folderName));
-        }else{
-            //This folder name already encoded. Return the original value
-            encodedFilename = folderName;
-        }
-        
-        return encodedFilename;
-    }
-    static hex2bin(s){
-      var ret = []
-      var i = 0
-      var l
-      s += ''
-      for (l = s.length; i < l; i += 2) {
-        var c = parseInt(s.substr(i, 1), 16)
-        var k = parseInt(s.substr(i + 1, 1), 16)
-        if (isNaN(c) || isNaN(k)) return false
-        ret.push((c << 4) | k)
-      }
-    
-      return String.fromCharCode.apply(String, ret)
-    }
-    
-    static bin2hex(s){
-         var i
-          var l
-          var o = ''
-          var n
-          s += ''
-          for (i = 0, l = s.length; i < l; i++) {
-            n = s.charCodeAt(i)
-              .toString(16)
-            o += n.length < 2 ? '0' + n : n
-          }
-          return o
-    }
-    
-    static decode_utf8(s) {
-      return decodeURIComponent(escape(s));
-    }
-}
-
-
-/**
-ArOZ Online Module Utils for quick deploy of ArOZ Online WebApps
-
-ao_module_utils.objectToAttr(object); //object to DOM attr
-ao_module_utils.attrToObject(attr); //DOM attr to Object
-ao_module_utils.getRandomUID(); //Get random UUID from timestamp
-ao_module_utils.getIconFromExt(ext); //Get icon tag from file extension
-
-ao_module_utils.getDropFileInfo(dropEvent); //Get the filepath and filename list from file explorer drag drop
-ao_module_utils.formatBytes(byte, decimals); //Format file byte size to human readable size
-ao_module_utils.timeConverter(unix_timestamp); //Get human readable timestamp 
-**/
-class ao_module_utils{
-    
-    //Two simple functions for converting any Javascript object into string that can be put into the attr value of an DOM object
-    static objectToAttr(object){
-       return encodeURIComponent(JSON.stringify(object));
-    }
-    
-    static attrToObject(attr){
-        return JSON.parse(decodeURIComponent(attr));
-    }
-    
-    //Get a random id for a new floatWindow, use with var uid = ao_module_utils.getRandomUID();
-    static getRandomUID(){
-        return new Date().getTime();
-    }
-
-    static stringToBlob(text, mimetype="text/plain"){
-        var blob = new Blob([text], { type: mimetype });
-        return blob
-    }
-
-    static blobToFile(blob, filename, mimetype="text/plain"){
-        var file = new File([blob], filename, {type: mimetype});
-        return file
-    }
-    
-    //Get the icon of a file with given extension (ext), use with ao_module_utils.getIconFromExt("ext");
-    static getIconFromExt(ext){
-        var ext = ext.toLowerCase().trim();
-        var iconList={
-            md:"file text outline",
-            txt:"file text outline",
-            pdf:"file pdf outline",
-            doc:"file word outline",
-            docx:"file word outline",
-            odt:"file word outline",
-            xlsx:"file excel outline",
-            ods:"file excel outline",
-            ppt:"file powerpoint outline",
-            pptx:"file powerpoint outline",
-            odp:"file powerpoint outline",
-            jpg:"file image outline",
-            png:"file image outline",
-            jpeg:"file image outline",
-            gif:"file image outline",
-            odg:"file image outline",
-            psd:"file image outline",
-            zip:"file archive outline",
-            '7z':"file archive outline",
-            rar:"file archive outline",
-            tar:"file archive outline",
-            mp3:"file audio outline",
-            m4a:"file audio outline",
-            flac:"file audio outline",
-            wav:"file audio outline",
-            aac:"file audio outline",
-            mp4:"file video outline",
-            webm:"file video outline",
-            php:"file code outline",
-            html:"file code outline",
-            htm:"file code outline",
-            js:"file code outline",
-            css:"file code outline",
-            xml:"file code outline",
-            json:"file code outline",
-            csv:"file code outline",
-            odf:"file code outline",
-            bmp:"file image outline",
-            rtf:"file text outline",
-            wmv:"file video outline",
-            mkv:"file video outline",
-            ogg:"file audio outline",
-            stl:"cube",
-            obj:"cube",
-            "3ds":"cube",
-            fbx:"cube",
-            collada:"cube",
-            step:"cube",
-            iges:"cube",
-            gcode:"cube",
-            shortcut:"external square",
-            opus:"file audio outline",
-            apscene:"cubes"
-        };
-        var icon = "";
-        if (ext == ""){
-            icon = "folder outline";
-        }else{
-            icon = iconList[ext];
-            if (icon == undefined){
-                icon = "file outline"
-            }
-        }
-        return icon;
-	}
-	
-	//Get the drop file properties {filepath: xxx, filename: xxx} from file drop events from file exploere
-	static getDropFileInfo(dropEvent){
-		if (dropEvent.dataTransfer.getData("filedata") !== ""){
-			var filelist = dropEvent.dataTransfer.getData("filedata");
-			filelist = JSON.parse(filelist);
-			return filelist;
-		}
-    }
-
-    static readFileFromFileObject(fileObject, successCallback, failedCallback=undefined){
-        let reader = new FileReader();
-        reader.readAsText(fileObject);
-        reader.onload = function() {
-            successCallback(reader.result);
-        };
-        reader.onerror = function() {
-            if (failedCallback != undefined){
-                failedCallback(reader.error);
-            }else{
-                console.log(reader.error);
-            }
-        };
-
-    }
-
-    static durationConverter(seconds){
-        var days = Math.floor(seconds / 86400);
-        seconds -= days * 86400;
-        var hours = Math.floor(seconds / 3600) % 24;
-        seconds -= hours * 3600;
-        var minutes = Math.floor(seconds / 60) % 60;
-        seconds -= minutes * 60;
-        var seconds = seconds % 60;
-
-        var resultDuration = "";
-        if (days > 0){
-            resultDuration += days + " Day";
-            if (days > 1){
-                resultDuration+= "s"
-            }
-            resultDuration += " "
-        }
-
-        if (hours > 0){
-            resultDuration += hours + " Hour"
-            if (hours > 1){
-                resultDuration += "s"
-            }
-            resultDuration += " "
-        }
-
-        if (minutes > 0){
-            resultDuration += minutes + " Minute"
-            if (minutes > 1){
-                resultDuration += "s"
-            }
-            resultDuration += " "
-        }
-
-        if (seconds > 0){
-            resultDuration += seconds + " Secound"
-            if (seconds > 1){
-                resultDuration += "s"
-            }
-            resultDuration += " "
-        }
-        
-        return resultDuration;
-    }
-
-    static timeConverter(UNIX_timestamp){
-        var a = new Date(UNIX_timestamp * 1000);
-        var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
-        var year = a.getFullYear();
-        var month = months[a.getMonth()];
-        var date = a.getDate();
-        var hour = a.getHours().toString().padStart(2, "0");
-        var min = a.getMinutes().toString().padStart(2, "0");
-        var sec = a.getSeconds().toString().padStart(2, "0");
-        var time = date + ' ' + month + ' ' + year + ' ' + hour + ':' + min + ':' + sec ;
-        return time;
-    }
-
-    static getWebSocketEndpoint(){
-        let protocol = "wss://";
-        if (location.protocol !== 'https:') {
-            protocol = "ws://";
-        }
-        let port = window.location.port;
-        if (window.location.port == ""){
-            if (location.protocol !== 'https:') {
-                port = "80";
-            }else{
-                port = "443";
-            }
-            
-        }
-        let wsept = (protocol + window.location.hostname + ":" + port);
-        return wsept;
-    }
-    
-    static formatBytes(a,b=2){if(0===a)return"0 Bytes";const c=0>b?0:b,d=Math.floor(Math.log(a)/Math.log(1024));return parseFloat((a/Math.pow(1024,d)).toFixed(c))+" "+["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"][d]}
-}
-
-/*
-    Backend Programming Logic
-
-    These code are design to use with ao_backend.js for wrapping
-    AGI programming gateway content
-*/
-
-function ao_module_backend(){
-    return {
-        timeout: 500,
-        start: function(libraryPath){
-            this.libpath = libraryPath;
-    
-            //Initialize the parent objects
-            this.appdata.parent = this;
-            this.file.parent = this;
-            this.http.parent = this;
-        },
-        _agi_run: function(data, callback, failedcallback = undefined){
-                    $.ajax({
-                        url: ao_root + "system/ajgi/interface?script=" + this.libpath,
-                        method: "POST",
-                        data: data,
-                        success: function(data){
-                            if (typeof(callback) != "undefined"){
-                                callback(data);
-                            }
-                        },
-                        error: function(){
-                            if (typeof(failedcallback) != "undefined"){
-                                failedcallback();
-                            }
-                            console.log("Request failed");
-                        },
-                        timeout: this.timeout
-                    })
-                },
-        appdata: {
-            readFile: function(filepath, callback=undefined){
-                this.parent._agi_run({
-                    opr: "appdata.readFile",
-                    filepath: filepath
-                }, callback)
-            },
-            listDir: function(filepath, callback=undefined){
-                this.parent._agi_run({
-                    opr: "appdata.listDir",
-                    filepath: filepath
-                }, callback)
-            },
-        },
-        file: {
-            writeFile: function(filepath, content, callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.writeFile",
-                    filepath: filepath,
-                    content: content,
-                }, callback)
-            },
-            readFile: function(filepath, callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.readFile",
-                    filepath: filepath
-                }, callback)
-            },
-            deleteFile: function(filepath, callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.deleteFile",
-                    filepath: filepath
-                }, callback)
-            },
-            readdir: function(filepath, callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.readdir",
-                    filepath: filepath
-                }, callback)
-            },
-            walk: function(filepath, mode="all", callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.walk",
-                    filepath: filepath,
-                    mode: mode,
-                }, callback)
-            },
-            glob: function(wildcard, sort="user", callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.glob",
-                    wildcard: wildcard,
-                    sort: sort,
-                }, callback)
-            },
-            aglob: function(wildcard, sort="user", callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.aglob",
-                    wildcard: wildcard,
-                    sort: sort,
-                }, callback)
-            },
-            filesize: function(filepath, callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.filesize",
-                    filepath: filepath
-                }, callback)
-            },
-            fileExists: function(filepath, callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.fileExists",
-                    filepath: filepath
-                }, callback)
-            },
-            isDir: function(filepath, callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.isDir",
-                    filepath: filepath
-                }, callback)
-            },
-            mkdir: function(filepath, callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.mkdir",
-                    filepath: filepath
-                }, callback)
-            },
-            mtime: function(filepath, callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.mtime",
-                    filepath: filepath
-                }, callback)
-            },
-            rootName: function(filepath, callback=undefined){
-                this.parent._agi_run({
-                    opr: "file.rootName",
-                    filepath: filepath
-                }, callback)
-            }
-        },
-        http: {
-            get: function(targetURL, callback=undefined){
-                this.parent._agi_run({
-                    opr: "http.get",
-                    targetURL: targetURL
-                }, callback)
-            },
-            post: function(targetURL, postdata="", callback=undefined){
-                this.parent._agi_run({
-                    opr: "http.post",
-                    targetURL: targetURL,
-                    postdata: postdata
-                }, callback)
-            },
-            head: function(targetURL, header="", callback=undefined){
-                this.parent._agi_run({
-                    opr: "http.head",
-                    targetURL: targetURL,
-                    header: header
-                }, callback)
-            },
-            download: function(targetURL, saveDir="tmp:/", saveFilename="", callback=undefined){
-                if (saveFilename == ""){
-                    saveFilename = targetURL.split("/").pop();
-                    saveFilename = decodeURIComponent(saveFilename);
-                }
-
-                this.parent._agi_run({
-                    opr: "http.download",
-                    targetURL: targetURL,
-                    saveDir: saveDir,
-                    saveFilename: saveFilename
-                }, callback)
-            },
-        }
-    }
-}
-
+/*
+    ArOZ Online Module Javascript Wrapper
+
+    This is a wrapper for module developers to access system API easier and need not to dig through the source code.
+    Basically: Write less do more (?)
+    WARNING! SOME FUNCTION ARE NOT COMPATIBILE WITH PREVIOUS VERSION OF AO_MODULE.JS.
+    PLEASE REFER TO THE SYSTEM DOCUMENTATION FOR MORE INFORMATION.
+
+    *** Please include this javascript file with relative path instead of absolute path.
+    E.g. ../script/ao_module.js (OK)
+           /script/ao_module.js (NOT OK)
+*/
+var ao_module_virtualDesktop = false;
+try{
+    ao_module_virtualDesktop = !(!parent.isDesktopMode);
+}catch(ex){
+    //Running ArozOS inside iframe for some reason
+    console.log("CORS Access Error. Entering compatibility mode with virtual desktop mode disabled.");
+}
+
+var ao_root = null;
+//Get the current windowID if in Virtual Desktop Mode, return false if VDI is not detected
+var ao_module_windowID = false;
+var ao_module_parentID = false;
+var ao_module_callback = false;
+var ao_module_ime = false;
+if (ao_module_virtualDesktop)ao_module_windowID = $(window.frameElement).parent().parent().attr("windowId");
+if (ao_module_virtualDesktop)ao_module_parentID = $(window.frameElement).parent().parent().attr("parent");
+if (ao_module_virtualDesktop)ao_module_callback = $(window.frameElement).parent().parent().attr("callback");
+if (ao_module_virtualDesktop)ao_module_parentURL = $(window.frameElement).parent().find("iframe").attr("src");
+ao_root = ao_module_getAORootFromScriptPath();
+
+/*
+    Event bindings
+    The following events are required for ao_module to operate normally 
+    under Web Desktop Mode. 
+*/
+
+document.addEventListener("DOMContentLoaded", function() {
+    if (ao_module_virtualDesktop){
+        if (parent.window.ime == null){
+            return;
+        }
+        //Add window focus handler
+        document.addEventListener("mousedown", function(event) {
+            //When click on this document, focus this
+            ao_module_focus();
+
+            if (event.target.tagName == "INPUT" || event.target.tagName == "TEXTAREA"){
+
+            }else{
+                if (parent.window.ime.focus != null){
+                    if (ao_module_ime){
+                        //This is clicking on ime windows. Do not change focus
+                    }else{
+                        parent.window.ime.focus = null;
+                    }
+                }
+            }
+        }, true);
+    
+        //Add IME registration handler
+        var inputFields = document.querySelectorAll("input,textarea");
+        for (var i = 0; i < inputFields.length; i++){
+            if ($(inputFields[i]).attr("type") != undefined){
+                var thisType = $(inputFields[i]).attr("type");
+                if ((thisType == "text" || thisType =="search" || thisType =="url")){
+                    //Supported types of input
+                    ao_module_bindCustomIMEEvents(inputFields[i]);
+                }else{
+                    //Not supported type of inputs
+
+                }
+            }else{
+                //text area
+                ao_module_bindCustomIMEEvents(inputFields[i]);
+            }
+        }
+    }
+    /*
+     //Load html2canvas
+     if ($){
+        $.getScript(ao_root + "script/html2canvas.min.js", function() {
+            console.log("Html2canvas loaded")
+        });
+    }
+    */
+});
+
+/*
+    Startup Section Script
+    
+    These functions handle the startup of an ao_module and adapt them into the
+    standard arozos desktop eco-system api
+*/
+
+//Function handle to bind custom IME events
+function ao_module_bindCustomIMEEvents(object){
+    parent.bindObjectToIMEEvents(object);
+}
+
+function ao_module_screenshot(callback){
+    html2canvas(document.querySelector("body")).then(screenshot => {
+        return callback(screenshot);
+    });
+}
+
+//Get the ao_root from script includsion path
+function ao_module_getAORootFromScriptPath(){
+    var possibleRoot = "";
+    $("script").each(function(){
+        if (this.hasAttribute("src") && $(this).attr("src").includes("ao_module.js")){
+            var tmp = $(this).attr("src");
+            tmp = tmp.split("script/ao_module.js");
+            possibleRoot = tmp[0];
+        }
+    });
+    return possibleRoot;
+}
+
+//Get the input filename and filepath from the window hash paramter
+function ao_module_loadInputFiles(){
+    try{
+        if (window.location.hash.length == 0){
+            return null;
+        }
+        var inputFileInfo = window.location.hash.substring(1,window.location.hash.length);
+        inputFileInfo = JSON.parse(decodeURIComponent(inputFileInfo));
+        return inputFileInfo
+    }catch{
+        return null;
+    }
+}
+
+//Set the ao_module window to fixed size (not allowing resize)
+function ao_module_setFixedWindowSize(){
+    if (!ao_module_virtualDesktop){
+        return;
+    }
+    parent.setFloatWindowResizePolicy(ao_module_windowID, false);
+}
+
+//Restore a float window to be resizble
+function ao_module_setResizableWindowSize(){
+    if (!ao_module_virtualDesktop){
+        return;
+    }
+    parent.setFloatWindowResizePolicy(ao_module_windowID, true);
+}
+
+//Update the window size of the given float window object
+function ao_module_setWindowSize(width, height){
+    if (!ao_module_virtualDesktop){
+        return;
+    }
+    parent.setFloatWindowSize(ao_module_windowID, width, height)
+}
+
+//Update the floatWindow title
+function ao_module_setWindowTitle(newTitle){
+    if (!ao_module_virtualDesktop){
+        document.title = newTitle;
+        return;
+    }
+    parent.setFloatWindowTitle(ao_module_windowID, newTitle);
+}   
+
+//Set new window theme, default dark, support {dark/white}
+function ao_module_setWindowTheme(newtheme="dark"){
+    if (!ao_module_virtualDesktop){
+        return;
+    }
+    parent.setFloatWindowTheme(ao_module_windowID, newtheme);
+}   
+
+//Check if there are any windows with the same path. 
+//If yes, replace its hash content and reload to the new one and close the current floatWindow
+function ao_module_makeSingleInstance(){
+    $(window.parent.document).find(".floatWindow").each(function(){
+        if ($(this).attr("windowid") == ao_module_windowID){
+            return
+        }
+        var currentPath = window.location.pathname;
+        if ("/" + $(this).find("iframe").attr('src').split("#").shift() == currentPath){
+            //Another instance already running. Replace it with the current path
+            $(this).find("iframe").attr('src', window.location.pathname.substring(1) + window.location.hash);
+            $(this).find("iframe")[0].contentWindow.location.reload();
+            //Move the other instant to top
+            var targetfw = parent.getFloatWindowByID($(this).attr("windowid"))
+            parent.MoveFloatWindowToTop(targetfw);
+            //Close the instance
+            ao_module_close();
+            return true
+        }
+    });
+    return false
+}
+
+//Use for cross frame communication, example: 
+//let targetOpeningInstances = ao_module_getInstanceByPath("NotepadA/index.html")
+function ao_module_getInstanceByPath(matchingPath){
+    let targetInstance = "";
+    $(window.parent.document).find(".floatWindow").each(function(){
+        if ($(this).attr("windowid") == ao_module_windowID){
+            return
+        }
+        let thisfwPath = $(this).find("iframe").attr('src').split("#").shift();
+        if (thisfwPath == matchingPath){
+            targetInstance = $(this).attr("windowid");
+        }
+    });
+
+    if (targetInstance == ""){
+        return null;
+    }
+        
+    return parent.getFloatWindowByID(targetInstance);
+}
+
+
+//Close the current window
+function ao_module_close(){
+    if (!ao_module_virtualDesktop){
+        window.close('','_parent','');
+        window.location.href = ao_root + "SystemAO/closeTabInsturction.html";
+        return;
+    }
+    parent.closeFwProcess(ao_module_windowID);
+}
+
+//Focus this floatWindow
+function ao_module_focus(){
+    parent.MoveFloatWindowToTop(parent.getFloatWindowByID(ao_module_windowID));
+}
+
+//Set the floatWindow to top most mode
+function ao_module_setTopMost(){
+    parent.PinFloatWindowToTopMostMode(parent.getFloatWindowByID(ao_module_windowID));
+}
+
+//Unset the floatWindow top most mode
+function ao_module_unsetTopMost(){
+    parent.UnpinFloatWindowFromTopMostMode(parent.getFloatWindowByID(ao_module_windowID));
+}
+
+//Popup a file selection window for uplaod
+function ao_module_selectFiles(callback, fileType="file", accept="*", allowMultiple=false){
+    var input = document.createElement('input');
+    input.type = fileType;
+    input.multiple = allowMultiple;
+    input.accept = accept;
+    input.onchange = e => { 
+        var files = e.target.files; 
+        callback(files);
+    }
+    input.click();
+}
+
+//Open a path with File Manager, optional highligh filename
+function ao_module_openPath(path, filename=undefined){
+    //Trim away the last / if exists
+    if (path.substr(path.length - 1, 1) == "/"){
+        path = path.substr(0, path.length - 1);
+    }
+
+    if (filename == undefined){
+        if (ao_module_virtualDesktop){
+            parent.newFloatWindow({
+                url: "SystemAO/file_system/file_explorer.html#" + encodeURIComponent(path),
+                appicon: "SystemAO/file_system/img/small_icon.png",
+                width:1080,
+                height:580,
+                title: "File Manager"
+            });
+        }else{
+            window.open(ao_root + "SystemAO/file_system/file_explorer.html#" + encodeURIComponent(path))
+        }
+    }else{
+        var fileObject = [{
+            filepath: path + "/" + filename,
+            filename: filename,
+        }];
+        if (ao_module_virtualDesktop){
+            parent.newFloatWindow({
+                url: "SystemAO/file_system/file_explorer.html#" + encodeURIComponent(JSON.stringify(fileObject)),
+                appicon: "SystemAO/file_system/img/small_icon.png",
+                width:1080,
+                height:580,
+                title: "File Manager"
+            });
+        }else{
+            window.open(ao_root + "SystemAO/file_system/file_explorer.html#" + encodeURIComponent(JSON.stringify(fileObject)))
+        }
+    }
+
+   
+}
+
+//Open a particular tab using System Setting module. Require 
+//1) Setting Group
+//2) Setting Name
+function ao_module_openSetting(group, name){
+    var requestObject = {
+        group: group,
+        name: name
+    }
+
+    requestObject = encodeURIComponent(JSON.stringify(requestObject));
+    var openURL = "SystemAO/system_setting/index.html#" + requestObject;
+    if (ao_module_virtualDesktop){
+        ao_module_newfw({
+            url: openURL,
+            width: 1080,
+            height: 580,
+            appicon: "SystemAO/system_setting/img/small_icon.png",
+            title: "System Setting"
+        });
+    }else{
+        window.open(ao_root + openURL)
+    }
+}
+
+
+/*
+    ao_module_newfw(launchConfig) => Create a new floatWindow object from the given paramters
+
+    Most basic usage: (With auto assign UID, size and location)
+    ao_module_newfw({
+        url: "Dummy/index.html",
+        title: "Dummy Module",
+        appicon: "Dummy/img/icon.png"
+    });
+
+    Example usage that involve all configs:
+    ao_module_newfw({
+        url: "Dummy/index.html",
+        uid: "CustomUUID",
+        width: 1024,
+        height: 768,
+        appicon: "Dummy/img/icon.png",
+        title: "Dummy Module",
+        left: 100,
+        top: 100,
+        parent: ao_module_windowID,
+        callback: "childCallbackHandler"
+    });
+*/
+function ao_module_newfw(launchConfig){
+    if (launchConfig["parent"] == undefined){
+        launchConfig["parent"] = ao_module_windowID;
+    }
+    if (ao_module_virtualDesktop){
+        parent.newFloatWindow(launchConfig);
+    }else{
+        window.open(ao_root + launchConfig.url);
+    }
+}
+
+/*
+    File Selector
+
+    Open a file selector and return selected item back to the current window
+    Tips: Unlike the beta version, you can use this function in both Virtual Desktop Mode and normal mode.
+
+    Possible selection type:
+    type => {file / folder / all / new}
+
+    Example usage: 
+    ao_module_openFileSelector(fileSelected, "user:/Desktop", "file",true);
+
+    function fileSelected(filedata){
+        for (var i=0; i < filedata.length; i++){
+            var filename = filedata[i].filename;
+            var filepath = filedata[i].filepath;
+            //Do something here
+        }
+    }
+
+    If you want to create a new file or folder object, you can use the following options paramters
+    option = {
+        defaultName: "newfile.txt",            //Default filename used in new operation
+        fnameOverride: "myfunction",           //For those defined with window.myfunction
+        filter: ["mp3","aac","ogg","flac","wav"] //File extension filter
+    }
+*/
+let ao_module_fileSelectionListener;
+let ao_module_fileSelectorWindow;
+function ao_module_openFileSelector(callback,root="user:/", type="file",allowMultiple=false, options=undefined){
+    var initInfo = {
+        root: root,
+        type: type,
+        allowMultiple: allowMultiple,
+        listenerUUID: "",
+        options: options
+    }
+    var initInfoEncoded = encodeURIComponent(JSON.stringify(initInfo))
+    if (ao_module_virtualDesktop){
+        var callbackname = callback.name;
+        if (typeof(options) != "undefined" && typeof(options.fnameOverride) != "undefined"){
+            callbackname = options.fnameOverride;
+        }
+        console.log(callbackname);
+        parent.newFloatWindow({
+            url: "SystemAO/file_system/file_selector.html#" + initInfoEncoded,
+            width: 700,
+            height: 440,
+            appicon: "SystemAO/file_system/img/selector.png",
+            title: "Open",
+            parent: ao_module_windowID,
+            callback: callbackname
+        });
+    }else{
+        //Create a return listener base on localStorage
+        let listenerUUID = "fileSelector_" + new Date().getTime();
+        ao_module_fileSelectionListener = setInterval(function(){
+            if (localStorage.getItem(listenerUUID) === undefined || localStorage.getItem(listenerUUID)=== null){
+                //Not ready
+            }else{
+                //File ready!
+                var selectedFiles = JSON.parse(localStorage.getItem(listenerUUID));
+                console.log("Removing Localstorage Item " + listenerUUID);
+                
+                localStorage.removeItem(listenerUUID); 
+                setTimeout(function(){
+                    localStorage.removeItem(listenerUUID); 
+                },500);
+                if(selectedFiles == "&&selection_canceled&&"){
+                    //Selection canceled. Returm empty array
+                    callback([]);
+                }else{
+                    //Files Selected
+                    callback(selectedFiles);
+                }
+                
+                clearInterval(ao_module_fileSelectionListener);
+                ao_module_fileSelectorWindow.close();
+            }
+        },1000);
+
+        //Open the file selector in a new tab
+        initInfo.listenerUUID = listenerUUID;
+        initInfoEncoded = encodeURIComponent(JSON.stringify(initInfo))
+        ao_module_fileSelectorWindow = window.open(ao_root + "SystemAO/file_system/file_selector.html#" + initInfoEncoded,);
+    }
+}
+
+//Check if there is parent to callback
+function ao_module_hasParentCallback(){
+    if (ao_module_virtualDesktop){
+        //Check if parent callback exists
+        var thisFw;
+        $(parent.window.document.body).find(".floatWindow").each(function(){
+            if ($(this).attr('windowid') == ao_module_windowID){
+                thisFw = $(this);
+            }
+        });
+        var parentWindowID = thisFw.attr("parent");
+        var parentCallback = thisFw.attr("callback");
+        if (parentWindowID == "" || parentCallback == ""){
+            //No parent window defined
+            return false;
+        }
+
+        //Check if parent windows is alive
+        var parentWindow = undefined;
+        $(parent.window.document.body).find(".floatWindow").each(function(){
+            if ($(this).attr('windowid') == parentWindowID){
+                parentWindow = $(this);
+            }
+        });
+        if (parentWindow == undefined){
+            //parent window not exists
+            return false;
+        }
+
+        //Parent callback is set and ready to callback
+        return true;
+    }else{
+        return false
+    }
+}
+
+//Callback to parent with results
+function ao_module_parentCallback(data=""){
+    if (ao_module_virtualDesktop){
+        var thisFw;
+        $(parent.window.document.body).find(".floatWindow").each(function(){
+            if ($(this).attr('windowid') == ao_module_windowID){
+                thisFw = $(this);
+            }
+        });
+        var parentWindowID = thisFw.attr("parent");
+        var parentCallback = thisFw.attr("callback");
+        if (parentWindowID == "" || parentCallback == ""){
+            //No parent window defined
+            console.log("Undefined parent window ID or callback name");
+            return false;
+        }
+        var parentWindow = undefined;
+        $(parent.window.document.body).find(".floatWindow").each(function(){
+            if ($(this).attr('windowid') == parentWindowID){
+                parentWindow = $(this);
+            }
+        });
+        if (parentWindow == undefined){
+            //parent window not exists
+            console.log("Parent Window not exists!")
+            return false;
+        }
+        $(parentWindow).find('iframe')[0].contentWindow.eval(parentCallback + "(" + JSON.stringify(data) + ");")
+
+        //Focus the parent windows
+        parent.MoveFloatWindowToTop(parentWindow);
+        return true;
+    }else{
+        console.log("[ao_module] WARNING! Invalid call to parentCallback under non-virtualDesktop mode");
+        return false;
+    }
+}
+
+
+function ao_module_agirun(scriptpath, data, callback, failedcallback = undefined, timeout=0){
+    $.ajax({
+        url: ao_root + "system/ajgi/interface?script=" + scriptpath,
+        method: "POST",
+        data: data,
+        success: function(data){
+            if (typeof(callback) != "undefined"){
+                callback(data);
+            }
+        },
+        error: function(){
+            if (typeof(failedcallback) != "undefined"){
+                failedcallback();
+            }
+        },
+        timeout: timeout
+    });
+}
+
+function ao_module_uploadFile(file, targetPath, callback=undefined, progressCallback=undefined, failedcallback=undefined) {
+    let url = ao_root + 'system/file_system/upload'
+    let formData = new FormData()
+    let xhr = new XMLHttpRequest()
+    formData.append('file', file);
+    formData.append('path', targetPath);
+
+    xhr.open('POST', url, true);
+
+    xhr.upload.addEventListener("progress", function(e) {
+        if (progressCallback !== undefined){
+            progressCallback((e.loaded * 100.0 / e.total) || 100);
+        }
+    });
+
+    xhr.addEventListener('readystatechange', function(e) {
+        if (xhr.readyState == 4 && xhr.status == 200) {
+            if (callback !== undefined){
+                callback(e.target.response);
+            }
+        }
+        else if (xhr.readyState == 4 && xhr.status != 200) {
+            if (failedcallback !== undefined){
+                failedcallback(xhr.status);
+            }
+        }
+    })
+
+    xhr.send(formData);
+}
+
+
+/*
+    ao_module_storage, allow key-value storage per module settings. 
+    WARNING: NOT CROSS USER READ-WRITABLE
+    
+    ao_module_storage.setStorage(moduleName, configName,configValue);
+    ao_module_storage.loadStorage(moduleName, configName);
+*/
+class ao_module_storage {
+    static setStorage(moduleName, configName,configValue){
+    	$.ajax({
+    	  type: 'GET',
+    	  url: ao_root + "system/file_system/preference",
+    	  data: {key: moduleName + "/" + configName,value:configValue},
+    	  success: function(data){},
+    	  async:true
+    	});
+    	return true;
+    }
+    
+    static loadStorage(moduleName, configName, callback=undefined){
+        var result = "";
+        if (callback == undefined){
+            //Do not use async
+            $.ajax({
+                type: 'GET',
+                url: ao_root + "system/file_system/preference",
+                data: {key: moduleName + "/" + configName},
+                success: function(data){
+                        if (data.error !== undefined){
+                            result = "";
+                        }else{
+                            result = data;
+                        }
+                    },
+                error: function(data){result = "";},
+                async:false,
+                timeout: 3000
+            });
+              return result;
+        }else{
+            //Use sync method
+            $.ajax({
+                type: 'GET',
+                url: ao_root + "system/file_system/preference",
+                data: {key: moduleName + "/" + configName},
+                success: function(data){
+                        if (data.error !== undefined){
+                            callback("");
+                        }else{
+                            callback(data);
+                        }
+                    },
+                error: function(evt){
+                    callback("");
+                },
+                timeout: 30000
+            });
+        }
+    	
+    	
+    }
+}
+
+class ao_module_codec{
+	//Decode umfilename into standard filename in utf-8, which umfilename usually start with "inith"
+	//Example: ao_module_codec.decodeUmFilename(umfilename_here);
+    static decodeUmFilename(umfilename){
+		if (umfilename.includes("inith")){
+			var data = umfilename.split(".");
+			if (data.length == 1){
+				//This is a filename without extension
+				data = data[0].replace("inith","");
+				var decodedname = ao_module_codec.decode_utf8(ao_module_codec.hex2bin(data));
+				if (decodedname != "false"){
+					//This is a umfilename
+					return decodedname;
+				}else{
+					//This is not a umfilename
+					return umfilename;
+				}
+			}else{
+				//This is a filename with extension
+				var extension = data.pop();
+				var filename = data[0];
+				filename = filename.replace("inith",""); //Javascript replace only remove the first instances (i.e. the first inith in filename)
+				var decodedname = ao_module_codec.decode_utf8(ao_module_codec.hex2bin(filename));
+				if (decodedname != "false"){
+					//This is a umfilename
+					return decodedname + "." + extension;
+				}else{
+					//This is not a umfilename
+					return umfilename;
+				}
+			}
+			
+		}else{
+			//This is not umfilename as it doesn't have the inith prefix
+			return umfilename;
+		}
+	}
+	
+	//Encode filename to UMfilename
+	//Example: ao_module_codec.encodeUMFilename("test.stl");
+	static encodeUMFilename(filename){
+	    if (filename.substring(0,5) != "inith"){
+	        //Check if the filename include extension. 
+	        if (filename.includes(".")){
+	            //Filename with extension. pop it out first.
+	            var info = filename.split(".");
+	            var ext = info.pop();
+	            var filenameOnly = info.join(".");
+	            var encodedFilename = "inith" + ao_module_codec.decode_utf8(ao_module_codec.bin2hex(filenameOnly)) + "." + ext;
+	            return encodedFilename;
+	        }else{
+	            //Filename with no extension. Convert the whole name into UMfilename
+	            var encodedFilename = "inith" + ao_module_codec.decode_utf8(ao_module_codec.bin2hex(filename));
+	            return encodedFilename;
+	        }
+	    }else{
+	        //This is already a UMfilename. return the raw filename.
+	        return filename;
+	    }
+	}
+	
+	//Decode hexFoldername into standard foldername in utf-8, return the original name if it is not a hex foldername
+	//Example: ao_module_codec.decodeHexFoldername(hexFolderName_here);
+	static decodeHexFoldername(folderName, prefix=true){
+	    var decodedFoldername = ao_module_codec.decode_utf8(ao_module_codec.hex2bin(folderName));
+		if (decodedFoldername == "false"){
+			//This is not a hex encoded foldername
+			decodedFoldername = folderName;
+		}else{
+			//This is a hex encoded foldername
+			if (prefix){
+			    	decodedFoldername = "*" + decodedFoldername;
+			}else{
+			    	decodedFoldername =decodedFoldername;
+			}
+		}
+		return decodedFoldername;
+	}
+    
+    //Encode foldername into hexfoldername
+    //Example: ao_module_codec.encodeHexFoldername("test");
+    static encodeHexFoldername(folderName){
+        var encodedFilename = "";
+        if (ao_module_codec.decodeHexFoldername(folderName) == folderName){
+            //This is not hex foldername. Encode it
+            encodedFilename = ao_module_codec.decode_utf8(ao_module_codec.bin2hex(folderName));
+        }else{
+            //This folder name already encoded. Return the original value
+            encodedFilename = folderName;
+        }
+        
+        return encodedFilename;
+    }
+    static hex2bin(s){
+      var ret = []
+      var i = 0
+      var l
+      s += ''
+      for (l = s.length; i < l; i += 2) {
+        var c = parseInt(s.substr(i, 1), 16)
+        var k = parseInt(s.substr(i + 1, 1), 16)
+        if (isNaN(c) || isNaN(k)) return false
+        ret.push((c << 4) | k)
+      }
+    
+      return String.fromCharCode.apply(String, ret)
+    }
+    
+    static bin2hex(s){
+         var i
+          var l
+          var o = ''
+          var n
+          s += ''
+          for (i = 0, l = s.length; i < l; i++) {
+            n = s.charCodeAt(i)
+              .toString(16)
+            o += n.length < 2 ? '0' + n : n
+          }
+          return o
+    }
+    
+    static decode_utf8(s) {
+      return decodeURIComponent(escape(s));
+    }
+}
+
+
+/**
+ArOZ Online Module Utils for quick deploy of ArOZ Online WebApps
+
+ao_module_utils.objectToAttr(object); //object to DOM attr
+ao_module_utils.attrToObject(attr); //DOM attr to Object
+ao_module_utils.getRandomUID(); //Get random UUID from timestamp
+ao_module_utils.getIconFromExt(ext); //Get icon tag from file extension
+
+ao_module_utils.getDropFileInfo(dropEvent); //Get the filepath and filename list from file explorer drag drop
+ao_module_utils.formatBytes(byte, decimals); //Format file byte size to human readable size
+ao_module_utils.timeConverter(unix_timestamp); //Get human readable timestamp 
+**/
+class ao_module_utils{
+    
+    //Two simple functions for converting any Javascript object into string that can be put into the attr value of an DOM object
+    static objectToAttr(object){
+       return encodeURIComponent(JSON.stringify(object));
+    }
+    
+    static attrToObject(attr){
+        return JSON.parse(decodeURIComponent(attr));
+    }
+    
+    //Get a random id for a new floatWindow, use with var uid = ao_module_utils.getRandomUID();
+    static getRandomUID(){
+        return new Date().getTime();
+    }
+
+    static stringToBlob(text, mimetype="text/plain"){
+        var blob = new Blob([text], { type: mimetype });
+        return blob
+    }
+
+    static blobToFile(blob, filename, mimetype="text/plain"){
+        var file = new File([blob], filename, {type: mimetype});
+        return file
+    }
+    
+    //Get the icon of a file with given extension (ext), use with ao_module_utils.getIconFromExt("ext");
+    static getIconFromExt(ext){
+        var ext = ext.toLowerCase().trim();
+        var iconList={
+            md:"file text outline",
+            txt:"file text outline",
+            pdf:"file pdf outline",
+            doc:"file word outline",
+            docx:"file word outline",
+            odt:"file word outline",
+            xlsx:"file excel outline",
+            ods:"file excel outline",
+            ppt:"file powerpoint outline",
+            pptx:"file powerpoint outline",
+            odp:"file powerpoint outline",
+            jpg:"file image outline",
+            png:"file image outline",
+            jpeg:"file image outline",
+            gif:"file image outline",
+            odg:"file image outline",
+            psd:"file image outline",
+            zip:"file archive outline",
+            '7z':"file archive outline",
+            rar:"file archive outline",
+            tar:"file archive outline",
+            mp3:"file audio outline",
+            m4a:"file audio outline",
+            flac:"file audio outline",
+            wav:"file audio outline",
+            aac:"file audio outline",
+            mp4:"file video outline",
+            webm:"file video outline",
+            php:"file code outline",
+            html:"file code outline",
+            htm:"file code outline",
+            js:"file code outline",
+            css:"file code outline",
+            xml:"file code outline",
+            json:"file code outline",
+            csv:"file code outline",
+            odf:"file code outline",
+            bmp:"file image outline",
+            rtf:"file text outline",
+            wmv:"file video outline",
+            mkv:"file video outline",
+            ogg:"file audio outline",
+            stl:"cube",
+            obj:"cube",
+            "3ds":"cube",
+            fbx:"cube",
+            collada:"cube",
+            step:"cube",
+            iges:"cube",
+            gcode:"cube",
+            shortcut:"external square",
+            opus:"file audio outline",
+            apscene:"cubes"
+        };
+        var icon = "";
+        if (ext == ""){
+            icon = "folder outline";
+        }else{
+            icon = iconList[ext];
+            if (icon == undefined){
+                icon = "file outline"
+            }
+        }
+        return icon;
+	}
+	
+	//Get the drop file properties {filepath: xxx, filename: xxx} from file drop events from file exploere
+	static getDropFileInfo(dropEvent){
+		if (dropEvent.dataTransfer.getData("filedata") !== ""){
+			var filelist = dropEvent.dataTransfer.getData("filedata");
+			filelist = JSON.parse(filelist);
+			return filelist;
+		}
+    }
+
+    static readFileFromFileObject(fileObject, successCallback, failedCallback=undefined){
+        let reader = new FileReader();
+        reader.readAsText(fileObject);
+        reader.onload = function() {
+            successCallback(reader.result);
+        };
+        reader.onerror = function() {
+            if (failedCallback != undefined){
+                failedCallback(reader.error);
+            }else{
+                console.log(reader.error);
+            }
+        };
+
+    }
+
+    static durationConverter(seconds){
+        var days = Math.floor(seconds / 86400);
+        seconds -= days * 86400;
+        var hours = Math.floor(seconds / 3600) % 24;
+        seconds -= hours * 3600;
+        var minutes = Math.floor(seconds / 60) % 60;
+        seconds -= minutes * 60;
+        var seconds = seconds % 60;
+
+        var resultDuration = "";
+        if (days > 0){
+            resultDuration += days + " Day";
+            if (days > 1){
+                resultDuration+= "s"
+            }
+            resultDuration += " "
+        }
+
+        if (hours > 0){
+            resultDuration += hours + " Hour"
+            if (hours > 1){
+                resultDuration += "s"
+            }
+            resultDuration += " "
+        }
+
+        if (minutes > 0){
+            resultDuration += minutes + " Minute"
+            if (minutes > 1){
+                resultDuration += "s"
+            }
+            resultDuration += " "
+        }
+
+        if (seconds > 0){
+            resultDuration += seconds + " Second"
+            if (seconds > 1){
+                resultDuration += "s"
+            }
+            resultDuration += " "
+        }
+        
+        return resultDuration;
+    }
+
+    static timeConverter(UNIX_timestamp){
+        var a = new Date(UNIX_timestamp * 1000);
+        var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
+        var year = a.getFullYear();
+        var month = months[a.getMonth()];
+        var date = a.getDate();
+        var hour = a.getHours().toString().padStart(2, "0");
+        var min = a.getMinutes().toString().padStart(2, "0");
+        var sec = a.getSeconds().toString().padStart(2, "0");
+        var time = date + ' ' + month + ' ' + year + ' ' + hour + ':' + min + ':' + sec ;
+        return time;
+    }
+
+    static getWebSocketEndpoint(){
+        let protocol = "wss://";
+        if (location.protocol !== 'https:') {
+            protocol = "ws://";
+        }
+        let port = window.location.port;
+        if (window.location.port == ""){
+            if (location.protocol !== 'https:') {
+                port = "80";
+            }else{
+                port = "443";
+            }
+            
+        }
+        let wsept = (protocol + window.location.hostname + ":" + port);
+        return wsept;
+    }
+    
+    static formatBytes(a,b=2){if(0===a)return"0 Bytes";const c=0>b?0:b,d=Math.floor(Math.log(a)/Math.log(1024));return parseFloat((a/Math.pow(1024,d)).toFixed(c))+" "+["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"][d]}
+}
+
+/*
+    Backend Programming Logic
+
+    These code are design to use with ao_backend.js for wrapping
+    AGI programming gateway content
+*/
+
+function ao_module_backend(){
+    return {
+        timeout: 500,
+        start: function(libraryPath){
+            this.libpath = libraryPath;
+    
+            //Initialize the parent objects
+            this.appdata.parent = this;
+            this.file.parent = this;
+            this.http.parent = this;
+        },
+        _agi_run: function(data, callback, failedcallback = undefined){
+                    $.ajax({
+                        url: ao_root + "system/ajgi/interface?script=" + this.libpath,
+                        method: "POST",
+                        data: data,
+                        success: function(data){
+                            if (typeof(callback) != "undefined"){
+                                callback(data);
+                            }
+                        },
+                        error: function(){
+                            if (typeof(failedcallback) != "undefined"){
+                                failedcallback();
+                            }
+                            console.log("Request failed");
+                        },
+                        timeout: this.timeout
+                    })
+                },
+        appdata: {
+            readFile: function(filepath, callback=undefined){
+                this.parent._agi_run({
+                    opr: "appdata.readFile",
+                    filepath: filepath
+                }, callback)
+            },
+            listDir: function(filepath, callback=undefined){
+                this.parent._agi_run({
+                    opr: "appdata.listDir",
+                    filepath: filepath
+                }, callback)
+            },
+        },
+        file: {
+            writeFile: function(filepath, content, callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.writeFile",
+                    filepath: filepath,
+                    content: content,
+                }, callback)
+            },
+            readFile: function(filepath, callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.readFile",
+                    filepath: filepath
+                }, callback)
+            },
+            deleteFile: function(filepath, callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.deleteFile",
+                    filepath: filepath
+                }, callback)
+            },
+            readdir: function(filepath, callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.readdir",
+                    filepath: filepath
+                }, callback)
+            },
+            walk: function(filepath, mode="all", callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.walk",
+                    filepath: filepath,
+                    mode: mode,
+                }, callback)
+            },
+            glob: function(wildcard, sort="user", callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.glob",
+                    wildcard: wildcard,
+                    sort: sort,
+                }, callback)
+            },
+            aglob: function(wildcard, sort="user", callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.aglob",
+                    wildcard: wildcard,
+                    sort: sort,
+                }, callback)
+            },
+            filesize: function(filepath, callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.filesize",
+                    filepath: filepath
+                }, callback)
+            },
+            fileExists: function(filepath, callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.fileExists",
+                    filepath: filepath
+                }, callback)
+            },
+            isDir: function(filepath, callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.isDir",
+                    filepath: filepath
+                }, callback)
+            },
+            mkdir: function(filepath, callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.mkdir",
+                    filepath: filepath
+                }, callback)
+            },
+            mtime: function(filepath, callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.mtime",
+                    filepath: filepath
+                }, callback)
+            },
+            rootName: function(filepath, callback=undefined){
+                this.parent._agi_run({
+                    opr: "file.rootName",
+                    filepath: filepath
+                }, callback)
+            }
+        },
+        http: {
+            get: function(targetURL, callback=undefined){
+                this.parent._agi_run({
+                    opr: "http.get",
+                    targetURL: targetURL
+                }, callback)
+            },
+            post: function(targetURL, postdata="", callback=undefined){
+                this.parent._agi_run({
+                    opr: "http.post",
+                    targetURL: targetURL,
+                    postdata: postdata
+                }, callback)
+            },
+            head: function(targetURL, header="", callback=undefined){
+                this.parent._agi_run({
+                    opr: "http.head",
+                    targetURL: targetURL,
+                    header: header
+                }, callback)
+            },
+            download: function(targetURL, saveDir="tmp:/", saveFilename="", callback=undefined){
+                if (saveFilename == ""){
+                    saveFilename = targetURL.split("/").pop();
+                    saveFilename = decodeURIComponent(saveFilename);
+                }
+
+                this.parent._agi_run({
+                    opr: "http.download",
+                    targetURL: targetURL,
+                    saveDir: saveDir,
+                    saveFilename: saveFilename
+                }, callback)
+            },
+        }
+    }
+}
+