/*
* This file is part of the Companion project
* Copyright (c) 2021 Bitfocus AS
*
* This program is free software.
* You should have received a copy of the MIT licence as well as the Bitfocus
* Individual Contributor License Agreement for companion along with
* this program.
*
* You can be released from the requirements of the license by purchasing
* a commercial license. Buying such a license is mandatory as soon as you
* develop commercial activities involving the Companion software without
* disclosing the source code of your own applications.
*
*/
const CoreBase = require('../Core/Base');
const shortid = require('shortid');
const { cloneDeep } = require('lodash');
/**
* Abstract class to be extended and used by bank controllers to track their items.
* See {@link BankActionItems} and {@link BankFeedbackItems}
*
* @extends CoreBase
* @author Håkon Nessjøen <haakon@bitfocus.io>
* @author Keith Rocheck <keith.rocheck@gmail.com>
* @author William Viker <william@bitfocus.io>
* @since 2.2.0
* @abstract
*/
class BankItemsBase extends CoreBase {
/**
* The parent controller
* @type {(BankActionController|BankFeedbackController)}
* @access protected
*/
controller = null;
/**
* The database to use load/save the items
* @type {string}
* @access protected
*/
dbKey = null;
/**
* Nested arrays for the definitions from the instances: <code>[instance_id][item_id]</code>
* @type {Object.<string,BankItemDefinition[]>}
* @access protected
*/
definitions = {};
/**
* Nested arrays containing the items in the banks: <code>[page][bank][item]</code>
* Also is the data that is stored to the DB.
* @type {Object.<number,Array.<number,Object.<number,BankItem>>>}
* @access protected
*/
items = null;
/**
* Creates an item base object
* @param {Registry} registry - the core registry
* @param {(BankActionController|BankFeedbackController)} controller - the item's parent controller
* @param {string} logSource - module name to be used in logs
* @param {string} dbKey - the key to fetch from the database
*/
constructor(registry, controller, logSource, dbKey) {
super(registry, logSource);
this.controller = controller;
this.dbKey = dbKey;
this.items = this.db().getKey(this.dbKey, {});
}
/**
* Add an item to a bank
* @param {number} page - the bank's page
* @param {number} bank - the bank number
* @param {string} item - item information in form: `"instance_id:type"`
* @access public
*/
addItem(page, bank, item) {
this.checkBankExists(page, bank);
const s = item.split(/:/);
let newItem = {
'id': shortid.generate(),
'label': item,
'type': s[1],
'instance': s[0],
'options': {}
};
if (this.definitions[s[0]] !== undefined && this.definitions[s[0]][s[1]] !== undefined) {
let definition = this.definitions[s[0]][s[1]];
if (definition.options !== undefined && definition.options.length > 0) {
for(let j in definition.options) {
let opt = definition.options[j];
newItem.options[opt.id] = opt.default;
}
}
}
this.items[page][bank].push(newItem);
this.subscribeItem(newItem);
}
/**
* Add item to a bank via a client socket
* @param {IO.Socket} client - the client socket sending the request
* @param {string} result - the name of the call to send the results back to the client
* @param {number} page - the bank's page
* @param {number} bank - the bank number
* @param {string} item - item information in form: `"instance_id:type"`
* @access public
*/
addItemByClient(client, result, page, bank, item) {
this.addItem(page, bank, item)
this.save();
client.emit(result, page, bank, this.items[page][bank] );
this.bank().checkBankStatus(page, bank);
}
/**
* Check if a bank exists and initialize it if it doesn't
* @param {number} page - the bank's page
* @param {number} bank - the bank number
* @access protected
*/
checkBankExists(page, bank) {
if (this.items[page] === undefined) {
this.items[page] = {};
}
if (this.items[page][bank] === undefined) {
this.items[page][bank] = [];
}
}
/**
* Scan the page/bank array to find items for an instance or just specific types and populate an array with the findings
* @param {string} id - the instance ID to check for
* @param {string} type - the item type to check for
* @param {?Array.<string,boolean>} checkQueue - array of flagged banks with keys of `[page_bank]`; empty by default but can have a populated version passed to be added to
* @returns {Array.<string,boolean>} the populated `checkQueue`
* @access public
*/
checkInstanceStatus(id, type, checkQueue = []) {
for (let page in this.items) {
if (this.items[page] !== undefined) {
for (let bank in this.items[page]) {
if (this.items[page][bank] !== undefined) {
for (let i = 0; i < this.items[page][bank].length; ++i) {
let item = this.items[page][bank][i];
if (item.instance == id && (type === undefined || item.type == type)) {
checkQueue[page + '_' + bank] = true;
this.checkStatus(page, bank, i);
}
}
}
}
}
}
return checkQueue;
}
/**
* Check an item's status
* @param {number} page - the item's page
* @param {number} bank - the item's bank
* @param {number} i - the item's index
* @access protected
*/
checkStatus(page, bank, i) {}
/**
* Scan the page/bank array for items from an instance and delete them
* @param {string} id - the instance ID to delete
* @param {?Array.<string,boolean>} checkQueue - array of changed banks with keys of `[page_bank]`; empty by default but can have a populated version passed to be added to
* @returns {Array.<string,boolean>} the populated `checkQueue`
* @access public
*/
deleteInstance(id, checkQueue = []) {
for (let page in this.items) {
for (let bank in this.items[page]) {
if (this.items[page][bank] !== undefined) {
for (let i = 0; i < this.items[page][bank].length ; ++i) {
let item = this.items[page][bank][i];
if (item.instance == id) {
this.debug('Deleting item ' + i + ' from button ' + page + '.' + bank);
this.deleteItem(page, bank. i);
checkQueue[page + '_' + bank] = true;
i--;
}
}
}
}
}
this.save();
return checkQueue;
}
/**
* Delete an item
* @param {number} page - the item's page
* @param {number} bank - the item's bank
* @param {number} index - the item's index
* @access public
*/
deleteItem(page, bank, index) {
if(this.items[page] !== undefined && this.items[page][bank] !== undefined && this.items[page][bank][index]!== undefined) {
this.unsubscribeItem(this.items[page][bank][index]);
this.items[page][bank].splice(index, 1);
}
}
/**
* Delete an item from a bank via a client socket
* @param {IO.Socket} client - the client socket sending the request
* @param {string} result - the name of the call to send the results back to the client
* @param {number} page - the item's page
* @param {number} bank - the item's bank
* @param {string} index - the item's id (`item.id`)
* @access public
*/
deleteItemByClient(client, result, page, bank, id) {
let ba = this.items[page][bank];
for (let n in ba) {
if (ba[n].id == id) {
this.deleteItem(page, bank, index);
break;
}
}
this.save();
client.emit(result, page, bank, this.items[page][bank] );
this.bank().checkBankStatus(page, bank);
}
/**
* Get the entire items array
* @param {boolean} clone - whether or not the return should be a deep clone
* @returns {Object} the array in the form `[page][bank][item]`
* @access public
*/
getAll(clone = false) {
let out;
if (this.items !== undefined) {
if (clone === true) {
out = cloneDeep(this.items);
// cleanup old keys
for (let p in out) {
for (let b in out[p]) {
for (let i in out[p][b]) {
if (out[p][b][i].action !== undefined && out[p][b][i].type !== undefined) {
delete out[p][b][i].action;
}
if (out[p][b][i].instance_id !== undefined && out[p][b][i].instance !== undefined) {
delete out[p][b][i].instance_id;
}
}
}
}
}
else {
out = this.items;
}
}
return out;
}
/**
* Get the items in a bank
* @param {number} page - the bank's page
* @param {number} bank - the bank number
* @param {boolean} clone - whether or not the return should be a deep clone
* @returns {BankItem[]} the items array
* @access public
*/
getBank(page, bank, clone = false) {
this.checkBankExists(page, bank);
let out;
if (clone === true) {
out = cloneDeep(this.items[page][bank]);
// cleanup old keys
for (let i in out) {
if (out[i].action !== undefined && out[i].type !== undefined) {
delete out[i].action;
}
if (out[i].instance_id !== undefined && out[i].instance !== undefined) {
delete out[i].instance_id;
}
}
}
else {
out = this.items[page][bank];
}
return out;
}
/**
* Get the items in a bank via a client socket
* @param {IO.Socket} client - the client socket sending the request
* @param {string} result - the name of the call to send the results back to the client
* @param {number} page - the bank's page
* @param {number} bank - the bank number
* @access public
*/
getBankByClient(client, result, page, bank) {
client.emit(result, page, bank, this.getBank(page, bank));
}
/**
* Get all the items for a specific instance
* @param {string} id - the instance id
* @param {boolean} clone - whether or not the return should be a deep clone
* @returns {BankItem[]} the items array
* @access public
*/
getInstanceItems(id, clone = false) {
let out = [];
for (let page in this.items) {
for (let bank in this.items[page]) {
for (let i in this.items[page][bank]) {
let item = this.items[page][bank][i];
if (item.instance == id) {
out.push(item);
}
}
}
}
if (clone === true) {
out = cloneDeep(out);
}
return out;
}
/**
* Get the items on a page
* @param {number} page - the page number
* @param {boolean} clone - whether or not the return should be a deep clone
* @returns {Array.<number,Array.<BankItem>>} the array in the form `[bank][item]`
* @access public
*/
getPage(page, clone = false) {
let out;
if (this.items[page] !== undefined) {
if (clone === true) {
out = cloneDeep(this.items[page]);
// cleanup old keys
for (let b in out) {
for (let i in out[b]) {
if (out[b][i].action !== undefined && out[b][i].type !== undefined) {
delete out[b][i].action;
}
if (out[b][i].instance_id !== undefined && out[b][i].instance !== undefined) {
delete out[b][i].instance_id;
}
}
}
}
else {
out = this.items[page];
}
}
return out;
}
/**
* Import and subscribe items to a bank
* @param {number} page - the bank's page
* @param {number} bank - the bank number
* @param {BankItem[]} items - the items to import
* @access public
*/
importBank(page, bank, items) {
if (items !== undefined) {
if (this.items[page] === undefined) {
this.items[page] = {};
}
if (this.items[page][bank] === undefined) {
this.items[page][bank] = [];
}
for (let i = 0; i < items.length; ++i) {
let obj = items[i];
obj.id = shortid.generate();
this.items[page][bank].push(obj);
}
this.subscribeBank(page, bank);
}
}
/**
* Unsubscribe, clear a bank, and save
* @param {number} page - the bank's page
* @param {number} bank - the bank number
* @access public
*/
resetBank(page, bank) {
this.unsubscribeBank(page, bank);
if (this.items[page] === undefined) {
this.items[page] = {};
}
this.items[page][bank] = [];
this.save();
}
/**
* Flag the database to save
* @access public
*/
save() {
//this.db().setKey(this.dbKey, this.items);
this.db().setDirty();
this.debug('saving');
}
/**
* Set a new definitions array
* @param {BankItemDefinition[]} definitions - the new definitions
* @access public
*/
setDefinitions(definitions) {
this.definitions = definitions;
}
/**
* Find a subscribe function for an item and execute it
* @param {BankItem} item - the item object
* @access protected
*/
subscribe(item) {
if (item.type !== undefined && item.instance !== undefined) {
if (this.definitions[item.instance] !== undefined && this.definitions[item.instance][item.type] !== undefined) {
let definition = this.definitions[item.instance][item.type];
// Run the subscribe function if needed
if (definition.subscribe !== undefined && typeof definition.subscribe == 'function') {
definition.subscribe(item);
}
}
}
}
/**
* Execute subscribes for all the items in a bank
* @param {number} page - the bank's page
* @param {number} bank - the bank number
* @access public
*/
subscribeBank(page, bank) {
if (this.items[page] !== undefined && this.items[page][bank] !== undefined) {
// find all instance-ids in items for bank
for (let i in this.items[page][bank]) {
this.subscribe(this.items[page][bank][i]);
}
}
}
/**
* Find an unsubscribe function for an item and execute it
* @param {BankItem} item - the item object
* @access protected
*/
unsubscribe(item) {
if (item.type !== undefined && item.instance !== undefined) {
if (this.definitions[item.instance] !== undefined && this.definitions[item.instance][item.type] !== undefined) {
let definition = this.definitions[item.instance][item.type];
// Run the subscribe function if needed
if (definition.unsubscribe !== undefined && typeof definition.unsubscribe == 'function') {
definition.unsubscribe(item);
}
}
}
}
/**
* Execute unsubscribes for all the items in a bank
* @param {number} page - the bank's page
* @param {number} bank - the bank number
* @access public
*/
unsubscribeBank(page, bank) {
if (this.items[page] !== undefined && this.items[page][bank] !== undefined) {
// find all instance-ids in items for bank
for (let i in this.items[page][bank]) {
this.unsubscribe(this.items[page][bank][i]);
}
}
}
/**
* Update an option for an item, subscribe, and save
* @param {number} page - the item's page
* @param {number} bank - the item's bank
* @param {string} item - the item's id (`item.id`)
* @param {string} option - the option id/key
* @param {(string|string[]|number|boolean)} value - the new value
* @access public
*/
updateItemOption(page, bank, item, option, value) {
this.debug('bank_update_item_option', page, bank, item, option, value);
let bp = this.getBank(page, bank);
if (bp !== undefined) {
for (let n in bp) {
let obj = bp[n];
if (obj !== undefined && obj.id === item) {
this.unsubscribeItem(obj);
if (obj.options === undefined) {
obj.options = {};
}
obj.options[option] = value;
this.subscribeItem(obj);
this.save();
}
}
}
}
/**
* Update a bank item order by swapping two keys
* @param {number} page - the bank's page
* @param {number} bank - the bank number
* @param {number} oldIndex - the moving item's index
* @param {number} newIndex - the other index to swap with
* @access public
*/
updateItemOrder(page, bank, oldIndex, newIndex) {
let bp = this.getBank(page, bank);
if (bp !== undefined) {
bp.splice(newIndex, 0, bp.splice(oldIndex, 1)[0]);
this.save();
}
}
}
exports = module.exports = BankItemsBase;