diff --git a/xtrn/DDAreaChoosers/DDFileAreaChooser.js b/xtrn/DDAreaChoosers/DDFileAreaChooser.js index 470dff54c38efc954ea3930370db70ce2b05c905..5ea5fdedb903167446695a18bb7c7eb069ed3f19 100644 --- a/xtrn/DDAreaChoosers/DDFileAreaChooser.js +++ b/xtrn/DDAreaChoosers/DDFileAreaChooser.js @@ -68,6 +68,12 @@ * the file library description. * 2024-03-13 Eric Oulashin Version 1.41 * Fix for the directory item counts + * 2024-11-08 Eric Oulashin Version 1.42 Beta + * Started working on a change to directory collapsing to allow an + * arbitrary amount of separators in the library/directory names to + * create multiple levels of categories + * 2025-03-17 Eric Oulashin Version 1.42 + * Releasing this version */ // TODO: Failing silently when 1st argument is true @@ -108,8 +114,8 @@ if (system.version_num < 31400) } // Version & date variables -var DD_FILE_AREA_CHOOSER_VERSION = "1.41"; -var DD_FILE_AREA_CHOOSER_VER_DATE = "2024-03-13"; +var DD_FILE_AREA_CHOOSER_VERSION = "1.42"; +var DD_FILE_AREA_CHOOSER_VER_DATE = "2025-03-17"; // Keyboard input key codes var CTRL_H = "\x08"; @@ -118,23 +124,18 @@ var KEY_ENTER = CTRL_M; var BACKSPACE = CTRL_H; var CTRL_F = "\x06"; var KEY_ESC = ascii(27); -// PageUp & PageDown keys - Synchronet 3.17 as of about December 18, 2017 -// use CTRL-P and CTRL-N for PageUp and PageDown, respectively. sbbsdefs.js -// defines them as KEY_PAGEUP and KEY_PAGEDN; I've used slightly different names -// in this script so that this script will work with Synchronet systems before -// and after the update containing those key definitions. -var KEY_PAGE_UP = "\x10"; // Ctrl-P -var KEY_PAGE_DOWN = "\x0e"; // Ctrl-N -// Ensure KEY_PAGE_UP and KEY_PAGE_DOWN are set to what's defined in sbbs.js -// for KEY_PAGEUP and KEY_PAGEDN in case they change -if (typeof(KEY_PAGEUP) === "string") - KEY_PAGE_UP = KEY_PAGEUP; -if (typeof(KEY_PAGEDN) === "string") - KEY_PAGE_DOWN = KEY_PAGEDN; - -// Key codes for display + +// Characters for display var UP_ARROW = ascii(24); var DOWN_ARROW = ascii(25); +var BLOCK1 = "\xB0"; // Dimmest block +var BLOCK2 = "\xB1"; +var BLOCK3 = "\xB2"; +var BLOCK4 = "\xDB"; // Brightest block +var MID_BLOCK = ascii(254); +var TALL_UPPER_MID_BLOCK = "\xFE"; +var UPPER_CENTER_BLOCK = "\xDF"; +var LOWER_CENTER_BLOCK = "\xDC"; // Characters for display var HORIZONTAL_SINGLE = "\xC4"; @@ -221,46 +222,37 @@ function DDFileAreaChooser() this.useDirCollapsing = true; // The separator character to use for directory collapsing this.dirCollapseSeparator = ":"; - // If useDirCollapsing is true, then lib_list will be populated - // with some information from Synchronet's file_area.lib_list, - // including a sub_list for each library. The sub_list arrays - // could have one that was collapsed from multiple directorys - // set up in the BBS - The sub_list within that one would then - // contain multiple directories split based on the dir collapse - // separator. - this.lib_list = []; // Set the functions for the object this.ReadConfigFile = DDFileAreaChooser_ReadConfigFile; this.SelectFileArea = DDFileAreaChooser_SelectFileArea; - this.SelectFileArea_Traditional = DDFileAreaChooser_SelectFileArea_Traditional; - this.SelectDirWithinFileLib_Traditional = DDFileAreaChooser_SelectDirWithinFileLib_Traditional; - this.SelectSubdirWithinDir_Traditional = DDFileAreaChooser_SelectSubdirWithinDir_Traditional; - this.GetActualLibIdx = DDFileAreaChooser_GetActualLibIdx; - this.ListFileLibs_Traditional = DDFileAreaChooser_ListFileLibs_Traditional; - this.ListDirsInFileLib_Traditional = DDFileAreaChooser_ListDirsInFileLib_Traditional; - this.ListSubdirsInFileDir_Traditional = DDFileAreaChooser_ListSubdirsInFileDir_Traditional; this.WriteLibListHdrLine = DDFileAreaChooser_WriteLibListHdrLine; this.WriteDirListHdr1Line = DDFileAreaChooser_WriteDirListHdr1Line; // Lightbar-specific functions - this.SelectFileArea_Lightbar = DDFileAreaChooser_SelectFileArea_Lightbar; - this.CreateLightbarFileLibMenu = DDFileAreaChooser_CreateLightbarFileLibMenu; - this.CreateLightbarFileDirMenu = DDFileAreaChooser_CreateLightbarFileDirMenu; + this.CreateLightbarMenu = DDFileAreaChooser_CreateLightbarMenu; + this.GetColorIndexInfoForLightbarMenu = DDFileAreaChooser_GetColorIndexInfoForLightbarMenu; + this.DisplayMenuHdrWithNumItems = DDFileAreaChooser_DisplayMenuHdrWithNumItems; this.WriteKeyHelpLine = DDFileAreaChooser_writeKeyHelpLine; // Help screen - this.ShowHelpScreen = DDFileAreaChooser_showHelpScreen; + this.ShowHelpScreen = DDFileAreaChooser_ShowHelpScreen; // Misc. functions // Function to build the directory printf information for a file lib this.BuildFileDirPrintfInfoForLib = DDFileAreaChooser_buildFileDirPrintfInfoForLib; + this.GetPrintfStrForDirWithoutAreaNum = DDFileAreaChooser_GetPrintfStrForDirWithoutAreaNum; // Function to display the header above the area list this.DisplayAreaChgHdr = DDFileAreaChooser_DisplayAreaChgHdr; this.WriteLightbarKeyHelpErrorMsg = DDFileAreaChooser_WriteLightbarKeyHelpErrorMsg; - this.SetUpLibListWithCollapsedDirs = DDFileAreaChooser_SetUpLibListWithCollapsedDirs; - this.FindFileDirIdxFromText = DDFileAreaChooser_FindFileDirIdxFromText; + this.FindFileAreaIdxFromText = DDFileAreaChooser_FindFileAreaIdxFromText; this.GetGreatestNumFiles = DDFileAreaChooser_GetGreatestNumFiles; // Read the settings from the config file. this.ReadConfigFile(); + + // lib_list will be set up with a file library/directory structure for + // the chooser to use to let the user choose a file lib & directory. It + // will be set up with the same basic format regardless of whether + // directory collapsing is to be used or not. + this.lib_list = getFileDirHeirarchy(this.useDirCollapsing, this.dirCollapseSeparator); // printf strings used for outputting the file libraries this.fileLibPrintfStr = " " + this.colors.areaNum + "%" + this.areaNumLen + "d " @@ -352,6 +344,9 @@ function DDFileAreaChooser() // created on the fly the first time the user lists directories for // a file library. this.fileDirListPrintfInfo = []; + // Build printf information for each file library + for (var libIdx = 0; libIdx < file_area.lib_list.length; ++libIdx) + this.BuildFileDirPrintfInfoForLib(libIdx); // areaChangeHdrLines is an array of text lines to use as a header to display // above the message area changer lists. @@ -366,609 +361,425 @@ function DDFileAreaChooser() // current file library. This is optional; defaults to true. function DDFileAreaChooser_SelectFileArea(pChooseLib) { - // If file directory collapsing is enabled, then set up - // this.lib_list. - if (this.useDirCollapsing) - this.SetUpLibListWithCollapsedDirs(); - - if (this.useLightbarInterface && console.term_supports(USER_ANSI)) - this.SelectFileArea_Lightbar(pChooseLib ? 1 : 2); // TODO: Fix for levels everywhere? - else - this.SelectFileArea_Traditional(pChooseLib ? 1 : 2); // TODO: Fix for levels everywhere? -} - -// For the DDFileAreaChooser class: Traditional user interface for -// letting the user choose a file area -// -// Parameters: -// pLevel: The file heirarchy level: -// 1: File libraries -// 2: File directories within libraries -// 3: File subdirectories within directories (if directory name collapsing is enabled) -// This is optional and defaults to 1. -// pLibIdx: Optional - The file library index, if choosing a file directory -// pDirIdx: Optional - The file directory index (within a library), for use with -// directory name collapsing -function DDFileAreaChooser_SelectFileArea_Traditional(pLevel, pLibIdx, pDirIdx) -{ - // If there are no file libraries, then don't let the user - // choose one. - if (file_area.lib_list.length == 0) - { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no file libraries.\r\n\x01p"); - return; - } - - if (pLevel > 1 && typeof(pLibIdx) !== "number") - pLibIdx = this.GetActualLibIdx(); - - var curLibIdx = 0; - var curDirIdx = 0; - if (typeof(bbs.curdir_code) == "string") - { - curLibIdx = file_area.dir[bbs.curdir_code].lib_index; - curDirIdx = file_area.dir[bbs.curdir_code].index; - } + var chooseLib = (typeof(pChooseLib) === "boolean" ? pChooseLib : true); - var continueChooseFileLib = true; - if (pLevel == 1) // Choose library + // Start with this.lib_list, which is the topmost file lib/dir structure + var fileLibStructure = this.lib_list; + if (!chooseLib) { - // Show the file libraries & directories and let the user choose one. - var selectedLibNum = 0; // The user's selected file library - var selectedDirNum = 0; // The user's selected file directory - var libSearchText = ""; - while (continueChooseFileLib) + for (var i = 0; i < this.lib_list.length; ++i) { - // Clear the BBS command string to make sure there are no extra - // commands in there that could cause weird things to happen. - bbs.command_str = ""; - - console.clear("\x01n"); - this.DisplayAreaChgHdr(1); - if (this.areaChangeHdrLines.length > 0) - console.crlf(); - this.ListFileLibs_Traditional(libSearchText); - console.print("\x01n\x01b\x01h� \x01n\x01cWhich, \x01hQ\x01n\x01cuit, \x01hCTRL-F\x01n\x01c, \x01h/\x01n\x01c, or [\x01h" + +(curLibIdx+1) + "\x01n\x01c]:\x01h "); - // Accept Q (quit) or a file library number - selectedLibNum = console.getkeys("QN/" + CTRL_F, file_area.lib_list.length); - - // If the user just pressed enter (selectedLibNum would be blank), - // default to the current library. - if (selectedLibNum.toString() == "") - selectedLibNum = curLibIdx + 1; - - // If the user chose to quit, then set continueChooseFileLib to - // false so we'll exit the loop. Otherwise, let the user chose - // a dir within the library. - if (selectedLibNum.toString() == "Q") - continueChooseFileLib = false; - else if ((selectedLibNum.toString() == "/") || (selectedLibNum.toString() == CTRL_F)) + if (fileDirStructureHasCurrentUserFileDir(this.lib_list[i])) { - console.crlf(); - var searchPromptText = "\x01n\x01c\x01hSearch\x01g: \x01n"; - console.print(searchPromptText); - var searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE); - if (searchText.length > 0) - libSearchText = searchText; - } - else - { - continueChooseFileLib = this.SelectFileArea_Traditional(2, selectedLibNum-1); + if (this.lib_list[i].hasOwnProperty("items")) + fileLibStructure = this.lib_list[i].items; + break; } } } - else if (pLevel == 2) // Choose a directory within a library - { - // Don't choose a library, just a directory within the user's current library. - var selectDirRetObj = this.SelectDirWithinFileLib_Traditional(pLibIdx, curDirIdx); - continueChooseFileLib = !selectDirRetObj.areaSelected; - if (selectDirRetObj.areaSelected) - { - var selectedDirIdx = selectDirRetObj.dirIdx; - if (this.useDirCollapsing && (this.lib_list[pLibIdx].dir_list[selectedDirIdx].subdir_list.length > 0)) - continueChooseFileLib = this.SelectFileArea_Traditional(3, pLibIdx, selectedDirIdx); - else if (selectDirRetObj.dirCode.length > 0) - bbs.curdir_code = selectDirRetObj.dirCode; - else - continueChooseFileLib = true; - } - } - else if ((pLevel == 3) && this.useDirCollapsing) // Choose a subdirectory within a directory - { - if (this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list.length > 0) - { - var selectSubdirRetObj = this.SelectSubdirWithinDir_Traditional(pLibIdx, pDirIdx); - continueChooseFileLib = !selectSubdirRetObj.areaSelected; - if (selectSubdirRetObj.areaSelected) - bbs.curdir_code = selectSubdirRetObj.dirCode; - else - continueChooseFileLib = true; - } - } - return continueChooseFileLib; // For recursive calls to this function -} - -// For the DDFileAreaChooser class: Lets the user select a file area (directory) -// within a specified file library - Traditional user interface. -// -// Parameters: -// pLibIdx: The file library index -// pSelectedDirIdx: The currently-selected file directory index -// -// Return value: An object containing the following properties: -// areaSelected: Boolean - Whether or not the user chose a file area. -// dirIdx: The index of the directory chosen in the file library (or -1 if none chosen) -// dirCode: The internal code of the directory chosen, if valid. If not valid -// (i.e., there are subdirectories), this will be an empty string. -function DDFileAreaChooser_SelectDirWithinFileLib_Traditional(pLibIdx, pSelectedDirIdx) -{ - var retObj = { - areaSelected: false, - dirIdx: -1, - dirCode: "" - }; - - // If there are no file directories in the given file libraary, then show - // an error and return. - if (file_area.lib_list[pLibIdx].dir_list.length == 0) + var previousFileLibStructures = []; // Will be used like a stack + var selectedItemIndexes = []; // Will be used like a stack + var selectedItemIdx = null; + var chosenLibOrSubdirName = ""; // Will have the name of the user's chosen library/subdir + var previousChosenLibOrSubdirNames = []; // Will be used like a stack + var directoriesLabelLen = 15; // The length of the "Directories of " label + var nameSep = " - "; // A string to use to separate lib/subdirectory names for the top header line + var numItemsWidth = 0; // Width of the header column for # of items (not including the space), for creating the menu + if (!this.useLightbarInterface || !console.term_supports(USER_ANSI)) + numItemsWidth = 5; + // Main loop + var selectionLoopContinueOn = true; + while (selectionLoopContinueOn) { console.clear("\x01n"); - console.print("\x01y\x01hThere are no directories in this library.\r\n\x01p"); - return retObj; - } - // If pLibIdx is invalid, then just return false. - if (pLibIdx < 0) - return retObj; - else - { - if (this.useDirCollapsing && (this.lib_list.length > 0)) + // If we're displaying the file libraries (top level), then we'll output 1 + // header line; otherwise, we'll output 2 header line; adjut the top line + // of the menu accordingly. + var menuTopRow = 0; + if (fileLibStructure == this.lib_list || chosenLibOrSubdirName.length == 0) + menuTopRow = this.areaChangeHdrLines.length + 2; + else { - if (pLibIdx >= this.lib_list.length) - return retObj; + menuTopRow = this.areaChangeHdrLines.length + 3; + printf("\x01n%sDirectories of \x01h%s\x01n", this.colors.fileAreaHdr, chosenLibOrSubdirName.substr(0, console.screen_columns - directoriesLabelLen - 1)); + console.crlf(); } - else if (pLibIdx >= file_area.lib_list[pLibIdx].dir_list.length) - return retObj; - } - - // Ensure that the file directory printf information is created for - // this file library. - this.BuildFileDirPrintfInfoForLib(pLibIdx); - - // Set the default directory #: The current directory, or if the - // user chose a different file library, then this should be set - // to the first directory. - function getDefaultDirNum(pDir, pUseDirCollapsing, pLibList) - { - var defaultDirNum = 0; - if (pUseDirCollapsing && (pLibList.length > 0)) + var createMenuRet = this.CreateLightbarMenu(fileLibStructure, previousFileLibStructures.length+1, menuTopRow, selectedItemIdx, numItemsWidth); + //if (user.is_sysop) printf("\x01n# items: %d\r\n\x01p", createMenuRet.menuObj.NumItems()); // Temporary + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) + numItemsWidth = createMenuRet.itemNumWidth; + var menu = createMenuRet.menuObj; + // Write the header lines, & write the key help line at the bottom of the screen + var numItemsColLabel = createMenuRet.allDirs ? "Files" : "Items"; + //this.DisplayMenuHdrWithNumItems(createMenuRet.itemNumWidth, createMenuRet.descWidth-3, createMenuRet.numItemsWidth, numItemsColLabel); + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) + this.DisplayMenuHdrWithNumItems(numItemsWidth, createMenuRet.descWidth-3, createMenuRet.numItemsWidth, numItemsColLabel); + else + this.DisplayMenuHdrWithNumItems(numItemsWidth, createMenuRet.descWidth-2, createMenuRet.numItemsWidth, numItemsColLabel); // TODO + if (!this.useLightbarInterface || !console.term_supports(USER_ANSI)) + console.crlf(); // Not drawing the hotkey help line (this.WriteKeyHelpLine();) + + // Show the menu in a loop and get user input + var lastSearchText = ""; + var lastSearchFoundIdx = -1; + var drawMenu = true; + var writeHdrLines = false; // Already displayed above + var writeKeyHelpLine = true; + // Menu input loop + var menuContinueOn = true; + while (menuContinueOn) { - if (typeof(bbs.curdir_code) == "string") + // Draw the header lines and key help line if needed + if (writeHdrLines) + { + //this.DisplayMenuHdrWithNumItems(createMenuRet.itemNumWidth, createMenuRet.descWidth-3, createMenuRet.numItemsWidth, numItemsColLabel); + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) + this.DisplayMenuHdrWithNumItems(numItemsWidth, createMenuRet.descWidth-3, createMenuRet.numItemsWidth, numItemsColLabel); + else + this.DisplayMenuHdrWithNumItems(numItemsWidth, createMenuRet.descWidth-3, createMenuRet.numItemsWidth, numItemsColLabel); // TODO + if (!this.useLightbarInterface || !console.term_supports(USER_ANSI)) + console.crlf(); // Not drawing the hotkey help line (this.WriteKeyHelpLine();) + writeHdrLines = false; + } + if (writeKeyHelpLine && this.useLightbarInterface && console.term_supports(USER_ANSI)) + { + this.WriteKeyHelpLine(); + writeKeyHelpLine = false; + } + + // Show the menu and get user input + var selectedMenuIdx = menu.GetVal(drawMenu); + drawMenu = true; + var lastUserInputUpper = (typeof(menu.lastUserInput) == "string" ? menu.lastUserInput.toUpperCase() : ""); + // Applicable for ANSI/lightbar mode: If the user typed a number, the menu input loop will + // exit with the return value being null and the user's input in lastUserInput. Test to see + // if the user typed a number (it will be a single number), and if it's within the number of + // menu items, set selectedMenuIdx to the menu item index. + if (this.useLightbarInterface && console.term_supports(USER_ANSI) && lastUserInputUpper.match(/[0-9]/)) { - for (var dirIdx = 0; dirIdx < pLibList[pLibIdx].dir_list.length; ++dirIdx) + var userInputNum = parseInt(lastUserInputUpper); + if (!isNaN(userInputNum) && userInputNum >= 1 && userInputNum <= menu.NumItems()) { - if (pLibList[pLibIdx].dir_list[dirIdx].subdir_list.length > 0) + // Put the user's input back in the input buffer to + // be used for getting the rest of the message number. + console.ungetstr(lastUserInputUpper); + // Go to the last row on the screen and prompt for the full item number + console.gotoxy(1, console.screen_rows); + console.clearline("\x01n"); + console.gotoxy(1, console.screen_rows); + var itemPromptWord = createMenuRet.allDirs ? "item" : "directory"; + printf("\x01cChoose %s #: \x01h", itemPromptWord); + var userInput = console.getnum(menu.NumItems()); + if (userInput > 0) { - for (var subdirIdx = 0; subdirIdx < pLibList[pLibIdx].dir_list[dirIdx].subdir_list.length; ++subdirIdx) + selectedMenuIdx = userInput - 1; + selectedItemIndexes.push(selectedMenuIdx); + previousChosenLibOrSubdirNames.push(chosenLibOrSubdirName); + if (previousChosenLibOrSubdirNames.length > 1) { - if (bbs.curdir_code == pLibList[pLibIdx].dir_list[dirIdx].subdir_list[subdirIdx].code) + chosenLibOrSubdirName = previousChosenLibOrSubdirNames[previousChosenLibOrSubdirNames.length-1]; + // If chosenLibOrSubdirName is now too long, remove some of the previous labels + while (chosenLibOrSubdirName.length > console.screen_columns - 1) { - defaultDirNum = dirIdx + 1; - break; + var sepIdx = chosenLibOrSubdirName.indexOf(nameSep); + if (sepIdx > -1) + chosenLibOrSubdirName = chosenLibOrSubdirName.substr(sepIdx + nameSep.length); } } + else + chosenLibOrSubdirName = fileLibStructure[selectedMenuIdx].name; } else { - if (bbs.curdir_code == pLibList[pLibIdx].dir_list[dirIdx].code) + // The user didn't make a selection. So, we need to refresh the + // screen (including the header, due to things being moved down one line). + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) + console.gotoxy(1, 1); + this.DisplayMenuHdrWithNumItems(createMenuRet.itemNumWidth, createMenuRet.descWidth-3, createMenuRet.numItemsWidth, numItemsColLabel); + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) + this.WriteKeyHelpLine(); + continue; // Continue to display the menu again and get the user's choice + } + } + } + + // The code block above will set selectedMenuIdx if the user typed a valid entry + + // Check for aborted & other uesr input and take appropriate action. Note the first check + // here is 'if' and not 'else if'; that's intentional. + if (console.aborted || lastUserInputUpper == CTRL_C) + { + // Fully quit out (note: This check/block must be before the test for Q/ESC/null return value) + menuContinueOn = false; + selectionLoopContinueOn = false; + } + else if (typeof(selectedMenuIdx) === "number") + { + // The user chose a valid item (the return value is the menu item index) + // The objects in this.lib_list have a 'name' property and either + // an 'items' property if it has sub-items or a 'dirObj' property + // if it's a file directory + selectedItemIndexes.push(selectedMenuIdx); + previousChosenLibOrSubdirNames.push(chosenLibOrSubdirName); + if (fileLibStructure[selectedMenuIdx].hasOwnProperty("items")) + { + previousFileLibStructures.push(fileLibStructure); + if (previousChosenLibOrSubdirNames.length > 1) + { + chosenLibOrSubdirName = previousChosenLibOrSubdirNames[previousChosenLibOrSubdirNames.length-1] + nameSep + fileLibStructure[selectedMenuIdx].name; + // If chosenLibOrSubdirName is now too long, remove some of the previous labels + while (chosenLibOrSubdirName.length > console.screen_columns - 1) { - defaultDirNum = dirIdx + 1; - break; + var sepIdx = chosenLibOrSubdirName.indexOf(nameSep); + if (sepIdx > -1) + chosenLibOrSubdirName = chosenLibOrSubdirName.substr(sepIdx + nameSep.length); } } + else + chosenLibOrSubdirName = fileLibStructure[selectedMenuIdx].name; + fileLibStructure = fileLibStructure[selectedMenuIdx].items; + menuContinueOn = false; + } + else if (fileLibStructure[selectedMenuIdx].hasOwnProperty("dirObj")) + { + // The user has selected a file directory + bbs.curdir_code = fileLibStructure[selectedMenuIdx].dirObj.code; + menuContinueOn = false; + selectionLoopContinueOn = false; } } - } - else - { - if (typeof(pDir) == "number") - defaultDirNum = pDir; - else if (typeof(bbs.curdir_code) == "string") + else if ((lastUserInputUpper == "/") || (lastUserInputUpper == CTRL_F)) // Start of find { - if (pLibIdx != file_area.dir[bbs.curdir_code].lib_index) - defaultDirNum = 1; + // Lightbar/ANSI mode + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) + { + console.gotoxy(1, console.screen_rows); + console.cleartoeol("\x01n"); + console.gotoxy(1, console.screen_rows); + var promptText = "Search: "; + console.print(promptText); + //var searchText = getStrWithTimeout(K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE, console.screen_columns - promptText.length - 1, SEARCH_TIMEOUT_MS); + var searchText = console.getstr(lastSearchText, console.screen_columns - promptText.length - 1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE); + lastSearchText = searchText; + // If the user entered text, then do the search, and if found, + // found, go to the page and select the item indicated by the + // search. + if (searchText.length > 0) + { + var oldLastSearchFoundIdx = lastSearchFoundIdx; + var oldSelectedItemIdx = menu.selectedItemIdx; + var idx = this.FindFileAreaIdxFromText(fileLibStructure, searchText, menu.selectedItemIdx); + lastSearchFoundIdx = idx; + if (idx > -1) + { + // Set the currently selected item in the menu, and ensure it's + // visible on the page + menu.selectedItemIdx = idx; + if (menu.selectedItemIdx >= menu.topItemIdx+menu.GetNumItemsPerPage()) + menu.topItemIdx = menu.selectedItemIdx - menu.GetNumItemsPerPage() + 1; + else if (menu.selectedItemIdx < menu.topItemIdx) + menu.topItemIdx = menu.selectedItemIdx; + else + { + // If the current index and the last index are both on the same page on the + // menu, then have the menu only redraw those items. + menu.nextDrawOnlyItems = [menu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; + } + } + else + { + var idx = this.FindFileAreaIdxFromText(fileLibStructure, searchText, 0); + lastSearchFoundIdx = idx; + if (idx > -1) + { + // Set the currently selected item in the menu, and ensure it's + // visible on the page + menu.selectedItemIdx = idx; + if (menu.selectedItemIdx >= menu.topItemIdx+menu.GetNumItemsPerPage()) + menu.topItemIdx = menu.selectedItemIdx - menu.GetNumItemsPerPage() + 1; + else if (menu.selectedItemIdx < menu.topItemIdx) + menu.topItemIdx = menu.selectedItemIdx; + else + { + // The current index and the last index are both on the same page on the + // menu, so have the menu only redraw those items. + menu.nextDrawOnlyItems = [menu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; + } + } + else + { + this.WriteLightbarKeyHelpErrorMsg("Not found"); + drawMenu = false; + } + } + } + else + drawMenu = false; + writeKeyHelpLine = true; + } else - defaultDirNum = file_area.dir[bbs.curdir_code].index + 1; - } - } - return defaultDirNum; - } - - var defaultDirNum = getDefaultDirNum(pSelectedDirIdx, this.useDirCollapsing, this.lib_list); - var searchText = ""; - var numDirsListed = 0; - var continueOn = false; - do - { - console.clear("\x01n"); - this.DisplayAreaChgHdr(1); - if (this.areaChangeHdrLines.length > 0) - console.crlf(); - numDirsListed = this.ListDirsInFileLib_Traditional(pLibIdx, defaultDirNum - 1, searchText); - if ((numDirsListed > 0) && (typeof(pSelectedDirIdx) == "number") && (pSelectedDirIdx >= 0) && (pSelectedDirIdx < numDirsListed)) - defaultDirNum = getDefaultDirNum(pSelectedDirIdx, this.useDirCollapsing, this.lib_list); - if (defaultDirNum >= 1) - console.print("\x01n\x01b\x01h� \x01n\x01cWhich, \x01hQ\x01n\x01cuit, \x01hCTRL-F\x01n\x01c, \x01h/\x01n\x01c, or [\x01h" + defaultDirNum + "\x01n\x01c]: \x01h"); - else - console.print("\x01n\x01b\x01h� \x01n\x01cWhich, \x01hQ\x01n\x01cuit, \x01hCTRL-F\x01n\x01c, \x01h/\x01n\x01c: \x01h"); - // Accept Q (quit), / or CTRL_F to search, or a file directory number - var selectedDirNum = console.getkeys("Q/" + CTRL_F, file_area.lib_list[pLibIdx].dir_list.length); + { + // Traditional/non-ANSI interface (menu will be using numbered mode) + // TODO: Ensure this is correct for file areas + console.attributes = "N"; + console.crlf(); + var promptText = "Search: "; + console.print(promptText); + var searchText = console.getstr(lastSearchText, console.screen_columns - promptText.length - 1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE); + var searchTextIsStr = (typeof(searchText) === "string"); + console.attributes = "N"; + console.crlf(); + // Don't need to set lastSearchFoundIdx + //if (searchTextIsStr) + // lastSearchText = searchText; + // If the user entered text, then do the search, and if found, + // found, go to the page and select the item indicated by the + // search. + if (searchTextIsStr && searchText.length > 0) + { + var oldLastSearchFoundIdx = lastSearchFoundIdx; + var oldSelectedItemIdx = menu.selectedItemIdx; + var idx = this.FindFileAreaIdxFromText(fileLibStructure, searchText, 0); + //lastSearchFoundIdx = idx; // Don't need to set lastSearchFoundIdx + var newMsgAreaStructure = []; + while (idx > -1) + { + newMsgAreaStructure.push(fileLibStructure[idx]); - // If the user just pressed enter (selectedDirNum would be blank), - // default the selected directory. - if (selectedDirNum.toString() == "Q") - continueOn = false; - else if (selectedDirNum.toString() == "") - selectedDirNum = defaultDirNum; - else if ((selectedDirNum == "/") || (selectedDirNum == CTRL_F)) - { - // Search - console.crlf(); - var searchPromptText = "\x01n\x01c\x01hSearch\x01g: \x01n"; - console.print(searchPromptText); - searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE); - console.attributes = "N"; - console.crlf(); - if (searchText.length > 0) - defaultDirNum = -1; - else - defaultDirNum = getDefaultDirNum(pSelectedDirIdx, this.useDirCollapsing, this.lib_list); - continueOn = true; - console.line_counter = 0; // To avoid pausing before the clear screen - } + // Find the next one + idx = this.FindFileAreaIdxFromText(fileLibStructure, searchText, idx+1); + } + if (newMsgAreaStructure.length > 0) + { + selectedItemIdx = selectedItemIndexes.push(selectedItemIdx); + fileLibStructure = previousFileLibStructures.push(fileLibStructure); + previousChosenLibOrSubdirNames.push(""); + fileLibStructure = newMsgAreaStructure; + createMenuRet = this.CreateLightbarMenu(newMsgAreaStructure, previousFileLibStructures.length+1, menuTopRow, 0, numItemsWidth); + menu = createMenuRet.menuObj; + } + else + console.print("Not found\r\n\x01p"); + } - // If the user chose a directory, then set the user's file directory. - if (selectedDirNum > 0) - { - continueOn = false; - retObj.areaSelected = true; - retObj.dirIdx = selectedDirNum - 1; - if (this.useDirCollapsing) - { - if (this.lib_list[pLibIdx].dir_list[retObj.dirIdx].subdir_list.length == 0) - retObj.dirCode = this.lib_list[pLibIdx].dir_list[retObj.dirIdx].code; + console.line_counter = 0; + console.clear("\x01n"); + writeHdrLines = true; + drawMenu = true; + writeKeyHelpLine = false; + } } - else - retObj.dirCode = file_area.lib_list[pLibIdx].dir_list[retObj.dirIdx].code; - } - } while (continueOn); - - return retObj; -} - -// For the DDFileAreaChooser class: Lets the user select a subdirectory within a -// file directory - Traditional user interface. This is meant for directory name -// collapsing, at the 3rd level. -// -// Parameters: -// pLibIdx: The file library index -// pDirIdx: The index of the directory within the file library -// -// Return value: An object containing the following properties: -// areaSelected: Boolean - Whether or not the user chose a file area. -// dirCode: The internal code of the directory chosen, if chose. If not chosen, -// this will be an empty string. -function DDFileAreaChooser_SelectSubdirWithinDir_Traditional(pLibIdx, pDirIdx) -{ - var retObj = { - areaSelected: false, - dirCode: "" - }; - - if (!this.useDirCollapsing || this.lib_list.length == 0) - return retObj; - if ((pLibIdx < 0) || (pLibIdx >= this.lib_list.length)) - return retObj; - if ((pDirIdx < 0) || (pDirIdx >= this.lib_list[pLibIdx].dir_list.length)) - { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no directories in this library.\r\n\x01p"); - return retObj; - } - if (this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list.length == 0) - { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no subdirectories in this directory.\r\n\x01p"); - return retObj; - } - - // Gets the default directory number (1-based) - function getDefaultSubdirNum(pLibList, pLibIdx, pDirIdx) - { - var subdirNum = 0; // Will be 1-based - for (var subdirIdx = 0; subdirIdx < pLibList[pLibIdx].dir_list[pDirIdx].subdir_list.length; ++subdirIdx) - { - if (bbs.curdir_code == pLibList[pLibIdx].dir_list[pDirIdx].subdir_list[subdirIdx].code) + else if (lastUserInputUpper == "N") // Next search result (requires an existing search term) { - subdirNum = subdirIdx + 1; - break; + // This works but seems a little strange sometimes. + // - Should this always start from the selected index? + // - If it wraps around to one of the items on the first page, + // should it always set the top index to 0? + if ((lastSearchText.length > 0) && (lastSearchFoundIdx > -1)) + { + var oldLastSearchFoundIdx = lastSearchFoundIdx; + var oldSelectedItemIdx = menu.selectedItemIdx; + // Do the search, and if found, go to the page and select the item + // indicated by the search. + var idx = this.FindFileAreaIdxFromText(fileLibStructure, searchText, lastSearchFoundIdx+1); + if (idx > -1) + { + lastSearchFoundIdx = idx; + // Set the currently selected item in the menu, and ensure it's + // visible on the page + menu.selectedItemIdx = idx; + if (menu.selectedItemIdx >= menu.topItemIdx+menu.GetNumItemsPerPage()) + { + menu.topItemIdx = menu.selectedItemIdx - menu.GetNumItemsPerPage() + 1; + if (menu.topItemIdx < 0) + menu.topItemIdx = 0; + } + else if (menu.selectedItemIdx < menu.topItemIdx) + menu.topItemIdx = menu.selectedItemIdx; + else + { + // The current index and the last index are both on the same page on the + // menu, so have the menu only redraw those items. + menu.nextDrawOnlyItems = [menu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; + } + } + else + { + idx = this.FindFileAreaIdxFromText(fileLibStructure, searchText, 0); + lastSearchFoundIdx = idx; + if (idx > -1) + { + // Set the currently selected item in the menu, and ensure it's + // visible on the page + menu.selectedItemIdx = idx; + if (menu.selectedItemIdx >= menu.topItemIdx+menu.GetNumItemsPerPage()) + { + menu.topItemIdx = menu.selectedItemIdx - menu.GetNumItemsPerPage() + 1; + if (menu.topItemIdx < 0) + menu.topItemIdx = 0; + } + else if (menu.selectedItemIdx < menu.topItemIdx) + menu.topItemIdx = menu.selectedItemIdx; + else + { + // The current index and the last index are both on the same page on the + // menu, so have the menu only redraw those items. + menu.nextDrawOnlyItems = [menu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; + } + } + else + { + this.WriteLightbarKeyHelpErrorMsg("Not found"); + drawMenu = false; + writeKeyHelpLine = true; + } + } + } + else + { + this.WriteLightbarKeyHelpErrorMsg("There is no previous search", true); + drawMenu = false; + writeKeyHelpLine = true; + } } - } - return subdirNum; - } - - // defaultSubdirNum is the default subdirectory # (will be 1-based) - var defaultSubdirNum = getDefaultSubdirNum(this.lib_list, pLibIdx, pDirIdx); - var searchText = ""; - var numDirsListed = 0; - var continueOn = false; - do - { - console.clear("\x01n"); - this.DisplayAreaChgHdr(1); - if (this.areaChangeHdrLines.length > 0) - console.crlf(); - numDirsListed = this.ListSubdirsInFileDir_Traditional(pLibIdx, pDirIdx, searchText); - if (defaultSubdirNum >= 1) - console.print("\x01n\x01b\x01h� \x01n\x01cWhich, \x01hQ\x01n\x01cuit, \x01hCTRL-F\x01n\x01c, \x01h/\x01n\x01c, or [\x01h" + defaultSubdirNum + "\x01n\x01c]: \x01h"); - else - console.print("\x01n\x01b\x01h� \x01n\x01cWhich, \x01hQ\x01n\x01cuit, \x01hCTRL-F\x01n\x01c, \x01h/\x01n\x01c: \x01h"); - // Accept Q (quit), / or CTRL_F to search, or a file directory number - var selectedSubdirNum = console.getkeys("Q/" + CTRL_F, this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list.length); - - // If the user just pressed enter (selectedSubdirNum would be blank), - // default the selected directory. - if (selectedSubdirNum.toString() == "Q") - continueOn = false; - else if (selectedSubdirNum.toString() == "") - selectedSubdirNum = defaultSubdirNum; - else if ((selectedSubdirNum == "/") || (selectedSubdirNum == CTRL_F)) - { - // Search - console.crlf(); - var searchPromptText = "\x01n\x01c\x01hSearch\x01g: \x01n"; - console.print(searchPromptText); - searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE); - console.attributes = "N"; - console.crlf(); - if (searchText.length > 0) - defaultSubdirNum = -1; - else - defaultSubdirNum = getDefaultSubdirNum(this.lib_list, pLibIdx, pDirIdx); - continueOn = true; - console.line_counter = 0; // To avoid pausing before the clear screen - } - - // If the user chose a directory, then set the user's file directory. - if (selectedSubdirNum > 0) - { - continueOn = false; - retObj.areaSelected = true; - retObj.dirCode = this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list[selectedSubdirNum-1].code; - } - } while (continueOn); - - return retObj; -} - -// For the DDFileAreaChooser class: Maps bbs.curlib to the collapsed library array library index -function DDFileAreaChooser_GetActualLibIdx() -{ - var libIdx = bbs.curlib; - if (this.useDirCollapsing) - { - var syncLibName = file_area.lib_list[bbs.curlib].name; - var syncLibDesc = file_area.lib_list[bbs.curlib].description; - for (var i = 0; i < this.lib_list.length; ++i) - { - if (this.lib_list[i].name == syncLibName && this.lib_list[i].description == syncLibDesc) + else if (lastUserInputUpper == "?") { - libIdx = i; - break; + var usingLightbar = this.useLightbarInterface && console.term_supports(USER_ANSI); + this.ShowHelpScreen(usingLightbar, true); + menuContinueOn = true; + selectionLoopContinueOn = true; + drawMenu = true; + writeHdrLines = true; + writeKeyHelpLine = true; } - } - } - return libIdx; -} - -// For the DDFileAreaChooser class: Traditional user interface for listing -// the file libraries -// -// Parameters: -// pSearchText: Text to search for in the file library names/descriptions. -// If blank or not a string, all will be displayed. -// -// Return value: The number of directories listed -function DDFileAreaChooser_ListFileLibs_Traditional(pSearchText) -{ - // Ensure that the file directory printf information is created for - // this file library. - for (var libIdx = 0; libIdx < file_area.lib_list.length; ++libIdx) - { - if (typeof(this.fileDirListPrintfInfo[libIdx]) == "undefined") - this.BuildFileDirPrintfInfoForLib(libIdx); - } - - var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : ""); - - // Print the list header - printf(this.fileLibListHdrPrintfStr, "Lib #", "Description", "# Dirs"); - console.crlf(); - console.attributes = "N"; - // Print the information for each file library - var numDirsListed = 0; - var printIt = true; - var currentDir = false; - var lib_list = (this.useDirCollapsing ? this.lib_list : file_area.lib_list); - for (var i = 0; i < lib_list.length; ++i) - { - if (searchText.length > 0) - printIt = ((lib_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (lib_list[i].description.toUpperCase().indexOf(searchText) >= 0)); - else - printIt = true; - - if (printIt) - { - ++numDirsListed; - // Print the library information. - var curLibIdx = 0; - if (typeof(bbs.curdir_code) == "string") - curLibIdx = file_area.dir[bbs.curdir_code].lib_index; - console.print(i == curLibIdx ? this.colors.areaMark + "*" : " "); - printf(this.fileLibPrintfStr, +(i+1), lib_list[i].description.substr(0, this.descFieldLen), - lib_list[i].dir_list.length); - console.crlf(); - } - } - return numDirsListed; -} - -// For the DDFileAreaChooser class: Traditional user interface for listing -// the directories in a file library -// -// Parameters: -// pLibIndex: The index of the file library (0-based) -// pMarkIndex: An index of a file library to display the "current" mark -// next to. This is optional. -// pSearchText: Text to search for in the file directories (blank or none to list all) -// -// Return value: The number of file directories listed -function DDFileAreaChooser_ListDirsInFileLib_Traditional(pLibIndex, pMarkIndex, pSearchText) -{ - // Set libIndex, the library index - var libIndex = 0; - if (typeof(pLibIndex) == "number") - libIndex = pLibIndex; - else if (typeof(bbs.curdir_code) == "string") - libIndex = file_area.dir[bbs.curdir_code].lib_index; - - // Set markIndex, the index of the item to highlight - var markIndex = -1; - if (typeof(pMarkIndex) == "number") - { - if ((pMarkIndex >= 0) && (pMarkIndex < file_area.lib_list[libIndex].dir_list.length)) - markIndex = pMarkIndex; - } - - var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : ""); - - // Ensure that the file directory printf information is created for - // this file library. - this.BuildFileDirPrintfInfoForLib(libIndex); - - // Print the header lines - console.print(this.colors.fileAreaHdr + "Directories of \x01h" + - file_area.lib_list[libIndex].description); - console.crlf(); - printf(this.fileDirHdrPrintfStr, "Dir #", "Description", "# Items"); - console.crlf(); - console.attributes = "N"; - var numDirsListed = 0; - var printIt = true; - var lib_list = (this.useDirCollapsing ? this.lib_list : file_area.lib_list); - for (var i = 0; i < lib_list[libIndex].dir_list.length; ++i) - { - if (searchText.length > 0) - printIt = ((lib_list[libIndex].dir_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (lib_list[libIndex].dir_list[i].description.toUpperCase().indexOf(searchText) >= 0)); - else - printIt = true; - if (printIt) - { - ++numDirsListed; - // See if this is the currently-selected directory. - console.print(markIndex > -1 && i == markIndex ? this.colors.areaMark + "*" : " "); - var dirDesc = lib_list[libIndex].dir_list[i].description; - var numItems = this.fileDirListPrintfInfo[libIndex].fileCounts[i]; - // For directory name collapsing, get subdirectory information for this directory, and - // append <subdirs> to the description if the directory has subdirectories. - if (this.useDirCollapsing) + // Quit - Note: This check should be last + else if (lastUserInputUpper == "Q" || lastUserInputUpper == KEY_ESC || selectedMenuIdx == null) { - if (lib_list[libIndex].dir_list[i].subdir_list.length > 0) + // Cancel/Quit + // Quit this menu loop and go back to the previous file lib/dir structure + menuContinueOn = false; + selectedItemIdx = selectedItemIndexes.pop(); + if (previousFileLibStructures.length == 0) { - numItems = lib_list[libIndex].dir_list[i].subdir_list.length; - dirDesc += " <subdirs>"; + // The user was at the first level in the lib/dir structure; fully quit out from here + selectionLoopContinueOn = false; + } + else // Go to the previous file lib/dir structure + { + fileLibStructure = previousFileLibStructures.pop(); + if (fileLibStructure == this.lib_list) + { + chosenLibOrSubdirName = ""; + previousChosenLibOrSubdirNames = []; + selectedItemIndexes = []; + } + else + chosenLibOrSubdirName = previousChosenLibOrSubdirNames.pop(); } - else - numItems = this.fileDirListPrintfInfo[libIndex].fileCounts[i]; } - // Print the directory information - printf(this.fileDirListPrintfInfo[libIndex].printfStr, +(i+1), - dirDesc.substr(0, this.descFieldLen), numItems); - console.crlf(); } } - return numDirsListed; -} - -// For the DDFileAreaChooser class: For directory name collapsing, this lists -// subdirectories in a directory, for the traditional user interface. -// -// Parameters: -// pLibIndex: The index of the file library (0-based) -// pDirIndex: The index of the file directory in the library (0-based) -// pSearchText: Text to search for in the file directories (blank or none to list all) -// -// Return value: The number of file subdirectories listed -function DDFileAreaChooser_ListSubdirsInFileDir_Traditional(pLibIndex, pDirIndex, pSearchText) -{ - // This is only for directory collapsing, to display subdirectories in a file directory - if (!this.useDirCollapsing || (this.lib_list.length == 0)) - return 0; - - // Set libIndex (the library index) - var libIndex = 0; - if (typeof(pLibIndex) == "number") - libIndex = pLibIndex; - else if (typeof(bbs.curdir_code) == "string") - libIndex = file_area.dir[bbs.curdir_code].lib_index; - - // Sanity checking - if ((libIndex < 0) || (libIndex >= this.lib_list.length)) - return 0; - if ((pDirIndex < 0) || (pDirIndex >= this.lib_list[libIndex].length)) - return 0; - if (this.lib_list[libIndex].dir_list[pDirIndex].subdir_list.length == 0) - return 0; - - var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : ""); - - // Ensure that the file directory printf information is created for - // this file library. - this.BuildFileDirPrintfInfoForLib(libIndex); - - // Print the header lines - console.print(this.colors.fileAreaHdr + "Directories of \x01h" + - file_area.lib_list[libIndex].description); - console.crlf(); - printf(this.fileDirHdrPrintfStr, "Dir #", "Description", "# Items"); - console.crlf(); - console.attributes = "N"; - var numDirsListed = 0; - var printIt = true; - for (var i = 0; i < this.lib_list[libIndex].dir_list[pDirIndex].subdir_list.length; ++i) - { - if (searchText.length > 0) - printIt = ((this.lib_list[libIndex].dir_list[pDirIndex].subdir_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (this.lib_list[libIndex].dir_list[i].description.toUpperCase().indexOf(searchText) >= 0)); - else - printIt = true; - if (printIt) - { - ++numDirsListed; - // See if this is the currently-selected directory. - var displayAreaMark = (bbs.curdir_code == this.lib_list[libIndex].dir_list[pDirIndex].subdir_list[i].code); - console.print(displayAreaMark ? this.colors.areaMark + "*" : " "); - var dirIdx = this.lib_list[libIndex].dir_list[pDirIndex].subdir_list[i].index; - var numFiles = this.fileDirListPrintfInfo[libIndex].fileCounts[dirIdx]; - var subdirDesc = this.lib_list[libIndex].dir_list[pDirIndex].subdir_list[i].description; - // Print the directory information - printf(this.fileDirListPrintfInfo[libIndex].printfStr, +(i+1), - subdirDesc.substr(0, this.descFieldLen), numFiles); - console.crlf(); - } - } - - return numDirsListed; } // For the DDFileAreaChooser class: Outputs the header line to appear above @@ -1018,768 +829,309 @@ function DDFileAreaChooser_WriteDirListHdr1Line(pLibIdx, pDirIdx, pNumPages, pPa // If using subdirectory collapsing, then build the description as needed. Otherwise, // just use the subdirectory description. var desc = ""; - if (this.useDirCollapsing) - { - // Ensure this.lib_list is set up - this.SetUpLibListWithCollapsedDirs(); - // The description should be the library's description. Also, if pDirIdx - // is a number, then append the directory description to the description. - desc = this.lib_list[pLibIdx].description; - if ((typeof(pDirIdx) === "number") && (this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list.length > 0)) - desc += this.dirCollapseSeparator + " " + this.lib_list[pLibIdx].dir_list[pDirIdx].description; - } - else - desc = file_area.lib_list[pLibIdx].description; + // The description should be the library's description. Also, if pDirIdx + // is a number, then append the directory description to the description. + desc = this.lib_list[pLibIdx].description; + if ((typeof(pDirIdx) === "number") && (this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list.length > 0)) + desc += this.dirCollapseSeparator + " " + this.lib_list[pLibIdx].dir_list[pDirIdx].description; printf(descFormatStr, desc.substr(0, descLen)); console.cleartoeol("\x01n"); } -// Lightbar functions - -// For the DDFileAreaChooser class: Lightbar interface for letting the user -// choose a file library and directory +// For the DDFileAreaChooser class: Creates a lightbar menu to choose a library/directory. // // Parameters: -// pLevel: The file heirarchy level: -// 1: File libraries -// 2: File directories within libraries -// 3: File subdirectories within directories (if directory name collapsing is enabled) -// This is optional and defaults to 1. -// pLibIdx: Optional - The file library index, if choosing a file directory -// pDirIdx: Optional - The file directory index (within a library), for use with -// directory name collapsing -// pCalledFromSelf: Optional boolean: Whether or not this function was called from itself. Defaults to false. -function DDFileAreaChooser_SelectFileArea_Lightbar(pLevel, pLibIdx, pDirIdx, pCalledFromSelf) -{ - if (pLevel > 1 && (pLibIdx == null || typeof(pLibIdx) !== "number")) - pLibIdx = this.GetActualLibIdx(); - // If there are file libraries, then don't let the user - // choose one. - if (file_area.lib_list.length == 0) - { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no file libraries.\r\n\x01p"); - return; - } - var level = (typeof(pLevel) == "number" ? pLevel : 1); - if ((level < 1) || (level > 3)) - return; - // 2: Choose a file directory within a library - // 3: Choose a subdirectory within a directory, for directory name collapsing - else if ((level == 2) || (level == 3)) - { - // If there are no directories in the given library index, then see if there's a next library with - // directories (and wrap around) - if (file_area.lib_list[pLibIdx].dir_list.length == 0) - { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no directories in " + file_area.lib_list[pLibIdx].description + ".\r\n\x01p"); - return; - } - } - - var calledFromSelf = (typeof(pCalledFromSelf) === "boolean" ? pCalledFromSelf : false); - - // Ensure that the file directory printf information is created for - // this file library. - if (pLevel > 1) - this.BuildFileDirPrintfInfoForLib(pLibIdx); - - // Displays the header & header lines above the list - function displayListHdrLines(pLevel, pAreaChooser, pLibIdx, pDirIdx, pNumPages, pPageNum, pClearScrFirst) - { - var clearScrFirst = (typeof(pClearScrFirst) === "boolean" ? pClearScrFirst : true); - if (clearScrFirst) - console.clear("\x01n"); - pAreaChooser.DisplayAreaChgHdr(1); - console.gotoxy(1, pAreaChooser.areaChangeHdrLines.length+1); - if (pLevel == 1) - pAreaChooser.WriteLibListHdrLine(pNumPages, pPageNum); - else - { - pAreaChooser.WriteDirListHdr1Line(pLibIdx, pDirIdx, pNumPages, pPageNum); - console.gotoxy(1, pAreaChooser.areaChangeHdrLines.length+2); - var numItemsColHdrTxt = ""; - if (pAreaChooser.useDirCollapsing) - { - if (pLevel == 2) - numItemsColHdrTxt = "# Items"; - else if (level == 3) - numItemsColHdrTxt = "# Files"; - - } - else - numItemsColHdrTxt = "# Files"; - printf(pAreaChooser.fileDirHdrPrintfStr, "Dir #", "Description", numItemsColHdrTxt); - } - } - - // Clear the screen, & write the key help line at the bottom of the screen - console.clear("\x01n"); - this.WriteKeyHelpLine(); - - // Create the menu and do the uesr input loop - var fileAreaMenu; - switch (level) - { - case 1: - fileAreaMenu = this.CreateLightbarFileLibMenu(); - break; - case 2: - case 3: - fileAreaMenu = this.CreateLightbarFileDirMenu(pLibIdx, pDirIdx, level); - break; - } - var drawMenu = true; - var lastSearchText = ""; - var lastSearchFoundIdx = -1; - var chosenIdx = -1; - var continueOn = true; - // Let the user choose a group, and also respond to other user choices - while (continueOn) - { - displayListHdrLines(level, this, pLibIdx, pDirIdx, null, null, false); - chosenIdx = -1; - var returnedMenuIdx = fileAreaMenu.GetVal(drawMenu); - drawMenu = true; - var lastUserInputUpper = (typeof(fileAreaMenu.lastUserInput) == "string" ? fileAreaMenu.lastUserInput.toUpperCase() : ""); - if (typeof(returnedMenuIdx) == "number") - chosenIdx = returnedMenuIdx; - // If userChoice is not a number, then it should be null in this case, - // and the user would have pressed one of the additional quit keys set - // up for the menu. So look at the menu's lastUserInput and do the - // appropriate thing. - else if ((lastUserInputUpper == "Q") || (lastUserInputUpper == KEY_ESC)) // Quit - continueOn = false; - else if ((lastUserInputUpper == "/") || (lastUserInputUpper == CTRL_F)) // Start of find - { - console.gotoxy(1, console.screen_rows); - console.cleartoeol("\x01n"); - console.gotoxy(1, console.screen_rows); - var promptText = "Search: "; - console.print(promptText); - var searchText = getStrWithTimeout(K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE, console.screen_columns - promptText.length - 1, SEARCH_TIMEOUT_MS); - lastSearchText = searchText; - // If the user entered text, then do the search, and if found, - // found, go to the page and select the item indicated by the - // search. - if (searchText.length > 0) - { - var oldLastSearchFoundIdx = lastSearchFoundIdx; - var oldSelectedItemIdx = fileAreaMenu.selectedItemIdx; - var idx = -1; - switch (level) - { - case 1: - idx = findFileLibIdxFromText(searchText, fileAreaMenu.selectedItemIdx, level, pDirIdx); - break; - case 2: - idx = this.FindFileDirIdxFromText(pLibIdx, searchText, fileAreaMenu.selectedItemIdx+1, level, pDirIdx); - break; - case 3: - idx = this.FindFileDirIdxFromText(pLibIdx, searchText, 0, level, pDirIdx); - break; - } - lastSearchFoundIdx = idx; - if (idx > -1) - { - // Set the currently selected item in the menu, and ensure it's - // visible on the page - fileAreaMenu.selectedItemIdx = idx; - if (fileAreaMenu.selectedItemIdx >= fileAreaMenu.topItemIdx+fileAreaMenu.GetNumItemsPerPage()) - fileAreaMenu.topItemIdx = fileAreaMenu.selectedItemIdx - fileAreaMenu.GetNumItemsPerPage() + 1; - else if (fileAreaMenu.selectedItemIdx < fileAreaMenu.topItemIdx) - fileAreaMenu.topItemIdx = fileAreaMenu.selectedItemIdx; - else - { - // If the current index and the last index are both on the same page on the - // menu, then have the menu only redraw those items. - fileAreaMenu.nextDrawOnlyItems = [fileAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; - } - } - else - { - switch (level) - { - case 1: - idx = findFileLibIdxFromText(searchText, 0); - break; - case 2: - idx = this.FindFileDirIdxFromText(pLibIdx, searchText, 0, level, pDirIdx); - break; - case 3: - idx = this.FindFileDirIdxFromText(pLibIdx, searchText, 0, level, pDirIdx); - break; - } - lastSearchFoundIdx = idx; - if (idx > -1) - { - // Set the currently selected item in the menu, and ensure it's - // visible on the page - fileAreaMenu.selectedItemIdx = idx; - if (fileAreaMenu.selectedItemIdx >= fileAreaMenu.topItemIdx+fileAreaMenu.GetNumItemsPerPage()) - fileAreaMenu.topItemIdx = fileAreaMenu.selectedItemIdx - fileAreaMenu.GetNumItemsPerPage() + 1; - else if (fileAreaMenu.selectedItemIdx < fileAreaMenu.topItemIdx) - fileAreaMenu.topItemIdx = fileAreaMenu.selectedItemIdx; - else - { - // The current index and the last index are both on the same page on the - // menu, so have the menu only redraw those items. - fileAreaMenu.nextDrawOnlyItems = [fileAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; - } - } - else - { - this.WriteLightbarKeyHelpErrorMsg("Not found"); - drawMenu = false; - } - } - } - else - drawMenu = false; - this.WriteKeyHelpLine(); - } - else if (lastUserInputUpper == "N") // Next search result (requires an existing search term) - { - // This works but seems a little strange sometimes. - // - Should this always start from the selected index? - // - If it wraps around to one of the items on the first page, - // should it always set the top index to 0? - if ((lastSearchText.length > 0) && (lastSearchFoundIdx > -1)) - { - var oldLastSearchFoundIdx = lastSearchFoundIdx; - var oldSelectedItemIdx = fileAreaMenu.selectedItemIdx; - // Do the search, and if found, go to the page and select the item - // indicated by the search. - var idx = -1; - switch (level) - { - case 1: - idx = findFileLibIdxFromText(searchText, lastSearchFoundIdx+1); - break; - case 2: - idx = this.FindFileDirIdxFromText(pLibIdx, searchText, lastSearchFoundIdx+1, level, pDirIdx); - break; - case 3: - // TODO - break; - } - if (idx > -1) - { - lastSearchFoundIdx = idx; - // Set the currently selected item in the menu, and ensure it's - // visible on the page - fileAreaMenu.selectedItemIdx = idx; - if (fileAreaMenu.selectedItemIdx >= fileAreaMenu.topItemIdx+fileAreaMenu.GetNumItemsPerPage()) - { - fileAreaMenu.topItemIdx = fileAreaMenu.selectedItemIdx - fileAreaMenu.GetNumItemsPerPage() + 1; - if (fileAreaMenu.topItemIdx < 0) - fileAreaMenu.topItemIdx = 0; - } - else if (fileAreaMenu.selectedItemIdx < fileAreaMenu.topItemIdx) - fileAreaMenu.topItemIdx = fileAreaMenu.selectedItemIdx; - else - { - // The current index and the last index are both on the same page on the - // menu, so have the menu only redraw those items. - fileAreaMenu.nextDrawOnlyItems = [fileAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; - } - } - else - { - switch (level) - { - case 1: - idx = findFileLibIdxFromText(searchText, 0); - break; - case 2: - idx = this.FindFileDirIdxFromText(pLibIdx, searchText, 0, level, pDirIdx); - break; - case 3: - // TODO - break; - } - lastSearchFoundIdx = idx; - if (idx > -1) - { - // Set the currently selected item in the menu, and ensure it's - // visible on the page - fileAreaMenu.selectedItemIdx = idx; - if (fileAreaMenu.selectedItemIdx >= fileAreaMenu.topItemIdx+fileAreaMenu.GetNumItemsPerPage()) - { - fileAreaMenu.topItemIdx = fileAreaMenu.selectedItemIdx - fileAreaMenu.GetNumItemsPerPage() + 1; - if (fileAreaMenu.topItemIdx < 0) - fileAreaMenu.topItemIdx = 0; - } - else if (fileAreaMenu.selectedItemIdx < fileAreaMenu.topItemIdx) - fileAreaMenu.topItemIdx = fileAreaMenu.selectedItemIdx; - else - { - // The current index and the last index are both on the same page on the - // menu, so have the menu only redraw those items. - fileAreaMenu.nextDrawOnlyItems = [fileAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; - } - } - else - { - this.WriteLightbarKeyHelpErrorMsg("Not found"); - drawMenu = false; - this.WriteKeyHelpLine(); - } - } - } - else - { - this.WriteLightbarKeyHelpErrorMsg("There is no previous search", true); - drawMenu = false; - this.WriteKeyHelpLine(); - } - } - else if (lastUserInputUpper == "?") // Show help - { - this.ShowHelpScreen(true, true); - console.pause(); - // Refresh the screen - displayListHdrLines(level, this, pLibIdx, pDirIdx); - this.WriteKeyHelpLine(); - } - // If the user entered a numeric digit, then treat it as - // the start of the file library number. - else if (lastUserInputUpper.match(/[0-9]/)) - { - // Put the user's input back in the input buffer to - // be used for getting the rest of the message number. - console.ungetstr(lastUserInputUpper); - // Move the cursor to the bottom of the screen and - // prompt the user for the message number. - console.gotoxy(1, console.screen_rows); - console.clearline("\x01n"); - var itemPromptWord = ""; - if (this.useDirCollapsing) - { - if (level == 1) - itemPromptWord = "library"; - else if (level == 2) - itemPromptWord = "item"; - else if (level == 3) - itemPromptWord = "directory"; - } - else - itemPromptWord = (level == 1 ? "library" : "directory"); - printf("\x01cChoose %s #: \x01h", itemPromptWord); - var userInput = console.getnum(fileAreaMenu.NumItems()); - if (userInput > 0) - chosenIdx = userInput - 1; - else - { - // The user didn't make a selection. So, we need to refresh - // the screen due to everything being moved up one line. - displayListHdrLines(level, this, pLibIdx, pDirIdx); - this.WriteKeyHelpLine(); - } - } - - // If a library/directory was chosen, then deal with it. - if (chosenIdx > -1) - { - // If choosing a file library, then let the user choose a file - // directory within the library. Otherwise, return the user's - // chosen file directory. - if (level == 1) - { - // Show a "Loading..." text in case there are many directories in - // the chosen file library - console.crlf(); - console.print("\x01nLoading..."); - console.line_counter = 0; // To prevent a pause before the message list comes up - // Ensure that the file dir printf information is created for - // the chosen file library. - this.BuildFileDirPrintfInfoForLib(chosenIdx); - // Make a backup of bbs.curdir_code, in case the directory - // changes at the 3rd level (if directory collapsing is - // enabled) - var dirCodeBackup = bbs.curdir_code; - var chosenFileDirIdx = this.SelectFileArea_Lightbar(level+1, chosenIdx, null, true); - // chosenFileDirIdx could actually be a boolean and could be false (returned - // when pLevel is 3 and the user chose a directory), so check its type and - // act accordingly. - var retValType = typeof(chosenFileDirIdx); - if (retValType === "boolean") - continueOn = chosenFileDirIdx; - else if (retValType === "number") - { - if (chosenFileDirIdx > -1) - { - // Set the current file directory - if (this.useDirCollapsing) - bbs.curdir_code = this.lib_list[chosenIdx].dir_list[chosenFileDirIdx].code; - else - bbs.curdir_code = file_area.lib_list[chosenIdx].dir_list[chosenFileDirIdx].code; - continueOn = false; - } - else - { - // If the dir changed (probably at level 3 because directory - // collapsing is enabled), then exit here. - if (bbs.curdir_code != dirCodeBackup) - continueOn = false; - else - { - // A file directory was not chosen, so we'll have to re-draw - // the header and key help line - displayListHdrLines(level, this, pLibIdx, pDirIdx); - this.WriteKeyHelpLine(); - } - } - } - } - else if (level == 2) - { - if (this.useDirCollapsing) - { - // Ensure this.lib_list is set up - this.SetUpLibListWithCollapsedDirs(); - - // If the current file directory has subdirectories, - // let the user choose one - // pLibIdx is the library index, and chosenIdx is the file directory index - if ((typeof(this.lib_list[pLibIdx].dir_list[chosenIdx]) !== "undefined") && (this.lib_list[pLibIdx].dir_list[chosenIdx].subdir_list.length > 0)) - { - //SelectFileArea_Lightbar(pLevel, pLibIdx, pDirIdx) - var chosenSubdirIdx = this.SelectFileArea_Lightbar(level+1, pLibIdx, chosenIdx, true); - if (chosenSubdirIdx > -1) - { - // Set the current file directory - bbs.curdir_code = this.lib_list[pLibIdx].dir_list[chosenIdx].subdir_list[chosenSubdirIdx].code; - continueOn = false; - // Return a false here so that after this function is called by itself when - // pLevel is 2, it will know that a sub-board has been chosen and will set - // continueOn to false so that the loop won't continue from there. - return false; - } - else - { - // A file directory was not chosen, so we'll have to re-draw - // the header and list of message groups. - displayListHdrLines(level, this, pLibIdx, pDirIdx); - this.WriteKeyHelpLine(); - } - } - else // No subdirectories - Return the chosen index - { - // If this function wasn't called from itself (i.e., letting the user choose a file directory - // directly within their chosen library), then set the user's directory here - if (!calledFromSelf) - bbs.curdir_code = this.lib_list[pLibIdx].dir_list[chosenIdx].code; - return chosenIdx; - } - } - else - { - // Not using directory name collapsing - // If this function wasn't called from itself (i.e., letting the user choose a file directory - // directly within their chosen library), then set the user's directory here - if (!calledFromSelf) - bbs.curdir_code = file_area.lib_list[pLibIdx].dir_list[chosenIdx].code; - return chosenIdx; // Return the chosen file directory index - } - } - else if (level == 3) - return chosenIdx; // Return the chosen subdirectory index - } - } -} - -// For the DDFileAreaChooser class: Creates the DDLightbarMenu for use with -// choosing a file library in lightbar mode. +// pDirHeirarchyObj: An object from this.lib_list, which is +// set up with a 'name' property and either +// an 'items' property if it has sub-items +// or a 'dirObj' property if it's a file +// directory +// pHeirarchyLevel: The level we're at in the heirarchy (1-based) +// pMenuTopRow: The screen row to use for the menu's top row +// pSelectedItemIdx: Optional - The index to use for the selected item. If not +// specified, the item with the user's current selected file +// directory will be used, if available. +// pNumItemsWidth: The character width of the column label for the number of items; mainly for the traditional (non-lightbar) UI // -// Return value: A DDLightbarMenu object for choosing a file library -function DDFileAreaChooser_CreateLightbarFileLibMenu() +// Return value: An object with the following properties: +// menuObj: The menu object +// allDirs: Whether or not all the items in the menu +// are file directiries. If not, some or all +// are other lists of items +// itemNumWidth: The width of the item numbers column +// descWidth: The width of the description column +// numItemsWidth: The width of the # of items column +function DDFileAreaChooser_CreateLightbarMenu(pDirHeirarchyObj, pHeirarchyLevel, pMenuTopRow, pSelectedItemIdx, pNumItemsWidth) { - // Start & end indexes for the various items in each file library list row - // Selected mark, lib#, description, # dirs - var fileLibListIdxes = { - markCharStart: 0, - markCharEnd: 1, - libNumStart: 1, - libNumEnd: 2 + (+this.areaNumLen) + var retObj = { + menuObj: null, + allDirs: true, + itemNumWidth: 0, + descWidth: 0, + numItemsWidth: 0 }; - fileLibListIdxes.descStart = fileLibListIdxes.libNumEnd; - fileLibListIdxes.descEnd = fileLibListIdxes.descStart + +this.descFieldLen; - fileLibListIdxes.numItemsStart = fileLibListIdxes.descEnd; - // Set numItemsEnd to -1 to let the whole rest of the lines be colored - fileLibListIdxes.numItemsEnd = -1; - var listStartRow = this.areaChangeHdrLines.length + 2; - var fileLibMenuHeight = console.screen_rows - listStartRow; - var fileLibMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, fileLibMenuHeight); - fileLibMenu.scrollbarEnabled = true; - fileLibMenu.borderEnabled = false; - fileLibMenu.SetColors({ - itemColor: [{start: fileLibListIdxes.markCharStart, end: fileLibListIdxes.markCharEnd, attrs: this.colors.areaMark}, - {start: fileLibListIdxes.libNumStart, end: fileLibListIdxes.libNumEnd, attrs: this.colors.areaNum}, - {start: fileLibListIdxes.descStart, end: fileLibListIdxes.descEnd, attrs: this.colors.desc}, - {start: fileLibListIdxes.numItemsStart, end: fileLibListIdxes.numItemsEnd, attrs: this.colors.numItems}], - selectedItemColor: [{start: fileLibListIdxes.markCharStart, end: fileLibListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, - {start: fileLibListIdxes.libNumStart, end: fileLibListIdxes.libNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight}, - {start: fileLibListIdxes.descStart, end: fileLibListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, - {start: fileLibListIdxes.numItemsStart, end: fileLibListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}] - }); - - fileLibMenu.multiSelect = false; - fileLibMenu.ampersandHotkeysInItems = false; - fileLibMenu.wrapNavigation = false; + // Get color index information for the menu + var colorIdxInfo = this.GetColorIndexInfoForLightbarMenu(pDirHeirarchyObj); + // Calculate column widths for the return object + retObj.itemNumWidth = colorIdxInfo.fileDirListIdxes.itemNumEnd - 1; + //retObj.descWidth = colorIdxInfo.fileDirListIdxes.descEnd - colorIdxInfo.fileDirListIdxes.descStart; + retObj.descWidth = this.descFieldLen; + retObj.numItemsWidth = console.screen_columns - colorIdxInfo.fileDirListIdxes.numItemsStart; + // Create and set up the menu + var fileDirMenuHeight = console.screen_rows - pMenuTopRow; + var fileDirMenu = new DDLightbarMenu(1, pMenuTopRow, console.screen_columns, fileDirMenuHeight); // Add additional keypresses for quitting the menu's input loop so we can // respond to these keys - fileLibMenu.AddAdditionalQuitKeys("nNqQ ?0123456789/" + CTRL_F); - - // Change the menu's NumItems() and GetItem() function to reference - // the message list in this object rather than add the menu items - // to the menu - fileLibMenu.areaChooser = this; // Add this object to the menu object - fileLibMenu.NumItems = function() { - return file_area.lib_list.length; - }; - fileLibMenu.GetItem = function(pLibIndex) { - var menuItemObj = this.MakeItemWithRetval(-1); - if ((pLibIndex >= 0) && (pLibIndex < file_area.lib_list.length)) + fileDirMenu.AddAdditionalQuitKeys("qQ?/" + CTRL_F); + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) + { + fileDirMenu.allowANSI = true; + // Additional quit keys for ANSI mode which we can respond to + // N: Next (search) and numbers for item input + fileDirMenu.AddAdditionalQuitKeys(" nN0123456789" + CTRL_C); + } + // If not using the lightbar interface (and ANSI behavior is not to be allowed), fileDirMenu.numberedMode + // will be set to true by default. Also, in that situation, set the menu's item number color + else + { + fileDirMenu.allowANSI = false; + fileDirMenu.colors.itemNumColor = this.colors.areaNum; + //retObj.numItemsWidth = maxNumItemsWidthInHeirarchy(pDirHeirarchyObj); + if (typeof(pNumItemsWidth) === "number") + retObj.itemNumWidth = pNumItemsWidth; + else + retObj.itemNumWidth = pDirHeirarchyObj.length.toString().length; + retObj.descWidth = console.screen_columns - retObj.itemNumWidth - retObj.numItemsWidth - 1; + // Temporary + if (user.is_sysop) { - if ((typeof(bbs.curdir_code) == "string") && (bbs.curdir_code != "")) - menuItemObj.text = (pLibIndex == file_area.dir[bbs.curdir_code].lib_index ? "*" : " "); - else - menuItemObj.text = " "; - menuItemObj.text += format(this.areaChooser.fileLibPrintfStr, +(pLibIndex+1), - file_area.lib_list[pLibIndex].description.substr(0, this.areaChooser.descFieldLen), - file_area.lib_list[pLibIndex].dir_list.length); - menuItemObj.text = strip_ctrl(menuItemObj.text); - menuItemObj.retval = pLibIndex; + //printf("\x01npNumItemsWidth: %d, itemNumWidth: %d, # items width: %d, descWidth: %d\r\n", pNumItemsWidth, retObj.itemNumWidth, retObj.numItemsWidth, retObj.descWidth); + //printf("\x01nLongest # items in heirarchy: %d\r\n", maxNumItemsWidthInHeirarchy(pDirHeirarchyObj)); } - - return menuItemObj; - }; - - // Set the currently selected item to the current group - fileLibMenu.selectedItemIdx = file_area.dir[bbs.curdir_code].lib_index; - if (fileLibMenu.selectedItemIdx >= fileLibMenu.topItemIdx+fileLibMenu.GetNumItemsPerPage()) - fileLibMenu.topItemIdx = fileLibMenu.selectedItemIdx - fileLibMenu.GetNumItemsPerPage() + 1; - - return fileLibMenu; -} - -// For the DDFileAreaChooser class: Creates the DDLightbarMenu for use with -// choosing a file directory in lightbar mode. -// -// Parameters: -// pLibIdx: The index of the file library -// pDirIdx: The index of a file directory, if name collapsing is being used -// pLevel: The level into the heirarchy. 2 would be file directories, and -// 3 would be subdirectories inside directories if name collapsing -// is being used -// -// Return value: A DDLightbarMenu object for choosing a file directory within -// the given file library -function DDFileAreaChooser_CreateLightbarFileDirMenu(pLibIdx, pDirIdx, pLevel) -{ - // Start & end indexes for the various items in each mssage group list row - // Selected mark, group#, description, # directorys - var fileDirListIdxes = { - markCharStart: 0, - markCharEnd: 1, - dirNumStart: 1, - dirNumEnd: 3 + (+this.areaNumLen) - }; - fileDirListIdxes.descStart = fileDirListIdxes.dirNumEnd; - fileDirListIdxes.descEnd = fileDirListIdxes.descStart + (+this.descFieldLen) + 1; - fileDirListIdxes.numItemsStart = fileDirListIdxes.descEnd; - // Set numItemsEnd to -1 to let the whole rest of the lines be colored - fileDirListIdxes.numItemsEnd = -1; - var listStartRow = this.areaChangeHdrLines.length + 3; // or + 2? - var fileDirMenuHeight = console.screen_rows - listStartRow; - var fileDirMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, fileDirMenuHeight); + // End Temporary + } + // If not using the lightbar interface (and ANSI behavior is not to be allowed), fileDirMenu.numberedMode + // will be set to true by default. Also, in that situation, set the menu's item number color + if (!fileDirMenu.allowANSI) + fileDirMenu.colors.itemNumColor = this.colors.areaNum; fileDirMenu.scrollbarEnabled = true; fileDirMenu.borderEnabled = false; + // Set the color arrays in the menu fileDirMenu.SetColors({ - itemColor: [{start: fileDirListIdxes.markCharStart, end: fileDirListIdxes.markCharEnd, attrs: this.colors.areaMark}, - {start: fileDirListIdxes.dirNumStart, end: fileDirListIdxes.dirNumEnd, attrs: this.colors.areaNum}, - {start: fileDirListIdxes.descStart, end: fileDirListIdxes.descEnd, attrs: this.colors.desc}, - {start: fileDirListIdxes.numItemsStart, end: fileDirListIdxes.numItemsEnd, attrs: this.colors.numItems}], - selectedItemColor: [{start: fileDirListIdxes.markCharStart, end: fileDirListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, - {start: fileDirListIdxes.dirNumStart, end: fileDirListIdxes.dirNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight}, - {start: fileDirListIdxes.descStart, end: fileDirListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, - {start: fileDirListIdxes.numItemsStart, end: fileDirListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}] + itemColor: colorIdxInfo.itemColor, + selectedItemColor: colorIdxInfo.selectedItemColor }); fileDirMenu.multiSelect = false; fileDirMenu.ampersandHotkeysInItems = false; fileDirMenu.wrapNavigation = false; - - // Add additional keypresses for quitting the menu's input loop so we can - // respond to these keys - fileDirMenu.AddAdditionalQuitKeys("nNqQ ?0123456789/" + CTRL_F); + // Menu prompt text for non-ANSI mode + fileDirMenu.nonANSIPromptText = "\x01n\x01b\x01h" + TALL_UPPER_MID_BLOCK + " \x01n\x01cWhich, \x01hQ\x01n\x01cuit, \x01hCTRL-F\x01n\x01c, \x01h/\x01n\x01c: \x01h"; // Build the file directory info for the given file library - this.BuildFileDirPrintfInfoForLib(pLibIdx); - // Change the menu's NumItems() and GetItem() function to reference - // the message list in this object rather than add the menu items - // to the menu - fileDirMenu.areaChooser = this; // Add this object to the menu object - fileDirMenu.libIdx = pLibIdx; - if (this.useDirCollapsing) + fileDirMenu.dirHeirarchyObj = pDirHeirarchyObj; + fileDirMenu.areaChooser = this; + fileDirMenu.allDirs = true; // Whether the menu has only directories (no file libraries) + if (Array.isArray(pDirHeirarchyObj)) { - if (pLevel == 2) + // See if any of the items in the array aren't directories, and set retObj.allDirs. + // Also, see which one has the user's current chosen directory so we can set the + // current menu item index - And save that index in the menu object for its + // reference later. + fileDirMenu.idxWithUserSelectedDir = -1; + for (var i = 0; i < pDirHeirarchyObj.length; ++i) { - fileDirMenu.NumItems = function() { - return this.areaChooser.lib_list[this.libIdx].dir_list.length; - }; - fileDirMenu.GetItem = function(pDirIdx) { - var menuItemObj = this.MakeItemWithRetval(-1); - if ((pDirIdx >= 0) && (pDirIdx < this.areaChooser.lib_list[this.libIdx].dir_list.length)) - { - var showDirMark = false; - if ((typeof(bbs.curdir_code) == "string") && (bbs.curdir_code != "")) - { - if (this.areaChooser.lib_list[this.libIdx].dir_list[pDirIdx].hasOwnProperty("subdir_list") && this.areaChooser.lib_list[this.libIdx].dir_list[pDirIdx].subdir_list.length > 0) - { - for (var subDirIdx = 0; subDirIdx < this.areaChooser.lib_list[this.libIdx].dir_list[pDirIdx].subdir_list.length && !showDirMark; ++subDirIdx) - showDirMark = (bbs.curdir_code == this.areaChooser.lib_list[this.libIdx].dir_list[pDirIdx].subdir_list[subDirIdx].code); - } - else if (this.areaChooser.lib_list[this.libIdx].dir_list[pDirIdx].hasOwnProperty("code")) - showDirMark = (bbs.curdir_code == this.areaChooser.lib_list[this.libIdx].dir_list[pDirIdx].code); - } - // Set the directory description. And if it has subdirectories, - // then append some text indicating so. - var dirDesc = this.areaChooser.lib_list[this.libIdx].dir_list[pDirIdx].description; - if (this.areaChooser.lib_list[this.libIdx].dir_list[pDirIdx].subdir_list.length > 0) - dirDesc += " <subdirs>"; - menuItemObj.text = (showDirMark ? "*" : " "); - menuItemObj.text += format(this.areaChooser.fileDirListPrintfInfo[this.libIdx].printfStr, +(pDirIdx+1), - dirDesc.substr(0, this.areaChooser.descFieldLen), - this.areaChooser.fileDirListPrintfInfo[this.libIdx].fileCounts[pDirIdx]); - menuItemObj.text = strip_ctrl(menuItemObj.text); - menuItemObj.retval = pDirIdx; - } + // Each object will have either an "items" or a "dirObj" + if (!pDirHeirarchyObj[i].hasOwnProperty("dirObj")) + { + retObj.allDirs = false; + fileDirMenu.allDirs = false; + } + // See if this one has the user's selected file directory + if (fileDirStructureHasCurrentUserFileDir(pDirHeirarchyObj[i])) + fileDirMenu.idxWithUserSelectedDir = i; + // If we've found all we need, then stop going through the array + if (!retObj.allDirs && fileDirMenu.idxWithUserSelectedDir > -1) + break; + } - return menuItemObj; - }; - - // Set the currently selected item. If the current directory is in this list, - // then set the selected item to that; otherwise, the selected item should be - // the first directory. - if (file_area.dir[bbs.curdir_code].lib_index == pLibIdx) + // Replace the menu's NumItems() function to return the correct number of items + fileDirMenu.NumItems = function() { + return this.dirHeirarchyObj.length; + }; + fileDirMenu.numItemsLen = fileDirMenu.NumItems().toString().length; + // Replace the menu's GetItem() function to create & return an item for the menu + fileDirMenu.descFieldLen = this.descFieldLen; // Mainly for lightbar mode + if (!fileDirMenu.allowANSI) + fileDirMenu.descFieldLen += 3; + fileDirMenu.GetItem = function(pItemIdx) { + var menuItemObj = this.MakeItemWithRetval(-1); + //var showDirMark = fileDirStructureHasCurrentUserFileDir(this.dirHeirarchyObj[pItemIdx]); + var showDirMark = (pItemIdx == this.idxWithUserSelectedDir); + var areaDesc = this.dirHeirarchyObj[pItemIdx].name; + var numItems = 0; + var adjustDescLen = false; + if (this.dirHeirarchyObj[pItemIdx].hasOwnProperty("items")) { - var currentIdx = -1; - for (var dirI = 0; dirI < this.lib_list[pLibIdx].dir_list.length && currentIdx == -1; ++dirI) - { - var dirSubdirsValid = this.lib_list[pLibIdx].dir_list[dirI].hasOwnProperty("subdir_list") && this.lib_list[pLibIdx].dir_list[dirI].subdir_list.length > 0; - if (dirSubdirsValid) - { - for (var subDirIdx = 0; subDirIdx < this.lib_list[pLibIdx].dir_list[dirI].subdir_list.length && currentIdx == -1; ++subDirIdx) - { - if (bbs.curdir_code == this.lib_list[pLibIdx].dir_list[dirI].subdir_list[subDirIdx].code) - currentIdx = dirI; - } - } - else - { - if (bbs.curdir_code == this.lib_list[pLibIdx].dir_list[dirI].code) - currentIdx = dirI; - } - } - if (currentIdx > -1) + numItems = this.dirHeirarchyObj[pItemIdx].items.length; + // If this isn't the top level (libraries), then add "<subdirs>" to the description + if (this.dirHeirarchyObj != this.areaChooser.lib_list) { - fileDirMenu.selectedItemIdx = currentIdx - if (fileDirMenu.selectedItemIdx >= fileDirMenu.topItemIdx+fileDirMenu.GetNumItemsPerPage()) - fileDirMenu.topItemIdx = fileDirMenu.selectedItemIdx - fileDirMenu.GetNumItemsPerPage() + 1; + areaDesc += " <subdirs>"; + //adjustDescLen = true; } } - } - else if (pLevel == 3) - { - fileDirMenu.dirIdx = pDirIdx; - fileDirMenu.NumItems = function() { - return this.areaChooser.lib_list[this.libIdx].dir_list[this.dirIdx].subdir_list.length; - }; - fileDirMenu.GetItem = function(pSubdirIdx) { - var menuItemObj = this.MakeItemWithRetval(-1); - if ((pSubdirIdx >= 0) && (pSubdirIdx < this.areaChooser.lib_list[this.libIdx].dir_list[this.dirIdx].subdir_list.length)) + else if (this.dirHeirarchyObj[pItemIdx].hasOwnProperty("dirObj")) + { + if (numItems = this.dirHeirarchyObj[pItemIdx].dirObj.hasOwnProperty("files")) + numItems = this.dirHeirarchyObj[pItemIdx].dirObj.files; // Added in Synchronet 3.18c + else { - var showDirMark = false; - if ((typeof(bbs.curdir_code) == "string") && (bbs.curdir_code != "")) - showDirMark = (bbs.curdir_code == this.areaChooser.lib_list[this.libIdx].dir_list[this.dirIdx].subdir_list[pSubdirIdx].code); - menuItemObj.text = (showDirMark ? "*" : " "); - var subdirDesc = this.areaChooser.lib_list[this.libIdx].dir_list[this.dirIdx].subdir_list[pSubdirIdx].description; - var subdirDirIdx = this.areaChooser.lib_list[this.libIdx].dir_list[this.dirIdx].subdir_list[pSubdirIdx].index; - var dirCode = this.areaChooser.lib_list[this.libIdx].dir_list[this.dirIdx].subdir_list[pSubdirIdx].code; - menuItemObj.text += format(this.areaChooser.fileDirListPrintfInfo[this.libIdx].printfStr, +(pSubdirIdx+1), - subdirDesc.substr(0, this.areaChooser.descFieldLen), - this.areaChooser.fileDirListPrintfInfo[this.libIdx].fileCountsByCode[dirCode]); - menuItemObj.text = strip_ctrl(menuItemObj.text); - menuItemObj.retval = pSubdirIdx; + var dirIdx = this.dirHeirarchyObj[pItemIdx].dirObj.index; + numItems = this.areaChooser.fileDirListPrintfInfo[this.dirHeirarchyObj[pItemIdx].libIdx].fileCounts[dirIdx]; } - - return menuItemObj; } - // Set the currently selected item. If the current directory is in this list, - // then set the selected item to that; otherwise, the selected item should be - // the first directory. - if (file_area.dir[bbs.curdir_code].lib_index == pLibIdx) + // Menu item text + menuItemObj.text = (showDirMark ? "*" : " "); + if (this.allowANSI) { - for (var i = 0; i < this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list.length; ++i) - { - if (bbs.curdir_code == this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list[i].code) - { - fileDirMenu.selectedItemIdx = i; - if (fileDirMenu.selectedItemIdx >= fileDirMenu.topItemIdx+fileDirMenu.GetNumItemsPerPage()) - fileDirMenu.topItemIdx = fileDirMenu.selectedItemIdx - fileDirMenu.GetNumItemsPerPage() + 1; - break; - } - } + menuItemObj.text += format(this.areaChooser.fileDirListPrintfInfo[this.dirHeirarchyObj[pItemIdx].libIdx].printfStr, pItemIdx+1, + areaDesc.substr(0, this.areaChooser.descFieldLen), numItems); } - } - } - else - { - fileDirMenu.NumItems = function() { - return file_area.lib_list[this.libIdx].dir_list.length; - }; - fileDirMenu.GetItem = function(pDirIdx) { - var menuItemObj = this.MakeItemWithRetval(-1); - if ((pDirIdx >= 0) && (pDirIdx < file_area.lib_list[this.libIdx].dir_list.length)) + else { - var showDirMark = false; - if ((typeof(bbs.curdir_code) == "string") && (bbs.curdir_code != "")) - showDirMark = ((this.libIdx == file_area.dir[bbs.curdir_code].lib_index) && (pDirIdx == file_area.dir[bbs.curdir_code].index)); - menuItemObj.text = (showDirMark ? "*" : " "); - menuItemObj.text += format(this.areaChooser.fileDirListPrintfInfo[this.libIdx].printfStr, +(pDirIdx+1), - file_area.lib_list[this.libIdx].dir_list[pDirIdx].description.substr(0, this.areaChooser.descFieldLen), - this.areaChooser.fileDirListPrintfInfo[this.libIdx].fileCounts[pDirIdx]); - menuItemObj.text = strip_ctrl(menuItemObj.text); - menuItemObj.retval = pDirIdx; + // No ANSI - Numbered mode + var numSpaces = retObj.itemNumWidth - this.numItemsLen - console.strlen(menuItemObj.text); + if (numSpaces > 0) + areaDesc = format("%*s", numSpaces, "") + areaDesc; + // TODO: In a library with mixed items that are a subdir or have more items (such + // as my BBS files), the ones with the items (subdirs) didn't have the correct length + // - The description length for the ones with subdirs is 1 too long, but the ones with + // items are good. But it seems fixed now with adjustDesclen not being set to true..? + var descWidth = console.screen_columns - this.numItemsLen - retObj.numItemsWidth - 2; + if (numSpaces > 0) + { + //descWidth -= (retObj.itemNumWidth - numSpaces); + descWidth -= numSpaces; + } + if (adjustDescLen || pHeirarchyLevel >= 3 || this.numItemsLen == 1) // Not sure why the heirarchy level matters here + ++descWidth; + var formatStr = this.areaChooser.GetPrintfStrForDirWithoutAreaNum(descWidth, retObj.numItemsWidth); + menuItemObj.text += format(formatStr, areaDesc.substr(0, descWidth), numItems); + + // TODO: Adjust/set colors for the menu item? + /* + //var itemColorInfo = this.areaChooser.GetColorIndexInfoForLightbarMenu(this.dirHeirarchyObj, null, this.descFieldLen+1); + var itemColorInfo = this.areaChooser.GetColorIndexInfoForLightbarMenu(this.dirHeirarchyObj, retObj.numItemsWidth, retObj.descWidth+1); + menuItemObj.itemColor = itemColorInfo.itemColor; + menuItemObj.itemSelectedColor = itemColorInfo.selectedItemColor; + */ } - + menuItemObj.text = strip_ctrl(menuItemObj.text); + menuItemObj.retval = pItemIdx; return menuItemObj; }; - - // Set the currently selected item. If the current directory is in this list, - // then set the selected item to that; otherwise, the selected item should be - // the first directory. - if (file_area.dir[bbs.curdir_code].lib_index == pLibIdx) + + // Set the currently selected item + var selectedIdx = fileDirMenu.idxWithUserSelectedDir; + if (typeof(pSelectedItemIdx) === "number" && pSelectedItemIdx >= 0 && pSelectedItemIdx < fileDirMenu.NumItems()) + selectedIdx = pSelectedItemIdx; + if (selectedIdx >= 0 && selectedIdx < fileDirMenu.NumItems()) { - fileDirMenu.selectedItemIdx = file_area.dir[bbs.curdir_code].index; + fileDirMenu.selectedItemIdx = selectedIdx; if (fileDirMenu.selectedItemIdx >= fileDirMenu.topItemIdx+fileDirMenu.GetNumItemsPerPage()) fileDirMenu.topItemIdx = fileDirMenu.selectedItemIdx - fileDirMenu.GetNumItemsPerPage() + 1; } - else - { - fileDirMenu.selectedItemIdx = 0; - fileDirMenu.topItemIdx = 0; - } } - return fileDirMenu; + retObj.menuObj = fileDirMenu; + return retObj; +} +// Helper for DDFileAreaChooser_CreateLightbarMenu(): Returns arrays of objects with start, end, and attrs properties +// for the lightbar menu to add colors to the menu items. +// +// Parameters: +// pDirHeirarchyObj: An object from this.lib_list, which is set up with a +// 'name' property and either an 'items' property if it +// has sub-items or a 'dirObj' property if it's a file +// directory +// pNumItemsWidthOverride: Optional - An override for the width of the # items column. Mainly for the traditional (non-lightbar) UI +// If this is not specified/null, then this.areaNumLen will be used. +// pDescWidthOverride: Optional - Description width override +function DDFileAreaChooser_GetColorIndexInfoForLightbarMenu(pDirHeirarchyObj, pNumItemsWidthOverride, pDescWidthOverride) +{ + var retObj = { + fileDirListIdxes: {}, // Start & end indexes for the various items in each item list row + itemColor: [], // Normal item color array + selectedItemColor: [] // Selected item color array + }; + + var areaNumLen = this.areaNumLen; + if (typeof(pNumItemsWidthOverride) === "number" && pNumItemsWidthOverride >= 0) + areaNumLen = pNumItemsWidthOverride; + var descLen = this.descFieldLen; + if (typeof(pDescWidthOverride) === "number" && pDescWidthOverride >= 0) + descLen = pDescWidthOverride; + + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) + { + retObj.fileDirListIdxes = { + markCharStart: 0, + markCharEnd: 1, + itemNumStart: 1, + itemNumEnd: 3 + areaNumLen + }; + retObj.fileDirListIdxes.descStart = retObj.fileDirListIdxes.itemNumEnd; + retObj.fileDirListIdxes.descEnd = retObj.fileDirListIdxes.descStart + descLen + 1; + retObj.fileDirListIdxes.numItemsStart = retObj.fileDirListIdxes.descEnd; + // Set numItemsEnd to -1 to let the whole rest of the lines be colored + retObj.fileDirListIdxes.numItemsEnd = -1; + // Item color arrays + retObj.itemColor = [{start: retObj.fileDirListIdxes.markCharStart, end: retObj.fileDirListIdxes.markCharEnd, attrs: this.colors.areaMark}, + {start: retObj.fileDirListIdxes.itemNumStart, end: retObj.fileDirListIdxes.itemNumEnd, attrs: this.colors.areaNum}, + {start: retObj.fileDirListIdxes.descStart, end: retObj.fileDirListIdxes.descEnd, attrs: this.colors.desc}, + {start: retObj.fileDirListIdxes.numItemsStart, end: retObj.fileDirListIdxes.numItemsEnd, attrs: this.colors.numItems}], + retObj.selectedItemColor = [{start: retObj.fileDirListIdxes.markCharStart, end: retObj.fileDirListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, + {start: retObj.fileDirListIdxes.itemNumStart, end: retObj.fileDirListIdxes.itemNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight}, + {start: retObj.fileDirListIdxes.descStart, end: retObj.fileDirListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, + {start: retObj.fileDirListIdxes.numItemsStart, end: retObj.fileDirListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}] + } + else + { + retObj.fileDirListIdxes = { + markCharStart: 0, + markCharEnd: 1, + descStart: 1, + }; + retObj.fileDirListIdxes.descEnd = 2 + descLen; + retObj.fileDirListIdxes.numItemsStart = retObj.fileDirListIdxes.descEnd; + // Set numItemsEnd to -1 to let the whole rest of the lines be colored + retObj.fileDirListIdxes.numItemsEnd = -1; + // Item color arrays + retObj.itemColor = [{start: retObj.fileDirListIdxes.markCharStart, end: retObj.fileDirListIdxes.markCharEnd, attrs: this.colors.areaMark}, + {start: retObj.fileDirListIdxes.descStart, end: retObj.fileDirListIdxes.descEnd, attrs: this.colors.desc}, + {start: retObj.fileDirListIdxes.numItemsStart, end: retObj.fileDirListIdxes.numItemsEnd, attrs: this.colors.numItems}], + retObj.selectedItemColor = [{start: retObj.fileDirListIdxes.markCharStart, end: retObj.fileDirListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, + {start: retObj.fileDirListIdxes.descStart, end: retObj.fileDirListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, + {start: retObj.fileDirListIdxes.numItemsStart, end: retObj.fileDirListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}] + } + + return retObj; +} + +// For the DDFileAreaChooser class: Displays a header line for use above the menu for items that +// contain a number of items (sub-boards or files) +function DDFileAreaChooser_DisplayMenuHdrWithNumItems(pItemNumLen, pDescLen, pNumItemsLen, pItemsColumnLabel) +{ + console.attributes = 0; + var itemNumLabel = (pItemNumLen >= 5 ? "Item#" : "#"); + var formatStr = "%s%" + pItemNumLen + "s %-" + pDescLen + "s %" + pNumItemsLen + "s"; + printf(formatStr, this.colors.header, itemNumLabel, "Description", "# " + pItemsColumnLabel); } +// For the DDFileAreaChooser class: Displays the key help line at the bottom of the screen function DDFileAreaChooser_writeKeyHelpLine() { console.gotoxy(1, console.screen_rows); @@ -1835,7 +1187,7 @@ function DDFileAreaChooser_ReadConfigFile() // pLightbar: Boolean - Whether or not to show lightbar help. If // false, then this function will show regular help. // pClearScreen: Boolean - Whether or not to clear the screen first -function DDFileAreaChooser_showHelpScreen(pLightbar, pClearScreen) +function DDFileAreaChooser_ShowHelpScreen(pLightbar, pClearScreen) { if (pClearScreen) console.clear("\x01n"); @@ -1965,8 +1317,6 @@ function DDFileAreaChooser_buildFileDirPrintfInfoForLib(pLibIndex) // so that it's valid. if (this.useDirCollapsing) { - // Ensure this.lib_list is set up - this.SetUpLibListWithCollapsedDirs(); this.fileDirListPrintfInfo[pLibIndex].fileCounts = []; this.fileDirListPrintfInfo[pLibIndex].fileCountsByCode = {}; for (var dirIdx = 0; dirIdx < this.lib_list[pLibIndex].dir_list.length; ++dirIdx) @@ -1992,10 +1342,8 @@ function DDFileAreaChooser_buildFileDirPrintfInfoForLib(pLibIndex) // Set the description field length and printf strings for // this file library - this.fileDirListPrintfInfo[pLibIndex].descFieldLen = - console.screen_columns - this.areaNumLen - - this.fileDirListPrintfInfo[pLibIndex].numFilesLen - 5; - this.fileDirListPrintfInfo[pLibIndex].printfStr = + this.fileDirListPrintfInfo[pLibIndex].descFieldLen = console.screen_columns - this.areaNumLen - this.fileDirListPrintfInfo[pLibIndex].numFilesLen - 5; + this.fileDirListPrintfInfo[pLibIndex].printfStr = this.colors.areaNum + " %" + this.areaNumLen + "d " + this.colors.desc + "%-" + this.fileDirListPrintfInfo[pLibIndex].descFieldLen @@ -2008,9 +1356,36 @@ function DDFileAreaChooser_buildFileDirPrintfInfoForLib(pLibIndex) + this.fileDirListPrintfInfo[pLibIndex].descFieldLen + "s " + this.colors.numItemsHighlight + "%" + this.fileDirListPrintfInfo[pLibIndex].numFilesLen +"d\x01n"; + this.fileDirListPrintfInfo[pLibIndex].printfStrWithoutAreaNum = + this.colors.desc + "%-" + + (this.fileDirListPrintfInfo[pLibIndex].descFieldLen+2) + + "s " + this.colors.numItems + "%" + + this.fileDirListPrintfInfo[pLibIndex].numFilesLen + "d"; + this.fileDirListPrintfInfo[pLibIndex].highlightPrintfStr = + "\x01n" + this.colors.bkgHighlight + + this.colors.areaNumHighlight + " %" + this.areaNumLen + + "d " + this.colors.descHighlight + "%-" + + this.fileDirListPrintfInfo[pLibIndex].descFieldLen + + "s " + this.colors.numItemsHighlight + "%" + + this.fileDirListPrintfInfo[pLibIndex].numFilesLen +"d\x01n"; } } + // Returns a printf string for an area item without an area number; mainly for + // the traditional/non-lightbar interface, which uses the menu in numbered mode + // + // Parameters: + // pDescLen: The length to use for the description + // pNumItemsLen: The lenggth of the field for the number of items in the area (right side) + // + // Return value: The printf string for the item +function DDFileAreaChooser_GetPrintfStrForDirWithoutAreaNum(pDescLen, pNumItemsLen) +{ + var printfStr = "\x01n" + this.colors.desc + "%-" + pDescLen + "s " + this.colors.numItems + "%" + + pNumItemsLen + "d"; + return printfStr; +} + // For the DDFileAreaChooser class: Displays the area chooser header // // Parameters: @@ -2082,303 +1457,42 @@ function DDFileAreaChooser_WriteLightbarKeyHelpErrorMsg(pErrorMsg, pRefreshHelpL console.gotoxy(1, console.screen_rows); console.print("\x01y\x01h" + pErrorMsg + "\x01n"); mswait(ERROR_WAIT_MS); - if (pRefreshHelpLine) + if (pRefreshHelpLine && this.useLightbarInterface && console.term_supports(USER_ANSI)) this.WriteKeyHelpLine(); } -// For the DDFileAreaChooser class: Sets up the lib_list array according to -// the file directory collapse separator -function DDFileAreaChooser_SetUpLibListWithCollapsedDirs() -{ - if (this.lib_list.length == 0) - { - // Copy some of the information from file_area.lib_list - for (var libIdx = 0; libIdx < file_area.lib_list.length; ++libIdx) - { - // Go through dir_list in the curent file library, and for - // any that have the collapse separator, add only one copy - // of the name to the dir_list property for this library. - // First, we'll have to see if multiple directories with - // the collapse separator have the same prefix. - // dirDescs is an object indexed by directory description, - // and the value will be how many times it was seen. - var dirDescs = {}; - // First, count the number of directories that have the separator. - // If all of the group's directories have the separator, then we - // won't collapse the directories. - var numDirsWithSeparator = 0; - for (var dirIdx = 0; dirIdx < file_area.lib_list[libIdx].dir_list.length; ++dirIdx) - { - // Check to see if the name before the separator is the same as the - // file library name, and only count it if it's not. - var dirDesc = file_area.lib_list[libIdx].dir_list[dirIdx].description; - var sepIdx = dirDesc.indexOf(this.dirCollapseSeparator); - if (sepIdx > 1) - { - var dirDescBeforeSep = file_area.lib_list[libIdx].dir_list[dirIdx].description.substr(0, sepIdx); - if (dirDescBeforeSep != file_area.lib_list[libIdx].dir_list[dirIdx].name) - ++numDirsWithSeparator; - } - } - // Whether or not to use directory collapsing for this file library - var collapseThisLib = (numDirsWithSeparator > 0 && numDirsWithSeparator < file_area.lib_list[libIdx].dir_list.length); - for (var dirIdx = 0; dirIdx < file_area.lib_list[libIdx].dir_list.length; ++dirIdx) - { - var dirDesc = file_area.lib_list[libIdx].dir_list[dirIdx].description; - if (collapseThisLib) - { - var sepIdx = dirDesc.indexOf(this.dirCollapseSeparator); - if (sepIdx > -1) - dirDesc = truncsp(dirDesc.substr(0, sepIdx)); // Remove trailing whitespace - } - if (dirDescs.hasOwnProperty(dirDesc)) - dirDescs[dirDesc] += 1; - else - dirDescs[dirDesc] = 1; - } - - // Create an initial file library object for this file library (except - // for dir_list, which we will build ourselves for this library) - var libObj = defaultFileLibObj(); - for (var prop in libObj) - { - if (prop != "dir_list") - libObj[prop] = file_area.lib_list[libIdx][prop]; - } - - // Go through the dirs in this library again. For each directory: - // If its whole description exists in dirDescs, then just add it to - // the dir_list array. Otherwise: - // If a collapse seprator exists, then split on that and see if the - // prefix was seen more than once. If so, then add the separate - // subdirs as their own items in the dir_list array. Otherwise, - // add the single directory to the dir_list array with the whole - // description. - var addedPrefixDescriptionDirs = {}; - for (var dirIdx = 0; dirIdx < file_area.lib_list[libIdx].dir_list.length; ++dirIdx) - { - var dirDesc = file_area.lib_list[libIdx].dir_list[dirIdx].description; - if (dirDescs.hasOwnProperty(dirDesc)) - libObj.dir_list.push(dirObjFromOfficialDir(libIdx, dirIdx)); - else - { - var subdirDesc = ""; - var sepIdx = dirDesc.indexOf(this.dirCollapseSeparator); - if (sepIdx > -1) - { - // dirDesc here will now be the shortened middle one before the separator character - dirDesc = truncsp(dirDesc.substr(0, sepIdx)); // Remove trailing whitespace - // If the directory description before the name separator character is different - // from the file library name, then add it with sub-subdirectories - if (dirDesc != file_area.lib_list[libIdx].name) - { - // If it has been seen more than once, then the description should - // be the prefix description - if (dirDescs[dirDesc] > 1) - { - // Find the file directory with a description matching dirDesc - var addedDirIdx = -1; //libObj.dir_list.length - 1; - for (var i = 0; i < libObj.dir_list.length && addedDirIdx == -1; ++i) - { - if (libObj.dir_list[i].description == dirDesc) - addedDirIdx = i; - } - if (!addedPrefixDescriptionDirs.hasOwnProperty(dirDesc)) - { - // Add it to dir_list - libObj.dir_list.push(dirObjFromOfficialDir(libIdx, dirIdx)); - addedDirIdx = libObj.dir_list.length - 1; - libObj.dir_list[addedDirIdx].description = dirDesc; - addedPrefixDescriptionDirs[dirDesc] = true; - } - // Add the subdirectory to the directory's subdirectory list - // Using skipsp() to strip leading whitespace - subdirDesc = skipsp(file_area.lib_list[libIdx].dir_list[dirIdx].description.substr(sepIdx+1)); - libObj.dir_list[addedDirIdx].subdir_list.push({ - description: subdirDesc, - code: file_area.lib_list[libIdx].dir_list[dirIdx].code, - index: file_area.lib_list[libIdx].dir_list[dirIdx].index, - lib_index: file_area.lib_list[libIdx].dir_list[dirIdx].lib_index - }); - } - else // Add it with the full description - libObj.dir_list.push(dirObjFromOfficialDir(libIdx, dirIdx)); - } - else - { - // The directory name before the name separator character is the same as the file - // library name. Add it as its own single subdirectory. - libObj.dir_list.push(dirObjFromOfficialDir(libIdx, dirIdx)); - } - } - if (dirDescs.hasOwnProperty(dirDesc)) - dirDescs[dirDescs] += 1; - else - dirDescs[dirDescs] = 1; - } - } - - this.lib_list.push(libObj); - } - } -} -// Returns a default object for a file library -function defaultFileLibObj() -{ - return { - index: 0, - number: 0, - name: "", - description: "", - ars: 0, - dir_list: [] - }; -} - -// Returns a default object for a file directory. It can potentially -// contain its own list of subdirectories. -function defaultFileDirObj() -{ - return { - index: 0, - number: 0, - lib_index: 0, - lib_number: 0, - lib_name: 0, - code: "", - name: "", - lib_name: "", - description: "", - ars: 0, - settings: 0, - subdir_list: [] - }; -} -function dirObjFromOfficialDir(pLibIdx, pDirIdx) -{ - var dirObj = defaultFileDirObj(); - for (var prop in dirObj) - { - if (prop != "subdir_list") // Doesn't exist in the official dir objects - dirObj[prop] = file_area.lib_list[pLibIdx].dir_list[pDirIdx][prop]; - } - return dirObj; -} - -// For the DDFileAreaChooser class: Finds a file directory index with search text, matching either the name or +// Finds a file area index with search text, matching either the name or // description, case-insensitive. // // Parameters: -// pGrpIdx: The index of the file library +// pFileAreaStructure: The file area structure from this.msgArea_list to search through // pSearchText: The name/description text to look for // pStartItemIdx: The item index to start at. Defaults to 0 -// pLevel: Level (only if using directory collapsing): 2 = directories, and 3 = subdirectories -// pDirIdx: If level 3 (subdirs), this specifies the directory index // -// Return value: The index of the file directory, or -1 if not found -function DDFileAreaChooser_FindFileDirIdxFromText(pLibIdx, pSearchText, pStartItemIdx, pLevel, pDirIdx) +// Return value: The index of the file area, or -1 if not found +function DDFileAreaChooser_FindFileAreaIdxFromText(pFileAreaStructure, pSearchText, pStartItemIdx) { - if (typeof(pLibIdx) != "number") - return -1; if (typeof(pSearchText) != "string") return -1; - var fileDirIdx = -1; - - var startIdx = (typeof(pStartItemIdx) == "number" ? pStartItemIdx : 0); + var areaIdx = -1; - // Temporary - //if (user.is_sysop) console.print("\x01n\r\npDir collapsing: " + this.useDirCollapsing + "; Level, pDirIdx: " + pLevel + ", " + pDirIdx + "\r\n\x01p"); - // End Temporary + var startIdx = (typeof(pStartItemIdx) === "number" ? pStartItemIdx : 0); + if ((startIdx < 0) || (startIdx > pFileAreaStructure.length)) + startIdx = 0; - // Go through the message group list and look for a match + // Go through the message area list and look for a match var searchTextUpper = pSearchText.toUpperCase(); - var continueOn = true; - if (this.useDirCollapsing) - { - if (typeof(pDirIdx) !== "number") - return -1; - if (pDirIdx < 0 || pDirIdx >= this.lib_list[pLibIdx].dir_list.length) - return -1; - if (typeof(pLevel) === "number") - { - if (pLevel == 2) - { - if ((startIdx < 0) || (startIdx >= this.lib_list[pLibIdx].dir_list.length)) - startIdx = 0; - while (continueOn && fileDirIdx == -1) - { - for (var i = startIdx; i < this.lib_list[pLibIdx].dir_list.length; ++i) - { - if ((this.lib_list[pLibIdx].dir_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) || - (this.lib_list[pLibIdx].dir_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1)) - { - fileDirIdx = i; - continueOn = false; - break; - } - } - if (fileDirIdx == -1) - { - if (startIdx > 0) - startIdx = 0; - else - continueOn = false; - } - } - } - else if (pLevel == 3) - { - if ((startIdx < 0) || (startIdx >= this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list.length)) - startIdx = 0; - while (continueOn && fileDirIdx == -1) - { - for (var i = startIdx; i < this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list.length; ++i) - { - if ((this.lib_list[pLibIdx].dir_list[pDirIdx].subdir_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1)) - { - fileDirIdx = i; - continueOn = false; - break; - } - } - if (fileDirIdx == -1) - { - if (startIdx > 0) - startIdx = 0; - else - continueOn = false; - } - } - } - } - } - else + for (var i = startIdx; i < pFileAreaStructure.length; ++i) { - if ((startIdx < 0) || (startIdx >= file_area.lib_list[pLibIdx].dir_list.length)) - startIdx = 0; - while (continueOn && fileDirIdx == -1) + if (pFileAreaStructure[i].name.toUpperCase().indexOf(searchTextUpper) > -1 || pFileAreaStructure[i].altName.toUpperCase().indexOf(searchTextUpper) > -1) { - for (var i = startIdx; i < file_area.lib_list[pLibIdx].dir_list.length; ++i) - { - if ((file_area.lib_list[pLibIdx].dir_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) || - (file_area.lib_list[pLibIdx].dir_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1)) - { - fileDirIdx = i; - break; - } - } - if (fileDirIdx == -1) - { - if (startIdx > 0) - startIdx = 0; - else - continueOn = false; - } + areaIdx = i; + break; } } - return fileDirIdx; + return areaIdx; } // Removes multiple, leading, and/or trailing spaces @@ -2461,34 +1575,12 @@ function DDFileAreaChooser_GetGreatestNumFiles(pLibIndex) fileCountsByCode: {} } - if (this.useDirCollapsing) - { - // Ensure this.lib_list is set up - this.SetUpLibListWithCollapsedDirs(); - - retObj.fileCounts = new Array(this.lib_list[pLibIndex].dir_list.length); - for (var dirIndex = 0; dirIndex < this.lib_list[pLibIndex].dir_list.length; ++dirIndex) - { - if (this.lib_list[pLibIndex].dir_list[dirIndex].subdir_list.length > 0) - retObj.fileCounts[dirIndex] = this.lib_list[pLibIndex].dir_list[dirIndex].subdir_list.length; - else - { - var dirCode = this.lib_list[pLibIndex].dir_list[dirIndex].code; - retObj.fileCounts[dirIndex] = numFilesInDir(file_area.dir[dirCode].lib_index, file_area.dir[dirCode].index); - } - if (retObj.fileCounts[dirIndex] > retObj.greatestNumFiles) - retObj.greatestNumFiles = retObj.fileCounts[dirIndex]; - } - } - else + retObj.fileCounts = new Array(file_area.lib_list[pLibIndex].dir_list.length); + for (var dirIndex = 0; dirIndex < file_area.lib_list[pLibIndex].dir_list.length; ++dirIndex) { - retObj.fileCounts = new Array(file_area.lib_list[pLibIndex].dir_list.length); - for (var dirIndex = 0; dirIndex < file_area.lib_list[pLibIndex].dir_list.length; ++dirIndex) - { - retObj.fileCounts[dirIndex] = numFilesInDir(pLibIndex, dirIndex); - if (retObj.fileCounts[dirIndex] > retObj.greatestNumFiles) - retObj.greatestNumFiles = retObj.fileCounts[dirIndex]; - } + retObj.fileCounts[dirIndex] = numFilesInDir(pLibIndex, dirIndex); + if (retObj.fileCounts[dirIndex] > retObj.greatestNumFiles) + retObj.greatestNumFiles = retObj.fileCounts[dirIndex]; } // Populate the fileCountsByCode dictionary for the given file library @@ -2501,65 +1593,6 @@ function DDFileAreaChooser_GetGreatestNumFiles(pLibIndex) return retObj; } -// Inputs a keypress from the user and handles some ESC-based -// characters such as PageUp, PageDown, and ESC. If PageUp -// or PageDown are pressed, this function will return the -// string "\x01PgUp" (KEY_PAGE_UP) or "\x01Pgdn" (KEY_PAGE_DOWN), -// respectively. Also, F1-F5 will be returned as "\x01F1" -// through "\x01F5", respectively. -// Thanks goes to Psi-Jack for the original impementation -// of this function. -// -// Parameters: -// pGetKeyMode: Optional - The mode bits for console.getkey(). -// If not specified, K_NONE will be used. -// -// Return value: The user's keypress -function getKeyWithESCChars(pGetKeyMode) -{ - var getKeyMode = K_NONE; - if (typeof(pGetKeyMode) == "number") - getKeyMode = pGetKeyMode; - - var userInput = console.getkey(getKeyMode); - if (userInput == KEY_ESC) { - switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) { - case '[': - switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) { - case 'V': - userInput = KEY_PAGE_UP; - break; - case 'U': - userInput = KEY_PAGE_DOWN; - break; - } - break; - case 'O': - switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) { - case 'P': - userInput = "\x01F1"; - break; - case 'Q': - userInput = "\x01F2"; - break; - case 'R': - userInput = "\x01F3"; - break; - case 'S': - userInput = "\x01F4"; - break; - case 't': - userInput = "\x01F5"; - break; - } - default: - break; - } - } - - return userInput; -} - // Loads a text file (an .ans or .asc) into an array. This will first look for // an .ans version, and if exists, convert to Synchronet colors before loading // it. If an .ans doesn't exist, this will look for an .asc version. @@ -2974,3 +2007,192 @@ function findNextLibIdxWithDirs(pLibIdx) } return nextLibIdx; } + +// Creates and returns an array which is a sort of recursive array structure +// of objects. Each entry in the array will be an object with a 'name' property +// and either an 'items' property if it has sub-items or a 'dirObj' property +// if it's a Synchronet file directory. The last item in the array chain +// will have the 'dirObj' property. +// +// Parameters: +// pCollapsing: Boolean - Whether or not to use directory collapsing +// pCollapsingSeparator: The separator used to split file lib/dir names when using collapsing +function getFileDirHeirarchy(pCollapsing, pCollapsingSeparator) +{ + var fileDirHeirarchy = []; + if (pCollapsing) + { + // For each library, go through each directory + for (var libIdx = 0; libIdx < file_area.lib_list.length; ++libIdx) + { + for (var dirIdx = 0; dirIdx < file_area.lib_list[libIdx].dir_list.length; ++dirIdx) + { + // 1. Join the library name & directory name separated by a colon + // 2. Split on colons into an array (of names) + // 3. Go through the array of names and build the appropriate structure in fileDirHeirarchy. + // TODO: Initially, I thought of having collapsing restricted to + // areas where there are no spaces before or after the :, but that + // isn't how I did it before.. + /* + var libAndDirName = file_area.lib_list[libIdx].description + ":" + file_area.lib_list[libIdx].dir_list[dirIdx].description; + var nameArray = splitStrNoSpacesBeforeSeparator(libAndDirName, pCollapsingSeparator); + */ + var libDesc = skipsp(truncsp(file_area.lib_list[libIdx].description)); + var dirDesc = skipsp(truncsp(file_area.lib_list[libIdx].dir_list[dirIdx].description)); + var libAndDirName = libDesc + ":" + dirDesc; + var nameArray = libAndDirName.split(pCollapsingSeparator); + var arrayToSearch = fileDirHeirarchy; + for (var i = 0; i < nameArray.length; ++i) + { + var name = skipsp(truncsp(nameArray[i])); + // Look for this one in the heirarchy; if not found, add it. + // Look for an entry in the array that matches the name and has its own "items" array + var heirarchyIdx = -1; + for (var j = 0; j < arrayToSearch.length; ++j) + { + if (arrayToSearch[j].name == name && arrayToSearch[j].hasOwnProperty("items")) + { + heirarchyIdx = j; + break; + } + } + if (heirarchyIdx > -1) + arrayToSearch = arrayToSearch[heirarchyIdx].items; + else + { + // If we're at the last name, add a dirObj item; otherwise, add + // an items array. + if (i == nameArray.length - 1) + { + arrayToSearch.push( + { + name: name, + altName: file_area.lib_list[libIdx].dir_list[dirIdx].name, + libIdx: libIdx, + dirObj: file_area.lib_list[libIdx].dir_list[dirIdx] + }); + } + else + { + arrayToSearch.push( + { + name: name, + altName: name, + libIdx: libIdx, + items: [] + }); + arrayToSearch = arrayToSearch[arrayToSearch.length-1].items; + } + } + } + } + } + } + else + { + // No collapsing. Have fileDirHeirarchy match the lib/dir structure + // configured on the BBS. + for (var libIdx = 0; libIdx < file_area.lib_list.length; ++libIdx) + { + fileDirHeirarchy.push( + { + name: file_area.lib_list[libIdx].description, + libIdx: libIdx, + items: [] + }); + var libIdxInHeirarchy = fileDirHeirarchy.length - 1; + for (var dirIdx = 0; dirIdx < file_area.lib_list[libIdx].dir_list.length; ++dirIdx) + { + fileDirHeirarchy[libIdxInHeirarchy].items.push( + { + name: file_area.lib_list[libIdx].dir_list[dirIdx].description, + libIdx: libIdx, + dirObj: file_area.lib_list[libIdx].dir_list[dirIdx] + }); + } + } + } + return fileDirHeirarchy; +} + +// Splits a string on a separator, except when there's a space after a +// separator; if there's a space after a the separator, both parts of the +// string before & after the separator are included as one item in the +// resulting array. For instance, "Magic: The Gathering:Information" +// would be split up such that "Magic: The Gathering" would be one string +// and "Information" would be another, whereas "Mirror:Simtel:DOS:Games" +// would all be split up into 4 strings +function splitStrNoSpacesBeforeSeparator(pStr, pSep) +{ + var strArray = []; + var splitArray = pStr.split(pSep); + for (var i = 0; i < splitArray.length; ++i) + { + if (i < splitArray.length-1 && splitArray[i+1].indexOf(" ") == 0) + strArray.push(splitArray[i] + ":" + splitArray[++i]); + else + strArray.push(splitArray[i]); + } + return strArray; +} + +// Given a file lib/directory heirarchy object built by this module, this +// function returns whether it contains the user's currently selected file +// directory. +// +// Parameters: +// pDirHeirarchyObj: An object from this.lib_list, which is +// set up with a 'name' property and either +// an 'items' property if it has sub-items +// or a 'dirObj' property if it's a file +// directory +// +// Return value: Whether or not the given structure has the user's currently selected file directory +function fileDirStructureHasCurrentUserFileDir(pDirHeirarchyObj) +{ + var currentUserFileDirFound = false; + if (Array.isArray(pDirHeirarchyObj)) + { + // This could be the top-level array or one of the 'items' properties, which is an array. + // Go through the array and call this function again recursively; this function will + // return when we get to an actual file directory that is the user's current selection. + for (var i = 0; i < pDirHeirarchyObj.length && !currentUserFileDirFound; ++i) + currentUserFileDirFound = fileDirStructureHasCurrentUserFileDir(pDirHeirarchyObj[i]); + } + else + { + // This is one of the objects with 'name' and an 'items' or 'dirObj' + if (pDirHeirarchyObj.hasOwnProperty("dirObj")) + currentUserFileDirFound = (bbs.curdir_code == pDirHeirarchyObj.dirObj.code); + else if (pDirHeirarchyObj.hasOwnProperty("items")) + currentUserFileDirFound = fileDirStructureHasCurrentUserFileDir(pDirHeirarchyObj.items); + } + return currentUserFileDirFound; +} + +// Given an array of file directory heirarchy objects, this returns the length of the +// longest number of items in the heirarchy - if there are any with an "items" array. +// If there are no items with their own "items" array, this will return 0. +// +// Parameters: +// pDirHeirarchyObj: An object from this.lib_list, which is +// set up with a 'name' property and either +// an 'items' property if it has sub-items +// or a 'dirObj' property if it's a file +// directory +// +// Return value: The length of the longest number of items in the heirarchy +function maxNumItemsWidthInHeirarchy(pDirHeirarchyObj) +{ + var maxNumItemsWidth = 0; + for (var i = 0; i < pDirHeirarchyObj.length; ++i) + { + if (pDirHeirarchyObj[i].hasOwnProperty("items")) + { + var numItemsLen = pDirHeirarchyObj[i].items.length.toString().length; + if (numItemsLen > maxNumItemsWidth) + maxNumItemsWidth = numItemsLen; + } + } + return maxNumItemsWidth; +} \ No newline at end of file diff --git a/xtrn/DDAreaChoosers/DDMsgAreaChooser.js b/xtrn/DDAreaChoosers/DDMsgAreaChooser.js index 029d95e837a023b9bc23f69c920d427de29c7f85..4a5c04826ecbcb2d5678a32ee381b57474735354 100644 --- a/xtrn/DDAreaChoosers/DDMsgAreaChooser.js +++ b/xtrn/DDAreaChoosers/DDMsgAreaChooser.js @@ -74,14 +74,14 @@ * 2023-10-27 Eric Oulashin Version 1.41 * Lightbar mode: When using name collapisng, ensure the menu item for * the appropriate subgroup is selected. + * 2024-11-12 Eric Oulashin Version 1.42 Beta + * Started working on a change to sub-board collapsing to allow an + * arbitrary amount of separators in the group/sub names to + * create multiple levels of categories + * 2025-03-17 Eric Oulashin Version 1.42 + * Releasing this version */ -// TODO: In the area list, the 10,000ths digit (for # posts) is in a different color) - -// TODO: Passing "false" as the first command-line argument no longer works. -// That should allow choosing a sub-board within the user's current message -// group. - /* Command-line arguments: 1 (argv[0]): Boolean - Whether or not to choose a message group first (default). If false, the user will only be able to choose a different sub-board within @@ -117,8 +117,8 @@ if (system.version_num < 31400) } // Version & date variables -var DD_MSG_AREA_CHOOSER_VERSION = "1.41"; -var DD_MSG_AREA_CHOOSER_VER_DATE = "2023-10-27"; +var DD_MSG_AREA_CHOOSER_VERSION = "1.42"; +var DD_MSG_AREA_CHOOSER_VER_DATE = "2025-03-17"; // Keyboard input key codes var CTRL_H = "\x08"; @@ -127,23 +127,18 @@ var KEY_ENTER = CTRL_M; var BACKSPACE = CTRL_H; var CTRL_F = "\x06"; var KEY_ESC = ascii(27); -// PageUp & PageDown keys - Synchronet 3.17 as of about December 18, 2017 -// use CTRL-P and CTRL-N for PageUp and PageDown, respectively. sbbsdefs.js -// defines them as KEY_PAGEUP and KEY_PAGEDN; I've used slightly different names -// in this script so that this script will work with Synchronet systems before -// and after the update containing those key definitions. -var KEY_PAGE_UP = "\x10"; // Ctrl-P -var KEY_PAGE_DOWN = "\x0e"; // Ctrl-N -// Ensure KEY_PAGE_UP and KEY_PAGE_DOWN are set to what's defined in sbbs.js -// for KEY_PAGEUP and KEY_PAGEDN in case they change -if (typeof(KEY_PAGEUP) === "string") - KEY_PAGE_UP = KEY_PAGEUP; -if (typeof(KEY_PAGEDN) === "string") - KEY_PAGE_DOWN = KEY_PAGEDN; - -// Key codes for display + +// Characters for display var UP_ARROW = ascii(24); var DOWN_ARROW = ascii(25); +var BLOCK1 = "\xB0"; // Dimmest block +var BLOCK2 = "\xB1"; +var BLOCK3 = "\xB2"; +var BLOCK4 = "\xDB"; // Brightest block +var MID_BLOCK = ascii(254); +var TALL_UPPER_MID_BLOCK = "\xFE"; +var UPPER_CENTER_BLOCK = "\xDF"; +var LOWER_CENTER_BLOCK = "\xDC"; // Characters for display var HORIZONTAL_SINGLE = "\xC4"; @@ -275,7 +270,7 @@ function DDMsgAreaChooser() // set up in the BBS - The dir_list within that one would then // contain multiple sub-board split based on the dir collapse // separator. - this.group_list = []; + //this.msgArea_list = []; // Set the function pointers for the object this.ReadConfigFile = DDMsgAreaChooser_ReadConfigFile; @@ -283,19 +278,10 @@ function DDMsgAreaChooser() this.WriteGrpListHdrLine = DDMsgAreaChooser_WriteGrpListHdrLine; this.WriteSubBrdListHdr1Line = DMsgAreaChooser_WriteSubBrdListHdr1Line; this.SelectMsgArea = DDMsgAreaChooser_SelectMsgArea; - this.SelectMsgArea_Lightbar = DDMsgAreaChooser_SelectMsgArea_Lightbar; - this.CreateLightbarMsgGrpMenu = DDMsgAreaChooser_CreateLightbarMsgGrpMenu; - this.CreateLightbarSubBoardMenu = DDMsgAreaChooser_CreateLightbarSubBoardMenu; - this.SelectMsgArea_Traditional = DDMsgAreaChooser_SelectMsgArea_Traditional; - this.SelectSubBoard_Traditional = DDMsgAreaChooser_SelectSubBoard_Traditional; - this.SelectSubSubWithinSub_Traditional = DDMsgAreaChooser_SelectSubSubWithinSub_Traditional; - this.ListMsgGrps = DDMsgAreaChooser_ListMsgGrps_Traditional; - this.ListSubBoardsInMsgGroup = DDMsgAreaChooser_ListSubBoardsInMsgGroup_Traditional; - this.CurrentSubBoardIsInSubSubsForSub = DDMsgAreaChooser_CurrentSubBoardIsInSubSubsForSub; - this.GetSubBoardInfo = DDMsgAreaChooser_GetSubBoardInfo; - // Lightbar-specific functions - this.WriteMsgGroupLine = DDMsgAreaChooser_writeMsgGroupLine; - this.GetMsgSubBrdLine = DDMsgAreaChooser_GetMsgSubBrdLine; + this.CreateLightbarMenu = DDMsgAreaChooser_CreateLightbarMenu; + this.GetColorIndexInfoForLightbarMenu = DDMsgAreaChooser_GetColorIndexInfoForLightbarMenu; + this.GetSubBoardColorIndexInfoForLightbarMenu = DDMsgAreaChooser_GetSubBoardColorIndexInfoForLightbarMenu; + // TODO: Anything we can remove? // Help screen this.ShowHelpScreen = DDMsgAreaChooser_showHelpScreen; // Function to build the sub-board printf information for a message @@ -305,12 +291,15 @@ function DDMsgAreaChooser() this.DisplayListHdrLines = DDMsgAreaChooser_DisplayListHdrLines; this.WriteLightbarKeyHelpErrorMsg = DDMsgAreaChooser_WriteLightbarKeyHelpErrorMsg; this.SetUpGrpListWithCollapsedSubBoards = DDMsgAreaChooser_SetUpGrpListWithCollapsedSubBoards; - this.FindMsgGrpIdxFromText = DDMsgAreaChooser_FindMsgGrpIdxFromText; - this.FindSubBoardIdxFromText = DDMsgAreaChooser_FindSubBoardIdxFromText; + this.FindMsgAreaIdxFromText = DDMsgAreaChooser_FindMsgAreaIdxFromText; this.GetSubNameLenAndNumMsgsLen = DDMsgAreaChooser_GetSubNameLenAndNumMsgsLen; // Read the settings from the config file. this.ReadConfigFile(); + + // Populate msgArea_list (should be done after reading the configuration file + // so that useSubCollapsing and subCollapseSeparator are set according to settings + this.msgArea_list = getMsgSubHeirarchy(this.useSubCollapsing, this.subCollapseSeparator); // These variables store default lengths of the various columns displayed in // the message group/sub-board lists. @@ -328,10 +317,13 @@ function DDMsgAreaChooser() // printf strings for various things // Message group information (printf strings) - this.msgGrpListPrintfStr = "\x01n " + this.colors.areaNum + "%" + this.areaNumLen + this.msgGrpListPrintfStr = "\x01n" + this.colors.areaNum + "%" + this.areaNumLen + "d " + this.colors.desc + "%-" + this.msgGrpDescLen + "s " + this.colors.numItems + "%" + this.numItemsLen + "d"; + this.msgGrpListPrintfStrWithoutAreaNum = "\x01n" + this.colors.desc + "%-" + + this.msgGrpDescLen + "s " + this.colors.numItems + + "%" + this.numItemsLen + "d"; this.msgGrpListHilightPrintfStr = "\x01n" + this.colors.bkgHighlight + " " + "\x01n" + this.colors.bkgHighlight + this.colors.areaNumHighlight + "%" + this.areaNumLen @@ -464,6 +456,10 @@ function DDMsgAreaChooser_WriteGrpListHdrLine(pNumPages, pPageNum) // above the sub-board list for a message group. // // Parameters: +// pMsgAreaHeirarchyObj: An object from this.msgArea_list, which is +// set up with a 'name' property and either +// an 'items' property if it has sub-items +// or a 'subObj' property if it's a sub-board // pGrpIndex: The index of the message group (assumed to be valid) // pSubIndex: Optional - The index of a sub-board within the message group (if using sub-board // collapsing, we might want to append the sub-board name to the description) @@ -471,7 +467,7 @@ function DDMsgAreaChooser_WriteGrpListHdrLine(pNumPages, pPageNum) // not passed, then it won't be used. // pPageNum: The page number. This is optional; if this is not passed, // then it won't be used. -function DMsgAreaChooser_WriteSubBrdListHdr1Line(pGrpIndex, pSubIndex, pNumPages, pPageNum) +function DMsgAreaChooser_WriteSubBrdListHdr1Line(pMsgAreaHeirarchyObj, pGrpIndex, pNumPages, pPageNum) { var descLen = 25; var descFormatStr = "\x01n" + this.colors.subBoardHeader + "Sub-boards of \x01h%-" + descLen + "s \x01n" @@ -482,21 +478,21 @@ function DMsgAreaChooser_WriteSubBrdListHdr1Line(pGrpIndex, pSubIndex, pNumPages descFormatStr += "(Page " + pPageNum + ")"; else if ((typeof(pPageNum) !== "number") && (typeof(pNumPages) === "number")) descFormatStr += "(" + pNumPages + (pNumPages == 1 ? " page)" : " pages)"); - // If using sub-board collapsing, then build the description as needed. Otherwise, - // just use the sub-board description. + + // Ensure this.msgArea_list is set up + this.SetUpGrpListWithCollapsedSubBoards(); + var desc = ""; - if (this.useSubCollapsing) + if (Array.isArray(pMsgAreaHeirarchyObj) && pGrpIndex >= 0 && pGrpIndex < pMsgAreaHeirarchyObj.length) { - // Ensure this.group_list is set up - this.SetUpGrpListWithCollapsedSubBoards(); - // The description should be the library's description. Also, if pSubIdx - // is a number, then append the directory description to the description. - desc = this.group_list[pGrpIndex].description; - if ((typeof(pSubIndex) === "number") && (this.group_list[pGrpIndex].sub_list[pSubIndex].sub_subboard_list.length > 0)) - desc += this.subCollapseSeparator + " " + this.group_list[pGrpIndex].sub_list[pSubIndex].description; + if (pMsgAreaHeirarchyObj[pGrpIndex].hasOwnProperty("name")) + desc = pMsgAreaHeirarchyObj[pGrpIndex].name; + } + else if (pMsgAreaHeirarchyObj.hasOwnProperty("items") && pGrpIndex >= 0 && pGrpIndex < pMsgAreaHeirarchyObj.items.length) + { + if (pMsgAreaHeirarchyObj.items[pGrpIndex].hasOwnProperty("name")) + desc = pMsgAreaHeirarchyObj.items[pGrpIndex].name; } - else - desc = msg_area.grp_list[pGrpIndex].description; printf(descFormatStr, desc.substr(0, descLen)); console.cleartoeol("\x01n"); } @@ -514,1669 +510,1211 @@ function DMsgAreaChooser_WriteSubBrdListHdr1Line(pGrpIndex, pSubIndex, pNumPages // for SelectMsgArea(). function DDMsgAreaChooser_SelectMsgArea(pChooseGroup, pGrpIdx) { - // If sub-board collapsing is enabled, then set up - // this.group_list. - if (this.useSubCollapsing) - this.SetUpGrpListWithCollapsedSubBoards(); - - if (this.useLightbarInterface && console.term_supports(USER_ANSI)) - return this.SelectMsgArea_Lightbar(pChooseGroup ? 1 : 2, pGrpIdx); - else - return this.SelectMsgArea_Traditional(pChooseGroup ? 1 : 2, pGrpIdx); -} + var chooseGrp = (typeof(pChooseGroup) === "boolean" ? pChooseGroup : true); -// For the DDMsgAreaChooser class: Displays the header & header lines above the list. -// -// Parameters: -// pScreenRow: The row on the screen to write the lines at. If no cursor movements are desired, this can be null. -// pChooseGroup: Boolean - Whether or not the user is choosing a message group -// pGrpIdx: The index of the message group being used -// pNumPages: Optional - The number of pages of items -// pPageNum: Optional - The current page number for the items -function DDMsgAreaChooser_DisplayListHdrLines(pScreenRow, pChooseGroup, pGrpIdx, pNumPages, pPageNum) -{ - this.DisplayAreaChgHdr(1); - if (typeof(pScreenRow) === "number") - console.gotoxy(1, pScreenRow); - if (pChooseGroup) - this.WriteGrpListHdrLine(pNumPages, pPageNum); - else + // Start with this.lib_list, which is the topmost file lib/dir structure + var msgAreaStructure = this.msgArea_list; + var selectedGrpIdx = null; + var previousMsgAreaStructure = null; + if (!chooseGrp) { - // For the number of items in the sub-board list, use the text "Posts" or "Items", depending - // on whether sub-board collapsing is enabled and there are sub-subboard for the given group - // & sub-board index - var numItemsText = "Posts"; - if (this.useSubCollapsing) + if (typeof(pGrpIdx) === "number") { - for (var subIdx in this.group_list[pGrpIdx].sub_list) + if (pGrpIdx >= 0 && pGrpIdx < this.msgArea_list.length) { - if (typeof(this.group_list[pGrpIdx].sub_list[subIdx].sub_subboard_list) !== "undefined" && this.group_list[pGrpIdx].sub_list[subIdx].sub_subboard_list.length > 0) + if (this.msgArea_list[pGrpIdx].hasOwnProperty("items")) { - numItemsText = "Items"; - break; + msgAreaStructure = this.msgArea_list[pGrpIdx].items; + selectedGrpIdx = pGrpIdx; + previousMsgAreaStructure = this.msgArea_list; } } } - // Write the list header lines - this.WriteSubBrdListHdr1Line(pGrpIdx); - if (typeof(pScreenRow) === "number") - console.gotoxy(1, pScreenRow+1); else - console.crlf(); - if (this.showDatesInSubBoardList) - printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + numItemsText, "Latest date & time"); - else - printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + numItemsText); - } -} - -// For the DDMsgAreaChooser class: Lets the user choose a message group and -// sub-board via numeric input, using a lightbar user interface. -// -// Parameters: -// pLevel: The file heirarchy level: -// 1: Message groups -// 2: Sub-boards within message groups -// 3: "Sub-subboards" within sub-boards (if sub-board name collapsing is enabled) -// This is optional and defaults to 1. -// pGrpIdx: Optional - The group index, if choosing a sub-board -function DDMsgAreaChooser_SelectMsgArea_Lightbar(pLevel, pGrpIdx, pSubIdx) -{ - // If there are no message groups or sub-boards, then don't let the user - // choose one. - if (msg_area.grp_list.length == 0) - { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no message groups.\r\n\x01p"); - return; - } - var level = (typeof(pLevel) === "number" ? pLevel : 1); - if (level < 1 || level > 3) - return; - - else if (level == 1) - { - // If there are no sub-boards in the given group index, then see if there's a next group with - // sub-boards (and wrap around) - if (msg_area.grp_list[pGrpIdx].sub_list.length == 0) { - var nextGrpIdx = findNextGrpIdxWithSubBoards(pGrpIdx); - if (nextGrpIdx > -1 && msg_area.grp_list[nextGrpIdx].sub_list.length > 0) - pGrpIdx = nextGrpIdx; - else + for (var i = 0; i < this.msgArea_list.length; ++i) { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no sub-boards available.\r\n\x01p"); - return; + if (msgAreaStructureHasCurrentUserSubBoard(this.msgArea_list[i])) + { + if (this.msgArea_list[i].hasOwnProperty("items")) + { + msgAreaStructure = this.msgArea_list[i].items; + selectedGrpIdx = i; + previousMsgAreaStructure = this.msgArea_list; + } + break; + } } } } - // 2: Choose a sub-board within a message group - // 3: Choose a sub-subboard within a sub-board, for sub-board name collapsing - else if ((level == 2) || (level == 3)) + var selectedItemIndexes = []; // Will be used like a stack + var selectedItemIdx = null; + var chosenGroupOrSubBoardName = ""; // Will have the name of the user's chosen library/subdir + var previousMsgAreaStructures = []; // Will be used like a stack + var previousChosenLibOrSubdirNames = []; // Will be used like a stack + var subBoardsLabelLen = 14; // The length of the "Sub-boards of " label + var nameSep = " - "; // A string to use to separate group/sub-board names for the top header line + var numItemsWidth = 0; // Width of the header column for # of items (not including the space), for creating the menu + // Main loop + var selectionLoopContinueOn = true; + while (selectionLoopContinueOn) { - if (typeof(pGrpIdx) !== "number") - return; - if (msg_area.grp_list[pGrpIdx].sub_list.length == 0) + console.clear("\x01n"); + + // If we're displaying the file libraries (top level), then we'll output 1 + // header line; otherwise, we'll output 2 header line; adjut the top line + // of the menu accordingly. + var menuTopRow = 0; + var choosingGroup = chooseGrp && (msgAreaStructure == this.msgArea_list || chosenGroupOrSubBoardName.length == 0); + //if (msgAreaStructure == this.msgArea_list || chosenGroupOrSubBoardName.length == 0) + if (choosingGroup) { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no sub-boards in " + msg_area.grp_list[pGrpIdx].description + ".\r\n\x01p"); - return; + menuTopRow = this.areaChangeHdrLines.length + 2; + numItemsWidth = 6; // "Group#" } - } - - var chooseGroup = (level == 1); - - // Clear the screen, write the header, help line, and list header(s) - console.clear("\x01n"); - this.DisplayListHdrLines(this.areaChangeHdrLines.length+1, chooseGroup, pGrpIdx); - this.WriteKeyHelpLine(); - - // Create the menu and do the user input loop - var msgAreaMenu = (chooseGroup ? this.CreateLightbarMsgGrpMenu() : this.CreateLightbarSubBoardMenu(pLevel, pGrpIdx, pSubIdx)); - var drawMenu = true; - var lastSearchText = ""; - var lastSearchFoundIdx = -1; - var chosenIdx = -1; - var continueOn = true; - // Let the user choose a group, and also respond to other user choices - while (continueOn) - { - chosenIdx = -1; - var returnedMenuIdx = msgAreaMenu.GetVal(drawMenu); - drawMenu = true; - var lastUserInputUpper = (typeof(msgAreaMenu.lastUserInput) === "string" ? msgAreaMenu.lastUserInput.toUpperCase() : ""); - if (typeof(returnedMenuIdx) === "number") - chosenIdx = returnedMenuIdx; - // If userChoice is not a number, then it should be null in this case, - // and the user would have pressed one of the additional quit keys set - // up for the menu. So look at the menu's lastUserInput and do the - // appropriate thing. - else if ((lastUserInputUpper == "Q") || (lastUserInputUpper == KEY_ESC)) // Quit - continueOn = false; - else if ((lastUserInputUpper == "/") || (lastUserInputUpper == CTRL_F)) // Start of find + else { - console.gotoxy(1, console.screen_rows); - console.cleartoeol("\x01n"); - console.gotoxy(1, console.screen_rows); - var promptText = "Search: "; - console.print(promptText); - var searchText = getStrWithTimeout(K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE, console.screen_columns - promptText.length - 1, SEARCH_TIMEOUT_MS); - lastSearchText = searchText; - // If the user entered text, then do the search, and if found, - // found, go to the page and select the item indicated by the - // search. - if (searchText.length > 0) + menuTopRow = this.areaChangeHdrLines.length + 3; + numItemsWidth = 6; // " Sub #" + // TODO: Remove? + /* + printf("\x01n%sSub-boards of \x01h%s\x01n", this.colors.header, chosenGroupOrSubBoardName.substr(0, console.screen_columns - subBoardsLabelLen - 1)); + console.crlf(); + */ + } + //var previousMsgAreaStructure = (previousMsgAreaStructures.length > 0 ? previousMsgAreaStructures[previousMsgAreaStructures.length-1] : null); + if (chooseGrp) + previousMsgAreaStructure = (previousMsgAreaStructures.length > 0 ? previousMsgAreaStructures[previousMsgAreaStructures.length-1] : null); + else if (previousMsgAreaStructures.length > 0) + previousMsgAreaStructure = previousMsgAreaStructures[previousMsgAreaStructures.length-1]; + var createMenuRet = this.CreateLightbarMenu(msgAreaStructure, previousMsgAreaStructures.length+1, menuTopRow, selectedItemIdx, numItemsWidth); + // createMenuRet.allSubs + // createMenuRet.allOnlyOtherItems + var menu = createMenuRet.menuObj; + // Write the header lines, & write the key help line at the bottom of the screen + var numItemsColLabel = createMenuRet.allSubs ? "Sub-boards" : "Items"; + //var previousMsgAreaStructure = (previousMsgAreaStructures.length > 0 ? previousMsgAreaStructures[previousMsgAreaStructures.length-1] : null); + //this.DisplayListHdrLines(this.areaChangeHdrLines.length+1, choosingGroup, previousMsgAreaStructure, selectedItemIdx); // Old + this.DisplayListHdrLines(this.areaChangeHdrLines.length+1, choosingGroup, previousMsgAreaStructure, selectedGrpIdx); + if (!this.useLightbarInterface || !console.term_supports(USER_ANSI)) + console.crlf(); // Not drawing the hotkey help line (this.WriteKeyHelpLine();) + + // Show the menu in a loop and get user input + var lastSearchText = ""; + var lastSearchFoundIdx = -1; + var drawMenu = true; + var writeHdrLines = false; // Already displayed above + var writeKeyHelpLine = true; + // Menu input loop + var menuContinueOn = true; + while (menuContinueOn) + { + // Draw the header lines and key help line if needed + if (writeHdrLines) { - var oldLastSearchFoundIdx = lastSearchFoundIdx; - var oldSelectedItemIdx = msgAreaMenu.selectedItemIdx; - var idx = -1; - if (chooseGroup) - idx = this.FindMsgGrpIdxFromText(searchText, msgAreaMenu.selectedItemIdx); - else - idx = this.FindSubBoardIdxFromText(pGrpIdx, (pLevel == 3 ? pSubIdx : null), searchText, msgAreaMenu.selectedItemIdx+1); - lastSearchFoundIdx = idx; - if (idx > -1) - { - // Set the currently selected item in the menu, and ensure it's - // visible on the page - msgAreaMenu.selectedItemIdx = idx; - if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage()) - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1; - else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx) - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx; - else - { - // If the current index and the last index are both on the same page on the - // menu, then have the menu only redraw those items. - msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; - } - } - else + this.DisplayListHdrLines(this.areaChangeHdrLines.length+1, choosingGroup, previousMsgAreaStructure, selectedGrpIdx); + if (!this.useLightbarInterface || !console.term_supports(USER_ANSI)) + console.crlf(); // Not drawing the hotkey help line (this.WriteKeyHelpLine();) + writeHdrLines = false; + } + if (writeKeyHelpLine && this.useLightbarInterface && console.term_supports(USER_ANSI)) + { + this.WriteKeyHelpLine(); + writeKeyHelpLine = false; + } + + // Show the menu and get user input + var selectedMenuIdx = menu.GetVal(drawMenu); + drawMenu = true; + var lastUserInputUpper = (typeof(menu.lastUserInput) == "string" ? menu.lastUserInput.toUpperCase() : ""); + // Applicable for ANSI/lightbar mode: If the user typed a number, the menu input loop will + // exit with the return value being null and the user's input in lastUserInput. Test to see + // if the user typed a number (it will be a single number), and if it's within the number of + // menu items, set selectedMenuIdx to the menu item index. + if (this.useLightbarInterface && console.term_supports(USER_ANSI) && lastUserInputUpper.match(/[0-9]/)) + { + var userInputNum = parseInt(lastUserInputUpper); + if (!isNaN(userInputNum) && userInputNum >= 1 && userInputNum <= menu.NumItems()) { - if (chooseGroup) - idx = this.FindMsgGrpIdxFromText(searchText, 0); - else - idx = this.FindSubBoardIdxFromText(pGrpIdx, (pLevel == 3 ? pSubIdx : null), searchText, 0); - lastSearchFoundIdx = idx; - if (idx > -1) + // Put the user's input back in the input buffer to + // be used for getting the rest of the message number. + console.ungetstr(lastUserInputUpper); + // Go to the last row on the screen and prompt for the full item number + console.gotoxy(1, console.screen_rows); + console.clearline("\x01n"); + console.gotoxy(1, console.screen_rows); + var itemPromptWord = createMenuRet.allSubs ? "item" : "sub-board"; + printf("\x01cChoose %s #: \x01h", itemPromptWord); + var userInput = console.getnum(menu.NumItems()); + if (userInput > 0) { - // Set the currently selected item in the menu, and ensure it's - // visible on the page - msgAreaMenu.selectedItemIdx = idx; - if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage()) - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1; - else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx) - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx; - else + selectedMenuIdx = userInput - 1; + selectedItemIndexes.push(selectedMenuIdx); + previousChosenLibOrSubdirNames.push(chosenGroupOrSubBoardName); + if (previousChosenLibOrSubdirNames.length > 1) { - // The current index and the last index are both on the same page on the - // menu, so have the menu only redraw those items. - msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; + chosenGroupOrSubBoardName = previousChosenLibOrSubdirNames[previousChosenLibOrSubdirNames.length-1] + nameSep + menu[selectedMenuIdx].name; + // If chosenGroupOrSubBoardName is now too long, remove some of the previous labels + while (chosenGroupOrSubBoardName.length > console.screen_columns - 1) + { + var sepIdx = chosenGroupOrSubBoardName.indexOf(nameSep); + if (sepIdx > -1) + chosenGroupOrSubBoardName = chosenGroupOrSubBoardName.substr(sepIdx + nameSep.length); + } } + else + chosenGroupOrSubBoardName = msgAreaStructure[selectedMenuIdx].name; } else { - this.WriteLightbarKeyHelpErrorMsg("Not found"); - drawMenu = false; + // The user didn't make a selection. So, we need to refresh the + // screen (including the header, due to things being moved down one line). + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) + console.gotoxy(1, 1); + this.DisplayMenuHdrWithNumItems(createMenuRet.itemNumWidth, createMenuRet.descWidth-3, createMenuRet.numItemsWidth, numItemsColLabel); + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) + this.WriteKeyHelpLine(); + continue; // Continue to display the menu again and get the user's choice } } } - else - drawMenu = false; - this.WriteKeyHelpLine(); - } - else if (lastUserInputUpper == "N") // Next search result (requires an existing search term) - { - // This works but seems a little strange sometimes. - // - Should this always start from the selected index? - // - If it wraps around to one of the items on the first page, - // should it always set the top index to 0? - if ((lastSearchText.length > 0) && (lastSearchFoundIdx > -1)) + + // The code block above will set selectedMenuIdx if the user typed a valid entry + + // Check for aborted & other uesr input and take appropriate action. Note the first check + // here is 'if' and not 'else if'; that's intentional. + if (console.aborted || lastUserInputUpper == CTRL_C) { - var oldLastSearchFoundIdx = lastSearchFoundIdx; - var oldSelectedItemIdx = msgAreaMenu.selectedItemIdx; - // Do the search, and if found, go to the page and select the item - // indicated by the search. - var idx = 0; - if (chooseGroup) - idx = this.FindMsgGrpIdxFromText(searchText, lastSearchFoundIdx+1); - else - idx = this.FindSubBoardIdxFromText(pGrpIdx, (pLevel == 3 ? pSubIdx : null), searchText, lastSearchFoundIdx+1); - if (idx > -1) + // Fully quit out (note: This check/block must be before the test for Q/ESC/null return value) + menuContinueOn = false; + selectionLoopContinueOn = false; + } + else if (typeof(selectedMenuIdx) === "number") + { + // The user chose a valid item (the return value is the menu item index) + // The objects in this.msgArea_list have a 'name' property and either + // an 'items' property if it has sub-items or a 'subObj' property + // if it's a file directory + selectedItemIdx = null; + selectedGrpIdx = selectedMenuIdx; + selectedItemIndexes.push(selectedMenuIdx); + previousChosenLibOrSubdirNames.push(msgAreaStructure[selectedMenuIdx].name); + if (msgAreaStructure[selectedMenuIdx].hasOwnProperty("items")) { - lastSearchFoundIdx = idx; - // Set the currently selected item in the menu, and ensure it's - // visible on the page - msgAreaMenu.selectedItemIdx = idx; - if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage()) + previousMsgAreaStructures.push(msgAreaStructure); + if (previousChosenLibOrSubdirNames.length > 1) { - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1; - if (msgAreaMenu.topItemIdx < 0) - msgAreaMenu.topItemIdx = 0; + chosenGroupOrSubBoardName = previousChosenLibOrSubdirNames[previousChosenLibOrSubdirNames.length-1] + nameSep + msgAreaStructure[selectedMenuIdx].name; + // If chosenGroupOrSubBoardName is now too long, remove some of the previous labels + while (chosenGroupOrSubBoardName.length > console.screen_columns - 1) + { + var sepIdx = chosenGroupOrSubBoardName.indexOf(nameSep); + if (sepIdx > -1) + chosenGroupOrSubBoardName = chosenGroupOrSubBoardName.substr(sepIdx + nameSep.length); + } } - else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx) - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx; else - { - // The current index and the last index are both on the same page on the - // menu, so have the menu only redraw those items. - msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; - } + chosenGroupOrSubBoardName = msgAreaStructure[selectedMenuIdx].name; + msgAreaStructure = msgAreaStructure[selectedMenuIdx].items; + menuContinueOn = false; } - else + else if (msgAreaStructure[selectedMenuIdx].hasOwnProperty("subObj")) { - if (chooseGroup) - idx = this.FindMsgGrpIdxFromText(searchText, 0); - else - idx = this.FindSubBoardIdxFromText(pGrpIdx, (pLevel == 3 ? pSubIdx : null), searchText, 0); - lastSearchFoundIdx = idx; - if (idx > -1) + // The user has selected a file directory + bbs.cursub_code = msgAreaStructure[selectedMenuIdx].subObj.code; + menuContinueOn = false; + selectionLoopContinueOn = false; + } + } + else if (lastUserInputUpper == "?") + { + var usingLightbar = this.useLightbarInterface && console.term_supports(USER_ANSI); + this.ShowHelpScreen(usingLightbar, true); + menuContinueOn = true; + selectionLoopContinueOn = true; + drawMenu = true; + writeHdrLines = true; + writeKeyHelpLine = true; + } + else if (lastUserInputUpper == "/" || lastUserInputUpper == CTRL_F) // Start of find + { + // Lightbar/ANSI mode + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) + { + console.gotoxy(1, console.screen_rows); + console.cleartoeol("\x01n"); + console.gotoxy(1, console.screen_rows); + var promptText = "Search: "; + console.print(promptText); + //var searchText = getStrWithTimeout(K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE, console.screen_columns - promptText.length - 1, SEARCH_TIMEOUT_MS); + var searchText = console.getstr(lastSearchText, console.screen_columns - promptText.length - 1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE); + var searchTextIsStr = (typeof(searchText) === "string"); + if (searchTextIsStr) + lastSearchText = searchText; + // If the user entered text, then do the search, and if found, + // found, go to the page and select the item indicated by the + // search. + if (searchTextIsStr && searchText.length > 0) { - // Set the currently selected item in the menu, and ensure it's - // visible on the page - msgAreaMenu.selectedItemIdx = idx; - if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage()) + var oldLastSearchFoundIdx = lastSearchFoundIdx; + var oldSelectedItemIdx = menu.selectedItemIdx; + var idx = this.FindMsgAreaIdxFromText(msgAreaStructure, searchText, menu.selectedItemIdx); + lastSearchFoundIdx = idx; + if (idx > -1) { - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1; - if (msgAreaMenu.topItemIdx < 0) - msgAreaMenu.topItemIdx = 0; + // Set the currently selected item in the menu, and ensure it's + // visible on the page + menu.selectedItemIdx = idx; + if (menu.selectedItemIdx >= menu.topItemIdx+menu.GetNumItemsPerPage()) + menu.topItemIdx = menu.selectedItemIdx - menu.GetNumItemsPerPage() + 1; + else if (menu.selectedItemIdx < menu.topItemIdx) + menu.topItemIdx = menu.selectedItemIdx; + else + { + // If the current index and the last index are both on the same page on the + // menu, then have the menu only redraw those items. + menu.nextDrawOnlyItems = [menu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; + } } - else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx) - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx; else { - // The current index and the last index are both on the same page on the - // menu, so have the menu only redraw those items. - msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; + var idx = this.FindMsgAreaIdxFromText(msgAreaStructure, searchText, 0); + lastSearchFoundIdx = idx; + if (idx > -1) + { + // Set the currently selected item in the menu, and ensure it's + // visible on the page + menu.selectedItemIdx = idx; + if (menu.selectedItemIdx >= menu.topItemIdx+menu.GetNumItemsPerPage()) + menu.topItemIdx = menu.selectedItemIdx - menu.GetNumItemsPerPage() + 1; + else if (menu.selectedItemIdx < menu.topItemIdx) + menu.topItemIdx = menu.selectedItemIdx; + else + { + // The current index and the last index are both on the same page on the + // menu, so have the menu only redraw those items. + menu.nextDrawOnlyItems = [menu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; + } + } + else + { + this.WriteLightbarKeyHelpErrorMsg("Not found"); + drawMenu = false; + } } } else - { - this.WriteLightbarKeyHelpErrorMsg("Not found"); drawMenu = false; - this.WriteKeyHelpLine(); - } - } - } - else - { - this.WriteLightbarKeyHelpErrorMsg("There is no previous search", true); - drawMenu = false; - this.WriteKeyHelpLine(); - } - } - else if (lastUserInputUpper == "?") // Show help - { - this.ShowHelpScreen(true, true); - console.pause(); - // Refresh the screen - console.clear("\x01n"); - this.DisplayAreaChgHdr(1); - this.DisplayListHdrLines(this.areaChangeHdrLines.length+1, chooseGroup, pGrpIdx); - this.WriteKeyHelpLine(); - } - // If the user entered a numeric digit, then treat it as - // the start of the message group number. - if (lastUserInputUpper.match(/[0-9]/)) - { - // Put the user's input back in the input buffer to - // be used for getting the rest of the message number. - console.ungetstr(lastUserInputUpper); - // Move the cursor to the bottom of the screen and - // prompt the user for the message number. - console.gotoxy(1, console.screen_rows); - console.clearline("\x01n"); - var itemPromptWord = ""; - if (this.useSubCollapsing) - { - if (level == 1) - itemPromptWord = "group"; - else if (level == 2) - itemPromptWord = "item"; - else if (level == 3) - itemPromptWord = "sub-board"; - } - else - itemPromptWord = (level == 1 ? "group" : "sub-board"); - printf("\x01cChoose %s #: \x01h", itemPromptWord); - var userInput = console.getnum(msgAreaMenu.NumItems()); - if (userInput > 0) - chosenIdx = userInput - 1; - else - { - // The user didn't make a selection. So, we need to refresh - // the screen due to everything being moved up one line. - this.DisplayListHdrLines(this.areaChangeHdrLines.length+1, chooseGroup, pGrpIdx); - this.WriteKeyHelpLine(); - } - } - - // If a group/sub-board/sub-subboard was chosen, then deal with it. - if (chosenIdx > -1) - { - // If choosing a message group, then let the user choose a - // sub-board within the group. Otherwise, return the user's - // chosen sub-board. - if (chooseGroup) - { - // Show a "Loading..." text in case there are many sub-boards in - // the chosen message group - console.crlf(); - console.print("\x01nLoading..."); - console.line_counter = 0; // To prevent a pause before the message list comes up - // Ensure that the sub-board printf information is created for - // the chosen message group. - this.BuildSubBoardPrintfInfoForGrp(chosenIdx); - var defaultSubIdx = chosenIdx == bbs.curgrp ? bbs.cursub : 0; - var subCodeBackup = bbs.cursub_code; - var chosenSubBoardIdx = this.SelectMsgArea_Lightbar(2, chosenIdx, defaultSubIdx); - // chosenSubBoardIdx could actually be a boolean and could be false (returned - // when pLevel is 3 and the user chose a sub-board), so check its type and - // act accordingly. - var retValType = typeof(chosenSubBoardIdx); - if (retValType === "boolean") - continueOn = chosenSubBoardIdx; - else if (typeof(chosenSubBoardIdx) === "number" && chosenSubBoardIdx > -1) - { - // Set the current sub-board - if (this.useSubCollapsing) - bbs.cursub_code = this.group_list[chosenIdx].sub_list[chosenSubBoardIdx].code; - else - bbs.cursub_code = msg_area.grp_list[chosenIdx].sub_list[chosenSubBoardIdx].code; - continueOn = false; + writeKeyHelpLine = true; } else { - // If the sub-board changed (probably at level 3 because name - // collapsing is enabled), then exit here. - if (bbs.cursub_code != subCodeBackup) - continueOn = false; - else + // Traditional/non-ANSI interface (menu will be using numbered mode) + console.attributes = "N"; + console.crlf(); + var promptText = "Search: "; + console.print(promptText); + var searchText = console.getstr(lastSearchText, console.screen_columns - promptText.length - 1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE); + var searchTextIsStr = (typeof(searchText) === "string"); + console.attributes = "N"; + console.crlf(); + // Don't need to set lastSearchFoundIdx + //if (searchTextIsStr) + // lastSearchText = searchText; + // If the user entered text, then do the search, and if found, + // found, go to the page and select the item indicated by the + // search. + if (searchTextIsStr && searchText.length > 0) { - // A message sub-board was not chosen, so we'll have to re-draw - // the header and key help line - this.DisplayListHdrLines(this.areaChangeHdrLines.length+1, chooseGroup, pGrpIdx); - this.WriteKeyHelpLine(); + var oldLastSearchFoundIdx = lastSearchFoundIdx; + var oldSelectedItemIdx = menu.selectedItemIdx; + var idx = this.FindMsgAreaIdxFromText(msgAreaStructure, searchText, 0); + //lastSearchFoundIdx = idx; // Don't need to set lastSearchFoundIdx + var newMsgAreaStructure = []; + while (idx > -1) + { + newMsgAreaStructure.push(msgAreaStructure[idx]); + + // Find the next one + idx = this.FindMsgAreaIdxFromText(msgAreaStructure, searchText, idx+1); + } + if (newMsgAreaStructure.length > 0) + { + selectedItemIdx = selectedItemIndexes.push(selectedItemIdx); + msgAreaStructure = previousMsgAreaStructures.push(msgAreaStructure); + previousChosenLibOrSubdirNames.push(""); + msgAreaStructure = newMsgAreaStructure; + createMenuRet = this.CreateLightbarMenu(newMsgAreaStructure, previousMsgAreaStructures.length+1, menuTopRow, 0, numItemsWidth); + menu = createMenuRet.menuObj; + } + else + console.print("Not found\r\n\x01p"); } + + console.line_counter = 0; + console.clear("\x01n"); + writeHdrLines = true; + drawMenu = true; + writeKeyHelpLine = false; } } - else if (level == 2) // Choosing a sub-board + else if (lastUserInputUpper == "N") // Next search result (requires an existing search term) { - if (this.useSubCollapsing) + // Lightbar/ANSI mode + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) { - // Ensure this.group_list is set up - this.SetUpGrpListWithCollapsedSubBoards(); - - // If the current file directory has subdirectories, - // let the user choose one - // pGrpIdx is the group index, and chosenIdx is the sub-board index - if ((typeof(this.group_list[pGrpIdx].sub_list[chosenIdx].sub_subboard_list) !== "undefined") && (this.group_list[pGrpIdx].sub_list[chosenIdx].sub_subboard_list.length > 0)) + // This works but seems a little strange sometimes. + // - Should this always start from the selected index? + // - If it wraps around to one of the items on the first page, + // should it always set the top index to 0? + if ((lastSearchText.length > 0) && (lastSearchFoundIdx > -1)) { - var chosenSubSubBoardIdx = this.SelectMsgArea_Lightbar(3, pGrpIdx, chosenIdx); - if (chosenSubSubBoardIdx > -1) + var oldLastSearchFoundIdx = lastSearchFoundIdx; + var oldSelectedItemIdx = menu.selectedItemIdx; + // Do the search, and if found, go to the page and select the item + // indicated by the search. + var idx = this.FindMsgAreaIdxFromText(msgAreaStructure, searchText, lastSearchFoundIdx+1); + if (idx > -1) { - // Set the current message sub-board - //bbs.cursub_code = this.group_list[pGrpIdx].sub_list[chosenIdx].sub_subboard_list[chosenSubSubBoardIdx].code; - bbs.cursub_code = this.group_list[pGrpIdx].sub_list[chosenIdx].sub_subboard_list[chosenSubSubBoardIdx].code; - continueOn = false; - //return chosenSubSubBoardIdx; - // Return a false here so that after this function is called by itself when - // pLevel is 2, it will know that a sub-board has been chosen and will set - // continueOn to false so that the loop won't continue from there. - return false; + lastSearchFoundIdx = idx; + // Set the currently selected item in the menu, and ensure it's + // visible on the page + menu.selectedItemIdx = idx; + if (menu.selectedItemIdx >= menu.topItemIdx+menu.GetNumItemsPerPage()) + { + menu.topItemIdx = menu.selectedItemIdx - menu.GetNumItemsPerPage() + 1; + if (menu.topItemIdx < 0) + menu.topItemIdx = 0; + } + else if (menu.selectedItemIdx < menu.topItemIdx) + menu.topItemIdx = menu.selectedItemIdx; + else + { + // The current index and the last index are both on the same page on the + // menu, so have the menu only redraw those items. + menu.nextDrawOnlyItems = [menu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; + } } else { - // A file directory was not chosen, so we'll have to re-draw - // the header and list of message groups. - // TODO: - //this.DisplayListHdrLines(level, pLibIdx); - //this.WriteKeyHelpLine(); + idx = this.FindMsgAreaIdxFromText(msgAreaStructure, searchText, 0); + lastSearchFoundIdx = idx; + if (idx > -1) + { + // Set the currently selected item in the menu, and ensure it's + // visible on the page + menu.selectedItemIdx = idx; + if (menu.selectedItemIdx >= menu.topItemIdx+menu.GetNumItemsPerPage()) + { + menu.topItemIdx = menu.selectedItemIdx - menu.GetNumItemsPerPage() + 1; + if (menu.topItemIdx < 0) + menu.topItemIdx = 0; + } + else if (menu.selectedItemIdx < menu.topItemIdx) + menu.topItemIdx = menu.selectedItemIdx; + else + { + // The current index and the last index are both on the same page on the + // menu, so have the menu only redraw those items. + menu.nextDrawOnlyItems = [menu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; + } + } + else + { + this.WriteLightbarKeyHelpErrorMsg("Not found"); + drawMenu = false; + writeKeyHelpLine = true; + } } } - else // No subdirectories - Return the chosen index - return chosenIdx; + else + { + this.WriteLightbarKeyHelpErrorMsg("There is no previous search", true); + drawMenu = false; + writeKeyHelpLine = true; + } } else - return chosenIdx; // Return the chosen file directory index + { + // Traditional/non-ANSI interface (menu will be using numbered mode) + // TODO + console.attributes = "N"; + console.crlf(); + console.print("TODO\r\n\x01p"); + console.clear("\x01n"); + writeHdrLines = true; + drawMenu = true; + writeKeyHelpLine = false; + } } - else if (level == 3) + // Quit - Note: This check should be last + else if (lastUserInputUpper == "Q" || lastUserInputUpper == KEY_ESC || selectedMenuIdx == null) { - return chosenIdx; // Return the chosen subdirectory index + // Cancel/Quit + // Quit this menu loop and go back to the previous file lib/dir structure + menuContinueOn = false; + selectedItemIdx = selectedItemIndexes.pop(); + // TODO: I don't remember why I had the following 2 lines: + /* + if (selectedItemIndexes.length > 0) + selectedItemIdx = selectedItemIndexes.pop(); + */ + selectedGrpIdx = selectedItemIdx; + if (previousMsgAreaStructures.length == 0) + { + // The user was at the first level in the lib/dir structure; fully quit out from here + selectionLoopContinueOn = false; + } + else // Go to the previous file lib/dir structure + { + msgAreaStructure = previousMsgAreaStructures.pop(); + if (msgAreaStructure == this.msgArea_list) + { + chosenGroupOrSubBoardName = ""; + previousChosenLibOrSubdirNames = []; + selectedItemIndexes = []; + } + else + chosenGroupOrSubBoardName = previousChosenLibOrSubdirNames.pop(); + } } } } } -// For the DDMsgAreaChooser class: Creates the DDLightbarMenu for use with -// choosing a message group in lightbar mode. +// For the DDMsgAreaChooser class: Displays the header & header lines above the list. // -// Return value: A DDLightbarMenu object for choosing a message group -function DDMsgAreaChooser_CreateLightbarMsgGrpMenu() +// Parameters: +// pScreenRow: The row on the screen to write the lines at. If no cursor movements are desired, this can be null. +// pChooseGroup: Boolean - Whether or not the user is choosing a message group +// pMsgAreaHeirarchyObj: An object from this.msgArea_list, which is +// set up with a 'name' property and either +// an 'items' property if it has sub-items +// or a 'subObj' property if it's a sub-board +// pGrpIndex: The index of the message group (assumed to be valid) +// pGrpIdx: The index of the message group being used +// pNumPages: Optional - The number of pages of items +// pPageNum: Optional - The current page number for the items +function DDMsgAreaChooser_DisplayListHdrLines(pScreenRow, pChooseGroup, pMsgAreaHeirarchyObj, pGrpIdx, pNumPages, pPageNum) { - // Start & end indexes for the various items in each mssage group list row - // Selected mark, group#, description, # sub-boards - var msgGrpListIdxes = { - markCharStart: 0, - markCharEnd: 1, - grpNumStart: 1, - grpNumEnd: 2 + (+this.areaNumLen) - }; - msgGrpListIdxes.descStart = msgGrpListIdxes.grpNumEnd; - msgGrpListIdxes.descEnd = msgGrpListIdxes.descStart + +this.msgGrpDescLen; - msgGrpListIdxes.numItemsStart = msgGrpListIdxes.descEnd; - // Set numItemsEnd to -1 to let the whole rest of the lines be colored - msgGrpListIdxes.numItemsEnd = -1; - var listStartRow = this.areaChangeHdrLines.length + 2; - var msgGrpMenuHeight = console.screen_rows - listStartRow; - var msgGrpMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, msgGrpMenuHeight); - msgGrpMenu.scrollbarEnabled = true; - msgGrpMenu.borderEnabled = false; - msgGrpMenu.SetColors({ - itemColor: [{start: msgGrpListIdxes.markCharStart, end: msgGrpListIdxes.markCharEnd, attrs: this.colors.areaMark}, - {start: msgGrpListIdxes.grpNumStart, end: msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaNum}, - {start: msgGrpListIdxes.descStart, end: msgGrpListIdxes.descEnd, attrs: this.colors.desc}, - {start: msgGrpListIdxes.numItemsStart, end: msgGrpListIdxes.numItemsEnd, attrs: this.colors.numItems}], - selectedItemColor: [{start: msgGrpListIdxes.markCharStart, end: msgGrpListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, - {start: msgGrpListIdxes.grpNumStart, end: msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight}, - {start: msgGrpListIdxes.descStart, end: msgGrpListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, - {start: msgGrpListIdxes.numItemsStart, end: msgGrpListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}] - }); - - msgGrpMenu.multiSelect = false; - msgGrpMenu.ampersandHotkeysInItems = false; - msgGrpMenu.wrapNavigation = false; - - // Add additional keypresses for quitting the menu's input loop so we can - // respond to these keys - msgGrpMenu.AddAdditionalQuitKeys("nNqQ ?0123456789/" + CTRL_F); - - // Change the menu's NumItems() and GetItem() function to reference - // the message list in this object rather than add the menu items - // to the menu - msgGrpMenu.areaChooser = this; // Add this object to the menu object - msgGrpMenu.NumItems = function() { - return msg_area.grp_list.length; - }; - msgGrpMenu.GetItem = function(pGrpIndex) { - var menuItemObj = this.MakeItemWithRetval(-1); - if ((pGrpIndex >= 0) && (pGrpIndex < msg_area.grp_list.length)) + this.DisplayAreaChgHdr(1); + if (typeof(pScreenRow) === "number") + console.gotoxy(1, pScreenRow); + if (pChooseGroup) + this.WriteGrpListHdrLine(pNumPages, pPageNum); + else + { + // See if the items in the directory heirarchy only contain a "subObj" or an "items" + var allItemsAreSubBoards = true; + var allItemsContainOnlyOtherItems = true; + if (typeof(pMsgAreaHeirarchyObj[pGrpIdx]) === "object" && !Array.isArray(pMsgAreaHeirarchyObj[pGrpIdx]) && pMsgAreaHeirarchyObj[pGrpIdx].hasOwnProperty("items")) { - var showAreaMark = false; - if (this.areaChooser.useSubCollapsing) - { - //this.group_list[pGrpIndex].sub_list[pSubIndex] - showAreaMark = ((typeof(bbs.curgrp) === "number") && (pGrpIndex == msg_area.sub[bbs.cursub_code].grp_index)); - } - else + for (var i = 0; i < pMsgAreaHeirarchyObj[pGrpIdx].items.length; ++i) { - showAreaMark = ((typeof(bbs.curgrp) === "number") && (pGrpIndex == msg_area.sub[bbs.cursub_code].grp_index)); + if (!pMsgAreaHeirarchyObj[pGrpIdx].items[i].hasOwnProperty("subObj") || pMsgAreaHeirarchyObj[pGrpIdx].items[i].hasOwnProperty("items")) + allItemsAreSubBoards = false; + if (pMsgAreaHeirarchyObj[pGrpIdx].items[i].hasOwnProperty("subObj") || !pMsgAreaHeirarchyObj[pGrpIdx].items[i].hasOwnProperty("items")) + allItemsContainOnlyOtherItems = false; } - menuItemObj.text = (showAreaMark ? "*" : " "); - menuItemObj.text += format(this.areaChooser.msgGrpListPrintfStr, +(pGrpIndex+1), - msg_area.grp_list[pGrpIndex].description.substr(0, this.areaChooser.msgGrpDescLen), - msg_area.grp_list[pGrpIndex].sub_list.length); - menuItemObj.text = strip_ctrl(menuItemObj.text); - menuItemObj.retval = pGrpIndex; } - return menuItemObj; - }; - - // Set the currently selected item to the current group - msgGrpMenu.selectedItemIdx = msg_area.sub[bbs.cursub_code].grp_index; - if (msgGrpMenu.selectedItemIdx >= msgGrpMenu.topItemIdx+msgGrpMenu.GetNumItemsPerPage()) - msgGrpMenu.topItemIdx = msgGrpMenu.selectedItemIdx - msgGrpMenu.GetNumItemsPerPage() + 1; - - return msgGrpMenu; + // Write the list header lines + this.WriteSubBrdListHdr1Line(pMsgAreaHeirarchyObj, pGrpIdx); + if (typeof(pScreenRow) === "number") + console.gotoxy(1, pScreenRow+1); + else + console.crlf(); + if (allItemsContainOnlyOtherItems) + this.WriteGrpListHdrLine(pNumPages, pPageNum); + else + { + var numItemsText = "Items"; + if (allItemsAreSubBoards) + numItemsText = "Posts"; + else if (allItemsContainOnlyOtherItems) + numItemsText = "Items"; + if (this.showDatesInSubBoardList) + printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + numItemsText, "Latest date & time"); + else + printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + numItemsText); + } + } } -// For the DDMsgAreaChooser class: Creates the DDLightbarMenu for use with -// choosing a sub-board in lightbar mode. +// For the DDMsgAreaChooser class: Creates a lightbar menu to choose a message group/sub-board. // // Parameters: -// pLevel: The level of menu (1=Group, 2=Sub-board, 3=Sub board within sub-board if sub-board name collapsing is enabled) -// pGrpIdx: The index of the message group -// pSubIdx: The sub-board index (for sub-board name collapsing) -// -// Return value: A DDLightbarMenu object for choosing a sub-board within -// the given message group -function DDMsgAreaChooser_CreateLightbarSubBoardMenu(pLevel, pGrpIdx, pSubIdx) +// pMsgAreaHeirarchyObj: An object from this.msgArea_list, which is +// set up with a 'name' property and either +// an 'items' property if it has sub-items +// or a 'subObj' property if it's a sub-board +// pHeirarchyLevel: The level we're at in the heirarchy (1-based) +// pMenuTopRow: The screen row to use for the menu's top row +// pSelectedItemIdx: The index to use for the selected item. If not +// specified, the item with the user's current selected file +// directory will be used, if available. +// pItemNumWidth: The character width of the column label for the item number; +// mainly for the traditional (non-lightbar) UI but is used for lightbar +// mode as well for level > 2 (not sure why needed though) +// +// Return value: An object with the following properties: +// menuObj: The menu object +// allSubs: Whether or not all the items in the menu +// are sub-boards. If not, some or all are +// other lists of items. If this is true, +// allOnlyOtherItems should be false. +// allOnlyOtherItems: Whether or not all the items in the menu +// only have arrays of other items. This is +// mutually exclusive with allSubs. If this +// is true, allSubs should be false. +// itemNumWidth: The width of the item numbers column +// descWidth: The width of the description column +// numItemsWidth: The width of the # of items column +function DDMsgAreaChooser_CreateLightbarMenu(pMsgAreaHeirarchyObj, pHeirarchyLevel, pMenuTopRow, pSelectedItemIdx, pItemNumWidth) { - // Start & end indexes for the various items in each mssage group list row - // Selected mark, group#, description, # sub-boards - var lengthsObj = this.GetSubNameLenAndNumMsgsLen(pGrpIdx); - var subBoardListIdxes = { - markCharStart: 0, - markCharEnd: 1, - subNumStart: 1, - subNumEnd: 3 + (+this.areaNumLen) + var retObj = { + menuObj: null, + allSubs: true, + allOnlyOtherItems: true, + itemNumWidth: 0, + descWidth: 0, + numItemsWidth: 0 }; - subBoardListIdxes.descStart = subBoardListIdxes.subNumEnd; - subBoardListIdxes.descEnd = subBoardListIdxes.descStart + lengthsObj.nameLen + 1; - subBoardListIdxes.numItemsStart = subBoardListIdxes.descEnd; - subBoardListIdxes.numItemsEnd = subBoardListIdxes.numItemsStart + lengthsObj.numMsgsLen + 1; - subBoardListIdxes.dateStart = subBoardListIdxes.numItemsEnd; - subBoardListIdxes.dateEnd = subBoardListIdxes.dateStart + +this.dateLen + 1; - subBoardListIdxes.timeStart = subBoardListIdxes.dateEnd; - // Set timeEnd to -1 to let the whole rest of the lines be colored - subBoardListIdxes.timeEnd = -1; - var listStartRow = this.areaChangeHdrLines.length + 3; // or + 3? - var subBoardMenuHeight = console.screen_rows - listStartRow; - var subBoardMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, subBoardMenuHeight); - subBoardMenu.scrollbarEnabled = true; - subBoardMenu.borderEnabled = false; - subBoardMenu.SetColors({ - itemColor: [{start: subBoardListIdxes.markCharStart, end: subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark}, - {start: subBoardListIdxes.subNumStart, end: subBoardListIdxes.subNumEnd, attrs: this.colors.areaNum}, - {start: subBoardListIdxes.descStart, end: subBoardListIdxes.descEnd, attrs: this.colors.desc}, - {start: subBoardListIdxes.numItemsStart, end: subBoardListIdxes.numItemsEnd, attrs: this.colors.numItems}, - {start: subBoardListIdxes.dateStart, end: subBoardListIdxes.dateEnd, attrs: this.colors.latestDate}, - {start: subBoardListIdxes.timeStart, end: subBoardListIdxes.timeEnd, attrs: this.colors.latestTime}], - selectedItemColor: [{start: subBoardListIdxes.markCharStart, end: subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, - {start: subBoardListIdxes.subNumStart, end: subBoardListIdxes.subNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight}, - {start: subBoardListIdxes.descStart, end: subBoardListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, - {start: subBoardListIdxes.numItemsStart, end: subBoardListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}, - {start: subBoardListIdxes.dateStart, end: subBoardListIdxes.dateEnd, attrs: this.colors.dateHighlight + this.colors.bkgHighlight}, - {start: subBoardListIdxes.timeStart, end: subBoardListIdxes.timeEnd, attrs: this.colors.timeHighlight + this.colors.bkgHighlight}] - }); - - subBoardMenu.multiSelect = false; - subBoardMenu.ampersandHotkeysInItems = false; - subBoardMenu.wrapNavigation = false; + // Get color index information for the menu + var colorIdxInfo = this.GetColorIndexInfoForLightbarMenu(pMsgAreaHeirarchyObj); + + // Calculate column widths for the return object + retObj.itemNumWidth = colorIdxInfo.subBoardListIdxes.subNumend - 1; + //retObj.descWidth = msgGrpListIdxes.descEnd - msgGrpListIdxes.descStart; + retObj.descWidth = this.subBoardNameLen; + retObj.numItemsWidth = console.screen_columns - colorIdxInfo.msgGrpListIdxes.numItemsStart; + + // Create the menu object + var fileDirMenuHeight = console.screen_rows - pMenuTopRow; + var msgAreaMenu = new DDLightbarMenu(1, pMenuTopRow, console.screen_columns, fileDirMenuHeight); // Add additional keypresses for quitting the menu's input loop so we can // respond to these keys - subBoardMenu.AddAdditionalQuitKeys("nNqQ ?0123456789/" + CTRL_F); - - // Change the menu's NumItems() and GetItem() function to reference - // the message list in this object rather than add the menu items - // to the menu - subBoardMenu.areaChooser = this; // Add this object to the menu object - subBoardMenu.grpIdx = pGrpIdx; - if (this.useSubCollapsing) + msgAreaMenu.AddAdditionalQuitKeys("qQ?/" + CTRL_F); + if (this.useLightbarInterface && console.term_supports(USER_ANSI)) { - if (pLevel == 2) - { - subBoardMenu.NumItems = function() { - return this.areaChooser.group_list[this.grpIdx].sub_list.length; - }; - subBoardMenu.GetItem = function(pSubIdx) { - var menuItemObj = this.MakeItemWithRetval(-1); - var subIdxValid = true; - /* - if (this.areaChooser.useSubCollapsing) - showSubBoardMark = this.areaChooser.CurrentSubBoardIsInSubSubsForSub(this.grpIdx, +pSubIdx); - */ - if ((pSubIdx >= 0) && (pSubIdx < this.areaChooser.group_list[this.grpIdx].sub_list.length)) - { - var showSubBoardMark = false; - if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != "")) - { - showSubBoardMark = this.areaChooser.CurrentSubBoardIsInSubSubsForSub(this.grpIdx, +pSubIdx); - /* - if (this.areaChooser.useSubCollapsing) - showSubBoardMark = this.areaChooser.CurrentSubBoardIsInSubSubsForSub(this.grpIdx, +pSubIdx); - else - showSubBoardMark = ((this.grpIdx == msg_area.sub[bbs.cursub_code].grp_index) && (pSubIdx == msg_area.sub[bbs.cursub_code].index)); - */ - } - // Set the sub-board description. And if it has sub-subboards, - // then append some text indicating so. - var subDesc = this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].description; - var numItems = 0; - var lastMsgPostTimestamp = 0; - if (this.areaChooser.showDatesInSubBoardList) - lastMsgPostTimestamp = getLatestMsgTime(this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].code); - var subSubBoardListExists = false; - if (Array.isArray(this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].sub_subboard_list) && this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].sub_subboard_list.length > 0) - { - subSubBoardListExists = true; - subDesc += " <subsubs>"; - numItems = this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].sub_subboard_list.length; - } - // Get information from the messagebase - var msgBase = new MsgBase(this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].code); - if (msgBase.open()) - { - if (!subSubBoardListExists) - { - // There is no sub-subboard list, so this is just a regular sub-board. - // Get the number of readable messages in the sub-board. - numItems = numReadableMsgs(msgBase, this.areaChooser.group_list[this.grpIdx].sub_list[pSubIdx].code); - } - msgBase.close(); - } + msgAreaMenu.allowANSI = true; + // Additional quit keys for ANSI mode which we can respond to + // N: Next (search) and numbers for item input + msgAreaMenu.AddAdditionalQuitKeys(" nN0123456789" + CTRL_C); + } + // If not using the lightbar interface (and ANSI behavior is not to be allowed), msgAreaMenu.numberedMode + // will be set to true by default. Also, in that situation, set the menu's item number color + else + { + msgAreaMenu.allowANSI = false; + msgAreaMenu.colors.itemNumColor = this.colors.areaNum; + retObj.itemNumWidth = pMsgAreaHeirarchyObj.length.toString().length; + //retObj.descWidth -= 3; + } + msgAreaMenu.scrollbarEnabled = true; + msgAreaMenu.borderEnabled = false; + // Menu prompt text for non-ANSI mode + msgAreaMenu.nonANSIPromptText = "\x01n\x01b\x01h" + TALL_UPPER_MID_BLOCK + " \x01n\x01cWhich, \x01hQ\x01n\x01cuit, \x01hCTRL-F\x01n\x01c, \x01h/\x01n\x01c: \x01h"; + + // Menu item colors + msgAreaMenu.itemColorForSubBoard = colorIdxInfo.itemColorForSubBoard; + msgAreaMenu.selectedItemColorForSubBoard = colorIdxInfo.selectedItemColorForSubBoard; + msgAreaMenu.itemColorForItemWithSubItems = colorIdxInfo.itemColorForItemWithSubItems; + msgAreaMenu.selectedItemColorForItemWithSubItems = colorIdxInfo.selectedItemColorForItemWithSubItems; + + // See if all the items in pMsgAreaHeirarchyObj are sub-boards. + // Also, see which one has the user's current chosen sub-board so we can set the + // current menu item index in the menu + msgAreaMenu.idxWithUserSelectedSubBoard = -1; + if (Array.isArray(pMsgAreaHeirarchyObj)) + { + //msgAreaMenu.idxWithUserSelectedSubBoard = -1; + for (var i = 0; i < pMsgAreaHeirarchyObj.length; ++i) + { + // Each object will have either an "items" or a "subObj" + if (!pMsgAreaHeirarchyObj[i].hasOwnProperty("subObj")) + retObj.allSubs = false; + // See if this one has the user's selected file directory + if (msgAreaStructureHasCurrentUserSubBoard(pMsgAreaHeirarchyObj[i])) + msgAreaMenu.idxWithUserSelectedSubBoard = i; + // If we've found all we need, then stop going through the array + if (!retObj.allSubs && msgAreaMenu.idxWithUserSelectedSubBoard > -1) + break; + } + } - menuItemObj.text = (showSubBoardMark ? "*" : " "); - if (this.areaChooser.showDatesInSubBoardList) - { - menuItemObj.text += format(this.areaChooser.subBoardListPrintfInfo[this.grpIdx].printfStr, +(pSubIdx+1), - subDesc.substr(0, this.areaChooser.descFieldLen), numItems, - strftime("%Y-%m-%d", lastMsgPostTimestamp), - strftime("%H:%M:%S", lastMsgPostTimestamp)); - } - else - { - menuItemObj.text += format(this.areaChooser.subBoardListPrintfInfo[this.grpIdx].printfStr, +(pSubIdx+1), - subDesc.substr(0, this.areaChooser.descFieldLen), numItems); - } - menuItemObj.text = strip_ctrl(menuItemObj.text); - menuItemObj.retval = pSubIdx; - } - - return menuItemObj; - }; + msgAreaMenu.multiSelect = false; + msgAreaMenu.ampersandHotkeysInItems = false; + msgAreaMenu.wrapNavigation = false; + + // Build the file directory info for the given file library + msgAreaMenu.msgAreaHeirarchyObj = pMsgAreaHeirarchyObj; + msgAreaMenu.areaChooser = this; + msgAreaMenu.allSubs = true; // Whether the menu has only sub-boards + if (Array.isArray(pMsgAreaHeirarchyObj)) + { + // See if any of the items in the array aren't directories, and set retObj.allSubs. + // Also, see which one has the user's current chosen directory so we can set the + // current menu item index - And save that index in the menu object for its + // reference later. + msgAreaMenu.idxWithUserSelectedSubBoard = -1; + for (var i = 0; i < pMsgAreaHeirarchyObj.length; ++i) + { + // Each object will have either an "items" or a "subObj" + if (!pMsgAreaHeirarchyObj[i].hasOwnProperty("subObj")) + { + retObj.allSubs = false; + msgAreaMenu.allSubs = false; + } + if (!pMsgAreaHeirarchyObj[i].hasOwnProperty("items")) + retObj.allOnlyOtherItems = false + // See if this one has the user's selected file directory + if (msgAreaStructureHasCurrentUserSubBoard(pMsgAreaHeirarchyObj[i])) + msgAreaMenu.idxWithUserSelectedSubBoard = i; + // If we've found all we need, then stop going through the array + if (!retObj.allSubs && msgAreaMenu.idxWithUserSelectedSubBoard > -1) + break; + } - // Set the currently selected item. If the current sub-board is in this list, - // then set the selected item to that; otherwise, the selected item should be - // the first sub-board. - var selectedItemIdxWasSet = false; - if (msg_area.sub[bbs.cursub_code].grp_index == pGrpIdx) + // Replace the menu's NumItems() function to return the correct number of items + msgAreaMenu.NumItems = function() { + return this.msgAreaHeirarchyObj.length; + }; + msgAreaMenu.numItemsLen = msgAreaMenu.NumItems().toString().length; + // Replace the menu's GetItem() function to create & return an item for the menu + msgAreaMenu.GetItem = function(pItemIdx) { + var menuItemObj = this.MakeItemWithRetval(-1); + //var showDirMark = msgAreaStructureHasCurrentUserSubBoard(this.msgAreaHeirarchyObj[pItemIdx]); + var showDirMark = (pItemIdx == this.idxWithUserSelectedSubBoard); + var areaDesc = this.msgAreaHeirarchyObj[pItemIdx].name; + var numItems = 0; + if (this.msgAreaHeirarchyObj[pItemIdx].hasOwnProperty("items")) { - //if (user.is_sysop) console.print("\x01n\r\nHere 1\r\n\x01p"); // Temporary - if ((pSubIdx >= 0) && (pSubIdx < this.group_list[pGrpIdx].sub_list.length)) + numItems = this.msgAreaHeirarchyObj[pItemIdx].items.length; + // If this isn't the top level (libraries), then add "<subdirs>" to the description + if (this.msgAreaHeirarchyObj != this.areaChooser.lib_list) + areaDesc += " <subsubs>"; + + // Menu item color arrays + menuItemObj.itemColor = this.itemColorForItemWithSubItems; + menuItemObj.itemSelectedColor = this.selectedItemColorForItemWithSubItems; + + // Menu item text + menuItemObj.text = (showDirMark ? "*" : " "); + if (this.allowANSI) { - var subSubsValid = Array.isArray(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list) && this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length > 0; - if (!subSubsValid && bbs.cursub_code == this.group_list[pGrpIdx].sub_list[pSubIdx].code) - { - //if (user.is_sysop) console.print("\x01n\r\nHere 3\r\n\x01p"); // Temporary - subBoardMenu.selectedItemIdx = pSubIdx; - if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage()) - subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1; - } + menuItemObj.text += " " + format(this.areaChooser.msgGrpListPrintfStr, +(pItemIdx+1), + this.msgAreaHeirarchyObj[pItemIdx].name.substr(0, this.areaChooser.msgGrpDescLen), + this.msgAreaHeirarchyObj[pItemIdx].items.length); + menuItemObj.text = strip_ctrl(menuItemObj.text); + } + else + { + // Traditional UI or no ANSI - Numbered mode + // Number of spaces before the group name + var numSpaces = pItemNumWidth - this.numItemsLen - console.strlen(menuItemObj.text); + if (numSpaces > 0) + menuItemObj.text += format("%*s", numSpaces, ""); + menuItemObj.text += format(this.areaChooser.msgGrpListPrintfStrWithoutAreaNum, + this.msgAreaHeirarchyObj[pItemIdx].name.substr(0, this.areaChooser.msgGrpDescLen), + this.msgAreaHeirarchyObj[pItemIdx].items.length); + menuItemObj.text = strip_ctrl(menuItemObj.text); } } - // If the selected item wasn't set, then check whether the current sub-board - // is within any of the sub-subboards for the subgroups in the group, and set - // it if so. - if (!selectedItemIdxWasSet) + else if (this.msgAreaHeirarchyObj[pItemIdx].hasOwnProperty("subObj")) { - for (var subIdx = 0; subIdx < this.group_list[pGrpIdx].sub_list.length; ++subIdx) + if (this.msgAreaHeirarchyObj[pItemIdx].subObj.hasOwnProperty("posts")) + numItems = this.msgAreaHeirarchyObj[pItemIdx].subObj.posts; // Added in Synchronet 3.18c + else { - if (this.CurrentSubBoardIsInSubSubsForSub(pGrpIdx, subIdx)) + // Get information from the messagebase + var msgBase = new MsgBase(this.msgAreaHeirarchyObj[pItemIdx].subObj.code); + if (msgBase.open()) { - subBoardMenu.selectedItemIdx = subIdx; - if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage()) - subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1; - break; + numItems = msgBase.total_msgs; + msgBase.close(); } } - } - } - else if (pLevel == 3) - { - subBoardMenu.subIdx = pSubIdx; - subBoardMenu.NumItems = function() { - return this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list.length; - }; - subBoardMenu.GetItem = function(pSubSubIdx) { - var menuItemObj = this.MakeItemWithRetval(-1); - if ((pSubSubIdx >= 0) && (pSubSubIdx < this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list.length)) + + /* + // If this menu contaons only sub-boards, then set the + // item color arrays here + if (this.allSubs) { - var showSubBoardMark = false; - if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != "")) - showSubBoardMark = this.areaChooser.CurrentSubBoardIsInSubSubsForSub(this.grpIdx, +(this.subIdx)); - menuItemObj.text = (showSubBoardMark ? "*" : " "); - var subdirDesc = this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list[pSubSubIdx].description; - var subdirDirIdx = this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list[pSubSubIdx].index; - var subCode = this.areaChooser.group_list[this.grpIdx].sub_list[this.subIdx].sub_subboard_list[pSubSubIdx].code; - menuItemObj.text = strip_ctrl(this.areaChooser.GetMsgSubBrdLine(this.grpIdx, msg_area.sub[subCode].index, false)); - menuItemObj.retval = pSubSubIdx; + // Menu item color arrays + menuItemObj.itemColor = msgAreaMenu.itemColorForSubBoard; + menuItemObj.itemSelectedColor = msgAreaMenu.selectedItemColorForSubBoard; } + */ - return menuItemObj; - } + var grpIdx = this.msgAreaHeirarchyObj[pItemIdx].subObj.grp_index; + // Ensure the subBoardListPrintfInfo object is built for the given group + this.areaChooser.BuildSubBoardPrintfInfoForGrp(grpIdx); + // Menu item text + menuItemObj.text = (showDirMark ? "*" : " "); + if (this.allowANSI) + { + // Menu item color arrays + // Not entirely sure why the heirarchy level matters + if (pHeirarchyLevel > 2) + { + + // Weird hacky stuff to get the color alignments right for this situation + var itemColorIdxInfo = this.areaChooser.GetSubBoardColorIndexInfoForLightbarMenu(pItemNumWidth, this.msgAreaHeirarchyObj.length.toString().length); + menuItemObj.itemColor = itemColorIdxInfo.itemColorForSubBoard; + menuItemObj.itemSelectedColor = itemColorIdxInfo.selectedItemColorForSubBoard; + } + else + { + menuItemObj.itemColor = this.itemColorForSubBoard; + menuItemObj.itemSelectedColor = this.selectedItemColorForSubBoard; + } - // Set the currently selected item. If the current sub-board is in this list, - // then set the selected item to that; otherwise, the selected item should be - // the first sub-board. - if (msg_area.sub[bbs.cursub_code].grp_index == pGrpIdx) - { - for (var i = 0; i < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length; ++i) + // Note: this.areaChooser.subBoardNameLen and this.areaChooser.subBoardListPrintfInfo[grpIdx].nameLen + // are probably the same. + // Get the timestamp of the last message, if configured to do so + if (this.areaChooser.showDatesInSubBoardList) + { + var lastMsgPostTimestamp = getLatestMsgTime(this.msgAreaHeirarchyObj[pItemIdx].subObj.code); + menuItemObj.text += format(this.areaChooser.subBoardListPrintfInfo[grpIdx].printfStr, pItemIdx+1, + this.msgAreaHeirarchyObj[pItemIdx].name.substr(0, this.areaChooser.subBoardNameLen), numItems, + strftime("%Y-%m-%d", lastMsgPostTimestamp), + strftime("%H:%M:%S", lastMsgPostTimestamp)); + } + else + { + menuItemObj.text += format(this.areaChooser.subBoardListPrintfInfo[grpIdx].printfStr, pItemIdx+1, + this.msgAreaHeirarchyObj[pItemIdx].name.substr(0, this.areaChooser.subBoardNameLen), numItems); + } + } + else { - if (bbs.cursub_code == this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[i].code) + // No ANSI - Numbered mode + + // Number of spaces before the group name + var numSpaces = pItemNumWidth - this.numItemsLen - console.strlen(menuItemObj.text); + if (numSpaces > 0) + menuItemObj.text += format("%*s", numSpaces, " "); + + // If this menu only has sub-boards, then use the original attribute arrays + // that were set up for the menu + if (this.allSubs) + { + // Not entirely sure why the heirarchy level matters + if (pHeirarchyLevel > 2) + { + var itemColorIdxInfo = this.areaChooser.GetColorIndexInfoForLightbarMenu(this.msgAreaHeirarchyObj, pItemNumWidth+1); + menuItemObj.itemColor = itemColorIdxInfo.itemColorForSubBoard; + menuItemObj.itemSelectedColor = itemColorIdxInfo.selectedItemColorForSubBoard; + } + else + { + menuItemObj.itemColor = msgAreaMenu.itemColorForSubBoard; + menuItemObj.itemSelectedColor = msgAreaMenu.selectedItemColorForSubBoard; + } + } + else + { + // This menu has a mix of sub-boards and other groups of items + // Menu item color arrays + //var itemColorIdxInfo = this.areaChooser.GetColorIndexInfoForLightbarMenu(this.msgAreaHeirarchyObj[pItemIdx]); + var itemColorIdxInfo = this.areaChooser.GetColorIndexInfoForLightbarMenu(this.msgAreaHeirarchyObj); + if (numSpaces > 0) + { + menuItemObj.itemColor = AdjustMenuAttrArrayIndexes(itemColorIdxInfo.itemColorForSubBoard, numSpaces-1); + menuItemObj.itemSelectedColor = AdjustMenuAttrArrayIndexes(itemColorIdxInfo.selectedItemColorForSubBoard, numSpaces-1); + } + else + { + menuItemObj.itemColor = itemColorIdxInfo.itemColorForSubBoard; + menuItemObj.itemSelectedColor = itemColorIdxInfo.selectedItemColorForSubBoard; + } + } + + // Generate the item text + if (this.areaChooser.showDatesInSubBoardList) + { + var lastMsgPostTimestamp = getLatestMsgTime(this.msgAreaHeirarchyObj[pItemIdx].subObj.code); + menuItemObj.text += format(this.areaChooser.subBoardListPrintfInfo[grpIdx].printfStrWithoutAreaNum, + this.msgAreaHeirarchyObj[pItemIdx].name.substr(0, this.areaChooser.subBoardNameLen), numItems, + strftime("%Y-%m-%d", lastMsgPostTimestamp), + strftime("%H:%M:%S", lastMsgPostTimestamp)); + //var itemColorIdxInfo = this.areaChooser.GetColorIndexInfoForLightbarMenu(this.msgAreaHeirarchyObj[pItemIdx]); + //var itemColorIdxInfo = this.areaChooser.GetColorIndexInfoForLightbarMenu(this.msgAreaHeirarchyObj); + } + else { - subBoardMenu.selectedItemIdx = i; - if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage()) - subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1; - break; + menuItemObj.text += format(this.areaChooser.subBoardListPrintfInfo[grpIdx].printfStrWithoutAreaNum, + this.msgAreaHeirarchyObj[pItemIdx].name.substr(0, this.areaChooser.subBoardNameLen), numItems); } } } - } - } - else - { - subBoardMenu.NumItems = function() { - return msg_area.grp_list[this.grpIdx].sub_list.length; - }; - subBoardMenu.GetItem = function(pSubIdx) { - var menuItemObj = this.MakeItemWithRetval(-1); - if ((pSubIdx >= 0) && (pSubIdx < msg_area.grp_list[this.grpIdx].sub_list.length)) - { - var showSubBoardMark = false; - if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != "")) - showSubBoardMark = ((this.grpIdx == msg_area.sub[bbs.cursub_code].grp_index) && (pSubIdx == msg_area.sub[bbs.cursub_code].index)); - menuItemObj.text = strip_ctrl(this.areaChooser.GetMsgSubBrdLine(this.grpIdx, pSubIdx, false)); - menuItemObj.retval = pSubIdx; - } + menuItemObj.text = strip_ctrl(menuItemObj.text); + menuItemObj.retval = pItemIdx; return menuItemObj; }; - - // Set the currently selected item. If the current sub-board is in this list, - // then set the selected item to that; otherwise, the selected item should be - // the first sub-board. - if (msg_area.sub[bbs.cursub_code].grp_index == pGrpIdx) - { - subBoardMenu.selectedItemIdx = msg_area.sub[bbs.cursub_code].index; - if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage()) - subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1; - } - else + + // Set the currently selected item + var selectedIdx = msgAreaMenu.idxWithUserSelectedSubBoard; + if (typeof(pSelectedItemIdx) === "number" && pSelectedItemIdx >= 0 && pSelectedItemIdx < msgAreaMenu.NumItems()) + selectedIdx = pSelectedItemIdx; + if (selectedIdx >= 0 && selectedIdx < msgAreaMenu.NumItems()) { - subBoardMenu.selectedItemIdx = 0; - subBoardMenu.topItemIdx = 0; + msgAreaMenu.selectedItemIdx = selectedIdx; + if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage()) + msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1; } } - return subBoardMenu; + retObj.menuObj = msgAreaMenu; + return retObj; } - -// For the DDMsgAreaChooser class: Lets the user choose a message group and -// sub-board via numeric input, using a traditional user interface. +// Helper for DDMsgAreaChooser_CreateLightbarMenu(): Returns arrays of objects with start, end, and attrs properties +// for the lightbar menu to add colors to the menu items. // // Parameters: -// pChooseGroup: Boolean - Whether or not to choose the message group. If false, -// then this will allow choosing a sub-board within the user's -// current message group. This is optional; defaults to true. -function DDMsgAreaChooser_SelectMsgArea_Traditional(pChooseGroup) +// pMsgAreaHeirarchyObj: An object from this.msgArea_list, which is +// set up with a 'name' property and either +// an 'items' property if it has sub-items +// or a 'subObj' property if it's a sub-board +// pAreaNumWidthOverride: Optional - An override for the width of the area # column. Mainly for the traditional (non-lightbar) UI +// If this is not specified/null, then this.areaNumLen will be used. +// pColorIdxOffset: Optional - An offset to adjust the color indexes by in the color arrays. +// Must be positive. +function DDMsgAreaChooser_GetColorIndexInfoForLightbarMenu(pMsgAreaHeirarchyObj, pAreaNumWidthOverride, pColorIdxOffset) { - // If there are no message groups, then don't let the user - // choose one. - if (msg_area.grp_list.length == 0) - { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no message groups.\r\n\x01p"); - return; - } + var retObj = { + msgGrpListIdxes: {}, + subBoardListIdxes: {}, + itemColorForSubBoard: [], + selectedItemColorForSubBoard: [], + itemColorForItemWithSubItems: [], + selectedItemColorForItemWithSubItems: [] + }; + + var usingLightbarInterface = this.useLightbarInterface && console.term_supports(USER_ANSI); + //var areaNumLen = usingLightbarInterface ? this.areaNumLen : pMsgAreaHeirarchyObj.length.toString().length; + var areaNumLen = this.areaNumLen; + if (typeof(pAreaNumWidthOverride) === "number" && pAreaNumWidthOverride >= 0) + areaNumLen = pAreaNumWidthOverride; - var chooseGroup = (typeof(pChooseGroup) == "boolean" ? pChooseGroup : true); - if (chooseGroup) + // Find the length of the highest number of messages of sub-boards in pMsgAreaHeirarchyObj + var highestNumMsgs = 0; + for (var i = 0; i < pMsgAreaHeirarchyObj.length; ++i) { - // Show the message groups & sub-boards and let the user choose one. - var selectedGrp = 0; // The user's selected message group - var selectedSubBoard = 0; // The user's selected sub-board - var usersCurrentIdxVals = getGrpAndSubIdxesFromCode(bbs.cursub_code, true); - var grpSearchText = ""; - var continueChoosingMsgArea = true; - while (continueChoosingMsgArea) + if (pMsgAreaHeirarchyObj[i].hasOwnProperty("subObj")) { - // Clear the BBS command string to make sure there are no extra - // commands in there that could cause weird things to happen. - bbs.command_str = ""; - - console.clear("\x01n"); - this.DisplayAreaChgHdr(1); - if (this.areaChangeHdrLines.length > 0) - console.crlf(); - this.ListMsgGrps(grpSearchText); - console.crlf(); - console.print("\x01n\x01b\x01h� \x01n\x01cWhich, \x01hQ\x01n\x01cuit, \x01hCTRL-F\x01n\x01c, \x01h/\x01n\x01c, or [\x01h" + +(usersCurrentIdxVals.grpIdx+1) + "\x01n\x01c]: \x01h"); - // Accept Q (quit), / or CTRL_F (Search) or a file library number - selectedGrp = console.getkeys("Q/" + CTRL_F, msg_area.grp_list.length); - - // If the user just pressed enter (selectedGrp would be blank), - // default to the current group. - if (selectedGrp.toString() == "") - selectedGrp = usersCurrentIdxVals.grpIdx + 1; - - if (selectedGrp.toString() == "Q") - continueChoosingMsgArea = false; - else if ((selectedGrp.toString() == "/") || (selectedGrp.toString() == CTRL_F)) + // The 'posts' property was added in Synchronet 3.18c + if (msg_area.sub[pMsgAreaHeirarchyObj[i].subObj.code].hasOwnProperty("posts")) { - console.crlf(); - var searchPromptText = "\x01n\x01c\x01hSearch\x01g: \x01n"; - console.print(searchPromptText); - var searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE); - if (searchText.length > 0) - grpSearchText = searchText; + if (msg_area.sub[pMsgAreaHeirarchyObj[i].subObj.code].posts > highestNumMsgs) + highestNumMsgs = msg_area.sub[pMsgAreaHeirarchyObj[i].subObj.code].posts; } else { - grpSearchText = ""; - - // If the user specified a message group number, then - // set it and let the user choose a sub-board within - // the group. - if (selectedGrp > 0) + // Get the total number of messages in the sub-board. It isn't accurate, but it's fast. + var msgBase = new MsgBase(pMsgAreaHeirarchyObj[i].subObj.code); + if (msgBase.open()) { - // Set the default sub-board #: The current sub-board, or if the - // user chose a different group, then this should be set - // to the first sub-board. - var defaultSubBoard = usersCurrentIdxVals.subIdx + 1; - if (selectedGrp-1 != usersCurrentIdxVals.grpIdx) - defaultSubBoard = 1; - - console.clear("\x01n"); - var selectSubRetVal = this.SelectSubBoard_Traditional(selectedGrp-1, defaultSubBoard-1); - // If the user chose a directory, then set the user's - // message sub-board and quit the message group loop. - if (selectSubRetVal.subBoardCode != "") - { - continueChoosingMsgArea = false; - bbs.cursub_code = selectSubRetVal.subBoardCode; - } + if (msgBase.total_msgs > highestNumMsgs) + highestNumMsgs = msgBase.total_msgs; + msgBase.close(); } } } } - else - { - // Don't choose a group, just a sub-board within the user's current group. - var idxVals = getGrpAndSubIdxesFromCode(bbs.cursub_code, true); - var selectSubRetVal = this.SelectSubBoard_Traditional(idxVals.grpIdx, idxVals.subIdx); - // If the user chose a directory, then set the user's sub-board. - if (selectSubRetVal.subBoardCode != "") - bbs.cursub_code = selectSubRetVal.subBoardCode; - } -} - -// For the DDMsgAreaChooser class: Allows the user to select a sub-board with the -// traditional user interface. -// -// Parameters: -// pGrpIdx: The index of the message group to choose a sub-board for -// pDefaultSubBoardIdx: The index of the default sub-board -// -// Return value: An object containing the following values: -// subBoardChosen: Boolean - Whether or not a sub-board was chosen. -// subBoardIndex: Numeric - The sub-board that was chosen (if any). -// Will be -1 if none chosen. -// subBoardCode: The internal code of the chosen sub-board (or "" if none chosen) -function DDMsgAreaChooser_SelectSubBoard_Traditional(pGrpIdx, pDefaultSubBoardIdx) -{ - var retObj = { - subBoardChosen: false, - subBoardIndex: 1, - subBoardCode: "" + var numMsgsLen = highestNumMsgs.toString().length; + + // Start & end indexes for the various items in each item list row + //var lengthsObj = this.GetSubNameLenAndNumMsgsLen(pGrpIdx); // TODO + //var nameLen = console.screen_columns - this.areaNumLen - numMsgsLen - this.dateLen - 14; // Was - 5 + var nameLen = console.screen_columns - areaNumLen - numMsgsLen - this.dateLen - 14; // Was - 5 + if (usingLightbarInterface) + { + retObj.msgGrpListIdxes = { + markCharStart: 0, + markCharEnd: 1, + grpNumStart: 1, + //grpNumEnd: 2 + (+this.areaNumLen) + grpNumEnd: 2 + areaNumLen + }; + retObj.msgGrpListIdxes.descStart = retObj.msgGrpListIdxes.grpNumEnd; + retObj.msgGrpListIdxes.descEnd = retObj.msgGrpListIdxes.descStart + +this.msgGrpDescLen; + + retObj.subBoardListIdxes = { + markCharStart: 0, + markCharEnd: 1, + subNumStart: 1, + //subNumEnd: 3 + (+this.areaNumLen) + subNumEnd: 3 + areaNumLen + }; + retObj.subBoardListIdxes.descStart = retObj.subBoardListIdxes.subNumEnd; } - - var searchText = ""; - var defaultSubBoardIdx = pDefaultSubBoardIdx; - var continueOn = false; - do - { - this.DisplayAreaChgHdr(1); - if (this.areaChangeHdrLines.length > 0) - console.crlf(); - this.ListSubBoardsInMsgGroup(pGrpIdx, null, defaultSubBoardIdx, searchText); - console.crlf(); - if (defaultSubBoardIdx >= 0) - console.print("\x01n\x01b\x01h� \x01n\x01cWhich, \x01hQ\x01n\x01cuit, \x01hCTRL-F\x01n\x01c, \x01h/\x01n\x01c, or [\x01h" + +(defaultSubBoardIdx+1) + "\x01n\x01c]: \x01h"); - else - console.print("\x01n\x01b\x01h� \x01n\x01cWhich, \x01hQ\x01n\x01cuit, \x01hCTRL-F\x01n\x01c, \x01h/\x01n\x01c: \x01h"); - // Accept Q (quit) or a sub-board number - var selectedSubBoard = console.getkeys("Q/" + CTRL_F, msg_area.grp_list[pGrpIdx].sub_list.length); - - // If the user just pressed enter (selectedSubBoard would be blank), - // default the selected directory. - var selectedSubBoardStr = selectedSubBoard.toString(); - if (selectedSubBoardStr == "") - { - if (defaultSubBoardIdx >= 0) - { - selectedSubBoard = defaultSubBoardIdx + 1; // Make this 1-based - continueOn = false; - } - } - else if ((selectedSubBoardStr == "/") || (selectedSubBoardStr == CTRL_F)) - { - // Search - console.crlf(); - var searchPromptText = "\x01n\x01c\x01hSearch\x01g: \x01n"; - console.print(searchPromptText); - searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE); - console.attributes = "N"; - console.crlf(); - if (searchText.length > 0) - defaultSubBoardIdx = -1; - else - defaultSubBoardIdx = pDefaultSubBoardIdx; - continueOn = true; - } - else if (selectedSubBoardStr == "Q") - continueOn = false; - - // If a sub-board was chosen, then select it. - if (selectedSubBoard > 0) - { - var selectedSubIdx = selectedSubBoard - 1; - // If using sub-board name collapsing and the selected sub-board has sub-subboards, then - // let the user choose a sub-subboard within the sub-board. Otherwise, just select the - // current sub-board. - if (this.useSubCollapsing && this.group_list[pGrpIdx].sub_list[selectedSubIdx].sub_subboard_list.length > 0) - { - var subSubRetObj = this.SelectSubSubWithinSub_Traditional(pGrpIdx, selectedSubIdx); - if (subSubRetObj.areaSelected) - { - retObj.subBoardChosen = true; - retObj.subBoardIndex = subSubRetObj.subIndex; - retObj.subBoardCode = subSubRetObj.subCode; - continueOn = false; - } - else // An area wasn't chosen - { - continueOn = true; - console.clear("\x01n"); - } - } - else - { - retObj.subBoardChosen = true; - retObj.subBoardIndex = selectedSubIdx; - retObj.subBoardCode = msg_area.grp_list[pGrpIdx].sub_list[selectedSubIdx].code; - continueOn = false; - } - } - } while (continueOn); - - return retObj; -} - -// For the DDMsgAreaChooser class: Lets the user select a sub-subbboard within a -// message sub-board - Traditional user interface. This is meant for sub-board -// name collapsing, at the 3rd level. -// -// Parameters: -// pGrpIdx: The message group index -// pSubIdx: The index of the sub-board within the message group -// -// Return value: An object containing the following properties: -// areaSelected: Boolean - Whether or not the user chose a sub-subboard. -// subIndex: The index of the sub-board in Synchronet's sub_list array in the group -// subCode: The internal code of the sub-board chosen, if chose. If not chosen, -// this will be an empty string. -function DDMsgAreaChooser_SelectSubSubWithinSub_Traditional(pGrpIdx, pSubIdx) -{ - var retObj = { - areaSelected: false, - subIndex: -1, - subCode: "" - }; - - if (!this.useSubCollapsing || this.group_list.length == 0) - return retObj; - if ((pGrpIdx < 0) || (pGrpIdx >= this.group_list.length)) - return retObj; - if ((pSubIdx < 0) || (pSubIdx >= this.group_list[pGrpIdx].sub_list.length)) + else { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no sub-boards in this message group.\r\n\x01p"); - return retObj; + retObj.msgGrpListIdxes = { + markCharStart: 0, + markCharEnd: 1, + descStart: 1 + }; + retObj.subBoardListIdxes = { + markCharStart: 0, + markCharEnd: 1, + descStart: 1 + }; } - if (this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length == 0) - { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no sub-subboards in this sub-board.\r\n\x01p"); - return retObj; + // Remainder of the message group colors + retObj.msgGrpListIdxes.descEnd = 2 + (+this.msgGrpDescLen); + retObj.msgGrpListIdxes.numItemsStart = retObj.msgGrpListIdxes.descEnd; + // Set numItemsEnd to -1 to let the whole rest of the lines be colored + retObj.msgGrpListIdxes.numItemsEnd = -1; + // Remainder of sub-board colors + // Note: this.areaChooser.subBoardNameLen and this.areaChooser.subBoardListPrintfInfo[grpIdx].nameLen + // for a message group are probably the same. + // Get the timestamp of the last message, if configured to do so + //retObj.subBoardListIdxes.descEnd = retObj.subBoardListIdxes.descStart + nameLen; + retObj.subBoardListIdxes.descEnd = retObj.subBoardListIdxes.descStart + (+this.subBoardNameLen); + // For the sub-board list, if not using the lightbar interface, we still need + // to account for the length of the item numbers, which will be displayed by + // the menu object rather than by us + if (!usingLightbarInterface) + { + //retObj.subBoardListIdxes.descEnd += this.areaNumLen; + retObj.subBoardListIdxes.descEnd += areaNumLen; } - - // Gets the default sub-subdirectory number (1-based) - function getDefaultSubSubNum(pGrpList, pGrpIdx, pSubIdx) + retObj.subBoardListIdxes.numItemsStart = retObj.subBoardListIdxes.descEnd; + retObj.subBoardListIdxes.numItemsEnd = retObj.subBoardListIdxes.numItemsStart + numMsgsLen + 1; + //retObj.subBoardListIdxes.numItemsEnd = retObj.subBoardListIdxes.numItemsStart + numMsgsLen+2 + 1; + if (this.showDatesInSubBoardList) { - var subSubNum = 0; // Will be 1-based - for (var subSubIdx = 0; subSubIdx < pGrpList[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length; ++subSubIdx) - { - if (bbs.curdir_code == pGrpList[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[subSubIdx].code) - { - subSubNum = subSubIdx + 1; - break; - } - } - return subSubNum; + retObj.subBoardListIdxes.dateStart = retObj.subBoardListIdxes.numItemsEnd; + retObj.subBoardListIdxes.dateEnd = retObj.subBoardListIdxes.dateStart + +this.dateLen + 1; + retObj.subBoardListIdxes.timeStart = retObj.subBoardListIdxes.dateEnd; + // Set timeEnd to -1 to let the whole rest of the lines be colored + retObj.subBoardListIdxes.timeEnd = -1; } - // defaultSubSubNum is the default sub-subboard # (will be 1-based) - var defaultSubSubNum = getDefaultSubSubNum(this.group_list, pGrpIdx, pSubIdx); - var searchText = ""; - var continueOn = false; - do + // Menu item colors + if (usingLightbarInterface) { - console.clear("\x01n"); - this.DisplayAreaChgHdr(1); - if (this.areaChangeHdrLines.length > 0) - console.crlf(); - // Note: This will list sub-subboards within the sub-board with a valid sub-board index - // as the 2nd parameter. - // TODO: When quitting out of the sub-suboard list, it's going back to the - // message group list - this.ListSubBoardsInMsgGroup(pGrpIdx, pSubIdx, defaultSubSubNum, searchText); - if (defaultSubSubNum >= 1) - console.print("\x01n\x01b\x01h� \x01n\x01cWhich, \x01hQ\x01n\x01cuit, \x01hCTRL-F\x01n\x01c, \x01h/\x01n\x01c, or [\x01h" + defaultSubSubNum + "\x01n\x01c]: \x01h"); - else - console.print("\x01n\x01b\x01h� \x01n\x01cWhich, \x01hQ\x01n\x01cuit, \x01hCTRL-F\x01n\x01c, \x01h/\x01n\x01c: \x01h"); - // Accept Q (quit), / or CTRL_F to search, or a file sub-board number - var selectedSubSubNum = console.getkeys("Q/" + CTRL_F, this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length); - - // If the user just pressed enter (selectedSubSubNum would be blank), - // default the selected sub-board. - if (selectedSubSubNum.toString() == "Q") - continueOn = false; - else if (selectedSubSubNum.toString() == "") - selectedSubSubNum = defaultSubSubNum; - else if ((selectedSubSubNum == "/") || (selectedSubSubNum == CTRL_F)) - { - // Search - console.crlf(); - var searchPromptText = "\x01n\x01c\x01hSearch\x01g: \x01n"; - console.print(searchPromptText); - searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE); - console.attributes = "N"; - console.crlf(); - if (searchText.length > 0) - defaultSubSubNum = -1; - else - defaultSubSubNum = getDefaultSubSubNum(this.group_list, pGrpIdx, pSubIdx); - continueOn = true; - console.line_counter = 0; // To avoid pausing before the clear screen - } - - // If the user chose a sub-board, then set the user's message sub-board. - if (selectedSubSubNum > 0) + // Colors for items that are sub-boards + if (this.showDatesInSubBoardList) { - continueOn = false; - retObj.areaSelected = true; - retObj.subCode = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[selectedSubSubNum-1].code; - retObj.subIndex = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[selectedSubSubNum-1].index; + retObj.itemColorForSubBoard = [{start: retObj.subBoardListIdxes.markCharStart, end: retObj.subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark}, + {start: retObj.subBoardListIdxes.subNumStart, end: retObj.subBoardListIdxes.subNumEnd, attrs: this.colors.areaNum}, + {start: retObj.subBoardListIdxes.descStart, end: retObj.subBoardListIdxes.descEnd, attrs: this.colors.desc}, + {start: retObj.subBoardListIdxes.numItemsStart, end: retObj.subBoardListIdxes.numItemsEnd, attrs: this.colors.numItems}, + {start: retObj.subBoardListIdxes.dateStart, end: retObj.subBoardListIdxes.dateEnd, attrs: this.colors.latestDate}, + {start: retObj.subBoardListIdxes.timeStart, end: retObj.subBoardListIdxes.timeEnd, attrs: this.colors.latestTime}]; + retObj.selectedItemColorForSubBoard = [{start: retObj.subBoardListIdxes.markCharStart, end: retObj.subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.subNumStart, end: retObj.subBoardListIdxes.subNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.descStart, end: retObj.subBoardListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.numItemsStart, end: retObj.subBoardListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.dateStart, end: retObj.subBoardListIdxes.dateEnd, attrs: this.colors.dateHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.timeStart, end: retObj.subBoardListIdxes.timeEnd, attrs: this.colors.timeHighlight + this.colors.bkgHighlight}]; } - } while (continueOn); - - return retObj; -} - -// For the DDMsgAreaChooser class: Lists all message groups (for the traditional -// user interface). -// -// Parameters: -// pSearchText: Optional - Search text for the message groups -function DDMsgAreaChooser_ListMsgGrps_Traditional(pSearchText) -{ - // Print the header - this.WriteGrpListHdrLine(); - console.attributes = "N"; - - var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : ""); - - // List the message groups - var printIt = true; - for (var i = 0; i < msg_area.grp_list.length; ++i) - { - if (searchText.length > 0) - printIt = ((msg_area.grp_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (msg_area.grp_list[i].description.toUpperCase().indexOf(searchText) >= 0)); else - printIt = true; - - if (printIt) { - console.crlf(); - this.WriteMsgGroupLine(i, false); - } + retObj.itemColorForSubBoard = [{start: retObj.subBoardListIdxes.markCharStart, end: retObj.subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark}, + {start: retObj.subBoardListIdxes.subNumStart, end: retObj.subBoardListIdxes.subNumEnd, attrs: this.colors.areaNum}, + {start: retObj.subBoardListIdxes.descStart, end: retObj.subBoardListIdxes.descEnd, attrs: this.colors.desc}, + {start: retObj.subBoardListIdxes.numItemsStart, end: retObj.subBoardListIdxes.numItemsEnd, attrs: this.colors.numItems}]; + retObj.selectedItemColorForSubBoard = [{start: retObj.subBoardListIdxes.markCharStart, end: retObj.subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.subNumStart, end: retObj.subBoardListIdxes.subNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.descStart, end: retObj.subBoardListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.numItemsStart, end: retObj.subBoardListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}]; + } + // Colors for items that have another list of items + retObj.itemColorForItemWithSubItems = [{start: retObj.msgGrpListIdxes.markCharStart, end: retObj.msgGrpListIdxes.markCharEnd, attrs: this.colors.areaMark}, + {start: retObj.msgGrpListIdxes.grpNumStart, end: retObj.msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaNum}, + {start: retObj.msgGrpListIdxes.descStart, end: retObj.msgGrpListIdxes.descEnd, attrs: this.colors.desc}, + {start: retObj.msgGrpListIdxes.numItemsStart, end: retObj.msgGrpListIdxes.numItemsEnd, attrs: this.colors.numItems}]; + retObj.selectedItemColorForItemWithSubItems = [{start: retObj.msgGrpListIdxes.markCharStart, end: retObj.msgGrpListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, + {start: retObj.msgGrpListIdxes.grpNumStart, end: retObj.msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight}, + {start: retObj.msgGrpListIdxes.descStart, end: retObj.msgGrpListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, + {start: retObj.msgGrpListIdxes.numItemsStart, end: retObj.msgGrpListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}]; } -} - -// For the DDMsgAreaChooser class: Lists the sub-boards in a message group -// (or sub-subboards in a sub-board, for sub-board name collapsing), for the -// traditional user interface. -// -// Parameters: -// pGrpIndex: The index of the message group (0-based) -// pSubIdx: Optional - For sub-board name collapsing, this is the (0-based) -// index of the sub-board to show sub-subboards for. To ignore this -// and only show sub-boards of the given group, this can be null or -1. -// pMarkIndex: An index of a message group to highlight. This -// is optional; if left off, this will default to -// the current sub-board. -// pSearchText: Optional - Search text for the sub-boards -// pSortType: Optional - A string describing how to sort the list (if desired): -// "none": Default behavior - Sort by sub-board # -// "dateAsc": Sort by date, ascending -// "dateDesc": Sort by date, descending -// "description": Sort by description -function DDMsgAreaChooser_ListSubBoardsInMsgGroup_Traditional(pGrpIndex, pSubIdx, pMarkIndex, pSearchText, pSortType) -{ - // Default to the current message group & sub-board if pGrpIndex - // and pMarkIndex aren't specified. - var grpIndex = 0; - if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != "")) - grpIndex = msg_area.sub[bbs.cursub_code].grp_index; - if ((pGrpIndex != null) && (typeof(pGrpIndex) === "number")) - grpIndex = pGrpIndex; - var highlightIndex = 0; - if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != "")) - highlightIndex = (pGrpIndex == msg_area.sub[bbs.cursub_code].index); - if ((pMarkIndex != null) && (typeof(pMarkIndex) === "number")) - highlightIndex = pMarkIndex; - - // Make sure grpIndex and highlightIndex are valid (they might not be for - // brand-new users). - if ((grpIndex == null) || (typeof(grpIndex) == "undefined")) - grpIndex = 0; - if ((highlightIndex == null) || (typeof(highlightIndex) == "undefined")) - highlightIndex = 0; - - // Check whether pSubIdx is valid - var subIdxValid = typeof(pSubIdx) === "number" && (pSubIdx > -1); - - // Ensure that the sub-board printf information is created for - // this message group. - this.BuildSubBoardPrintfInfoForGrp(grpIndex); - - // Print the headers - this.WriteSubBrdListHdr1Line(grpIndex); - console.crlf(); - var itemsHdrStr = "Posts"; - if (this.useSubCollapsing) - itemsHdrStr = subIdxValid ? "Posts" : "Items"; - if (this.showDatesInSubBoardList) - printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + itemsHdrStr, "Latest date & time"); else - printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# " + itemsHdrStr); - console.attributes = "N"; - - // Make the search text uppercase for case-insensitive matching - var searchTextUpper = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : ""); - - // List each sub-board in the message group. - var subBoardArray = null; // For sorting, if desired - var newestDate = {}; // For storing the date of the newest post in a sub-board - var msgBase = null; // For opening the sub-boards with a MsgBase object - var msgHeader = null; // For getting the date & time of the newest post in a sub-board - var subBoardNum = 0; // 0-based sub-board number (because the array index is the number as a str) - // If a sort type is specified, then add the sub-board information to - // subBoardArray so that it can be sorted. - if ((typeof(pSortType) == "string") && (pSortType != "") && (pSortType != "none")) - { - var addSubBoard = true; - subBoardArray = []; - var subBoardInfo = null; - var subList = msg_area.grp_list[grpIndex].sub_list; - if (this.useSubCollapsing) - { - if (subIdxValid) - subList = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list; - else - subList = this.group_list[grpIndex].sub_list; - } - for (var subIdx in subList) - { - // Open the current sub-board with the msgBase object. - // If the search text is set, then use it to filter the sub-boards. - addSubBoard = true; - if (searchTextUpper.length > 0) - { - if (this.useSubCollapsing && subIdxValid) - { - // For sub-board name collapsing, sub-subboards only have descriptions, no names - addSubBoard = (this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].description.indexOf(searchTextUpper) >= 0); - } - else - { - addSubBoard = ((msg_area.grp_list[grpIndex].sub_list[subIdx].name.indexOf(searchTextUpper) >= 0) || - (msg_area.grp_list[grpIndex].sub_list[subIdx].description.indexOf(searchTextUpper) >= 0)); - } - } - if (addSubBoard) - { - var subCode = ""; - if (this.useSubCollapsing && subIdxValid) - subCode = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].code; - else - subCode = msg_area.grp_list[grpIndex].sub_list[subIdx].code; - msgBase = new MsgBase(subCode); - if (msgBase.open()) - { - subBoardInfo = new MsgSubBoardInfo(); - subBoardInfo.subBoardNum = +(subIdx); - if (this.useSubCollapsing && subIdxValid) - { - subBoardInfo.subBoardIdx = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].index; - subBoardInfo.description = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].description; - } - else - { - subBoardInfo.subBoardIdx = msg_area.grp_list[grpIndex].sub_list[subIdx].index; - subBoardInfo.description = msg_area.grp_list[grpIndex].sub_list[subIdx].description; - } - - subBoardInfo.numPosts = numReadableMsgs(msgBase, msg_area.grp_list[grpIndex].sub_list[subIdx].code); - - // Get the date & time when the last message was imported. - if (this.showDatesInSubBoardList && (subBoardInfo.numPosts > 0)) - { - //var msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true); - var msgHeader = getLatestMsgHdr(msg_area.grp_list[grpIndex].sub_list[subIdx].code); - if (msgHeader === null) - msgHeader = getBogusMsgHdr(); - if (this.showImportDates) - subBoardInfo.newestPostDate = msgHeader.when_imported_time; - else - { - var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader); - if (msgWrittenLocalBBSTime != -1) - subBoardInfo.newestPostDate = msgWrittenLocalBBSTime; - else - subBoardInfo.newestPostDate = msgHeader.when_written_time; - } - } - } - msgBase.close(); - subBoardArray.push(subBoardInfo); - delete msgBase; // Free some memory? - } - } - - // Sort sub-board list. - if (pSortType == "dateAsc") - { - subBoardArray.sort(function(pA, pB) - { - // Return -1, 0, or 1, depending on whether pA's date comes - // before, is equal to, or comes after pB's date. - var returnValue = 0; - if (pA.newestPostDate < pB.newestPostDate) - returnValue = -1; - else if (pA.newestPostDate > pB.newestPostDate) - returnValue = 1; - return returnValue; - }); - } - else if (pSortType == "dateDesc") - { - if (this.showDatesInSubBoardList) - { - subBoardArray.sort(function(pA, pB) - { - // Return -1, 0, or 1, depending on whether pA's date comes - // after, is equal to, or comes before pB's date. - var returnValue = 0; - if (pA.newestPostDate > pB.newestPostDate) - returnValue = -1; - else if (pA.newestPostDate < pB.newestPostDate) - returnValue = 1; - return returnValue; - }); - } - } - else if (pSortType == "description") - { - // Binary safe string comparison - // - // version: 909.322 - // discuss at: http://phpjs.org/functions/strcmp // + original by: Waldo Malqui Silva - // + input by: Steve Hilder - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + revised by: gorthaur - // * example 1: strcmp( 'waldo', 'owald' ); // * returns 1: 1 - // * example 2: strcmp( 'owald', 'waldo' ); - // * returns 2: -1 - subBoardArray.sort(function(pA, pB) - { - return ((pA.description == pB.description) ? 0 : ((pA.description > pB.description) ? 1 : -1)); - }); - } - - // Display the sub-board list. - for (var i = 0; i < subBoardArray.length; ++i) - { - console.crlf(); - var showSubBoardMark = false; - if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != "")) - { - if (subBoardArray[i].subBoardNum == highlightIndex) - showSubBoardMark = ((grpIndex == msg_area.sub[bbs.cursub_code].grp_index) && (highlightIndex == subBoardArray[i].subBoardIdx)); - } - console.print(showSubBoardMark ? "\x01n" + this.colors.areaMark + "*" : " "); - if (this.showDatesInSubBoardList) - { - printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardArray[i].subBoardNum+1), - subBoardArray[i].description.substr(0, this.subBoardNameLen), - subBoardArray[i].numPosts, strftime("%Y-%m-%d", subBoardArray[i].newestPostDate), - strftime("%H:%M:%S", subBoardArray[i].newestPostDate)); - } - else - { - printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardArray[i].subBoardNum+1), - subBoardArray[i].description.substr(0, this.subBoardNameLen), - subBoardArray[i].numPosts, strftime("%Y-%m-%d", subBoardArray[i].newestPostDate)); - } - } - } - // If no sort type is specified, then output the sub-board information in - // order of sub-board number. - else - { - var includeSubBoard = true; - //var subList = this.useSubCollapsing ? this.group_list[grpIndex].sub_list : msg_area.grp_list[grpIndex].sub_list; - var subList = msg_area.grp_list[grpIndex].sub_list; - if (this.useSubCollapsing) - { - if (subIdxValid) - subList = this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list; - else - subList = this.group_list[grpIndex].sub_list; - } - for (var subIdx in subList) - { - // If the search text is set, then use it to filter the sub-board list. - includeSubBoard = true; - if (searchTextUpper.length > 0) - { - if (this.useSubCollapsing && subIdxValid) - { - // For sub-board name collapsing, sub-subboards only have descriptions, no names - includeSubBoard = (this.group_list[grpIndex].sub_list[pSubIdx].sub_subboard_list[subIdx].description.indexOf(searchTextUpper) >= 0); - } - else - { - includeSubBoard = ((msg_area.grp_list[grpIndex].sub_list[subIdx].name.toUpperCase().indexOf(searchTextUpper) >= 0) || - (msg_area.grp_list[grpIndex].sub_list[subIdx].description.toUpperCase().indexOf(searchTextUpper) >= 0)); - } - } - if (includeSubBoard) - { - // Call GetSubBoardInfo() to get the information about the sub-board or sub-subboard. - // Make sure we pass the correct indexes. - var subInfo = null; - if (this.useSubCollapsing) - { - // For some reason subIdx is a string, so ensure it's a number when calling GetSubBoardInfo() - if (subIdxValid) // If pSubIdx is valid - subInfo = this.GetSubBoardInfo(grpIndex, pSubIdx, +subIdx); - else - subInfo = this.GetSubBoardInfo(grpIndex, +subIdx); - } - else - subInfo = this.GetSubBoardInfo(grpIndex, subIdx); - newestDate.date = strftime("%Y-%m-%d", subInfo.newestTime); - newestDate.time = strftime("%H:%M:%S", subInfo.newestTime); - // Print the sub-board information - subBoardNum = +(subIdx); - console.crlf(); - var showSubBoardMark = false; - if (this.useSubCollapsing) - { - if (subIdxValid) // If using sub-subboards - showSubBoardMark = (bbs.cursub_code == subList[subIdx].code); - else - showSubBoardMark = this.CurrentSubBoardIsInSubSubsForSub(grpIndex, +subIdx); - } - else - showSubBoardMark = (subBoardNum == highlightIndex); - console.print(showSubBoardMark ? "\x01n" + this.colors.areaMark + "*" : " "); - var lengthsObj = this.GetSubNameLenAndNumMsgsLen(grpIndex); - if (this.showDatesInSubBoardList) - { - printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1), - subInfo.desc.substr(0, lengthsObj.nameLen), subInfo.numItems, - newestDate.date, newestDate.time); - } - else - { - printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1), - subInfo.desc.substr(0, lengthsObj.nameLen, subInfo.numItems)); - } - } - } - } -} - -// For the DDMsgAreaChooser class: Returns whether the user's current selected sub-board is -// one of the sub-subboards for a sub-board (if sub-board name collapsing is enabled), or -// is the given sub-board in the message group. -// -// Parameters: -// pGrpIdx: The index of the message group (0-based) -// pSubIdx: The index of the sub-board within the message group (0-based) -// -// Return value: True/false, whether or not the user's current selected sub-board is one of -// the sub-subboards for a sub-board (if sub-board name collapsing is enabled) -// or is the given sub-board in the message group. -function DDMsgAreaChooser_CurrentSubBoardIsInSubSubsForSub(pGrpIdx, pSubIdx) -{ - // Sanity checking - if (typeof(bbs.cursub_code) !== "string") // Rare case, for brand new user accounts - return false; - if (typeof(pGrpIdx) !== "number") - return false; - if (typeof(pSubIdx) !== "number") - return false; - - var chosenSubMatch = false; - if (this.useSubCollapsing) { - if (pGrpIdx >= 0 && pGrpIdx < this.group_list.length && pSubIdx >= 0 && pSubIdx < this.group_list[pGrpIdx].sub_list.length) + // Colors for items that are sub-boards + if (this.showDatesInSubBoardList) { - // If this sub-board has a list of sub-subboards, then go through the sub-subboards and see if the - // user's current sub-board is one of the sub-subboards. - if (typeof(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list) !== "undefined" && this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length > 0) - { - for (var subSubIdx = 0; subSubIdx < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length && !chosenSubMatch; ++subSubIdx) - chosenSubMatch = (bbs.cursub_code == this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[subSubIdx].code); - } - else // There is no sub-subboard list for this sub-board - chosenSubMatch = (bbs.cursub_code == this.group_list[pGrpIdx].sub_list[pSubIdx].code); + retObj.itemColorForSubBoard = [{start: retObj.subBoardListIdxes.markCharStart, end: retObj.subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark}, + {start: retObj.subBoardListIdxes.descStart, end: retObj.subBoardListIdxes.descEnd, attrs: this.colors.desc}, + {start: retObj.subBoardListIdxes.numItemsStart, end: retObj.subBoardListIdxes.numItemsEnd, attrs: this.colors.numItems}, + {start: retObj.subBoardListIdxes.dateStart, end: retObj.subBoardListIdxes.dateEnd, attrs: this.colors.latestDate}, + {start: retObj.subBoardListIdxes.timeStart, end: retObj.subBoardListIdxes.timeEnd, attrs: this.colors.latestTime}]; + retObj.selectedItemColorForSubBoard = [{start: retObj.subBoardListIdxes.markCharStart, end: retObj.subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.descStart, end: retObj.subBoardListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.numItemsStart, end: retObj.subBoardListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.dateStart, end: retObj.subBoardListIdxes.dateEnd, attrs: this.colors.dateHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.timeStart, end: retObj.subBoardListIdxes.timeEnd, attrs: this.colors.timeHighlight + this.colors.bkgHighlight}]; } + else + { + retObj.itemColorForSubBoard = [{start: retObj.subBoardListIdxes.markCharStart, end: retObj.subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark}, + {start: retObj.subBoardListIdxes.descStart, end: retObj.subBoardListIdxes.descEnd, attrs: this.colors.desc}, + {start: retObj.subBoardListIdxes.numItemsStart, end: retObj.subBoardListIdxes.numItemsEnd, attrs: this.colors.numItems}]; + retObj.selectedItemColorForSubBoard = [{start: retObj.subBoardListIdxes.markCharStart, end: retObj.subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.descStart, end: retObj.subBoardListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.numItemsStart, end: retObj.subBoardListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}]; + } + // Colors for items that have another list of items + retObj.itemColorForItemWithSubItems = [{start: retObj.msgGrpListIdxes.markCharStart, end: retObj.msgGrpListIdxes.markCharEnd, attrs: this.colors.areaMark}, + {start: retObj.msgGrpListIdxes.descStart, end: retObj.msgGrpListIdxes.descEnd, attrs: this.colors.desc}, + {start: retObj.msgGrpListIdxes.numItemsStart, end: retObj.msgGrpListIdxes.numItemsEnd, attrs: this.colors.numItems}]; + retObj.selectedItemColorForItemWithSubItems = [{start: retObj.msgGrpListIdxes.markCharStart, end: retObj.msgGrpListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, + {start: retObj.msgGrpListIdxes.descStart, end: retObj.msgGrpListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, + {start: retObj.msgGrpListIdxes.numItemsStart, end: retObj.msgGrpListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}]; } - else + + // If a positive color index offset was given, then adjust the indexes in the + // arrays + if (typeof(pColorIdxOffset) === "number" && pColorIdxOffset > 0) { - if (pGrpIdx >= 0 && pGrpIdx < msg_area.grp_list.length && pSubIdx >= 0 && pSubIdx < msg_area.grp_list[pGrpIdx].sub_list.length) - chosenSubMatch = (bbs.cursub_code == msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code); + retObj.itemColorForSubBoard = AdjustMenuAttrArrayIndexes(retObj.itemColorForSubBoard, pColorIdxOffset); + retObj.selectedItemColorForSubBoard = AdjustMenuAttrArrayIndexes(retObj.selectedItemColorForSubBoard, pColorIdxOffset); } - return chosenSubMatch; + + return retObj; } -// For the DDMsgAreaChooser class: With a group & sub-board index, this function gets the date -// & time of the latest posted message from a sub-board (or group of sub-boards, if using -// sub-board name collapsing). This function also gets the description of the sub-board (or -// group of sub-boards if using sub-board name collapsing). -// -// Parameters: -// pGrpIdx: The index of the message group -// pSubIdx: The index of the sub-board in the message group (if using -// sub-board name collapsing, this could be the index of a set -// of sub-subboards). -// pSubSubIdx: Optional - When sub-board name collapsing is being used, -// this specifies the index of the sub-subboard within the subboard. -// -// Return value: An object containing the following properties: -// desc: The description of the sub-board (or group of sub-subboards if using name collapsing) -// numItems: The number of messages in the sub-board or number of sub-subboards in the group, -// if using sub-board name collapsing -// subCode: The internal code of the sub-board (this will be an empty string if it's a group of sub-subboards) -// newestTime: A value containing the date & time of the newest post in the sub-board or group of sub-boards -function DDMsgAreaChooser_GetSubBoardInfo(pGrpIdx, pSubIdx, pSubSubIdx) +// pItemNumHdrWidth: The width of the item # column header (including the mark character) +// pNumItemsWidth: The width of the # items (not column header) +function DDMsgAreaChooser_GetSubBoardColorIndexInfoForLightbarMenu(pItemNumHdrWidth, pNumItemsWidth) { var retObj = { - desc: "", - numItems: 0, - subCode: "", - newestTime: 0 + itemColorForSubBoard: {}, + selectedItemColorForSubBoard: {} }; - // If using sub-board name collapsing and the given group & sub-board indexes has a - // group of sub-subboards, then look through those sub-boards for information. - if (this.useSubCollapsing) + var usingLightbarInterface = this.useLightbarInterface && console.term_supports(USER_ANSI); + if (usingLightbarInterface) { - if (typeof(pSubSubIdx) === "number" && pSubSubIdx >= 0 && pSubSubIdx < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length) + // Colors for items that are sub-boards + if (this.showDatesInSubBoardList) { - retObj.desc = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[pSubSubIdx].description; - retObj.subCode = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[pSubSubIdx].code; - retObj.numItems = getNumMsgsInSubBoard(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[pSubSubIdx].code); - retObj.newestTime = getLatestMsgTime(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[pSubSubIdx].code); + var itemNumWidth = pItemNumHdrWidth - 1; + // Sub # Name # Posts Latest date & time + var markStart = 0; + var markEnd = 1; + var itemNumStart = markEnd; + var itemNumEnd = itemNumStart + itemNumWidth; + // "Latest date & time": 18 + // # spaces: 3 + // Total: 21 + var descWidth = console.screen_columns - pItemNumHdrWidth - pNumItemsWidth - 22; + var descStart = itemNumEnd; + var descEnd = descStart + descWidth; + var numItemsStart = descEnd; + var numItemsEnd = numItemsStart + pNumItemsWidth + 1; + var dateStart = numItemsEnd; + // 2025-01-01 + var dateEnd = dateStart + 11; + var timeStart = dateEnd; + var timeEnd = -1; + retObj.itemColorForSubBoard = [{start: markStart, end: markEnd, attrs: this.colors.areaMark}, + {start: itemNumStart, end: itemNumEnd, attrs: this.colors.areaNum}, + {start: descStart, end: descEnd, attrs: this.colors.desc}, + {start: numItemsStart, end: numItemsEnd, attrs: this.colors.numItems}, + {start: dateStart, end: dateEnd, attrs: this.colors.latestDate}, + {start: timeStart, end: timeEnd, attrs: this.colors.latestTime}]; + retObj.selectedItemColorForSubBoard = [{start: markStart, end: markEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, + {start: itemNumStart, end: itemNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight}, + {start: descStart, end: descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, + {start: numItemsStart, end: numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}, + {start: dateStart, end: dateEnd, attrs: this.colors.dateHighlight + this.colors.bkgHighlight}, + {start: timeStart, end: timeEnd, attrs: this.colors.timeHighlight + this.colors.bkgHighlight}]; } - else // pSubSubIdx wasn't specified or is invalid + else { - retObj.numItems = this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length; - retObj.desc = this.group_list[pGrpIdx].sub_list[pSubIdx].description; - if (this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length > 0) - { - for (var subSubIdx = 0; subSubIdx < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list; ++subSubIdx) - { - var latestPostTime = getLatestMsgTime(this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[subSubIdx].code); - if (latestPostTime > retObj.newestTime) - retObj.newestTime = latestPostTime; - } - } - else - { - // No sub-subboards in this sub-board - retObj.subCode = this.group_list[pGrpIdx].sub_list[pSubIdx].code; - retObj.numItems = getNumMsgsInSubBoard(this.group_list[pGrpIdx].sub_list[pSubIdx].code); - retObj.newestTime = getLatestMsgTime(this.group_list[pGrpIdx].sub_list[pSubIdx].code); - } + var itemNumWidth = pItemNumHdrWidth - 1; + // Sub # Name # Posts Latest date & time + var markStart = 0; + var markEnd = 1; + var itemNumStart = markEnd; + var itemNumEnd = itemNumStart + itemNumWidth; + var descWidth = console.screen_columns - pItemNumHdrWidth - pNumItemsWidth - 3; + var descStart = itemNumEnd; + var descEnd = descStart + descWidth; + var numItemsStart = descEnd; + var numItemsEnd = -1; + retObj.itemColorForSubBoard = [{start: markStart, end: markEnd, attrs: this.colors.areaMark}, + {start: itemNumStart, end: itemNumEnd, attrs: this.colors.areaNum}, + {start: descStart, end: descEnd, attrs: this.colors.desc}, + {start: numItemsStart, end: numItemsEnd, attrs: this.colors.numItems}]; + retObj.selectedItemColorForSubBoard = [{start: markStart, end: markEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, + {start: itemNumStart, end: itemNumEnd, attrs: this.colors.areaNumHighlight + this.colors.bkgHighlight}, + {start: descStart, end: descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, + {start: numItemsStart, end: numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}]; } } - else // No sub-board name collapsing, or there are no sub-subboards in this sub-board + else { - retObj.desc = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].description; - retObj.subCode = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code; - - // Get the number of messages in the sub-board - var numMsgs = numReadableMsgs(null, msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code); - if (numMsgs > 0) + // TODO: Non-lightbar + /* + // Colors for items that are sub-boards + if (this.showDatesInSubBoardList) { - retObj.numItems = numMsgs; - //var msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true); - var msgHeader = getLatestMsgHdr(retObj.subCode); - if (msgHeader === null) - msgHeader = getBogusMsgHdr(); - // Set the newest post time - if (this.showImportDates) - retObj.newestTime = msgHeader.when_imported_time; - else - { - var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader); - if (msgWrittenLocalBBSTime != -1) - retObj.newestTime = msgWrittenLocalBBSTime; - else - retObj.newestTime = msgHeader.when_written_time; - } + retObj.itemColorForSubBoard = [{start: retObj.subBoardListIdxes.markCharStart, end: retObj.subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark}, + {start: retObj.subBoardListIdxes.descStart, end: retObj.subBoardListIdxes.descEnd, attrs: this.colors.desc}, + {start: retObj.subBoardListIdxes.numItemsStart, end: retObj.subBoardListIdxes.numItemsEnd, attrs: this.colors.numItems}, + {start: retObj.subBoardListIdxes.dateStart, end: retObj.subBoardListIdxes.dateEnd, attrs: this.colors.latestDate}, + {start: retObj.subBoardListIdxes.timeStart, end: retObj.subBoardListIdxes.timeEnd, attrs: this.colors.latestTime}]; + retObj.selectedItemColorForSubBoard = [{start: retObj.subBoardListIdxes.markCharStart, end: retObj.subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.descStart, end: retObj.subBoardListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.numItemsStart, end: retObj.subBoardListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.dateStart, end: retObj.subBoardListIdxes.dateEnd, attrs: this.colors.dateHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.timeStart, end: retObj.subBoardListIdxes.timeEnd, attrs: this.colors.timeHighlight + this.colors.bkgHighlight}]; + } + else + { + retObj.itemColorForSubBoard = [{start: retObj.subBoardListIdxes.markCharStart, end: retObj.subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark}, + {start: retObj.subBoardListIdxes.descStart, end: retObj.subBoardListIdxes.descEnd, attrs: this.colors.desc}, + {start: retObj.subBoardListIdxes.numItemsStart, end: retObj.subBoardListIdxes.numItemsEnd, attrs: this.colors.numItems}]; + retObj.selectedItemColorForSubBoard = [{start: retObj.subBoardListIdxes.markCharStart, end: retObj.subBoardListIdxes.markCharEnd, attrs: this.colors.areaMark + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.descStart, end: retObj.subBoardListIdxes.descEnd, attrs: this.colors.descHighlight + this.colors.bkgHighlight}, + {start: retObj.subBoardListIdxes.numItemsStart, end: retObj.subBoardListIdxes.numItemsEnd, attrs: this.colors.numItemsHighlight + this.colors.bkgHighlight}]; } + */ } return retObj; } -////////////////////////////////////////////// -// Message group list stuff (lightbar mode) // -////////////////////////////////////////////// - -// For the DDMsgAreaChooser class - Writes a message group information line. +// Adjusts the attribute indexes in an attribute array to be used with a DDLightbarMenu // // Parameters: -// pGrpIndex: The index of the message group to write (assumed to be valid) -// pHighlight: Boolean - Whether or not to write the line highlighted. -function DDMsgAreaChooser_writeMsgGroupLine(pGrpIndex, pHighlight) -{ - console.attributes = "N"; - // Write the highlight background color if pHighlight is true. - if (pHighlight) - console.print(this.colors.bkgHighlight); - - // Write the message group information line - var grpIsSelected = false; - if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != "")) - grpIsSelected = (pGrpIndex == msg_area.sub[bbs.cursub_code].grp_index); - console.print(grpIsSelected ? this.colors.areaMark + "*" : " "); - printf((pHighlight ? this.msgGrpListHilightPrintfStr : this.msgGrpListPrintfStr), - +(pGrpIndex+1), - msg_area.grp_list[pGrpIndex].description.substr(0, this.msgGrpDescLen), - msg_area.grp_list[pGrpIndex].sub_list.length); - console.cleartoeol("\x01n"); -} - -////////////////////////////////////////////////// -// Message sub-board list stuff (lightbar mode) // -////////////////////////////////////////////////// - -// For the DDMsgAreaChooser class: Gets a message sub-board information line. +// pMenuAttrIdxArray: The attribute array to be adjusted +// pAttrIdxOffset: The offset to adjust the indexes by. Must be positive. // -// Parameters: -// pGrpIndex: The index of the message group (assumed to be valid) -// pSubIndex: The index of the sub-board within the message group to write (assumed to be valid) -// pHighlight: Boolean - Whether or not to write the line highlighted. - - -function DDMsgAreaChooser_GetMsgSubBrdLine(pGrpIndex, pSubIndex, pHighlight) +// Return value: The adjusted array +function AdjustMenuAttrArrayIndexes(pMenuAttrIdxArray, pAttrIdxOffset) { - var subBoardLine = "\x01n"; - // Write the highlight background color if pHighlight is true. - if (pHighlight) - subBoardLine += this.colors.bkgHighlight; - - // Determine if pGrpIndex and pSubIndex specify the user's - // currently-selected group and sub-board. - var currentSub = false; - if ((typeof(bbs.cursub_code) == "string") && (bbs.cursub_code != "")) - currentSub = ((pGrpIndex == msg_area.sub[bbs.cursub_code].grp_index) && (pSubIndex == msg_area.sub[bbs.cursub_code].index)); - - // Open the current sub-board with the msgBase object (so that we can get - // the date & time of the last imported message). - var msgBase = new MsgBase(msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].code); - if (msgBase.open()) - { - var numMsgs = numReadableMsgs(msgBase, msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].code); - var newestDate = {}; // For storing the date of the newest post - // Get the date & time when the last message was imported. - var msgHeader = getLatestMsgHdr(msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].code); - if (msgHeader != null) - { - // Construct the date & time strings of the latest post - if (this.showImportDates) - { - newestDate.date = strftime("%Y-%m-%d", msgHeader.when_imported_time); - newestDate.time = strftime("%H:%M:%S", msgHeader.when_imported_time); - } - else - { - var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader); - if (msgWrittenLocalBBSTime != -1) - { - newestDate.date = strftime("%Y-%m-%d", msgWrittenLocalBBSTime); - newestDate.time = strftime("%H:%M:%S", msgWrittenLocalBBSTime); - } - else - { - newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time); - newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time); - } - } - } - else - newestDate.date = newestDate.time = ""; + if (pAttrIdxOffset <= 0) + return pMenuAttrIdxArray; - // Print the sub-board information line. - var lengthsObj = this.GetSubNameLenAndNumMsgsLen(pGrpIndex); - subBoardLine += (currentSub ? this.colors.areaMark + "*" : " "); - if (this.showDatesInSubBoardList) - { - subBoardLine += format((pHighlight ? this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr : this.subBoardListPrintfInfo[pGrpIndex].printfStr), - +(pSubIndex+1), msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].description.substr(0, lengthsObj.nameLen), - numMsgs, newestDate.date, newestDate.time); - } - else - { - subBoardLine += format((pHighlight ? this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr : this.subBoardListPrintfInfo[pGrpIndex].printfStr), - +(pSubIndex+1), msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].description.substr(0, lengthsObj.nameLen), - numMsgs); - } - msgBase.close(); + var newArray = pMenuAttrIdxArray; + for (var i = 0; i < newArray.length; ++i) + { + newArray[i].start += pAttrIdxOffset; + if (newArray[i].end > -1) + newArray[i].end += pAttrIdxOffset; } - - return subBoardLine; + // To make DDLightbarMenu happy, ensure the first object in the array + // goes from index 0 to the next one + if (newArray.length > 0) + { + newArray.splice(0, 0, { + start: 0, + end: 0, + attrs: "\x01n" + }); + newArray[0].end = newArray[1].start; + } + return newArray; } /////////////////////////////////////////////// @@ -2329,11 +1867,18 @@ function DDMsgAreaChooser_BuildSubBoardPrintfInfoForGrp(pGrpIndex) + this.subBoardListPrintfInfo[pGrpIndex].nameLen + "s " + this.colors.numItems + "%" + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d"; + this.subBoardListPrintfInfo[pGrpIndex].printfStrWithoutAreaNum = this.colors.desc + "%-" + + this.subBoardListPrintfInfo[pGrpIndex].nameLen + "s " + + this.colors.numItems + "%" + + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d"; if (this.showDatesInSubBoardList) { this.subBoardListPrintfInfo[pGrpIndex].printfStr += " " + this.colors.latestDate + "%" + this.dateLen + "s " + this.colors.latestTime + "%" + this.timeLen + "s"; + this.subBoardListPrintfInfo[pGrpIndex].printfStrWithoutAreaNum += " " + + this.colors.latestDate + "%" + this.dateLen + "s " + + this.colors.latestTime + "%" + this.timeLen + "s"; } this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr = "\x01n" + this.colors.bkgHighlight + " " + "\x01n" @@ -2346,6 +1891,12 @@ function DDMsgAreaChooser_BuildSubBoardPrintfInfoForGrp(pGrpIndex) + this.colors.bkgHighlight + this.colors.numItemsHighlight + "%" + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d"; + this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStrWithoutAreaNum = "\x01n" + this.colors.bkgHighlight + + this.colors.descHighlight + "%-" + + this.subBoardListPrintfInfo[pGrpIndex].nameLen + "s \x01n" + + this.colors.bkgHighlight + + this.colors.numItemsHighlight + "%" + + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d"; if (this.showDatesInSubBoardList) { this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr += " \x01n" @@ -2353,6 +1904,11 @@ function DDMsgAreaChooser_BuildSubBoardPrintfInfoForGrp(pGrpIndex) + this.colors.dateHighlight + "%" + this.dateLen + "s \x01n" + this.colors.bkgHighlight + this.colors.timeHighlight + "%" + this.timeLen + "s\x01n"; + this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStrWithoutAreaNum += " \x01n" + + this.colors.bkgHighlight + + this.colors.dateHighlight + "%" + this.dateLen + "s \x01n" + + this.colors.bkgHighlight + + this.colors.timeHighlight + "%" + this.timeLen + "s\x01n"; } } } @@ -2436,7 +1992,7 @@ function DDMsgAreaChooser_WriteLightbarKeyHelpErrorMsg(pErrorMsg, pRefreshHelpLi // the sub-board collapse separator function DDMsgAreaChooser_SetUpGrpListWithCollapsedSubBoards() { - if (this.group_list.length == 0) + if (this.msgArea_list.length == 0) { // Copy some of the information from msg_area.grp_list for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx) @@ -2563,7 +2119,7 @@ function DDMsgAreaChooser_SetUpGrpListWithCollapsedSubBoards() } } - this.group_list.push(msgGrpObj); + this.msgArea_list.push(msgGrpObj); } } } @@ -2584,7 +2140,6 @@ function defaultMsgGrpObj() // contain its own list of sub-boards within the original subboard. function defaultSubBoardObj() { - /* return { index: 0, number: 0, @@ -2599,21 +2154,6 @@ function defaultSubBoardObj() settings: 0, sub_subboard_list: [] }; - */ - var obj = new Object(); - obj.index = 0; - obj.number = 0; - obj.grp_index = 0; - obj.grp_number = 0; - obj.grp_name = 0; - obj.code = ""; - obj.name = ""; - obj.grp_name = ""; - obj.description = ""; - obj.ars = 0; - obj.settings = 0; - obj.sub_subboard_list = [] - return obj; } function subBoardObjFromOfficialSubBoard(pGrpIdx, pSubIdx) { @@ -2728,69 +2268,6 @@ function getNumMsgsInSubBoard(pSubCode) return numMsgs; } -// Inputs a keypress from the user and handles some ESC-based -// characters such as PageUp, PageDown, and ESC. If PageUp -// or PageDown are pressed, this function will return the -// string "\x01PgUp" (KEY_PAGE_UP) or "\x01Pgdn" (KEY_PAGE_DOWN), -// respectively. Also, F1-F5 will be returned as "\x01F1" -// through "\x01F5", respectively. -// Thanks goes to Psi-Jack for the original impementation -// of this function. -// -// Parameters: -// pGetKeyMode: Optional - The mode bits for console.getkey(). -// If not specified, K_NONE will be used. -// -// Return value: The user's keypress -function getKeyWithESCChars(pGetKeyMode) -{ - var getKeyMode = K_NONE; - if (typeof(pGetKeyMode) === "number") - getKeyMode = pGetKeyMode; - - var userInput = console.getkey(getKeyMode); - if (userInput == KEY_ESC) - { - switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) - { - case '[': - switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) - { - case 'V': - userInput = KEY_PAGE_UP; - break; - case 'U': - userInput = KEY_PAGE_DOWN; - break; - } - break; - case 'O': - switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) - { - case 'P': - userInput = "\x01F1"; - break; - case 'Q': - userInput = "\x01F2"; - break; - case 'R': - userInput = "\x01F3"; - break; - case 'S': - userInput = "\x01F4"; - break; - case 't': - userInput = "\x01F5"; - break; - } - default: - break; - } - } - - return userInput; -} - // Loads a text file (an .ans or .asc) into an array. This will first look for // an .ans version, and if exists, convert to Synchronet colors before loading // it. If an .ans doesn't exist, this will look for an .asc version. @@ -3463,149 +2940,38 @@ function getLatestMsgTime(pSubCode) return latestPostTime; } -// Finds a message group index with search text, matching either the name or -// description, case-insensitive. -// -// Parameters: -// pSearchText: The name/description text to look for -// pStartItemIdx: The item index to start at. Defaults to 0 -// -// Return value: The index of the message group, or -1 if not found -function DDMsgAreaChooser_FindMsgGrpIdxFromText(pSearchText, pStartItemIdx) -{ - if (typeof(pSearchText) != "string") - return -1; - - var grpIdx = -1; - - var startIdx = (typeof(pStartItemIdx) === "number" ? pStartItemIdx : 0); - if (this.useSubCollapsing) - { - if ((startIdx < 0) || (startIdx > this.group_list.length)) - startIdx = 0; - } - else - { - if ((startIdx < 0) || (startIdx > msg_area.grp_list.length)) - startIdx = 0; - } - - // Go through the message group list and look for a match - var searchTextUpper = pSearchText.toUpperCase(); - if (this.useSubCollapsing) - { - for (var i = startIdx; i < this.group_list.length; ++i) - { - if ((this.group_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) || - (this.group_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1)) - { - grpIdx = i; - break; - } - } - } - else - { - for (var i = startIdx; i < msg_area.grp_list.length; ++i) - { - if ((msg_area.grp_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) || - (msg_area.grp_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1)) - { - grpIdx = i; - break; - } - } - } - - return grpIdx; -} - -// For the DDMsgAreaChooser class: Finds a message group index with search text, matching either the name or +// Finds a message area index with search text, matching either the name or // description, case-insensitive. // // Parameters: -// pGrpIdx: The index of the message group -// pSubIdx: Optional - The index of the sub-board, for sub-board name collapsing. -// If this is null, then sub-board collapsing won't be used. +// pMsgAreaStructure: The message area structure from this.msgArea_list to search through // pSearchText: The name/description text to look for // pStartItemIdx: The item index to start at. Defaults to 0 // -// Return value: The index of the sub-board, or -1 if not found -function DDMsgAreaChooser_FindSubBoardIdxFromText(pGrpIdx, pSubIdx, pSearchText, pStartItemIdx) +// Return value: The index of the message area, or -1 if not found +function DDMsgAreaChooser_FindMsgAreaIdxFromText(pMsgAreaStructure, pSearchText, pStartItemIdx) { - if (typeof(pGrpIdx) !== "number") - return -1; if (typeof(pSearchText) != "string") return -1; - var subBoardIdx = -1; + var areaIdx = -1; var startIdx = (typeof(pStartItemIdx) === "number" ? pStartItemIdx : 0); - if (this.useSubCollapsing) - { - if (typeof(pSubIdx) === "number") - { - if ((startIdx < 0) || (startIdx > this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length)) - startIdx = 0; - } - else - { - if ((startIdx < 0) || (startIdx > this.group_list[pGrpIdx].sub_list.length)) - startIdx = 0; - } - } - else - { - if ((startIdx < 0) || (startIdx > msg_area.grp_list[pGrpIdx].sub_list.length)) - startIdx = 0; - } + if ((startIdx < 0) || (startIdx > pMsgAreaStructure.length)) + startIdx = 0; - // Go through the message sub-board list in the group (or sub-subboard list in the sub-board) - // and look for a match + // Go through the message area list and look for a match var searchTextUpper = pSearchText.toUpperCase(); - if (this.useSubCollapsing) - { - if (typeof(pSubIdx) === "number") - { - // Look through the sub-subboards of the sub-board - for (var i = startIdx; i < this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list.length; ++i) - { - // For the sub-subboards, there is only a description, no name - if (this.group_list[pGrpIdx].sub_list[pSubIdx].sub_subboard_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1) - { - subBoardIdx = i; - break; - } - } - } - else - { - // Look through the sub-boards in the group - for (var i = startIdx; i < this.group_list[pGrpIdx].sub_list.length; ++i) - { - if ((this.group_list[pGrpIdx].sub_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) || - (this.group_list[pGrpIdx].sub_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1)) - { - subBoardIdx = i; - break; - } - } - } - } - else + for (var i = startIdx; i < pMsgAreaStructure.length; ++i) { - for (var i = startIdx; i < msg_area.grp_list[pGrpIdx].sub_list.length; ++i) + if (pMsgAreaStructure[i].name.toUpperCase().indexOf(searchTextUpper) > -1 || pMsgAreaStructure[i].altName.toUpperCase().indexOf(searchTextUpper) > -1) { - if ((msg_area.grp_list[pGrpIdx].sub_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) || - (msg_area.grp_list[pGrpIdx].sub_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1)) - { - subBoardIdx = i; - break; - } + areaIdx = i; + break; } } - return subBoardIdx; + return areaIdx; } // For the DDMsgAreaChooser class: Gets the lengths for the sub-board column and # of messages @@ -3696,3 +3062,214 @@ function findNextGrpIdxWithSubBoards(pGrpIdx) } return nextGrpIdx; } + +// Creates and returns an array which is a sort of recursive array structure +// of objects. Each entry in the array will be an object with a 'name' property +// and either an 'items' property if it has sub-items or a 'subObj' property +// if it's a Synchronet message sub-board. The last item in the array chain +// will have the 'subObj' property. +// +// Parameters: +// pCollapsing: Boolean - Whether or not to use sub-board collapsing +// pCollapsingSeparator: The separator used to split file lib/dir names when using collapsing +function getMsgSubHeirarchy(pCollapsing, pCollapsingSeparator) +{ + var msgSubHeirarchy = []; + if (pCollapsing) + { + // For each message group, go through each sub-board + for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx) + { + for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx) + { + // 1. Join the message group name & sub-board name separated by a colon + // 2. Split on colons into an array (of names) + // 3. Go through the array of names and build the appropriate structure in msgSubHeirarchy. + // TODO: Initially, I thought of having collapsing restricted to + // areas where there are no spaces before or after the :, but that + // isn't how I did it before.. + /* + var libAndDirName = msg_area.grp_list[grpIdx].description + ":" + msg_area.grp_list[grpIdx].sub_list[subIdx].description; + var nameArray = splitStrNoSpacesBeforeSeparator(libAndDirName, pCollapsingSeparator); + */ + var libDesc = skipsp(truncsp(msg_area.grp_list[grpIdx].description)); + var dirDesc = skipsp(truncsp(msg_area.grp_list[grpIdx].sub_list[subIdx].description)); + var libAndDirName = libDesc + pCollapsingSeparator + dirDesc; + var nameArray = libAndDirName.split(pCollapsingSeparator); + var arrayToSearch = msgSubHeirarchy; + for (var i = 0; i < nameArray.length; ++i) + { + var name = skipsp(truncsp(nameArray[i])); + // Look for this one in the heirarchy; if not found, add it. + // Look for an entry in the array that matches the name and has its own "items" array + var heirarchyIdx = -1; + for (var j = 0; j < arrayToSearch.length; ++j) + { + if (arrayToSearch[j].name == name && arrayToSearch[j].hasOwnProperty("items")) + { + heirarchyIdx = j; + break; + } + } + if (heirarchyIdx > -1) + arrayToSearch = arrayToSearch[heirarchyIdx].items; + else + { + // If we're at the last name, add a subObj item; otherwise, add + // an items array. + if (i == nameArray.length - 1) + { + arrayToSearch.push( + { + name: name, + altName: msg_area.grp_list[grpIdx].sub_list[subIdx].name, + grpIdx: grpIdx, + subObj: msg_area.grp_list[grpIdx].sub_list[subIdx] + }); + } + else + { + arrayToSearch.push( + { + name: name, + altName: name, + grpIdx: grpIdx, + items: [] + }); + arrayToSearch = arrayToSearch[arrayToSearch.length-1].items; + } + } + } + } + } + } + else + { + // No collapsing. Have msgSubHeirarchy match the group/sub structure + // configured on the BBS. + for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx) + { + msgSubHeirarchy.push( + { + name: msg_area.grp_list[grpIdx].description, + grpIdx: grpIdx, + items: [] + }); + var libIdxInHeirarchy = msgSubHeirarchy.length - 1; + for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx) + { + msgSubHeirarchy[libIdxInHeirarchy].items.push( + { + name: msg_area.grp_list[grpIdx].sub_list[subIdx].description, + grpIdx: grpIdx, + subObj: msg_area.grp_list[grpIdx].sub_list[subIdx] + }); + } + } + } + + // Add 'allSubs' and 'allItems' properties to the items in the heirarchy + //addAllSubsAndAllItemsBooleansToMsgHeirarchy(msgSubHeirarchy); + + return msgSubHeirarchy; +} +// Hepler for getMsgSubHeirarchy() +function addAllSubsAndAllItemsBooleansToMsgHeirarchy(pMsgSubHeirarchyObj, pParentObj) +{ + var retObj = { + allItems: false, + allSubs: false + }; + + if (pMsgSubHeirarchyObj.hasOwnProperty("items")) + { + var sawItems = false; + var sawSubs = true; + for (var i = 0; i < pMsgSubHeirarchyObj.items.length; ++i) + { + pMsgSubHeirarchyObj.items[i].parent = pMsgSubHeirarchyObj; + sawItems = pMsgSubHeirarchyObj.items[i].hasOwnProperty("items"); + sawSubs = pMsgSubHeirarchyObj.items[i].hasOwnProperty("subObj"); + if (sawSubObj && sawItems) + { + pMsgSubHeirarchyObj.allSubs = false; + pMsgSubHeirarchyObj.allItems = false; + } + else if (sawSubObj) + { + pMsgSubHeirarchyObj.allSubs = true; + pMsgSubHeirarchyObj.allItems = false; + } + else if (sawItems) + { + pMsgSubHeirarchyObj.allSubs = false; + pMsgSubHeirarchyObj.allItems = true; + } + + addAllSubsAndAllItemsBooleansToMsgHeirarchy(pMsgSubHeirarchyObj.items[i]); + } + } + else + { + if (pParentObj != null) + pMsgSubHeirarchyObj.parent = pParentObj; + pMsgSubHeirarchyObj.allSubs = true; + } + + return retObj; +} + +// Splits a string on a separator, except when there's a space after a +// separator; if there's a space after a the separator, both parts of the +// string before & after the separator are included as one item in the +// resulting array. For instance, "Magic: The Gathering:Information" +// would be split up such that "Magic: The Gathering" would be one string +// and "Information" would be another, whereas "Mirror:Simtel:DOS:Games" +// would all be split up into 4 strings +function splitStrNoSpacesBeforeSeparator(pStr, pSep) +{ + var strArray = []; + var splitArray = pStr.split(pSep); + for (var i = 0; i < splitArray.length; ++i) + { + if (i < splitArray.length-1 && splitArray[i+1].indexOf(" ") == 0) + strArray.push(splitArray[i] + ":" + splitArray[++i]); + else + strArray.push(splitArray[i]); + } + return strArray; +} + +// Given a message group/sub-board heirarchy object built by this module, this +// function returns whether it contains the user's currently selected message +// sub-board. +// +// Parameters: +// pMsgSubHeirarchyObj: An object from this.msgArea_list, which is +// set up with a 'name' property and either +// an 'items' property if it has sub-items +// or a 'subObj' property if it's a message +// sub-board +// +// Return value: Whether or not the given structure has the user's currently selected message sub-board +function msgAreaStructureHasCurrentUserSubBoard(pMsgSubHeirarchyObj) +{ + var currentUserSubBoardFound = false; + if (Array.isArray(pMsgSubHeirarchyObj)) + { + // This could be the top-level array or one of the 'items' properties, which is an array. + // Go through the array and call this function again recursively; this function will + // return when we get to an actual file directory that is the user's current selection. + for (var i = 0; i < pMsgSubHeirarchyObj.length && !currentUserSubBoardFound; ++i) + currentUserSubBoardFound = msgAreaStructureHasCurrentUserSubBoard(pMsgSubHeirarchyObj[i]); + } + else + { + // This is one of the objects with 'name' and an 'items' or 'subObj' + if (pMsgSubHeirarchyObj.hasOwnProperty("subObj")) + currentUserSubBoardFound = (bbs.cursub_code == pMsgSubHeirarchyObj.subObj.code); + else if (pMsgSubHeirarchyObj.hasOwnProperty("items")) + currentUserSubBoardFound = msgAreaStructureHasCurrentUserSubBoard(pMsgSubHeirarchyObj.items); + } + return currentUserSubBoardFound; +} diff --git a/xtrn/DDAreaChoosers/readme.txt b/xtrn/DDAreaChoosers/readme.txt index d2a72c52ab6c795722c4b88911c3b34f7de532e6..965058c6132dbcd203b3250bf471ccc85ece94bf 100644 --- a/xtrn/DDAreaChoosers/readme.txt +++ b/xtrn/DDAreaChoosers/readme.txt @@ -1,6 +1,6 @@ Digital Distortion Area Choosers - Version 1.41 - Release date: 2024-03-13 + Version 1.42 + Release date: 2025-03-17 by @@ -406,17 +406,23 @@ lightbarHelpLineParen The color to use for the ) characters in ======================================== Sub-board name collapsing (for message areas) and directory name collapsing (for file areas) is a way to simplify long lists of sub-boards and file directories -that share a common theme. A 3rd tier of options will be created for those with -a common theme. For example, within a file library, if you have multiple -directories relating to Windows, the file area chooser can just include one -directory entry for Windows, and when the user chooses Windows, another list -of options will be shown for the Windows directories. Similarly, for message -sub-boards, if you have multiple sub-boards in a message group that share a -common theme, the message area chooser can show just one sub-board entry for -that, and when the user selects it, the chooser will show a 3rd tier of options -for the sub-boards in that group. - -For example, if you have the following file directory structure: +that share a common theme. If you use a separator character (such as :, and it +can be configured) in your message library/sub-board and file library/directory +names, the 'collapsing' feature will split the menus up into multiple tiers of +options. For instance, if you have a file library called "Mirror:Simtelnet" and +directories in there such as "C-Net:Mac:Business" and "C-Net:Mac:Games:action", +the menu heirarchy will be arranged as follows: +Mirror + Simetlnet + C-Net + Mac + Business + Games + Action +This only affects how the areas will be displayed in the menus; your configured +areas will be unaffected. + +Another example: If you have the following file directory structure: BBS files DOS: BBS software DOS: BBS doors @@ -425,7 +431,8 @@ BBS files Windows: BBS doors Windows: BBS utilities -With directory name collapsing, the selection will be come 3 tiers, as follows: +With directory name collapsing enabled, the selection will be come 3 tiers, as +follows: BBS files DOS BBS software @@ -436,10 +443,11 @@ BBS files BBS doors BBS utilities -The message area chooser has similar functionality for sub-boards. You can -configure what text to split the names on (i.e, by default, it splits on a -colon, :). This only happens at the file directory/message sub-board level, not -at the file library/message group level. +The message area chooser has the same functionality for sub-boards as the file +area chooser. + +You can configure what text to split the names on (i.e, by default, it splits +on a colon, :). Name collapsing for message areas can be configured with the useSubCollapsing and subCollapseSeparator options in DDMsgAreaChooser.cfg, and for files, with diff --git a/xtrn/DDAreaChoosers/version_history.txt b/xtrn/DDAreaChoosers/version_history.txt index fbb1764f53ce238205719dcd765047e2254aff56..aad71aef1f64b89e095d02274ee6b76f9021b078 100644 --- a/xtrn/DDAreaChoosers/version_history.txt +++ b/xtrn/DDAreaChoosers/version_history.txt @@ -5,6 +5,8 @@ Revision History (change log) ============================= Version Date Description ------- ---- ----------- +1.42 2025-03-17 For both: Name collapsing now supports an arbitrary number + of areas (depth) with an arbitrary number of separators 1.41 2024-03-13 File area chooser: Fix for the directory item counts 1.41 2023-10-27 Message area chooser only: In lightbar mode, when using name collapisng, ensure the menu item for the appropriate