enigma-bbs/mods/file_area_list.js

623 lines
17 KiB
JavaScript

/* jslint node: true */
'use strict';
// ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController;
const ansi = require('../core/ansi_term.js');
const theme = require('../core/theme.js');
const FileEntry = require('../core/file_entry.js');
const stringFormat = require('../core/string_format.js');
const createCleanAnsi = require('../core/string_util.js').createCleanAnsi;
const FileArea = require('../core/file_base_area.js');
const Errors = require('../core/enig_error.js').Errors;
const ArchiveUtil = require('../core/archive_util.js');
const Config = require('../core/config.js').config;
const DownloadQueue = require('../core/download_queue.js');
const FileAreaWeb = require('../core/file_area_web.js');
const FileBaseFilters = require('../core/file_base_filter.js');
const cleanControlCodes = require('../core/string_util.js').cleanControlCodes;
// deps
const async = require('async');
const _ = require('lodash');
const moment = require('moment');
exports.moduleInfo = {
name : 'File Area List',
desc : 'Lists contents of file an file area',
author : 'NuSkooler',
};
const FormIds = {
browse : 0,
details : 1,
detailsGeneral : 2,
detailsNfo : 3,
detailsFileList : 4,
};
const MciViewIds = {
browse : {
desc : 1,
navMenu : 2,
customRangeStart : 10, // 10+ = customs
},
details : {
navMenu : 1,
infoXyTop : 2, // %XY starting position for info area
infoXyBottom : 3,
customRangeStart : 10, // 10+ = customs
},
detailsGeneral : {
customRangeStart : 10, // 10+ = customs
},
detailsNfo : {
nfo : 1,
customRangeStart : 10, // 10+ = customs
},
detailsFileList : {
fileList : 1,
customRangeStart : 10, // 10+ = customs
},
};
exports.getModule = class FileAreaList extends MenuModule {
constructor(options) {
super(options);
if(options.extraArgs) {
this.filterCriteria = options.extraArgs.filterCriteria;
}
this.dlQueue = new DownloadQueue(this.client);
if(!this.filterCriteria) {
this.filterCriteria = FileBaseFilters.getActiveFilter(this.client);
}
if(_.isString(this.filterCriteria)) {
this.filterCriteria = JSON.parse(this.filterCriteria);
}
if(_.has(options, 'lastMenuResult.value')) {
this.lastMenuResultValue = options.lastMenuResult.value;
}
this.menuMethods = {
nextFile : (formData, extraArgs, cb) => {
if(this.fileListPosition + 1 < this.fileList.length) {
this.fileListPosition += 1;
return this.displayBrowsePage(true, cb); // true=clerarScreen
}
return cb(null);
},
prevFile : (formData, extraArgs, cb) => {
if(this.fileListPosition > 0) {
--this.fileListPosition;
return this.displayBrowsePage(true, cb); // true=clearScreen
}
return cb(null);
},
viewDetails : (formData, extraArgs, cb) => {
this.viewControllers.browse.setFocus(false);
return this.displayDetailsPage(cb);
},
detailsQuit : (formData, extraArgs, cb) => {
this.viewControllers.details.setFocus(false);
return this.displayBrowsePage(true, cb); // true=clearScreen
},
toggleQueue : (formData, extraArgs, cb) => {
this.dlQueue.toggle(this.currentFileEntry);
this.updateQueueIndicator();
return cb(null);
},
showWebDownloadLink : (formData, extraArgs, cb) => {
return this.fetchAndDisplayWebDownloadLink(cb);
}
};
}
enter() {
super.enter();
}
leave() {
super.leave();
}
getSaveState() {
return {
fileList : this.fileList,
fileListPosition : this.fileListPosition,
};
}
restoreSavedState(savedState) {
if(savedState) {
this.fileList = savedState.fileList;
this.fileListPosition = savedState.fileListPosition;
}
}
updateFileEntryWithMenuResult(cb) {
if(!this.lastMenuResultValue) {
return cb(null);
}
if(_.isNumber(this.lastMenuResultValue.rating)) {
const fileId = this.fileList[this.fileListPosition];
FileEntry.persistUserRating(fileId, this.client.user.userId, this.lastMenuResultValue.rating, err => {
if(err) {
this.client.log.warn( { error : err.message, fileId : fileId }, 'Failed to persist file rating' );
}
return cb(null);
});
} else {
return cb(null);
}
}
initSequence() {
const self = this;
async.series(
[
function preInit(callback) {
return self.updateFileEntryWithMenuResult(callback);
},
function beforeArt(callback) {
return self.beforeArt(callback);
},
function display(callback) {
return self.displayBrowsePage(false, callback);
}
],
() => {
self.finishedLoading();
}
);
}
populateCurrentEntryInfo(cb) {
const config = this.menuConfig.config;
const currEntry = this.currentFileEntry;
const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD';
const area = FileArea.getFileAreaByTag(currEntry.areaTag);
const hashTagsSep = config.hashTagsSep || ', ';
const isQueuedIndicator = config.isQueuedIndicator || 'Y';
const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N';
const entryInfo = this.currentFileEntry.entryInfo = {
fileId : currEntry.fileId,
areaTag : currEntry.areaTag,
areaName : area.name || 'N/A',
areaDesc : area.desc || 'N/A',
fileSha256 : currEntry.fileSha256,
fileName : currEntry.fileName,
desc : currEntry.desc || '',
descLong : currEntry.descLong || '',
userRating : currEntry.userRating,
uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat),
hashTags : Array.from(currEntry.hashTags).join(hashTagsSep),
isQueued : this.dlQueue.isQueued(this.currentFileEntry) ? isQueuedIndicator : isNotQueuedIndicator,
webDlLink : '', // :TODO: fetch web any existing web d/l link
webDlExpire : '', // :TODO: fetch web d/l link expire time
};
//
// We need the entry object to contain meta keys even if they are empty as
// consumers may very likely attempt to use them
//
const metaValues = FileEntry.getWellKnownMetaValues();
metaValues.forEach(name => {
const value = !_.isUndefined(currEntry.meta[name]) ? currEntry.meta[name] : 'N/A';
entryInfo[_.camelCase(name)] = value;
});
if(entryInfo.archiveType) {
entryInfo.archiveTypeDesc = _.has(Config, [ 'archives', 'formats', entryInfo.archiveType, 'desc' ]) ?
Config.archives.formats[entryInfo.archiveType].desc :
entryInfo.archiveType;
} else {
entryInfo.archiveTypeDesc = 'N/A';
}
entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported
entryInfo.hashTags = entryInfo.hashTags || '(none)';
// create a rating string, e.g. "**---"
const userRatingTicked = config.userRatingTicked || '*';
const userRatingUnticked = config.userRatingUnticked || '';
entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe!
entryInfo.userRatingString = new Array(entryInfo.userRating + 1).join(userRatingTicked);
if(entryInfo.userRating < 5) {
entryInfo.userRatingString += new Array( (5 - entryInfo.userRating) + 1).join(userRatingUnticked);
}
FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => {
if(err) {
entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated';
entryInfo.webDlExpire = '';
} else {
const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
entryInfo.webDlLink = serveItem.url;
entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat);
}
return cb(null);
});
}
populateCustomLabels(category, startId) {
return this.updateCustomViewTextsWithFilter(category, startId, this.currentFileEntry.entryInfo);
}
displayArtAndPrepViewController(name, options, cb) {
const self = this;
const config = this.menuConfig.config;
async.waterfall(
[
function readyAndDisplayArt(callback) {
if(options.clearScreen) {
self.client.term.rawWrite(ansi.clearScreen());
}
theme.displayThemedAsset(
config.art[name],
self.client,
{ font : self.menuConfig.font, trailingLF : false },
(err, artData) => {
return callback(err, artData);
}
);
},
function prepeareViewController(artData, callback) {
if(_.isUndefined(self.viewControllers[name])) {
const vcOpts = {
client : self.client,
formId : FormIds[name],
};
if(!_.isUndefined(options.noInput)) {
vcOpts.noInput = options.noInput;
}
const vc = self.addViewController(name, new ViewController(vcOpts));
if('details' === name) {
try {
self.detailsInfoArea = {
top : artData.mciMap.XY2.position,
bottom : artData.mciMap.XY3.position,
};
} catch(e) {
return callback(Errors.DoesNotExist('Missing XY2 and XY3 position indicators!'));
}
}
const loadOpts = {
callingMenu : self,
mciMap : artData.mciMap,
formId : FormIds[name],
};
return vc.loadFromMenuConfig(loadOpts, callback);
}
self.viewControllers[name].setFocus(true);
return callback(null);
},
],
err => {
return cb(err);
}
);
}
displayBrowsePage(clearScreen, cb) {
const self = this;
async.series(
[
function prepArtAndViewController(callback) {
return self.displayArtAndPrepViewController('browse', { clearScreen : clearScreen }, callback);
},
function fetchEntryData(callback) {
if(self.fileList) {
return callback(null);
}
return self.loadFileIds(false, callback); // false=do not force
},
function loadCurrentFileInfo(callback) {
self.currentFileEntry = new FileEntry();
self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => {
if(err) {
return callback(err);
}
return self.populateCurrentEntryInfo(callback);
});
},
function populateViews(callback) {
if(_.isString(self.currentFileEntry.desc)) {
const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc);
if(descView) {
createCleanAnsi(
self.currentFileEntry.desc,
{ height : self.client.termHeight, width : descView.dimens.width },
cleanDesc => {
// :TODO: use cleanDesc -- need to finish createCleanAnsi() !!
//descView.setText(cleanDesc);
descView.setText( self.currentFileEntry.desc );
self.updateQueueIndicator();
self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart);
return callback(null);
}
);
}
} else {
self.updateQueueIndicator();
self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart);
return callback(null);
}
}
],
err => {
if(cb) {
return cb(err);
}
}
);
}
displayDetailsPage(cb) {
const self = this;
async.series(
[
function prepArtAndViewController(callback) {
return self.displayArtAndPrepViewController('details', { clearScreen : true }, callback);
},
function populateViews(callback) {
self.populateCustomLabels('details', MciViewIds.details.customRangeStart);
return callback(null);
},
function prepSection(callback) {
return self.displayDetailsSection('general', false, callback);
},
function listenNavChanges(callback) {
const navMenu = self.viewControllers.details.getView(MciViewIds.details.navMenu);
navMenu.setFocusItemIndex(0);
navMenu.on('index update', index => {
const sectionName = {
0 : 'general',
1 : 'nfo',
2 : 'fileList',
}[index];
if(sectionName) {
self.displayDetailsSection(sectionName, true);
}
});
return callback(null);
}
],
err => {
return cb(err);
}
);
}
fetchAndDisplayWebDownloadLink(cb) {
const self = this;
async.series(
[
function generateLinkIfNeeded(callback) {
if(self.currentFileEntry.webDlExpireTime < moment()) {
return callback(null);
}
const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes');
FileAreaWeb.createAndServeTempDownload(
self.client,
self.currentFileEntry,
{ expireTime : expireTime },
(err, url) => {
if(err) {
return callback(err);
}
self.currentFileEntry.webDlExpireTime = expireTime;
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
self.currentFileEntry.entryInfo.webDlLink = url;
self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat);
return callback(null);
}
);
},
function updateActiveViews(callback) {
self.updateCustomViewTextsWithFilter(
'browse',
MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo,
{ filter : [ '{webDlLink}', '{webDlExpire}' ] }
);
return callback(null);
}
],
err => {
return cb(err);
}
);
}
updateQueueIndicator() {
const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y';
const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N';
this.currentFileEntry.entryInfo.isQueued = stringFormat(
this.dlQueue.isQueued(this.currentFileEntry) ?
isQueuedIndicator :
isNotQueuedIndicator
);
this.updateCustomViewTextsWithFilter(
'browse',
MciViewIds.browse.customRangeStart,
this.currentFileEntry.entryInfo,
{ filter : [ '{isQueued}' ] }
);
}
cacheArchiveEntries(cb) {
// check cache
if(this.currentFileEntry.archiveEntries) {
return cb(null, 'cache');
}
const areaInfo = FileArea.getFileAreaByTag(this.currentFileEntry.areaTag);
if(!areaInfo) {
return cb(Errors.Invalid('Invalid area tag'));
}
const filePath = this.currentFileEntry.filePath;
const archiveUtil = ArchiveUtil.getInstance();
archiveUtil.listEntries(filePath, this.currentFileEntry.entryInfo.archiveType, (err, entries) => {
if(err) {
return cb(err);
}
this.currentFileEntry.archiveEntries = entries;
return cb(null, 're-cached');
});
}
populateFileListing() {
const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList);
if(this.currentFileEntry.entryInfo.archiveType) {
this.cacheArchiveEntries( (err, cacheStatus) => {
if(err) {
// :TODO: Handle me!!!
fileListView.setItems( [ 'Failed getting file listing' ] ); // :TODO: make this not suck
return;
}
if('re-cached' === cacheStatus) {
const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here?
const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat;
fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) );
fileListView.setFocusItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(focusFileListEntryFormat, entry) ) );
fileListView.redraw();
}
});
} else {
fileListView.setItems( [ stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ) ] );
}
}
displayDetailsSection(sectionName, clearArea, cb) {
const self = this;
const name = `details${_.upperFirst(sectionName)}`;
async.series(
[
function detachPrevious(callback) {
if(self.lastDetailsViewController) {
self.lastDetailsViewController.detachClientEvents();
}
return callback(null);
},
function prepArtAndViewController(callback) {
function gotoTopPos() {
self.client.term.rawWrite(ansi.goto(self.detailsInfoArea.top[0], 1));
}
gotoTopPos();
if(clearArea) {
self.client.term.rawWrite(ansi.reset());
let pos = self.detailsInfoArea.top[0];
const bottom = self.detailsInfoArea.bottom[0];
while(pos++ <= bottom) {
self.client.term.rawWrite(ansi.eraseLine() + ansi.down());
}
gotoTopPos();
}
return self.displayArtAndPrepViewController(name, { clearScreen : false, noInput : true }, callback);
},
function populateViews(callback) {
self.lastDetailsViewController = self.viewControllers[name];
switch(sectionName) {
case 'nfo' :
{
const nfoView = self.viewControllers.detailsNfo.getView(MciViewIds.detailsNfo.nfo);
if(nfoView) {
nfoView.setText(self.currentFileEntry.entryInfo.descLong);
}
}
break;
case 'fileList' :
self.populateFileListing();
break;
}
self.populateCustomLabels(name, MciViewIds[name].customRangeStart);
return callback(null);
}
],
err => {
if(cb) {
return cb(err);
}
}
);
}
loadFileIds(force, cb) {
if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) {
this.fileListPosition = 0;
FileEntry.findFiles(this.filterCriteria, (err, fileIds) => {
this.fileList = fileIds;
return cb(err);
});
}
}
};