diff --git a/xtrn/ddfilelister.js b/xtrn/ddfilelister.js new file mode 100644 index 0000000000000000000000000000000000000000..7451fba24d5c83328374bf14c0a61d9b6aa70aa6 --- /dev/null +++ b/xtrn/ddfilelister.js @@ -0,0 +1,4212 @@ +/* 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). + * 2022-02-07 Eric Oulashin Version 2.01 + * Fixed file description being undefined when viewing + * file info. Fixed command bar refreshing when pressing + * the hotkeys. Added an option to pause after viewing a + * file (defaults to true). + * 2022-02-13 Eric Oulashin Version 2.02 + * Things overall look good. Releasing this version. Added + * the ability to do searching via filespec, description, and + * new file search (started working on this 2022-02-08). + * 2022-02-27 Eric Oulashin Version 2.03 + * For terminals over 25 rows tall, the file info window will + * now be up to 45 rows tall. Also, fixed the display of the + * trailing blocks for the list header for wide terminals (over + * 80 columns). + * 2022-03-09 Eric Oulashin Version 2.04 + * Bug fix: Now successfully formats filenames without extensions + * when listing files. + * 2022-03-12 Eric Oulashin Version 2.05 + * Now makes use of the user's extended file description setting: + * If the user's extended file description setting is enabled, + * the lister will now show extended file descriptions on the + * main screen in a split format, with the lightbar file list + * on the left and the extended file description for the + * highlighted file on the right. Also, made the file info + * window taller for terminals within 25 lines high. + * I had started work on this on March 9, 2022. + * 2022-03-13 Eric Oulashin Version 2.05a + * Fix for "fileDesc is not defined" error when displaying + * the file description on the main screen. Also made a + * small refactor to the main screen refresh function. + * 2022-04-13 Eric Oulashin Version 2.06 + * When extended file descriptions are enabled, the file + * date is now shown with the file description on the last + * line. + * 2022-12-02 Eric Oulashin Version 2.07 + * In a file's extended description, added the number of times + * downloaded and date/time last downloaded. Also, fixed a bug + * where some descriptions were blank in the Frame object because + * of a leading normal attribute (the fix may be a kludge though). + * 2023-01-18 Eric Oulashin Version 2.08 + * When doing a file search in multiple directories, the file + * library & directory is now shown in the header as the user + * scrolls through the file list/search results. Also, + * used lfexpand() to ensure the extended description has + * CRLF endings, useful for splitting it into multiple lines properly. + * 2023-02-25 Eric Oulashin Version 2.09 + * Now supports being used as a loadable module for + * Scan Dirs and List Files + * 2023-02-27 Eric Oulashin Version 2.10 + * Now allows downloading a single selected file with the D key. + * Also, ddfilelister now checks whether the user has permission (based + * on ARS) to download before allowing adding files to their batch + * download queue (and downloading a single file as well). +*/ + +"use strict"; + +if (typeof(require) === "function") +{ + require("sbbsdefs.js", "K_UPPER"); + require('key_defs.js', 'KEY_UP'); + 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"); + require("attr_conv.js", "convertAttrsToSyncPerSysCfg"); +} +else +{ + load("sbbsdefs.js"); + load('key_defs.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"); + load("attr_conv.js"); +} + + +/* +Configured in SCFG->System->Loadable Modules: +Scan Dirs: User scans one or more directories for (e.g. new) files +List Files: User lists files within a file directory +View File Info: User views detailed information on files in a directory + +This addresses/fixes feature request #521 for Nightfox + +Will need to document the mode argument bit values on the wiki, but +it's the usual suspects: FL_* for scandirs and listfiles and FI_* for +fileinfo. The scandirs_mod will be passed an extra bool (0/1) arg that +indicates whether or not the user is scanning *all* directories. +*/ + + +// This script requires Synchronet version 3.19 or newer. +// If the Synchronet version is below the minimum, then exit. +if (system.version_num < 31900) +{ + if (user.is_sysop) + { + var message = "\x01n\x01h\x01y\x01i* Warning:\x01n\x01h\x01w Digital Distortion File Lister " + + "requires version \x01g3.19\x01w or\r\n" + + "newer of Synchronet. This BBS is using version \x01g" + system.version + + "\x01w.\x01n"; + console.crlf(); + console.print(message); + console.crlf(); + console.pause(); + } + exit(); +} + +// Lister version information +var LISTER_VERSION = "2.10"; +var LISTER_DATE = "2023-02-27"; + + +/////////////////////////////////////////////////////////////////////////////// +// Global variables + +var KEY_BACKSPACE = CTRL_H; + +// 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; +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: "\x01n\x01b\x01h", + fileSize: "\x01n\x01m\x01h", + desc: "\x01n\x01w", + bkgHighlight: "\x01n\x01" + "4", + filenameHighlight: "\x01c\x01h", + fileSizeHighlight: "\x01c\x01h", + descHighlight: "\x01c\x01h", + fileTimestamp: "\x01g\x01h", + fileInfoWindowBorder: "\x01r", + fileInfoWindowTitle: "\x01g", + errorBoxBorder: "\x01g\x01h", + errorMessage: "\x01y\x01h", + successMessage: "\x01c", + + batchDLInfoWindowBorder: "\x01r", + batchDLInfoWindowTitle: "\x01g", + confirmFileActionWindowBorder: "\x01r", + confirmFileActionWindowWindowTitle: "\x01g", + + fileAreaMenuBorder: "\x01b", + fileNormalBkg: "\x01" + "4", + fileAreaNum: "\x01w", + fileAreaDesc: "\x01w", + fileAreaNumItems: "\x01w", + + fileAreaMenuHighlightBkg: "\x01" + "7", + fileAreaNumHighlight: "\x01b", + fileAreaDescHighlight: "\x01b", + fileAreaNumItemsHighlight: "\x01b" +}; + + +// Actions +var FILE_VIEW_INFO = 1; +var FILE_VIEW = 2; +var FILE_ADD_TO_BATCH_DL = 3; +var FILE_DOWNLOAD_SINGLE = 4; +var HELP = 5; +var QUIT = 6; +var FILE_MOVE = 7; // Sysop action +var FILE_DELETE = 8; // Sysop action + +// Search/list modes +var MODE_LIST_DIR = 1; +var MODE_SEARCH_FILENAME = 2; +var MODE_SEARCH_DESCRIPTION = 3; +var MODE_NEW_FILE_SEARCH = 4; + +// Sort orders (not included in FileBase.SORT) +var SORT_FL_ULTIME = 50; // Sort by upload time +var SORT_FL_DLTIME = 51; // Sort by download time + +// The searc/list mode for the current run +var gScriptMode = MODE_LIST_DIR; // Default +var gListBehavior = FL_NONE; // From sbbsdefs.js + +// The directory internal code to list +var gDirCode = bbs.curdir_code; + + + +// 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; + +// Whether or not to pause after viewing a file +var gPauseAfterViewingFile = true; + +/////////////////////////////////////////////////////////////////////////////// +// Script execution code + +// The filename pattern to match +var gFilespec = "*"; + +// The sort order to use for the file list +var gFileSortOrder = FileBase.SORT.NATURAL; // Natural sort order, same as DATE_A (import date ascending) + +var gSearchVerbose = false; + +// When called as a lodable module, one of the options is to scan all dirs +var gScanAllDirs = false; + +// Read the configuration file and set the settings +readConfigFile(); + +// Parse command-line arguments (which sets program options) +parseArgs(argv); + +// If the user's terminal doesn't support ANSI, then just call the standard Synchronet +// file list function and exit now +if (!console.term_supports(USER_ANSI)) +{ + var exitCode = 0; + if (gScriptMode == MODE_SEARCH_FILENAME || gScriptMode == MODE_SEARCH_DESCRIPTION || gScriptMode == MODE_NEW_FILE_SEARCH) + bbs.scan_dirs(gListBehavior, gScanAllDirs); + else + exitCode = bbs.list_files(gDirCode, gFilespec, gListBehavior); + exit(exitCode); +} + +// This array will contain file metadata objects +var gFileList = []; + +// Populate the file list based on the script mode (list/search). +// It's important that this is called before createFileListMenu(), +// since this adjusts gListIdxes.filenameEnd based on the longest +// filename length and terminal width. +var listPopRetObj = populateFileList(gScriptMode); +if (listPopRetObj.exitNow) + exit(listPopRetObj.exitCode); + +// If there are no files, then say so and exit. +if (gFileList.length == 0) +{ + console.crlf(); + console.print("\x01n\x01c"); + if (gScriptMode == MODE_LIST_DIR) + { + if (gFilespec == "*" || gFilespec == "*.*") + console.print("There are no files in the directory."); + else + console.print("No files in the directory were found matching " + gFilespec); + } + else + console.print("No files were found."); + console.print("\x01n"); + console.crlf(); + console.pause(); + exit(0); +} + + +// Clear the screen and display the header lines +console.clear("\x01n"); +if ((gListBehavior & FL_NO_HDR) != FL_NO_HDR) + displayFileLibAndDirHeader(); +// 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 + KEY_DEL); +// 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({}); +// If using extended descriptions, write the first file's description on the screen +if (extendedDescEnabled()) + displayFileExtDescOnMainScreen(0); +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 currentActionVal = 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) + { + currentActionVal = fileMenuBar.getCurrentSelectedAction(); + fileMenuBar.setCurrentActionCode(currentActionVal); + actionRetObj = doAction_ANSI(currentActionVal, gFileList, gFileListMenu); + } + // Allow the delete key as a special key for sysops to delete the selected file(s). Also allow backspace + // due to some terminals returning backspace for delete. + else if (lastUserInputUpper == KEY_DEL || lastUserInputUpper == KEY_BACKSPACE) + { + if (user.is_sysop) + { + fileMenuBar.setCurrentActionCode(FILE_DELETE, true); + actionRetObj = doAction_ANSI(FILE_DELETE, gFileList, gFileListMenu); + currentActionVal = FILE_DELETE; + } + } + else + { + currentActionVal = fileMenuBar.getActionFromChar(lastUserInputUpper, false); + fileMenuBar.setCurrentActionCode(currentActionVal, true); + actionRetObj = doAction_ANSI(currentActionVal, 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 ((gListBehavior & FL_NO_HDR) != FL_NO_HDR) + { + if (actionRetObj.reDrawHeaderTextOnly) + { + console.print("\x01n"); + displayFileLibAndDirHeader(true); // Will move the cursor where it needs to be + } + else if (actionRetObj.reDrawListerHeader) + { + console.print("\x01n"); + console.gotoxy(1, 1); + displayFileLibAndDirHeader(); + } + } + if (actionRetObj.reDrawCmdBar) // Could call fileMenuBar.constructPromptText(); if needed + fileMenuBar.writePromptLine(); + var redrewPartOfFileListMenu = false; + // If we are to re-draw the main screen content, then + // enable the flag to draw the file list menu on the next + // GetVal(); also, if extended descriptions are being shown, + // write the current file's extended description too. + if (actionRetObj.reDrawMainScreenContent) + { + drawFileListMenu = true; + if (extendedDescEnabled()) + displayFileExtDescOnMainScreen(gFileListMenu.selectedItemIdx); + } + else + { + // If there is partial redraw information available, then use it + // to re-draw that part of the main screen + if (actionRetObj.fileListPartialRedrawInfo != null) + { + drawFileListMenu = false; + var startX = actionRetObj.fileListPartialRedrawInfo.absStartX; + var startY = actionRetObj.fileListPartialRedrawInfo.absStartY; + var width = actionRetObj.fileListPartialRedrawInfo.width; + var height = actionRetObj.fileListPartialRedrawInfo.height; + refreshScreenMainContent(startX, startY, width, height, true); + actionRetObj.refreshedSelectedFilesAlready = true; + redrewPartOfFileListMenu = true; + } + else + { + // Partial screen re-draw information was not returned. + continueDoingFileList = actionRetObj.continueFileLister; + drawFileListMenu = actionRetObj.reDrawMainScreenContent; + // If displaying extended descriptions and the user deleted some files, then + // refresh the file description area to erase the delete confirmation text + if (extendedDescEnabled()/* && currentActionVal == FILE_DELETE*/) + { + if (actionRetObj.hasOwnProperty("filesDeleted") && actionRetObj.filesDeleted) + { + var numFiles = gFileListMenu.NumItems(); + if (numFiles > 0 && gFileListMenu.selectedItemIdx >= 0 && gFileListMenu.selectedItemIdx < numFiles) + displayFileExtDescOnMainScreen(gFileListMenu.selectedItemIdx); + } + else + { + var firstLine = startY + gFileListMenu.pos.y; + var lastLine = console.screen_rows - 1; + var width = console.screen_columns - gFileListMenu.size.width - 1; + displayFileExtDescOnMainScreen(gFileListMenu.selectedItemIdx, firstLine, lastLine, width); + } + } + } + } + // Remove checkmarks from any selected files in the file menu. + // For efficiency, we'd probably only do this if not re-drawing the wohle + // menu, but that's not working for now. + if (!actionRetObj.refreshedSelectedFilesAlready && /*!drawFileListMenu &&*/ gFileListMenu.numSelectedItemIndexes() > 0) + { + var bottomItemIdx = gFileListMenu.GetBottomItemIdx(); + var redrawTopY = -1; + var redrawBottomY = -1; + if (actionRetObj.fileListPartialRedrawInfo != null) + { + redrawTopY = actionRetObj.fileListPartialRedrawInfo.absStartY; + redrawBottomY = actionRetObj.fileListPartialRedrawInfo.height + height - 1; + } + for (var idx in gFileListMenu.selectedItemIndexes) + { + var idxNum = +idx; + if (idxNum >= gFileListMenu.topItemIdx && idxNum <= bottomItemIdx) + { + var drawItem = true; + if (redrawTopY > -1 && redrawBottomY > redrawTopY) + { + var screenRowForItem = gFileListMenu.ScreenRowForItem(idxNum); + drawItem = (screenRowForItem < redrawTopY || screenRowForItem > redrawBottomY) + } + if (drawItem) + { + var isSelected = (idxNum == gFileListMenu.selectedItemIdx); + gFileListMenu.WriteItemAtItsLocation(idxNum, isSelected, false); + } + else + console.print("\x01n\r\nNot drawing idx " + idxNum + "\r\n\x01p"); + } + } + } + // 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); + } + } +} + + + + +/////////////////////////////////////////////////////////////////////////////// +// Functions: File actions + +// Performs a specified file action based on an action code. For the ANSI user interface. +// +// Parameters: +// pActionCode: A code specifying an action to do. Must be one of the global +// action codes. +// 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_ANSI(pActionCode, pFileList, pFileListMenu) +{ + if (typeof(pActionCode) !== "number") + return getDefaultActionRetObj(); + + var fileMetadata = pFileList[pFileListMenu.selectedItemIdx]; + + var retObj = null; + switch (pActionCode) + { + case FILE_VIEW_INFO: + retObj = showFileInfo_ANSI(fileMetadata); + break; + case FILE_VIEW: + retObj = viewFile_ANSI(fileMetadata); + break; + case FILE_ADD_TO_BATCH_DL: + if (userCanDownloadFromFileArea_ShowErrorIfNot(fileMetadata.dirCode)) + retObj = addSelectedFilesToBatchDLQueue_ANSI(fileMetadata, pFileList); + else + { + retObj = getDefaultActionRetObj(); + retObj.reDrawListerHeader = true; + retObj.reDrawHeaderTextOnly = false; + retObj.reDrawMainScreenContent = true; + retObj.reDrawCmdBar = true; + } + break; + case FILE_DOWNLOAD_SINGLE: + if (userCanDownloadFromFileArea_ShowErrorIfNot(fileMetadata.dirCode) && pFileListMenu.selectedItemIdx >= 0 && pFileListMenu.selectedItemIdx < pFileListMenu.NumItems()) + retObj = letUserDownloadSelectedFile_ANSI(fileMetadata); + else + { + retObj = getDefaultActionRetObj(); + retObj = getDefaultActionRetObj(); + retObj.reDrawListerHeader = true; + retObj.reDrawHeaderTextOnly = false; + retObj.reDrawMainScreenContent = true; + retObj.reDrawCmdBar = true; + } + break; + case HELP: + retObj = displayHelpScreen(); + break; + case QUIT: + retObj = getDefaultActionRetObj(); + retObj.continueFileLister = false; + break; + case FILE_MOVE: // Sysop action + if (user.is_sysop) + retObj = chooseFilebaseAndMoveFileToOtherFilebase_ANSI(pFileList, pFileListMenu); + break; + case FILE_DELETE: // Sysop action + if (user.is_sysop) + retObj = confirmAndRemoveFilesFromFilebase_ANSI(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 +// reDrawMainScreenContent: Boolean - Whether or not to re-draw the main screen content +// (file list, and extended description area if applicable) +// reDrawListerHeader: Boolean - Whether or not to re-draw the header at the top of the screen +// reDrawHeaderTextOnly: Boolean - Whether or not to re-draw the header text only. This should +// take precedence over reDrawListerHeader. +// 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 +// refreshedSelectedFilesAlready: Whether or not selected file checkmark items +// have already been refreshed (boolean) +// 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, + reDrawMainScreenContent: false, + reDrawListerHeader: false, + reDrawHeaderTextOnly: false, + reDrawCmdBar: false, + fileListPartialRedrawInfo: null, + refreshedSelectedFilesAlready: false, + exitNow: false + }; +} + +// Shows extended information about a file to the user. +// +// Parameters: +// pFileMetadata: The file metadata object for the file to view information about +// +// Return value: An object with values to indicate status & screen refresh actions; see +// getDefaultActionRetObj() for details. +function showFileInfo_ANSI(pFileMetadata) +{ + 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; // TODO: Remove? + var frameWidth = console.screen_columns - 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. + // The metadata object in pFileList should have a dirCode added by this script. + var dirCode = gDirCode; + if (pFileMetadata.hasOwnProperty("dirCode")) + dirCode = pFileMetadata.dirCode; + var fileMetadata = null; + if (extendedDescEnabled()) + fileMetadata = pFileMetadata; + else + fileMetadata = getFileInfoFromFilebase(dirCode, pFileMetadata.name, FileBase.DETAIL.EXTENDED); + // Build a string with the file information + // Make sure the displayed filename isn't too crazy long + var frameInnerWidth = frameWidth - 2; // Without borders + var adjustedFilename = shortenFilename(fileMetadata.name, frameInnerWidth, false); + var fileInfoStr = "\x01n\x01wFilename"; + if (adjustedFilename.length < fileMetadata.name.length) + fileInfoStr += " (shortened)"; + fileInfoStr += ":\r\n"; + fileInfoStr += gColors.filename + adjustedFilename + "\x01n\x01w\r\n"; + // Note: File size can also be retrieved by calling a FileBase's get_size(fileMetadata.name) + // TODO: Shouldn't need the max length here + fileInfoStr += "Size: " + gColors.fileSize + getFileSizeStr(fileMetadata.size, 99999) + "\x01n\x01w\r\n"; + fileInfoStr += "Timestamp: " + gColors.fileTimestamp + strftime("%Y-%m-%d %H:%M:%S", fileMetadata.time) + "\x01n\x01w\r\n"; + fileInfoStr += "\r\n"; + + // File library/directory information + var libIdx = file_area.dir[dirCode].lib_index; + var dirIdx = file_area.dir[dirCode].index; + var libDesc = file_area.lib_list[libIdx].description; + var dirDesc = file_area.dir[dirCode].description; + fileInfoStr += "\x01c\x01hLib\x01g: \x01n\x01c" + libDesc.substr(0, frameInnerWidth-5) + "\x01n\x01w\r\n"; + fileInfoStr += "\x01c\x01hDir\x01g: \x01n\x01c" + dirDesc.substr(0, frameInnerWidth-5) + "\x01n\x01w\r\n"; + fileInfoStr += "\r\n"; + + // fileMetadata should have extdDesc, but check just in case + var fileDesc = ""; + if (fileMetadata.hasOwnProperty("extdesc") && fileMetadata.extdesc.length > 0) + fileDesc = fileMetadata.extdesc; + else + fileDesc = fileMetadata.desc; + // It's possible for fileDesc to be undefined (due to extDesc or desc being undefined), + // so make sure it's a string. + // Also, if it's a string, reformat certain types of strings that don't look good in a + // Frame object + if (typeof(fileDesc) === "string") + { + // Check to see if it starts with a normal attribute and remove if so, + // since that seems to cause problems with displaying the description in a Frame object. This + // may be a kludge, and perhaps there's a better solution.. + fileDesc = fileDesc.replace(/^\x01[nN]/, ""); + // Fix line endings if necessary + fileDesc = lfexpand(fileDesc); + } + else + fileDesc = ""; + // This might be overkill, but just in case, convert any non-Synchronet + // attribute codes to Synchronet attribute codes in the description. + if (!fileMetadata.hasOwnProperty("attrsConverted")) + { + fileDesc = convertAttrsToSyncPerSysCfg(fileDesc); + fileMetadata.attrsConverted = true; + if (fileMetadata.hasOwnProperty("extdesc")) + fileMetadata.extdesc = fileDesc; + else + fileMetadata.desc = fileDesc; + } + + fileInfoStr += gColors.desc; + if (fileDesc.length > 0) + fileInfoStr += "Description:\r\n" + fileDesc; // Don't want to use strip_ctrl(fileDesc) + else + fileInfoStr += "No description available"; + fileInfoStr += "\r\n"; + // # of times downloaded and last downloaded date/time + var fieldFormatStr = "\r\n\x01n\x01c\x01h%s\x01g:\x01n\x01c %s"; + var timesDownloaded = fileMetadata.hasOwnProperty("times_downloaded") ? fileMetadata.times_downloaded : 0; + fileInfoStr += format(fieldFormatStr, "Times downloaded", timesDownloaded); + if (fileMetadata.hasOwnProperty("last_downloaded")) + fileInfoStr += format(fieldFormatStr, "Last downloaded", strftime("%Y-%m-%d %H:%M", fileMetadata.last_downloaded)); + // Some more fields for the sysop + if (user.is_sysop) + { + var sysopFields = [ "from", "cost", "added"]; + for (var sI = 0; sI < sysopFields.length; ++sI) + { + var prop = sysopFields[sI]; + if (fileMetadata.hasOwnProperty(prop)) + { + if (typeof(fileMetadata[prop]) === "string" && fileMetadata[prop].length == 0) + continue; + var propName = prop.charAt(0).toUpperCase() + prop.substr(1); + var infoValue = ""; + if (prop == "added") + infoValue = strftime("%Y-%m-%d %H:%M:%S", fileMetadata.added); + else + infoValue = fileMetadata[prop].toString().substr(0, frameInnerWidth); + fileInfoStr += format(fieldFormatStr, propName, infoValue); + fileInfoStr += "\x01n\x01w"; + } + } + } + fileInfoStr += "\x01n\x01w"; + + // Construct & draw a frame with the file information & do the input loop + // for the frame until the user closes the frame. + var frameUpperLeftX = 3; + var frameUpperLeftY = gNumHeaderLinesDisplayed + 3; + // Note: frameWidth is declared earlier + var frameHeight = console.screen_rows - 4 - frameUpperLeftY; + // If the user's console is more than 25 rows high, then make the info window + // taller so that its bottom row is 10 from the bottom, but only up to 45 rows tall. + if (console.screen_rows > 25) + { + var frameBottomRow = console.screen_rows - 4; + frameHeight = frameBottomRow - frameUpperLeftY + 1; + if (frameHeight > 45) + frameHeight = 45; + } + 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: frameUpperLeftX - gFileListMenu.pos.x + 1, // Relative to the file menu + startY: frameUpperLeftY - gFileListMenu.pos.y + 1, // Relative to the file menu + absStartX: frameUpperLeftX, + absStartY: frameUpperLeftY, + width: frameWidth, + height: frameHeight + }; + + return retObj; +} +// Splits a string on a given string and then re-combines the string with \r\n (carriage return & newline) +// at the end of each line +// +// Parameters: +// pStr: The string to split & recombine +// pSplitStr: The string to split the first string on +// +// Return value: The string split on pSplitStr and re-combined with \r\n at the end of each line +function splitStrAndCombineWithRN(pStr, pSplitStr) +{ + if (typeof(pStr) !== "string") + return ""; + if (typeof(pSplitStr) !== "string") + return pStr; + + var newStr = ""; + var strs = pStr.split(pSplitStr); + if (strs.length > 0) + { + for (var i = 0; i < strs.length; ++i) + newStr += strs[i] + "\r\n"; + newStr = newStr.replace(/\r\n$/, ""); + } + else + newStr = pStr; + return newStr; +} + +// Lets the user view a file. +// +// Parameters: +// pFileMetadata: The file metadata object for the file to view +// +// Return value: An object with values to indicate status & screen refresh actions; see +// getDefaultActionRetObj() for details. +function viewFile_ANSI(pFileMetadata) +{ + var retObj = getDefaultActionRetObj(); + + // Open the filebase & get the fully pathed filename + var fullyPathedFilename = ""; + var filebase = new FileBase(pFileMetadata.dirCode); + if (filebase.open()) + { + fullyPathedFilename = filebase.get_path(pFileMetadata); + filebase.close(); + } + else + { + displayMsg("Failed to open the filebase!", true, true); + return retObj; + } + + // View the file + console.gotoxy(1, console.screen_rows); + console.print("\x01n"); + console.crlf(); + var successfullyViewed = bbs.view_file(fullyPathedFilename); + console.print("\x01n"); + if (gPauseAfterViewingFile || !successfullyViewed) + console.pause(); + + retObj.reDrawListerHeader = true; + retObj.reDrawMainScreenContent = true; + retObj.reDrawCmdBar = true; + return retObj; +} + +// Allows the user to add their selected file to their batch downloaded queue +// +// Parameters: +// pFileMetadata: The file metadata object for the file +// pFileList: The list of file metadata objects from the file directory +// +// Return value: An object with values to indicate status & screen refresh actions; see +// getDefaultActionRetObj() for details. +function addSelectedFilesToBatchDLQueue_ANSI(pFileMetadata, pFileList) +{ + var retObj = getDefaultActionRetObj(); + if (!userCanDownloadFromFileArea_ShowErrorIfNot(pFileMetadata.dirCode)) + return retObj; + + // 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(pFileMetadata.name); + metadataObjects.push(pFileMetadata); + } + // Note that confirmFileActionWithUser() will re-draw the parts of the file + // list menu that are necessary. + var addFilesConfirmed = confirmFileActionWithUser(filenames, "Batch DL add", false); + retObj.refreshedSelectedFilesAlready = true; + 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) + fileAlreadyInQueue = (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. Also, this script should add a + // dirCode property to each metadata object in the list. + addToQueueSuccessful = batchDLFile.iniSetValue(metadataObjects[i].name, "dir", metadataObjects[i].dirCode); + 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 = console.screen_columns - 4; // Used to be gFileListMenu.size.width - 4; + var frameInnerWidth = frameWidth - 2; // Without borders + var frameHeight = 8; + + // 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("\x01n"); + 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)?"; + // \x01cFiles: \x01h1 \x01n\x01c(\x01h100 \x01n\x01cMax) Credits: 0 Bytes: \x01h2,228,254 \x01n\x01c Time: 00:09:40 + // Note: The maximum number of allowed files in the batch download queue doesn't seem to + // be available to JavaScript. + var totalQueueSize = batchDLQueueStats.totalSize + pFileMetadata.size; + var totalQueueCost = batchDLQueueStats.totalCost + pFileMetadata.cost; + var queueStats = "\x01n\x01cFiles: \x01h" + batchDLQueueStats.numFilesInQueue + " \x01n\x01cCredits: \x01h" + + totalQueueCost + "\x01n\x01c Bytes: \x01h" + numWithCommas(totalQueueSize) + "\x01n\x01w\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); + // The main screen content (file list & extended description if applicable) + // will need to be redrawn after this. + retObj.reDrawMainScreenContent = true; + // If the user chose to download their file queue, then send it to the user. + // And the lister headers will need to be re-drawn as well. + if (lastUserInput.toUpperCase() == "Y") + { + retObj.reDrawListerHeader = true; + retObj.reDrawCmdBar = true; + console.print("\x01n"); + console.gotoxy(1, console.screen_rows); + console.crlf(); + bbs.batch_download(); + // If the user is still online (chose not to hang up after transfer), + // then pause so that the user can see the batch download status + if (bbs.online > 0) + console.pause(); + } + } + } + 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 = "\x01n\x01w"; + 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, ""); + // Add the file list redraw info. Note that the X and Y are relative + // to the file list menu, not absolute screen coordinates. + // To make the list refresh info to return to the main script loop + retObj.fileListPartialRedrawInfo = { + startX: 3, + startY: 3, + absStartX: gFileListMenu.pos.x + 3 - 1, // 1-based + absStartY: gFileListMenu.pos.y + 3 - 1, // 1-based + width: frameWidth + 1, + height: 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.attributes = "N"; + console.crlf(); + 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(); + } + } + } + } + batchDLFile.close(); + } + + return retObj; +} + +// Lets the user download the currently selected file on the file list menu +// +// Parameters: +// pFileMetadata: The file metadata object for the file to download +// +// Return value: An object with values to indicate status & screen refresh actions; see +// getDefaultActionRetObj() for details. +function letUserDownloadSelectedFile_ANSI(pFileMetadata) +{ + var retObj = getDefaultActionRetObj(); + console.attributes = "N"; + console.crlf(); + console.crlf(); + // If the user has the security level to download the file, let them do so + if (userCanDownloadFromFileArea_ShowErrorIfNot(pFileMetadata.dirCode)) + { + console.print("\x01cDownloading \x01h" + pFileMetadata.name + "\x01n"); + console.crlf(); + var selectedFilanmeFullPath = backslash(file_area.dir[pFileMetadata.dirCode].path) + pFileMetadata.name; + bbs.send_file(selectedFilanmeFullPath); + } + + retObj.reDrawListerHeader = true; + retObj.reDrawHeaderTextOnly = false; + retObj.reDrawMainScreenContent = true; + retObj.reDrawCmdBar = true; + + return retObj; +} + +// Displays the help screen. +function displayHelpScreen() +{ + var retObj = getDefaultActionRetObj(); + + console.clear("\x01n"); + // Display program information + displayTextWithLineBelow("Digital Distortion File Lister", true, "\x01n\x01c\x01h", "\x01k\x01h") + console.center("\x01n\x01cVersion \x01g" + LISTER_VERSION + " \x01w\x01h(\x01b" + LISTER_DATE + "\x01w)"); + console.crlf(); + + // If listing files in a directory, display information about the current file directory. + if (gScriptMode == MODE_LIST_DIR) + { + var libIdx = file_area.dir[gDirCode].lib_index; + var dirIdx = file_area.dir[gDirCode].index; + console.print("\x01n\x01cCurrent file library: \x01g" + file_area.lib_list[libIdx].description); + console.crlf(); + console.print("\x01cCurrent file directory: \x01g" + file_area.dir[gDirCode].description); + console.crlf(); + console.print("\x01cThere are \x01g" + file_area.dir[gDirCode].files + " \x01cfiles in this directory."); + } + else if (gScriptMode == MODE_SEARCH_FILENAME) + console.print("\x01n\x01cCurrently performing a filename search"); + else if (gScriptMode == MODE_SEARCH_DESCRIPTION) + console.print("\x01n\x01cCurrently performing a description search"); + else if (gScriptMode == MODE_NEW_FILE_SEARCH) + console.print("\x01n\x01cCurrently performing a new file search"); + 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 (user.is_sysop) + 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 = "\x01n\x01c\x01h%-" + commandStrWidth + "s\x01g: \x01n\x01c%s\r\n"; + printf(printfStr, "I", "Display extended file information"); + printf(printfStr, "V", "View the file"); + printf(printfStr, "B", "Flag the selected file(s) for batch download"); + printf(printfStr, "D", "Download the highlighted (selected) file"); + if (user.is_sysop) + { + printf(printfStr, "M", "Move the file(s) to another directory"); + printf(printfStr, "DEL", "Delete the file(s)"); + } + printf(printfStr, "?", "Show this help screen"); + printf(printfStr, "Q", "Quit back to the BBS"); + console.print("\x01n"); + console.crlf(); + //console.pause(); + + retObj.reDrawListerHeader = true; + retObj.reDrawMainScreenContent = true; + retObj.reDrawCmdBar = true; + return retObj; +} + +// Allows the user to move the selected file to another filebase. Only for sysops! +// +// Parameters: +// 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_ANSI(pFileList, pFileListMenu) +{ + var retObj = getDefaultActionRetObj(); + + var fileMetadata = pFileList[pFileListMenu.selectedItemIdx]; + + // Confirm with the user to move the file(s). If they don't want to, + // then just return now. + var filenames = []; + if (pFileListMenu.numSelectedItemIndexes() > 0) + { + for (var idx in pFileListMenu.selectedItemIndexes) + filenames.push(pFileList[+idx].name); + } + else + filenames.push(fileMetadata.name); + // Note that confirmFileActionWithUser() will re-draw the parts of the file + // list menu that are necessary. + var moveFilesConfirmed = confirmFileActionWithUser(filenames, "Move", false); + retObj.refreshedSelectedFilesAlready = true; + if (!moveFilesConfirmed) + return retObj; + + + // Create a file library menu for the user to choose a file library (and then directory) + var fileLibMenu = createFileLibMenu(); + // For screen refresh purposes, construct the file list redraw info. Note that the X and Y are relative + // to the file list menu, not absolute screen coordinates. + var topYForRefresh = fileLibMenu.pos.y - 1; // - 1 because of the label above the menu + var fileListPartialRedrawInfo = { + startX: fileLibMenu.pos.x - pFileListMenu.pos.x + 1, + startY: topYForRefresh - pFileListMenu.pos.y + 1, + absStartX: fileLibMenu.pos.x, + absStartY: topYForRefresh, + width: fileLibMenu.size.width + 1, + height: fileLibMenu.size.height + 1 // + 1 because of the label above the menu + }; + console.gotoxy(fileLibMenu.pos.x, fileLibMenu.pos.y-1); + printf("\x01n\x01c\x01h|\x01n\x01c%-" + +(fileLibMenu.size.width-1) + "s\x01n", "Choose a destination area"); + // Prompt the user which directory to move the file to + var chosenDirCode = null; + var continueOn = true; + while (continueOn) + { + var chosenLibIdx = fileLibMenu.GetVal(); + if (typeof(chosenLibIdx) === "number") + { + // The file dir menu will be created at the same position & with the same size + // as the file library menu + var fileDirMenu = createFileDirMenu(chosenLibIdx); + chosenDirCode = fileDirMenu.GetVal(); + if (typeof(chosenDirCode) === "string") + { + if (chosenDirCode != fileMetadata.dirCode) + 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) + { + // For logging + var libIdx = file_area.dir[chosenDirCode].lib_index; + var dirIdx = file_area.dir[chosenDirCode].index; + var libDesc = file_area.lib_list[libIdx].description; + var dirDesc = file_area.dir[chosenDirCode].description; + var destLibAndDirDesc = libDesc + " - " + dirDesc; + + // Build an array of file indexes and sort the array + var fileIndexes = []; + if (pFileListMenu.numSelectedItemIndexes() > 0) + { + for (var idx in pFileListMenu.selectedItemIndexes) + fileIndexes.push(+idx); + } + else + fileIndexes.push(+(pFileListMenu.selectedItemIdx)); + // Ensure the file indexes are sorted in numerical order + fileIndexes.sort(function(a, b) { return a - b}); + + // 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]; + // For logging + libIdx = file_area.dir[pFileList[fileIdx].dirCode].lib_index; + dirIdx = file_area.dir[pFileList[fileIdx].dirCode].index; + libDesc = file_area.lib_list[libIdx].description; + dirDesc = file_area.dir[pFileList[fileIdx].dirCode].description; + var srcLibAndDirDesc = libDesc + " - " + dirDesc; + + var moveRetObj = moveFileToOtherFilebase(pFileList[fileIdx], chosenDirCode); + var logMsg = ""; + var logLevel = LOG_INFO; + if (moveRetObj.moveSucceeded) + { + logMsg = "Digital Distotion File Lister: Successfully moved " + pFileList[fileIdx].name + + " from " + srcLibAndDirDesc + " to " + destLibAndDirDesc; + + // If we're listing files in the user's current directory, then remove + // the file info object from the file list array. Otherwise, update + // the metadata object in the list. + if (gScriptMode == MODE_LIST_DIR) + { + 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; + // Have the file list menu set up its description width, colors, and format + // string again in case it no longer needs to use its scrollbar + pFileListMenu.SetItemWidthsColorsAndFormatStr(); + retObj.reDrawMainScreenContent = true; + } + else + { + // Note: getFileInfoFromFilebase() will add dirCode to the metadata object + var fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM); + pFileList[fileIdx] = getFileInfoFromFilebase(chosenDirCode, pFileList[fileIdx].name, fileDetail); + } + } + else + { + moveAllSucceeded = false; + logLevel = LOG_ERR; + logMsg = "Digital Distotion File Lister: Failed to move " + pFileList[fileIdx].name + + " from " + srcLibAndDirDesc + " to " + destLibAndDirDesc; + } + log(logLevel, logMsg); + bbs.log_str(logMsg); + } + // Adjust the selected item index in the file list menu if necesary + if (pFileListMenu.NumItems() == 0) + pFileListMenu.selectedItemIdx = 0; + else if (pFileListMenu.selectedItemIdx >= pFileListMenu.NumItems() - 1) + pFileListMenu.selectedItemIdx = pFileListMenu.NumItems() - 1; + // If doing a search (not listing files in the user's current directory), then + // if all files were in the same directory, then we'll need to update the header + // lines at the top of the file list. If there's only one file in the list, + // the header lines will need to display the correct directory. Otherwise, + // set allSameDir to false so the header lines will now say "various". + // However, if not all files were in the same directory, check to see if they + // are now, and if so, we'll need to re-draw the header lines. + if (gScriptMode != MODE_LIST_DIR && typeof(pFileList.allSameDir) == "boolean") + { + if (pFileList.allSameDir) + { + if (pFileList.length > 1) + pFileList.allSameDir = false; + retObj.reDrawHeaderTextOnly = true; + } + else + { + pFileList.allSameDir = true; // Until we find it's not true + for (var fileListIdx = 1; fileListIdx < pFileList.length && pFileList.allSameDir; ++fileListIdx) + pFileList.allSameDir = (pFileList[fileListIdx].dirCode == pFileList[0].dirCode); + retObj.reDrawHeaderTextOnly = pFileList.allSameDir; + } + } + // Display a success/fail message + if (moveAllSucceeded) + { + var msg = "Successfully moved the file(s) to " + destLibAndDirDesc; + displayMsg(msg, false, true); + } + else + { + displayMsg("Failed to move the file(s)!", true, true); + } + // After moving the files, if there are no more files (in the directory or otherwise), + // say so and exit now. + if (gScriptMode == MODE_LIST_DIR && file_area.dir[gDirCode].files == 0) + { + displayMsg("There are no more files in the directory.", false); + retObj.exitNow = true; + } + else if (pFileList.length == 0) + { + displayMsg("There are no more files.", false); + retObj.exitNow = true; + } + } + + // If not exiting now, we'll want to re-draw part of the file list to erase the + // area chooser menu. + if (!retObj.exitNow) + retObj.fileListPartialRedrawInfo = fileListPartialRedrawInfo; + + return retObj; +} + +// Allows the user to remove the selected file(s) from the filebase. Only for sysops! +// +// Parameters: +// 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. For this function, the object +// returned will have the following additional properties: +// filesDeleted: Boolean - Whether or not files were actually deleted (after +// confirmation) +function confirmAndRemoveFilesFromFilebase_ANSI(pFileList, pFileListMenu) +{ + var retObj = getDefaultActionRetObj(); + retObj.filesDeleted = false; + + // 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 (pFileListMenu.numSelectedItemIndexes() > 0) + { + for (var idx in pFileListMenu.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); + retObj.refreshedSelectedFilesAlready = true; + if (removeFilesConfirmed) + { + retObj.filesDeleted = true; // Assume true even if some deletions may fail + + var fileIndexes = []; + if (pFileListMenu.numSelectedItemIndexes() > 0) + { + for (var idx in pFileListMenu.selectedItemIndexes) + fileIndexes.push(+idx); + } + else + fileIndexes.push(+(pFileListMenu.selectedItemIdx)); + // Ensure the file indexes are sorted in numerical order + fileIndexes.sort(function(a, b) { return a - b}); + + // Go through all the selected files and remove them. + // Note: Going through the list of indexes in reverse order so that + // removing each one from pFileList (gFileList) is simpler. + var removeAllSucceeded = true; + //for (var i = 0; i < fileIndexes.length; ++i) + for (var i = fileIndexes.length-1; i >= 0; --i) + { + var fileIdx = fileIndexes[i]; + if (typeof(pFileList[fileIdx]) === "undefined") + { + removeAllSucceeded = false; + continue; + } + // For logging + var libIdx = file_area.dir[pFileList[fileIdx].dirCode].lib_index; + var dirIdx = file_area.dir[pFileList[fileIdx].dirCode].index; + var libDesc = file_area.lib_list[libIdx].description; + var dirDesc = file_area.dir[pFileList[fileIdx].dirCode].description; + var libAndDirDesc = libDesc + " - " + dirDesc; + + // Open the filebase and remove the file + var removeFileSucceeded = false; + var numFilesRemaining = 0; + var filebase = new FileBase(pFileList[fileIdx].dirCode); + if (filebase.open()) + { + var filenameFullPath = filebase.get_path(pFileList[fileIdx].name); // For logging + try + { + removeFileSucceeded = filebase.remove(pFileList[fileIdx].name, true); + } + catch (error) + { + removeFileSucceeded = false; + // Make an entry in the BBS log that deleting the file failed + var logMsg = "ddfilelister: " + error; + log(LOG_ERR, logMsg); + bbs.log_str(logMsg); + } + // If the remove failed with deleting the file, then try without deleting the file + if (!removeFileSucceeded) + { + removeFileSucceeded = filebase.remove(pFileList[fileIdx].name, false); + if (removeFileSucceeded) + { + var logMsg = "ddfilelister: Removed " + filenameFullPath + " from the " + + "filebase but couldn't actually delete the file"; + log(LOG_INFO, logMsg); + bbs.log_str(logMsg); + } + } + if (removeFileSucceeded) + { + if (gScriptMode == MODE_LIST_DIR) + numFilesRemaining = filebase.files; + } + filebase.close(); + } + else + removeAllSucceeded = false; + + // Log a success/error message + var logMsg = ""; + var logLevel = LOG_INFO; + if (removeFileSucceeded) + { + logMsg = "Digital Distortion File Lister: Successfully removed " + pFileList[fileIdx].name + + " from " + libAndDirDesc + " (by " + user.alias + ")"; + // Remove the file info object from the file list array + pFileList.splice(fileIdx, 1); + // If we were going through the list in forward order, we'd have to + // subtract 1 from the remaining indexes: + /* + // 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 + { + removeAllSucceeded = false; + logMsg = "Digital Distortion File Lister: Failed to remove " + pFileList[fileIdx].name + + " from " + libAndDirDesc + " (by " + user.alias + ")"; + logLevel = LOG_ERR; + } + log(logLevel, logMsg); + bbs.log_str(logMsg); + } + // Display a success/failure message + if (removeAllSucceeded) + displayMsg("Successfully removed the file(s)", false, true); + else + displayMsg("Failed to remove 1 or more files", true, true); + // Adjust the selected item index in the file list menu if necesary + if (pFileListMenu.NumItems() == 0) + pFileListMenu.selectedItemIdx = 0; + else if (pFileListMenu.selectedItemIdx >= pFileListMenu.NumItems() - 1) + pFileListMenu.selectedItemIdx = pFileListMenu.NumItems() - 1; + // If the file list 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. + numFilesRemaining = pFileList.length; + if (numFilesRemaining > 0) + { + // Have the file list menu set up its description width, colors, and format + // string again in case it no longer needs to use its scrollbar + pFileListMenu.SetItemWidthsColorsAndFormatStr(); + retObj.reDrawMainScreenContent = true; + // If all files were not in the same directory, then check to see if all + // remaining files are now. If so, we'll need to update the header lines + // at the top of the file list. + if (typeof(pFileList.allSameDir) == "boolean") + { + if (!pFileList.allSameDir) + { + pFileList.allSameDir = true; // Until we find it's not true + for (var i = 1; i < pFileList.length && pFileList.allSameDir; ++i) + pFileList.allSameDir = (pFileList[i].dirCode == pFileList[0].dirCode); + //retObj.reDrawListerHeader = pFileList.allSameDir; + retObj.reDrawHeaderTextOnly = pFileList.allSameDir; + } + } + // Also, if the file list menu can now show all its items on one + // page (not needing the scrollbar), set its top item index to 0. + if (pFileListMenu.CanShowAllItemsInWindow()) + pFileListMenu.topItemIdx = 0; + } + else + { + if (gScriptMode == MODE_LIST_DIR) + displayMsg("The directory now has no files.", false, true); + else + displayMsg("There are no more files to show.", false, true); + retObj.exitNow = true; + } + } + + 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)); + this.cmdArray.push(new DDFileMenuBarItem("DL", 0, FILE_DOWNLOAD_SINGLE)); + if (user.is_sysop) + { + this.cmdArray.push(new DDFileMenuBarItem("Move", 0, FILE_MOVE)); + //this.cmdArray.push(new DDFileMenuBarItem("Del", 0, FILE_DELETE)); + this.cmdArray.push(new DDFileMenuBarItem("DEL", 0, FILE_DELETE, KEY_DEL)); + } + 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 = "\x01n\x01w" + 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 += "\x01w" + THIN_RECTANGLE_RIGHT; + for (var i = 0; i < numSolidBlocksPerSide; ++i) + this.promptText += BLOCK4; + this.promptText += BLOCK3 + BLOCK2 + BLOCK1 + "\x01n"; +} +// 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.currentCommandIdx) + 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.currentCommandIdx); + console.gotoxy(this.cmdArray[this.currentCommandIdx].pos, this.pos.y); + console.print("\x01n" + 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("\x01n" + 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; + + // Re-construct the bar text to make sure it's up to date with the selected action + this.constructPromptText(); +} +// 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 = "\x01n"; + if (selected) + itemText += "\x01" + "1\x01r\x01h" + firstChar + "\x01n\x01" + "1\x01k" + restOfText; + else + itemText += "\x01" + "6\x01c\x01h" + firstChar + "\x01n\x01" + "6\x01k" + restOfText; + itemText += "\x01n"; + if (withTrailingBlock) + itemText += "\x01w" + THIN_RECTANGLE_RIGHT + THIN_RECTANGLE_LEFT + "\x01n"; + return itemText; +} +// For the DDFileMenuBar class: Increments to the next menu item and refreshes the +// menu bar on the screen +function DDFileMenuBar_incrementMenuItemAndRefresh() +{ + var newCmdIdx = this.currentCommandIdx + 1; + if (newCmdIdx >= this.cmdArray.length) + newCmdIdx = 0; + // Will set this.currentCommandIdx + this.refreshWithNewAction(newCmdIdx); +} +// For the DDFileMenuBar class: Decrements to the previous menu item and refreshes the +// menu bar on the screen +function DDFileMenuBar_decrementMenuItemAndRefresh() +{ + var newCmdIdx = this.currentCommandIdx - 1; + if (newCmdIdx < 0) + newCmdIdx = this.cmdArray.length - 1; + // Will set this.currentCommandIdx + this.refreshWithNewAction(newCmdIdx); +} +// 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].hotkeyOverride != null && typeof(this.cmdArray[i].hotkeyOverride) !== "undefined") + { + if (pChar == this.cmdArray[i].hotkeyOverride) + retCode = this.cmdArray[i].retCode; + } + else 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].hotkeyOverride != null && typeof(this.cmdArray[i].hotkeyOverride) !== "undefined") + { + if (pChar == this.cmdArray[i].hotkeyOverride) + retCode = this.cmdArray[i].retCode; + } + else 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 +// pRefreshOnScreen: Optional boolean - Whether or not to refresh the command bar on the screen. +// Defaults to false. +function DDFileMenuBar_setCurrentActionCode(pActionCode, pRefreshOnScreen) +{ + if (typeof(pActionCode) !== "number") + return; + + var refreshOnScreen = (typeof(pRefreshOnScreen) === "boolean" ? pRefreshOnScreen : false); + + for (var i = 0; i < this.cmdArray.length; ++i) + { + if (this.cmdArray[i].retCode == pActionCode) + { + if (refreshOnScreen) + this.refreshWithNewAction(i); + else + { + 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 +// pHotkeyOverride: Optional: A key to use for the action instead of the first character in pItemText +function DDFileMenuBarItem(pItemText, pPos, pRetCode, pHotkeyOverride) +{ + this.itemText = pItemText; + this.pos = pPos; + this.retCode = pRetCode; + this.hotkeyOverride = null; + if (pHotkeyOverride != null && typeof(pHotkeyOverride) !== "undefined") + this.hotkeyOverride = pHotkeyOverride; +} + + +/////////////////////////////////////////////////////////////////////////////// +// Helper functions + +// Gets file metadata & its file time from a filebase. +// +// Parameters: +// pDirCode: The internal code of the directory the file is in +// pFilename: The name of the file (without the full path) +// pDetail: The detail level of the metadata object to get - See FileBase.DETAIL in JS docs +// +// Return value: An object containing the following properties: +// fileMetadataObj: An object with extended file metadata from the filebase. +// This will have a dirCode property added. +// fileTime: The timestamp of the file +function getFileInfoFromFilebase(pDirCode, pFilename, pDetail) +{ + if (typeof(pDirCode) !== "string" || pDirCode.length == 0 || typeof(pFilename) !== "string" || pFilename.length == 0) + return null; + + var fileMetadataObj = null; + var filebase = new FileBase(pDirCode); + if (filebase.open()) + { + // Just in case the file has the full path, get just the filename from it. + var filename = file_getname(pFilename); + var fileDetail = (typeof(pDetail) === "number" ? pDetail : FileBase.DETAIL.NORM); + if (typeof(pDetail) === "number") + fileDetail = pDetail; + else + fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM); + fileMetadataObj = filebase.get(filename, fileDetail); + fileMetadataObj.dirCode = pDirCode; + //fileMetadataObj.size = filebase.get_size(filename); + if (!fileMetadataObj.hasOwnProperty("time")) + fileMetadataObj.time = filebase.get_time(filename); + filebase.close(); + } + + return fileMetadataObj; +} + +// Moves a file from one filebase to another +// +// Parameters: +// 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(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 srcFilebase = new FileBase(pSrcFileMetadata.dirCode); + if (srcFilebase.open()) + { + var extdFileInfo = srcFilebase.get(pSrcFileMetadata, FileBase.DETAIL.MAX); + // Move the file over, remove it from the original filebase, and add it to the new filebase + var srcFilenameFull = srcFilebase.get_path(pSrcFileMetadata); + var destFilenameFull = file_area.dir[pDestDirCode].path + pSrcFileMetadata.name; + if (file_rename(srcFilenameFull, destFilenameFull)) + { + if (srcFilebase.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 = srcFilebase.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"; + + srcFilebase.close(); + } + else + { + var libIdx = file_area.dir[pSrcFileMetadata.dirCode].lib_index; + var dirIdx = file_area.dir[pSrcFileMetadata.dirCode].index; + var libDesc = file_area.lib_list[libIdx].description; + var dirDesc = file_area.dir[pSrcFileMetadata.dirCode].description; + retObj.failreason = "Failed to open filebase: " + libDesc + " - " + dirDesc; + } + + 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 = "\x01n\x01c\x01hQ\x01b/\x01cEnter\x01b/\x01cESC\x01y: \x01gClose\x01b"; + var scrollLoopNavHelp = "\x01c\x01hUp\x01b/\x01cDn\x01b/\x01cHome\x01b/\x01cEnd\x01b/\x01cPgup\x01b/\x01cPgDn\x01y: \x01gNav"; + if (console.screen_columns >= 80) + keyHelpStr += ", " + scrollLoopNavHelp; + var borderColor = (typeof(pBorderColor) === "string" ? pBorderColor : "\x01r"); + 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, "\x01n"); + 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: +// pTextOnly: Only draw the library & directory text (no decoration or other text). +// This is optional & defaults to false. +// pDirCodeOverride: Optional string: If this is valid, this will be used for the library & directory name +function displayFileLibAndDirHeader(pTextOnly, pDirCodeOverride) +{ + // If the behavior flags include no header, then just return immediately + if ((gListBehavior & FL_NO_HDR) == FL_NO_HDR) + return; + + var textOnly = (typeof(pTextOnly) === "boolean" ? pTextOnly : false); + + // Determine if this is the first time this function has been called. If so, + // we'll want to update gNumHeaderLinesDisplayed at the end. + var dispHdrFirstRun = false; + if (typeof(displayFileLibAndDirHeader.firstRun) == "undefined") + { + dispHdrFirstRun = true; + displayFileLibAndDirHeader.firstRun = true; + } + + // For the library & directory text to display, if we're just listing the user's + // current directory, use that. Otherwise, if all search results are in the same + // directory, then use a directory code from the file list. + var libIdx = 0; + var dirIdx = 0; + var libDesc = ""; + var dirDesc = ""; + var dirCode = ""; + if (gScriptMode == MODE_LIST_DIR) + dirCode = gDirCode; + else if (typeof(gFileList.allSameDir) === "boolean" && gFileList.allSameDir && gFileList.length > 0) + dirCode = gFileList[0].dirCode; + if (dirCode.length > 0) + { + libIdx = file_area.dir[dirCode].lib_index; + dirIdx = file_area.dir[dirCode].index; + libDesc = file_area.lib_list[libIdx].description; + dirDesc = file_area.dir[dirCode].description; + } + else if (typeof(pDirCodeOverride) === "string" && file_area.dir.hasOwnProperty(pDirCodeOverride)) + { + libIdx = file_area.dir[pDirCodeOverride].lib_index; + dirIdx = file_area.dir[pDirCodeOverride].index; + libDesc = file_area.lib_list[libIdx].description; + dirDesc = file_area.dir[pDirCodeOverride].description; + } + else + { + libIdx = -1; + dirIdx = -1; + libDesc = "Various"; + dirDesc = "Various"; + } + + var hdrTextWidth = console.screen_columns - 21; + var descWidth = hdrTextWidth - 11; + var libText = format("\x01cLib \x01w\x01h#\x01b%4d\x01c: \x01n\x01c%-" + descWidth + "s\x01n", +(libIdx+1), libDesc.substr(0, descWidth)); + var dirText = format("\x01cDir \x01w\x01h#\x01b%4d\x01c: \x01n\x01c%-" + descWidth + "s\x01n", +(dirIdx+1), dirDesc.substr(0, descWidth)); + + // Library line + if (textOnly) + { + console.gotoxy(6, 1); + console.print("\x01n" + libText); + console.gotoxy(6, 2); + console.print("\x01n" + dirText); + } + else + { + console.print("\x01n\x01w" + BLOCK1 + BLOCK2 + BLOCK3 + BLOCK4 + THIN_RECTANGLE_LEFT); + console.print(libText); + console.print("\x01w" + THIN_RECTANGLE_RIGHT + "\x01k\x01h" + BLOCK4 + "\x01n\x01w" + THIN_RECTANGLE_LEFT + + "\x01g\x01hDD File\x01n\x01w"); + console.print(THIN_RECTANGLE_RIGHT + BLOCK4 + BLOCK3 + BLOCK2 + BLOCK1); + console.crlf(); + // Directory line + console.print("\x01n\x01w" + BLOCK1 + BLOCK2 + BLOCK3 + BLOCK4 + THIN_RECTANGLE_LEFT); + console.print(dirText); + console.print("\x01w" + THIN_RECTANGLE_RIGHT + "\x01k\x01h" + BLOCK4 + "\x01n\x01w" + THIN_RECTANGLE_LEFT + + "\x01g\x01hLister \x01n\x01w"); + console.print(THIN_RECTANGLE_RIGHT + BLOCK4 + BLOCK3 + BLOCK2 + BLOCK1); + console.print("\x01n"); + + // List header + console.crlf(); + displayListHdrLine(false); + + if (dispHdrFirstRun) + { + gNumHeaderLinesDisplayed = 3; + 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 descLen = gListIdxes.descriptionEnd - gListIdxes.descriptionStart + 1; + var formatStr = "\x01n\x01w\x01h%-" + filenameLen + "s %" + fileSizeLen + "s %-" + + +(descLen-7) + "s\x01n\x01w%5s\x01n"; + 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; + // If we'll be displaying short (one-line) file descriptions, then use the whole width + // of the terminal (minus 1) for the menu width. But if the user has extended (multi-line) + // file descriptions enabled, then set the menu width only up through the file size, since + // the extended file description will be displayed to the right of the menu. + var menuWidth = console.screen_columns - 1; + if (extendedDescEnabled()) + menuWidth = gListIdxes.fileSizeEnd + 1; + var menuHeight = console.screen_rows - (startRow-1) - 1; + var fileListMenu = new DDLightbarMenu(1, startRow, menuWidth, menuHeight); + fileListMenu.scrollbarEnabled = true; + fileListMenu.borderEnabled = false; + fileListMenu.multiSelect = true; + fileListMenu.ampersandHotkeysInItems = false; + fileListMenu.wrapNavigation = false; + + fileListMenu.extdDescEnabled = extendedDescEnabled(); + + // Add additional keypresses for quitting the menu's input loop so we can + // respond to these keys. + if (typeof(pQuitKeys) === "string") + fileListMenu.AddAdditionalQuitKeys(pQuitKeys); + + // Define the menu's function to get the number of items. This must be done here + // in order for the menu's CanShowAllItemsInWindow() to work propertly. + fileListMenu.NumItems = function() { + return gFileList.length; + }; + + // Define a function for setting the description width, colors, and format string + // based on whether the menu's scrollbar will be used. + // + // Return value: Boolean: Whether or not the width changed + fileListMenu.SetItemWidthsColorsAndFormatStr = function() { + var widthChanged = false; + var oldDescriptionEnd = gListIdxes.descriptionEnd; + gListIdxes.descriptionEnd = console.screen_columns - 1; // Leave 1 character remaining on the screen + // If the file list menu will use its scrollbar, then reduce the description width by 1 + if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow()) + gListIdxes.descriptionEnd -= 1; + widthChanged = (gListIdxes.descriptionEnd != oldDescriptionEnd); + this.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}] + }); + + this.filenameLen = gListIdxes.filenameEnd - gListIdxes.filenameStart; + this.fileSizeLen = gListIdxes.fileSizeEnd - gListIdxes.fileSizeStart -1; + this.shortDescLen = gListIdxes.descriptionEnd - gListIdxes.descriptionStart + 1; + // If extended descriptions are enabled, then we won't be writing a description here + if (this.extdDescEnabled) + this.fileFormatStr = "%-" + this.filenameLen + "s %" + this.fileSizeLen + "s"; + else + { + this.fileFormatStr = "%-" + this.filenameLen + "s %" + this.fileSizeLen + + "s %-" + this.shortDescLen + "s"; + } + return widthChanged; + }; + // Set up the menu's description width, colors, and format string + fileListMenu.SetItemWidthsColorsAndFormatStr(); + + fileListMenu.lastFileDirCode = ""; + // Define the menu function for getting an item + fileListMenu.GetItem = function(pIdx) { + // In here, 'this' refers to the fileListMenu object + // If doing a file search, then update the header with the file library & directory + // name of the currently selected file (instead of displaying "Various"). This seems + // like a bit of a hack, but it works. + var allSameDir = (typeof(gFileList.allSameDir) === "boolean" ? gFileList.allSameDir : false); + if (isDoingFileSearch() && !allSameDir && gFileList[pIdx].dirCode != this.lastFileDirCode) + { + if ((gListBehavior & FL_NO_HDR) != FL_NO_HDR) + { + var originalCurPos = console.getxy(); + displayFileLibAndDirHeader(true, gFileList[pIdx].dirCode); + console.gotoxy(originalCurPos); + } + } + this.lastFileDirCode = gFileList[pIdx].dirCode; + + var menuItemObj = this.MakeItemWithRetval(pIdx); + var filename = shortenFilename(gFileList[pIdx].name, this.filenameLen, true); + // FileBase.format_name() could also be called to format the filename for display: + /* + var filebase = new FileBase(gFileList[pIdx].dirCode); + if (filebase.open()) + { + filename = filebase.format_name(gFileList[pIdx].name, this.filenameLen, true); + filebase.close(); + } + */ + var desc = (typeof(gFileList[pIdx].desc) === "string" ? gFileList[pIdx].desc : ""); + menuItemObj.text = format(this.fileFormatStr, + filename, + getFileSizeStr(gFileList[pIdx].size, this.fileSizeLen), + desc.substr(0, this.shortDescLen)); + return menuItemObj; + } + + fileListMenu.selectedItemIndexes = {}; + fileListMenu.numSelectedItemIndexes = function() { + return Object.keys(this.selectedItemIndexes).length; + }; + + // If extended file descriptions are enabled, set the OnItemNav function for + // when the user navigates to a new item, to display the file description + // next to the file menu. + if (extendedDescEnabled()) + { + fileListMenu.OnItemNav = function(pOldItemIdx, pNewItemIdx) { + displayFileExtDescOnMainScreen(pNewItemIdx); + }; + } + + 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("\x01n\x01y\x01hThere are no file libraries available\x01n"); + 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); + + // Re-define the menu's function for getting the number of items. This is necessary + // here in order for the menu's CanShowAllItemsInWindow() to work properly. + fileLibMenu.NumItems = function() { + return file_area.lib_list.length; + }; + + // 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 + // If the scrollbar will be showing, reduce the inner width by 1 + if (fileLibMenu.scrollbarEnabled && !fileLibMenu.CanShowAllItemsInWindow()) + --menuInnerWidth; + // 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: "\x01n" + gColors.fileNormalBkg + gColors.fileAreaNum}, + {start: descStart, end:descEnd, attrs: "\x01n" + gColors.fileNormalBkg + gColors.fileAreaDesc}, + {start: numDirsStart, end: -1, attrs: "\x01n" + gColors.fileNormalBkg + gColors.fileAreaNumItems}], + selectedItemColor: [{start: libNumStart, end: libNumEnd, attrs: "\x01n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaNumHighlight}, + {start: descStart, end:descEnd, attrs: "\x01n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaDescHighlight}, + {start: numDirsStart, end: -1, attrs: "\x01n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaNumItemsHighlight}] + }); + + fileLibMenu.topBorderText = "\x01y\x01hFile Libraries"; + // Define the menu function for getting an item + 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) + { + displayMsg("There are no directories in this file library", true, true); + 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); + + // Re-define the menu's function for getting the number of items. This is necessary + // here in order for the menu's CanShowAllItemsInWindow() to work properly. + fileDirMenu.NumItems = function() { + return file_area.lib_list[this.libIdx].dir_list.length; + }; + + 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 + // If the scrollbar will be showing, reduce the inner width by 1 + if (fileDirMenu.scrollbarEnabled && !fileDirMenu.CanShowAllItemsInWindow()) + --menuInnerWidth; + // 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: "\x01n" + gColors.fileNormalBkg + gColors.fileAreaNum}, + {start: descStart, end:descEnd, attrs: "\x01n" + gColors.fileNormalBkg + gColors.fileAreaDesc}, + {start: numDirsStart, end: -1, attrs: "\x01n" + gColors.fileNormalBkg + gColors.fileAreaNumItems}], + selectedItemColor: [{start: dirNumStart, end: dirNumEnd, attrs: "\x01n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaNumHighlight}, + {start: descStart, end:descEnd, attrs: "\x01n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaDescHighlight}, + {start: numDirsStart, end: -1, attrs: "\x01n" + gColors.fileAreaMenuHighlightBkg + gColors.fileAreaNumItemsHighlight}] + }); + + fileDirMenu.topBorderText = "\x01y\x01h" + ("File directories of " + file_area.lib_list[pLibIdx].description).substr(0, fileDirMenu.size.width-2); + // Define the menu function for ggetting an item + 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), + file_area.lib_list[this.libIdx].dir_list[pIdx].files); + return menuItemObj; + } + + return fileDirMenu; +} + +// 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 = file_area.lib_list[pLibIdx].dir_list[dirIdx].files; + 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 : "\x01n\x01w"); + var lineColor = (typeof(pLineColor) == "string" ? pLineColor : "\x01n\x01k\x01h"); + + // 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 = "\x01n" + (pIsError ? gColors.errorMessage : gColors.successMessage); + var innerWidth = gErrorMsgBoxWidth - 2; + var msgFormatStr = msgColor + "%-" + innerWidth + "s\x01n"; + 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); + // This used to call gFileListMenu.DrawPartialAbs + refreshScreenMainContent(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("\x01n" + 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 = "\x01n" + pTitleColor + titleTextWithoutAttrs; + console.print(borderChars.preText + "\x01n" + substrWithAttrCodes(titleText, 0, titleLen) + + "\x01n" + 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 + "\x01n" + substrWithAttrCodes(pBottomBorderText, 0, textLen) + + "\x01n" + 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("\x01n\x01g\x01h"); + for (var i = 0; i < width; ++i) + console.print(HORIZONTAL_SINGLE); + console.print("\x01n"); +} + +// 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("\x01n"); + console.gotoxy(1, console.screen_rows-1); + var shortFilename = shortenFilename(filename, Math.floor(console.screen_columns/2), false); + if (pDefaultYes) + actionConfirmed = console.yesno(pActionName + " " + shortFilename); + else + actionConfirmed = !console.noyes(pActionName + " " + shortFilename); + // Refresh the main screen content, to erase the confirmation prompt + refreshScreenMainContent(1, console.screen_rows-2, console.screen_columns, 2, true); + } + 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 frameWidth = console.screen_columns - 4; + var frameHeight = 10; + var frameTitle = pActionName + " files? (Y/N)"; + var additionalQuitKeys = "yYnN"; + var frameInnerWidth = frameWidth - 2; // Without borders; for filename lengths + var fileListStr = "\x01n\x01w"; + 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"); + // Refresh the main screen content, to erase the confirmation window + refreshScreenMainContent(frameUpperLeftX, frameUpperLeftY, frameWidth, frameHeight, true); + } + + 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) + 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); + var 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 if (valueUpper == "ULTIME") + gFileSortOrder = SORT_FL_ULTIME; + else if (valueUpper == "DLTIME") + gFileSortOrder = SORT_FL_DLTIME; + else // Default + gFileSortOrder = FileBase.SORT.NATURAL; + } + else if (settingUpper == "PAUSEAFTERVIEWINGFILE") + gPauseAfterViewingFile = (value.toUpperCase() == "TRUE"); + 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("\x01n"); + console.crlf(); + console.print("\x01w\x01hUnable to open the configuration file: \x01y" + cfgFilename); + console.crlf(); + console.print("\x01wDefault 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 "\x01" or "\1" + // with the Synchronet attribute control character. + gColors[setting] = trimSpaces(value, true, false, true).replace(/\\[xX]01/g, "\x01").replace(/\\1/g, "\x01"); + } + } + } + + 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("\x01n"); + console.crlf(); + console.print("\x01w\x01hUnable to open the theme file: \x01y" + themeFilename); + console.crlf(); + console.print("\x01wDefault 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 shortenedFilename = ""; // Will contain the shortened filename + // Get the filename extension. And the way we shorten the filename + // will depend on whether the filename actually has an extension or not. + var filenameExt = file_getext(pFilename); + if (typeof(filenameExt) === "string") + { + 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); + if (fillWidth) + shortenedFilename = format("%-" + maxWithoutExtLen + "s%s", filenameWithoutExt, filenameExt); + else + shortenedFilename = filenameWithoutExt + filenameExt; + } + else + shortenedFilename = pFilename.substr(0, pMaxLen); + return shortenedFilename; +} + + +// Parses command-line arguments, where each argument in the given array is in +// the format -arg=val. If the value is the string "true" or "false", then the +// value will be a boolean. Otherwise, the value will be a string. +// +// Parameters: +// argv: An array of strings containing values in the format -arg=val +// +// Return value: Boolean: Whether or not this script was run as a loadable +// module (depending on the arguments used). +function parseArgs(argv) +{ + // Default program options + gScriptMode = MODE_LIST_DIR; + + // Sanity checking for argv - Make sure it's an array + if ((typeof(argv) != "object") || (typeof(argv.length) != "number")) + return false; + + // This script accepts arguments in -arg=val format; See if there are any of those. + // If so, process arguments with that assumption; otherwise, we'll check for args + // passed when Synchronet runs this as a loadable module. + var scriptRanAsLoadableModule = false; + var anySettingEqualsVal = false; + for (var i = 0; i < argv.length && !anySettingEqualsVal; ++i) + anySettingEqualsVal = (typeof(argv[i]) === "string" && argv[i].length > 0 && argv[i].charAt(0) == "-" && argv[i].indexOf("=") > 1); + if (anySettingEqualsVal) + { + // Go through argv looking for strings in the format -arg=val and parse them + // into objects in the argVals array. + var equalsIdx = 0; + var argName = ""; + var argVal = ""; + var argValUpper = ""; // For case-insensitive matching + for (var i = 0; i < argv.length; ++i) + { + // We're looking for strings that start with "-", except strings that are + // only "-". + if (typeof(argv[i]) !== "string" || argv[i].length == 0 || argv[i].charAt(0) != "-" || argv[i] == "-") + continue; + + // Look for an = and if found, split the string on the = + equalsIdx = argv[i].indexOf("="); + // If a = is found, then split on it and add the argument name & value + // to the array. Otherwise (if the = is not found), then treat the + // argument as a boolean and set it to true (to enable an option). + if (equalsIdx > -1) + { + argName = argv[i].substring(1, equalsIdx).toUpperCase(); + argVal = argv[i].substr(equalsIdx+1); + argValUpper = argVal.toUpperCase(); + if (argName === "MODE") + { + if (argValUpper === "SEARCH_FILENAME") + gScriptMode = MODE_SEARCH_FILENAME; + else if (argValUpper === "SEARCH_DESCRIPTION") + gScriptMode = MODE_SEARCH_DESCRIPTION; + else if (argValUpper === "NEW_FILE_SEARCH") + gScriptMode = MODE_NEW_FILE_SEARCH; + else if (argValUpper === "LIST_CURDIR") + gScriptMode = MODE_LIST_DIR; + } + } + // Nothing to do if an = was not found + } + } + else + { + // Check for arguments as if this was run by Synchronet as a loadable module + // (for either Scan Dirs or List Files) + + /* + // bbs.list_files() & bbs.scan_dirs() + //******************************************** + var FL_NONE =0; // No special behavior + var FL_ULTIME =(1<<0); // List files by upload time + var FL_DLTIME =(1<<1); // List files by download time + var FL_NO_HDR =(1<<2); // Don't list directory header + var FL_FINDDESC =(1<<3); // Find text in description + var FL_EXFIND =(1<<4); // Find text in description - extended info + var FL_VIEW =(1<<5); // View ZIP/ARC/GIF etc. info + */ + + // There must be either 2 or 3 arguments + if (argv.length < 2) + return false; + // The 2nd argument is the mode/behavior bits in either case + var FLBehavior = parseInt(argv[1]); + if (isNaN(FLBehavior)) + return false; + else + gListBehavior = FLBehavior; + scriptRanAsLoadableModule = true; + + // Default gScriptmode to MODE_LIST_DIR; for FLBehavior as FL_NONE, no special behavior + gScriptMode = MODE_LIST_DIR; + + // 2 args - Scanning/searching + if (argv.length == 2) + { + // - 0: Bool (scanning all directories): 0/1 + // - 1: FL_ mode value + gScanAllDirs = (argv[0] == "1"); + if ((FLBehavior & FL_ULTIME) == FL_ULTIME) + gScriptMode = MODE_NEW_FILE_SEARCH; + else if ((FLBehavior & FL_FINDDESC) == FL_FINDDESC || (FLBehavior & FL_EXFIND) == FL_EXFIND) + gScriptMode = MODE_SEARCH_DESCRIPTION; + if ((FLBehavior & FL_VIEW) == FL_VIEW) + { + // View ZIP/ARC/GIF etc. info + // TODO: Not sure what to do with this + } + } + // 3 args - Listing + else if (argv.length >= 3) //==3 + { + // - 0: Directory internal code + // - 1: FL_ mode value + // - 2: Filespec (i.e., *, *.zip, etc.) + if (!file_area.dir.hasOwnProperty(argv[0])) + return false; + gDirCode = argv[0]; + gFilespec = argv[2]; + if ((FLBehavior & FL_VIEW) == FL_VIEW) + { + // View ZIP/ARC/GIF etc. info + // TODO: Not sure what to do with this + } + } + + // Options that apply to both searching and listing + if ((FLBehavior & FL_ULTIME) == FL_ULTIME) + gFileSortOrder = SORT_FL_ULTIME; + else if ((FLBehavior & FL_DLTIME) == FL_DLTIME) + gFileSortOrder = SORT_FL_DLTIME; + + } + + return scriptRanAsLoadableModule; +} + +// Populates the file list (gFileList). +// +// Parameters: +// pSearchMode: The search mode +// +// Return value: An obmect with the following properties: +// exitNow: Boolean: Whether or not this script should exit after calling this function +// exitCode: The exit code to use if needing to exit after calling this function +function populateFileList(pSearchMode) +{ + var retObj = { + exitNow: false, + exitCode: 0 + }; + + var dirErrors = []; + var allSameDir = true; + + // Do the things for list or search, depending on the specified mode + if (pSearchMode == MODE_LIST_DIR) // This is the default + { + var filebase = new FileBase(gDirCode); + if (filebase.open()) + { + // If there are no files in the filebase, then say so and exit now. + if (filebase.files == 0) + { + filebase.close(); + var libIdx = file_area.dir[gDirCode].lib_index; + console.crlf(); + console.print("\x01n\x01cThere are no files in \x01h" + file_area.lib_list[libIdx].description + "\x01n\x01c - \x01h" + + file_area.dir[gDirCode].description + "\x01n"); + console.crlf(); + console.pause(); + retObj.exitNow = true; + retObj.exitCode = 0; + return retObj; + } + + // Get a list of file data + var fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM); + if (gFileSortOrder == SORT_FL_ULTIME || gFileSortOrder == SORT_FL_DLTIME) + { + gFileList = filebase.get_list(gFilespec, fileDetail, 0, true, FileBase.SORT.NATURAL); + if (gFileSortOrder == SORT_FL_ULTIME) + gFileList.sort(fileInfoSortULTime); + else if (gFileSortOrder == SORT_FL_DLTIME) + gFileList.sort(fileInfoSortDLTime); + } + else + gFileList = filebase.get_list(gFilespec, fileDetail, 0, true, gFileSortOrder); + filebase.close(); + // Add a dirCode property to the file metadata objects (for consistency, + // as file search results may contain files from multiple directories). + // Also, if the metadata objects have an extdesc, remove any trailing CRLF + // from the end. + for (var i = 0; i < gFileList.length; ++i) + { + gFileList[i].dirCode = gDirCode; + if (gFileList[i].hasOwnProperty("extdesc") && /\r\n$/.test(gFileList[i].extdesc)) + { + gFileList[i].extdesc = gFileList[i].extdesc.substr(0, gFileList[i].extdesc.length-2); + // Fix line endings if necessary + gFileList[i].extdesc = lfexpand(gFileList[i].extdesc); + } + } + } + else + { + console.crlf(); + console.print("\x01n\x01h\x01yUnable to open \x01w" + file_area.dir[gDirCode].description + "\x01n"); + console.crlf(); + console.pause(); + retObj.exitNow = true; + retObj.exitCode = 1; + return retObj; + } + } + else if (pSearchMode == MODE_SEARCH_FILENAME) + { + var lastDirCode = ""; + + // If not searching all already, prompt the user for directory, library, or all + var validInputOptions = "DLA"; + var userInputDLA = ""; + if (gScanAllDirs) + userInputDLA = "A"; + else + { + console.attributes = "N"; + console.crlf(); + console.mnemonics(bbs.text(DirLibOrAll)); + userInputDLA = console.getkeys(validInputOptions, -1, K_UPPER); + } + var userFilespec = ""; + if (userInputDLA.length > 0 && validInputOptions.indexOf(userInputDLA) > -1) + { + // Prompt the user for a filespec to search for + console.mnemonics(bbs.text(FileSpecStarDotStar)); + userFilespec = console.getstr(); + if (userFilespec.length == 0) + userFilespec = "*"; // Should this be *.*? + console.print("Searching...."); + } + var searchRetObj = searchDirGroupOrAll(userInputDLA, function(pDirCode) { + return searchDirWithFilespec(pDirCode, userFilespec); + }); + allSameDir = searchRetObj.allSameDir; + for (var i = 0; i < searchRetObj.errors.length; ++i) + dirErrors.push(searchRetObj.errors[i]); + } + else if (pSearchMode == MODE_SEARCH_DESCRIPTION) + { + var lastDirCode = ""; + + // If not saerching all already, prompt the user for directory, library, or all + var validInputOptions = "DLA"; + var userInputDLA = ""; + if (gScanAllDirs) + userInputDLA = "A"; + else + { + console.print("\x01n"); + console.crlf(); + //console.print("\r\n\x01c\x01hFind Text in File Descriptions (no wildcards)\x01n\r\n"); + console.mnemonics(bbs.text(DirLibOrAll)); + console.print("\x01n"); + userInputDLA = console.getkeys(validInputOptions, -1, K_UPPER); + } + var searchDescription = ""; + if (userInputDLA.length > 0 && validInputOptions.indexOf(userInputDLA) > -1) + { + // Prompt the user for a description to search for + console.mnemonics(bbs.text(SearchStringPrompt)); + searchDescription = console.getstr(40, K_LINE|K_UPPER); + if (searchDescription.length > 0) + console.print("Searching...."); + else + { + retObj.exitNow = true; + retObj.exitCode = 0; + return retObj; + } + } + var searchRetObj = searchDirGroupOrAll(userInputDLA, function(pDirCode) { + return searchDirWithDescUpper(pDirCode, searchDescription); + }); + allSameDir = searchRetObj.allSameDir; + for (var i = 0; i < searchRetObj.errors.length; ++i) + dirErrors.push(searchRetObj.errors[i]); + } + else if (pSearchMode == MODE_NEW_FILE_SEARCH) + { + // New file search + var lastDirCode = ""; + + // 2022-02-011 - Digital Man said: + /* + Upon logon to the terminal server, bbs.new_file_time and bbs.last_new_file_time + are set to the current user.new_file_time value. + + A new file scan displays files uploaded (added/imported) since the current + bbs.new_file_time value. The bbs.new_file_time value can be manipulated by the + user, e.g. they want to review files that have been uploaded over the past + month or whatever. + + When the user executes a new file scan, bbs.last_new_file_time is updated to + the current time (this happens automatically when using the built-in file + listing logic). The bbs.last_new_file_time isn't actually used for anything + until the user logs-off and its value is then copied to the user.new_file_time + to be used for the next logon (this copy/sync of the last_new_file_time -> + user.new_file_time is built-in to the BBS's logout logci and nothing a script + would need to do). + + This scheme insures that a user will never "miss" the display of new files on + the BBS and that they would not normally see the same/repeat "new" files + between successive sessions. + */ + + // If not searching all dirs already, prompt the user for directory, library, or all + var userInputDLA = ""; + if (gScanAllDirs) + userInputDLA = "A"; + else + { + console.print("\x01n"); + console.crlf(); + console.mnemonics(bbs.text(DirLibOrAll)); + var validInputOptions = "DLA"; + userInputDLA = console.getkeys(validInputOptions, -1, K_UPPER); + console.print("\x01n"); + console.crlf(); + } + if (userInputDLA == "D" || userInputDLA == "L" || userInputDLA == "A") + { + console.print("\x01n\x01cSearching for files uploaded after \x01h" + system.timestr(bbs.new_file_time) + "\x01n"); + console.crlf(); + } + var searchRetObj = searchDirGroupOrAll(userInputDLA, function(pDirCode) { + return searchDirNewFiles(pDirCode, bbs.new_file_time); + }); + // Now bbs.last_new_file_time needs to be updated with the current time + bbs.last_new_file_time = time(); + // user.new_file_time should be updated with the value of bbs.last_new_file_time + // when the user logs off. + allSameDir = searchRetObj.allSameDir; + for (var i = 0; i < searchRetObj.errors.length; ++i) + dirErrors.push(searchRetObj.errors[i]); + } + else + { + retObj.exitNow = true; + retObj.exitCode = 1; + return retObj; + } + + if (pSearchMode != MODE_LIST_DIR) + gFileList.allSameDir = allSameDir; + + if (dirErrors.length > 0) + { + console.print("\x01n\x01y\x01h"); + for (var i = 0; i < dirErrors.length; ++i) + { + console.print(dirErrors[i]); + console.crlf(); + } + console.print("\x01n"); + console.pause(); + retObj.exitNow = true; + retObj.exitCode = 1; + return retObj; + } + + // Figure out the longest filename in the list. + var longestFilenameLen = 0; + for (var i = 0; i < gFileList.length; ++i) + { + if (gFileList[i].name.length > longestFilenameLen) + longestFilenameLen = gFileList[i].name.length; + } + var displayFilenameLen = gListIdxes.filenameEnd - gListIdxes.filenameStart + 1; + // If the user has extended descriptions enabled, then allow 47 characters for the + // description and adjust the filename length accordingly + gListIdxes.descriptionEnd = console.screen_columns - 1; // Leave 1 character remaining on the screen + if (extendedDescEnabled()) + { + gListIdxes.descriptionStart = gListIdxes.descriptionEnd - 47 + 1; + gListIdxes.fileSizeEnd = gListIdxes.descriptionStart; + gListIdxes.fileSizeStart = gListIdxes.fileSizeEnd - 7; + gListIdxes.filenameEnd = gListIdxes.fileSizeStart; + } + // If not displaying extended descriptions, then if the longest filename + // is longer than the current display filename length and the user's + // terminal is at least 100 columns wide, then increase the filename length + // for the list by 20; + else if (longestFilenameLen > displayFilenameLen && console.screen_columns >= 100) + { + gListIdxes.filenameEnd += 20; + gListIdxes.fileSizeStart = gListIdxes.filenameEnd; + gListIdxes.fileSizeEnd = gListIdxes.fileSizeStart + 7; + gListIdxes.descriptionStart = gListIdxes.fileSizeEnd; + } + + return retObj; +} + +// Searches files in a directory by filespec. +// +// Parameters: +// pDirCode: The internal code of a directory to search +// pFilespec: A filespec describing files to search for +// +// Return value: An object with the following properties: +// foundFiles: Boolean - Whether or not any files were found +// dirErrors: An array of strings that will be populated with any errors that occur +function searchDirWithFilespec(pDirCode, pFilespec) +{ + var retObj = { + foundFiles: false, + dirErrors: [] + }; + if (typeof(pDirCode) !== "string" || pDirCode.length == 0 || typeof(pFilespec) !== "string") + return retObj; + + var filebase = new FileBase(pDirCode); + if (filebase.open()) + { + var fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM); + var fileList; + if (gFileSortOrder == SORT_FL_ULTIME || gFileSortOrder == SORT_FL_DLTIME) + { + fileList = filebase.get_list(pFilespec, fileDetail, 0, true, FileBase.SORT.NATURAL); + if (gFileSortOrder == SORT_FL_ULTIME) + fileList.sort(fileInfoSortULTime); + else if (gFileSortOrder == SORT_FL_DLTIME) + fileList.sort(fileInfoSortDLTime); + } + else + fileList = filebase.get_list(pFilespec, fileDetail, 0, true, gFileSortOrder); + retObj.foundFiles = (fileList.length > 0); + filebase.close(); + for (var i = 0; i < fileList.length; ++i) + { + // Fix line endings in extdesc if necessary + if (fileList[i].hasOwnProperty("extdesc") && /\r\n$/.test(fileList[i].extdesc)) + fileList[i].extdesc = lfexpand(fileList[i].extdesc); + fileList[i].dirCode = pDirCode; + gFileList.push(fileList[i]); + } + } + else + { + var libAndDirDesc = file_area.lib_list[libIdx].description + " - " + + file_area.lib_list[libIdx].dir_list[dirIdx].description + " (" + + file_area.lib_list[libIdx].dir_list[dirIdx].code + ")"; + retObj.dirErrors.push("Failed to open " + libAndDirDesc); + } + return retObj; +} + +// Searches files in a directory by description. +// +// Parameters: +// pDirCode: The internal code of a directory to search +// pDescUpper: The description to search for. This is assumed to be uppercase. +// +// Return value: An object with the following properties: +// foundFiles: Boolean - Whether or not any files were found +// dirErrors: An array of strings that will be populated with any errors that occur +function searchDirWithDescUpper(pDirCode, pDescUpper) +{ + var retObj = { + foundFiles: false, + dirErrors: [] + }; + if (typeof(pDirCode) !== "string" || pDirCode.length == 0 || typeof(pDescUpper) !== "string" || pDescUpper.length == 0) + return retObj; + + var filebase = new FileBase(pDirCode); + if (filebase.open()) + { + //var fileList = filebase.get_list(gFilespec, FileBase.DETAIL.EXTENDED, 0, true, gFileSortOrder); + var fileList; + if (gFileSortOrder == SORT_FL_ULTIME || gFileSortOrder == SORT_FL_DLTIME) + { + fileList = filebase.get_list(gFilespec, FileBase.DETAIL.EXTENDED, 0, true, FileBase.SORT.NATURAL); + if (gFileSortOrder == SORT_FL_ULTIME) + fileList.sort(fileInfoSortULTime); + else if (gFileSortOrder == SORT_FL_DLTIME) + fileList.sort(fileInfoSortDLTime); + } + else + fileList = filebase.get_list(gFilespec, FileBase.DETAIL.EXTENDED, 0, true, gFileSortOrder); + filebase.close(); + for (var i = 0; i < fileList.length; ++i) + { + // Search both 'desc' and 'extdesc', if available in the file metadata object + var fileIsMatch = false; + if (fileList[i].hasOwnProperty("desc")) + { + var fileDesc = strip_ctrl(fileList[i].desc).toUpperCase(); + fileIsMatch = (fileDesc.indexOf(pDescUpper) > -1); + } + if (!fileIsMatch && fileList[i].hasOwnProperty("extdesc")) + { + // Fix line endings if necessary + fileList[i].extdesc = lfexpand(fileList[i].extdesc); + var descLines = fileList[i].extdesc.split("\r\n"); + var fileDesc = strip_ctrl(descLines.join(" ")).toUpperCase(); + fileIsMatch = (fileDesc.indexOf(pDescUpper) > -1); + } + if (fileIsMatch) + { + retObj.foundFiles = true; + fileList[i].dirCode = pDirCode; + gFileList.push(fileList[i]); + } + } + } + else + { + var libAndDirDesc = file_area.lib_list[libIdx].description + " - " + + file_area.lib_list[libIdx].dir_list[dirIdx].description + " (" + + file_area.lib_list[libIdx].dir_list[dirIdx].code + ")"; + retObj.dirErrors.push("Failed to open " + libAndDirDesc); + } + return retObj; +} + +// Searches files in a directory that were added later than a given time. +// +// Parameters: +// pDirCode: The internal code of a directory to search +// pSinceTime: The time after which to look for newer files +// +// Return value: An object with the following properties: +// foundFiles: Boolean - Whether or not any files were found +// dirErrors: An array of strings that will be populated with any errors that occur +function searchDirNewFiles(pDirCode, pSinceTime) +{ + var retObj = { + foundFiles: false, + dirErrors: [] + }; + if (typeof(pDirCode) !== "string" || pDirCode.length == 0 || typeof(pSinceTime) !== "number") + return retObj; + + var filebase = new FileBase(pDirCode); + if (filebase.open()) + { + var fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM); + var fileList; + if (gFileSortOrder == SORT_FL_ULTIME || gFileSortOrder == SORT_FL_DLTIME) + { + fileList = filebase.get_list(gFilespec, fileDetail, 0, true, FileBase.SORT.NATURAL); + if (gFileSortOrder == SORT_FL_ULTIME) + fileList.sort(fileInfoSortULTime); + else if (gFileSortOrder == SORT_FL_DLTIME) + fileList.sort(fileInfoSortDLTime); + } + else + fileList = filebase.get_list(gFilespec, fileDetail, 0, true, gFileSortOrder); + filebase.close(); + for (var i = 0; i < fileList.length; ++i) + { + if (fileList[i].added >= pSinceTime) + { + retObj.foundFiles = true; + // Fix line endings in extdesc if necessary + if (fileList[i].hasOwnProperty("extdesc")) + fileList[i].extdesc = lfexpand(fileList[i].extdesc); + fileList[i].dirCode = pDirCode; + gFileList.push(fileList[i]); + } + } + } + else + { + var libAndDirDesc = file_area.lib_list[libIdx].description + " - " + + file_area.lib_list[libIdx].dir_list[dirIdx].description + " (" + + file_area.lib_list[libIdx].dir_list[dirIdx].code + ")"; + retObj.dirErrors.push("Failed to open " + libAndDirDesc); + } + return retObj; +} + +// Searches either the user's current directory, library, or all dirs in all +// libraries, using a search function that populages gFileList. +// +// Parameters: +// pSearchOption: A string that specifies "D" for directory, "L" for library, or "A" for all +// pDirSearchFn: A search function that searches a file directory for criteria and populates gFileList. +// This function must return an object with the following properties: +// foundFiles: Boolean - Whether any files were found +// dirErrors: An array containing strings for any errors found +// +// Return value: An object with the following properties: +// allSameDir: Boolean - Whethre or not all files found are in the same directory +// errors: An array containing strings for any errors found +function searchDirGroupOrAll(pSearchOption, pDirSearchFn) +{ + var retObj = { + allSameDir: true, + errors: [] + }; + + if (typeof(pSearchOption) !== "string") + { + retObj.errors.push("Invalid search option"); + return retObj; + } + if (typeof(pDirSearchFn) !== "function") + { + retObj.errors.push("No search function"); + return retObj; + } + + var searchOptionUpper = pSearchOption.toUpperCase(); + if (searchOptionUpper == "D") + { + // Current directory + var searchRetObj = pDirSearchFn(gDirCode); + retObj.allSameDir = true; + for (var i = 0; i < searchRetObj.dirErrors.length; ++i) + retObj.errors.push(searchRetObj.dirErrors[i]); + } + else if (searchOptionUpper == "L") + { + // Current file library + var libIdx = file_area.dir[gDirCode].lib_index; + for (var dirIdx = 0; dirIdx < file_area.lib_list[libIdx].dir_list.length; ++dirIdx) + { + if (file_area.lib_list[libIdx].dir_list[dirIdx].can_access) // And can_download? + { + if (gSearchVerbose) + { + console.print(" " + file_area.lib_list[libIdx].dir_list[dirIdx].description + ".."); + console.crlf(); + } + var lastDirCode = (gFileList.length > 0 ? gFileList[gFileList.length-1].dirCode : ""); + var dirCode = file_area.lib_list[libIdx].dir_list[dirIdx].code; + var searchRetObj = pDirSearchFn(dirCode); + if (retObj.allSameDir && searchRetObj.foundFiles && lastDirCode.length > 0) + retObj.allSameDir = (dirCode == lastDirCode); + for (var i = 0; i < searchRetObj.dirErrors.length; ++i) + retObj.errors.push(searchRetObj.dirErrors[i]); + } + } + } + else if (searchOptionUpper == "A") + { + // All file libraries & directories + for (var libIdx = 0; libIdx < file_area.lib_list.length; ++libIdx) + { + if (gSearchVerbose) + { + console.print("Searching " + file_area.lib_list[libIdx].description + ".."); + console.crlf(); + } + for (var dirIdx = 0; dirIdx < file_area.lib_list[libIdx].dir_list.length; ++dirIdx) + { + if (file_area.lib_list[libIdx].dir_list[dirIdx].can_access) // And can_download? + { + if (gSearchVerbose) + { + console.print(" " + file_area.lib_list[libIdx].dir_list[dirIdx].description + ".."); + console.crlf(); + } + var lastDirCode = (gFileList.length > 0 ? gFileList[gFileList.length-1].dirCode : ""); + var dirCode = file_area.lib_list[libIdx].dir_list[dirIdx].code; + var searchRetObj = pDirSearchFn(dirCode); + if (retObj.allSameDir && searchRetObj.foundFiles && lastDirCode.length > 0) + retObj.allSameDir = (dirCode == lastDirCode); + for (var i = 0; i < searchRetObj.dirErrors.length; ++i) + retObj.errors.push(searchRetObj.dirErrors[i]); + } + } + } + } + else + { + //retObj.errors.push("Invalid search option" + (pSearchOption.length > 0 ? ": " + pSearchOption : "")); + retObj.errors.push("Aborted"); + } + + return retObj; +} + +// Returns whether the user has their extended file description setting enabled +// and if it can be supported in the user's terminal mode. Extended descriptions +// will be displayed in the main screen if the user has that option enabled and +// the user's terminal is at least 80 columns wide. +function extendedDescEnabled() +{ + var userExtDescEnabled = ((user.settings & USER_EXTDESC) == USER_EXTDESC); + return userExtDescEnabled && console.screen_columns >= 80; +} + +// Displays a file's extended description on the main screen, next to the +// file list menu. This is to be used when the user's extended file description +// option is enabled (where the menu would take up about the left half of +// the screen). +// +// Parameters: +// pFileIdx: The index of the file metadata object in gFileList to use +// pStartScreenRow: Optional - The screen row number to start printing, for partial +// screen refreshing (can be in the middle of the extended description) +// pEndScreenRow: Optional - The screen row number to stop printing, for partial +// screen refreshing (can be in the middle of the extended description) +// pMaxWidth: Optional - The maximum width to use for printing the description lines +function displayFileExtDescOnMainScreen(pFileIdx, pStartScreenRow, pEndScreenRow, pMaxWidth) +{ + if (typeof(pFileIdx) !== "number") + return; + if (pFileIdx < 0 || pFileIdx >= gFileList.length) + return; + + // Get the file description from its metadata object + var fileMetadata = gFileList[pFileIdx]; + var fileDesc = ""; + if (fileMetadata.hasOwnProperty("extdesc") && fileMetadata.extdesc.length > 0) + fileDesc = fileMetadata.extdesc; + else + fileDesc = fileMetadata.desc; + if (typeof(fileDesc) != "string") + fileDesc = ""; + + // This might be overkill, but just in case, convert any non-Synchronet + // attribute codes to Synchronet attribute codes in the description. + // This will help simplify getting substrings for formatting. Then for + // efficiency, put the converted description back into the metadata + // object in the array so that it doesn't have to be converted again. + if (!fileMetadata.hasOwnProperty("attrsConverted")) + { + fileDesc = convertAttrsToSyncPerSysCfg(fileDesc); + fileMetadata.attrsConverted = true; + if (fileMetadata.hasOwnProperty("extdesc")) + fileMetadata.extdesc = fileDesc; + else + fileMetadata.desc = fileDesc; + } + + // Calculate where to write the description on the screen + var startX = gFileListMenu.size.width + 1; // Assuming the file menu starts at the leftmost column + var maxDescLen = console.screen_columns - startX; + if (typeof(pMaxWidth) === "number" && pMaxWidth >= 0 && pMaxWidth < maxDescLen) + maxDescLen = pMaxWidth; + // Go to the location on the screen and write the file description + var formatStr = "%-" + maxDescLen + "s"; + // firstScreenRow is the first row on the screen where the extended description + // should start at. lastScreenRow is the last row (inclusive) to use for + // printing the extended description + var firstScreenRow = gNumHeaderLinesDisplayed + 1; + var lastScreenRow = console.screen_rows - 1; // This is inclusive + // screenRowForPrinting will be used for the actual screen row we're at while + // printing the extended description lines + var screenRowForPrinting = firstScreenRow; + // If pStartScreenRow or pEndScreenRow are specified, then use + // them to specify the start & end screen rows to actually print + if (typeof(pStartScreenRow) === "number" && pStartScreenRow >= firstScreenRow && pStartScreenRow <= lastScreenRow) + screenRowForPrinting = pStartScreenRow; + if (typeof(pEndScreenRow) === "number" && pEndScreenRow > firstScreenRow && pStartScreenRow <= lastScreenRow) + lastScreenRow = pEndScreenRow; + var fileDescArray = fileDesc.split("\r\n"); + console.print("\x01n"); + // screenRowNum is to keep track of the row on the screen where the + // description line would be placed, in case the start row is after that + var screenRowNum = firstScreenRow; + for (var i = 0; i < fileDescArray.length; ++i) + { + if (screenRowForPrinting > screenRowNum++) + continue; + // Note: substrWithAttrCodes() is defined in dd_lightbar_menu.js + // Normally it would be handy to use printf() to print the text line: + //printf(formatStr, substrWithAttrCodes(fileDescArray[i], 0, maxDescLen)); + // However, printf() doesn't account for attribute codes and thus may not + // fill the rest of the width. So, we do that manually. + var descLine = substrWithAttrCodes(fileDescArray[i], 0, maxDescLen); + var lineTextLength = console.strlen(descLine); + if (lineTextLength > 0) + { + console.gotoxy(startX, screenRowForPrinting++); + console.print(descLine); + var remainingLen = maxDescLen - lineTextLength; + if (remainingLen > 0) + printf("%" + remainingLen + "s", ""); + } + // Stop printing the description lines when we reach the last line on + // the screen where we want to print. + if (screenRowForPrinting > lastScreenRow) + break; + } + // If there is room, shoe the file date on the next line + if (screenRowForPrinting <= lastScreenRow && fileMetadata.hasOwnProperty("time")) + { + console.print("\x01n"); + console.gotoxy(startX, screenRowForPrinting++); + var dateStr = "Date: " + strftime("%Y-%m-%d", fileMetadata.time); + printf("%-" + maxDescLen + "s", dateStr.substr(0, maxDescLen)); + } + // Clear the rest of the lines to the bottom of the list area + console.print("\x01n"); + while (screenRowForPrinting <= lastScreenRow) + { + console.gotoxy(startX, screenRowForPrinting++); + printf(formatStr, ""); + } +} + +// Refreshes (re-draws) the main content of the screen (file list menu, +// and extended description area if enabled). The coordinates are absolute +// screen coordinates. +// +// Parameters: +// pUpperLeftX: The X coordinate of the upper-left corner of the area to re-draw +// pUpperLeftY: The Y coordinate of the upper-left corner of the area to re-draw +// pWidth: The width of the area to re-draw +// pHeight: The height of the area to re-draw +// pSelectedItemIdxes: Optional: An object with selected item indexes for the file menu. +// If not passed, an empty object will be used. +// This can also be a boolean, and if true, will refresh the +// selected items on the file menu (with checkmarks) outside the +// given top & bottom screen rows. +function refreshScreenMainContent(pUpperLeftX, pUpperLeftY, pWidth, pHeight, pSelectedItemIdxes) +{ + // Have the file list menu partially re-draw itself if necessary + var startXWithinFileList = (pUpperLeftX >= gFileListMenu.pos.x && pUpperLeftX < gFileListMenu.pos.x + gFileListMenu.size.width); + var startYWithinFileList = (pUpperLeftY >= gFileListMenu.pos.y && pUpperLeftY < gFileListMenu.pos.y + gFileListMenu.size.height); + if (startXWithinFileList && startYWithinFileList) + { + var selectedItemIdxesIsValid = (typeof(pSelectedItemIdxes) === "object"); + var selectedItemIdxes = (selectedItemIdxesIsValid ? pSelectedItemIdxes : {}); + gFileListMenu.DrawPartialAbs(pUpperLeftX, pUpperLeftY, pWidth, pHeight, selectedItemIdxes); + } + // If pSelectedItemIdxes is a bool instead of an object and is true, + // refresh the selected items (with checkmarks) outside the top & bottom + // lines on the file menu + if (!selectedItemIdxesIsValid && typeof(pSelectedItemIdxes) === "boolean" && pSelectedItemIdxes && gFileListMenu.numSelectedItemIndexes() > 0) + { + var bottomScreenRow = pUpperLeftY + pHeight - 1; + for (var idx in gFileListMenu.selectedItemIndexes) + { + var idxNum = +idx; + var itemScreenRow = gFileListMenu.ScreenRowForItem(idxNum); + if (itemScreenRow == -1) + continue; + if (itemScreenRow < pUpperLeftY || itemScreenRow > bottomScreenRow) + { + var isSelected = (idxNum == gFileListMenu.selectedItemIdx); + gFileListMenu.WriteItemAtItsLocation(idxNum, isSelected, false); + } + } + } + // If the user has extended descriptions enabled, then the file menu + // is only taking up about half the screen on the left, and we'll also + // have to refresh the description area. + if (extendedDescEnabled()) + { + var fileMenuRightX = gFileListMenu.pos.x + gFileListMenu.size.width - 1; + var width = pWidth - (fileMenuRightX - pUpperLeftX + 1); + if (width > 0) + { + var firstRow = pUpperLeftY; + // The last row is inclusive. It seems like there might be an off-by-1 + // problem here? I thought 1 would need to be subtracted from lastRow + var lastRow = pUpperLeftY + pHeight; + // We don't want to overwrite the last row on the screen, since that's + // used for the command bar + if (lastRow == console.screen_rows) + --lastRow; + displayFileExtDescOnMainScreen(gFileListMenu.selectedItemIdx, firstRow, lastRow, width); + } + } +} + +// Returns whether or not the lister is doing a file search +function isDoingFileSearch() +{ + return (gScriptMode == MODE_SEARCH_FILENAME || gScriptMode == MODE_SEARCH_DESCRIPTION || gScriptMode == MODE_NEW_FILE_SEARCH); +} + +// Custom file info sort function for upload time +function fileInfoSortULTime(pA, pB) +{ + if (pA.hasOwnProperty("added") && pB.hasOwnProperty("added")) + { + if (pA.added < pB.added) + return -1; + else if (pA.added > pB.added) + return 1; + else + return 0; + } + else + { + if (pA.time < pB.time) + return -1; + else if (pA.time > pB.time) + return 1; + else + return 0; + } +} + +// Custom file info sort function for download time +function fileInfoSortDLTime(pA, pB) +{ + if (pA.hasOwnProperty("last_downloaded") && pB.hasOwnProperty("last_downloaded")) + { + if (pA.last_downloaded < pB.last_downloaded) + return -1; + else if (pA.last_downloaded > pB.last_downloaded) + return 1; + else + return 0; + } + else + return 0; +} + +// Returns whether or not a user can download from a file directory, and writes an error if not. +// +// Parameters: +// pDirCode: The internal directory code of a file directory +// +// Return value: Boolean - Whether or not the user can download from the file directory given +function userCanDownloadFromFileArea_ShowErrorIfNot(pDirCode) +{ + var userCanDownload = bbs.compare_ars(file_area.dir[pDirCode].download_ars); + if (!userCanDownload) + { + // The user doesn't have permission to download from this directory + //file_area.dir[pDirCode].name + var areaFullDesc = file_area.dir[pDirCode].lib_name + ": " + + file_area.dir[pDirCode].description; + areaFullDesc = word_wrap(areaFullDesc, console.screen_columns-1, areaFullDesc.length).replace(/\r|\n/g, "\r\n"); + while (areaFullDesc.lastIndexOf("\r\n") == areaFullDesc.length-2) + areaFullDesc = areaFullDesc.substr(0, areaFullDesc.length-2); + console.crlf(); + console.print(areaFullDesc); + console.crlf(); + console.mnemonics(bbs.text(CantDownloadFromDir)); + console.crlf(); + console.pause(); + } + return userCanDownload; +} \ No newline at end of file diff --git a/xtrn/ddfilelister/ddfilelister.js b/xtrn/ddfilelister/ddfilelister.js index 465e2610a61611b45b6e07c667f7b51e41341b89..28eda16bda163ce08280132b232a9d6250658e4a 100644 --- a/xtrn/ddfilelister/ddfilelister.js +++ b/xtrn/ddfilelister/ddfilelister.js @@ -61,6 +61,11 @@ * 2023-02-25 Eric Oulashin Version 2.09 * Now supports being used as a loadable module for * Scan Dirs and List Files + * 2023-02-27 Eric Oulashin Version 2.10 + * Now allows downloading a single selected file with the D key. + * Also, ddfilelister now checks whether the user has permission to + * download before allowing adding files to their batch download queue + * (and downloading a single file as well). */ "use strict"; @@ -68,6 +73,7 @@ if (typeof(require) === "function") { require("sbbsdefs.js", "K_UPPER"); + require('key_defs.js', 'KEY_UP'); require("text.js", "Email"); // Text string definitions (referencing text.dat) require("dd_lightbar_menu.js", "DDLightbarMenu"); require("frame.js", "Frame"); @@ -78,6 +84,7 @@ if (typeof(require) === "function") else { load("sbbsdefs.js"); + load('key_defs.js'); load("text.js"); // Text string definitions (referencing text.dat) load("dd_lightbar_menu.js"); load("frame.js"); @@ -121,13 +128,15 @@ if (system.version_num < 31900) } // Lister version information -var LISTER_VERSION = "2.09"; -var LISTER_DATE = "2023-02-25"; +var LISTER_VERSION = "2.10"; +var LISTER_DATE = "2023-02-27"; /////////////////////////////////////////////////////////////////////////////// // Global variables +var KEY_BACKSPACE = CTRL_H; + // Block characters var BLOCK1 = "\xB0"; // Dimmest block var BLOCK2 = "\xB1"; @@ -194,10 +203,11 @@ var gColors = { 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 +var FILE_DOWNLOAD_SINGLE = 4; +var HELP = 5; +var QUIT = 6; +var FILE_MOVE = 7; // Sysop action +var FILE_DELETE = 8; // Sysop action // Search/list modes var MODE_LIST_DIR = 1; @@ -259,10 +269,7 @@ if (!console.term_supports(USER_ANSI)) { var exitCode = 0; if (gScriptMode == MODE_SEARCH_FILENAME || gScriptMode == MODE_SEARCH_DESCRIPTION || gScriptMode == MODE_NEW_FILE_SEARCH) - { - //if (user.is_sysop) console.print("\x01n\r\nScan dirs\r\n"); // Temporary bbs.scan_dirs(gListBehavior, gScanAllDirs); - } else exitCode = bbs.list_files(gDirCode, gFilespec, gListBehavior); exit(exitCode); @@ -337,15 +344,16 @@ while (continueDoingFileList) { currentActionVal = fileMenuBar.getCurrentSelectedAction(); fileMenuBar.setCurrentActionCode(currentActionVal); - actionRetObj = doAction(currentActionVal, gFileList, gFileListMenu); + actionRetObj = doAction_ANSI(currentActionVal, gFileList, gFileListMenu); } - // Allow the delete key as a special key for sysops to delete the selected file(s) - else if (lastUserInputUpper == KEY_DEL) + // Allow the delete key as a special key for sysops to delete the selected file(s). Also allow backspace + // due to some terminals returning backspace for delete. + else if (lastUserInputUpper == KEY_DEL || lastUserInputUpper == KEY_BACKSPACE) { if (user.is_sysop) { fileMenuBar.setCurrentActionCode(FILE_DELETE, true); - actionRetObj = doAction(FILE_DELETE, gFileList, gFileListMenu); + actionRetObj = doAction_ANSI(FILE_DELETE, gFileList, gFileListMenu); currentActionVal = FILE_DELETE; } } @@ -353,7 +361,7 @@ while (continueDoingFileList) { currentActionVal = fileMenuBar.getActionFromChar(lastUserInputUpper, false); fileMenuBar.setCurrentActionCode(currentActionVal, true); - actionRetObj = doAction(currentActionVal, gFileList, gFileListMenu); + actionRetObj = doAction_ANSI(currentActionVal, 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. @@ -478,7 +486,7 @@ while (continueDoingFileList) /////////////////////////////////////////////////////////////////////////////// // Functions: File actions -// Performs a specified file action based on an action code. +// Performs a specified file action based on an action code. For the ANSI user interface. // // Parameters: // pActionCode: A code specifying an action to do. Must be one of the global @@ -488,22 +496,45 @@ while (continueDoingFileList) // // Return value: An object with values to indicate status & screen refresh actions; see // getDefaultActionRetObj() for details. -function doAction(pActionCode, pFileList, pFileListMenu) +function doAction_ANSI(pActionCode, pFileList, pFileListMenu) { if (typeof(pActionCode) !== "number") return getDefaultActionRetObj(); + var fileMetadata = pFileList[pFileListMenu.selectedItemIdx]; + var retObj = null; switch (pActionCode) { case FILE_VIEW_INFO: - retObj = showFileInfo(pFileList, pFileListMenu); + retObj = showFileInfo_ANSI(fileMetadata); break; case FILE_VIEW: - retObj = viewFile(pFileList, pFileListMenu); + retObj = viewFile_ANSI(fileMetadata); break; case FILE_ADD_TO_BATCH_DL: - retObj = addSelectedFilesToBatchDLQueue(pFileList, pFileListMenu); + if (userCanDownloadFromFileArea_ShowErrorIfNot(fileMetadata.dirCode)) + retObj = addSelectedFilesToBatchDLQueue_ANSI(fileMetadata, pFileList); + else + { + retObj = getDefaultActionRetObj(); + retObj.reDrawListerHeader = true; + retObj.reDrawHeaderTextOnly = false; + retObj.reDrawMainScreenContent = true; + retObj.reDrawCmdBar = true; + } + break; + case FILE_DOWNLOAD_SINGLE: + if (userCanDownloadFromFileArea_ShowErrorIfNot(fileMetadata.dirCode) && pFileListMenu.selectedItemIdx >= 0 && pFileListMenu.selectedItemIdx < pFileListMenu.NumItems()) + retObj = letUserDownloadSelectedFile_ANSI(fileMetadata); + else + { + retObj = getDefaultActionRetObj(); + retObj.reDrawListerHeader = true; + retObj.reDrawHeaderTextOnly = false; + retObj.reDrawMainScreenContent = true; + retObj.reDrawCmdBar = true; + } break; case HELP: retObj = displayHelpScreen(); @@ -514,11 +545,11 @@ function doAction(pActionCode, pFileList, pFileListMenu) break; case FILE_MOVE: // Sysop action if (user.is_sysop) - retObj = chooseFilebaseAndMoveFileToOtherFilebase(pFileList, pFileListMenu); + retObj = chooseFilebaseAndMoveFileToOtherFilebase_ANSI(pFileList, pFileListMenu); break; case FILE_DELETE: // Sysop action if (user.is_sysop) - retObj = confirmAndRemoveFilesFromFilebase(pFileList, pFileListMenu); + retObj = confirmAndRemoveFilesFromFilebase_ANSI(pFileList, pFileListMenu); break; } @@ -563,12 +594,11 @@ function getDefaultActionRetObj() // Shows extended information about a file to the user. // // Parameters: -// pFileList: The list of file metadata objects, as retrieved from the filebase -// pFileListMenu: The file list menu +// pFileMetadata: The file metadata object for the file to view information about // // Return value: An object with values to indicate status & screen refresh actions; see // getDefaultActionRetObj() for details. -function showFileInfo(pFileList, pFileListMenu) +function showFileInfo_ANSI(pFileMetadata) { var retObj = getDefaultActionRetObj(); @@ -582,13 +612,13 @@ function showFileInfo(pFileList, pFileListMenu) // can display the extended description. // The metadata object in pFileList should have a dirCode added by this script. var dirCode = gDirCode; - if (pFileList[pFileListMenu.selectedItemIdx].hasOwnProperty("dirCode")) - dirCode = pFileList[pFileListMenu.selectedItemIdx].dirCode; + if (pFileMetadata.hasOwnProperty("dirCode")) + dirCode = pFileMetadata.dirCode; var fileMetadata = null; if (extendedDescEnabled()) - fileMetadata = pFileList[pFileListMenu.selectedItemIdx]; + fileMetadata = pFileMetadata; else - fileMetadata = getFileInfoFromFilebase(dirCode, pFileList[pFileListMenu.selectedItemIdx].name, FileBase.DETAIL.EXTENDED); + fileMetadata = getFileInfoFromFilebase(dirCode, pFileMetadata.name, FileBase.DETAIL.EXTENDED); // Build a string with the file information // Make sure the displayed filename isn't too crazy long var frameInnerWidth = frameWidth - 2; // Without borders @@ -746,21 +776,20 @@ function splitStrAndCombineWithRN(pStr, pSplitStr) // Lets the user view a file. // // Parameters: -// pFileList: The list of file metadata objects, as retrieved from the filebase -// pFileListMenu: The file list menu +// pFileMetadata: The file metadata object for the file to view // // Return value: An object with values to indicate status & screen refresh actions; see // getDefaultActionRetObj() for details. -function viewFile(pFileList, pFileListMenu) +function viewFile_ANSI(pFileMetadata) { var retObj = getDefaultActionRetObj(); // Open the filebase & get the fully pathed filename var fullyPathedFilename = ""; - var filebase = new FileBase(pFileList[pFileListMenu.selectedItemIdx].dirCode); + var filebase = new FileBase(pFileMetadata.dirCode); if (filebase.open()) { - fullyPathedFilename = filebase.get_path(pFileList[pFileListMenu.selectedItemIdx]); + fullyPathedFilename = filebase.get_path(pFileMetadata); filebase.close(); } else @@ -787,14 +816,16 @@ function viewFile(pFileList, pFileListMenu) // Allows the user to add their selected file to their batch downloaded queue // // Parameters: +// pFileMetadata: The file metadata object for the file // 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(pFileList, pFileListMenu) +function addSelectedFilesToBatchDLQueue_ANSI(pFileMetadata, pFileList) { var retObj = getDefaultActionRetObj(); + if (!userCanDownloadFromFileArea_ShowErrorIfNot(pFileMetadata.dirCode)) + return retObj; // Confirm with the user to add the file(s) to their batch queue. If they don't want to, // then just return now. @@ -811,8 +842,8 @@ function addSelectedFilesToBatchDLQueue(pFileList, pFileListMenu) } else { - filenames.push(pFileList[pFileListMenu.selectedItemIdx].name); - metadataObjects.push(pFileList[pFileListMenu.selectedItemIdx]); + filenames.push(pFileMetadata.name); + metadataObjects.push(pFileMetadata); } // Note that confirmFileActionWithUser() will re-draw the parts of the file // list menu that are necessary. @@ -832,7 +863,7 @@ function addSelectedFilesToBatchDLQueue(pFileList, pFileListMenu) // 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); + fileAlreadyInQueue = (batchDLQueueStats.filenames[fIdx].filename == metadataObjects[i].name); if (!fileAlreadyInQueue) { var addToQueueSuccessful = true; @@ -902,8 +933,8 @@ function addSelectedFilesToBatchDLQueue(pFileList, pFileListMenu) // \x01cFiles: \x01h1 \x01n\x01c(\x01h100 \x01n\x01cMax) Credits: 0 Bytes: \x01h2,228,254 \x01n\x01c Time: 00:09:40 // 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 totalQueueSize = batchDLQueueStats.totalSize + pFileMetadata.size; + var totalQueueCost = batchDLQueueStats.totalCost + pFileMetadata.cost; var queueStats = "\x01n\x01cFiles: \x01h" + batchDLQueueStats.numFilesInQueue + " \x01n\x01cCredits: \x01h" + totalQueueCost + "\x01n\x01c Bytes: \x01h" + numWithCommas(totalQueueSize) + "\x01n\x01w\r\n"; for (var i = 0; i < batchDLQueueStats.filenames.length; ++i) @@ -991,7 +1022,8 @@ function getUserDLQueueStats() // See if a section exists for the filename //File.iniGetAllObjects([name_property] [,prefix=none] [,lowercase=false] [,blanks=false]) var allIniObjs = batchDLFile.iniGetAllObjects(); - console.print("\x01n\r\n"); + console.attributes = "N"; + console.crlf(); for (var i = 0; i < allIniObjs.length; ++i) { if (typeof(allIniObjs[i]) === "object") @@ -1024,6 +1056,36 @@ function getUserDLQueueStats() return retObj; } +// Lets the user download the currently selected file on the file list menu +// +// Parameters: +// pFileMetadata: The file metadata object for the file to download +// +// Return value: An object with values to indicate status & screen refresh actions; see +// getDefaultActionRetObj() for details. +function letUserDownloadSelectedFile_ANSI(pFileMetadata) +{ + var retObj = getDefaultActionRetObj(); + console.attributes = "N"; + console.crlf(); + console.crlf(); + // If the user has the security level to download the file, let them do so + if (userCanDownloadFromFileArea_ShowErrorIfNot(pFileMetadata.dirCode)) + { + console.print("\x01cDownloading \x01h" + pFileMetadata.name + "\x01n"); + console.crlf(); + var selectedFilanmeFullPath = backslash(file_area.dir[pFileMetadata.dirCode].path) + pFileMetadata.name; + bbs.send_file(selectedFilanmeFullPath); + } + + retObj.reDrawListerHeader = true; + retObj.reDrawHeaderTextOnly = false; + retObj.reDrawMainScreenContent = true; + retObj.reDrawCmdBar = true; + + return retObj; +} + // Displays the help screen. function displayHelpScreen() { @@ -1076,11 +1138,12 @@ function displayHelpScreen() var printfStr = "\x01n\x01c\x01h%-" + commandStrWidth + "s\x01g: \x01n\x01c%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"); + printf(printfStr, "B", "Flag the selected file(s) for batch download"); + printf(printfStr, "D", "Download the highlighted (selected) file"); if (user.is_sysop) { printf(printfStr, "M", "Move the file(s) to another directory"); - printf(printfStr, "D", "Delete the file(s)"); + printf(printfStr, "DEL", "Delete the file(s)"); } printf(printfStr, "?", "Show this help screen"); printf(printfStr, "Q", "Quit back to the BBS"); @@ -1102,10 +1165,12 @@ function displayHelpScreen() // // Return value: An object with values to indicate status & screen refresh actions; see // getDefaultActionRetObj() for details. -function chooseFilebaseAndMoveFileToOtherFilebase(pFileList, pFileListMenu) +function chooseFilebaseAndMoveFileToOtherFilebase_ANSI(pFileList, pFileListMenu) { var retObj = getDefaultActionRetObj(); + var fileMetadata = pFileList[pFileListMenu.selectedItemIdx]; + // Confirm with the user to move the file(s). If they don't want to, // then just return now. var filenames = []; @@ -1115,7 +1180,7 @@ function chooseFilebaseAndMoveFileToOtherFilebase(pFileList, pFileListMenu) filenames.push(pFileList[+idx].name); } else - filenames.push(pFileList[pFileListMenu.selectedItemIdx].name); + filenames.push(fileMetadata.name); // Note that confirmFileActionWithUser() will re-draw the parts of the file // list menu that are necessary. var moveFilesConfirmed = confirmFileActionWithUser(filenames, "Move", false); @@ -1153,7 +1218,7 @@ function chooseFilebaseAndMoveFileToOtherFilebase(pFileList, pFileListMenu) chosenDirCode = fileDirMenu.GetVal(); if (typeof(chosenDirCode) === "string") { - if (chosenDirCode != pFileList[pFileListMenu.selectedItemIdx].dirCode) + if (chosenDirCode != fileMetadata.dirCode) continueOn = false; else { @@ -1309,7 +1374,7 @@ function chooseFilebaseAndMoveFileToOtherFilebase(pFileList, pFileListMenu) // returned will have the following additional properties: // filesDeleted: Boolean - Whether or not files were actually deleted (after // confirmation) -function confirmAndRemoveFilesFromFilebase(pFileList, pFileListMenu) +function confirmAndRemoveFilesFromFilebase_ANSI(pFileList, pFileListMenu) { var retObj = getDefaultActionRetObj(); retObj.filesDeleted = false; @@ -1523,10 +1588,12 @@ function DDFileMenuBar(pPos) 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)); + this.cmdArray.push(new DDFileMenuBarItem("DL", 0, FILE_DOWNLOAD_SINGLE)); if (user.is_sysop) { this.cmdArray.push(new DDFileMenuBarItem("Move", 0, FILE_MOVE)); - this.cmdArray.push(new DDFileMenuBarItem("Del", 0, FILE_DELETE)); + //this.cmdArray.push(new DDFileMenuBarItem("Del", 0, FILE_DELETE)); + this.cmdArray.push(new DDFileMenuBarItem("DEL", 0, FILE_DELETE, KEY_DEL)); } this.cmdArray.push(new DDFileMenuBarItem("?", 0, HELP)); this.cmdArray.push(new DDFileMenuBarItem("Quit", 0, QUIT)); @@ -1695,7 +1762,12 @@ function DDFileMenuBar_getActionFromChar(pChar, pCaseSensitive) { 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) + if (this.cmdArray[i].hotkeyOverride != null && typeof(this.cmdArray[i].hotkeyOverride) !== "undefined") + { + if (pChar == this.cmdArray[i].hotkeyOverride) + retCode = this.cmdArray[i].retCode; + } + else if (this.cmdArray[i].itemText.length > 0 && this.cmdArray[i].itemText.charAt(0) == pChar) retCode = this.cmdArray[i].retCode; } } @@ -1705,7 +1777,12 @@ function DDFileMenuBar_getActionFromChar(pChar, pCaseSensitive) 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) + if (this.cmdArray[i].hotkeyOverride != null && typeof(this.cmdArray[i].hotkeyOverride) !== "undefined") + { + if (pChar == this.cmdArray[i].hotkeyOverride) + retCode = this.cmdArray[i].retCode; + } + else if (this.cmdArray[i].itemText.length > 0 && this.cmdArray[i].itemText.charAt(0).toUpperCase() == charUpper) retCode = this.cmdArray[i].retCode; } } @@ -1769,11 +1846,15 @@ function DDFileMenuBar_getAllActionKeysStr(pLowercase, pUppercase) // 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) +// pHotkeyOverride: Optional: A key to use for the action instead of the first character in pItemText +function DDFileMenuBarItem(pItemText, pPos, pRetCode, pHotkeyOverride) { this.itemText = pItemText; this.pos = pPos; this.retCode = pRetCode; + this.hotkeyOverride = null; + if (pHotkeyOverride != null && typeof(pHotkeyOverride) !== "undefined") + this.hotkeyOverride = pHotkeyOverride; } @@ -3409,7 +3490,7 @@ function populateFileList(pSearchMode) userInputDLA = "A"; else { - console.print("\x01n"); + console.attributes = "N"; console.crlf(); console.mnemonics(bbs.text(DirLibOrAll)); userInputDLA = console.getkeys(validInputOptions, -1, K_UPPER); @@ -4100,3 +4181,30 @@ function fileInfoSortDLTime(pA, pB) else return 0; } + +// Returns whether or not a user can download from a file directory, and writes an error if not. +// +// Parameters: +// pDirCode: The internal directory code of a file directory +// +// Return value: Boolean - Whether or not the user can download from the file directory given +function userCanDownloadFromFileArea_ShowErrorIfNot(pDirCode) +{ + var userCanDownload = file_area.dir[pDirCode].can_download; + if (!userCanDownload) + { + // The user doesn't have permission to download from this directory + //file_area.dir[pDirCode].name + var areaFullDesc = file_area.dir[pDirCode].lib_name + ": " + file_area.dir[pDirCode].description; + areaFullDesc = word_wrap(areaFullDesc, console.screen_columns-1, areaFullDesc.length).replace(/\r|\n/g, "\r\n"); + while (areaFullDesc.lastIndexOf("\r\n") == areaFullDesc.length-2) + areaFullDesc = areaFullDesc.substr(0, areaFullDesc.length-2); + console.crlf(); + console.print(areaFullDesc); + console.crlf(); + console.mnemonics(bbs.text(CantDownloadFromDir)); + console.crlf(); + console.pause(); + } + return userCanDownload; +} \ No newline at end of file diff --git a/xtrn/ddfilelister/readme.txt b/xtrn/ddfilelister/readme.txt index 5e5aea7785eb8de45510bf053270432d8182b77b..3ec5c200482015a72cc348c87ab4f04f7403612c 100644 --- a/xtrn/ddfilelister/readme.txt +++ b/xtrn/ddfilelister/readme.txt @@ -1,6 +1,6 @@ Digital Distortion File Lister - Version 2.09 - Release date: 2023-02-25 + Version 2.10 + Release date: 2023-02-27 by diff --git a/xtrn/ddfilelister/revision_history.txt b/xtrn/ddfilelister/revision_history.txt index 688287ba6f8e99ef5aecd8cd1113894c5e2dcf14..fd099bb894deacb46169b90101e959800c205f52 100644 --- a/xtrn/ddfilelister/revision_history.txt +++ b/xtrn/ddfilelister/revision_history.txt @@ -5,6 +5,7 @@ Revision History (change log) ============================= Version Date Description ------- ---- ----------- +2.10 2023-02-27 Now allows downloading a single selected file with the D key 2.09 2023-02-25 Now supports being used as a loadable module for Scan Dirs and List Files (applicable for Synchronet 3.20+) 2.08 2023-01-18 When doing a file search in multiple directories, the @@ -43,4 +44,4 @@ Version Date Description info. Fixed command bar refreshing when pressing the hotkeys. Added an option to pause after viewing a file (defaults to true). -2.00 2022-02-06 Initial version. \ No newline at end of file +2.00 2022-02-06 Initial version.