diff --git a/exec/load/xtrnmenulib.js b/exec/load/xtrnmenulib.js new file mode 100644 index 0000000000000000000000000000000000000000..e7cff9fab33e45554f2dd4c01a553cac101bcc5d --- /dev/null +++ b/exec/load/xtrnmenulib.js @@ -0,0 +1,410 @@ +/** + * Custom External Program Menu Library for Custom External Program Menus + * by Michael Long mlong innerrealmbbs.us + * + * This provides common functionality for retrieving menus used by + * the loadable module xtrnmenu.js and by the web interface + * 099-xtrnmenu-games.xjs + */ + +"use strict"; + +load("sbbsdefs.js", "K_NONE"); + +/* text.dat entries */ +require("text.js", "XtrnProgLstFmt"); + +function ExternalMenus() { + this.options = {}; + this.xtrn_custommenu_options = {}; + this.menuconfig = {}; + + this.getOptions(); + this.getMenuConfig(); +} + +ExternalMenus.prototype.getMenuConfig = function() { + var config_file = new File(system.ctrl_dir + "xtrnmenu.cfg"); + var config_src; + this.menuconfig = undefined; + if (config_file.open('r+')) { + config_src = config_file.read(); + config_file.close(); + } + + if (typeof config_src !== "undefined") { + this.menuconfig = JSON.parse(config_src.toString()); + } +} + +ExternalMenus.prototype.getOptions = function(menutype, menuid) { + if (typeof menutype === "undefined") { + menutype = 'custommenu'; + } + + // Get xtrn_sec options from modopts.ini [xtrn_sec] + if ((this.options = load({}, "modopts.js", "xtrn_sec")) == null) { + this.options = { multicolumn: true, sort: false }; + } + + // Get xtrn_custommenu options from modopts.ini [xtrn_custommenu] + if ((this.xtrn_custommenu_options = load({}, "modopts.js", "xtrnmenu")) == null) { + this.xtrn_custommenu_options = { }; + } + + // in all cases, we start with the xtrn_sec options as the base and set the defaults + if (this.options.multicolumn === undefined) + this.options.multicolumn = true; + + if (this.options.multicolumn_separator === undefined) + this.options.multicolumn_separator = " "; + + if (this.options.multicolumn_fmt === undefined) + this.options.multicolumn_fmt = system.text(XtrnProgLstFmt); + + if (this.options.singlecolumn_fmt === undefined) + this.options.singlecolumn_fmt = "\x01h\x01c%3u \xb3 \x01n\x01c%s\x01h "; + + if (this.options.singlecolumn_margin == undefined) + this.options.singlecolumn_margin = 7; + + if (typeof bbs !== "undefined") { + if (this.options.singlecolumn_height == undefined) + this.options.singlecolumn_height = console.screen_rows - this.options.singlecolumn_margin; + + // override and turn off multicolumn if terminal width is less than 80 + if (console.screen_columns < 80) + options.multicolumn = false; + } + + if (this.options.restricted_user_msg === undefined) + this.options.restricted_user_msg = system.text(R_ExternalPrograms); + + if (this.options.no_programs_msg === undefined) + this.options.no_programs_msg = system.text(NoXtrnPrograms); + + if (this.options.header_fmt === undefined) + this.options.header_fmt = system.text(XtrnProgLstHdr); + + if (this.options.titles === undefined) + this.options.titles = system.text(XtrnProgLstTitles); + + if (this.options.underline === undefined) + this.options.underline = system.text(XtrnProgLstUnderline); + + if (this.options.which === undefined) + this.options.which = system.text(WhichXtrnProg); + + if (this.options.clear_screen === undefined) + this.options.clear_screen = true; + + if (this.options.section_fmt === undefined) + this.options.section_fmt = "\x01y\x01g%3d:\x01n\x01g %s" + + if (this.options.section_header_fmt === undefined) + this.options.section_header_fmt = "\x01-\x01gSelect \x01hExternal Program Section\x01-\x01g:" + + if (this.options.section_which === undefined) + this.options.section_which = "\r\n\x01-\x01gWhich, \x01w\x01h~Q\x01n\x01guit or [1]: \x01h" + + // if its a custom menu, then override with custommenu_options + if (menutype == 'custommenu') { + if (typeof this.xtrn_custommenu_options.multicolumn_fmt !== "undefined") { + this.options.multicolumn_fmt = this.xtrn_custommenu_options.multicolumn_fmt; + } else { + // cannot default to xtrn_sec multicolumn_fmt due to use of %u instead of %s + this.options.multicolumn_fmt = "\x01h\x01c%3s \xb3 \x01n\x01c%-32.32s\x01h "; + } + + if (typeof this.xtrn_custommenu_options.singlecolumn_fmt !== "undefined") { + this.options.singlecolumn_fmt = this.xtrn_custommenu_options.singlecolumn_fmt; + } else { + // cannot default to xtrn_sec multicolumn_fmt due to use of %u instead of %s + this.options.singlecolumn_fmt = "\x01h\x01c%3s \xb3 \x01n\x01c%s\x01h "; + } + + this.options.header_fmt = (typeof this.xtrn_custommenu_options.header_fmt !== "undefined") + ? this.xtrn_custommenu_options.header_fmt : this.options.header_fmt; + + this.options.titles = (typeof this.xtrn_custommenu_options.titles !== "undefined") + ? this.xtrn_custommenu_options.titles : this.options.titles; + + this.options.which = (typeof this.xtrn_custommenu_options.which !== "undefined") + ? this.xtrn_custommenu_options.which : this.options.which; + + this.options.underline = (typeof this.xtrn_custommenu_options.underline !== "undefined") + ? this.xtrn_custommenu_options.underline : this.options.underline; + + this.options.multicolumn_separator = (typeof this.xtrn_custommenu_options.multicolumn_separator !== "undefined") + ? this.xtrn_custommenu_options.multicolumn_separator : this.options.multicolumn_separator; + + this.options.multicolumn = (typeof this.xtrn_custommenu_options.multicolumn !== "undefined") + ? this.xtrn_custommenu_options.multicolumn : this.options.multicolumn; + + this.options.sort = (typeof this.xtrn_custommenu_options.sort !== "undefined") + ? this.xtrn_custommenu_options.sort : this.options.sort; + + this.options.clear_screen = (typeof this.xtrn_custommenu_options.clear_screen !== "undefined") + ? this.xtrn_custommenu_options.clear_screen : this.options.clear_screen; + + this.options.singlecolumn_margin = (typeof this.xtrn_custommenu_options.singlecolumn_margin !== "undefined") + ? this.xtrn_custommenu_options.singlecolumn_margin : this.options.singlecolumn_margin; + + if (typeof bbs !== "undefined") { + this.options.singlecolumn_height = (typeof this.xtrn_custommenu_options.singlecolumn_height !== "undefined") + ? this.xtrn_custommenu_options.singlecolumn_height : this.options.singlecolumn_height; + } + + // no need to override restricted_user_msg or no_programs_msg - these + // will be the same for both types of menus + + // Allow overriding on a per-menu basis + var menuoptions = load({}, "modopts.js", "xtrnmenu:" + menuid); + if ((typeof menuid !== "undefined") && (menuoptions != null)) { + for (var m in menuoptions) { + this.options[m] = menuoptions[m]; + } + } + } + + // these options only apply to terminals/consoles + + // the intention is to obtain all the mod_opts options for xtrn_sec, and + // override if a custom menu global setting is set + + //// The following are used for the enhanced custom menu functionality + if (this.options.custom_menu_not_found_msg === undefined) { + this.options.custom_menu_not_found_msg = "Menu %MENUID% not found"; + } + + if (this.options.custom_menu_program_not_found_msg === undefined) { + this.options.custom_menu_program_not_found_msg = "Program %PROGRAMID% not found"; + } + + return this.options; +} + +// return a custom menu object +ExternalMenus.prototype.getMenu = function(menuid) { + // grab the specified menu, or get the main menu + if ((typeof menuid === "undefined") || !menuid) { + menuid = "main"; + } else { + menuid = menuid.toLowerCase(); + } + + var menu; + + if ((typeof this.menuconfig !== "undefined") && (typeof this.menuconfig.menus !== "undefined")) { + this.menuconfig.menus.some(function (indmenu) { + if (indmenu.id.toLowerCase() == menuid) { + menu = indmenu; + } + }); + } + + if (!menu && (menuid == "main")) { + // no custom menus defined, make one to mimic old behavior + + var menuitems = []; + + var i; + xtrn_area.sec_list.forEach(function (sec) { + if (sec.can_access) { + menuitems.push({ + "input": i, + "target": sec.code, + "type": "xtrnmenu", + "title": sec.name, + "access_string": sec.ars + }); + i++; + } + }); + + menu = { + "id": "main", + "title": "Main Menu", + "items": menuitems + }; + } + return menu; +} + +// return a section menu object (stock synchronet external door section) +ExternalMenus.prototype.getSectionMenu = function(menuid) { + + // grab the specified menu, or get the main menu + if ((typeof menuid === "undefined") || !menuid) { + return false; + } + + var menuitems = []; + var menu, title; + + xtrn_area.sec_list.some(function (sec) { + + if (sec.code.toLowerCase() == menuid.toLowerCase()) { + title = sec.name; + + if (!sec.can_access || sec.prog_list.length < 1) { + return false; + } + + var i = 1; + sec.prog_list.some(function (prog) { + menuitems.push({ + 'input' : i, + 'target': prog.code, + 'title': prog.name, + 'type': 'xtrnprog', + 'access_string': prog.ars, + 'cost': prog.cost + }); + i++; + }); + + if (menuitems.length > 0) { + menu = { + 'id': menuid, + 'title': title, + 'items': menuitems, + }; + } + return; + } + }); + + return menu; +} + +// Sort the menu items according to options +ExternalMenus.prototype.getSortedItems = function(menuobj) { + var sort_type; + + if ((typeof menuobj.sort_type !== "undefined") && menuobj.sort_type) { + sort_type = menuobj.sort_type; // "name" or "key" + } else { + sort_type = this.options.sort; // bool + } + + // first, build a new menu with only options they have access to + var menuitemsfiltered = []; + for (i in menuobj.items) { + switch (menuobj.items[i].type) { + case 'xtrnmenu': + for (j in xtrn_area.sec_list) { + if (xtrn_area.sec_list[j].code.toUpperCase() == menuobj.items[i].target.toUpperCase()) { + if (xtrn_area.sec_list[j].can_access) { + menuitemsfiltered.push(menuobj.items[i]); + } + } + } + break; + + case 'xtrnprog': + for (j in xtrn_area.sec_list) { + for (var k in xtrn_area.sec_list[j].prog_list) { + if (xtrn_area.sec_list[j].prog_list[k].code.toUpperCase() == menuobj.items[i].target.toUpperCase()) { + if (xtrn_area.sec_list[j].prog_list[k].can_access) { + menuitemsfiltered.push(menuobj.items[i]); + } + } + } + } + break; + + case 'custommenu': + default: + if ((typeof menuobj.items[i].access_string === "undefined") || !menuobj.items[i].access_string) { + // no access string defined, everyone gets access + menuitemsfiltered.push(menuobj.items[i]); + } else { + if (user.compare_ars(menuobj.items[i].access_string)) { + // they have access + menuitemsfiltered.push(menuobj.items[i]); + } + } + break; + } + } + + // if no custom input keys are specified for an input, then assign a numeric input + // this would mimic the built-in external section menu functionality + // but this is only assigned on sort_type key because if the sort type is for + // titles, we need to sort by title first before assigning the sequential numbers + var sortind = 0; + // make sure we only use the next available number for automatic assignment + for (i in menuitemsfiltered) { + if (!isNaN(menuitemsfiltered[i].input)) { + if (menuitemsfiltered[i].input > sortind) { + sortind = menuitemsfiltered[i].input; + } + } + } + sortind++; + + if (sort_type == "key") { + for (i in menuitemsfiltered) { + if (!menuitemsfiltered[i].input) { + menuitemsfiltered[i].input = sortind; + sortind++; + } + } + } + + if (sort_type) { + switch (sort_type) { + case "key": + menuitemsfiltered.sort(this.sort_by_input); + break; + default: + case "title": + case "name": + menuitemsfiltered.sort(this.sort_by_title); + break; + } + } + + // if this is a sort by title and the key is empty, it will be assigned the next available number + // this is to support auto-generated inputs like is done on the built-in section menus + // however, to keep the numeric keys in order, the numbers could not be pre-assigned like it done + // on the sort_type key + for (i in menuitemsfiltered) { + if ((sort_type !== "key") && !menuitemsfiltered[i].input) { + menuitemsfiltered[i].input = sortind; + sortind++; + } + } + + return menuitemsfiltered; +} + +// Original sort function used by external_program_menu and external_section_menu +ExternalMenus.prototype.sort_by_name = function(a, b) { + if(a.name.toLowerCase()>b.name.toLowerCase()) return 1; + if(a.name.toLowerCase()<b.name.toLowerCase()) return -1; + return 0; +} + +// Sort by title - used by external_section_menu_custom +ExternalMenus.prototype.sort_by_title = function(a, b) { + if (a.title.toLowerCase() < b.title.toLowerCase()) { + return -1; + } + if (a.title.toLowerCase() > b.title.toLowerCase()) { + return 1; + } + return 0; +} + +// Sort by input key - used by external_section_menu_custom +ExternalMenus.prototype.sort_by_input = function(a, b) { + if (a.input.toString().toLowerCase() < b.input.toString().toLowerCase()) { + return -1; + } + if (a.input.toString().toLowerCase() > b.input.toString().toLowerCase()) { + return 1; + } + return 0; +} diff --git a/exec/xtrnmenu.js b/exec/xtrnmenu.js new file mode 100644 index 0000000000000000000000000000000000000000..7a32868f43ebfac10e18da3372c0a19a9279786f --- /dev/null +++ b/exec/xtrnmenu.js @@ -0,0 +1,266 @@ +/** + * Xtrn Menu Mod + * Custom External Program Menus + * by Michael Long mlong innerrealmbbs.us + * + * This is the loadable module that displays the custom external menus + * in terminal server (telnet/rlogin/ssh) + * + * To jump to a specific menu, pass the ID as an argument + * + * To set options, add to modopts.ini [xtrnmenu] + * + * See instructions at http://wiki.synchro.net/module:xtrnmenu + */ + +"use strict"; + +require("sbbsdefs.js", "K_NONE"); + +load("xtrnmenulib.js"); + +var options, xsec = -1; + +//// Main +var ExternalMenus = new ExternalMenus(); +const menuconfig = ExternalMenus.menuconfig; + +{ + var i,j; + for(i in argv) { + for(j in xtrn_area.sec_list) { + if(argv[i].toLowerCase()==xtrn_area.sec_list[j].code) + xsec=j; + } + } +} +if (xsec > -1) { + // if its the id of a standard section menu, send it to the + // stock menu + js.exec("xtrn_sec.js", {}, xsec); +} else if (typeof argv[0] !== "undefined") { + // if its not a section menu, assume it is a custom menu + external_section_menu_custom(argv[0]); +} else { + // main custom menu + external_section_menu_custom(); +} + + +// Renders the top-level external menu +function external_section_menu_custom(menuid) +{ + var i, menucheck, menuobj, item_multicolumn_fmt, item_singlecolumn_fmt, + cost, multicolumn, menuitemsfiltered = []; + var validkeys = []; // valid chars on menu selection + var keymax = 0; // max integer allowed on menu selection + + var options = ExternalMenus.getOptions('custommenu', menuid); + + menuobj = ExternalMenus.getMenu(menuid); + + // Allow overriding auto-format on a per-menu basis + var multicolumn_fmt = options.multicolumn_fmt; + var singlecolumn_fmt = options.singlecolumn_fmt; + + while (bbs.online) { + console.aborted = false; + + if (typeof menuobj === "undefined") { + doerror(options.custom_menu_not_found_msg.replace('%MENUID%', menuid)); + break; + } + + if (options.clear_screen) { + console.clear(LIGHTGRAY); + } + + if (user.security.restrictions&UFLAG_X) { + write(options.restricted_user_msg); + break; + } + + if (!xtrn_area.sec_list.length) { + write(options.no_programs_msg); + break; + } + + var keyin; + + system.node_list[bbs.node_num-1].aux = 0; /* aux is 0, only if at menu */ + bbs.node_action = NODE_XTRN; + bbs.node_sync(); + + menuitemsfiltered = ExternalMenus.getSortedItems(menuobj); + + if (!bbs.menu("xtrnmenu_head_" + menuid, P_NOERROR) && !bbs.menu("xtrnmenu_head", P_NOERROR)) { + bbs.menu("xtrn_head", P_NOERROR); + } + + // if file exists text/menu/xtrnmenu_(menuid).[rip|ans|mon|msg|asc], + // then display that, otherwise dynamiic + if (!bbs.menu("xtrnmenu_" + menuid, P_NOERROR)) { + + // if no custom menu file in text/menu, create a dynamic one + multicolumn = options.multicolumn && menuitemsfiltered.length > options.singlecolumn_height; + + printf(options.header_fmt, menuobj.title); + if(options.titles.trimRight() != '') + write(options.titles); + if(multicolumn) { + write(options.multicolumn_separator); + if (options.titles.trimRight() != '') + write(options.titles); + } + if(options.underline.trimRight() != '') { + console.crlf(); + write(options.underline); + } + if(multicolumn) { + write(options.multicolumn_separator); + if (options.underline.trimRight() != '') + write(options.underline); + } + console.crlf(); + + // n is the number of items for the 1st column + var n; + if (multicolumn) { + n = Math.floor(menuitemsfiltered.length / 2) + (menuitemsfiltered.length & 1); + } else { + n = menuitemsfiltered.length; + } + + // j is the index for each menu item on 2nd column + var j = n; // start j at the first item for 2nd column + for (i = 0; i < n && !console.aborted; i++) { + cost = ""; + if (menuitemsfiltered[i].type == "xtrnprog") { + // if its an external program, get the cost + cost = xtrn_area.prog[menuitemsfiltered[i].target.toLowerCase()].cost; + } + + console.add_hotspot(menuitemsfiltered[i].input.toString()); + + validkeys.push(menuitemsfiltered[i].input.toString()); + var intCheck = Number(menuitemsfiltered[i].input); + if (!intCheck.isNaN) { + if (intCheck > keymax) { + keymax = menuitemsfiltered[i].input; + } + } + + // allow overriding format on a per-item basis + // great for featuring a specific game + var checkkey = menuitemsfiltered[i].target + '-multicolumn_fmt'; + checkkey = checkkey.toLowerCase(); + item_multicolumn_fmt = (typeof options[checkkey] !== "undefined") ? + options[checkkey] : options.multicolumn_fmt; + + checkkey = menuitemsfiltered[i].target + '-singlecolumn_fmt' + checkkey = checkkey.toLowerCase(); + item_singlecolumn_fmt = (typeof options[checkkey] !== "undefined") ? + options[checkkey] : options.singlecolumn_fmt; + + printf(multicolumn ? item_multicolumn_fmt : item_singlecolumn_fmt, + menuitemsfiltered[i].input.toString().toUpperCase(), + menuitemsfiltered[i].title, + cost + ); + + if (multicolumn) { + if ((typeof(menuitemsfiltered[j]) !== "undefined")) { + validkeys.push(menuitemsfiltered[j].input.toString()); + + var intCheck = Number(menuitemsfiltered[j].input); + if (!intCheck.isNaN) { + if (intCheck > keymax) { + keymax = menuitemsfiltered[j].input; + } + } + + // allow overriding format on a per-item basis + // great for featuring a specific game + var checkkey = menuitemsfiltered[j].target + '-multicolumn_fmt'; + checkkey = checkkey.toLowerCase(); + item_multicolumn_fmt = (typeof options[checkkey] !== "undefined") ? + options[checkkey] : options.multicolumn_fmt; + + checkkey = menuitemsfiltered[j].target + '-singlecolumn_fmt' + checkkey = checkkey.toLowerCase(); + + write(options.multicolumn_separator); + console.add_hotspot(menuitemsfiltered[j].input.toString()); + printf(item_multicolumn_fmt, + menuitemsfiltered[j].input.toString().toUpperCase(), + menuitemsfiltered[j].title, + cost + ); + } else { + write(options.multicolumn_separator); + } + j++; + } + console.crlf(); + } + + if (!bbs.menu("xtrnmenu_tail_" + menuid, P_NOERROR) && !bbs.menu("xtrnmenu_tail", P_NOERROR)) { + bbs.menu("xtrn_tail", P_NOERROR); + } + + bbs.node_sync(); + console.mnemonics(options.which); + } + + validkeys.push('q'); + keyin = console.getkeys(validkeys, keymax, K_NONE); + keyin = keyin.toString().toLowerCase(); + + if (keyin) { + // q for quit + if (keyin == "q") { + console.clear(); + break; + } + + menuitemsfiltered.some(function (menuitemfiltered) { + var menutarget = menuitemfiltered.target.toLowerCase(); + var menuinput = menuitemfiltered.input.toString().toLowerCase(); + + if (menuinput == keyin) { + switch (menuitemfiltered.type) { + // custom menu, defined in xtrnmenu.json + case 'custommenu': + external_section_menu_custom(menutarget); + return true; + // external program section + case 'xtrnmenu': + js.exec("xtrn_sec.js", {}, menutarget); + //js.exec(js.exec_dir + 'xtrn_sec.js ' + menutarget) + return true; + // external program + case 'xtrnprog': + // run the external program + if (typeof xtrn_area.prog[menutarget] !== "undefined") { + bbs.exec_xtrn(menutarget); + return true; + } else { + doerror(options.custom_menu_program_not_found_msg.replace('%PROGRAMID%', menutarget)); + } + break; + } //switch + } // if menu item matched keyin + }); // foreach menu item + } // if keyin + } // main bbs.online loop +} + + +// Display error message to console and save to log +function doerror(msg) +{ + console.crlf(); + log(LOG_ERR, msg); + console.putmsg('\x01+\x01h\x01r' +msg + ". The sysop has been notified." + '\x01-\r\n'); +} + diff --git a/exec/xtrnmenucfg.js b/exec/xtrnmenucfg.js new file mode 100644 index 0000000000000000000000000000000000000000..9329b925fd5ff107615e799c6f74fad5a19a3b13 --- /dev/null +++ b/exec/xtrnmenucfg.js @@ -0,0 +1,826 @@ +"use strict" + +/** + * Menu editor for Custom External Program Menus + * by Michael Long mlong innerrealmbbs.us + * + * This edits the file xtrnmenu.cfg + */ + +load("sbbsdefs.js"); +load("uifcdefs.js"); + +var log = function(msg) { + var f = new File("uifc.log"); + f.open("a"); + f.writeln(system.timestr() + ": " + msg); + f.close(); +} + +/** + * Write the menus out to a file + */ +var saveMenus = function() { + + var filename = system.ctrl_dir + "xtrnmenu.cfg"; + + file_backup(filename, 5); + + var config_file = new File(filename); + if (!config_file.open('w+')) { + uifc.msg("ERROR: Could not write to " + filename); + return; + } + config_file.write(JSON.stringify(menuconfig, null, ' ')); + config_file.close(); + + uifc.msg("Config Saved"); +} + +/** + * Edit a custom menu + * @param menuid + */ +var editMenu = function(menuid) { + var menuindex, menu, menuindex, selection, selection2, editproperty; + var last = 0; + var selections = [], displayoptions = [], displayoptionids = []; + var menusize = menuconfig.menus.length; + + // new menu but no code given, make one + if ((typeof menuid === "undefined") || (!menuid)) { + menuid = time(); + } + + // look for existing menu + for (var i in menuconfig.menus) { + if (menuconfig.menus[i].id == menuid) { + menu = menuconfig.menus[i]; + menuindex = i; + } + } + + if (typeof menu === "undefined") { + menuindex = menusize; + menuconfig.menus[menuindex] = { + 'id': menuid, + 'title': "New Generated Menu " + menuid, + "sort_type": "title", + 'items': [] + }; + menu = menuconfig.menus[menuindex]; + } + + while(1) { + uifc.help_text = word_wrap("This screen allows you to edit the configuration options for the custom menu.\r\n\r\nMost options default or are set in modopts.ini, but here you can define them on a per-menu basis.\r\n\r\nClick Edit Items to edit the individual entries (programs, menus, etc.)"); + + selections = []; + for (var j in menu.items) { + selections.push(menu.items.index); + } + + displayoptions = []; + displayoptionids = []; + + // setup display menu + displayoptions.push(format("%23s: %s", "id", + ("id" in menu ? menu.id : time()))); + displayoptionids.push("id"); + + displayoptions.push(format("%23s: %s", "title", + ("title" in menu ? menu.title : ""))); + displayoptionids.push("title"); + + displayoptions.push(format("%23s: %s", "sort_type", + ("sort_type" in menu ? menu.sort_type : "(default)"))); + displayoptionids.push("sort_type"); + + displayoptions.push(format("%23s: %s", "Edit Items", "[...]")); + displayoptionids.push("items"); + + selection = uifc.list(WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC, 0, 0, 0, last, last, + menu.title + ": Options", displayoptions); + + if (selection < 0) { + // escape key + break; + } + + editproperty = displayoptionids[selection]; + + switch (editproperty) { + case 'id': + uifc.help_text = word_wrap("This is a unique ID for the menu, which can be used as the target to\r\ncall the menu from other menus.\r\n\r\nFor the top-level menu, the id should be 'main'."); + var selection2 = uifc.input(WIN_MID, "Menu ID", menu.id, 50, K_EDIT); + if ((selection2 < 0) || (selection2 == null)) { + // escape key + break; + } + menu.id = selection2; + break; + case 'title': + uifc.help_text = word_wrap("Title for the menu, to be shown at the top of the menu"); + selection2 = uifc.input(WIN_MID, "Menu Title", menu.title, 255, K_EDIT); + if ((selection2 < 0) || (selection2 == null)) { + // escape key + break; + } + menu.title = selection2; + break; + + case 'sort_type': + uifc.help_text = word_wrap("How to sort the menu:\r\nby input key\r\nby title\r\nnone (the items remain in the order they are in the config)"); + + switch (uifc.list(WIN_ORG | WIN_MID, "Sort Type", ["key", "title", "none"])) { + case 0: + menu.sort_type = "key"; + break; + case 1: + menu.sort_type = "title"; + break; + case 2: + delete menu.sort_type; + break; + default: + //esc key + break; + } + + case 'items': + editItems(menuid); + break; + + default: + // this isn't supposed to happen + uifc.msg("Unknown option"); + break; + } + + last = Math.max(selection, 0); + } +} + +/** + * Edit menu items in a menu + * @param menuid + */ +var editItems = function(menuid) { + var menuindex, menu, selection, selection2, keyused, items = [], itemids = []; + var i, last = 0; + + // cur bar top left width + var ctxm = new uifc.list.CTX(0, 0, 0, 0, 0); + + if (typeof menuid === "undefined") { + uifc.msg("Menu could not be found"); + return; + } else { + for (i in menuconfig.menus) { + if (menuconfig.menus[i].id == menuid) { + menu = menuconfig.menus[i]; + menuindex = i; + } + } + } + + if ((typeof menu.items == "undefined") || (menu.items.length == 0)) { + // no items, prompt them to make one + editItem(menu.id, 0); + } + + + uifc.help_text = word_wrap("This menu allows editing the various items in this menu.\r\n\r\n" + + "If you leave input key blank, it will use an auto-generated number at display time.\r\n\r\n" + + "Choose a type first and the dropdown to choose tha target will allow you to select your target.\r\n\r\n" + + "Access string only applies to custom menu items. For external sections or external programs, use the access settings in scfg.\r\n\r\n"); + + while(1) { + items = []; + itemids = []; + for(i in menu.items) { + items.push(format( + "%6s %10s %s", + menu.items[i].input ? menu.items[i].input : '(auto)', + menu.items[i].type, + menu.items[i].title + )); + itemids.push(i); + } + // WIN_ORG = original menu + // WIN_MID = centered mid + // WIN_ACT = menu remains active after a selection + // WIN_ESC = screen is active when escape is pressed + // WIN_XTR = blank line to insert + // WIN_INS = insert key + // WIN_DEL = delete + // WIN_CUT = cut ctrl-x + // WIN_COPY = copy ctrl-c + // WIN_PUT = paste ctrl-v + // WIN_SAV = use context/save position + selection = uifc.list( + WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC|WIN_XTR|WIN_INS|WIN_DEL|WIN_CUT|WIN_COPY|WIN_PASTE|WIN_SAV, + menu.title + ": Items", + items, + ctxm + ); + + if (selection == -1) { + // esc key + break; + } + + if ((selection & MSK_ON) == MSK_DEL) { + // delete item + selection &= MSK_OFF; + //renumber array so there are no gaps + var menuitems2 = []; + for (var i in menu.items) { + if (i != itemids[selection]) { + menuitems2.push(menu.items[i]); + } + } + menu.items = menuitems2; + } else if (((selection & MSK_ON) == MSK_INS)) { + // new item from INSERT KEY + editItem(menuid, null); + } else if ((selection & MSK_ON) == MSK_COPY) { + // copy item + selection &= MSK_OFF; + copyitem = JSON.parse(JSON.stringify(menu.items[itemids[selection]])); // make copy + } else if ((selection & MSK_ON) == MSK_CUT) { + // cut item + selection &= MSK_OFF; + copyitem = menu.items[itemids[selection]]; + + //renumber array so there are no gaps + var menuitems2 = []; + for (var i in menu.items) { + if (i != itemids[selection]) { + menuitems2.push(menu.items[i]); + } + } + menu.items = menuitems2; + } else if ((selection & MSK_ON) == MSK_PASTE) { + // paste item + selection &= MSK_OFF; + + var oktopaste = true; + + // only paste if there is an item copied + if ("type" in copyitem) { + // if item already exists in list, modify if since you can't have dupes (except empty input keys) + for (var i in menu.items) { + if ((menu.items[i].input == copyitem.input) && (copyitem.input !== null) && (copyitem.input !== "")) { + oktopaste = true; + while(1) { + selection2 = uifc.input(WIN_MID, "Enter New Input Key", "", 3, K_EDIT); + if ((selection2 == -1) || (selection2 === "") || (selection2 === null)) { + // escape key + copyitem.input = null; + break; + } + if (selection2 || selection2 === 0) { + selection2 = selection2.toUpperCase(); + keyused = false; + for (var j in menu.items) { + if (menu.items[j].input && (menu.items[j].input.toUpperCase() == selection2)) { + keyused = true; + } + } + if (keyused) { + uifc.msg("This input key is alread used for another item"); + oktopaste = false; + } else { + copyitem.input = selection2; + oktopaste = true; + break; + } + } + } + copyitem.input = selection2; + } + } + if ((oktopaste) || (copyitem.input === "null") || (copyitem.input === "")) { + var menuitems2 = []; + for (i in menu.items) { + menuitems2.push(menu.items[i]); + // paste copied item after selected item + if (i == itemids[selection]) { + menuitems2.push(copyitem); + ctxm.cur = i-1; + } + } + menu.items = menuitems2; + } + } + } else if (selection >= menu.items.length) { + // new item from blank line + editItem(menuid, null); + } else { + editItem(menuid, itemids[selection]); + } + last = Math.max(selection, 0); + } +} + +/** + * Edit a specific menu item entry + * @param menuid + * @param itemindex + */ +var editItem = function(menuid, itemindex) { + var menu, menuindex, item; + var keyused, selection, selection2, i, last = 0; + var displayoptions = [], displayoptionids = [], newitems = []; + // used for building target selection + var custommenuitems = [], custommenuitemsids = [], custommenunames = []; + + if (typeof menuid === "undefined") { + uifc.msg("Menu could not be found"); + return; + } else { + for (i in menuconfig.menus) { + if (menuconfig.menus[i].id == menuid) { + menu = menuconfig.menus[i]; + menuindex = i; + } + } + } + + if (typeof menu.items[itemindex] === "undefined") { + // new item + menu.items.push({ + "input": null, + "title": "New Item " + time(), + "type": null, + "target": null, + "access_string": null, + }); + itemindex = menu.items.length - 1; + present_select_targettype(menu.items[itemindex]); + } + item = menu.items[itemindex]; + + var itemctx = new uifc.list.CTX(0,0,0,0,0); + while(1) { + displayoptions = []; + displayoptionids = []; + + // setup display menu + displayoptions.push(format("%23s: %s", "input", + (("input" in item) && (item.input !== null) && (item.input !== "") ? item.input : "(auto)"))); + displayoptionids.push("input"); + + displayoptions.push(format("%23s: %s", "title", + ("title" in item ? item.title : ""))); + displayoptionids.push("title"); + + displayoptions.push(format("%23s: %s", "type", + ("type" in item ? item.type : ""))); + displayoptionids.push("type"); + + displayoptions.push(format("%23s: %s", "target", + ("target" in item ? item.target : ""))); + displayoptionids.push("target"); + + if (item.type == "custommenu") { + displayoptions.push(format("%23s: %s", "access_string", + ("access_string" in item ? item.access_string : "(default)"))); + displayoptionids.push("access_string"); + } + + selection = uifc.list(WIN_ORG | WIN_MID | WIN_ACT | WIN_ESC, + menu.title + ": Item " + itemindex, displayoptions, itemctx); + + if (selection < 0) { + if (!item.title || !item.type || !item.target) { + if (uifc.list(WIN_ORG | WIN_MID, "This item is missing required items.", ["Remove Item", "Edit Item"]) == 0) { + // delete item and continue + newitems = []; + for (i in menu.items) { + if (i != itemindex) { + newitems.push(menu.items[i]); + } + } + menu.items = newitems; + break; + } + } else { + // leave menu + break; + } + } + + switch (displayoptionids[selection]) { + + case 'input': + uifc.help_text = word_wrap("The input key to access this item. Can be anything except Q. Leave blank to auto-generate a number."); + selection2 = uifc.input(WIN_MID, "Input Key", item.input, 3, K_EDIT); + if ((selection2 < 0) || (selection2 == null)) { + // escape key + break; + } + + if (selection2 !== "") { + selection2 = selection2.toUpperCase(); + + keyused = false; + for (i in menu.items) { + if ((menu.items[i].input === null) || (menu.items[i].input === "")) { + // continue here as toUpperCase would break it, and they don't need to match + // anyway because you can have multiple auto-assigned input items + continue; + } + if ((menu.items[i].input.toUpperCase() == selection2) && (i != itemindex)) { + keyused = true; + } + } + + if (keyused) { + uifc.msg("This input key is already used by another item."); + } else { + item.input = selection2; + } + } else { + // save blank + item.input = selection2; + } + break; + + case 'title': + uifc.help_text = word_wrap("The menu item title."); + selection2 = uifc.input(WIN_MID, "Title", item.title, 255, K_EDIT); + if ((selection2 < 0) || (selection2 == null)) { + // escape key + break; + } + if (!selection2 && selection2 !== 0) { + uifc.msg("Title is required."); + } else { + item.title = selection2; + } + break; + + case 'type': + present_select_targettype(item); + break; + + case 'target': + present_select_target(item); + break; + + case 'access_string': + uifc.help_text = word_wrap("The access string for the custom menu.\r\n\r\nOnly applies to custom menu items.\r\n\r\nExample: LEVEL 60"); + selection2 = uifc.input(WIN_MID, "Access String", item.access_string, 255, K_EDIT); + if ((selection2 < 0) || (selection2 == null)) { + // escape key + break; + } + item.access_string = selection2; + break; + + } + last = Math.max(selection, 0); + } +} + +function present_select_targettype(item) +{ + uifc.help_text = word_wrap( + "This is the type of target this item points to.\r\n\r\n" + + "custommenu is a custom menu defined in this tool.\r\n\r\n" + + "xtrnmenu is a standard Syncrhonet External Section Menu (refer to the scfg tool).\r\n\r\n" + + "xtrnprog is a direct link to an external program (refer to the scfg tool)"); + + var targetypectx = uifc.list.CTX(0, 0, 0, 0, 0); + if (typeof item.type !== "undefined") { + switch (item.type) { + case 'custommenu': + targetypectx.cur = 0; + targetypectx.bar = 0; + break; + case 'xtrnmenu': + targetypectx.cur = 1; + targetypectx.bar = 1; + break; + case 'xtrnprog': + targetypectx.cur = 2; + targetypectx.bar = 2; + break; + } + } + switch (uifc.list(WIN_ORG | WIN_MID | WIN_SAV, + "Target Type", ["custommenu", "xtrnmenu", "xtrnprog"], targetypectx)) { + case 0: + item.type = "custommenu"; + break; + case 1: + item.type = "xtrnmenu" + break; + case 2: + item.type = "xtrnprog"; + break; + default: + // includes escape key + break; + } + + // convienence... enter target selection + present_select_target(item) +} + +function present_select_target(item) +{ + uifc.help_text = word_wrap("This is the ID of the custom menu, external program section, or external program to link to."); + + var targetctx = uifc.list.CTX(0, 0, 0, 0, 0); + + var custommenuitems = []; + var custommenuitemsids = []; + var custommenunames = []; + + var selection2; + + switch (item.type) { + case "custommenu": + // present list of custom menus + for (i in menuconfig.menus) { + custommenuitems.push(format("%23s: %s", menuconfig.menus[i].id, menuconfig.menus[i].title)); + custommenuitemsids.push(menuconfig.menus[i].id); + custommenunames.push(menuconfig.menus[i].title); + } + + if ((typeof item.target !== "undefined") && item.target) { + for (var p in custommenuitemsids) { + if (custommenuitemsids[p] == item.target) { + targetctx.cur = p; + targetctx.bar = p; + } + } + } + + selection2 = uifc.list(WIN_ORG | WIN_MID | WIN_SAV, "Target", custommenuitems, targetctx); + if ((selection2 < 0) || (selection2 == null)) { + // escape key + break; + } + + item.target = custommenuitemsids[selection2]; + + while(1) { + if (uifc.list(WIN_ORG | WIN_MID, "Replace item title with sections's name?", ["Yes", "No"]) == 0) { + item.title = custommenunames[selection2]; // for external program, change title to program name + } + break; + } + break; + + case "xtrnmenu": + // present list of external program sections + var seclist = []; + for (i in xtrn_area.sec_list) { + seclist.push({ code: xtrn_area.sec_list[i].code, name: xtrn_area.sec_list[i].name}); + }; + seclist.sort(sort_by_code); + + for (i in seclist) { + custommenuitems.push(format("%23s: %s", seclist[i].code, seclist[i].name)); + custommenuitemsids.push(seclist[i].code); + custommenunames.push(seclist[i].name); + } + + if ((typeof item.target !== "undefined") && item.target) { + for (var p in custommenuitemsids) { + if (custommenuitemsids[p].toLowerCase() == item.target.toLowerCase()) { + targetctx.cur = p; + targetctx.bar = p; + } + } + } + + selection2 = uifc.list(WIN_ORG | WIN_MID | WIN_SAV, "Target", custommenuitems, targetctx); + if ((selection2 < 0) || (selection2 == null)) { + // escape key + break; + } + + item.target = custommenuitemsids[selection2]; + + while(1) { + if (uifc.list(WIN_ORG | WIN_MID, "Replace item title with sections's name?", ["Yes", "No"]) == 0) { + item.title = custommenunames[selection2]; // for external program, change title to program name + } + break; + } + break; + + case "xtrnprog": + + // present list of external programs + // create sorted list + var proglist = []; + + for (i in xtrn_area.prog) { + proglist.push({ code: xtrn_area.prog[i].code, name: xtrn_area.prog[i].name}); + }; + proglist.sort(sort_by_code); + for (i in proglist) { + custommenuitems.push(format("%23s: %s", proglist[i].code, proglist[i].name)); + custommenuitemsids.push(proglist[i].code); + custommenunames.push(proglist[i].name); + } + + if ((typeof item.target !== "undefined") && item.target) { + for (var p in custommenuitemsids) { + if (custommenuitemsids[p].toLowerCase() == item.target.toLowerCase()) { + targetctx.cur = p; + targetctx.bar = p; + } + } + } + + selection2 = uifc.list(WIN_ORG | WIN_MID | WIN_SAV, "Target", custommenuitems, targetctx); + if ((selection2 < 0) || (selection2 == null)) { + // escape key + break; + } + if (selection2 || selection2 === 0) { + item.target = custommenuitemsids[selection2]; + while(1) { + if (uifc.list(WIN_ORG | WIN_MID, "Replace item title with sections's name?", ["Yes", "No"]) == 0) { + item.title = custommenunames[selection2]; // for external program, change title to program name + } + break; + } + } + break; + + default: + selection2 = uifc.input(WIN_ORG | WIN_MID, "Target", item.target, 50, K_EDIT); + if ((selection2 < 0) || (selection2 == null)) { + // escape key + break; + } + + item.target = selection2; + break; + } +} + +function sort_by_name(a, b) +{ + if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + return 0; +} + +function sort_by_code(a, b) +{ + if (a.code.toLowerCase() > b.code.toLowerCase()) return 1; + if (a.code.toLowerCase() < b.code.toLowerCase()) return -1; + return 0; +} + +function sort_by_id(a, b) +{ + if (a.id.toLowerCase() > b.id.toLowerCase()) return 1; + if (a.id.toLowerCase() < b.id.toLowerCase()) return -1; + return 0; +} + + +// MAIN +try { + var menuconfig = {}; + var copyitem = {}; // for menu item copy/paste + var config_file = new File(file_cfgname(system.ctrl_dir, "xtrnmenu.cfg")); + if (config_file.open('r+')) { + var config_src = config_file.read(); + try { + menuconfig = JSON.parse(config_src.toString()); + if (!menuconfig) { + writeln("ERROR! Could not parse xtrnmenu.cfg. JSON may be invalid."); + exit(); + } + } catch (e) { + writeln("ERROR! Could not parse xtrnmenu.cfg. JSON may be invalid. " + e.toString()); + exit(); + } + } + config_file.close(); + + if (typeof menuconfig.menus === "undefined") { + menuconfig.menus = []; + } + + uifc.init("Enhanced External Program Menus Configurator"); + uifc.lightbar_color = 120; + uifc.background_color = 21; + uifc.frame_color = 15; + js.on_exit("if (uifc.initialized) uifc.bail()"); + + // cur bar top left width + var ctx = new uifc.list.CTX(0, 0, 0, 0, 0); + + while(1) { + uifc.help_text = word_wrap("This program allows managing the Enhanced External Program Menu feature."); + + // no menus or no main menu + var mainmenufound = false; + for (var m in menuconfig.menus) { + if (menuconfig.menus[m].id.toLowerCase() == "main") { + mainmenufound = true; + } + } + if (!mainmenufound || (menuconfig.menus.length < 1)) { + uifc.msg("No menus defined and/or missing the main menu. Setting up now."); + editMenu("main"); + } + + // menus is array of menuconfig menu ids + var menus = []; + var menuTitles = []; + menuconfig.menus.sort(sort_by_id); + for (var m in menuconfig.menus) { + menus.push(menuconfig.menus[m].id); + menuTitles.push(format("%20s: %s", menuconfig.menus[m].id, menuconfig.menus[m].title)); + } + + menuTitles.push(format("%20s %s", '', "[Save Config Without Exit]")); + + // WIN_ORG = original menu, destroy valid screen area + // WIN_MID = place window in middle of screen + // WIN_XTR = add extra line at end for inserting at end + // WIN_DEL = allow user to use delete key + // WIN_ACT = menu remains active after a selection + // WIN_ESC = screen is active when escape is hit + // WIN_INS = allow user to use insert key + var selection = uifc.list( + WIN_ORG|WIN_MID|WIN_XTR|WIN_DEL|WIN_ACT|WIN_ESC|WIN_INS|WIN_SAV, + "Enhanced External Menus", + menuTitles, + ctx + ); + + if (selection == (menuTitles.length - 1)) { + // last item - save config + saveMenus(); + uifc.pop("Config saved."); + } else if (selection < 0) { + while (1) { + var ret = uifc.list(WIN_ORG | WIN_MID, "Save Changes Before Exit?", ["Yes", "No", "Cancel"]); + if (ret == 0) { + saveMenus(); + exit(); + } else if (ret == 1) { + // no - exit + exit(); + } else { + // cancel + break; + } + } + + } else if ((selection & MSK_ON) == MSK_DEL) { + selection &= MSK_OFF; + for (var m in menuconfig.menus) { + if (menuconfig.menus[m].id == menus[selection]) { + delete menuconfig.menus[m]; + } + } + //selection--; + } else if (((selection & MSK_ON) == MSK_INS) || (selection >= menuconfig.menus.length)) { + // new menu + var newid = uifc.input( + WIN_MID, + "Enter a short unique id for the menu", + "", + 0 + ); + if (typeof newid !== "undefined") { + var menufound = false; + for (var mf in menuconfig.menus) { + if (menuconfig.menus[mf].id == newid) { + menufound = true; + } + } + if (menufound) { + uifc.msg("That ID is already in use. Please choose another."); + } else { + editMenu(newid); + } + } + } else { + editMenu(menuconfig.menus[selection].id); + } + } +} catch(err) { + if ((typeof uifc !== "undefined") && uifc.initialized) { + uifc.bail(); + } + writeln(err); + log(err); + if (typeof console !== "undefined") { + console.pause(); + } +} diff --git a/webv4/pages/099-xtrnmenu-games.xjs b/webv4/pages/099-xtrnmenu-games.xjs new file mode 100644 index 0000000000000000000000000000000000000000..c7106969f157d3872695bbe5d7e9b140abcde545 --- /dev/null +++ b/webv4/pages/099-xtrnmenu-games.xjs @@ -0,0 +1,149 @@ +<!--HIDDEN:Games--> +<?xjs +/** + * Web Display for Custom External Program Menus + * by Michael Long mlong innerrealmbbs.us + * + * See wiki at http://wiki.synchro.net/module:xtrnmenumod + */ + + load("ftelnethelper.js"); + load('sbbsdefs.js'); + load("xtrnmenulib.js"); + load(settings.web_lib + 'ftelnet.js'); + load(settings.web_lib + 'request.js'); + + var ExternalMenus = new ExternalMenus(); + + if (typeof settings.xtrn_blacklist === 'string') { + settings.xtrn_blacklist = settings.xtrn_blacklist.toLowerCase().split(','); + } else { + settings.xtrn_blacklist = []; + } + + var menuitems = []; +?> + +<style>.fTelnetStatusBar { display : none; }</style> + +<a name="fTelnet"></a> +<div id="fTelnetContainer" class="fTelnetContainer" style="margin-bottom:1em;"></div> + +<div id="xtrn-list" class="list-group"> + +<?xjs + var menuobj; + if ((Request.get_param('type') == 'xtrnmenu') && Request.has_param('target')) { + menuobj = ExternalMenus.getSectionMenu(Request.get_param('target')); + } else { + if (Request.has_param('target')) { + menuobj = ExternalMenus.getMenu(Request.get_param('target')); + } else { + menuobj = ExternalMenus.getMenu('main'); + } + } + + if ((typeof menuobj === "undefined") || !menuobj + || (typeof menuobj.items === "undefined") || (menuobj.items.length < 1)) { + writeln("<h4>" + ExternalMenus.options.no_programs_msg + "</h4>"); + } else if (user.security.restrictions&UFLAG_X) { + writeln("<h4>" + options.restricted_user_msg + "</h4>"); + } else { + // ok to display menu + + writeln("<h4>" + menuobj.title + "</h4>"); + var menuitemsfiltered = ExternalMenus.getSortedItems(menuobj); + menuitemsfiltered.forEach(function (menuitem) { + if (settings.xtrn_blacklist.indexOf(menuitem.target.toLowerCase()) > -1) { + return; + } + menuitems.push({ + 'itemtitle': menuitem.title, + 'itemtype': menuitem.type, + 'itemtarget': menuitem.target + }); + }); + } +?> +</div> + +<script id="fTelnetScript" src="<?xjs write(get_url()); ?>"></script> +<script type="text/javascript"> + var wsp = <?xjs write(settings.wsp || GetWebSocketServicePort()); ?>; + var wssp = <?xjs write(settings.wssp || GetWebSocketServicePort(true)); ?>; + var Options = new fTelnetOptions(); + Options.BareLFtoCRLF = false; + Options.BitsPerSecond = 57600; + Options.ConnectionType = 'rlogin'; + Options.Emulation = 'ansi-bbs'; + Options.Enter = '\r'; + Options.Font = 'CP437'; + Options.ForceWss = false; + Options.Hostname = '<?xjs write(http_request.vhost); ?>'; + Options.LocalEcho = false; + Options.Port = location.protocol == 'https:' ? wssp : wsp; + Options.RLoginClientUsername = '<?xjs write(user.security.password); ?>'; + Options.RLoginServerUsername = '<?xjs write(user.alias); ?>'; + Options.ScreenColumns = 80; + Options.ScreenRows = 25; + Options.SplashScreen = Options.SplashScreen = '<?xjs write(get_splash()); ?>'; + Options.WebSocketUrlPath = '?Port=<?xjs write(GetRLoginPort()); ?>'; + var fTelnet = new fTelnetClient('fTelnetContainer', Options); + fTelnet.OnConnectionClose = function () { + window.location.reload(); + }; + + async function launchXtrn() { + var code = event.srcElement.id; + await v4_get('./api/system.ssjs?call=set-xtrn-intent&code=' + code); + fTelnet._Options.RLoginTerminalType = 'xtrn=' + code; + fTelnet.Connect(); + } + + var menuitems = <?xjs write(JSON.stringify(menuitems)); ?>; + var currentTarget = "<?xjs Request.write_param('target'); ?>"; + var currentType = "<?xjs Request.write_param('type'); ?>"; + var currentTitle = `<?xjs write(menuobj.title) ?>`; + if (currentTitle && !currentTarget) { + // main menu - store title for breadcrumb + sessionStorage.setItem("mainmenu", currentTitle); + } + var div = $('#xtrn-list'); + menuitems.forEach(function (menuitem) { + var a = document.createElement('a'); + $(a).addClass("list-group-item"); + $(a).addClass("striped"); + a.text = menuitem.itemtitle; + + if (menuitem.itemtype == "xtrnprog") { + a.href = "#fTelnet"; + a.id = menuitem.itemtarget; + a.onclick = function () { launchXtrn(); }; + } else { + a.href = "/?page=<?xjs Request.write_param('page') ?>&type=" +menuitem.itemtype + + "&target=" + menuitem.itemtarget; + a.onclick = function () { + sessionStorage.setItem('prev:' + menuitem.itemtarget.toLowerCase(), '<?xjs Request.write_param('target')?>'); + sessionStorage.setItem('prevtype:' + menuitem.itemtarget.toLowerCase(), '<?xjs Request.write_param('type')?>'); + sessionStorage.setItem('prevtitle:' + menuitem.itemtarget.toLowerCase(), currentTitle); + }; + } + $(div).append(a); + }); + + // breadcrumb + var prevTarget = sessionStorage.getItem('prev:' + currentTarget.toLowerCase()); + var prevType = sessionStorage.getItem('prevtype:' + currentTarget.toLowerCase()); + var prevTitle = sessionStorage.getItem('prevtitle:' + currentTarget.toLowerCase()); + if (prevType && prevTarget && prevTitle) { + $('#xtrn-list').prepend('<ol class="breadcrumb"><a href="/?page=<?xjs Request.write_param('page') ?>&type=' + prevType + + '&target=' + prevTarget + '">' + prevTitle + '</a></ol>'); + } else if (currentTarget) { + // level 2, not main menu + var mainmenuTitle = sessionStorage.getItem('mainmenu'); + if (!mainmenuTitle) { + mainmenuTitle = 'Games'; + } + $('#xtrn-list').prepend('<ol class="breadcrumb"><a href="/?page=<?xjs Request.write_param('page') ?>">' + mainmenuTitle + '</a></ol>'); + } +</script>