diff --git a/exec/avatar_chooser.js b/exec/avatar_chooser.js new file mode 100644 index 0000000000000000000000000000000000000000..ca68b91825ec91960dd054ec16047111083d81a0 --- /dev/null +++ b/exec/avatar_chooser.js @@ -0,0 +1,517 @@ +load('sbbsdefs.js'); +load('frame.js'); +load('tree.js'); +load('scrollbar.js'); +load('event-timer.js'); + +const sauce_lib = load({}, 'sauce_lib.js'); +const avatar_lib = load({}, 'avatar_lib.js'); + +const AVATAR_WIDTH = 10; +const AVATAR_HEIGHT = 6; +const AVATAR_SIZE = AVATAR_WIDTH * AVATAR_HEIGHT * 2; +const BORDER = [ BLUE, LIGHTBLUE, CYAN, LIGHTCYAN, WHITE ]; +const TITLE = 'Avatar Settings'; +const TITLE_COLOR = WHITE; +const QUIT_TEXT = 'Press Q to quit'; +const EXCLUDE_FILES = /\.\d+\.bin$/; + +Frame.prototype.drawBorder = function (color, title) { + this.pushxy(); + var theColor = color; + if (Array.isArray(color)) { + var sectionLength = Math.round(this.width / color.length); + } + for (var y = 1; y <= this.height; y++) { + for (var x = 1; x <= this.width; x++) { + if (x > 1 && x < this.width && y > 1 && y < this.height) continue; + var msg; + this.gotoxy(x, y); + if (y == 1 && x == 1) { + msg = ascii(218); + } else if (y == 1 && x == this.width) { + msg = ascii(191); + } else if (y == this.height && x == 1) { + msg = ascii(192); + } else if (y == this.height && x == this.width) { + msg = ascii(217); + } else if (x == 1 || x == this.width) { + msg = ascii(179); + } else { + msg = ascii(196); + } + if (typeof sectionLength != 'undefined') { + if (x == 1) { + theColor = color[0]; + } else if (x % sectionLength == 0 && x < this.width) { + theColor = color[x / sectionLength]; + } else if (x == this.width) { + theColor = color[color.length - 1]; + } + } + this.putmsg(msg, theColor); + } + } + if (typeof title == 'object') { + this.gotoxy(title.x, title.y); + this.attr = title.attr; + this.putmsg(ascii(180) + title.text + ascii(195)); + } + this.popxy(); +} + +Frame.prototype.nest = function (paddingX, paddingY) { + if (typeof this.parent === 'undefined') return; + const xOffset = (typeof paddingX === 'number' ? paddingX : 0); + const yOffset = (typeof paddingY === 'number' ? paddingY : 0); + this.x = this.parent.x + xOffset; + this.y = this.parent.y + yOffset; + this.width = this.parent.width - (xOffset * 2); + this.height = this.parent.height - (yOffset * 2); +} + +Frame.prototype.blit = function (bin, w, h, x, y, str, sc) { + var o = 0; // offset into 'bin' + for (var yy = 0; yy < h; yy++) { + for (var xx = 0; xx < w; xx++) { + this.setData(x + xx, y + yy, bin.substr(o, 1), ascii(bin.substr(o + 1, 1))); + o = o + 2; + } + } + if (typeof str !== 'undefined') { + // get fancy and center this later + str = str.substr(0, 10); + for (var n = 0; n < str.length; n++) { + this.setData(x + n, y + h, str.substr(n, 1), sc); + } + } +} + +function bury_cursor() { + console.gotoxy(console.screen_columns, console.screen_rows); +} + +function CollectionBrowser(filename, parent_frame) { + + const frames = { + parent : parent_frame, + container : null, + highlight : null, + scrollbar : null + }; + + const collection = { + title : '', + descriptions : [], + count : 0 + }; + + const state = { + cols : 0, + rows : 0, + selected : 0 + }; + + function parse_sauce() { + const sauce = sauce_lib.read(filename); + collection.title = sauce.title; + collection.descriptions = sauce.comment; + collection.count = Math.floor(sauce.rows / AVATAR_HEIGHT); + } + + function draw_collection(offset) { + + const f = new File(filename); + f.open('rb'); + const avatars = f.read(); + f.close(); + + var x = 1, y = 2; + for (var a = 0; a < collection.count; a++) { + frames.container.blit(avatars.substr(a * AVATAR_SIZE, AVATAR_SIZE), AVATAR_WIDTH, AVATAR_HEIGHT, x, y, collection.descriptions[a], WHITE); + x += AVATAR_WIDTH + 2; + if (x + AVATAR_WIDTH >= frames.container.x + frames.container.width) { + x = 1; + y += AVATAR_HEIGHT + 2; + } + } + + highlight(); + + } + + function highlight() { + // Column and row of this avatar in the list + var col = state.selected % state.cols; + var row = Math.floor(state.selected / state.cols); + // Actual x, y coordinates of avatar graphic within frames.container + var x = 1 + (col * AVATAR_WIDTH) + (2 * col); + var y = 2 + (row * AVATAR_HEIGHT) + (2 * row); + if (y - 1 + AVATAR_HEIGHT + 3 > frames.container.height) { + frames.container.scrollTo(0, y - 1); + } else if (y - 1 < frames.container.offset.y) { + frames.container.scrollTo(0, y - 1); + } + frames.highlight.moveTo(frames.container.x + x - 1, frames.container.y + y - 1 - frames.container.offset.y); + } + + function flashy_flashy() { + for (var n = 0; n < 5; n++) { + frames.highlight.close(); + frames.highlight.cycle(); + bury_cursor(); + mswait(75); + frames.highlight.open(); + frames.highlight.cycle(); + bury_cursor(); + mswait(75); + } + } + + this.open = function () { + + parse_sauce(); + + frames.container = new Frame(1, 1, 1, 1, WHITE, frames.parent); + frames.container.nest(1, 1); + frames.container.gotoxy(1, 1); + frames.container.center(collection.title); + frames.container.checkbounds = false; + frames.container.v_scroll = true; + + frames.highlight = new Frame(frames.container.x + 1, frames.container.y + 1, AVATAR_WIDTH + 2, AVATAR_HEIGHT + 3, WHITE, frames.container); + frames.highlight.transparent = true; + frames.highlight.drawBorder(BORDER); + + state.cols = Math.floor((frames.container.width - 3) / (AVATAR_WIDTH + 2)); + state.rows = Math.floor((frames.container.height - 2) / (AVATAR_HEIGHT + 2)); + + frames.scrollbar = new ScrollBar(frames.container); + + if (frames.parent.is_open) frames.container.open(); + draw_collection(); + + } + + this.getcmd = function (cmd) { + var ret = null; + switch (cmd.toLowerCase()) { + case KEY_LEFT: + if (state.selected > 0) { + state.selected--; + highlight(); + } + break; + case KEY_RIGHT: + if (state.selected < collection.count - 1) { + state.selected++; + highlight(); + } + break; + case KEY_UP: + if (state.selected - state.cols >= 0) { + state.selected -= state.cols; + highlight(); + } + break; + case KEY_DOWN: + if (state.selected + state.cols < collection.count) { + state.selected += state.cols; + highlight(); + } + break; + case '\r': + case '\n': + flashy_flashy(); + ret = state.selected; + break; + case 'q': + ret = -1; + break; + default: + break; + } + return ret; + } + + this.cycle = function () { + frames.scrollbar.cycle(); + } + + this.close = function () { + frames.highlight.delete(); + frames.container.delete(); + } + +} + +function CollectionLister(dir, parent_frame) { + + const frames = { + container : null, + tree : null, + info : null, + parent : parent_frame + }; + + const state = { + tree : null, + cb : null, + collection : null + }; + + function display_collection_info(sauce) { + frames.info.clear(); + frames.info.putmsg('Author: ' + (sauce.author.length ? sauce.author : 'Unknown') + '\r\n'); + frames.info.putmsg('Group: ' + (sauce.group.length ? sauce.group : 'Unknown') + '\r\n'); + frames.info.putmsg('Avatars: ' + Math.floor(sauce.rows / AVATAR_HEIGHT) + '\r\n'); + frames.info.putmsg('ICE Colors: ' + (sauce.ice_color ? 'Yes' : 'No') + '\r\n'); + frames.info.putmsg('Updated: ' + sauce.date.toLocaleDateString()); + } + + this.open = function () { + + frames.container = new Frame(1, 1, 1, 1, WHITE, parent_frame); + frames.container.transparent = true; + frames.container.nest(1, 1); + frames.container.center('Avatar Collections'); + + frames.tree = new Frame( + frames.container.x, + frames.container.y + 2, + Math.floor((frames.container.width - 2) / 2), + frames.container.height - 1, + 0, + frames.container + ); + + frames.info = new Frame( + frames.tree.x + frames.tree.width + 1, + frames.container.y + 2, + frames.tree.width - 1, + 5, + 15, + frames.container + ); + + if (frames.parent.is_open) frames.container.open(); + + state.tree = new Tree(frames.tree, 'Avatar collections'); + state.tree.colors.fg = WHITE; + state.tree.colors.bg = BG_BLACK; + state.tree.colors.lfg = WHITE; + state.tree.colors.lbg = BG_BLUE; + state.tree.colors.kfg = LIGHTCYAN; + directory(dir + '/*.bin').forEach( + function (e, i) { + if (e.search(EXCLUDE_FILES) > -1) return; + const sauce = sauce_lib.read(e); + const ti = state.tree.addItem( + sauce.title.length ? sauce.title : 'Unknown', + function () { + state.collection = e; + state.cb = new CollectionBrowser(e, frames.parent); + state.cb.open(); + } + ); + ti.sauce = sauce; + if (i == 0) display_collection_info(sauce); + } + ); + state.tree.open(); + + } + + function get_avatar(i) { + const f = new File(state.collection); + f.open('rb'); + const contents = f.read(); + f.close(); + return contents.substr(i * AVATAR_SIZE, AVATAR_SIZE); + } + + this.getcmd = function (cmd) { + if (state.cb !== null) { + var ret = state.cb.getcmd(cmd); + if (typeof ret == 'number') { + if (ret >= 0) { + // set user avatar here + var obj = { + created : new Date(), + updated : new Date(), + data : base64_encode(get_avatar(ret)) + }; + avatar_lib.write_localuser(user.number, obj); + } + state.cb.close(); + state.cb = null; + state.collection = null; + } + } else { + if (cmd.toLowerCase() == 'q') { + return false; + } else if (state.tree.getcmd(cmd)) { + frames.info.clear(); + display_collection_info(state.tree.currentItem.sauce); + } + } + return true; + } + + this.cycle = function () { + if (state.cb !== null) state.cb.cycle(); + } + + this.close = function () { + state.tree.close(); + frames.info.delete(); + frames.tree.delete(); + frames.container.delete(); + } + +} + +function MainMenu(parent_frame) { + + const user_fname = avatar_lib.localuser_fname(user.number); + + const frames = { + parent : parent_frame, + tree : null, + user_avatar : null + }; + + const state = { + cl : null, + tree : null, + timer : new Timer(), + utime : file_exists(user_fname) ? file_date(user_fname) : -1 + }; + + function load_user_avatar() { + var user_avatar = avatar_lib.read_localuser(user.number); + if (user_avatar !== null) { + frames.user_avatar.clear(); + frames.user_avatar.drawBorder(BORDER); + frames.user_avatar.blit(base64_decode(user_avatar.data), AVATAR_WIDTH, AVATAR_HEIGHT, 1, 1, 'My Avatar', WHITE); + } + } + + function test_user_file() { + if (file_exists(user_fname)) { + const utime = file_date(user_fname); + if (utime > state.utime) { + state.utime = utime; + load_user_avatar(); + } + } + } + + this.open = function () { + + frames.tree = new Frame( + frames.parent.x + 1, + frames.parent.y + 2, + Math.floor((frames.parent.width - 2) / 2), + frames.parent.height - 2, + 0, + frames.parent + ); + + frames.user_avatar = new Frame( + frames.parent.x + frames.parent.width - 1 - AVATAR_WIDTH - 2, + frames.parent.y + frames.parent.height - 1 - AVATAR_HEIGHT - 3, + AVATAR_WIDTH + 2, + AVATAR_HEIGHT + 3, + 15, + frames.parent + ); + + load_user_avatar(); + + if (frames.parent.is_open) { + frames.tree.open(); + frames.user_avatar.open(); + } + + state.tree = new Tree(frames.tree, 'Avatar collections'); + state.tree.colors.fg = WHITE; + state.tree.colors.bg = BG_BLACK; + state.tree.colors.lfg = WHITE; + state.tree.colors.lbg = BG_BLUE; + state.tree.colors.kfg = LIGHTCYAN; + state.tree.addItem( + 'Select an avatar', function () { + state.tree.close(); + state.cl = new CollectionLister(avatar_lib.local_library(), parent_frame); + state.cl.open(); + } + ); + state.tree.addItem( + 'Upload an avatar', function () { + // placeholder + } + ); + state.tree.addItem( + 'Download your avatar', function () { + // placeholder + } + ); + state.tree.addItem( + 'Edit your avatar', function () { + // placeholder + } + ); + state.tree.open(); + + state.timer.addEvent(2000, true, test_user_file); + + } + + this.getcmd = function (cmd) { + if (state.cl !== null) { + if (!state.cl.getcmd(cmd)) { + state.cl.close(); + delete state.cl; + state.cl = null; + state.tree.open(); + } + } else if (cmd.toLowerCase() == 'q') { + return false; + } else { + state.tree.getcmd(cmd); + } + return true; + } + + this.cycle = function () { + state.timer.cycle(); + if (state.cl !== null) state.cl.cycle(); + } + + this.close = function () { + state.tree.close(); + frames.user_avatar.delete(); + frames.tree.delete(); + } + +} + +const frame = new Frame(1, 1, console.screen_columns, console.screen_rows, 0); +frame.transparent = true; +frame.v_scroll = true; +frame.drawBorder(BORDER, { x : 5, y : 1, attr : TITLE_COLOR, text: TITLE }); +frame.gotoxy(frame.x + frame.width - 20, frame.y + frame.height - 1); +frame.putmsg(ascii(180) + QUIT_TEXT + ascii(195)); +frame.open(); + +const menu = new MainMenu(frame); +menu.open(); +while (true) { + var i = console.inkey(K_NONE); + if (i !== '' && !menu.getcmd(i)) break; + menu.cycle(); + if (frame.cycle()) bury_cursor(); +} +menu.close(); +frame.close(); \ No newline at end of file