The problem
I'm using an external program to gather some specific information from file objects, archives etc which I need to display in a column so the first thing I thought of was dumping it in a descript.ion file and use the metadata.other.userdescription. The file was written in the proper format and was working however I've ran into some problems with its behavior:
- Dopus wouldn't reread the descript.ion file every time that one was written to displaying older cached values, even when forcing refresh via F5
- ..or worse sometimes it decided to randomly overwrite my descript.ion with the old cached values for no apparent reason
- Those things were triggered by navigating to and away from the folder
The solution
That's why I started this mini-project which illustrates how to add your custom descript.ion like column.
Here are the gains from such an approach:
- Comments are Read-only and will never be changed or deleted on dopus' whims
- Changes are picked up the moment the description file is modified.
- Results are cached for the session speeding up access - even between listers and tabs
- Coexistence with NTFS comments enabled as userdescription.
- Virtually unlimited columns based on different descript.ion style files
- Possibility to extend as multi-column so you can store more data ala csv in a single description file (not illustrated here)
Getting started
First let's look at a descript.ion file dopus generates when setting comments on files and NTFS comments are disabled. Those are saved in UTF-8 encoding without BOM (having a byte order mark doesn't hurt as opus ignores it when reading). You might want to stick to this format when generating your files:
<encoding#> UTF-8
item_without_space.rar Item description bla bla bla
"Item With Spaces.doc" Description of this thing as well
It resides in the directory which CONTAINS the files and folders to be described.
- Now we look at the add-in code which initializes our new column: DescriptionX in this case:
// Set the script type to JScript to use this script
// The OnInit function is called by Directory Opus to initialize the script add-in
function OnInit(initData) {
// Provide basic information about the script by initializing the properties of the ScriptInitData object
initData.name = "DescriptionX";
initData.desc = "Descript.ion-like data loader";
initData.copyright = "Devocalypse";
initData.default_enable = true;
// Create a new ScriptColumn object and initialize it to add the column to Opus
var col1 = initData.AddColumn();
col1.name = "DescriptionX";
col1.method = "DescriptionX";
col1.label = "DescriptionX";
col1.autogroup = false;
col1.namerefresh = true;
col1.justify = "left";
col1.match.push_back("Yes");
col1.match.push_back("No");
}
function DescriptionX(scriptColData) {
//define file path - changing the last part allow you to have as many lists as needed
var infoFilePath = DOpus.FSUtil.NewPath(scriptColData.item.path + "\\descript.ion");
//abort if no file
if (!DOpus.FSUtil.Exists(infoFilePath)) return;
//load or use available
var map = DOpus.Vars.Exists(infoFilePath) ? DOpus.Vars.Get(infoFilePath) : LoadData(infoFilePath);
//force reload if description file has been modified since last load
//trivia: comparator fails if both are not cast as strings
if (String(DOpus.FSUtil.GetItem(infoFilePath).modify_utc) != String(map('~DateTimeUtc'))) {
DOpus.Output("Cache is invalidated. Description source was modified");
map = LoadData(infoFilePath);
}
//get data for item if present in the list
if (map.exists(scriptColData.item.name)) {
scriptColData.value = map(scriptColData.item.name);
} else {
//DOpus.Output("Not in map: " + scriptColData.item.name);
}
}
function LoadData(descriptionFile) {
DOpus.Output("Reloading: " + descriptionFile);
var file = DOpus.FSUtil.OpenFile(descriptionFile);
var map = DOpus.Create().Map();
if ((file) && (file.error == 0)) {
var blob = file.Read(file.size);
file.close();
var str = DOpus.Create().StringTools().Decode(blob, "utf-8");
var ar = str.split("\n");
//comment out the following lines if you don't write the <encoding#> UTF-8 header
//ar.splice(0, 1); //remove unicode designation: <encoding#> UTF-8
ar.shift(); //remove unicode designation: <encoding#> UTF-8
//map for hashtable style dictionary lookups
ar.forEach(function (key, val) {
if (val.length < 1) return;
if (val.startsWith('"')) {
//filename contains spaces and is quoted
var parts = val.split('"');
map(parts[1]) = parts[2];
} else {
//filename has no spaces and is not quoted
var parts = val.splitLimit(' ', 1);
map(parts[0]) = parts[1];
}
});
DOpus.Output("Added descriptions: " + map.count);
}
//add a timestamp for last updated
map('~DateTimeUtc') = DOpus.FSUtil.GetItem(descriptionFile).modify_utc;
//cache in global vars
DOpus.Vars.Set(descriptionFile, map);
return map;
}
Prototypes and helper functions
// <editor-fold desc="Prototypes">
String.prototype.splitLimit = function (delim, count) {
var parts = this.split(delim);
var tail = parts.slice(count).join(delim);
var result = parts.slice(0, count);
result.push(tail);
return result;
};
Object.prototype.forEach = function (func, context) {
var value;
context = context || this; //apply the function to 'this' by default
for (key in this) {
if (this.hasOwnProperty(key)) { //to rule out inherited properties
value = this[key];
func.call(context, key, value);
}
}
};
String.prototype.contains = function (str, ignoreCase) {
return (ignoreCase ? this.toUpperCase() : this).indexOf(ignoreCase ? str.toUpperCase() : str) >= 0;
};
String.prototype.startsWith = function (str, ignoreCase) {
return (ignoreCase ? this.toUpperCase() : this).indexOf(ignoreCase ? str.toUpperCase() : str) == 0;
};
String.prototype.endsWith = function (str, ignoreCase) {
return (ignoreCase ? this.toUpperCase() : this).slice(-str.length) == (ignoreCase ? str.toUpperCase() : str);
};
// </editor-fold>
Extras
Button script to force a reload even if the data is fresh
function OnClick(clickData) {
var command = DOpus.NewCommand;
var descfile = clickData.func.sourcetab.path + "\\descript.ion";
DOpus.Output(descfile);
if (DOpus.Vars.Exists(descfile)) {
var map = DOpus.Vars.Get(descfile);
DOpus.Vars.Delete(descfile);
DOpus.Output("Cleared " + descfile + " data. Had " + map.length + " items.");
command.RunCommand("Go REFRESH");
} else {
DOpus.Output("Variable " + descfile + " is not set.");
}
}
Download full add-in
DescriptionX.zip (2.3 KB)