diff --git a/WHATSNEW.md b/WHATSNEW.md index 484755ab..e15f1b8e 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -9,6 +9,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * New `PV` ACS check for arbitrary user properties. See [ACS](./docs/configuration/acs.md) for details. * The `message` arg used by `msg_list` has been deprecated. Please starting using `messageIndex` for this purpose. Support for `message` will be removed in the future. * A number of new MCI codes (see [MCI](./docs/art/mci.md)) +* Added ability to export/download messages. This is enabled in the default menu. See `messageAreaViewPost` in [the default message base template](./misc/menu_templates/message_base.in.hjson) and look for the download options (`@method:addToDownloadQueue`, etc.) for details on adding to your system! ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/art/themes/luciano_blocktronics/mb_export_dl_queue.ans b/art/themes/luciano_blocktronics/mb_export_dl_queue.ans new file mode 100644 index 00000000..c02f2ec7 Binary files /dev/null and b/art/themes/luciano_blocktronics/mb_export_dl_queue.ans differ diff --git a/core/bbs.js b/core/bbs.js index e3d08ed4..90c31beb 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -13,6 +13,7 @@ const resolvePath = require('./misc_util.js').resolvePath; const UserProps = require('./user_property.js'); const SysProps = require('./system_property.js'); const SysLogKeys = require('./system_log.js'); +const UserLogNames = require('./user_log_name'); // deps const async = require('async'); @@ -209,7 +210,7 @@ function initialize(cb) { process.on('SIGINT', shutdownSystem); - require('later').date.localTime(); // use local times for later.js/scheduling + require('@breejs/later').date.localTime(); // use local times for later.js/scheduling return callback(null); }, @@ -287,6 +288,42 @@ function initialize(cb) { return callback(null); }); }, + function initUserTransferStats(callback) { + const StatLog = require('./stat_log'); + + const entries = [ + [ UserLogNames.UlFiles, [ SysProps.FileUlTodayCount, 'count' ] ], + [ UserLogNames.UlFileBytes, [ SysProps.FileUlTodayBytes, 'obj' ] ], + [ UserLogNames.DlFiles, [ SysProps.FileDlTodayCount, 'count' ] ], + [ UserLogNames.DlFileBytes, [ SysProps.FileDlTodayBytes, 'obj' ] ], + ]; + + async.each(entries, (entry, nextEntry) => { + const [ logName, [sysPropName, resultType] ] = entry; + + const filter = { + logName, + resultType, + date : moment(), + }; + + filter.logName = logName; + + StatLog.findUserLogEntries(filter, (err, stat) => { + if (!err) { + if (resultType === 'obj') { + stat = stat.reduce( (bytes, entry) => bytes + parseInt(entry.log_value) || 0, 0); + } + + StatLog.setNonPersistentSystemStat(sysPropName, stat); + } + return nextEntry(null); + }); + }, + () => { + return callback(null); + }); + }, function initMessageStats(callback) { return require('./message_area.js').startup(callback); }, diff --git a/core/client_term.js b/core/client_term.js index e961394e..4cbd603c 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -132,6 +132,9 @@ ClientTerminal.prototype.isANSI = function() { // // Reports from various terminals // + // NetRunner v2.00beta 20 + // * This version adds 256 colors and reports as "ansi-256color" + // // syncterm: // * SyncTERM // @@ -150,7 +153,7 @@ ClientTerminal.prototype.isANSI = function() { // linux: // * JuiceSSH (note: TERM=linux also) // - return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].includes(this.termType); + return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color' ].includes(this.termType); }; // :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) diff --git a/core/event_scheduler.js b/core/event_scheduler.js index 4b7da062..4a464cd8 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -8,7 +8,7 @@ const Log = require('./logger.js').log; const { Errors } = require('./enig_error.js'); const _ = require('lodash'); -const later = require('later'); +const later = require('@breejs/later'); const path = require('path'); const pty = require('node-pty'); const sane = require('sane'); diff --git a/core/file_area_web.js b/core/file_area_web.js index b6a3d8ae..95bdeb4b 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -478,6 +478,9 @@ class FileAreaWebAccess { StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1); StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes); + StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayCount, 1); + StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayBytes, dlBytes); + return callback(null, user); }, function sendEvent(user, callback) { diff --git a/core/file_transfer.js b/core/file_transfer.js index 44ebdc11..bb341051 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -544,6 +544,9 @@ exports.getModule = class TransferFileModule extends MenuModule { StatLog.incrementSystemStat(SysProps.FileDlTotalCount, downloadCount); StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, downloadBytes); + StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayCount, downloadCount); + StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayBytes, downloadBytes); + fileIds.forEach(fileId => { FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1); }); @@ -575,6 +578,9 @@ exports.getModule = class TransferFileModule extends MenuModule { StatLog.incrementSystemStat(SysProps.FileUlTotalCount, uploadCount); StatLog.incrementSystemStat(SysProps.FileUlTotalBytes, uploadBytes); + StatLog.incrementNonPersistentSystemStat(SysProps.FileUlTodayCount, uploadCount); + StatLog.incrementNonPersistentSystemStat(SysProps.FileUlTodayBytes, uploadBytes); + return cb(null); }); } diff --git a/core/fse.js b/core/fse.js index 5e018df7..ba278a01 100644 --- a/core/fse.js +++ b/core/fse.js @@ -21,17 +21,25 @@ const { isAnsi, stripAnsiControlCodes, insert } = require('./string_util.js'); +const { stripMciColorCodes } = require('./color_codes.js'); const Config = require('./config.js').get; const { getAddressedToInfo } = require('./mail_util.js'); const Events = require('./events.js'); const UserProps = require('./user_property.js'); const SysProps = require('./system_property.js'); +const FileArea = require('./file_base_area.js'); +const FileEntry = require('./file_entry.js'); +const DownloadQueue = require('./download_queue.js'); // deps const async = require('async'); const assert = require('assert'); const _ = require('lodash'); const moment = require('moment'); +const fse = require('fs-extra'); +const fs = require('graceful-fs'); +const paths = require('path'); +const sanatizeFilename = require('sanitize-filename'); exports.moduleInfo = { name : 'Full Screen Editor (FSE)', @@ -255,7 +263,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul viewModeMenuHelp : function(formData, extraArgs, cb) { self.viewControllers.footerView.setFocus(false); return self.displayHelp(cb); - } + }, + + addToDownloadQueue : (formData, extraArgs, cb) => { + this.viewControllers.footerView.setFocus(false); + return this.addToDownloadQueue(cb); + }, }; } @@ -853,7 +866,11 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); this.setHeaderText(MciViewIds.header.to, this.message.toUserName); this.setHeaderText(MciViewIds.header.subject, this.message.subject); - this.setHeaderText(MciViewIds.header.modTimestamp, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat())); + + this.setHeaderText(MciViewIds.header.modTimestamp, moment(this.message.modTimestamp).format( + this.menuConfig.config.modTimestampFormat || this.client.currentTheme.helpers.getDateTimeFormat()) + ); + this.setHeaderText(MciViewIds.header.msgNum, (this.messageIndex + 1).toString()); this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString()); @@ -901,6 +918,98 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul ); } + addToDownloadQueue(cb) { + const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); + const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); + + const msgInfo = this.getHeaderFormatObj(); + + const outputFileName = paths.join( + sysTempDownloadDir, + sanatizeFilename( + `(${msgInfo.messageId}) ${msgInfo.subject}_(${this.message.modTimestamp.format('YYYY-MM-DD')}).txt`) + ); + + async.waterfall( + [ + (callback) => { + const header = + `+${'-'.repeat(79)} +| To : ${msgInfo.toUserName} +| From : ${msgInfo.fromUserName} +| When : ${moment(this.message.modTimestamp).format('dddd, MMMM Do YYYY, h:mm:ss a (UTCZ)')} +| Subject : ${msgInfo.subject} +| ID : ${this.message.messageUuid} (${msgInfo.messageId}) ++${'-'.repeat(79)} +`; + const body = this.viewControllers.body + .getView(MciViewIds.body.message) + .getData( { forceLineTerms : true } ); + + const cleanBody = stripMciColorCodes( + stripAnsiControlCodes(body, { all : true } ) + ); + + const exportedMessage = `${header}\r\n${cleanBody}`; + + fse.mkdirs(sysTempDownloadDir, err => { + return callback(err, exportedMessage); + }); + }, + (exportedMessage, callback) => { + return fs.writeFile(outputFileName, exportedMessage, 'utf8', callback); + }, + (callback) => { + fs.stat(outputFileName, (err, stats) => { + return callback(err, stats.size); + }); + }, + (fileSize, callback) => { + const newEntry = new FileEntry({ + areaTag : sysTempDownloadArea.areaTag, + fileName : paths.basename(outputFileName), + storageTag : sysTempDownloadArea.storageTags[0], + meta : { + upload_by_username : this.client.user.username, + upload_by_user_id : this.client.user.userId, + byte_size : fileSize, + session_temp_dl : 1, // download is valid until session is over + } + }); + + newEntry.desc = `${msgInfo.messageId} - ${msgInfo.subject}`; + + newEntry.persist(err => { + if(!err) { + // queue it! + DownloadQueue.get(this.client).addTemporaryDownload(newEntry); + } + return callback(err); + }); + }, + (callback) => { + const artSpec = this.menuConfig.config.art.expToDlQueue || + Buffer.from('Exported message has been added to your download queue!'); + this.displayAsset( + artSpec, + { clearScreen : true }, + () => { + this.client.waitForKeyPress( () => { + this.redrawScreen( () => { + this.viewControllers[this.getFooterName()].setFocus(true); + return callback(null); + }); + }); + } + ); + } + ], + err => { + return cb(err); + } + ); + } + displayQuoteBuilder() { // // Clear body area diff --git a/core/message.js b/core/message.js index 73ebe761..c5ad490b 100644 --- a/core/message.js +++ b/core/message.js @@ -634,7 +634,9 @@ module.exports = class Message { self.fromUserName = msgRow.from_user_name; self.subject = msgRow.subject; self.message = msgRow.message; - self.modTimestamp = moment(msgRow.modified_timestamp); + + // We use parseZone() to *preserve* the time zone information + self.modTimestamp = moment.parseZone(msgRow.modified_timestamp); return callback(err); } diff --git a/core/msg_area_view_fse.js b/core/msg_area_view_fse.js index 1ca5617c..ed057438 100644 --- a/core/msg_area_view_fse.js +++ b/core/msg_area_view_fse.js @@ -102,11 +102,10 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { self.client.log(extraArgs, 'Missing extraArgs.menu'); return cb(null); - } + }, }); } - loadMessageByUuid(uuid, cb) { const msg = new Message(); msg.load( { uuid : uuid, user : this.client.user }, () => { diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 50de9304..0842a739 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -292,10 +292,22 @@ const PREDEFINED_MCI_GENERATORS = { TP : function totalMessagesOnSystem() { // Obv/2 return StatLog.getFriendlySystemStat(SysProps.MessageTotalCount, 0); }, + FT : function totalUploadsToday() { // Obv/2 + return StatLog.getFriendlySystemStat(SysProps.FileUlTodayCount, 0); + }, + FB : function totalUploadBytesToday() { + const byteSize = StatLog.getSystemStatNum(SysProps.FileUlTodayBytes); + return formatByteSize(byteSize, true); // true=withAbbr + }, + DD : function totalDownloadsToday() { // iNiQUiTY + return StatLog.getFriendlySystemStat(SysProps.FileDlTodayCount, 0); + }, + DB : function totalDownloadBytesToday() { + const byteSize = StatLog.getSystemStatNum(SysProps.FileDlTodayBytes); + return formatByteSize(byteSize, true); // true=withAbbr + }, // :TODO: NT - New users today (Obv/2) - // :TODO: FT - Files uploaded/added *today* (Obv/2) - // :TODO: DD - Files downloaded *today* (iNiQUiTY) // :TODO: LC - name of last caller to system (Obv/2) // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) // :TODO: ?? - Total users on system diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 2bb2df46..068ff111 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -30,7 +30,7 @@ const _ = require('lodash'); const paths = require('path'); const async = require('async'); const fs = require('graceful-fs'); -const later = require('later'); +const later = require('@breejs/later'); const temptmp = require('temptmp').createTrackedSession('ftn_bso'); const assert = require('assert'); const sane = require('sane'); diff --git a/core/stat_log.js b/core/stat_log.js index 55c4620a..fee54a0f 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -259,75 +259,18 @@ class StatLog { ); } - /* - Find System Log entries by |filter|: - - filter.logName (required) - filter.resultType = (obj) | count - where obj contains timestamp and log_value - filter.limit - filter.date - exact date to filter against - filter.order = (timestamp) | timestamp_asc | timestamp_desc | random - */ + // + // Find System Log entry(s) by |filter|: + // + // - logName: Name of log (required) + // - resultType: 'obj' | 'count' (default='obj') + // - limit: Limit returned results + // - date: exact date to filter against + // - order: 'timestamp' | 'timestamp_asc' | 'timestamp_desc' | 'random' + // (default='timestamp') + // findSystemLogEntries(filter, cb) { - filter = filter || {}; - if(!_.isString(filter.logName)) { - return cb(Errors.MissingParam('filter.logName is required')); - } - - filter.resultType = filter.resultType || 'obj'; - filter.order = filter.order || 'timestamp'; - - let sql; - if('count' === filter.resultType) { - sql = - `SELECT COUNT() AS count - FROM system_event_log`; - } else { - sql = - `SELECT timestamp, log_value - FROM system_event_log`; - } - - sql += ' WHERE log_name = ?'; - - if(filter.date) { - filter.date = moment(filter.date); - sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format('YYYY-MM-DD')}")`; - } - - if('count' !== filter.resultType) { - switch(filter.order) { - case 'timestamp' : - case 'timestamp_asc' : - sql += ' ORDER BY timestamp ASC'; - break; - - case 'timestamp_desc' : - sql += ' ORDER BY timestamp DESC'; - break; - - case 'random' : - sql += ' ORDER BY RANDOM()'; - break; - } - } - - if(_.isNumber(filter.limit) && 0 !== filter.limit) { - sql += ` LIMIT ${filter.limit}`; - } - - sql += ';'; - - if('count' === filter.resultType) { - sysDb.get(sql, [ filter.logName ], (err, row) => { - return cb(err, row ? row.count : 0); - }); - } else { - sysDb.all(sql, [ filter.logName ], (err, rows) => { - return cb(err, rows); - }); - } + return this._findLogEntries('system_event_log', filter, cb); } getSystemLogEntries(logName, order, limit, cb) { @@ -389,6 +332,22 @@ class StatLog { return cb(null); } + // + // Find User Log entry(s) by |filter|: + // + // - logName: Name of log (required) + // - userId: User ID in which to restrict entries to (missing=all) + // - sessionId: Session ID in which to restrict entries to (missing=any) + // - resultType: 'obj' | 'count' (default='obj') + // - limit: Limit returned results + // - date: exact date to filter against + // - order: 'timestamp' | 'timestamp_asc' | 'timestamp_desc' | 'random' + // (default='timestamp') + // + findUserLogEntries(filter, cb) { + return this._findLogEntries('user_event_log', filter, cb); + } + _refreshSystemStat(statName) { switch (statName) { case SysProps.SystemLoadStats : @@ -431,6 +390,75 @@ class StatLog { // :TODO: log me }); } + + _findLogEntries(logTable, filter, cb) { + filter = filter || {}; + if(!_.isString(filter.logName)) { + return cb(Errors.MissingParam('filter.logName is required')); + } + + filter.resultType = filter.resultType || 'obj'; + filter.order = filter.order || 'timestamp'; + + let sql; + if('count' === filter.resultType) { + sql = + `SELECT COUNT() AS count + FROM ${logTable}`; + } else { + sql = + `SELECT timestamp, log_value + FROM ${logTable}`; + } + + sql += ' WHERE log_name = ?'; + + if (_.isNumber(filter.userId)) { + sql += ` AND user_id = ${filter.userId}`; + } + + if (filter.sessionId) { + sql += ` AND session_id = ${filter.sessionId}`; + } + + if(filter.date) { + filter.date = moment(filter.date); + sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format('YYYY-MM-DD')}")`; + } + + if('count' !== filter.resultType) { + switch(filter.order) { + case 'timestamp' : + case 'timestamp_asc' : + sql += ' ORDER BY timestamp ASC'; + break; + + case 'timestamp_desc' : + sql += ' ORDER BY timestamp DESC'; + break; + + case 'random' : + sql += ' ORDER BY RANDOM()'; + break; + } + } + + if(_.isNumber(filter.limit) && 0 !== filter.limit) { + sql += ` LIMIT ${filter.limit}`; + } + + sql += ';'; + + if('count' === filter.resultType) { + sysDb.get(sql, [ filter.logName ], (err, row) => { + return cb(err, row ? row.count : 0); + }); + } else { + sysDb.all(sql, [ filter.logName ], (err, rows) => { + return cb(err, rows); + }); + } + } } module.exports = new StatLog(); diff --git a/core/system_log.js b/core/system_log.js index e753c68b..33f2588c 100644 --- a/core/system_log.js +++ b/core/system_log.js @@ -6,6 +6,6 @@ // module.exports = { UserAddedRumorz : 'system_rumorz', - UserLoginHistory : 'user_login_history', + UserLoginHistory : 'user_login_history', // JSON object }; diff --git a/core/system_property.js b/core/system_property.js index a7ebae9d..40c501aa 100644 --- a/core/system_property.js +++ b/core/system_property.js @@ -17,6 +17,11 @@ module.exports = { FileDlTotalCount : 'dl_total_count', FileDlTotalBytes : 'dl_total_bytes', + FileUlTodayCount : 'ul_today_count', // non-persistent + FileUlTodayBytes : 'ul_today_bytes', // non-persistent + FileDlTodayCount : 'dl_today_count', // non-persistent + FileDlTodayBytes : 'dl_today_bytes', // non-persistent + MessageTotalCount : 'message_post_total_count', // total non-private messages on the system; non-persistent MessagesToday : 'message_post_today', // non-private messages posted/imported today; non-persistent diff --git a/docs/art/mci.md b/docs/art/mci.md index 394f39ee..235a92a6 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -88,9 +88,13 @@ There are many predefined MCI codes that can be used anywhere on the system (pla | `SU` | Total uploads, system wide | | `SP` | Total uploaded amount, system wide (formatted to appropriate bytes/megs/etc.) | | `TF` | Total number of files on the system | -| `TB` | Total amount of files on the system (formatted to appropriate bytes/megs/gigs/etc.) | +| `TB` | Total file base size (formatted to appropriate bytes/megs/gigs/etc.) | | `TP` | Total messages posted/imported to the system *currently* | | `PT` | Total messages posted/imported to the system *today* | +| `FT` | Total number of uploads to the system *today* | +| `FB` | Total upload amount *today* (formatted to appropriate bytes/megs/etc. ) | +| `DD` | Total number of downloads from the system *today* | +| `DB` | Total download amount *today* (formatted to appropriate bytes/megs/etc. ) | | `MB` | System memory | | `MF` | System _free_ memory | | `LA` | System load average (e.g. 0.25)
(Not available for all platforms) | @@ -113,32 +117,53 @@ Some additional special case codes also exist: the time so also check out [core/predefined_mci.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) for a full listing. -:note: Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, etc. +:memo: Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, etc. ## Views A **View** is a control placed on a **form** that can display variable data or collect input. One example of a View is a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu. -| Code | Name | Description | -|------|----------------------|------------------| -| `TL` | Text Label | Displays text | -| `ET` | Edit Text | Collect user input | -| `ME` | Masked Edit Text | Collect user input using a *mask* | -| `MT` | Multi Line Text Edit | Multi line edit control | -| `BT` | Button | A button | -| `VM` | Vertical Menu | A vertical menu aka a vertical lightbar | -| `HM` | Horizontal Menu | A horizontal menu aka a horizontal lightbar | -| `SM` | Spinner Menu | A spinner input control | -| `TM` | Toggle Menu | A toggle menu commonly used for Yes/No style input | -| `KE` | Key Entry | A *single* key input control | +| Code | Name | Description | Notes | +|------|----------------------|------------------|-------| +| `TL` | Text Label | Displays text | Static content | +| `ET` | Edit Text | Collect user input | Single line entry | +| `ME` | Masked Edit Text | Collect user input using a *mask* | See **Mask Edits** below | +| `MT` | Multi Line Text Edit | Multi line edit control | Used for FSE, display of FILE_ID.DIZ, etc. | +| `BT` | Button | A button | ...it's a button | +| `VM` | Vertical Menu | A vertical menu | AKA a vertical lightbar; Useful for lists | +| `HM` | Horizontal Menu | A horizontal menu | AKA a horizontal lightbar | +| `SM` | Spinner Menu | A spinner input control | Select *one* from multiple options | +| `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input | +| `KE` | Key Entry | A *single* key input control | Think hotkeys | -:information_source: Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to -see additional information. +:information_source: Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to see additional information. + +### Mask Edits +Mask Edits (`%ME`) use the special `maskPattern` property to control a _mask_. This can be useful for gathering dates, phone numbers, so on. + +`maskPattern`'s can be composed of the following characters: +* `#`: Numeric 0-9 +* `A`: Alpha a-z, A-Z +* `@`: Alphanumeric (combination of the previous patterns) +* `&`: Any "printable" character + +Any other characters are literals. + +An example of a mask for a date may look like this: `##/##/####`. + +Additionally, the following theme stylers can be applied: +* `styleSGR1`: Controls literal character colors for non-focused controls +* `styleSGR2`: Controls literal character colors for focused controls +* `styleSGR3`: Controls fill colors (characters that have not yet received input). + +All of the style properties can take pipe codes such as `|00|08`. ### View Identifiers As mentioned above, MCI codes can (and often should) be explicitly tied to a *View Identifier*. Simply speaking this is a number representing the particular view. These can be useful to reference in code, apply themes, etc. +A view ID is tied to a MCI code by specifying it after the code. For example: `%VM1` or `%SM10`. + ## Properties & Theming Predefined MCI codes and other Views can have properties set via `menu.hjson` and further *themed* via `theme.hjson`. See [Themes](themes.md) for more information on this subject. diff --git a/docs/modding/show-art.md b/docs/modding/show-art.md index 5ffce10c..69be395d 100644 --- a/docs/modding/show-art.md +++ b/docs/modding/show-art.md @@ -1,6 +1,6 @@ --- layout: page -title: User List +title: The Show Art Module --- ## The Show Art Module The built in `show_art` module add some advanced ways in which you can configure your system to display art assets beyond what a standard menu entry can provide. For example, based on user selection of a file or message base area. diff --git a/misc/menu_templates/message_base.in.hjson b/misc/menu_templates/message_base.in.hjson index 51bf4a19..367e1903 100644 --- a/misc/menu_templates/message_base.in.hjson +++ b/misc/menu_templates/message_base.in.hjson @@ -477,10 +477,11 @@ module: msg_area_view_fse config: { art: { - header: MSGVHDR - body: MSGBODY + header: MSGVHDR + body: MSGBODY footerView: MSGVFTR - help: MSGVHLP + help: MSGVHLP + expToDlQueue: mb_export_dl_queue }, editorMode: view editorType: area @@ -525,7 +526,7 @@ mci: { HM1: { // :TODO: (#)Jump/(L)Index (msg list)/Last - items: [ "prev", "next", "reply", "quit", "help" ] + items: [ "prev", "next", "reply", "quit", "download", "help" ] focusItemIndex: 1 } } @@ -552,6 +553,10 @@ } { value: { 1: 4 } + action: @method:addToDownloadQueue + } + { + value: { 1: 5 } action: @method:viewModeMenuHelp } ] @@ -576,6 +581,10 @@ keys: [ "escape", "q", "shift + q" ] action: @systemMethod:prevMenu } + { + keys: [ "d", "shift + d" ] + action: @method:addToDownloadQueue + } { keys: [ "?" ] action: @method:viewModeMenuHelp diff --git a/package.json b/package.json index f0563e95..bdc62adf 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "iconv-lite": "^0.6.2", "ini-config-parser": "^1.0.4", "inquirer": "7.3.3", - "later": "1.2.0", + "@breejs/later" : "^4.0.2", "lodash": "4.17.20", "lru-cache": "^5.1.1", "mime-types": "2.1.27", diff --git a/yarn.lock b/yarn.lock index 2929c695..0ea64d4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,6 +23,11 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@breejs/later@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@breejs/later/-/later-4.0.2.tgz#38c85cc98b717c7a196f87238090adaea01f8c9e" + integrity sha512-EN0SlbyYouBdtZis1htdsgGlwFePzkXPwdIeqaBaavxkJT1G2/bitc2LSixjv45z2njXslxlJI1mW2O/Gmrb+A== + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -1392,11 +1397,6 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== -later@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/later/-/later-1.2.0.tgz#f2cf6c4dd7956dd2f520adf0329836e9876bad0f" - integrity sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8= - levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"