From 841fd6bf12431efebe4fa5832894e21cab2ac63d Mon Sep 17 00:00:00 2001 From: Eric Oulashin <eric.oulashin@gmail.com> Date: Sun, 6 Feb 2022 14:12:20 -0800 Subject: [PATCH] Created a file lister (in JS) that lists files in the user's current file directory with a lightbar interface, as well as message windows etc. If the user does not have an ANSI terminal, this lister will run Synchronet's stock file lister interface. --- exec/load/dd_lightbar_menu.js | 260 ++- xtrn/ddfilelister/ddfilelister.cfg | 12 + xtrn/ddfilelister/ddfilelister.js | 2525 ++++++++++++++++++++++++ xtrn/ddfilelister/defaultTheme.cfg | 57 + xtrn/ddfilelister/readme.txt | 247 +++ xtrn/ddfilelister/revision_history.txt | 8 + 6 files changed, 3092 insertions(+), 17 deletions(-) create mode 100644 xtrn/ddfilelister/ddfilelister.cfg create mode 100644 xtrn/ddfilelister/ddfilelister.js create mode 100644 xtrn/ddfilelister/defaultTheme.cfg create mode 100644 xtrn/ddfilelister/readme.txt create mode 100644 xtrn/ddfilelister/revision_history.txt diff --git a/exec/load/dd_lightbar_menu.js b/exec/load/dd_lightbar_menu.js index 1487fcb6b9..9b1e5320dd 100644 --- a/exec/load/dd_lightbar_menu.js +++ b/exec/load/dd_lightbar_menu.js @@ -285,6 +285,29 @@ The 'key down' behavior can be called explicitly, if needed, by calling the DoKe It takes 2 parameters: An object of selected item indexes (as passed to GetVal()) and, optionally, the pre-calculated number of items. lbMenu.DoKeyDown(pNumItems, pSelectedItemIndexes); + + +For screen refreshing, DDLightbarMenu includes the function DrawPartial(), which can be used to +redraw only a portion of the menu, specified by starting X & Y coordinates, width, and height. +The starting X & Y coordinates are relative to the upper-left corner of the menu (not absolute +screen coordinates) and start at (1, 1). The function signature looks like this: + DrawPartial(pStartX, pStartY, pWidth, pHeight, pSelectedItemIndexes) +The parameters: + pStartX: The column of the character in the menu to start at + pStartY: The row of the character in the menu to start at + pWidth: The width of the content to draw + pHeight: The height of the content to draw + pSelectedItemIndexes: Optional - An object containing indexes of selected items + +Another function, DrawPartialAbs(), provies the same functionality but with absolute screen coordinates +(also starting at (1, 1) in the upper-left corner): + DrawPartialAbs(pStartX, pStartY, pWidth, pHeight, pSelectedItemIndexes) +The parameters: + pStartX: The column of the character in the menu to start at + pStartY: The row of the character in the menu to start at + pWidth: The width of the content to draw + pHeight: The height of the content to draw + pSelectedItemIndexes: Optional - An object containing indexes of selected items */ if (typeof(require) === "function") @@ -451,6 +474,8 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight) this.DrawBorder = DDLightbarMenu_DrawBorder; this.WriteItem = DDLightbarMenu_WriteItem; this.WriteItemAtItsLocation = DDLightbarMenu_WriteItemAtItsLocation; + this.DrawPartial = DDLightbarMenu_DrawPartial; + this.DrawPartialAbs = DDLightbarMenu_DrawPartialAbs; this.GetItemText = DDLightbarMenu_GetItemText; this.Erase = DDLightbarMenu_Erase; this.SetItemHotkey = DDLightbarMenu_SetItemHotkey; @@ -730,6 +755,8 @@ function DDLightbarMenu_Draw(pSelectedItemIndexes, pDrawBorders, pDrawScrollbar) // the rest of the height of the menu. if (numItemsWritten < numPossibleItems) { + var numberFormatStr = "%" + this.itemNumLen + "s "; + var itemFormatStr = "%-" + itemLen + "s"; for (; numItemsWritten < numPossibleItems; ++numItemsWritten) { writeTheItem = ((this.nextDrawOnlyItems.length == 0) || (this.nextDrawOnlyItems.indexOf(numItemsWritten) > -1)); @@ -738,8 +765,8 @@ function DDLightbarMenu_Draw(pSelectedItemIndexes, pDrawBorders, pDrawScrollbar) console.gotoxy(curPos.x, curPos.y++); console.print("\1n"); if (this.numberedMode) - printf("\1n%" + this.itemNumLen + "s ", ""); - var itemText = addAttrsToString(format("%-" + itemLen + "s", ""), this.colors.itemColor); + printf(numberFormatStr, ""); + var itemText = addAttrsToString(format(itemFormatStr, ""), this.colors.itemColor); console.print(itemText); } } @@ -876,6 +903,209 @@ function DDLightbarMenu_WriteItemAtItsLocation(pIdx, pHighlight, pSelected) this.WriteItem(pIdx, null, pHighlight, pSelected); } +// Draws part of the menu, starting at a certain location within the menu and +// with a given width & height (for screen refreshing). The start X and Y location +// are relative to the menu (not the screen), and they start at (1, 1) in the upper-left +// +// Parameters: +// pStartX: The column of the character in the menu to start at +// pStartY: The row of the character in the menu to start at +// pWidth: The width of the content to draw +// pHeight: The height of the content to draw +// pSelectedItemIndexes: Optional - An object containing indexes of selected items +function DDLightbarMenu_DrawPartial(pStartX, pStartY, pWidth, pHeight, pSelectedItemIndexes) +{ + // Sanity check the parameters + if (typeof(pStartX) !== "number" || typeof(pStartY) !== "number" || typeof(pWidth) !== "number" || typeof(pHeight) !== "number") + return; + if (pStartX < 1 || pStartX > this.size.width) + return; + if (pStartY < 1 || pStartY > this.size.height) + return; + + // Fix the width & height if needed + var width = pWidth; + if (width > (this.size.width - pStartX + 1)) + width = (this.size.width - pStartX + 1); + var height = pHeight; + if (height > (this.size.height - pStartY + 1)) + height = (this.size.height - pStartY + 1); + + var selectedItemIndexes = { }; // For multi-select mode + if (typeof(pSelectedItemIndexes) == "object") + selectedItemIndexes = pSelectedItemIndexes; + + // If borders are enabled, draw any border characters in the region first + // The X & Y locations are 1-based + var lastLineNum = (pStartY + this.pos.y + height) - 1; // Last line # on the screen + if (lastLineNum > this.pos.y + this.size.height - 1) + lastLineNum = this.pos.y + this.size.height - 1; + if (this.borderEnabled) + { + var lastX = pStartX + width - 1; + for (var lineNum = pStartY + this.pos.y - 1; lineNum <= lastLineNum; ++lineNum) + { + // Top line + if (lineNum == this.pos.y) + { + console.print("\1n" + this.colors.borderColor); + for (var posX = pStartX; posX <= lastX; ++posX) + { + console.gotoxy(posX, lineNum); + if (posX == this.pos.x) + console.print(this.borderChars.upperLeft); + else if (posX == this.pos.x + this.size.width - 1) + console.print(this.borderChars.upperRight); + else + console.print(this.borderChars.top); + } + } + // Bottom line + else if (lineNum == this.pos.y + this.size.height - 1) + { + console.print("\1n" + this.colors.borderColor); + for (var posX = pStartX; posX <= lastX; ++posX) + { + console.gotoxy(posX, lineNum); + if (posX == this.pos.x) + console.print(this.borderChars.lowerLeft); + else if (posX == this.pos.x + this.size.width - 1) + console.print(this.borderChars.lowerRight); + else + console.print(this.borderChars.bottom); + } + } + // Somewhere between the top & bottom line + else + { + var printedBorderColor = false; + for (var posX = pStartX; posX <= lastX; ++posX) + { + console.gotoxy(posX, lineNum); + if (posX == this.pos.x) + { + if (!printedBorderColor) + { + console.print("\1n" + this.colors.borderColor); + printedBorderColor = true; + } + console.print(this.borderChars.left); + } + else if (posX == this.pos.x + this.size.width - 1) + { + if (!printedBorderColor) + { + console.print("\1n" + this.colors.borderColor); + printedBorderColor = true; + } + console.print(this.borderChars.right); + } + } + } + } + } + // Calculate the width and starting index of the menu items + // Note that pStartX is relative to the menu, not the screen + var itemLen = width; + var writeMenuItems = true; // Might not if the draw area only includes the scrollbar or border + var itemTxtStartIdx = pStartX - 1; + if (this.borderEnabled) + { + if (itemTxtStartIdx > 0) + --itemTxtStartIdx; // pStartX - 2 + if (pStartX == 1) + --itemLen; + // Starts on 2 & width is 5: 2, 3, 4, 5, 6 + var lastCol = this.pos.x + pStartX + width - 1; + if (this.pos.x + pStartX + width - 1 >= lastCol) // The last column drawn will contain the right border char + --itemLen; + if ((pStartX == 1 && width == 1) || pStartX == this.size.width) + writeMenuItems = false; + else if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow() && pStartX == this.size.width-1) + writeMenuItems = false; + } + if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow()) + { + var scrollbarCol = this.borderEnabled ? this.pos.x + this.size.width - 2 : this.pos.x + this.size.width - 1; + if (this.pos.x + pStartX + width - 1 >= scrollbarCol) // The last column drawn includes the scrollbar + --itemLen; + if (!this.borderEnabled && pStartX == this.size.width) + writeMenuItems = false; + // Just draw the whole srollbar to ensure it's updated + this.DisplayInitialScrollbar(this.scrollbarInfo.solidBlockLastStartRow, this.scrollbarInfo.numSolidScrollBlocks); + } + if (itemTxtStartIdx < 0) + itemTxtStartIdx = 0; + // Write the menu items + if (writeMenuItems) + { + var blankItemTextFormatStr = "\1n%" + itemLen + "s"; + for (var lineNum = pStartY + this.pos.y - 1; lineNum <= lastLineNum; ++lineNum) + { + var startX = pStartX; + // If borders are enabled, skip the top & bottom lines since borders were already drawn + if (this.borderEnabled) + { + if (lineNum == this.pos.y || lineNum == lastLineNum) + continue; + else + { + if (pStartX + this.pos.x - 1 == this.pos.x) + ++startX; + } + } + // Write the menu item text + var itemIdx = this.topItemIdx + (lineNum - this.pos.y); + if (this.borderEnabled) --itemIdx; + var highlightItem = itemIdx == this.selectedItemIdx; + var itemText = this.GetItemText(itemIdx, null, highlightItem, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + var shortenedText = substrWithAttrCodes(itemText, itemTxtStartIdx, itemLen); + // If shortenedText is empty (perhaps there's no menu item for this line), + // then make shortenedText consist of all spaces at the proper length + if (shortenedText.length == 0) + shortenedText = format(blankItemTextFormatStr, ""); + console.gotoxy(startX, lineNum); + console.print(shortenedText + "\1n"); + } + } +} +// Draws part of the menu, starting at a certain location within the menu and +// with a given width & height (for screen refreshing). For this version, the start X +// and Y location are absolute on the screen. They start at (1, 1) in the upper-left. +// +// Parameters: +// pStartX: The column of the character in the menu to start at +// pStartY: The row of the character in the menu to start at +// pWidth: The width of the content to draw +// pHeight: The height of the content to draw +// pSelectedItemIndexes: Optional - An object containing indexes of selected items +function DDLightbarMenu_DrawPartialAbs(pStartX, pStartY, pWidth, pHeight, pSelectedItemIndexes) +{ + if (typeof(pStartX) !== "number" || typeof(pStartY) !== "number" || typeof(pWidth) !== "number" || typeof(pHeight) !== "number") + return; + + // Calculate the start X & Y coordinates relative to the menu (1-based), and adjust height & + // width if necessary. Then draw partial. + var height = pHeight; + var width = pWidth; + var startX = pStartX - this.pos.x + 1; + var startY = pStartY - this.pos.y + 1; + if (startX < 1) + { + var XDiff = 1 - startX; + startX += XDiff; + width -= XDiff; + } + if (startY < 1) + { + var YDiff = 1 - startY; + startY += YDiff; + height -= YDiff; + } + this.DrawPartial(startX, startY, width, height, pSelectedItemIndexes); +} + + // Gets the text of a menu item with colors applied // // Parameters: @@ -1258,10 +1488,6 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes) if (mk !== null && mk.mouse !== null) { goAheadAndExit = !mouseNoAction; // Only really needed with an input timer? - // Temporary - console.print("\1n\r\nHere! - mouseNoAction: " + goAheadAndExit + ", goAheadAndExit: " + goAheadAndExit + "\r\n"); - console.pause(); - // End Temporary } if (goAheadAndExit) { @@ -1513,7 +1739,7 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes) if (this.multiSelect) { if (Object.keys(selectedItemIndexes).length == 0) - selectedItemIndexes[this.selectedItemIdx] = true; + selectedItemIndexes[+(this.selectedItemIdx)] = true; } else retVal = this.GetItem(this.selectedItemIdx).retval; @@ -1539,8 +1765,8 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes) if (allowSelectItem) { var added = false; // Will be true if added or false if deleted - if (selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)) - delete selectedItemIndexes[this.selectedItemIdx]; + if (selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx))) + delete selectedItemIndexes[+(this.selectedItemIdx)]; else { var addIt = true; @@ -1548,7 +1774,7 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes) addIt = (Object.keys(selectedItemIndexes).length < this.maxNumSelections); if (addIt) { - selectedItemIndexes[this.selectedItemIdx] = true; + selectedItemIndexes[+(this.selectedItemIdx)] = true; added = true; } } @@ -1620,15 +1846,15 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes) this.selectedItemIdx = userEnteredItemNum-1; if (this.multiSelect) { - if (selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)) - delete selectedItemIndexes[this.selectedItemIdx]; + if (selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx))) + delete selectedItemIndexes[+(this.selectedItemIdx)]; else { var addIt = true; if (this.maxNumSelections > 0) addIt = (Object.keys(selectedItemIndexes).length < this.maxNumSelections); if (addIt) - selectedItemIndexes[this.selectedItemIdx] = true; + selectedItemIndexes[+(this.selectedItemIdx)] = true; } // TODO: Put a check-mark next to the selected item // TODO: Screen refresh? @@ -1715,7 +1941,7 @@ function DDLightbarMenu_DoKeyDown(pSelectedItemIndexes, pNumItems) if (this.selectedItemIdx < numItems-1) { // Draw the current item in regular colors - this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx))); ++this.selectedItemIdx; // Draw the new current item in selected colors // If the selected item is below the bottom of the menu, then we'll need to @@ -1730,7 +1956,7 @@ function DDLightbarMenu_DoKeyDown(pSelectedItemIndexes, pNumItems) { // The selected item is not below the bottom of the menu, so we can // just draw the selected item highlighted. - this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx))); } } else @@ -1740,7 +1966,7 @@ function DDLightbarMenu_DoKeyDown(pSelectedItemIndexes, pNumItems) if (this.wrapNavigation) { // Draw the current item in regular colors - this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx))); // Go to the first item and scroll to the top if necessary this.selectedItemIdx = 0; var oldTopItemIdx = this.topItemIdx; @@ -1750,7 +1976,7 @@ function DDLightbarMenu_DoKeyDown(pSelectedItemIndexes, pNumItems) else { // Draw the new current item in selected colors - this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx)); + this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx))); } } } diff --git a/xtrn/ddfilelister/ddfilelister.cfg b/xtrn/ddfilelister/ddfilelister.cfg new file mode 100644 index 0000000000..a6c515ddbd --- /dev/null +++ b/xtrn/ddfilelister/ddfilelister.cfg @@ -0,0 +1,12 @@ +; The sort order for the file list +; NATURAL: Natural sort order (same as DATE_A) +; NAME_AI: Filename ascending, case insensitive sort order +; NAME_DI: Filename descending, case insensitive sort order +; NAME_AS: Filename ascending, case sensitive sort order +; NAME_DS: Filename descending, case sensitive sort order +; DATE_A: Import date/time ascending sort order +; DATE_D: Import date/time descending sort order +sortOrder=NATURAL + +; The name of the color theme configuration file +themeFilename=defaultTheme.cfg diff --git a/xtrn/ddfilelister/ddfilelister.js b/xtrn/ddfilelister/ddfilelister.js new file mode 100644 index 0000000000..2f2b4e4280 --- /dev/null +++ b/xtrn/ddfilelister/ddfilelister.js @@ -0,0 +1,2525 @@ +/* This is a file lister door for Synchronet. + * + * Author: Eric Oulashin (AKA Nightfox) + * BBS: Digital Distortion + * BBS address: digitaldistortionbbs.com (or digdist.synchro.net) + * + * Date Author Description + * 2022-01-17 Eric Oulashin Version 0.01 + * Started work on this script + * 2022-02-06 Eric Oulashin Version 2.00 + * Functionality implemented (for lightbar/ANSI terminal). + * Seems to work as expected. Releasing this version. + * I'm calling this version 2.00 because I had already + * released a file lister mod years ago (modding the stock + * Synchronet file list interface). +*/ + +if (typeof(require) === "function") +{ + require("sbbsdefs.js", "K_UPPER"); + require("text.js", "Email"); // Text string definitions (referencing text.dat) + require("dd_lightbar_menu.js", "DDLightbarMenu"); + require("frame.js", "Frame"); + require("scrollbar.js", "ScrollBar"); + require("mouse_getkey.js", "mouse_getkey"); +} +else +{ + load("sbbsdefs.js"); + load("text.js"); // Text string definitions (referencing text.dat) + load("dd_lightbar_menu.js"); + load("frame.js"); + load("scrollbar.js"); + load("mouse_getkey.js"); +} + + +// If the user's terminal doesn't support ANSI, then just call the standard Synchronet +// file list function and exit now +// TODO: Create a traditional user interface? +if (!console.term_supports(USER_ANSI)) +{ + bbs.list_files(); + exit(); +} + + +// Store whether the user is a sysop +var gUserIsSysop = user.compare_ars("SYSOP"); + + +// This script requires Synchronet version 3.19 or higher. +// If the Synchronet version is below the minimum, then just call the standard +// Synchronet file list and exit. +if (system.version_num < 31900) +{ + if (gUserIsSysop) + { + var message = "\1n\1h\1y\1i* Warning:\1n\1h\1w Digital Distortion File Lister " + + "requires version \1g3.19\1w or\r\n" + + "higher of Synchronet. This BBS is using version \1g" + system.version + + "\1w.\1n"; + console.crlf(); + console.print(message); + console.crlf(); + console.pause(); + } + bbs.list_files(); + exit(); +} + +// Lister version information +var LISTER_VERSION = "2.00"; +var LISTER_DATE = "2022-02-06"; + + +/////////////////////////////////////////////////////////////////////////////// +// Global variables + +// Block characters +var BLOCK1 = "\xB0"; // Dimmest block +var BLOCK2 = "\xB1"; +var BLOCK3 = "\xB2"; +var BLOCK4 = "\xDB"; // Brightest block +var THIN_RECTANGLE_LEFT = "\xDD"; +var THIN_RECTANGLE_RIGHT = "\xDE"; +var RIGHT_T_HDOUBLE_VSINGLE = "\xB5"; +var LEFT_T_HDOUBLE_VSINGLE = "\xcC6"; + +// For file sizes +//var BYTES_PER_TB = 1099511627776; // Seems to be too big for JS +var BYTES_PER_GB = 1073741824; +var BYTES_PER_MB = 1048576; +var BYTES_PER_KB = 1024; + +// File list column indexes (0-based). The end indexes are one past the last index. +// These defaults assume an 80-character wide terminal. +var gListIdxes = { + filenameStart: 0 +}; +// The end index of each column includes the trailing space so that +// highlight colors will highlight the whole field +gListIdxes.filenameEnd = gListIdxes.filenameStart + 13; +// For terminals that are at least 100 characters wide, allow 10 more characters +// for the filename. This will also give more space for the description. +if (console.screen_columns >= 100) + gListIdxes.filenameEnd += 10; +gListIdxes.fileSizeStart = gListIdxes.filenameEnd; +gListIdxes.fileSizeEnd = gListIdxes.fileSizeStart + 7; +gListIdxes.descriptionStart = gListIdxes.fileSizeEnd; +gListIdxes.descriptionEnd = console.screen_columns - 1; // Leave 1 character remaining on the screen +// Colors +var gColors = { + filename: "\1n\1b\1h", + fileSize: "\1n\1m\1h", + desc: "\1n\1w", + bkgHighlight: "\1n\1" + "4", + filenameHighlight: "\1c\1h", + fileSizeHighlight: "\1c\1h", + descHighlight: "\1c\1h", + fileTimestamp: "\1g\1h", + fileInfoWindowBorder: "\1r", + fileInfoWindowTitle: "\1g", + errorBoxBorder: "\1g\1h", + errorMessage: "\1y\1h", + successMessage: "\1c", + + batchDLInfoWindowBorder: "\1r", + batchDLInfoWindowTitle: "\1g", + confirmFileActionWindowBorder: "\1r", + confirmFileActionWindowWindowTitle: "\1g", + + fileAreaMenuBorder: "\1b", + fileNormalBkg: "\1" + "4", + fileAreaNum: "\1w", + fileAreaDesc: "\1w", + fileAreaNumItems: "\1w", + + fileAreaMenuHighlightBkg: "\1" + "7", + fileAreaNumHighlight: "\1b", + fileAreaDescHighlight: "\1b", + fileAreaNumItemsHighlight: "\1b" +}; + + +// Actions +var FILE_VIEW_INFO = 1; +var FILE_VIEW = 2; +var FILE_ADD_TO_BATCH_DL = 3; +var HELP = 4; +var QUIT = 5; +var FILE_MOVE = 6; // Sysop action +var FILE_DELETE = 7; // Sysop action + + + +// This will store the number of header lines that were displayed. This will control +// the starting row of the file list menu. +var gNumHeaderLinesDisplayed = 0; + +// The number of milliseconds to wait after displaying an error message +var gErrorMsgWaitMS = 1500; +// The upper-left position, width, & size of the error message box +var gErrorMsgBoxULX = 2; +var gErrorMsgBoxULY = 4; +var gErrorMsgBoxWidth = console.screen_columns - 2; +var gErrorMsgBoxHeight = 3; + + +/////////////////////////////////////////////////////////////////////////////// +// Script execution code + +var gFilebase = new FileBase(bbs.curdir_code); +if (!gFilebase.open()) +{ + console.crlf(); + console.print("\1n\1h\1yUnable to open \1w" + file_area.dir[bbs.curdir_code].description + "\1n"); + console.crlf(); + console.pause(); + exit(1); +} + +// If we got here, the gFilebase successfully opened. +// If there are no files in the filebase, then say so and exit now. +if (gFilebase.files == 0) +{ + var libIdx = file_area.dir[bbs.curdir_code].lib_index; + console.crlf(); + console.print("\1n\1cThere are no files in \1h" + file_area.lib_list[libIdx].description + "\1n\1c - \1h" + + file_area.dir[bbs.curdir_code].description + "\1n"); + console.crlf(); + console.pause(); + exit(); +} + +// The sort order to use for the file list +var gFileSortOrder = FileBase.SORT.NATURAL; // Natural sort order, same as DATE_A (import date ascending) + +// Read the configuration file and set the settings +readConfigFile(); + +// To check a user's file basic/extended detail information setting: +// if ((user.settings & USER_EXTDESC) == USER_EXTDESC) + +// Get a list of file data with normal detail (without extended info). When the user +// selects a file to view extended info, we'll get metadata about the file with extended detail. +//var gFileList = gFilebase.get_list("*", FileBase.DETAIL.NORM); // FileBase.DETAIL.EXTENDED +var gFileList = gFilebase.get_list("*", FileBase.DETAIL.NORM, 0, true, gFileSortOrder); // FileBase.DETAIL.EXTENDED + +// Clear the screen and display the header lines +console.clear("\1n"); +displayFileLibAndDirHeader(bbs.curdir_code); +// Construct and display the menu/command bar at the bottom of the screen +var fileMenuBar = new DDFileMenuBar({ x: 1, y: console.screen_rows }); +fileMenuBar.writePromptLine(); +// Create the file list menu +var gFileListMenu = createFileListMenu(fileMenuBar.getAllActionKeysStr(true, true) + KEY_LEFT + KEY_RIGHT); +// In a loop, show the file list menu, allowing the user to scroll the file list, +// and respond to user input until the user decides to quit. +gFileListMenu.Draw({}); +var continueDoingFileList = true; +var drawFileListMenu = false; // For screen refresh optimization +while (continueDoingFileList) +{ + // Clear the menu's selected item indexes so it's 'fresh' for this round + for (var prop in gFileListMenu.selectedItemIndexes) + delete gFileListMenu.selectedItemIndexes[prop]; + var actionRetObj = null; + var userChoice = gFileListMenu.GetVal(drawFileListMenu, gFileListMenu.selectedItemIndexes); + drawFileListMenu = false; // For screen refresh optimization + var lastUserInputUpper = gFileListMenu.lastUserInput != null ? gFileListMenu.lastUserInput.toUpperCase() : null; + if (lastUserInputUpper == null || lastUserInputUpper == "Q") + continueDoingFileList = false; + else if (lastUserInputUpper == KEY_LEFT) + fileMenuBar.decrementMenuItemAndRefresh(); + else if (lastUserInputUpper == KEY_RIGHT) + fileMenuBar.incrementMenuItemAndRefresh(); + else if (lastUserInputUpper == KEY_ENTER) + { + var currentActionVal = fileMenuBar.getCurrentSelectedAction(); + fileMenuBar.setCurrentActionCode(currentActionVal); + actionRetObj = doAction(currentActionVal, bbs.curdir_code, gFilebase, gFileList, gFileListMenu); + } + else + { + var currentActionVal = fileMenuBar.getActionFromChar(lastUserInputUpper, false); + fileMenuBar.setCurrentActionCode(currentActionVal); + actionRetObj = doAction(currentActionVal, bbs.curdir_code, gFilebase, gFileList, gFileListMenu); + } + // If an action was done (actionRetObj is not null), then look at actionRetObj and + // do what's needed. Note that quit (for the Q key) is already handled. + if (actionRetObj != null) + { + if (actionRetObj.exitNow) + continueDoingFileList = false; + else + { + if (actionRetObj.reDrawListerHeader) + { + console.print("\1n"); + console.gotoxy(1, 1); + displayFileLibAndDirHeader(bbs.curdir_code); + } + if (actionRetObj.reDrawCmdBar) + { + //fileMenuBar.constructPromptText(); + fileMenuBar.writePromptLine(); + } + var redrewPartOfFileListMenu = false; + if (actionRetObj.fileListPartialRedrawInfo != null) + { + drawFileListMenu = false; + var startX = actionRetObj.fileListPartialRedrawInfo.startX; + var startY = actionRetObj.fileListPartialRedrawInfo.startY; + var width = actionRetObj.fileListPartialRedrawInfo.width; + var height = actionRetObj.fileListPartialRedrawInfo.height; + gFileListMenu.DrawPartial(startX, startY, width, height, {}); + redrewPartOfFileListMenu = true; + } + else + { + continueDoingFileList = actionRetObj.continueFileLister; + drawFileListMenu = actionRetObj.reDrawFileListMenu; + } + // If we're not redrawing the whole file list menu, then remove + // checkmarks from any selected files + if (!drawFileListMenu && gFileListMenu.numSelectedItemIndexes() > 0) + { + var lastItemIdxOnScreen = gFileListMenu.topItemIdx + gFileListMenu.size.height - 1; + var listItemStartRow = gFileListMenu.pos.y; + var redrawStartX = gFileListMenu.pos.x + gFileListMenu.size.width - 1; + var redrawWidth = 1; + if (gFileListMenu.borderEnabled) // Shouldn't have this enabled + { + --lastItemIdxOnScreen; + ++listItemStartRow; + --redrawStartX; + } + if (gFileListMenu.scrollbarEnabled && !gFileListMenu.CanShowAllItemsInWindow()) + { + --redrawStartX; + ++redrawWidth; + } + for (var idx in gFileListMenu.selectedItemIndexes) + { + var idxNum = +idx; + if (idxNum >= gFileListMenu.topItemIdx && idxNum <= lastItemIdxOnScreen) + { + gFileListMenu.DrawPartialAbs(redrawStartX, listItemStartRow+idxNum, redrawWidth, 1, {}); + redrewPartOfFileListMenu = true; + } + } + } + // If part of the file list menu was re-drawn (partially, not completely), move the cursor + // to the lower-right corner of the screen so that it's out of the way + if (redrewPartOfFileListMenu) + console.gotoxy(console.screen_columns-1, console.screen_rows); + } + } +} + +gFilebase.close(); + + + +/////////////////////////////////////////////////////////////////////////////// +// Functions: File actions + +// Performs a specified file action based on an action code. +// +// Parameters: +// pActionCode: A code specifying an action to do. Must be one of the global +// action codes. +// pDirCode: The internal code of the file directory +// pFilebase: A Filebase object representing the downloadable file directory. This +// is assumed to be open. +// pFileList: The list of file metadata objects, as retrieved from the filebase +// pFileListMenu: The file list menu +// +// Return value: An object with values to indicate status & screen refresh actions; see +// getDefaultActionRetObj() for details. +function doAction(pActionCode, pDirCode, pFilebase, pFileList, pFileListMenu) +{ + if (typeof(pActionCode) !== "number") + return getDefaultActionRetObj(); + if (pFilebase == null || typeof(pFilebase) !== "object") + return getDefaultActionRetObj(); + + var retObj = null; + switch (pActionCode) + { + case FILE_VIEW_INFO: + retObj = showFileInfo(pFilebase, pFileList, pFileListMenu); + break; + case FILE_VIEW: + retObj = viewFile(pFilebase, pFileList, pFileListMenu); + break; + case FILE_ADD_TO_BATCH_DL: + retObj = addSelectedFilesToBatchDLQueue(pDirCode, pFilebase, pFileList, pFileListMenu); + break; + case HELP: + retObj = displayHelpScreen(pDirCode, pFilebase); + break; + case QUIT: + retObj = getDefaultActionRetObj(); + retObj.continueFileLister = false; + break; + case FILE_MOVE: // Sysop action + if (gUserIsSysop) + retObj = chooseFilebaseAndMoveFileToOtherFilebase_Lightbar(pDirCode, pFilebase, pFileList, pFileListMenu); + break; + case FILE_DELETE: // Sysop action + if (gUserIsSysop) + retObj = removeFileFromFilebase(pDirCode, pFilebase, pFileList, pFileListMenu); + break; + } + + return retObj; +} + +// Returns an object for use for returning from performing a file action, +// with default values. +// +// Return value: An object with the following properties: +// continueFileLister: Boolean - Whether or not the file lister should continue, or exit +// reDrawFileListMenu: Boolean - Whether or not to re-draw the whole file list +// reDrawListerHeader: Boolean - Whether or not to re-draw the header at the top of the screen +// reDrawCmdBar: Boolean - Whether or not to re-draw the command bar at the bottom of the screen +// fileListPartialRedrawInfo: If part of the file list menu needs to be re-drawn, +// this will be an object that includes the following properties: +// startX: The starting X coordinate for where to re-draw +// startY: The starting Y coordinate for where to re-draw +// width: The width to re-draw +// height: The height to re-draw +// exitNow: Exit the file lister now (boolean) +// If no part of the file list menu needs to be re-drawn, this will be null. +function getDefaultActionRetObj() +{ + return { + continueFileLister: true, + reDrawFileListMenu: false, + reDrawListerHeader: false, + reDrawCmdBar: false, + fileListPartialRedrawInfo: null, + exitNow: false + }; +} + +// Shows extended information about a file to the user. +// +// Parameters: +// pFilebase: A Filebase object representing the downloadable file directory. This +// is assumed to be open. +// pFileList: The list of file metadata objects, as retrieved from the filebase +// pFileListMenu: The file list menu +// +// Return value: An object with values to indicate status & screen refresh actions; see +// getDefaultActionRetObj() for details. +function showFileInfo(pFilebase, pFileList, pFileListMenu) +{ + var retObj = getDefaultActionRetObj(); + + // The width of the frame to display the file info (including borders). This + // is declared early so that it can be used for string length adjustment. + var frameWidth = pFileListMenu.size.width - 4; + + // pFileList[pFileListMenu.selectedItemIdx] has a file metadata object without + // extended information. Get a metadata object with extended information so we + // can display the extended description. + var extdFileInfo = pFilebase.get(pFileList[pFileListMenu.selectedItemIdx], FileBase.DETAIL.EXTENDED); + // Build a string with the file information + var fileTime = pFilebase.get_time(extdFileInfo.name); + // Make sure the displayed filename isn't too crazy long + var adjustedFilename = shortenFilename(extdFileInfo.name, frameWidth-2, false); + var fileInfoStr = "\1n\1wFilename"; + if (adjustedFilename.length < extdFileInfo.name.length) + fileInfoStr += " (shortened)"; + fileInfoStr += ":\r\n"; + fileInfoStr += gColors.filename + adjustedFilename + "\1n\1w\r\n"; + // Note: File size can also be retrieved by calling pFilebase.get_size(extdFileInfo.name) + // TODO: Shouldn't need the max length here + fileInfoStr += "Size: " + gColors.fileSize + getFileSizeStr(extdFileInfo.size, 99999) + "\1n\1w\r\n"; + fileInfoStr += "Timestamp: " + gColors.fileTimestamp + strftime("%Y-%m-%d %H:%M:%S", fileTime) + "\1n\1w\r\n" + fileInfoStr += "\r\n"; + fileInfoStr += gColors.desc; + // extdFileInfo should have extdDesc, but check just in case + var fileDesc = ""; + if (extdFileInfo.hasOwnProperty("extdesc") && extdFileInfo.extdesc.length > 0) + fileDesc = extdFileInfo.extdesc; + else + fileDesc = extdFileInfo.desc; + if (fileDesc.length > 0) + fileInfoStr += "Description:\r\n" + fileDesc; + else + fileInfoStr += "No description available"; + fileInfoStr += "\1n\1w"; + + // Construct & draw a frame with the file information & do the input loop + // for the frame until the user closes the frame. + var frameUpperLeftX = pFileListMenu.pos.x + 2; + var frameUpperLeftY = pFileListMenu.pos.y + 2; + // Note: frameWidth is declared earlier + var frameHeight = 10; + var frameTitle = "File Info"; + displayBorderedFrameAndDoInputLoop(frameUpperLeftX, frameUpperLeftY, frameWidth, frameHeight, + gColors.fileInfoWindowBorder, frameTitle, + gColors.fileInfoWindowTitle, fileInfoStr); + + // Construct the file list redraw info. Note that the X and Y are relative + // to the file list menu, not absolute screen coordinates. + retObj.fileListPartialRedrawInfo = { + startX: 2, + startY: 2, + width: frameWidth+1, + height: frameHeight + }; + + return retObj; +} + +// Lets the user view a file. +// +// Parameters: +// pFilebase: A Filebase object representing the downloadable file directory. This +// is assumed to be open. +// pFileList: The list of file metadata objects, as retrieved from the filebase +// pFileListMenu: The file list menu +// +// Return value: An object with values to indicate status & screen refresh actions; see +// getDefaultActionRetObj() for details. +function viewFile(pFilebase, pFileList, pFileListMenu) +{ + var retObj = getDefaultActionRetObj(); + + var fullyPathedFilename = pFilebase.get_path(pFileList[pFileListMenu.selectedItemIdx]); + console.gotoxy(1, console.screen_rows); + console.print("\1n"); + console.crlf(); + var successfullyViewed = bbs.view_file(fullyPathedFilename); + if (!successfullyViewed) + console.pause(); + + retObj.reDrawListerHeader = true; + retObj.reDrawFileListMenu = true; + retObj.reDrawCmdBar = true; + return retObj; +} + +// Allows the user to add their selected file to their batch downloaded queue +// +// Parameters: +// pDirCode: The internal code of the file directory +// pFilebase: The FileBase object representing the file directory (assumed open) +// pFileList: The list of file metadata objects from the file directory +// pFileListMenu: The menu object for the file diretory +// +// Return value: An object with values to indicate status & screen refresh actions; see +// getDefaultActionRetObj() for details. +function addSelectedFilesToBatchDLQueue(pDirCode, pFilebase, pFileList, pFileListMenu) +{ + var retObj = getDefaultActionRetObj(); + + // Confirm with the user to add the file(s) to their batch queue. If they don't want to, + // then just return now. + var filenames = []; + var metadataObjects = []; + if (gFileListMenu.numSelectedItemIndexes() > 0) + { + for (var idx in gFileListMenu.selectedItemIndexes) + { + var idxNum = +idx; + filenames.push(pFileList[idxNum].name); + metadataObjects.push(pFileList[idxNum]); + } + } + else + { + filenames.push(pFileList[pFileListMenu.selectedItemIdx].name); + metadataObjects.push(pFileList[pFileListMenu.selectedItemIdx]); + } + // Note that confirmFileActionWithUser() will re-draw the parts of the file + // list menu that are necessary. + var addFilesConfirmed = confirmFileActionWithUser(filenames, "Batch DL add", false); + if (addFilesConfirmed) + { + var batchDLQueueStats = getUserDLQueueStats(); + var filenamesFailed = []; // To store filenames that failed to get added to the queue + var batchDLFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".dnload"; + var batchDLFile = new File(batchDLFilename); + if (batchDLFile.open(batchDLFile.exists ? "r+" : "w+")) + { + displayMsg("Adding file(s) to batch DL queue..", false, false); + for (var i = 0; i < metadataObjects.length; ++i) + { + // If the file isn't in the user's batch DL queue already, then add it. + var fileAlreadyInQueue = false; + for (var fIdx = 0; fIdx < batchDLQueueStats.filenames.length && !fileAlreadyInQueue; ++fIdx) + exists = (batchDLQueueStats.filenames[fIdx].filename == metadataObjects[i].name); + if (!fileAlreadyInQueue) + { + var addToQueueSuccessful = true; + batchDLFile.writeln(""); + + // Add the required "dir" and "desc" properties to the user's batch download + // queue file. The section is the filename. + addToQueueSuccessful = batchDLFile.iniSetValue(metadataObjects[i].name, "dir", pDirCode); + if (addToQueueSuccessful) + { + addToQueueSuccessful = batchDLFile.iniSetValue(metadataObjects[i].name, "desc", metadataObjects[i].desc); + // Update the batch DL queue stats object + ++(batchDLQueueStats.numFilesInQueue); + batchDLQueueStats.filenames.push({ filename: metadataObjects[i].name, desc: metadataObjects[i].desc }); + batchDLQueueStats.totalSize += +(metadataObjects[i].size); + batchDLQueueStats.totalCost += +(metadataObjects[i].cost); + } + + if (!addToQueueSuccessful) + filenamesFailed.push(metadataObjects[i].name); + } + } + + batchDLFile.close(); + } + + // Frame location & size for batch DL queue stats or filenames that failed + var frameUpperLeftX = gFileListMenu.pos.x + 2; + var frameUpperLeftY = gFileListMenu.pos.y + 2; + var frameWidth = gFileListMenu.size.width - 4; + var frameInnerWidth = frameWidth - 2; // Without borders + var frameHeight = 8; + // To make the list refresh info to return to the main script loop + function makeBatchRefreshInfoObj(pFrameWidth, pFrameHeight) + { + return { + startX: 3, + startY: 3, + width: pFrameWidth+1, + height: pFrameHeight + }; + } + + // If there were no failures, then show a success message & prompt the user if they + // want to download their batch queue. Otherwise, show the filenames that failed to + // get added. + if (filenamesFailed.length == 0) + { + displayMsg("Your batch DL queue was sucessfully updated", false, true); + // Prompt if the user wants to download their batch queue + if (bbs.batch_dnload_total > 0) + { + // Clear most of the screen area so the user has focus on the batch DL queue stats + var fullLineFormatStr = "%" + console.screen_columns + "s"; + var leftFormatStr = "%" + frameUpperLeftX + "s"; + var rightFormatStr = "%" + +(frameUpperLeftX+frameWidth-1) + "s"; + var lastFrameRow = frameUpperLeftY + frameHeight - 1; + var lastRow = console.screen_rows - 1; + console.print("\1n"); + for (var screenRow = gNumHeaderLinesDisplayed+1; screenRow <= lastRow; ++screenRow) + { + console.gotoxy(1, screenRow); + if (screenRow < frameUpperLeftY || screenRow > lastFrameRow) + printf(fullLineFormatStr, ""); + else + { + printf(leftFormatStr, ""); + console.gotoxy(frameUpperLeftX+frameWidth, screenRow); + printf(rightFormatStr, ""); + } + } + + // Build a frame with batch DL queue stats and prompt the user if they want to + // download their batch DL queue + var frameTitle = "Download your batch queue (Y/N)?"; + // \1cFiles: \1h1 \1n\1c(\1h100 \1n\1cMax) Credits: 0 Bytes: \1h2,228,254 \1n\1c Time: 00:09:40 + //var fileSize = gFilebase.get_size(gFileList[pIdx].name); + // Note: The maximum number of allowed files in the batch download queue doesn't seem to + // be available to JavaScript. + var totalQueueSize = batchDLQueueStats.totalSize + pFileList[pFileListMenu.selectedItemIdx].size; + var totalQueueCost = batchDLQueueStats.totalCost + pFileList[pFileListMenu.selectedItemIdx].cost; + var queueStats = "\1n\1cFiles: \1h" + batchDLQueueStats.numFilesInQueue + " \1n\1cCredits: \1h" + + totalQueueCost + "\1n\1c Bytes: \1h" + numWithCommas(totalQueueSize) + "\1n\1w\r\n"; + for (var i = 0; i < batchDLQueueStats.filenames.length; ++i) + { + queueStats += shortenFilename(batchDLQueueStats.filenames[i].filename, frameInnerWidth, false) + "\r\n"; + queueStats += batchDLQueueStats.filenames[i].desc.substr(0, frameInnerWidth) + "\r\n"; + if (i < batchDLQueueStats.filenames.length-1) + queueStats += "\r\n"; + } + var additionalQuitKeys = "yYnN"; + var lastUserInput = displayBorderedFrameAndDoInputLoop(frameUpperLeftX, frameUpperLeftY, frameWidth, + frameHeight, gColors.batchDLInfoWindowBorder, + frameTitle, gColors.batchDLInfoWindowTitle, + queueStats, additionalQuitKeys); + if (lastUserInput.toUpperCase() == "Y") + { + retObj.reDrawFileListMenu = true; + retObj.reDrawListerHeader = true; + retObj.reDrawCmdBar = true; + console.print("\1n"); + console.gotoxy(1, console.screen_rows); + console.crlf(); + bbs.batch_download(); + } + else + { + retObj.reDrawFileListMenu = true; + // Construct the file list redraw info. Note that the X and Y are relative + // to the file list menu, not absolute screen coordinates. + //retObj.fileListPartialRedrawInfo = makeBatchRefreshInfoObj(frameWidth, frameHeight); + } + } + } + else + { + eraseMsgBoxScreenArea(); + // Build a frame object to show the names of the files that failed to be added to the + // user's batch DL queue + var frameTitle = "Failed to add these files to batch DL queue"; + var fileListStr = "\1n\1w"; + for (var i = 0; i < filenamesFailed.length; ++i) + fileListStr += shortenFilename(filenamesFailed[i], frameInnerWidth, false) + "\r\n"; + var lastUserInput = displayBorderedFrameAndDoInputLoop(frameUpperLeftX, frameUpperLeftY, frameWidth, + frameHeight, gColors.batchDLInfoWindowBorder, + frameTitle, gColors.batchDLInfoWindowTitle, + fileListStr, ""); + // Construct the file list redraw info. Note that the X and Y are relative + // to the file list menu, not absolute screen coordinates. + retObj.fileListPartialRedrawInfo = makeBatchRefreshInfoObj(frameWidth, frameHeight); + } + } + + return retObj; +} +// Gets stats about the user's batch download queue. +// +// Return value: An object containing the following properties: +// numFilesInQueue: The number of files already in the queue +// totalSize: The total size of the files in the queue +// totalCost: The total cost of the files in the queue +// filenames: An array of objects, each containing the filename and +// descriptions (desc) of the files in the download queue +function getUserDLQueueStats() +{ + var retObj = { + numFilesInQueue: 0, + totalSize: 0, + totalCost: 0, + filenames: [] + }; + + var batchDLFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".dnload"; + var batchDLFile = new File(batchDLFilename); + if (batchDLFile.open(batchDLFile.exists ? "r+" : "w+")) + { + // See if a section exists for the filename + //File.iniGetAllObjects([name_property] [,prefix=none] [,lowercase=false] [,blanks=false]) + var allIniObjs = batchDLFile.iniGetAllObjects(); + console.print("\1n\r\n"); + for (var i = 0; i < allIniObjs.length; ++i) + { + if (typeof(allIniObjs[i]) === "object") + { + ++(retObj.numFilesInQueue); + //allIniObjs[i].name + //allIniObjs[i].dir + //allIniObjs[i].desc + retObj.filenames.push({ filename: allIniObjs[i].name, desc: allIniObjs[i].desc }); + // dir is the internal directory code + if (allIniObjs[i].dir.length > 0) + { + var filebase = new FileBase(allIniObjs[i].dir); + if (filebase.open()) + { + var fileInfo = filebase.get(allIniObjs[i].name); + if (typeof(fileInfo) === "object") + { + retObj.totalSize += +(fileInfo.size); + retObj.totalCost += +(fileInfo.cost); + } + filebase.close(); + } + } + } + } + + /* + var sections = batchDLFile.iniGetSections(); + retObj.numFilesInQueue = sections.length; + for (var i = 0; i < sections.length; ++i) + { + //var desc = + //retObj.filenames.push({ filename: sections[i], desc: }); + // Get the dir code from the section, then get the size and cost for + // the file from the filebase and add them to the totals in retObj + var dirCode = batchDLFile.iniGetValue(sections[i], "dir", ""); + if (dirCode.length > 0) + { + var filebase = new FileBase(dirCode); + if (filebase.open()) + { + var fileInfo = filebase.get(sections[i]); + if (typeof(fileInfo) === "object") + { + retObj.totalSize += +(fileInfo.size); + retObj.totalCost += +(fileInfo.cost); + } + filebase.close(); + } + } + } + */ + batchDLFile.close(); + } + + return retObj; +} + +// Displays the help screen. +// +// Parameters: +// pDirCode: The internal code of the file directory being used +// pFilebase: A Filebase object representing the downloadable file directory. This +// is assumed to be open. +function displayHelpScreen(pDirCode, pFilebase) +{ + var retObj = getDefaultActionRetObj(); + + console.clear("\1n"); + // Display program information + displayTextWithLineBelow("Digital Distortion File Lister", true, "\1n\1c\1h", "\1k\1h") + console.center("\1n\1cVersion \1g" + LISTER_VERSION + " \1w\1h(\1b" + LISTER_DATE + "\1w)"); + console.crlf(); + + // Display information about the current file directory + var libIdx = file_area.dir[pDirCode].lib_index; + var dirIdx = file_area.dir[pDirCode].index; + console.print("\1n\1cCurrent file library: \1g" + file_area.lib_list[libIdx].description); + console.crlf(); + console.print("\1cCurrent file directory: \1g" + file_area.dir[pDirCode].description); + console.crlf(); + console.print("\1cThere are \1g" + pFilebase.files + " \1cfiles in this directory."); + console.crlf(); + console.crlf(); + + // Display information about the lister + var helpStr = "This lists files in your current file directory with a lightbar interface (for an ANSI terminal). "; + helpStr += "The file list can be navigated using the up & down arrow keys, PageUp, PageDown, Home, and End keys. " + helpStr += "The currently highlighted file in the menu is used by default for the various actions. For batch download " + helpStr += "selection, "; + if (gUserIsSysop) + helpStr += "moving, and deleting, "; + helpStr += "you can select multiple files by using the spacebar. "; + helpStr += "There is also a command bar accross the bottom of the screen - You can select an action on the "; + helpStr += "action bar by using the left & right arrow keys and pressing enter to choose an action. Alternately, "; + helpStr += "you can press the first character of the action word to perform the action."; + helpStr += " Also, the following actions are available:"; + // Wrap the help string to the user's terminal width, and replace all instances of + // newlines with carriage return + newline, then display the help text. + helpStr = word_wrap(helpStr, console.screen_columns - 1).replace(/\n/g, "\r\n"); + console.print(helpStr); + // Display the commands available + var commandStrWidth = 8; + var printfStr = "\1n\1c\1h%-" + commandStrWidth + "s\1g: \1n\1c%s\r\n"; + printf(printfStr, "I", "Display extended file information"); + printf(printfStr, "V", "View the file"); + printf(printfStr, "B", "Flag the file(s) for batch download"); + if (gUserIsSysop) + { + printf(printfStr, "M", "Move the file(s) to another directory"); + printf(printfStr, "D", "Delete the file(s)"); + } + printf(printfStr, "?", "Show this help screen"); + printf(printfStr, "Q", "Quit back to the BBS"); + console.print("\1n"); + console.crlf(); + //console.pause(); + + retObj.reDrawListerHeader = true; + retObj.reDrawFileListMenu = true; + retObj.reDrawCmdBar = true; + return retObj; +} + +// Allows the user to move the selected file to another filebase. Only for sysops! +// +// Parameters: +// pDirCode: The internal code of the original file directory +// pFilebase: The FileBase object representing the file directory (assumed open) +// pFileList: The list of file metadata objects from the file directory +// pFileListMenu: The menu object for the file diretory +// +// Return value: An object with values to indicate status & screen refresh actions; see +// getDefaultActionRetObj() for details. +function chooseFilebaseAndMoveFileToOtherFilebase_Lightbar(pDirCode, pFilebase, pFileList, pFileListMenu) +{ + var retObj = getDefaultActionRetObj(); + + // Confirm with the user to move the file(s). If they don't want to, + // then just return now. + var filenames = []; + if (gFileListMenu.numSelectedItemIndexes() > 0) + { + for (var idx in gFileListMenu.selectedItemIndexes) + filenames.push(pFileList[+idx].name); + } + else + filenames.push(pFileList[pFileListMenu.selectedItemIdx].name); + // Note that confirmFileActionWithUser() will re-draw the parts of the file + // list menu that are necessary. + var moveFilesConfirmed = confirmFileActionWithUser(filenames, "Move", false); + if (!moveFilesConfirmed) + return retObj; + + + retObj.reDrawFileListMenu = true; + // Prompt the user which directory to move the file to + var chosenDirCode = null; + var fileLibMenu = createFileLibMenu(); + console.gotoxy(fileLibMenu.pos.x, fileLibMenu.pos.y-1); + printf("\1n\1c\1h|\1n\1c%-" + +(fileLibMenu.size.width-1) + "s\1n", "Choose a destination area"); + var continueOn = true; + while (continueOn) + { + var chosenLibIdx = fileLibMenu.GetVal(); + if (typeof(chosenLibIdx) === "number") + { + var fileDirMenu = createFileDirMenu(chosenLibIdx); + chosenDirCode = fileDirMenu.GetVal(); + if (typeof(chosenDirCode) === "string") + { + if (chosenDirCode != pDirCode) + continueOn = false; + else + { + chosenDirCode = ""; + displayMsg("Can't move to the same directory", true); + } + } + } + else + continueOn = false; + } + // If the user chose a directory, then move the file there. + if (typeof(chosenDirCode) === "string" && chosenDirCode.length > 0) + { + // Build an array of file indexes and sort the array + var fileIndexes = []; + if (gFileListMenu.numSelectedItemIndexes() > 0) + { + for (var idx in gFileListMenu.selectedItemIndexes) + fileIndexes.push(+idx); + } + else + fileIndexes.push(+(pFileListMenu.selectedItemIdx)); + fileIndexes.sort(); + + // Go through the list of files and move each of them + var moveAllSucceeded = true; + for (var i = 0; i < fileIndexes.length; ++i) + { + var fileIdx = fileIndexes[i]; + var moveRetObj = moveFileToOtherFilebase(pFilebase, pFileList[fileIdx], chosenDirCode); + if (moveRetObj.moveSucceeded) + { + // Remove the file info object from the file list array + pFileList.splice(fileIdx, 1); + // Subtract 1 from the remaining indexes in the fileIndexes array + for (var j = i+1; j < fileIndexes.length; ++j) + fileIndexes[j] = fileIndexes[j] - 1; + } + else + { + moveAllSucceeded = false; + displayMsg(pFileList[fileIdx].name, true); + displayMsg(moveRetObj.failReason, true); + } + } + if (moveAllSucceeded) + { + var libIdx = file_area.dir[chosenDirCode].lib_index; + var msg = "Successfully moved the file(s) to " + + file_area.lib_list[libIdx].description + " - " + + file_area.dir[chosenDirCode].description + displayMsg(msg, false); + } + // After moving the files, if the file directory is empty, say so + if (pFilebase.files == 0) + { + displayMsg("The directory now has no files.", false); + retObj.exitNow = true; + } + } + + return retObj; +} + +// Allows the user to remove the selected file from the filebase. Only for sysops! +// +// Parameters: +// pDirCode: The internal code of the original file directory +// pFilebase: The FileBase object representing the file directory (assumed open) +// pFileList: The list of file metadata objects from the file directory +// pFileListMenu: The menu object for the file diretory +// +// Return value: An object with values to indicate status & screen refresh actions; see +// getDefaultActionRetObj() for details. +function removeFileFromFilebase(pDirCode, pFilebase, pFileList, pFileListMenu) +{ + var retObj = getDefaultActionRetObj(); + + // Confirm the action with the user. If the user confirms, then remove the file(s). + // If there are multiple selected files, then prompt to remove each of them. + // Otherwise, prompt for the one selected file. + var filenames = []; + if (gFileListMenu.numSelectedItemIndexes() > 0) + { + for (var idx in gFileListMenu.selectedItemIndexes) + filenames.push(pFileList[+idx].name); + } + else + filenames.push(pFileList[pFileListMenu.selectedItemIdx].name); + // Note that confirmFileActionWithUser() will re-draw the parts of the file list menu + // that are necessary. + var removeFilesConfirmed = confirmFileActionWithUser(filenames, "Remove", false); + if (removeFilesConfirmed) + { + // FileBase.remove(filename [,delete=false]) + var succeeded = pFilebase.remove(pFileList[pFileListMenu.selectedItemIdx].name, true); + if (succeeded) + { + var messages = [ "Successfully removed the file(s)." ]; + // Remove the file info object from the file list array + pFileList.splice(pFileListMenu.selectedItemIdx, 1); + // Adjust the file list menu's current selected index + --pFileListMenu.selectedItemIdx; + if (pFileListMenu.selectedItemIdx < 0) + pFileListMenu.selectedItemIdx = 0; + if (pFileListMenu.topItemIdx > pFileListMenu.selectedItemIdx) + pFileListMenu.topItemIdx = pFileListMenu.selectedItemIdx; + // If the file directory still has files in it, have the menu redraw + // itself to refresh with the missing entry. Otherwise (no files left), + // say so and have the lister exit now. + if (pFilebase.files > 0) + retObj.reDrawFileListMenu = true; + else + { + messages.push("The directory now has no files."); + retObj.exitNow = true; + } + displayMsgs(messages, false); + } + else + displayMsg("Failed to remove the file!", true); // console.print("\1y\1hFailed to remove the file!\1n"); + } + + return retObj; +} + +/////////////////////////////////////////////////////////////////////////////// +// DDFileMenuBar stuff + +function DDFileMenuBar(pPos) +{ + // Member functions + this.constructPromptText = DDFileMenuBar_constructPromptText; + this.getItemTextFromIdx = DDFileMenuBar_getItemTextFromIdx; + this.writePromptLine = DDFileMenuBar_writePromptLine; + this.refreshWithNewAction = DDFileMenuBar_refreshWithNewAction; + this.getDDFileMenuBarItemText = DDFileMenuBar_getDDFileMenuBarItemText; + this.incrementMenuItemAndRefresh = DDFileMenuBar_incrementMenuItemAndRefresh; + this.decrementMenuItemAndRefresh = DDFileMenuBar_decrementMenuItemAndRefresh; + this.getCurrentSelectedAction = DDFileMenuBar_getCurrentSelectedAction; + this.getActionFromChar = DDFileMenuBar_getActionFromChar; + this.setCurrentActionCode = DDFileMenuBar_setCurrentActionCode; + this.getAllActionKeysStr = DDFileMenuBar_getAllActionKeysStr; + + // Member variables + this.pos = { + x: 1, + y: 1 + }; + if (typeof(pPos) === "object" && pPos.hasOwnProperty("x") && pPos.hasOwnProperty("y") && typeof(pPos.x) === "number" && typeof(pPos.y) === "number") + { + if (pPos.x >= 1 && pPos.x <= console.screen_columns) + this.pos.x = pPos.x; + if (pPos.y >= 1 && pPos.y <= console.screen_rows) + this.pos.y = pPos.y; + } + + this.currentCommandIdx = 0; // The index of the current command for the menu array + this.lastCommandIdx = 0; // To keep track of the previous command index + + // Construct this.cmdArray: An array of the options + this.cmdArray = []; + this.cmdArray.push(new DDFileMenuBarItem("Info", 0, FILE_VIEW_INFO)); + this.cmdArray.push(new DDFileMenuBarItem("View", 0, FILE_VIEW)); + this.cmdArray.push(new DDFileMenuBarItem("Batch", 0, FILE_ADD_TO_BATCH_DL)); + if (gUserIsSysop) + { + this.cmdArray.push(new DDFileMenuBarItem("Move", 0, FILE_MOVE)); + this.cmdArray.push(new DDFileMenuBarItem("Del", 0, FILE_DELETE)); + } + this.cmdArray.push(new DDFileMenuBarItem("?", 0, HELP)); + this.cmdArray.push(new DDFileMenuBarItem("Quit", 0, QUIT)); + + // Construct the prompt text (this must happen after this.cmdArray is built) + this.promptText = ""; + this.constructPromptText(); // this.promptText will be constructed here +} +// For the DDFileMenuBar class: Constructs the prompt text. This must be called after +// this.cmdArray is built. +// +// Return value: The number of additional solid blocks used to fill the whole screen row +function DDFileMenuBar_constructPromptText() +{ + var totalItemTextLen = 0; + for (var i = 0; i < this.cmdArray.length; ++i) + totalItemTextLen += this.cmdArray[i].itemText.length; + // The number of inner characters (without the outer solid blocks) is the total text + // length of all the items + 2 characters for each item except the last one + var numInnerChars = totalItemTextLen + (2 * (this.cmdArray.length-1)); + // The number of solid blocks: Subtracting 11 because there will be 5 block characters on each side, + // and subtract 1 extra so it doesn't fill the last character on the screen + var numSolidBlocks = console.screen_columns - numInnerChars - 11; + var numSolidBlocksPerSide = Math.floor(numSolidBlocks / 2); + // Build the prompt text: Start with the left blocks + this.promptText = "\1n\1w" + BLOCK1 + BLOCK2 + BLOCK3 + BLOCK4; + for (var i = 0; i < numSolidBlocksPerSide; ++i) + this.promptText += BLOCK4; + this.promptText += THIN_RECTANGLE_LEFT; + // Add the menu item text & block characters + var menuItemXPos = 6 + numSolidBlocksPerSide; // The X position of the start of item text for each item + for (var i = 0; i < this.cmdArray.length; ++i) + { + this.cmdArray[i].pos = menuItemXPos; + var numTrailingBlockChars = 0; + var selected = (i == this.currentCommandIdx); + var withTrailingBlock = false; + if (i < this.cmdArray.length-1) + { + withTrailingBlock = true; + numTrailingBlockChars = 2; + } + menuItemXPos += this.cmdArray[i].itemText.length + numTrailingBlockChars; + this.promptText += this.getDDFileMenuBarItemText(this.cmdArray[i].itemText, selected, withTrailingBlock); + } + // Add the right-side blocks + this.promptText += "\1w" + THIN_RECTANGLE_RIGHT; + for (var i = 0; i < numSolidBlocksPerSide; ++i) + this.promptText += BLOCK4; + this.promptText += BLOCK3 + BLOCK2 + BLOCK1 + "\1n"; +} +// For the DDFileMenuBar class: Gets the text for a prompt item based on its index +function DDFileMenuBar_getItemTextFromIdx(pIdx) +{ + if (typeof(pIdx) !== "number" || pIdx < 0 || pIdx >= this.cmdArray.length) + return ""; + return this.cmdArray[pIdx].itemText; +} +// For the DDFileMenuBar class: Writes the prompt text at the defined location +function DDFileMenuBar_writePromptLine() +{ + // Place the cursor at the defined location, then write the prompt text + console.gotoxy(this.pos.x, this.pos.y); + console.print(this.promptText); +} +// For the DDFileMenuBar class: Refreshes 2 items in the command bar text line +// +// Parameters: +// pCmdIdx: The index of the new/current command +function DDFileMenuBar_refreshWithNewAction(pCmdIdx) +{ + if (typeof(pCmdIdx) !== "number") + return; + if (pCmdIdx == this.lastCommandIdx) + return; + + // Refresh the prompt area for the previous index with regular colors + // Re-draw the last item text with regular colors + var itemText = this.getItemTextFromIdx(this.lastCommandIdx); + console.gotoxy(this.cmdArray[this.lastCommandIdx].pos, this.pos.y); + console.print("\1n" + this.getDDFileMenuBarItemText(itemText, false, false)); + // Draw the new item text with selected colors + itemText = this.getItemTextFromIdx(pCmdIdx); + console.gotoxy(this.cmdArray[pCmdIdx].pos, this.pos.y); + console.print("\1n" + this.getDDFileMenuBarItemText(itemText, true, false)); + console.gotoxy(this.pos.x+strip_ctrl(this.promptText).length-1, this.pos.y); + + this.lastCommandIdx = this.currentCommandIdx; + this.currentCommandIdx = pCmdIdx; +} +// For the DDFileMenuBar class: Returns a string containing a piece of text for the +// menu bar text with its color attributes. +// +// Parameters: +// pText: The text for the item +// pSelected: Boolean - Whether or not the item is selected +// pWithTrailingBlock: Boolean - Whether or not to include the trailing block +// +// Return value: A string containing the item text for the action bar +function DDFileMenuBar_getDDFileMenuBarItemText(pText, pSelected, pWithTrailingBlock) +{ + if (typeof(pText) !== "string" || pText.length == 0) + return ""; + + var selected = (typeof(pSelected) === "boolean" ? pSelected : false); + var withTrailingBlock = (typeof(pWithTrailingBlock) === "boolean" ? pWithTrailingBlock : false); + + // Separate the first character from the rest of the text + var firstChar = pText.length > 0 ? pText.charAt(0) : ""; + var restOfText = pText.length > 1 ? pText.substr(1, pText.length - 1) : ""; + // Build the item text and return it + var itemText = "\1n"; + if (selected) + itemText += "\1" + "1\1r\1h" + firstChar + "\1n\1" + "1\1k" + restOfText; + else + itemText += "\1" + "6\1c\1h" + firstChar + "\1n\1" + "6\1k" + restOfText; + itemText += "\1n"; + if (withTrailingBlock) + itemText += "\1w" + THIN_RECTANGLE_RIGHT + THIN_RECTANGLE_LEFT + "\1n"; + return itemText; +} +// For the DDFileMenuBar class: Increments to the next menu item and refreshes the +// menu bar on the screen +function DDFileMenuBar_incrementMenuItemAndRefresh() +{ + ++this.currentCommandIdx; + if (this.currentCommandIdx >= this.cmdArray.length) + this.currentCommandIdx = 0; + this.refreshWithNewAction(this.currentCommandIdx); +} +// For the DDFileMenuBar class: Decrements to the previous menu item and refreshes the +// menu bar on the screen +function DDFileMenuBar_decrementMenuItemAndRefresh() +{ + --this.currentCommandIdx; + if (this.currentCommandIdx < 0) + this.currentCommandIdx = this.cmdArray.length - 1; + this.refreshWithNewAction(this.currentCommandIdx); +} +// For the DDFileMenuBar class: Gets the return code for the currently selected action +function DDFileMenuBar_getCurrentSelectedAction() +{ + return this.cmdArray[this.currentCommandIdx].retCode; +} +// For the DDFileMenuBar class: Gets the return code matching a given character. +// If there is no match, this will return -1. +// +// Parameters: +// pChar: The character to match +// pCaseSensitive: Optional - Boolean - Whether or not to do a case-sensitive match. +// This defaults to false. +function DDFileMenuBar_getActionFromChar(pChar, pCaseSensitive) +{ + if (typeof(pChar) !== "string" || pChar.length == 0) + return -1; + + var caseSensitive = (typeof(pCaseSensitive) === "boolean" ? pCaseSensitive : false); + + var retCode = -1; + if (caseSensitive) + { + for (var i = 0; i < this.cmdArray.length && retCode == -1; ++i) + { + if (this.cmdArray[i].itemText.length > 0 && this.cmdArray[i].itemText.charAt(0) == pChar) + retCode = this.cmdArray[i].retCode; + } + } + else + { + // Not case sensitive + var charUpper = pChar.toUpperCase(); + for (var i = 0; i < this.cmdArray.length && retCode == -1; ++i) + { + if (this.cmdArray[i].itemText.length > 0 && this.cmdArray[i].itemText.charAt(0).toUpperCase() == charUpper) + retCode = this.cmdArray[i].retCode; + } + } + return retCode; +} +// For the DDFileMenuBar class: Sets the current command item in the menu bar based on its +// action code +// +// Parameters: +// pActionCode: The code of the action +function DDFileMenuBar_setCurrentActionCode(pActionCode) +{ + if (typeof(pActionCode) !== "number") + return; + + for (var i = 0; i < this.cmdArray.length; ++i) + { + if (this.cmdArray[i].retCode == pActionCode) + { + this.currentCommandIdx = i; + this.lastCommandIdx = i; + break; + } + } +} +// For the DDFileMenuBar: Gets all the action hotkeys as a string +// +// Parameters: +// pLowercase: Boolean - Whether or not to include letters as lowercase +// pUppercase: Boolean - Whether or not to include letters as uppercase +// +// Return value: All the action hotkeys as a string +function DDFileMenuBar_getAllActionKeysStr(pLowercase, pUppercase) +{ + var hotkeysStr = ""; + for (var i = 0; i < this.cmdArray.length; ++i) + { + if (this.cmdArray[i].itemText.length > 0) + hotkeysStr += this.cmdArray[i].itemText.charAt(0); + } + var hotkeysToReturn = ""; + if (pLowercase) + hotkeysToReturn += hotkeysStr.toLowerCase(); + if (pUppercase) + hotkeysToReturn += hotkeysStr.toUpperCase(); + return hotkeysToReturn; +} + +// Consctructor for an DDFileMenuBarItem +// +// Parameters: +// pItemText: The text of the item +// pPos: Horizontal (or vertical) starting location in the bar +// pRetCode: The item's return code +function DDFileMenuBarItem(pItemText, pPos, pRetCode) +{ + this.itemText = pItemText; + this.pos = pPos; + this.retCode = pRetCode; +} + + +/////////////////////////////////////////////////////////////////////////////// +// Helper functions + +// Moves a file from one filebase to another +// +// Parameters: +// pSrcFilebase: A FileBase object representing the source filebase. This is assumed to be open. +// pSrcFileMetadata: Metadata for the source file. This is assumed to contain 'normal' detail (not extended) +// pDestDirCode: The internal code of the destination filebase to move to the file to +// +// Return value: An object containing the following properties: +// moveSucceeded: Boolean - Whether or not the move succeeded +// failReason: If the move failed, this is a string that specifies why it failed +function moveFileToOtherFilebase(pSrcFilebase, pSrcFileMetadata, pDestDirCode) +{ + var retObj = { + moveSucceeded: false, + failReason: "" + }; + + // pSrcFileMetadata is assumed to be a basic file metadata object, without extended + // information. Get a metadata object with maximum information so we have all + // metadata available. + var extdFileInfo = pSrcFilebase.get(pSrcFileMetadata, FileBase.DETAIL.MAX); + // Move the file over, remove it from the original filebase, and add it to the new filebase + var srcFilenameFull = pSrcFilebase.get_path(pSrcFileMetadata); + var destFilenameFull = file_area.dir[pDestDirCode].path + pSrcFileMetadata.name; + if (file_rename(srcFilenameFull, destFilenameFull)) + { + if (pSrcFilebase.remove(pSrcFileMetadata.name, false)) + { + // Add the file to the other directory + var destFilebase = new FileBase(pDestDirCode); + if (destFilebase.open()) + { + retObj.moveSucceeded = destFilebase.add(extdFileInfo); + destFilebase.close(); + } + else + { + retObj.failReason = "Failed to open the destination filebase"; + // Try to add the file back to the source filebase + var moveBackSucceeded = false; + if (file_rename(destFilenameFull, srcFilenameFull)) + moveBackSucceeded = pSrcFilebase.add(extdFileInfo); + if (!moveBackSucceeded) + retObj.failReason += " & moving the file back failed"; + } + } + else + retObj.failReason = "Failed to remove the file from the source directory"; + } + else + retObj.failReason = "Failed to move the file to the new filebase directory"; + + return retObj; +} + +// Counts the number of occurrences of a substring within a string +// +// Parameters: +// pStr: The string to count occurences in +// pSubstr: The string to look for within pStr +// +// Return value: The number of occurrences of pSubstr found in pStr +function countOccurrencesInStr(pStr, pSubstr) +{ + if (typeof(pStr) !== "string" || typeof(pSubstr) !== "string") return 0; + if (pStr.length == 0 || pSubstr.length == 0) return 0; + + var count = 0; + var strIdx = pStr.indexOf(pSubstr); + while (strIdx > -1 && strIdx < pStr.length) + { + ++count; + strIdx = pStr.indexOf(pSubstr, strIdx+1); + } + return count; +} + +// Constructs & displays a frame with a border around it, and performs a user input loop +// until the user quits out of the input loop. +// +// Parameters: +// pFrameX: The X coordinate of the upper-left corner of the frame (including border) +// pFrameY: The Y coordinate of the upper-left corner of the frame (including border) +// pFrameWidth: The width of the frame (including border) +// pFrameHeight: The height of the frame (including border) +// pBorderColor: The attribute codes for the border color +// pFrameTitle: The title (text) to use in the frame border +// pTitleColor: Optional string - The attribute codes for the color to use for the frame title +// pFrameContents: The contents to display in the frame +// pAdditionalQuitKeys: Optional - A string containing additional keys to quit the +// input loop. This is case-sensitive. +// +// Return value: The last keypress/input from the user +function displayBorderedFrameAndDoInputLoop(pFrameX, pFrameY, pFrameWidth, pFrameHeight, pBorderColor, pFrameTitle, pTitleColor, pFrameContents, pAdditionalQuitKeys) +{ + if (typeof(pFrameX) !== "number" || typeof(pFrameY) !== "number" || typeof(pFrameWidth) !== "number" || typeof(pFrameHeight) !== "number") + return; + + // Display the border for the frame + var keyHelpStr = "\1n\1c\1hQ\1b/\1cEnter\1b/\1cESC\1y: \1gClose\1b"; + var scrollLoopNavHelp = "\1c\1hUp\1b/\1cDn\1b/\1cHome\1b/\1cEnd\1b/\1cPgup\1b/\1cPgDn\1y: \1gNav"; + if (console.screen_columns >= 80) + keyHelpStr += ", " + scrollLoopNavHelp; + var borderColor = (typeof(pBorderColor) === "string" ? pBorderColor : "\1r"); + drawBorder(pFrameX, pFrameY, pFrameWidth, pFrameHeight, borderColor, "double", pFrameTitle, pTitleColor, keyHelpStr); + + // Construct the frame window for the file info + // Create a Frame here with the full filename, extended description, etc. + var frameX = pFrameX + 1; + var frameY = pFrameY + 1; + var frameWidth = pFrameWidth - 2; + var frameHeight = pFrameHeight - 2; + var frameObj = new Frame(frameX, frameY, frameWidth, frameHeight, BG_BLACK); + frameObj.attr &=~ HIGH; + frameObj.v_scroll = true; + frameObj.h_scroll = false; + frameObj.scrollbars = true; + var scrollbarObj = new ScrollBar(frameObj, {bg: BG_BLACK, fg: LIGHTGRAY, orientation: "vertical", autohide: false}); + // Put the file info string in the frame window, then start the + // user input loop for the frame + frameObj.putmsg(pFrameContents, "\1n"); + var lastUserInput = doFrameInputLoop(frameObj, scrollbarObj, pFrameContents, pAdditionalQuitKeys); + //infoFrame.bottom(); + + return lastUserInput; +} + +// Displays a Frame object and handles the input loop for navigation until +// the user presses Q, Enter, or ESC To quit the input loop +// +// Parameters: +// pFrame: The Frame object +// pScrollbar: The Scrollbar object for the Frame +// pFrameContentStr: The string content that was added to the Frame +// pAdditionalQuitKeys: Optional - A string containing additional keys to quit the +// input loop. This is case-sensitive. +// +// Return value: The last keypress/input from the user +function doFrameInputLoop(pFrame, pScrollbar, pFrameContentStr, pAdditionalQuitKeys) +{ + var checkAdditionalQuitKeys = (typeof(pAdditionalQuitKeys) === "string" && pAdditionalQuitKeys.length > 0); + + // Input loop for the frame to let the user scroll it + var frameContentTopYOffset = 0; + //var maxFrameYOffset = pFrameContentStr.split("\r\n").length - pFrame.height; + var maxFrameYOffset = countOccurrencesInStr(pFrameContentStr, "\r\n") - pFrame.height; + if (maxFrameYOffset < 0) maxFrameYOffset = 0; + var userInput = ""; + var continueOn = true; + do + { + pFrame.scrollTo(0, frameContentTopYOffset); + pFrame.invalidate(); + pScrollbar.cycle(); + pFrame.cycle(); + pFrame.draw(); + // Note: getKeyWithESCChars() is defined in dd_lightbar_menu.js. + userInput = getKeyWithESCChars(K_NOECHO|K_NOSPIN|K_NOCRLF, 30000).toUpperCase(); + if (userInput == KEY_UP) + { + if (frameContentTopYOffset > 0) + --frameContentTopYOffset; + } + else if (userInput == KEY_DOWN) + { + if (frameContentTopYOffset < maxFrameYOffset) + ++frameContentTopYOffset; + } + else if (userInput == KEY_PAGEUP) + { + frameContentTopYOffset -= pFrame.height; + if (frameContentTopYOffset < 0) + frameContentTopYOffset = 0; + } + else if (userInput == KEY_PAGEDN) + { + frameContentTopYOffset += pFrame.height; + if (frameContentTopYOffset > maxFrameYOffset) + frameContentTopYOffset = maxFrameYOffset; + } + else if (userInput == KEY_HOME) + frameContentTopYOffset = 0; + else if (userInput == KEY_END) + frameContentTopYOffset = maxFrameYOffset; + + // Check for whether to continue the input loop + continueOn = (userInput != "Q" && userInput != KEY_ENTER && userInput != KEY_ESC); + // If the additional quit keys does not contain the user's keypress, then continue + // the input loop. + // In other words, if the additional quit keys includes the user's keypress, then + // don't continue. + if (continueOn && checkAdditionalQuitKeys) + continueOn = (pAdditionalQuitKeys.indexOf(userInput) < 0); + } while (continueOn); + + return userInput; +} + +// Displays the header lines for showing above the file list +// +// Parameters: +// pDirCode: The internal code of the file directory to use +function displayFileLibAndDirHeader(pDirCode) +{ + if (typeof(pDirCode) !== "string") + return; + if (typeof(file_area.dir[pDirCode]) === "undefined") + return; + + var libIdx = file_area.dir[pDirCode].lib_index; + var dirIdx = file_area.dir[pDirCode].index; + var libDesc = file_area.lib_list[libIdx].description; + var dirDesc = file_area.dir[pDirCode].description; + + var hdrTextWidth = console.screen_columns - 21; + var descWidth = hdrTextWidth - 11; + + // Library line + console.print("\1n\1w" + BLOCK1 + BLOCK2 + BLOCK3 + BLOCK4 + THIN_RECTANGLE_LEFT); + printf("\1cLib \1w\1h#\1b%4d\1c: \1n\1c%-" + descWidth + "s\1n", +(libIdx+1), libDesc.substr(0, descWidth)); + console.print("\1w" + THIN_RECTANGLE_RIGHT + "\1k\1h" + BLOCK4 + "\1n\1w" + THIN_RECTANGLE_LEFT + + "\1g\1hDD File\1n\1w"); + console.print(THIN_RECTANGLE_RIGHT + BLOCK4 + BLOCK3 + BLOCK2 + BLOCK1); + console.crlf(); + // Directory line + console.print("\1n\1w" + BLOCK1 + BLOCK2 + BLOCK3 + BLOCK4 + THIN_RECTANGLE_LEFT); + printf("\1cDir \1w\1h#\1b%4d\1c: \1n\1c%-" + descWidth + "s\1n", +(dirIdx+1), dirDesc.substr(0, descWidth)); + console.print("\1w" + THIN_RECTANGLE_RIGHT + "\1k\1h" + BLOCK4 + "\1n\1w" + THIN_RECTANGLE_LEFT + + "\1g\1hLister \1n\1w"); + console.print(THIN_RECTANGLE_RIGHT + BLOCK4 + BLOCK3 + BLOCK2 + BLOCK1); + console.print("\1n"); + gNumHeaderLinesDisplayed = 2; + + // List header + console.crlf(); + displayListHdrLine(false); + ++gNumHeaderLinesDisplayed; + + gErrorMsgBoxULY = gNumHeaderLinesDisplayed; // Note: console.screen_rows is 1-based +} +// Displays the header line with the column headers for the file list +// +// Parameters: +// pMoveToLocationFirst: Boolean - Whether to move the cursor to the required location first. +function displayListHdrLine(pMoveToLocationFirst) +{ + if (pMoveToLocationFirst && console.term_supports(USER_ANSI)) + console.gotoxy(1, 3); + var filenameLen = gListIdxes.filenameEnd - gListIdxes.filenameStart; + var fileSizeLen = gListIdxes.fileSizeEnd - gListIdxes.fileSizeStart -1; + var shortDescLen = gListIdxes.descriptionEnd - gListIdxes.descriptionStart + 1; + var formatStr = "\1n\1w\1h%-" + filenameLen + "s %" + fileSizeLen + "s %-" + + +(shortDescLen-7) + "s\1n\1w%5s\1n"; + var listHdrEndText = THIN_RECTANGLE_RIGHT + BLOCK4 + BLOCK3 + BLOCK2 + BLOCK1; + printf(formatStr, "Filename", "Size", "Description", listHdrEndText); +} + +// Creates the menu for displaying the file list +// +// Parameters: +// pQuitKeys: A string containing hotkeys to use as the menu's quit keys +// +// Return value: The DDLightbarMenu object for the file list in the file directory +function createFileListMenu(pQuitKeys) +{ + //DDLightbarMenu(pX, pY, pWidth, pHeight) + // Create the menu object. Place it below the header lines (which should have been written + // before this), and also leave 1 row at the bottom for the prompt line + var startRow = gNumHeaderLinesDisplayed > 0 ? gNumHeaderLinesDisplayed + 1 : 1; + var fileListMenu = new DDLightbarMenu(1, startRow, console.screen_columns - 1, console.screen_rows - (startRow-1) - 1); + fileListMenu.scrollbarEnabled = true; + fileListMenu.borderEnabled = false; + fileListMenu.multiSelect = true; + fileListMenu.ampersandHotkeysInItems = false; + fileListMenu.wrapNavigation = false; + + // Add additional keypresses for quitting the menu's input loop so we can + // respond to these keys. + if (typeof(pQuitKeys) === "string") + fileListMenu.AddAdditionalQuitKeys(pQuitKeys); + + fileListMenu.SetColors({ + itemColor: [{start: gListIdxes.filenameStart, end: gListIdxes.filenameEnd, attrs: gColors.filename}, + {start: gListIdxes.fileSizeStart, end: gListIdxes.fileSizeEnd, attrs: gColors.fileSize}, + {start: gListIdxes.descriptionStart, end: gListIdxes.descriptionEnd, attrs: gColors.desc}], + selectedItemColor: [{start: gListIdxes.filenameStart, end: gListIdxes.filenameEnd, attrs: gColors.bkgHighlight + gColors.filenameHighlight}, + {start: gListIdxes.fileSizeStart, end: gListIdxes.fileSizeEnd, attrs: gColors.bkgHighlight + gColors.fileSizeHighlight}, + {start: gListIdxes.descriptionStart, end: gListIdxes.descriptionEnd, attrs: gColors.bkgHighlight + gColors.descHighlight}] + }); + + fileListMenu.filenameLen = gListIdxes.filenameEnd - gListIdxes.filenameStart; + fileListMenu.fileSizeLen = gListIdxes.fileSizeEnd - gListIdxes.fileSizeStart -1; + fileListMenu.shortDescLen = gListIdxes.descriptionEnd - gListIdxes.descriptionStart + 1; + fileListMenu.fileFormatStr = "%-" + fileListMenu.filenameLen + + "s %" + fileListMenu.fileSizeLen + + "s %-" + fileListMenu.shortDescLen + "s"; + + // Define the menu functions for getting the number of items and getting an item + fileListMenu.NumItems = function() { + // could also return gFilebase.files + return gFileList.length; + }; + fileListMenu.GetItem = function(pIdx) { + var menuItemObj = this.MakeItemWithRetval(pIdx); + var filename = shortenFilename(gFileList[pIdx].name, this.filenameLen, true); + // Note: The file size is in bytes + var fileSize = gFilebase.get_size(gFileList[pIdx].name); + var desc = (typeof(gFileList[pIdx].desc) === "string" ? gFileList[pIdx].desc : ""); + menuItemObj.text = format(this.fileFormatStr, + filename,//gFileList[pIdx].name.substr(0, this.filenameLen), + getFileSizeStr(fileSize, this.fileSizeLen), + desc.substr(0, this.shortDescLen)); + return menuItemObj; + } + + fileListMenu.selectedItemIndexes = {}; + fileListMenu.numSelectedItemIndexes = function() { + return Object.keys(this.selectedItemIndexes).length; + }; + return fileListMenu; +} + +// Creates the menu for choosing a file library (for moving a message to another message area). +// The return value of the chosen item is the file library index. +// +// Return value: The DDLightbarMenu object for choosing a file library +function createFileLibMenu() +{ + // This probably shouldn't happen, but check to make sure there are file libraries + if (file_area.lib_list.length == 0) + { + console.crlf(); + console.print("\1n\1y\1hThere are no file libraries available\1n"); + console.crlf(); + console.pause(); + return; + } + + //DDLightbarMenu(pX, pY, pWidth, pHeight) + // Create the menu object + var startRow = gNumHeaderLinesDisplayed + 4; + var fileLibMenu = new DDLightbarMenu(5, startRow, console.screen_columns - 10, console.screen_rows - startRow - 5); + fileLibMenu.scrollbarEnabled = true; + fileLibMenu.borderEnabled = true; + fileLibMenu.multiSelect = false; + fileLibMenu.ampersandHotkeysInItems = false; + fileLibMenu.wrapNavigation = false; + + // Add additional keypresses for quitting the menu's input loop. + // Q: Quit + var additionalQuitKeys = "qQ"; + fileLibMenu.AddAdditionalQuitKeys(additionalQuitKeys); + + // Construct a format string for the file libraries + var largestNumDirs = getLargestNumDirsWithinFileLibs(); + fileLibMenu.libNumLen = file_area.lib_list.length.toString().length; + fileLibMenu.numDirsLen = largestNumDirs.toString().length; + var menuInnerWidth = fileLibMenu.size.width - 2; // Menu width excluding borders + // Allow 2 for spaces + fileLibMenu.libDescLen = menuInnerWidth - fileLibMenu.libNumLen - fileLibMenu.numDirsLen - 2; + fileLibMenu.libFormatStr = "%" + fileLibMenu.libNumLen + "d %-" + fileLibMenu.libDescLen + "s %" + fileLibMenu.numDirsLen + "d"; + + // Colors and their indexes + fileLibMenu.borderColor = gColors.fileAreaMenuBorder; + var libNumStart = 0; + var libNumEnd = fileLibMenu.libNumLen; + var descStart = libNumEnd; + var descEnd = descStart + fileLibMenu.libDescLen; + var numDirsStart = descEnd; + //var numDirsEnd = numDirsStart + fileLibMenu.numDirsLen; + fileLibMenu.SetColors({ + itemColor: [{start: libNumStart, end: libNumEnd, attrs: "\1n" + gColors.fileNormalBkg + gColors.fileAreaNum}, + {start: descStart, end:descEnd, attrs: "\1n" + gColors.fileNormalBkg + gColors.fileAreaDesc}, + {start: numDirsStart, end: -1, attrs: "\1n" + gColors.fileNormalBkg + gColors.fileAreaNumItems}], + selectedItemColor: [{start: libNumStart, end: libNumEnd, attrs: "\1n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaNumHighlight}, + {start: descStart, end:descEnd, attrs: "\1n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaDescHighlight}, + {start: numDirsStart, end: -1, attrs: "\1n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaNumItemsHighlight}] + }); + + fileLibMenu.topBorderText = "\1y\1hFile Libraries"; + // Define the menu functions for getting the number of items and getting an item + fileLibMenu.NumItems = function() { + return file_area.lib_list.length; + }; + fileLibMenu.GetItem = function(pIdx) { + var menuItemObj = this.MakeItemWithRetval(pIdx); + menuItemObj.text = format(this.libFormatStr, + pIdx + 1,//file_area.lib_list[pIdx].number + 1, + file_area.lib_list[pIdx].description.substr(0, this.libDescLen), + file_area.lib_list[pIdx].dir_list.length); + return menuItemObj; + } + + return fileLibMenu; +} +// Helper for createFileLibMenu(): Returns the largest number of directories within the file libraries +function getLargestNumDirsWithinFileLibs() +{ + var largestNumDirs = 0; + for (var libIdx = 0; libIdx < file_area.lib_list.length; ++libIdx) + { + if (file_area.lib_list[libIdx].dir_list.length > largestNumDirs) + largestNumDirs = file_area.lib_list[libIdx].dir_list.length; + } + return largestNumDirs; +} + +// Creates the menu for choosing a file directory within a file library (for moving +// a message to another message area). +// The return value of the chosen item is the internal code for the file directory. +// +// Parameters: +// pLibIdx: The file directory index +// +// Return value: The DDLightbarMenu object for choosing a file directory within the +// file library at the given index +function createFileDirMenu(pLibIdx) +{ + if (typeof(pLibIdx) !== "number") + return null; + + var startRow = gNumHeaderLinesDisplayed + 4; + // Make sure there are directories in this library + if (file_area.lib_list[pLibIdx].dir_list.length == 0) + { + // TODO: Better error display + console.gotoxy(5, startRow); + console.print("\1n\1y\1hThere are no directories in this file library \1n"); + console.crlf(); + console.pause(); + return null; + } + + //DDLightbarMenu(pX, pY, pWidth, pHeight) + // Create the menu object + var fileDirMenu = new DDLightbarMenu(5, startRow, console.screen_columns - 10, console.screen_rows - startRow - 5); + fileDirMenu.scrollbarEnabled = true; + fileDirMenu.borderEnabled = true; + fileDirMenu.multiSelect = false; + fileDirMenu.ampersandHotkeysInItems = false; + fileDirMenu.wrapNavigation = false; + + // Add additional keypresses for quitting the menu's input loop. + // Q: Quit + var additionalQuitKeys = "qQ"; + fileDirMenu.AddAdditionalQuitKeys(additionalQuitKeys); + + fileDirMenu.libIdx = pLibIdx; + // Construct a format string for the file libraries + var largestNumFiles = getLargestNumFilesInLibDirs(pLibIdx); + fileDirMenu.dirNumLen = file_area.lib_list[pLibIdx].dir_list.length.toString().length; + fileDirMenu.numFilesLen = largestNumFiles.toString().length; + var menuInnerWidth = fileDirMenu.size.width - 2; // Menu width excluding borders + // Allow 2 for spaces + fileDirMenu.dirDescLen = menuInnerWidth - fileDirMenu.dirNumLen - fileDirMenu.numFilesLen - 2; + fileDirMenu.dirFormatStr = "%" + fileDirMenu.dirNumLen + "d %-" + fileDirMenu.dirDescLen + "s %" + fileDirMenu.numFilesLen + "d"; + + // Colors and their indexes + fileDirMenu.borderColor = gColors.fileAreaMenuBorder; + var dirNumStart = 0; + var dirNumEnd = fileDirMenu.dirNumLen; + var descStart = dirNumEnd; + var descEnd = descStart + fileDirMenu.dirDescLen; + var numDirsStart = descEnd; + //var numDirsEnd = numDirsStart + fileDirMenu.numDirsLen; + fileDirMenu.SetColors({ + itemColor: [{start: dirNumStart, end: dirNumEnd, attrs: "\1n" + gColors.fileNormalBkg + gColors.fileAreaNum}, + {start: descStart, end:descEnd, attrs: "\1n" + gColors.fileNormalBkg + gColors.fileAreaDesc}, + {start: numDirsStart, end: -1, attrs: "\1n" + gColors.fileNormalBkg + gColors.fileAreaNumItems}], + selectedItemColor: [{start: dirNumStart, end: dirNumEnd, attrs: "\1n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaNumHighlight}, + {start: descStart, end:descEnd, attrs: "\1n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaDescHighlight}, + {start: numDirsStart, end: -1, attrs: "\1n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaNumItemsHighlight}] + }); + + fileDirMenu.topBorderText = "\1y\1h" + ("File directories of " + file_area.lib_list[pLibIdx].description).substr(0, fileDirMenu.size.width-2); + // Define the menu functions for getting the number of items and getting an item + fileDirMenu.NumItems = function() { + return file_area.lib_list[this.libIdx].dir_list.length; + }; + fileDirMenu.GetItem = function(pIdx) { + // Return the internal code for the directory for the item + var menuItemObj = this.MakeItemWithRetval(file_area.lib_list[this.libIdx].dir_list[pIdx].code); + menuItemObj.text = format(this.dirFormatStr, + pIdx + 1,//file_area.lib_list[this.libIdx].dir_list[pIdx].number + 1, + file_area.lib_list[this.libIdx].dir_list[pIdx].description.substr(0, this.dirDescLen), + getNumFilesInDir(this.libIdx, pIdx)); + return menuItemObj; + } + + return fileDirMenu; +} +// Returns the number of files in a file directory +// +// Parameters: +// pLibIdx: The library index +// pDirIdx: The directory index within the file library +// +// Return value: The number of files in the file directory +function getNumFilesInDir(pLibIdx, pDirIdx) +{ + if (typeof(pLibIdx) !== "number" || typeof(pDirIdx) !== "number") + return 0; + if (pLibIdx < 0 || pLibIdx >= file_area.lib_list.length) + return 0; + if (pDirIdx < 0 || pDirIdx >= file_area.lib_list[pLibIdx].dir_list.length) + return 0; + + var numFiles = 0; + var filebase = new FileBase(file_area.lib_list[pLibIdx].dir_list[pDirIdx].code); + if (filebase.open()) + { + numFiles = filebase.files; + filebase.close(); + } + return numFiles; +} +// Returns the largest number of files in all directories in a file library +// +// Parameters: +// pLibIdx: The index of a file library +// +// Return value: The largest number of files of all the directories in the file library +function getLargestNumFilesInLibDirs(pLibIdx) +{ + var largestNumFiles = 0; + for (var dirIdx = 0; dirIdx < file_area.lib_list[pLibIdx].dir_list.length; ++dirIdx) + { + var numFilesInDir = getNumFilesInDir(pLibIdx, dirIdx); + if (numFilesInDir > largestNumFiles) + largestNumFiles = numFilesInDir; + } + return largestNumFiles; +} + +// Returns a formatted string representation of a file size. Tries +// to put a size designation at the end if possible. +// +// Parameters: +// pFileSize: The size of the file in bytes +// pMaxLen: Optional - The maximum length of the string +// +// Return value: A formatted string representation of the file size +function getFileSizeStr(pFileSize, pMaxLen) +{ + var fileSizeStr = "?"; + if (typeof(pFileSize) !== "number" || pFileSize < 0) + return fileSizeStr; + + // TODO: Improve + if (pFileSize >= BYTES_PER_GB) // Gigabytes + { + fileSizeStr = format("%.02fG", +(pFileSize / BYTES_PER_GB)); + if (typeof(pMaxLen) === "number" && pMaxLen > 0 && fileSizeStr.length > pMaxLen) + { + fileSizeStr = format("%.1fG", +(pFileSize / BYTES_PER_GB)); + if (fileSizeStr.length > pMaxLen) + { + // If there's a decimal point, then put the size designation after it + var dotIdx = fileSizeStr.lastIndexOf("."); + if (dotIdx > 0) + { + if (/\.$/.test(fileSizeStr)) + fileSizeStr = fileSizeStr.substr(0, dotIdx) + "G"; + else + fileSizeStr = fileSizeStr.substr(0, fileSizeStr.length-1) + "G"; + } + } + fileSizeStr = fileSizeStr.substr(0, pMaxLen); + } + } + else if (pFileSize >= BYTES_PER_MB) // Megabytes + { + fileSizeStr = format("%.02fM", +(pFileSize / BYTES_PER_MB)); + if (typeof(pMaxLen) === "number" && pMaxLen > 0 && fileSizeStr.length > pMaxLen) + { + fileSizeStr = format("%.1fM", +(pFileSize / BYTES_PER_MB)); + if (fileSizeStr.length > pMaxLen) + { + // If there's a decimal point, then put the size designation after it + var dotIdx = fileSizeStr.lastIndexOf("."); + if (dotIdx > 0) + { + if (/\.$/.test(fileSizeStr)) + fileSizeStr = fileSizeStr.substr(0, dotIdx) + "M"; + else + fileSizeStr = fileSizeStr.substr(0, fileSizeStr.length-1) + "M"; + } + } + fileSizeStr = fileSizeStr.substr(0, pMaxLen); + } + } + else if (pFileSize >= BYTES_PER_KB) // Kilobytes + { + fileSizeStr = format("%.02fK", +(pFileSize / BYTES_PER_KB)); + if (typeof(pMaxLen) === "number" && pMaxLen > 0 && fileSizeStr.length > pMaxLen) + { + fileSizeStr = format("%.1fK", +(pFileSize / BYTES_PER_KB)); + if (fileSizeStr.length > pMaxLen) + { + // If there's a decimal point, then put the size designation after it + var dotIdx = fileSizeStr.lastIndexOf("."); + if (dotIdx > 0) + { + if (/\.$/.test(fileSizeStr)) + fileSizeStr = fileSizeStr.substr(0, dotIdx) + "K"; + else + fileSizeStr = fileSizeStr.substr(0, fileSizeStr.length-1) + "K"; + } + } + fileSizeStr = fileSizeStr.substr(0, pMaxLen); + } + } + else + { + fileSizeStr = pFileSize.toString(); + if (typeof(pMaxLen) === "number" && pMaxLen > 0 && fileSizeStr.length > pMaxLen) + fileSizeStr = fileSizeStr.substr(0, pMaxLen); + } + + return fileSizeStr; +} + +// Displays some text with a solid horizontal line on the next line. +// +// Parameters: +// pText: The text to display +// pCenter: Whether or not to center the text. Optional; defaults +// to false. +// pTextColor: The color to use for the text. Optional; by default, +// normal white will be used. +// pLineColor: The color to use for the line underneath the text. +// Optional; by default, bright black will be used. +function displayTextWithLineBelow(pText, pCenter, pTextColor, pLineColor) +{ + var centerText = (typeof(pCenter) == "boolean" ? pCenter : false); + var textColor = (typeof(pTextColor) == "string" ? pTextColor : "\1n\1w"); + var lineColor = (typeof(pLineColor) == "string" ? pLineColor : "\1n\1k\1h"); + + // Output the text and a solid line on the next line. + if (centerText) + { + console.center(textColor + pText); + var solidLine = ""; + var textLength = console.strlen(pText); + for (var i = 0; i < textLength; ++i) + solidLine += HORIZONTAL_SINGLE; + console.center(lineColor + solidLine); + } + else + { + console.print(textColor + pText); + console.crlf(); + console.print(lineColor); + var textLength = console.strlen(pText); + for (var i = 0; i < textLength; ++i) + console.print(HORIZONTAL_SINGLE); + console.crlf(); + } +} + +// Returns a string for a number with commas added every 3 places +// +// Parameters: +// pNum: The number to format +// +// Return value: A string with the number formatted with commas every 3 places +function numWithCommas(pNum) +{ + var numStr = ""; + if (typeof(pNum) === "number") + numStr = pNum.toString(); + else if (typeof(pNum) === "string") + numStr = pNum; + else + return ""; + + // Check for a decimal point in the number + var afterDotSuffix = ""; + var dotIdx = numStr.lastIndexOf("."); + if (dotIdx > -1) + { + afterDotSuffix = numStr.substr(dotIdx+1); + numStr = numStr.substr(0, dotIdx); + } + // First, build an array containing sections of the number containing + // 3 digits each (the last may contain less than 3) + var numParts = []; + var i = numStr.length - 1; + var continueOn = true; + while (continueOn/*i >= 0*/) + { + if (i >= 3) + { + numParts.push(numStr.substr(i-2, 3)); + i -= 3; + } + else + { + numParts.push(numStr.substr(0, i+1)); + i -= i; + continueOn = false; + } + } + // Reverse the array so the sections of digits are in forward order + numParts.reverse(); + // Re-build the number string with commas + numStr = ""; + for (var i = 0; i < numParts.length; ++i) + numStr += numParts[i] + ","; + if (/,$/.test(numStr)) + numStr = numStr.substr(0, numStr.length-1); + // Append back the value after the decimal place if there was one + if (afterDotSuffix.length > 0) + numStr += "." + afterDotSuffix; + return numStr; +} + +// Displays a set of messages at the status location on the screen, along with a border. Then +// refreshes the area of the screen to erase the message box. +// +// Parameters: +// pMsgArray: An array containing the messages to display +// pIsError: Boolean - Whether or not the messages are an error +// pWaitAndErase: Optional boolean - Whether or not to automatically wait a moment and then +// erase the message box after drawing. Defaults to true. +function displayMsgs(pMsgArray, pIsError, pWaitAndErase) +{ + if (typeof(pMsgArray) !== "object" || pMsgArray.length == 0) + return; + + var waitAndErase = (typeof(pWaitAndErase) === "boolean" ? pWaitAndErase : true); + + // Draw the box border, then write the messages + var title = pIsError ? "Error" : "Message"; + var titleColor = pIsError ? gColors.errorMessage : gColors.successMessage; + drawBorder(gErrorMsgBoxULX, gErrorMsgBoxULY, gErrorMsgBoxWidth, gErrorMsgBoxHeight, + gColors.errorBoxBorder, "single", title, titleColor, ""); + var msgColor = "\1n" + (pIsError ? gColors.errorMessage : gColors.successMessage); + var innerWidth = gErrorMsgBoxWidth - 2; + var msgFormatStr = msgColor + "%-" + innerWidth + "s\1n"; + for (var i = 0; i < pMsgArray.length; ++i) + { + console.gotoxy(gErrorMsgBoxULX+1, gErrorMsgBoxULY+1); + printf(msgFormatStr, pMsgArray[i].substr(0, innerWidth)); + if (waitAndErase) + { + // Wait for the error wait duration + mswait(gErrorMsgWaitMS); + } + } + if (waitAndErase) + eraseMsgBoxScreenArea(); +} +function displayMsg(pMsg, pIsError, pWaitAndErase) +{ + if (typeof(pMsg) !== "string") + return; + displayMsgs([ pMsg ], pIsError, pWaitAndErase); +} +// Erases the message box screen area by re-drawing the necessary components +function eraseMsgBoxScreenArea() +{ + // Refresh the list header line and have the file list menu refresh itself over + // the error message window + displayListHdrLine(true); + gFileListMenu.DrawPartialAbs(gErrorMsgBoxULX, gErrorMsgBoxULY+1, gErrorMsgBoxWidth, gErrorMsgBoxHeight-2); +} + +// Draws a border +// +// Parameters: +// pX: The X location of the upper left corner +// pY: The Y location of the upper left corner +// pWidth: The width of the box +// pHeight: The height of the box +// pColor: A string containing color/attribute codes for the border characters +// pLineStyle: A string specifying the border character style, either "single" or "double" +// pTitle: Optional - A string specifying title text for the top border +// pTitleColor: Optional - Attribute codes for the color to use for the title text +// pBottomBorderText: Optional - A string specifying text to include in the bottom border +function drawBorder(pX, pY, pWidth, pHeight, pColor, pLineStyle, pTitle, pTitleColor, pBottomBorderText) +{ + if (typeof(pX) !== "number" || typeof(pY) !== "number" || typeof(pWidth) !== "number" || typeof(pHeight) !== "number") + return; + if (typeof(pColor) !== "string") + return; + + var borderChars = { + UL: UPPER_LEFT_SINGLE, + UR: UPPER_RIGHT_SINGLE, + LL: LOWER_LEFT_SINGLE, + LR: LOWER_RIGHT_SINGLE, + preText: RIGHT_T_SINGLE, + postText: LEFT_T_SINGLE, + horiz: HORIZONTAL_SINGLE, + vert: VERTICAL_SINGLE + }; + if (typeof(pLineStyle) === "string" && pLineStyle.toUpperCase() == "DOUBLE") + { + borderChars.UL = UPPER_LEFT_DOUBLE; + borderChars.UR = UPPER_RIGHT_DOUBLE; + borderChars.LL = LOWER_LEFT_DOUBLE; + borderChars.LR = LOWER_RIGHT_DOUBLE; + borderChars.preText = RIGHT_T_DOUBLE; + borderChars.postText = LEFT_T_DOUBLE + borderChars.horiz = HORIZONTAL_DOUBLE; + borderChars.vert = VERTICAL_DOUBLE; + } + + // Top border + console.gotoxy(pX, pY); + console.print("\1n" + pColor); + console.print(borderChars.UL); + var innerWidth = pWidth - 2; + // Include the title text in the top border, if there is any specified + var titleTextWithoutAttrs = strip_ctrl(pTitle); // Possibly used twice, so only call strip_ctrl() once + if (typeof(pTitle) === "string" && titleTextWithoutAttrs.length > 0) + { + var titleLen = strip_ctrl(pTitle).length; + if (titleLen > pWidth - 4) + titleLen = pWidth - 4; + innerWidth -= titleLen; + innerWidth -= 2; // ?? Correctional + // Note: substrWithAttrCodes() is defined in dd_lightbar_menu.js + var titleText = pTitle; + if (typeof(pTitleColor) === "string") + titleText = "\1n" + pTitleColor + titleTextWithoutAttrs; + console.print(borderChars.preText + "\1n" + substrWithAttrCodes(titleText, 0, titleLen) + + "\1n" + pColor + borderChars.postText); + if (innerWidth > 0) + console.print(pColor); + } + for (var i = 0; i < innerWidth; ++i) + console.print(borderChars.horiz); + console.print(borderChars.UR); + // Side borders + var rightCol = pX + pWidth - 1; + var endScreenRow = pY + pHeight - 1; + for (var screenRow = pY + 1; screenRow < endScreenRow; ++screenRow) + { + console.gotoxy(pX, screenRow); + console.print(borderChars.vert); + console.gotoxy(rightCol, screenRow); + console.print(borderChars.vert); + } + // Bottom border + console.gotoxy(pX, endScreenRow); + console.print(borderChars.LL); + innerWidth = pWidth - 2; + // Include the bottom border text in the top border, if there is any specified + if (typeof(pBottomBorderText) === "string" && pBottomBorderText.length > 0) + { + var textLen = strip_ctrl(pBottomBorderText).length; + if (textLen > pWidth - 4) + textLen = pWidth - 4; + innerWidth -= textLen; + innerWidth -= 2; // ?? Correctional + // Note: substrWithAttrCodes() is defined in dd_lightbar_menu.js + console.print(borderChars.preText + "\1n" + substrWithAttrCodes(pBottomBorderText, 0, textLen) + + "\1n" + pColor + borderChars.postText); + if (innerWidth > 0) + console.print(pColor); + } + for (var i = 0; i < innerWidth; ++i) + console.print(borderChars.horiz); + console.print(borderChars.LR); +} + +// Draws a horizontal separator line on the screen, in high green +// +// Parameters: +// pX: The X coordinate to start at +// pY: The Y coordinate to start at +// pWidth: The width of the line to draw +function drawSeparatorLine(pX, pY, pWidth) +{ + if (typeof(pX) !== "number" || typeof(pY) !== "number" || typeof(pWidth) !== "number") + return; + if (pX < 1 || pX > console.screen_columns) + return; + if (pY < 1 || pY > console.screen_rows) + return; + if (pWidth < 1) + return; + + var width = pWidth; + var maxWidth = console.screen_columns - pX + 1; + if (width > maxWidth) + width = maxWidth; + + console.gotoxy(pX, pY); + console.print("\1n\1g\1h"); + for (var i = 0; i < width; ++i) + console.print(HORIZONTAL_SINGLE); + console.print("\1n"); +} + +// Confirms with the user to perform an action with a file or set of files +// +// Parameters: +// pFilenames: An array of filenames (as strings), or a string containing a filename +// pActionName: String - The name of the action to confirm +// pDefaultYes: Boolean - True if the default is to be yes, or false if no +// +// Return value: Boolean - True if the user confirmed, or false if not +function confirmFileActionWithUser(pFilenames, pActionName, pDefaultYes) +{ + if (typeof(pFilenames) !== "object" && typeof(pFilenames) !== "string") + return false; + if (typeof(pActionName) !== "string") + return false; + + var actionConfirmed = false; + + var numFilenames = 1; + if (typeof(pFilenames) === "object") + numFilenames = pFilenames.length; + if (numFilenames < 1) + return false; + // If there is only 1 filename, then prompt the user near the bottom of the screen + else if (numFilenames == 1) + { + var filename = (typeof(pFilenames) === "string" ? pFilenames : pFilenames[0]); + drawSeparatorLine(1, console.screen_rows-2, console.screen_columns-1); + console.gotoxy(1, console.screen_rows-1); + console.cleartoeol("\1n"); + console.gotoxy(1, console.screen_rows-1); + var shortFilename = shortenFilename(filename, console.screen_columns-28, false); + if (pDefaultYes) + actionConfirmed = console.yesno(pActionName + " " + shortFilename); + else + actionConfirmed = !console.noyes(pActionName + " " + shortFilename); + gFileListMenu.DrawPartialAbs(1, console.screen_rows-2, console.screen_columns, 2, {}); + } + else + { + // Construct & draw a frame with the file list & display the frame to confirm with the + // user to delete the files + var frameUpperLeftX = gFileListMenu.pos.x + 2; + var frameUpperLeftY = gFileListMenu.pos.y + 2; + var frameWidth = gFileListMenu.size.width - 4; + var frameHeight = 10; + var frameTitle = pActionName + " files? (Y/N)"; + var additionalQuitKeys = "yYnN"; + var frameInnerWidth = frameWidth - 2; // Without borders; for filename lengths + var fileListStr = "\1n\1w"; + for (var i = 0; i < pFilenames.length; ++i) + fileListStr += shortenFilename(pFilenames[i], frameInnerWidth, false) + "\r\n"; + var lastUserInput = displayBorderedFrameAndDoInputLoop(frameUpperLeftX, frameUpperLeftY, frameWidth, + frameHeight, gColors.confirmFileActionWindowBorder, + frameTitle, gColors.confirmFileActionWindowWindowTitle, + fileListStr, additionalQuitKeys); + actionConfirmed = (lastUserInput.toUpperCase() == "Y"); + gFileListMenu.DrawPartialAbs(frameUpperLeftX, frameUpperLeftY, frameWidth, frameHeight, {}); + } + + return actionConfirmed; +} + + +// Reads the configuration file and sets the settings accordingly +function readConfigFile() +{ + this.cfgFileSuccessfullyRead = false; + + var themeFilename = ""; // In case a theme filename is specified + + // Determine the script's startup directory. + // This code is a trick that was created by Deuce, suggested by Rob Swindell + // as a way to detect which directory the script was executed in. I've + // shortened the code a little. + var startupPath = '.'; + try { throw dig.dist(dist); } catch(e) { startupPath = e.fileName; } + startupPath = backslash(startupPath.replace(/[\/\\][^\/\\]*$/,'')); + + // Open the main configuration file. First look for it in the sbbs/mods + // directory, then sbbs/ctrl, then in the same directory as this script. + var cfgFilename = "ddfilelister.cfg"; + var cfgFilenameFullPath = file_cfgname(system.mods_dir, cfgFilename); + if (!file_exists(cfgFilenameFullPath)) + cfgFilenameFullPath = file_cfgname(system.ctrl_dir, cfgFilename); + if (!file_exists(cfgFilenameFullPath)) + cfgFilenameFullPath = file_cfgname(startupPath, cfgFilename); + var cfgFile = new File(cfgFilenameFullPath); + if (cfgFile.open("r")) + { + this.cfgFileSuccessfullyRead = true; + + var fileLine = null; // A line read from the file + var equalsPos = 0; // Position of a = in the line + var commentPos = 0; // Position of the start of a comment + var setting = null; // A setting name (string) + var settingUpper = null; // Upper-case setting name + var value = null; // To store a value for a setting (string) + var valueUpper = null; // Upper-cased value for a setting (string) + while (!cfgFile.eof) + { + // Read the next line from the config file. + fileLine = cfgFile.readln(2048); + + // fileLine should be a string, but I've seen some cases + // where it isn't, so check its type. + if (typeof(fileLine) != "string") + continue; + + // If the line starts with with a semicolon (the comment + // character) or is blank, then skip it. + if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0)) + continue; + + // If the line has a semicolon anywhere in it, then remove + // everything from the semicolon onward. + commentPos = fileLine.indexOf(";"); + if (commentPos > -1) + fileLine = fileLine.substr(0, commentPos); + + // Look for an equals sign, and if found, separate the line + // into the setting name (before the =) and the value (after the + // equals sign). + equalsPos = fileLine.indexOf("="); + if (equalsPos > 0) + { + // Read the setting & value, and trim leading & trailing spaces. + setting = trimSpaces(fileLine.substr(0, equalsPos), true, false, true); + settingUpper = setting.toUpperCase(); + value = trimSpaces(fileLine.substr(equalsPos+1), true, false, true); + valueUpper = value.toUpperCase(); + + // Set the appropriate valueUpper in the settings object. + if (settingUpper == "SORTORDER") + { + // FileBase.SORT properties + // Name Type Description + // NATURAL number Natural sort order (same as DATE_A) + // NAME_AI number Filename ascending, case insensitive sort order + // NAME_DI number Filename descending, case insensitive sort order + // NAME_AS number Filename ascending, case sensitive sort order + // NAME_DS number Filename descending, case sensitive sort order + // DATE_A number Import date/time ascending sort order + // DATE_D number Import date/time descending sort order + if (valueUpper == "NATURAL") + gFileSortOrder = FileBase.SORT.NATURAL; + else if (valueUpper == "NAME_AI") + gFileSortOrder = FileBase.SORT.NAME_AI; + else if (valueUpper == "NAME_DI") + gFileSortOrder = FileBase.SORT.NAME_DI; + else if (valueUpper == "NAME_AS") + gFileSortOrder = FileBase.SORT.NAME_AS; + else if (valueUpper == "NAME_DS") + gFileSortOrder = FileBase.SORT.NAME_DS; + else if (valueUpper == "DATE_A") + gFileSortOrder = FileBase.SORT.DATE_A; + else if (valueUpper == "DATE_D") + gFileSortOrder = FileBase.SORT.DATE_D; + else // Default + gFileSortOrder = FileBase.SORT.NATURAL; + } + else if (settingUpper == "THEMEFILENAME") + { + // First look for the theme config file in the sbbs/mods + // directory, then sbbs/ctrl, then the same directory as + // this script. + themeFilename = system.mods_dir + value; + if (!file_exists(themeFilename)) + themeFilename = system.ctrl_dir + value; + if (!file_exists(themeFilename)) + themeFilename = startupPath + value; + } + } + } + + cfgFile.close(); + } + else + { + // Was unable to read the configuration file. Output a warning to the user + // that defaults will be used and to notify the sysop. + console.print("\1n"); + console.crlf(); + console.print("\1w\1hUnable to open the configuration file: \1y" + cfgFilename); + console.crlf(); + console.print("\1wDefault settings will be used. Please notify the sysop."); + mswait(2000); + } + + // If a theme filename was specified, then read the colors & strings + // from it. + if (themeFilename.length > 0) + { + var themeFile = new File(themeFilename); + if (themeFile.open("r")) + { + var fileLine = null; // A line read from the file + var equalsPos = 0; // Position of a = in the line + var commentPos = 0; // Position of the start of a comment + var setting = null; // A setting name (string) + var value = null; // To store a value for a setting (string) + while (!themeFile.eof) + { + // Read the next line from the config file. + fileLine = themeFile.readln(2048); + + // fileLine should be a string, but I've seen some cases + // where it isn't, so check its type. + if (typeof(fileLine) != "string") + continue; + + // If the line starts with with a semicolon (the comment + // character) or is blank, then skip it. + if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0)) + continue; + + // If the line has a semicolon anywhere in it, then remove + // everything from the semicolon onward. + commentPos = fileLine.indexOf(";"); + if (commentPos > -1) + fileLine = fileLine.substr(0, commentPos); + + // Look for an equals sign, and if found, separate the line + // into the setting name (before the =) and the value (after the + // equals sign). + equalsPos = fileLine.indexOf("="); + if (equalsPos > 0) + { + // Read the setting (without leading/trailing spaces) & value + setting = trimSpaces(fileLine.substr(0, equalsPos), true, false, true); + value = fileLine.substr(equalsPos+1); + + if (gColors.hasOwnProperty(setting)) + { + // Trim leading & trailing spaces from the value when + // setting a color. Also, replace any instances of "\1" + // with the Synchronet attribute control character. + gColors[setting] = trimSpaces(value, true, false, true).replace(/\\1/g, "\1"); + } + } + } + + themeFile.close(); + } + else + { + // Was unable to read the theme file. Output a warning to the user + // that defaults will be used and to notify the sysop. + this.cfgFileSuccessfullyRead = false; + console.print("\1n"); + console.crlf(); + console.print("\1w\1hUnable to open the theme file: \1y" + themeFilename); + console.crlf(); + console.print("\1wDefault colors will be used. Please notify the sysop."); + mswait(2000); + } + } +} + +// Removes multiple, leading, and/or trailing spaces +// The search & replace regular expressions used in this +// function came from the following URL: +// http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces +// +// Parameters: +// pString: The string to trim +// pLeading: Whether or not to trim leading spaces (optional, defaults to true) +// pMultiple: Whether or not to trim multiple spaces (optional, defaults to true) +// pTrailing: Whether or not to trim trailing spaces (optional, defaults to true) +// +// Return value: The string with whitespace trimmed +function trimSpaces(pString, pLeading, pMultiple, pTrailing) +{ + if (typeof(pString) !== "string") + return ""; + + var leading = (typeof(pLeading) === "boolean" ? pLeading : true); + var multiple = (typeof(pMultiple) === "boolean" ? pMultiple : true); + var trailing = (typeof(pTrailing) === "boolean" ? pTrailing : true); + + // To remove both leading & trailing spaces: + //pString = pString.replace(/(^\s*)|(\s*$)/gi,""); + + var string = pString; + if (leading) + string = string.replace(/(^\s*)/gi,""); + if (multiple) + string = string.replace(/[ ]{2,}/gi," "); + if (trailing) + string = string.replace(/(\s*$)/gi,""); + + return string; +} + +// Given a filename, this returns a filename shortened to a maximum length, preserving the +// filename extension. +// +// Parameters: +// pFilename: The filename to shorten +// pMaxLen: The maximum length of the shortened filename +// pFillWidth: Boolean - Whether or not to fill the width specified by pMaxLen +// +// Return value: A string with the shortened filename +function shortenFilename(pFilename, pMaxLen, pFillWidth) +{ + if (typeof(pFilename) !== "string") + return ""; + if (typeof(pMaxLen) !== "number" || pMaxLen < 1) + return ""; + + var filenameExt = file_getext(pFilename); + var filenameWithoutExt = file_getname(pFilename); + var extIdx = filenameWithoutExt.indexOf(filenameExt); + if (extIdx >= 0) + filenameWithoutExt = filenameWithoutExt.substr(0, extIdx); + + var maxWithoutExtLen = pMaxLen - filenameExt.length; + if (filenameWithoutExt.length > maxWithoutExtLen) + filenameWithoutExt = filenameWithoutExt.substr(0, maxWithoutExtLen); + + var fillWidth = (typeof(pFillWidth) === "boolean" ? pFillWidth : false); + var adjustedFilename = ""; + if (fillWidth) + adjustedFilename = format("%-" + maxWithoutExtLen + "s%s", filenameWithoutExt, filenameExt); + else + adjustedFilename = filenameWithoutExt + filenameExt; + + return adjustedFilename; +} diff --git a/xtrn/ddfilelister/defaultTheme.cfg b/xtrn/ddfilelister/defaultTheme.cfg new file mode 100644 index 0000000000..257282e71d --- /dev/null +++ b/xtrn/ddfilelister/defaultTheme.cfg @@ -0,0 +1,57 @@ +; Default color theme + +; Filename in the file list +filename=\1n\1b\1h +; File size in the file list +fileSize=\1n\1m\1h +; Description in the file list +desc=\1n\1w +; Background color for the highlighted (selected) file menu itme +bkgHighlight=\1n4 +; Highlight filename color for the file list +filenameHighlight=\1c\1h +; Highlight file size color for the file list +fileSizeHighlight=\1c\1h +; Highlight description color for the file list +descHighlight=\1c\1h +; File timestamp color for showing an extended file description +fileTimestamp=\1g\1h +; For the extended file information box border +fileInfoWindowBorder=\1r +; For the title of the extended file information box +fileInfoWindowTitle=\1g +; Error box border +errorBoxBorder=\1g\1h +; Error message color +errorMessage=\1y\1h +; Success message color +successMessage=\1c +; Batch download confirm/info window border color +batchDLInfoWindowBorder=\1r +; Batch download info window title color +batchDLInfoWindowTitle=\1g +; Multi-file action confirm window border color +confirmFileActionWindowBorder=\1r +; Multi-file action confirm window title color +confirmFileActionWindowWindowTitle=\1g + +; Colors related to moving a file +; The color of the file area menu border (for moving a file) +fileAreaMenuBorder=\1b +; The file area entry background color for 'normal' colors (for moving a file) +fileNormalBkg=\1n4 +; The file library/directory number for 'normal' colors (for moving a file) +fileAreaNum=\1w +; The file library/directory description for 'normal' colors (for moving a file) +fileAreaDesc=\1w +; The number of directories/files for 'normal' colors (for moving a file) +fileAreaNumItems=\1w + +; The file area entry background color for 'highlight' colors (for moving a file) +fileAreaMenuHighlightBkg=\1n7 +; The file library/directory number for 'highlight' colors (for moving a file) +fileAreaNumHighlight=\1b +; The file library/directory description for 'highlight' colors (for moving a file) +fileAreaDescHighlight=\1b +; The number of directories/files for 'highlight' colors (for moving a file) +fileAreaNumItemsHighlight=\1b \ No newline at end of file diff --git a/xtrn/ddfilelister/readme.txt b/xtrn/ddfilelister/readme.txt new file mode 100644 index 0000000000..ed433c7ee6 --- /dev/null +++ b/xtrn/ddfilelister/readme.txt @@ -0,0 +1,247 @@ + Digital Distortion File Lister + Version 2.00 + Release date: 2022-02-06 + + by + + Eric Oulashin + Sysop of Digital Distortion + BBS internet address: digitaldistortionbbs.com + Alternate address: digdist.bbsindex.com + Email: eric.oulashin@gmail.com + + + +This file describes the Digital Distortion File Lister. + +Contents +======== +1. Disclaimer +2. Introduction +3. Installation & Setup + - Command shell setup + - Background: Running JavaScript scripts in Synchronet +4. Configuration file & color/text theme configuration file + + +1. Disclaimer +============= +I cannot guarantee that this script is 100% free of bugs. However, I have +tested it, and I used it often during development, so I have some confidence +that there are no serious issues with it (at least, none that I have seen). + + +2. Introduction +=============== +This release is version 2.00 because I had previously released a message lister +mod for Synchronet which was just a list header and a command bar to display +under the list, and it still used Synchronet's stock file list. Now that +Synchronet provides a JavaScript interface to its filebases (as of version +3.19), more customization is possible with JavaScript. + +Digital Distortion File Lister is a script for Synchronet that provides an +enhanced user interface (for ANSI terminals) for listing files in the user's +current file directory. This file lister uses a lightbar interface to list the +files, a 'command bar' at the bottom of the screen allowing the user to use the +left & right arrow keys or the first character of the action to select an +action. The file lister also uses message boxes to display information. + +If the user's terminal does not support ANSI, the file lister will run the +stock Synchronet file lister interface instead. + +When adding files to the user's batch download queue or (for the sysop) +selecting files to move or delete, multi-select mode can be used, allowing +the user to select multiple files using the spacebar. If the spacebar is not +used, the file at the current highlighted lightbar location will be used. + +For viewing file information or viewining file contents, only the current file +where the lightbar location will be used. + +For the lightbar file list, basic descriptions and short filenames are used in +order for the information for each file to fit on a single line in the list. +The user can view extended information for each file, in which case a window +will be displayed with the filename, size, timestamp, and extended description. +The file lister also provides the ability to view files (according to +Synchronet's viewable files configuration), and adding files to the user's +batch download queue. Additionally, sysops can delete files and move files to +another file directory. + + +3. Installation & Setup +======================= +Aside from readme.txt revision_history.txt, Digital Distortion File Lister is +comprised of the following files: + +1. ddfilelister.js The Digital Distortion File Lister script + +2. ddfilelister.cfg The file lister configuration file + +3. defaultTheme.cfg The default theme file containing colors used in the + file lister + +The configuration files are plain text files, so they can be edited using any +editor. + +The .js script and .cfg files can be placed together in any directory. When +the lister reads the configuration file & theme file, the lister will first +look in your sbbs/mods directory, then sbbs/ctrl, then in the same directory +where the .js script is located. So, if you desire, you could place +ddfilelister.js in sbbs/exec and the .cfg file sin your sbbs/ctrl directory, +for example. + +Command shell setup +------------------- +Running the file lister involves simply having the BBS run a command to run +ddfilelister.js. No command-line parameters are required. The command to run +ddfilelister.js can be placed in a command shell for a command key, or in +SCFG > External Programs > Online Programs (Doors) in one of your program +groups, and then you could have your command shell run the lister via the +internal program code you configured in SCFG. + +Installing into a command shell is described in the subsection "Installing +into a command shell". + +Background: Running JavaScript scripts in Synchronet +---------------------------------------------------- +The command line to run this script would be as follows (if the script is in +your sbbs/xtrn/ddfilelister directory): +?../xtrn/ddfilelister/ddfilelister.js + +In a Baja script, you can use the 'exec' command to run a JavaScript script, as +in the following example: +exec "?../xtrn/ddfilelister/ddfilelister.js" + +In a JavaScript script, you can use the bbs.exec() function to run a JavaScript +script, as in the following example: +bbs.exec("?../xtrn/ddfilelister/ddfilelister.js"); + +To install the file lister as an external program (in SCFG in External +Programs > Online Programs (Doors)), see the following document for more +information: +http://wiki.synchro.net/howto:door:index?s[]=doors + + +4. Configuration file & color/text theme configuration file +=========================================================== +Digital Distortion File Lister allows changing some settings and colors via +configuration files. The configuration files are plain text and can be edited +with any text editor. These are the configuration files used by Digital +Distortion File Lister: +- ddfilelister.cfg: The main configuration file +- defaultTheme.cfg: The default theme configuration file which defines colors + for items displayed in the file lister. The name of this file can be + specified in ddfilelister.cfg, so that alternate "theme" configuration files + can be used if desired. + +Each setting in the configuration files has the format setting=value, where +"setting" is the name of the setting or color, and "value" is the corresponding +value to use. The colors specified in the theme configuration file are +Synchronet color/attribute codes. Comments are allowed in the configuration +files - Commented lines begin with a semicolon (;). + +Digital Distortion File Lister will look for the configuration files in the +following directories, in the following order: +1. sbbs/mods +2. sbbs/ctrl +3. The same directory as ddfilelister.js +If you customize your configuration files, you can copy them to your sbbs/mods +or sbbs/ctrl directory so that they'll be more difficutl to accidentally +override if you update your xtrn/DDMsgReader from the Synchronet CVS +repository, where this reader's files are checked in. + +The configuration settings are described in the sections below: + +Main configuration file (DDMsgReader.cfg) +----------------------------------------- +Setting Description +------- ----------- +sortOrder String: The file sort order to use. + Valid values are: + NATURAL: Natural sort order (same as DATE_A) + NAME_AI: Filename ascending, case insensitive sort order + NAME_DI: Filename descending, case insensitive sort order + NAME_AS: Filename ascending, case sensitive sort order + NAME_DS: Filename descending, case sensitive sort order + DATE_A: Import date/time ascending sort order + DATE_D: Import date/time descending sort order + +themeFilename The name of the configuration file to + use for colors & string settings + +Theme configuration file +------------------------ +The convention for the setting names in the theme configuration file is that +setting names ending in 'Text' are for whole text strings, and the setting +names that don't end in 'Text' are for colors. + +Setting Element in the file lister +------- -------------------------- +filename Filename in the file list + +fileSize File size in the file list + +desc Description in the file list + +bkgHighlight Background color for the highlighted + (selected) file menu itme + +filenameHighlight Highlight filename color for the file list + +fileSizeHighlight Highlight file size color for the file + list + +descHighlight Highlight description color for the file + list + +fileTimestamp File timestamp color for showing an + extended file description + +fileInfoWindowBorder For the extended file information box + border + +fileInfoWindowTitle For the title of the extended file + information box + +errorBoxBorder Error box border + +errorMessage Error message color + +successMessage Success message color + +batchDLInfoWindowBorder Batch download confirm/info window border + color + +batchDLInfoWindowTitle Batch download info window title color + +confirmFileActionWindowBorder Multi-file action confirm window border + color + +confirmFileActionWindowWindowTitle Multi-file action confirm window title + color + +fileAreaMenuBorder The color of the file area menu border (for + moving a file) + +fileNormalBkg The file area entry background color for + 'normal' colors (for moving a file) + +fileAreaNum The file library/directory number for + 'normal' colors (for moving a file) + +fileAreaDesc The file library/directory description for + 'normal' colors (for moving a file) + +fileAreaNumItems The number of directories/files for + 'normal' colors (for moving a file) + +fileAreaMenuHighlightBkg The file area entry background color for + 'highlight' colors (for moving a file) + +fileAreaNumHighlight The file library/directory number for + 'highlight' colors (for moving a file) + +fileAreaDescHighlight The file library/directory description for + 'highlight' colors (for moving a file) + +fileAreaNumItemsHighlight The number of directories/files for + 'highlight' colors (for moving a file) \ No newline at end of file diff --git a/xtrn/ddfilelister/revision_history.txt b/xtrn/ddfilelister/revision_history.txt new file mode 100644 index 0000000000..ab09abb2f7 --- /dev/null +++ b/xtrn/ddfilelister/revision_history.txt @@ -0,0 +1,8 @@ +This file lists all of the changes made for each release of the Digital +Distortion File Lister. + +Revision History (change log) +============================= +Version Date Description +------- ---- ----------- +2.00 2022-02-06 Initial version. \ No newline at end of file -- GitLab