diff --git a/js/tinySSB-for-Chrome/20241222-tinySSB-SW-struct.pdf b/js/tinySSB-for-Chrome/20241222-tinySSB-SW-struct.pdf new file mode 100644 index 0000000..8315214 Binary files /dev/null and b/js/tinySSB-for-Chrome/20241222-tinySSB-SW-struct.pdf differ diff --git a/js/tinySSB-for-Chrome/README.md b/js/tinySSB-for-Chrome/README.md new file mode 100644 index 0000000..e4cb929 --- /dev/null +++ b/js/tinySSB-for-Chrome/README.md @@ -0,0 +1,10 @@ +# tinySSB Development: virtual backend for the Chrome Browser + +Rationale: instead of writing JS and HTML code for your tinySSB +mini-app and then compiling it for Android for trying it out, this +"tinySSB-for-Chrome" environment enables you to test your mini-app +directly inside a Chrome brower tab. + +Using Chrome's broadcast feature between tabs, new log entries are +sent to all tabs where they are funneled to your JS app logic. See the +attached PDF for details. diff --git a/js/tinySSB-for-Chrome/board.js b/js/tinySSB-for-Chrome/board.js new file mode 100644 index 0000000..48968a7 --- /dev/null +++ b/js/tinySSB-for-Chrome/board.js @@ -0,0 +1,804 @@ +//board.js + +"use strict"; + +var curr_board; +var curr_context_menu; +var curr_column; +var curr_item; + +var curr_rename_item; + +// all available operations +const Operation = { + BOARD_CREATE: 'board/create', + BOARD_RENAME: 'board/rename', + COLUMN_CREATE: 'column/create', + ITEM_CREATE: 'item/create', + COLUMN_REMOVE: 'column/remove', + COLUMN_RENAME: 'column/rename', + ITEM_REMOVE: 'item/remove', + ITEM_RENAME: 'item/rename', + ITEM_MOVE: 'item/moveTo', + ITEM_SET_DESCRIPTION: 'item/setDiscription', + ITEM_POST_COMMENT: 'item/post', + ITEM_ASSIGN: 'item/assign', + ITEM_UNASSIGN: 'item/unassign', + ITEM_COLOR: 'item/color', + INVITE: 'invite', + INVITE_ACCEPT: 'invite/accept', + INVITE_DECLINE: 'invite/decline', + LEAVE: 'leave' +} + +const FLAG = { + PERSONAL: 'personal' +} + + +function createBoard(name, flags) { + var cmd = [Operation.BOARD_CREATE, name] + if (flags != null) + cmd = cmd.concat(flags) + + var data = { + 'bid': null, + 'cmd': cmd, + 'prev': null + } + board_send_to_backend(data) +} + +function renameBoard(bid, name) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.BOARD_RENAME, name], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function createColumn(bid, name) { + console.log("create column") + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.COLUMN_CREATE, name], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function createColumnItem(bid, cid, name) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.ITEM_CREATE, cid, name], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function removeColumn(bid, cid) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.COLUMN_REMOVE, cid], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function renameColumn(bid, cid, newName) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.COLUMN_RENAME, cid, newName], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function removeItem(bid, iid) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.ITEM_REMOVE, iid], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function renameItem(bid, iid, new_name) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.ITEM_RENAME, iid, new_name], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function moveItem(bid, iid, new_cid) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.ITEM_MOVE, iid, new_cid], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function setItemDescription(bid, iid, description) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.ITEM_SET_DESCRIPTION, iid, description], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function postItemComment(bid, iid, comment) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.ITEM_POST_COMMENT, iid, comment], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function assignToItem(bid, iid, assigned) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.ITEM_ASSIGN, iid, assigned], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function unassignFromItem(bid, iid, unassign) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.ITEM_UNASSIGN, iid, unassign], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function setItemColor(bid, iid, color) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.ITEM_COLOR, iid, color], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function inviteUser(bid, userID) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.INVITE, userID], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + +function inviteAccept(bid, prev) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.INVITE_ACCEPT], + 'prev': prev + } + board_send_to_backend(data) +} + +function inviteDecline(bid, prev) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.INVITE_DECLINE], + 'prev': prev + } + board_send_to_backend(data) +} + +function leave(bid) { + var board = tremola.board[bid] + var data = { + 'bid': bid, + 'cmd': [Operation.LEAVE], + 'prev': board.curr_prev + } + board_send_to_backend(data) +} + + +function board_send_to_backend(data) { + var bid = data['bid'] != null ? data['bid'] : "null" + var prevs = data['prev'] != null ? btoa(data['prev'].map(btoa)) : "null" + var op = data['cmd'][0] //btoa(data['cmd'][0]) + var args = data['cmd'].length > 1 ? btoa(data['cmd'].slice(1).map(unicodeStringToTypedArray).map(btoa)) : "null" + var to_backend = ['kanban', bid, prevs, op, args] + backend(to_backend.join(" ")) +} + +function kanban_new_event(e) { + // parse data + var op = e.public[3] + var bid = op == Operation.BOARD_CREATE ? e.header.ref : e.public[1] + var prev = e.public[2] != "null" ? e.public[2] : [] // TODO: change to null instead of "null" if backend sends this field as Bipf.mkNone() + var args = e.public.length > 4 ? e.public.slice(4) : [] + + // add new entry if it is a new board + if (!(bid in tremola.board)) { + tremola.board[bid] = { + "operations": {}, // all received operations for this board + "sortedOperations": new Timeline(), // "linear timeline", sorted list of operationIds + "members": [e.header.fid], // members of the board + "forgotten": false, // flag for hiding this board from the board list + "name": bid.toString().slice(0, 15) + '...', // name of the board + "curr_prev": [], // prev pointer + "columns": {}, + "items": {}, + "numOfActiveColumns": 0, + "history": [], + "lastUpdate": Date.now(), + "unreadEvents": 0, + "subscribed": false, + "pendingInvitations": {}, // User: [inviteIds] + "key": bid.toString(), + "flags": [] + } + } + + var board = tremola.board[bid] + + if (op == Operation.BOARD_CREATE) { + board.name = args[0] + board.flags = args.slice(1) + if (document.getElementById('kanban-invitations-overlay').style.display != 'none') + if (document.getElementById("kanban_invitation_" + bid)) + menu_board_invitation_create_entry(bid) + if (e.header.fid == myId) + board.subscribed = true // the creator of the board is automatically subscribed + } + + if (!(board.sortedOperations instanceof Timeline)) { // deserialize ScuttleSort-Timeline + board.sortedOperations = Timeline.fromJSON(board.sortedOperations) + } + + if (e.header.ref in board.operations) + return + + + // translation of the event format into the kanban board format + var body = { + 'bid': bid, + 'cmd': [op].concat(args), + 'prev': prev + } + + // store new event + var p = {"key": e.header.ref, "fid": e.header.fid, "fid_seq": e.header.seq, "body": body, "when": e.header.tst}; + board["operations"][e.header.ref] = p; + + if (op == Operation.LEAVE && e.header.fid == myId) { + delete board.pendingInvitations[myId] + board.subscribed = false + load_board_list() + } + + if (board.subscribed) { + board.sortedOperations.add(e.header.ref, prev) + + var independentOPs = [Operation.COLUMN_CREATE, Operation.ITEM_CREATE, Operation.COLUMN_REMOVE, Operation.ITEM_REMOVE, Operation.LEAVE] // these operations cannot be overwritten; their position in the linear timeline does not affect the resulting board + + // Ui update + update optimization // board.operations[e.header.ref].indx == board.sortedOperations.length -1 + if (board.sortedOperations.name2p[e.header.ref].indx == board.sortedOperations.linear.length - 1 || independentOPs.indexOf(board.operations[e.header.ref].body.cmd[0]) >= 0) { //if the new event is inserted at the end of the linear timeline or the position is irrelevant for this operation + if (curr_scenario == 'board' && curr_board == bid) + apply_operation(bid, e.header.ref, true) // the board is currently displayed; additionally perform operation on UI + else + apply_operation(bid, e.header.ref, false) + } else { + console.log("DEBUG APPLYALL") + apply_all_operations(bid) + } + + board.curr_prev = board.sortedOperations.get_tips() + board.lastUpdate = Date.now() + + if (curr_scenario != 'board' || curr_board != bid) + board.unreadEvents++ + + load_board_list() + + // invite selected users (during Kanban board creation) + if (op == Operation.BOARD_CREATE && e.header.fid == myId) { + var pendingInvites = [] + for (var m in tremola.contacts) { + var d = document.getElementById(m); + if (m != myId && d && d.checked) { + inviteUser(bid, m) + console.log("Invited: " + m) + } + } + if (curr_scenario == 'members') + load_board(bid) + } + + // creates Personal Board + if (board.flags.includes(FLAG.PERSONAL) && !restream) { + if (op == Operation.BOARD_CREATE && Object.values(board.columns).length == 0) + createColumn(bid, 'Your Kanban Board') + else if (Object.values(board.columns).length == 1 && Object.values(board.columns)[0].item_ids.length == 0) + createColumnItem(bid, Object.values(board.columns)[0].id, 'Click me!') + else if (Object.values(board.columns)[0].item_ids.length == 1 && board.items[Object.values(board.columns)[0].item_ids[0]].description == "") { + setItemDescription(bid, Object.values(board.columns)[0].item_ids[0], "Use cards and lists to organize your projects") + board.unreadEvents = 1 + } + } + } else { + if (op == Operation.INVITE && body.cmd[1] == myId) { // received invitation to board + if (myId in board.pendingInvitations) + board.pendingInvitations[myId].push(e.header.ref) + else { + board.pendingInvitations[myId] = [e.header.ref] + launch_snackbar('New invitation received') + if (document.getElementById('kanban-invitations-overlay').style.display != 'none') { + menu_board_invitation_create_entry(bid) + console.log("create invite NAME:" + tremola.board['bid'].name) + + } + + } + } + + if (op == Operation.INVITE_ACCEPT && e.header.fid == myId) { // invitation accepted -> start sorting all events + board.subscribed = true + board_reload(bid) + board.lastUpdate = Date.now() + board.unreadEvents++ + board.curr_prev = board.sortedOperations.get_tips() + load_board_list() + return + } + + if (op == Operation.INVITE_DECLINE && e.header.fid == myId) { + delete board.pendingInvitations[myId] + } + } + +} + +function reload_curr_board() { + if (curr_board) + board_reload(curr_board) +} + +function board_reload(bid) { + console.log("Board reload " + bid) + var board = tremola.board[bid] + board.columns = {} + board.numOfActiveColumns = 0 + board.items = {} + board.pendingOperations = {} + board.pendingInvitations = {} + board.members = [] + board.sortedOperations = new Timeline() + + for (var op in board.operations) { + console.log("ADD op: " + op + ", prev:" + board.operations[op].body.prev) + board.sortedOperations.add(op, board.operations[op].body.prev) + } + apply_all_operations(bid) + + if (curr_scenario == 'board' && curr_board == bid) { + closeOverlay() + curr_item = null + curr_column = null + curr_context_menu = null + load_board(bid) + } +} + +/** + * Creates a snapshot of the given kanban board. It applies all operations and updates the user interface + * + * @param {string} bid - Id of the kanban board + */ +function apply_all_operations(bid) { + var board = tremola.board[bid] + board.history = [] + + var old_state = JSON.parse(JSON.stringify(board)); + + //execute operations and save results to local storage + var validOps = helper_linear_timeline_without_pending_prevs(board.sortedOperations) + for (var i in validOps) { + apply_operation(bid, validOps[i], false) + } + + if (curr_scenario == 'board' && curr_board == bid) { // update ui + ui_update_board(bid, old_state) + console.log("UP CURR") + } +} + +// returns linear timeline that does not contain nodes which have only pending predecessors +function helper_linear_timeline_without_pending_prevs(timeline) { + var lst = [] + for (let n of timeline.linear) { + var validPrevs = 0 + for (let p of n.prev) { + if ((typeof p != "string") && !(p.name in timeline.pending)) + validPrevs++ + } + if (validPrevs > 0 || n.prev.length == 0) { + lst.push(n.name); + } + } + + return lst; +} + +function apply_operation(bid, operationID, apply_on_ui) { + console.log("Apply:" + operationID) + var board = tremola.board[bid] + var curr_op = board['operations'][operationID] + + var author_name = tremola.contacts[curr_op.fid].alias + var historyMessage = author_name + " " + + switch (curr_op.body.cmd[0]) { + case Operation.BOARD_CREATE: + historyMessage += "created the board \"" + curr_op.body.cmd[1] + "\"" + board.name = curr_op.body.cmd[1] + if (board.members.indexOf(curr_op.fid) < 0) + board.members.push(curr_op.fid) + if (curr_op.fid == myId) + board.subscribed = true + /*if(board.members.indexOf(curr_op.fid) < 0) + board.members.push(curr_op.fid) + */ + break + case Operation.BOARD_RENAME: + historyMessage += "renamed the board \"" + board.name + "\" to \"" + curr_op.body.cmd[1] + "\"" + board.name = curr_op.body.cmd[1] + + if (apply_on_ui) + ui_update_board_title(bid) + break + case Operation.COLUMN_CREATE: + historyMessage += "created the list \"" + curr_op.body.cmd[1] + "\"" + var newPos = 0 + if (curr_op.key in board.columns) { + if (board.columns[curr_op.key].removed) + break + newPos = board.columns[curr_op.key].position + } else + newPos = ++board.numOfActiveColumns + + board.columns[curr_op.key] = { + 'name': curr_op.body.cmd[1], + 'id': curr_op.key.toString(), + 'item_ids': [], + 'position': newPos, + 'numOfActiveItems': 0, + 'removed': false + } + + if (apply_on_ui) + load_column(curr_op.key) + break + case Operation.COLUMN_REMOVE: + if (!(curr_op.body.cmd[1] in board.columns)) + return + historyMessage += "removed list \"" + board.columns[curr_op.body.cmd[1]].name + "\"" + board.columns[curr_op.body.cmd[1]].removed = true + + for (var i in board.columns) { + if (board.columns[i].removed) + continue + + if (board.columns[i].position > board.columns[curr_op.body.cmd[1]].position) { + --board.columns[i].position + } + + } + board.numOfActiveColumns-- + + if (apply_on_ui) + ui_remove_column(curr_op.body.cmd[1]) + break + case Operation.COLUMN_RENAME: + if (!(curr_op.body.cmd[1] in board.columns)) + break + historyMessage += "renamed list \"" + board.columns[curr_op.body.cmd[1]].name + "\" to \"" + curr_op.body.cmd[2] + "\"" + board.columns[curr_op.body.cmd[1]].name = curr_op.body.cmd[2] + + if (apply_on_ui) + ui_rename_column(curr_op.body.cmd[1], curr_op.body.cmd[2]) + break + case Operation.ITEM_CREATE: + if (!(curr_op.body.cmd[1] in board.columns)) + break + historyMessage += "created a card in list \"" + board.columns[curr_op.body.cmd[1]].name + "\" with the name: \"" + curr_op.body.cmd[2] + "\"" + var newPos = 0 + if (curr_op.key in board.items) { + if (board.items[curr_op.key].removed) { + break + } + newPos = board.items[curr_op.key].position //there is already a position assigned to the item + } else { + newPos = ++board.columns[curr_op.body.cmd[1]].numOfActiveItems + } + + board.items[curr_op.key] = { + 'name': curr_op.body.cmd[2], + 'id': curr_op.key.toString(), + 'curr_column': curr_op.body.cmd[1], + 'assignees': [], + 'comments': [], + 'description': "", + 'position': newPos, + 'color': Color.BLACK, + 'removed': false + } + board.columns[curr_op.body.cmd[1]].item_ids.push(curr_op.key.toString()) + + if (apply_on_ui) + load_item(curr_op.key) + break + case Operation.ITEM_REMOVE: + if (!(curr_op.body.cmd[1] in board.items)) + break + var item = board.items[curr_op.body.cmd[1]] + var column = board.columns[item.curr_column] + historyMessage += "removed card \"" + item.name + "\" from list \"" + column.name + "\"" + if (item.removed) + break + item.removed = true + column.numOfActiveItems-- + column.item_ids.splice(column.item_ids.indexOf(curr_op.body.cmd[1]), 1) + + for (var i in column.item_ids) { + var curr_item = board.items[column.item_ids[i]] + if (curr_item.position > board.items[curr_op.body.cmd[1]].position) { + curr_item.position-- + } + } + + if (apply_on_ui) + ui_remove_item(curr_op.body.cmd[1]) + break + case Operation.ITEM_RENAME: + if (!(curr_op.body.cmd[1] in board.items)) + break + var item = board.items[curr_op.body.cmd[1]] + historyMessage += "renamed card \"" + item.name + "\" of list \"" + board.columns[item.curr_column].name + "\" to \"" + curr_op.body.cmd[2] + "\"" + item.name = curr_op.body.cmd[2] + + if (apply_on_ui) + ui_update_item_name(curr_op.body.cmd[1], curr_op.body.cmd[2]) + break + case Operation.ITEM_MOVE: + if (!(curr_op.body.cmd[1] in board.items)) + break + if (!(curr_op.body.cmd[2] in board.columns)) + break + + var item = board.items[curr_op.body.cmd[1]] + historyMessage += "moved card \"" + item.name + "\" of list \"" + board.columns[item.curr_column].name + "\" to list \"" + board.columns[curr_op.body.cmd[2]].name + "\"" + + var old_column = board.columns[item.curr_column] + var old_pos = item.position + old_column.item_ids.splice(old_column.item_ids.indexOf(curr_op.body.cmd[1]), 1) + old_column.numOfActiveItems-- + board.columns[curr_op.body.cmd[2]].numOfActiveItems++ + item.position = board.columns[curr_op.body.cmd[2]].numOfActiveItems + board.columns[curr_op.body.cmd[2]].item_ids.push(curr_op.body.cmd[1]) + item.curr_column = curr_op.body.cmd[2].toString() + for (var iid of old_column.item_ids) { + let i = board.items[iid] + if (i.position > old_pos) { + i.position-- + } + } + + if (apply_on_ui) + ui_update_item_move_to_column(curr_op.body.cmd[1], curr_op.body.cmd[2], item.position) + break + case Operation.ITEM_SET_DESCRIPTION: + if (!(curr_op.body.cmd[1] in board.items)) + break + var item = board.items[curr_op.body.cmd[1]] + historyMessage += "changed description of card \"" + item.name + "\" of list \"" + board.columns[item.curr_column].name + "\" from \"" + item.description + "\" to \"" + curr_op.body.cmd[2] + "\"" + item.description = curr_op.body.cmd[2] + + if (apply_on_ui) + ui_update_item_description(curr_op.body.cmd[1], curr_op.body.cmd[2]) + break + case Operation.ITEM_POST_COMMENT: + if (!(curr_op.body.cmd[1] in board.items)) + break + + var item = board.items[curr_op.body.cmd[1]] + historyMessage += "posted \"" + curr_op.body.cmd[2] + "\" on card \"" + item.name + "\" of list \"" + board.columns[item.curr_column].name + "\"" + item.comments.push([curr_op.fid, curr_op.body.cmd[2]]) + + if (apply_on_ui) + ui_item_update_chat(curr_op.body.cmd[1]) + break + case Operation.ITEM_ASSIGN: + if (!(curr_op.body.cmd[1] in board.items)) + break + if (!(curr_op.body.cmd[2] in tremola.contacts)) + break + + var item = board.items[curr_op.body.cmd[1]] + historyMessage += "assigned \"" + tremola.contacts[curr_op.body.cmd[2]].alias + "\" to card \"" + item.name + "\" of list \"" + board.columns[item.curr_column].name + "\"" + if (item.assignees.indexOf(curr_op.body.cmd[2]) < 0) + item.assignees.push(curr_op.body.cmd[2]) + + if (apply_on_ui) + ui_update_item_assignees(curr_op.body.cmd[1]) + break + case Operation.ITEM_UNASSIGN: + if (!(curr_op.body.cmd[1] in board.items)) + break + if (!(curr_op.body.cmd[2] in tremola.contacts)) + break + var item = board.items[curr_op.body.cmd[1]] + historyMessage += "unassigned \"" + tremola.contacts[curr_op.body.cmd[2]].alias + "\" from card \"" + item.name + "\" of list \"" + board.columns[item.curr_column].name + "\"" + if (item.assignees.indexOf(curr_op.body.cmd[2]) >= 0) + item.assignees.splice(item.assignees.indexOf(curr_op.body.cmd[2]), 1) + + if (apply_on_ui) + ui_update_item_assignees(curr_op.body.cmd[1]) + break + case Operation.ITEM_COLOR: + if (!(curr_op.body.cmd[1] in board.items)) + break + var item = board.items[curr_op.body.cmd[1]] + historyMessage += "changed color of card \"" + item.name + "\" to " + curr_op.body.cmd[2] + item.color = curr_op.body.cmd[2] + + if (apply_on_ui) + ui_update_item_color(curr_op.body.cmd[1], curr_op.body.cmd[2]) + break + case Operation.INVITE: + historyMessage += "invited " + curr_op.body.cmd[1] + "." + + console.log("IDX: " + board.members.indexOf(curr_op.body.cmd[1])) + console.log("INVITE USER: " + curr_op.body.cmd[1]) + console.log("PENDING: " + board.pendingInvitations) + + if (board.members.indexOf(curr_op.body.cmd[1]) < 0) { + if (!(curr_op.body.cmd[1] in board.pendingInvitations)) + board.pendingInvitations[curr_op.body.cmd[1]] = [] + console.log("PENDING: " + board.pendingInvitations) + board.pendingInvitations[curr_op.body.cmd[1]].push(curr_op.key) + } + + if (apply_on_ui) + menu_invite_create_entry(curr_op.body.cmd[1]) + + break + case Operation.INVITE_ACCEPT: + console.log("invite/accept"); + if (curr_op.fid in board.pendingInvitations) { // check if the invite accept operation is valid + console.log("valid invite", board.pendingInvitations, + board.pendingInvitations[curr_op.fid]) + // check if one of the prevs of the accept message is actual a valid invitation + // if (board.pendingInvitations[curr_op.fid].filter(op => board.operations[curr_op.key].body.prev.includes(op)).length > 0) { + if (board.pendingInvitations[curr_op.fid] in board.operations) { + historyMessage += "accepted invitation" + delete board.pendingInvitations[curr_op.fid] + if (board.members.indexOf(curr_op.fid) < 0) { //should always be the case + board.members.push(curr_op.fid) + console.log("MEMBERS" + board.members) + } + if (curr_op.fid == myId) + board.subscribed = true + if (apply_on_ui) { + ui_update_board_title(bid) + menu_invite_create_entry(curr_op.fid) + if (curr_op.fid != myId) + launch_snackbar("A new user joined the kanban board") + } + break + } + } + console.log("WRONG INVITATION") + break + case Operation.INVITE_DECLINE: + if (curr_op.fid in board.pendingInvitations) { // check if the invite accept operation is valid + if (board.pendingInvitations[curr_op.fid].filter(op => board.operations[curr_op.key].body.prev.includes(op)).length > 0) { + historyMessage += "declined invitation" + delete board.pendingInvitations[curr_op.fid] + var idx = board.members.indexOf(curr_op.fid) + if (idx >= 0) { // should never be the case + board.members.splice(idx, 1) + } + } + if (apply_on_ui) + menu_invite_create_entry(curr_op.fid) + } + break + case Operation.LEAVE: + historyMessage += "left" + var idx = board.members.indexOf(curr_op.fid) + if (idx >= 0) { + board.members.splice(idx, 1) + } + delete board.pendingInvitations[curr_op.fid] + + if (apply_on_ui) { + ui_update_board_title(bid) + menu_invite_create_entry(curr_op.fid) + if (curr_op.fid != myId) + launch_snackbar("A user has left the kanban board") + } + + + break + } + //historyMessage += ", " + curr_op.key // debug + board.history.push([curr_op.fid, historyMessage]) + persist() +} + +function clear_board() { // removes all active columns from the board + var board = tremola.board[curr_board] + + for (var i in board.columns) { + removeColumn(curr_board, i) + } + closeOverlay() +} + +/* + Debug menu +*/ + +function ui_debug() { + closeOverlay() + document.getElementById('div:debug').style.display = 'initial' + document.getElementById('txt:debug').value = debug_toDot()//JSON.stringify(tremola.board[curr_board]) + document.getElementById("overlay-bg").style.display = 'initial'; +} + +function debug_toDot() { + var board = tremola.board[curr_board] + var exportStr = "digraph {\n" + exportStr += " rankdir=RL;\n" + exportStr += " splines=true;\n" + exportStr += " subgraph dag {\n" + exportStr += " node[shape=Mrecord];\n" + + if (!(board.sortedOperations instanceof Timeline)) { // deserialize ScuttleSort-Timeline + board.sortedOperations = Timeline.fromJSON(board.sortedOperations) + } + for (var sortNode of board.sortedOperations.linear) { + exportStr += ' ' + '"' + sortNode.name + '"' + ' [label="hash=' + sortNode.name + '\\nop=' + tremola.board[curr_board].operations[sortNode.name].body.cmd + '\\nr=' + sortNode.rank + '\\nindx=' + sortNode.indx + '"]\n' + for (var prev of sortNode.prev) { + exportStr += ' "' + sortNode.name + '" -> "' + prev.name + '"\n' + } + } + exportStr += " }\n" + exportStr += " subgraph time {\n" + exportStr += " node[shape=plain];\n" + exportStr += ' " t" -> " " [dir=back];\n' + exportStr += " }\n" + exportStr += "}" + + return exportStr +} diff --git a/js/tinySSB-for-Chrome/board_ui.js b/js/tinySSB-for-Chrome/board_ui.js new file mode 100644 index 0000000..b59bdff --- /dev/null +++ b/js/tinySSB-for-Chrome/board_ui.js @@ -0,0 +1,1175 @@ +//board_ui.js + +"use strict"; + +const Color = { // all available colors for card title + BLACK: 'black', + RED: 'red', + GREEN: 'green', + BLUE: 'blue', + YELLOW: 'yellow', + CYAN: 'cyan', + MAGENTA: 'magenta', + ORANGE: 'orange' +} + +var display_create_personal_board = true // Whether to prompt the user to create a personal board when they open the Kanban application + +function allowDrop(ev) { + ev.preventDefault(); +} + +function myTouchFct(ev) { + console.log('touch'); + console.log(ev); +} + +function dragStart(ev) { + console.log('drag started ' + ev.target.id); + ev.dataTransfer.setData("text", ev.target.id); +} + +function dragDrop(ev) { + ev.preventDefault(); + console.log("event", ev) + console.log('dragDrop', ev.target); + console.log(ev.target); + var s = ev.dataTransfer.getData("text").split('-'); + var t = ev.target.id.split('-'); + if (t.length == 1) { + t = ev.target.parentNode.id.split('-'); + if (t.length == 1) + t = ev.target.parentNode.parentNode.id.split('-'); + } + console.log(s, t); + + var itemTargets = ['item', 'itemDescr', 'itemAssignees'] + if (itemTargets.indexOf(s[1]) >= 0) { + if (itemTargets.indexOf(t[1]) >= 0) { + var oldColID = tremola.board[curr_board].items[s[0]].curr_column; + var newColID = tremola.board[curr_board].items[t[0]].curr_column; + var newPos = tremola.board[curr_board].items[t[0]].position; + if (oldColID != newColID) + moveItem(curr_board, s[0], newColID); + ui_change_item_order(s[0], newPos); + } else if (t[1] == 'columnHdr') + moveItem(curr_board, s[0], t[0]); + return; + } + if (s[1] == 'columnWrapper') { + var colID; + if (t[1] == 'columnWrapper' || t[1] == 'columnHdr') + colID = t[0]; + else if (itemTargets.indexOf(t[1]) >= 0) + colID = tremola.board[curr_board].items[t[0]].curr_column; + // console.log('colID', colID); + var targetPos = tremola.board[curr_board].columns[colID].position; + ui_move_column(s[0], targetPos); + return; + } +} + +function load_board_list() { + document.getElementById('lst:kanban').innerHTML = ''; + if (Object.keys(tremola.board).length === 0) + return + var subBoardIds = Object.keys(tremola.board).filter(key => tremola.board[key].subscribed).map(key => ({[key]: tremola.board[key]})) + if (subBoardIds.length > 0) { + var subscribedBoards = Object.assign(...subBoardIds) + var bidTimestamp = Object.keys(subscribedBoards).map(function (key) { + return [key, subscribedBoards[key].lastUpdate] + }) // [0] = bid, [1] = timestamp + bidTimestamp.sort(function (a, b) { + return b[1] - a[1]; + }) + + for (var i in bidTimestamp) { + var bid = bidTimestamp[i][0] + var board = tremola.board[bid] + var date = new Date(bidTimestamp[i][1]) + date = date.toDateString() + ' ' + date.toTimeString().substring(0, 5); + if (board.forgotten && tremola.settings.hide_forgotten_boards) + continue + var cl, mem, item, bg, row, badge, badgeId, cnt; + cl = document.getElementById('lst:kanban'); + mem = recps2display(board.members) + item = document.createElement('div'); + item.setAttribute('style', "padding: 0px 5px 10px 5px; margin: 3px 3px 6px 3px;"); + if (board.forgotten) bg = ' gray'; else bg = ' light'; + row = ""; + row += "" + item.innerHTML = row; + cl.appendChild(item); + ui_set_board_list_badge(bid) + } + } +} + +function ui_set_board_list_badge(bid) { + var board = tremola.board[bid] + var e = document.getElementById(bid + "-badge_board") + var cnt + if (board.unreadEvents == 0) { + e.style.display = 'none' + return + } + e.style.display = null + if (board.unreadEvents > 9) cnt = ">9"; else cnt = "" + board.unreadEvents + e.innerHTML = cnt +} + +function create_personal_board() { + createBoard('Personal Board', [FLAG.PERSONAL]) +} + +function menu_create_personal_board() { + closeOverlay() + document.getElementById('kanban-create-personal-board-overlay').style.display = 'initial' + document.getElementById('overlay-trans-core').style.display = 'initial' +} + +function btn_create_personal_board_accept() { + create_personal_board() + closeOverlay() +} + +function btn_create_personal_board_decline() { + closeOverlay() + display_create_personal_board = false +} + +function load_board(bid) { //switches scene to board and changes title to board name + curr_board = bid + var b = tremola.board[bid] + + b.unreadEvents = 0 + persist() + ui_set_board_list_badge(bid) + + var title = document.getElementById("conversationTitle"), bg, box; + title.style.display = null; + title.setAttribute('classList', bid.forgotten ? ['gray'] : []); + box = "
" + "Kanban: " + escapeHTML(b.name) + "
"; + box += "
" + escapeHTML(recps2display(b.members)) + "
"; + title.innerHTML = box; + + document.getElementById("div:columns_container").innerHTML = "" //clear old content + setScenario('board') + document.getElementById("tremolaTitle").style.display = 'none'; + document.getElementById("tremolaTitle").style.position = 'fixed'; + title.style.display = null; + + load_all_columns() + load_all_items() + +} + +/** + * Compares the previous snapshot of the Kanban board with the current one and updates the elements accordingly + * + * @param {string} bid - Id of the kanban board + * @param {object} old_state - Previous snapshot of the kanban board + */ +function ui_update_board(bid, old_state) { + var board = tremola.board[bid] + console.log("DEBUG UPDATE") + + if (curr_board != bid) + return + + console.log("DEBUG UPDATE NOT INTERUPPTED") + + console.log("DEBUG oldstate members: " + String(old_state.members)) + console.log("DEBUG curr members: " + String(board.members)) + + // board title (name + members) + if (board.name != old_state.name || !equalArrays(board.members, old_state.members)) { + ui_update_board_title(bid) + var changed_members = board.members.filter(member => !old_state.members.includes(member)) + changed_members.concat(old_state.members.filter(member => !board.members.includes(member))) + console.log("Changed members: " + changed_members) + for (var member of changed_members) + menu_invite_create_entry(member) + } + + // invite menu + if (!equalArrays(Object.keys(board.pendingInvitations), Object.keys(old_state.pendingInvitations))) { + var changed_members = board.members.filter(member => !old_state.members.includes(member)) + changed_members.concat(old_state.members.filter(member => !board.members.includes(member))) + console.log("Changed members (invite):" + changed_members) + for (var member of changed_members) + menu_invite_create_entry(member) + } + + + // columns + for (var i in board.columns) { + var old_column = old_state.columns[i] + var new_column = board.columns[i] + + if (!old_column) { + load_column(i) + return + } + if (new_column.removed && (old_column.removed != new_column.removed)) { + ui_remove_column(i) + return + } + if (old_column.name != new_column.name) + ui_rename_column(i, new_column.name) + } + + //items + for (var i in board.items) { + var old_item = old_state.items[i] + var new_item = board.items[i] + + if (!old_item) { + load_item(i) + return + } + if (new_item.removed && (old_item.removed != new_item.removed)) { + ui_remove_item(i) + return + } + if (old_item.name != new_item.name) + ui_update_item_name(i, new_item.name) + if (old_item.description != new_item.description) + ui_update_item_description(i, new_item.description) + if (!equalArrays(old_item.assignees, new_item.assignees)) + ui_update_item_assignees(i) + if (!equalArrays(old_item.comments, new_item.comments)) + ui_item_update_chat(i) + if (old_item.curr_column != new_item.curr_column) + ui_update_item_move_to_column(i, new_item.curr_column, new_item.position) + if (old_item.color != new_item.color) + ui_update_item_color(i, new_item.color) + } +} + +/** + * Compares two arrays and returns whether the are equal + * + * @param {Array} array1 + * @param {Array} array2 + * @return {boolean} - Whether the two given arrays are equal + */ +function equalArrays(array1, array2) { + if (array1.length != array2.length) + return false + + for (var i in array1) { + if (array1[i] instanceof Array && array2[i] instanceof Array) { + if (!equalArrays(array1[i], array2[i])) + return false + } else if (array1[i] != array2[i]) { + return false + } + } + return true +} + +function close_board_context_menu() { + if (curr_context_menu) { + var context_menu = document.getElementById(curr_context_menu) + if (context_menu) + context_menu.style.display = 'none'; + } + curr_context_menu = null +} + +function menu_history() { + closeOverlay() + document.getElementById('overlay-bg').style.display = 'initial'; + document.getElementById('menu_history_content').innerHTML = '' + overlayIsActive = true; + var board = tremola.board[curr_board] + + var reversedHistory = board.history.slice().reverse() + + for (var i in reversedHistory) { + + var author_name = tremola.contacts[reversedHistory[i][0]].alias + var author_color = tremola.contacts[reversedHistory[i][0]].color + var author_initial = tremola.contacts[reversedHistory[i][0]].initial + + var entryHTML = "
" + entryHTML += "" + entryHTML += "
" + entryHTML += "
" + reversedHistory[i][1] + "
" + entryHTML += "
" + + document.getElementById('menu_history_content').innerHTML += entryHTML + + } + document.getElementById('div:menu_history').style.display = 'initial' +} + +function history_sort_select(obj) { + var history = document.getElementById('menu_history_content') + switch (obj.value) { + case('latest_first'): + if (history.style.flexDirection != 'column') + history.style.flexDirection = 'column' + break + case('oldest_first'): + if (history.style.flexDirection != 'column-reverse') { + history.style.flexDirection = 'column-reverse' + history.scrollTo(0, -history.scrollHeight) + } + + break + } +} + +function menu_board_invitations() { + closeOverlay() + document.getElementById("kanban-invitations-overlay").style.display = 'initial'; + document.getElementById("overlay-bg").style.display = 'initial'; + document.getElementById("kanban_invitations_list").innerHTML = "" + + for (var bid in tremola.board) { + menu_board_invitation_create_entry(bid) + } +} + +// creates new entry in invitation (to accept or reject invitations) or updates existing entry +function menu_board_invitation_create_entry(bid) { + + var board = tremola.board[bid] + + if (document.getElementById("kanban_invitation_" + bid)) { + if (board.subscribed || !(myId in board.pendingInvitations)) + document.getElementById("kanban_invitation_" + bid).outerHTML = "" + else + document.getElementById("kanban_invitation_" + bid + "_name").innerHTML = board.name.length < 15 ? board.name : board.name.slice(0, 15) + '...' + return + } + + + if (board.subscribed) // already subscribed + return + + console.log("Create invitation for BOARD: " + bid) + console.log("PENDING LIST: " + Object.keys(board.pendingInvitations)) + + if (!(myId in board.pendingInvitations)) // not invited + return + + var invitationId = board.pendingInvitations[myId][0] + var inviteUserId = board.operations[invitationId].fid + var inviteUserName = tremola.contacts[inviteUserId].alias + var board_name = board.name.length < 15 ? board.name : board.name.slice(0, 15) + '...' + + + var invHTML = "
" + invHTML += "
" + invHTML += "
" + board_name + "
" + invHTML += "
From: " + inviteUserName + "
" + + invHTML += "
" + invHTML += "
" + //invHTML += "
" + invHTML += ""//
" + invHTML += "" + invHTML += "
" + + document.getElementById("kanban_invitations_list").innerHTML += invHTML +} + +function btn_invite_accept(bid) { + inviteAccept(bid, tremola.board[bid].pendingInvitations[myId]) + delete tremola.board[bid].pendingInvitations[myId] + var inv = document.getElementById("kanban_invitation_" + bid) + launch_snackbar("Invitation accepted") + if (inv) + inv.outerHTML = "" +} + +function btn_invite_decline(bid) { + inviteDecline(bid, tremola.board[bid].pendingInvitations[myId]) + delete tremola.board[bid].pendingInvitations[myId] + var inv = document.getElementById("kanban_invitation_" + bid) + launch_snackbar("Invitation declined") + if (inv) + inv.outerHTML = "" +} + +function menu_new_board() { + closeOverlay() + fill_members(); + prev_scenario = 'kanban'; + setScenario("members"); + + document.getElementById("div:textarea").style.display = 'none'; + document.getElementById("div:confirm-members").style.display = 'flex'; + document.getElementById("tremolaTitle").style.display = 'none'; + var c = document.getElementById("conversationTitle"); + c.style.display = null; + c.innerHTML = "Create New Board
Select members to invite"; + document.getElementById('plus').style.display = 'none'; +} + + +function menu_new_board_name() { + menu_edit('new_board', 'Enter the name of the new board', '') +} + +function menu_rename_board() { + var board = tremola.board[curr_board] + menu_edit('board_rename', 'Enter a new name for this board', board.name) +} + +function ui_update_board_title(bid) { + var board = tremola.board[bid] + // update board list + load_board_list() + // update title name + if (curr_board == bid) { + var title = document.getElementById("conversationTitle"), bg, box; + title.style.display = null; + title.setAttribute('classList', bid.forgotten ? ['gray'] : []); + box = "
" + "Kanban: " + escapeHTML(board.name) + "
"; + box += "
" + escapeHTML(recps2display(board.members)) + "
"; + title.innerHTML = box; + } +} + +function board_toggle_forget() { + var board = tremola.board[curr_board] + board.forgotten = !board.forgotten + persist() + closeOverlay() + load_board_list() + setScenario('kanban') +} + +function menu_invite() { + var board = tremola.board[curr_board] + closeOverlay() + + if (board.flags.includes(FLAG.PERSONAL)) { + launch_snackbar("You can't invite other people to your personal board!") + return + } + + document.getElementById("div:invite_menu").style.display = 'initial'; + document.getElementById("overlay-bg").style.display = 'initial'; + + document.getElementById("menu_invite_content").innerHTML = '' + + for (var c in tremola.contacts) { + menu_invite_create_entry(c) + } +} + +// adds an entry to the invite menu or updates an already existing entry +function menu_invite_create_entry(id) { + var board = tremola.board[curr_board] + + if (document.getElementById("div:invite_menu").style.display == 'none') + return + + if (document.getElementById('invite_' + id)) { + if (board.members.indexOf(id) >= 0) + document.getElementById('invite_' + id).outerHTML = '' + else if (id in board.pendingInvitations) { + document.getElementById('invite_' + id).classList.add("gray") + document.getElementById('invite_author_' + id).innerHTML = 'Already Invited' + document.getElementById('invite_btn_' + id).style.display = 'none' + } else { + console.log("enable invite for" + id) + document.getElementById('invite_' + id).classList.remove("gray") + document.getElementById('invite_author_' + id).innerHTML = '' + document.getElementById('invite_btn_' + id).style.display = 'initial' + } + + + return + } + + if (id == myId || board.members.indexOf(id) >= 0) + return + + var isAlreadyInvited = id in board.pendingInvitations + var bg = isAlreadyInvited ? ' gray' : ' light' + + var invHTML = "
" + invHTML += "
" + invHTML += "
" + tremola.contacts[id].alias + "
" + + if (isAlreadyInvited) + invHTML += "
Already Invited
" + else + invHTML += "
" + + invHTML += "
" + invHTML += "
" + if (!isAlreadyInvited) + invHTML += "" + invHTML += "
" + + document.getElementById("menu_invite_content").innerHTML += invHTML +} + +function btn_invite(userId, bid) { + console.log("INVITE: " + userId + ", bid: " + bid) + inviteUser(bid, userId) + launch_snackbar("User invited") +} + +function leave_curr_board() { + closeOverlay() + + if (tremola.board[curr_board].flags.includes(FLAG.PERSONAL)) { + launch_snackbar("You can't leave your personal board!") + return + } + + leave(curr_board) + setScenario('kanban') +} + +/* + Columns +*/ +function menu_new_column() { + menu_edit("board_new_column", "Enter name of new List: ", "") +} + +function load_column(columnID) { + var board = tremola.board[curr_board] + + if (document.getElementById(columnID + "-columnWrapper")) { + document.getElementById(columnID + "-columnWrapper").outerHTML = "" + } + + if (board.columns[columnID].removed) { + return + } + + var column_name = board.columns[columnID].name + var column_position = board.columns[columnID].position + + var columnsHTML = "
" + columnsHTML += "
" + columnsHTML += "
" + columnsHTML += "
" + column_name + "
" + columnsHTML += "
" + columnsHTML += "
" + //columnsHTML += "
" + columnsHTML += "" + columnsHTML += "
" + + document.getElementById("div:columns_container").innerHTML += columnsHTML + +} + +function load_all_columns() { + var board = tremola.board[curr_board] + for (var i in board.columns) { + load_column(i) + } +} + +function context_menu_column_options(columnID) { + close_board_context_menu() + document.getElementById("overlay-trans").style.display = 'initial'; + var context_menu = document.getElementById('context_options-' + columnID) + context_menu.style.display = 'block' + curr_context_menu = 'context_options-' + columnID + context_menu.innerHTML = "" + context_menu.innerHTML += "" + // context_menu.innerHTML += "" + context_menu.innerHTML += "" + overlayIsActive = true +} + +function contextmenu_move_column(columnID) { + document.getElementById('context_options-' + columnID).innerHTML = '' + var board = tremola.board[curr_board] + var availablePos = []; + for (var i = 1; i <= board.numOfActiveColumns; i++) { + availablePos.push(i); + } + availablePos.splice(availablePos.indexOf(board.columns[columnID].position), 1) + var menuHTML = "" + + for (var i in availablePos) { + menuHTML += "" + } + + document.getElementById('context_options-' + columnID).innerHTML = menuHTML +} + +function menu_rename_column(columnID) { + curr_column = columnID + menu_edit('board_rename_column', 'Enter new name: ', tremola.board[curr_board].columns[columnID].name) +} + +function btn_remove_column(columnID) { + removeColumn(curr_board, columnID) + closeOverlay() +} + +function ui_rename_column(columnID, new_name) { + var wrapper = document.getElementById(columnID + '-columnWrapper') + + if (!wrapper) { + load_column(columnID) + return + } + + wrapper.getElementsByClassName('column_hdr')[0].getElementsByTagName('b')[0].innerHTML = new_name +} + +function ui_remove_column(columnID) { + var board = tremola.board[curr_board] + + if (!document.getElementById(columnID + "-columnWrapper")) + return + document.getElementById(columnID + "-columnWrapper").outerHTML = "" //remove column from ui +} + +function ui_move_column(columnID, insertPos) { + console.log('move', columnID, insertPos); + closeOverlay() + insertPos = parseInt(insertPos) + var board = tremola.board[curr_board] + var oldPos = board.columns[columnID].position + if (oldPos == insertPos) // column is already on given position + return + + for (var i in board.columns) { + var curr_column = board.columns[i] + + if (curr_column.removed) + continue + + if (i == columnID) + continue + + if (oldPos > insertPos) { + if (curr_column.position < insertPos) { + continue + } else if (curr_column.position > board.columns[columnID].position) { + continue + } else { + document.getElementById(i + "-columnWrapper").style.order = ++curr_column.position + } + } else { + if (curr_column.position < board.columns[columnID].position) { + continue + } else if (curr_column.position > insertPos) { + continue + } else { + document.getElementById(i + "-columnWrapper").style.order = --curr_column.position + } + } + + } + document.getElementById(columnID + '-columnWrapper').style.order = insertPos + board.columns[columnID].position = insertPos + persist() +} + +/* + Items +*/ + +function menu_create_item(columnID) { + curr_column = columnID + menu_edit('board_new_item', 'Enter name of new Card', '') +} + +function load_item(itemID) { + var board = tremola.board[curr_board] + + var name = board.items[itemID].name + var pos = board.items[itemID].position + var columnID = board.items[itemID].curr_column + var color = board.items[itemID].color + + if (document.getElementById(itemID + "-item")) { + document.getElementById(itemID + "-item").outerHTML = "" + } + + if (board.items[itemID].removed || board.columns[columnID].removed) + return + + var itemHTML = "
" //board_item_button + itemHTML += "
" + name + "
" + itemHTML += "
" + board.items[itemID].description + "
" + itemHTML += "
" + + for (var i in board.items[itemID].assignees) { + var assigneeID = board.items[itemID].assignees[i] + var assigneeInitial = tremola.contacts[assigneeID].initial + var assigneeColor = tremola.contacts[assigneeID].color + + itemHTML += "
" + itemHTML += "" + itemHTML += "
" + } + + itemHTML += "
" // + + document.getElementById(columnID + "-columnContent").innerHTML += itemHTML; + document.getElementById(itemID + "-item").addEventListener('mousedown', myTouchFct, false); +} + +function load_all_items() { + var board = tremola.board[curr_board] + for (var i in board.items) { + load_item(i) + } +} + +function item_menu(itemID) { + closeOverlay() + curr_rename_item = itemID + curr_item = itemID + document.getElementById('overlay-bg').style.display = 'initial'; + overlayIsActive = true; + + var item = tremola.board[curr_board].items[itemID] + document.getElementById('div:item_menu').style.display = 'initial' + document.getElementById('btn:item_menu_description_save').style.display = 'none' + document.getElementById('btn:item_menu_description_cancel').style.display = 'none' + document.getElementById('item_menu_comment_text').value = '' + document.getElementById('item_menu_title').innerHTML = "" + item.name + "" + // console.log(item) + document.getElementById('item_menu_title').innerHTML = "" + item.name + "" + + //load description + var descDiv = document.getElementById('div:item_menu_description') + document.getElementById('div:item_menu_description_text').value = item.description + + document.getElementById('div:item_menu_description_text').addEventListener('focus', + function (event) { + document.getElementById('btn:item_menu_description_save').style.display = 'initial' + document.getElementById('btn:item_menu_description_cancel').style.display = 'initial' + event.target.style.border = "solid 2px black" + document.getElementById('div:item_menu_description_text').removeEventListener('focus', event) + }); + + //load chat + var draftDiv = document.getElementById('div:item_menu_comments') + var commentsHTML = '' + var commentsList = item.comments.slice().reverse() + for (var i in commentsList) { + var author_name = tremola.contacts[commentsList[i][0]].alias + var author_color = tremola.contacts[commentsList[i][0]].color + var author_initial = tremola.contacts[commentsList[i][0]].initial + + commentsHTML += "
" + commentsHTML += "" + commentsHTML += "
" + commentsHTML += "
" + commentsList[i][1] + "
" + commentsHTML += "
" + } + document.getElementById('lst:item_menu_posts').innerHTML = commentsHTML + + //load assignees display + var assigneesDiv = document.getElementById('div:item_menu_assignees') + var order = 0; + var assigneesHTML = "" + for (var i in item.assignees) { + var author_color = tremola.contacts[item.assignees[i]].color + var author_initial = tremola.contacts[item.assignees[i]].initial + assigneesHTML += "" + } + document.getElementById("div:item_menu_assignees").innerHTML = assigneesHTML +} + +function item_menu_save_description() { + var new_description = document.getElementById('div:item_menu_description_text').value + var item = tremola.board[curr_board].items[curr_item] + + if (item.description != new_description) { + setItemDescription(curr_board, curr_item, new_description) + } + document.getElementById('btn:item_menu_description_save').style.display = 'none' + document.getElementById('div:item_menu_description_text').style.border = 'none' + document.getElementById('btn:item_menu_description_cancel').style.display = 'none' +} + +function item_menu_cancel_description() { + document.getElementById('div:item_menu_description_text').style.border = 'none' + document.getElementById('div:item_menu_description_text').value = tremola.board[curr_board].items[curr_item].description + document.getElementById('btn:item_menu_description_save').style.display = 'none' + document.getElementById('btn:item_menu_description_cancel').style.display = 'none' +} + + +function contextmenu_change_column() { + close_board_context_menu() + document.getElementById("overlay-trans").style.display = 'initial'; + + var board = tremola.board[curr_board] + var OptionHTML = "
" + + if (board.numOfActiveColumns <= 1) { + launch_snackbar("There is only one list") + } + + var columnPositionList = Object.keys(board.columns).map(function (key) { + return [key, board.columns[key].position]; + }) + columnPositionList.sort(function (a, b) { + return a[1] - b[1]; + }) + + for (var i in columnPositionList) { + var columnID = columnPositionList[i][0] + if ((board.items[curr_item].curr_column == columnID) || (board.columns[columnID].removed)) + continue + + OptionHTML += "" + } + OptionHTML += "
" + + curr_context_menu = 'context_options-' + curr_item + "-changeColumn" + document.getElementById("change_column_options").innerHTML = OptionHTML + document.getElementById(curr_context_menu).style.display = 'block' + overlayIsActive = true +} + +function btn_move_item(item, new_column) { + moveItem(curr_board, item, new_column) + close_board_context_menu() +} + +function btn_remove_item() { + removeItem(curr_board, curr_item) + closeOverlay() +} + +function contextmenu_item_change_position() { + close_board_context_menu() + + var board = tremola.board[curr_board] + var itemList = board.columns[board.items[curr_item].curr_column].item_ids + var itemPosList = [] + + for (var i in itemList) { + var item = board.items[itemList[i]] + + if (item.removed) + continue + if (item.id == board.items[curr_item].id) + continue + + itemPosList.push([i, item.position]) + } + itemPosList.sort(function (a, b) { + return a[1] - b[1]; + }) + // console.log(itemPosList) + var posHTML = "
" + for (var i in itemPosList) { + posHTML += "" + } + curr_context_menu = 'context_options-' + curr_item + "-changePosition" + document.getElementById("change_position_options").innerHTML = posHTML + document.getElementById(curr_context_menu).style.display = 'block' + overlayIsActive = true +} + +function contextmenu_item_assign() { + close_board_context_menu() + var board = tremola.board[curr_board] + + var aliasList = [] + + for (var m in board.members) { + aliasList.push([tremola.contacts[board.members[m]].alias, board.members[m]]) + } + aliasList.sort() + + var assignHTML = "
" + for (var i in aliasList) { + var alias = aliasList[i][0] + var fid = aliasList[i][1] + var assigned = board.items[curr_item].assignees.indexOf(fid) >= 0 + var color = assigned ? "background-color: #aaf19f;" : ""; + + assignHTML += "" + } + curr_context_menu = 'context_options-' + curr_item + "-assign" + document.getElementById("assign_options").innerHTML = assignHTML + document.getElementById(curr_context_menu).style.display = 'block' + overlayIsActive = true +} + +function btn_post_comment() { + var comment = document.getElementById('item_menu_comment_text').value + if (comment == '') + return + + postItemComment(curr_board, curr_item, comment) + item_menu(curr_item) +} + +function btn_rename_item() { + +} + +function ui_change_item_order(itemID, newPos) { + close_board_context_menu() + newPos = parseInt(newPos) + var board = tremola.board[curr_board] + var oldPos = board.items[itemID].position + var column = board.columns[board.items[itemID].curr_column] + + if (oldPos == newPos) // column is already on given position + return + + for (var i of column.item_ids) { + var curr_item = board.items[i] + + if (curr_item.removed) + continue + + if (i == itemID) + continue + + if (oldPos > newPos) { + if (curr_item.position < newPos) { + continue + } else if (curr_item.position > board.items[itemID].position) { + continue + } else { + document.getElementById(i + "-item").style.order = ++curr_item.position + } + } else { + if (curr_item.position < board.items[itemID].position) { + continue + } else if (curr_item.position > newPos) { + continue + } else { + document.getElementById(i + "-item").style.order = --curr_item.position + } + } + + } + document.getElementById(itemID + '-item').style.order = newPos + board.items[itemID].position = newPos + persist() +} + +function ui_remove_item(itemID) { + var board = tremola.board[curr_board] + var column = board.columns[board.items[itemID].curr_column] + + if (!document.getElementById(itemID + '-item')) + return + + for (var i in column.item_ids) { + var curr_item = board.items[column.item_ids[i]] + if (curr_item.position > board.items[itemID].position) { + curr_item.position-- + document.getElementById(curr_item.id + '-item').style.order = curr_item.position + } + } + + document.getElementById(itemID + '-item').outerHTML = "" +} + +function ui_item_assign(fid) { + //close_board_context_menu() + var board = tremola.board[curr_board] + var alreadyAssigned = board.items[curr_item].assignees.includes(fid) + + if (alreadyAssigned) + unassignFromItem(curr_board, curr_item, fid) + else + assignToItem(curr_board, curr_item, fid) +} + +function ui_move_item(itemID, old_pos) { + var board = tremola.board[curr_board] + var item = board.items[itemID] + var old_column = board.columns[item.curr_column] + + document.getElementById(itemID + '-item').style.order = item.position + for (var i in old_column.item_ids) { + if (board.items[old_column.item_ids[i]].position > old_pos) + document.getElementById(old_column.item_ids[i] + '-item').style.order = --board.items[old_column.item_ids[i]].position + } + load_item(curr_op.body.cmd[1]) +} + +function contextmenu_change_color() { + close_board_context_menu() + var board = tremola.board[curr_board] + var item = board.items[curr_item] + + var colors = Object.values(Color) + colors.splice(colors.indexOf(item.color), 1) + + var colorHTML = "
" + + for (var c of colors) { + colorHTML += "" + } + + curr_context_menu = 'context_options-' + curr_item + "-color" + document.getElementById("change_color_options").innerHTML = colorHTML + document.getElementById(curr_context_menu).style.display = 'block' + overlayIsActive = true +} + +function btn_change_item_color(iid, color) { + close_board_context_menu() + setItemColor(curr_board, iid, color) +} + +function ui_update_item_name(itemID, new_name) { + var hdr = document.getElementById(itemID + '-itemHdr') + + if (hdr) { + hdr.innerHTML = new_name + } else { + load_item(itemID) + } + + if (curr_item == itemID) { + var itemMenuHdr = document.getElementById(itemID + '-itemMenuHdr') + if (itemMenuHdr) { + itemMenuHdr.innerHTML = new_name + } else { + item_menu(itemID) + } + } +} + +function ui_update_item_color(itemID, new_color) { + var hdr = document.getElementById(itemID + '-itemHdr') + + if (hdr) { + hdr.style.color = new_color + } else { + load_item(itemID) + } + + if (curr_item == itemID) { + var itemMenuHdr = document.getElementById(itemID + '-itemMenuHdr') + if (itemMenuHdr) { + itemMenuHdr.style.color = new_color + } else { + item_menu(itemID) + } + if (curr_context_menu && curr_context_menu.split('-')[2] == 'color') + contextmenu_change_color() + } +} + +function ui_update_item_description(itemID, new_descr) { + var itemDescr = document.getElementById(itemID + '-itemDescr') + + + if (itemDescr) { + itemDescr.innerHTML = new_descr + } else { + load_item(itemID) + } + + if (curr_item == itemID) { + var itemMenuDescr = document.getElementById('div:item_menu_description_text') + if (itemMenuDescr) { + itemMenuDescr.value = new_descr + } else { + item_menu(itemID) + } + } +} + +function ui_update_item_assignees(itemID) { + var board = tremola.board[curr_board] + var item = board.items[itemID] + + var assigneesWrapper = document.getElementById(itemID + '-itemAssignees') + if (assigneesWrapper) { + var assigneesHTML = '' + for (var i in board.items[itemID].assignees) { + var assigneeID = board.items[itemID].assignees[i] + var assigneeInitial = tremola.contacts[assigneeID].initial + var assigneeColor = tremola.contacts[assigneeID].color + + assigneesHTML += "" + } + assigneesWrapper.innerHTML = assigneesHTML + } else { + load_item(itemID) + } + + if (curr_item == itemID) { + var assigneesMenuWrapper = document.getElementById('div:item_menu_assignees') + if (assigneesMenuWrapper) { + var assigneesMenuHTML = "" + for (var i in item.assignees) { + var author_color = tremola.contacts[item.assignees[i]].color + var author_initial = tremola.contacts[item.assignees[i]].initial + assigneesMenuHTML += "" + } + assigneesMenuWrapper.innerHTML = assigneesMenuHTML + } else { + item_menu(itemID) + } + if (curr_context_menu && curr_context_menu.split('-')[2] == 'assign') { + contextmenu_item_assign() + } + } +} + +function ui_item_update_chat(itemID) { + var board = tremola.board[curr_board] + var item = board.items[itemID] + + if (curr_item == itemID) { + var chatWrapper = document.getElementById('lst:item_menu_posts') + if (chatWrapper) { + var commentsHTML = '' + var commentsList = [...item.comments].reverse() + for (var i in commentsList) { + var author_name = tremola.contacts[commentsList[i][0]].alias + var author_color = tremola.contacts[commentsList[i][0]].color + var author_initial = tremola.contacts[commentsList[i][0]].initial + + commentsHTML += "
" + commentsHTML += "" + commentsHTML += "
" + commentsHTML += "
" + commentsList[i][1] + "
" + commentsHTML += "
" + } + chatWrapper.innerHTML = commentsHTML + } else { + item_menu(itemID) + } + } +} + +function ui_update_item_move_to_column(itemID, columnID, newPos) { + var itemHTML = document.getElementById(itemID + "-item").outerHTML + document.getElementById(itemID + "-item").outerHTML = '' + document.getElementById(columnID + "-columnContent").innerHTML += itemHTML + document.getElementById(itemID + '-item').style.order = newPos + + if (curr_item == itemID) { + if (curr_context_menu) { + if (curr_context_menu.split('-')[2] == 'changePosition') + contextmenu_item_change_position() + else if (curr_context_menu.split('-')[2] == 'changeColumn') + contextmenu_change_column() + } + } +} diff --git a/js/tinySSB-for-Chrome/img/back.svg b/js/tinySSB-for-Chrome/img/back.svg new file mode 100644 index 0000000..7d36575 --- /dev/null +++ b/js/tinySSB-for-Chrome/img/back.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/tinySSB-for-Chrome/img/cancel.svg b/js/tinySSB-for-Chrome/img/cancel.svg new file mode 100644 index 0000000..a1aa942 --- /dev/null +++ b/js/tinySSB-for-Chrome/img/cancel.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/tinySSB-for-Chrome/img/chat.svg b/js/tinySSB-for-Chrome/img/chat.svg new file mode 100644 index 0000000..c261e9f --- /dev/null +++ b/js/tinySSB-for-Chrome/img/chat.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/tinySSB-for-Chrome/img/checked.svg b/js/tinySSB-for-Chrome/img/checked.svg new file mode 100644 index 0000000..f06c0f7 --- /dev/null +++ b/js/tinySSB-for-Chrome/img/checked.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/tinySSB-for-Chrome/img/chnk-icon.svg b/js/tinySSB-for-Chrome/img/chnk-icon.svg new file mode 100644 index 0000000..93fe449 --- /dev/null +++ b/js/tinySSB-for-Chrome/img/chnk-icon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/js/tinySSB-for-Chrome/img/color-wheel.png b/js/tinySSB-for-Chrome/img/color-wheel.png new file mode 100644 index 0000000..d541c33 Binary files /dev/null and b/js/tinySSB-for-Chrome/img/color-wheel.png differ diff --git a/js/tinySSB-for-Chrome/img/contacts.svg b/js/tinySSB-for-Chrome/img/contacts.svg new file mode 100644 index 0000000..14bc9c1 --- /dev/null +++ b/js/tinySSB-for-Chrome/img/contacts.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/tinySSB-for-Chrome/img/eraser.png b/js/tinySSB-for-Chrome/img/eraser.png new file mode 100644 index 0000000..e92b9ec Binary files /dev/null and b/js/tinySSB-for-Chrome/img/eraser.png differ diff --git a/js/tinySSB-for-Chrome/img/fountain.png b/js/tinySSB-for-Chrome/img/fountain.png new file mode 100644 index 0000000..26ec912 Binary files /dev/null and b/js/tinySSB-for-Chrome/img/fountain.png differ diff --git a/js/tinySSB-for-Chrome/img/goset-icon.svg b/js/tinySSB-for-Chrome/img/goset-icon.svg new file mode 100644 index 0000000..a5871a5 --- /dev/null +++ b/js/tinySSB-for-Chrome/img/goset-icon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/js/tinySSB-for-Chrome/img/inbox.svg b/js/tinySSB-for-Chrome/img/inbox.svg new file mode 100644 index 0000000..c092ffb --- /dev/null +++ b/js/tinySSB-for-Chrome/img/inbox.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/tinySSB-for-Chrome/img/kanban.svg b/js/tinySSB-for-Chrome/img/kanban.svg new file mode 100644 index 0000000..0be95eb --- /dev/null +++ b/js/tinySSB-for-Chrome/img/kanban.svg @@ -0,0 +1,3 @@ + + + diff --git a/js/tinySSB-for-Chrome/img/map-1862.jpg b/js/tinySSB-for-Chrome/img/map-1862.jpg new file mode 100644 index 0000000..2c97206 Binary files /dev/null and b/js/tinySSB-for-Chrome/img/map-1862.jpg differ diff --git a/js/tinySSB-for-Chrome/img/paperclip.svg b/js/tinySSB-for-Chrome/img/paperclip.svg new file mode 100644 index 0000000..f4d06c2 --- /dev/null +++ b/js/tinySSB-for-Chrome/img/paperclip.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/tinySSB-for-Chrome/img/playbook.svg b/js/tinySSB-for-Chrome/img/playbook.svg new file mode 100644 index 0000000..8235a17 --- /dev/null +++ b/js/tinySSB-for-Chrome/img/playbook.svg @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/js/tinySSB-for-Chrome/img/qr-code-scan.svg b/js/tinySSB-for-Chrome/img/qr-code-scan.svg new file mode 100644 index 0000000..dba11b9 --- /dev/null +++ b/js/tinySSB-for-Chrome/img/qr-code-scan.svg @@ -0,0 +1,15 @@ + + + +Created with Fabric.js 1.7.22 + + + + + + + + + + + diff --git a/js/tinySSB-for-Chrome/img/receive.png b/js/tinySSB-for-Chrome/img/receive.png new file mode 100644 index 0000000..ada10b0 Binary files /dev/null and b/js/tinySSB-for-Chrome/img/receive.png differ diff --git a/js/tinySSB-for-Chrome/img/record.svg b/js/tinySSB-for-Chrome/img/record.svg new file mode 100644 index 0000000..19a44ec --- /dev/null +++ b/js/tinySSB-for-Chrome/img/record.svg @@ -0,0 +1,7 @@ + + + + + Svg Vector Icons : http://www.onlinewebfonts.com/icon + + \ No newline at end of file diff --git a/js/tinySSB-for-Chrome/img/send-button.svg b/js/tinySSB-for-Chrome/img/send-button.svg new file mode 100644 index 0000000..84dc63f --- /dev/null +++ b/js/tinySSB-for-Chrome/img/send-button.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/js/tinySSB-for-Chrome/img/send.svg b/js/tinySSB-for-Chrome/img/send.svg new file mode 100644 index 0000000..fee1d8f --- /dev/null +++ b/js/tinySSB-for-Chrome/img/send.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/tinySSB-for-Chrome/img/signal.svg b/js/tinySSB-for-Chrome/img/signal.svg new file mode 100644 index 0000000..e9fd0fa --- /dev/null +++ b/js/tinySSB-for-Chrome/img/signal.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/tinySSB-for-Chrome/img/splash-as-background.jpg b/js/tinySSB-for-Chrome/img/splash-as-background.jpg new file mode 100644 index 0000000..53e9c1b Binary files /dev/null and b/js/tinySSB-for-Chrome/img/splash-as-background.jpg differ diff --git a/js/tinySSB-for-Chrome/pako.min.js b/js/tinySSB-for-Chrome/pako.min.js new file mode 100644 index 0000000..2535eaf --- /dev/null +++ b/js/tinySSB-for-Chrome/pako.min.js @@ -0,0 +1,2 @@ +/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).pako={})}(this,(function(t){"use strict";function e(t){let e=t.length;for(;--e>=0;)t[e]=0}const a=256,i=286,n=30,s=15,r=new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]),o=new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]),l=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]),h=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),d=new Array(576);e(d);const _=new Array(60);e(_);const f=new Array(512);e(f);const c=new Array(256);e(c);const u=new Array(29);e(u);const w=new Array(n);function m(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}let b,g,p;function k(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}e(w);const v=t=>t<256?f[t]:f[256+(t>>>7)],y=(t,e)=>{t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255},x=(t,e,a)=>{t.bi_valid>16-a?(t.bi_buf|=e<>16-t.bi_valid,t.bi_valid+=a-16):(t.bi_buf|=e<{x(t,a[2*e],a[2*e+1])},A=(t,e)=>{let a=0;do{a|=1&t,t>>>=1,a<<=1}while(--e>0);return a>>>1},E=(t,e,a)=>{const i=new Array(16);let n,r,o=0;for(n=1;n<=s;n++)o=o+a[n-1]<<1,i[n]=o;for(r=0;r<=e;r++){let e=t[2*r+1];0!==e&&(t[2*r]=A(i[e]++,e))}},R=t=>{let e;for(e=0;e{t.bi_valid>8?y(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0},U=(t,e,a,i)=>{const n=2*e,s=2*a;return t[n]{const i=t.heap[a];let n=a<<1;for(;n<=t.heap_len&&(n{let n,s,l,h,d=0;if(0!==t.sym_next)do{n=255&t.pending_buf[t.sym_buf+d++],n+=(255&t.pending_buf[t.sym_buf+d++])<<8,s=t.pending_buf[t.sym_buf+d++],0===n?z(t,s,e):(l=c[s],z(t,l+a+1,e),h=r[l],0!==h&&(s-=u[l],x(t,s,h)),n--,l=v(n),z(t,l,i),h=o[l],0!==h&&(n-=w[l],x(t,n,h)))}while(d{const a=e.dyn_tree,i=e.stat_desc.static_tree,n=e.stat_desc.has_stree,r=e.stat_desc.elems;let o,l,h,d=-1;for(t.heap_len=0,t.heap_max=573,o=0;o>1;o>=1;o--)S(t,a,o);h=r;do{o=t.heap[1],t.heap[1]=t.heap[t.heap_len--],S(t,a,1),l=t.heap[1],t.heap[--t.heap_max]=o,t.heap[--t.heap_max]=l,a[2*h]=a[2*o]+a[2*l],t.depth[h]=(t.depth[o]>=t.depth[l]?t.depth[o]:t.depth[l])+1,a[2*o+1]=a[2*l+1]=h,t.heap[1]=h++,S(t,a,1)}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],((t,e)=>{const a=e.dyn_tree,i=e.max_code,n=e.stat_desc.static_tree,r=e.stat_desc.has_stree,o=e.stat_desc.extra_bits,l=e.stat_desc.extra_base,h=e.stat_desc.max_length;let d,_,f,c,u,w,m=0;for(c=0;c<=s;c++)t.bl_count[c]=0;for(a[2*t.heap[t.heap_max]+1]=0,d=t.heap_max+1;d<573;d++)_=t.heap[d],c=a[2*a[2*_+1]+1]+1,c>h&&(c=h,m++),a[2*_+1]=c,_>i||(t.bl_count[c]++,u=0,_>=l&&(u=o[_-l]),w=a[2*_],t.opt_len+=w*(c+u),r&&(t.static_len+=w*(n[2*_+1]+u)));if(0!==m){do{for(c=h-1;0===t.bl_count[c];)c--;t.bl_count[c]--,t.bl_count[c+1]+=2,t.bl_count[h]--,m-=2}while(m>0);for(c=h;0!==c;c--)for(_=t.bl_count[c];0!==_;)f=t.heap[--d],f>i||(a[2*f+1]!==c&&(t.opt_len+=(c-a[2*f+1])*a[2*f],a[2*f+1]=c),_--)}})(t,e),E(a,d,t.bl_count)},O=(t,e,a)=>{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),e[2*(a+1)+1]=65535,i=0;i<=a;i++)n=r,r=e[2*(i+1)+1],++o{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),i=0;i<=a;i++)if(n=r,r=e[2*(i+1)+1],!(++o{x(t,0+(i?1:0),3),Z(t),y(t,a),y(t,~a),a&&t.pending_buf.set(t.window.subarray(e,e+a),t.pending),t.pending+=a};var N=(t,e,i,n)=>{let s,r,o=0;t.level>0?(2===t.strm.data_type&&(t.strm.data_type=(t=>{let e,i=4093624447;for(e=0;e<=31;e++,i>>>=1)if(1&i&&0!==t.dyn_ltree[2*e])return 0;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return 1;for(e=32;e{let e;for(O(t,t.dyn_ltree,t.l_desc.max_code),O(t,t.dyn_dtree,t.d_desc.max_code),T(t,t.bl_desc),e=18;e>=3&&0===t.bl_tree[2*h[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e})(t),s=t.opt_len+3+7>>>3,r=t.static_len+3+7>>>3,r<=s&&(s=r)):s=r=i+5,i+4<=s&&-1!==e?L(t,e,i,n):4===t.strategy||r===s?(x(t,2+(n?1:0),3),D(t,d,_)):(x(t,4+(n?1:0),3),((t,e,a,i)=>{let n;for(x(t,e-257,5),x(t,a-1,5),x(t,i-4,4),n=0;n{F||((()=>{let t,e,a,h,k;const v=new Array(16);for(a=0,h=0;h<28;h++)for(u[h]=a,t=0;t<1<>=7;h(t.pending_buf[t.sym_buf+t.sym_next++]=e,t.pending_buf[t.sym_buf+t.sym_next++]=e>>8,t.pending_buf[t.sym_buf+t.sym_next++]=i,0===e?t.dyn_ltree[2*i]++:(t.matches++,e--,t.dyn_ltree[2*(c[i]+a+1)]++,t.dyn_dtree[2*v(e)]++),t.sym_next===t.sym_end),_tr_align:t=>{x(t,2,3),z(t,256,d),(t=>{16===t.bi_valid?(y(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)})(t)}};var C=(t,e,a,i)=>{let n=65535&t|0,s=t>>>16&65535|0,r=0;for(;0!==a;){r=a>2e3?2e3:a,a-=r;do{n=n+e[i++]|0,s=s+n|0}while(--r);n%=65521,s%=65521}return n|s<<16|0};const M=new Uint32Array((()=>{let t,e=[];for(var a=0;a<256;a++){t=a;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[a]=t}return e})());var H=(t,e,a,i)=>{const n=M,s=i+a;t^=-1;for(let a=i;a>>8^n[255&(t^e[a])];return-1^t},j={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"},K={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8};const{_tr_init:P,_tr_stored_block:Y,_tr_flush_block:G,_tr_tally:X,_tr_align:W}=B,{Z_NO_FLUSH:q,Z_PARTIAL_FLUSH:J,Z_FULL_FLUSH:Q,Z_FINISH:V,Z_BLOCK:$,Z_OK:tt,Z_STREAM_END:et,Z_STREAM_ERROR:at,Z_DATA_ERROR:it,Z_BUF_ERROR:nt,Z_DEFAULT_COMPRESSION:st,Z_FILTERED:rt,Z_HUFFMAN_ONLY:ot,Z_RLE:lt,Z_FIXED:ht,Z_DEFAULT_STRATEGY:dt,Z_UNKNOWN:_t,Z_DEFLATED:ft}=K,ct=258,ut=262,wt=42,mt=113,bt=666,gt=(t,e)=>(t.msg=j[e],e),pt=t=>2*t-(t>4?9:0),kt=t=>{let e=t.length;for(;--e>=0;)t[e]=0},vt=t=>{let e,a,i,n=t.w_size;e=t.hash_size,i=e;do{a=t.head[--i],t.head[i]=a>=n?a-n:0}while(--e);e=n,i=e;do{a=t.prev[--i],t.prev[i]=a>=n?a-n:0}while(--e)};let yt=(t,e,a)=>(e<{const e=t.state;let a=e.pending;a>t.avail_out&&(a=t.avail_out),0!==a&&(t.output.set(e.pending_buf.subarray(e.pending_out,e.pending_out+a),t.next_out),t.next_out+=a,e.pending_out+=a,t.total_out+=a,t.avail_out-=a,e.pending-=a,0===e.pending&&(e.pending_out=0))},zt=(t,e)=>{G(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,xt(t.strm)},At=(t,e)=>{t.pending_buf[t.pending++]=e},Et=(t,e)=>{t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e},Rt=(t,e,a,i)=>{let n=t.avail_in;return n>i&&(n=i),0===n?0:(t.avail_in-=n,e.set(t.input.subarray(t.next_in,t.next_in+n),a),1===t.state.wrap?t.adler=C(t.adler,e,n,a):2===t.state.wrap&&(t.adler=H(t.adler,e,n,a)),t.next_in+=n,t.total_in+=n,n)},Zt=(t,e)=>{let a,i,n=t.max_chain_length,s=t.strstart,r=t.prev_length,o=t.nice_match;const l=t.strstart>t.w_size-ut?t.strstart-(t.w_size-ut):0,h=t.window,d=t.w_mask,_=t.prev,f=t.strstart+ct;let c=h[s+r-1],u=h[s+r];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(a=e,h[a+r]===u&&h[a+r-1]===c&&h[a]===h[s]&&h[++a]===h[s+1]){s+=2,a++;do{}while(h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&sr){if(t.match_start=e,r=i,i>=o)break;c=h[s+r-1],u=h[s+r]}}}while((e=_[e&d])>l&&0!=--n);return r<=t.lookahead?r:t.lookahead},Ut=t=>{const e=t.w_size;let a,i,n;do{if(i=t.window_size-t.lookahead-t.strstart,t.strstart>=e+(e-ut)&&(t.window.set(t.window.subarray(e,e+e-i),0),t.match_start-=e,t.strstart-=e,t.block_start-=e,t.insert>t.strstart&&(t.insert=t.strstart),vt(t),i+=e),0===t.strm.avail_in)break;if(a=Rt(t.strm,t.window,t.strstart+t.lookahead,i),t.lookahead+=a,t.lookahead+t.insert>=3)for(n=t.strstart-t.insert,t.ins_h=t.window[n],t.ins_h=yt(t,t.ins_h,t.window[n+1]);t.insert&&(t.ins_h=yt(t,t.ins_h,t.window[n+3-1]),t.prev[n&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=n,n++,t.insert--,!(t.lookahead+t.insert<3)););}while(t.lookahead{let a,i,n,s=t.pending_buf_size-5>t.w_size?t.w_size:t.pending_buf_size-5,r=0,o=t.strm.avail_in;do{if(a=65535,n=t.bi_valid+42>>3,t.strm.avail_outi+t.strm.avail_in&&(a=i+t.strm.avail_in),a>n&&(a=n),a>8,t.pending_buf[t.pending-2]=~a,t.pending_buf[t.pending-1]=~a>>8,xt(t.strm),i&&(i>a&&(i=a),t.strm.output.set(t.window.subarray(t.block_start,t.block_start+i),t.strm.next_out),t.strm.next_out+=i,t.strm.avail_out-=i,t.strm.total_out+=i,t.block_start+=i,a-=i),a&&(Rt(t.strm,t.strm.output,t.strm.next_out,a),t.strm.next_out+=a,t.strm.avail_out-=a,t.strm.total_out+=a)}while(0===r);return o-=t.strm.avail_in,o&&(o>=t.w_size?(t.matches=2,t.window.set(t.strm.input.subarray(t.strm.next_in-t.w_size,t.strm.next_in),0),t.strstart=t.w_size,t.insert=t.strstart):(t.window_size-t.strstart<=o&&(t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,t.insert>t.strstart&&(t.insert=t.strstart)),t.window.set(t.strm.input.subarray(t.strm.next_in-o,t.strm.next_in),t.strstart),t.strstart+=o,t.insert+=o>t.w_size-t.insert?t.w_size-t.insert:o),t.block_start=t.strstart),t.high_watern&&t.block_start>=t.w_size&&(t.block_start-=t.w_size,t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,n+=t.w_size,t.insert>t.strstart&&(t.insert=t.strstart)),n>t.strm.avail_in&&(n=t.strm.avail_in),n&&(Rt(t.strm,t.window,t.strstart,n),t.strstart+=n,t.insert+=n>t.w_size-t.insert?t.w_size-t.insert:n),t.high_water>3,n=t.pending_buf_size-n>65535?65535:t.pending_buf_size-n,s=n>t.w_size?t.w_size:n,i=t.strstart-t.block_start,(i>=s||(i||e===V)&&e!==q&&0===t.strm.avail_in&&i<=n)&&(a=i>n?n:i,r=e===V&&0===t.strm.avail_in&&a===i?1:0,Y(t,t.block_start,a,r),t.block_start+=a,xt(t.strm)),r?3:1)},Dt=(t,e)=>{let a,i;for(;;){if(t.lookahead=3&&(t.ins_h=yt(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!==a&&t.strstart-a<=t.w_size-ut&&(t.match_length=Zt(t,a)),t.match_length>=3)if(i=X(t,t.strstart-t.match_start,t.match_length-3),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=3){t.match_length--;do{t.strstart++,t.ins_h=yt(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart}while(0!=--t.match_length);t.strstart++}else t.strstart+=t.match_length,t.match_length=0,t.ins_h=t.window[t.strstart],t.ins_h=yt(t,t.ins_h,t.window[t.strstart+1]);else i=X(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++;if(i&&(zt(t,!1),0===t.strm.avail_out))return 1}return t.insert=t.strstart<2?t.strstart:2,e===V?(zt(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(zt(t,!1),0===t.strm.avail_out)?1:2},Tt=(t,e)=>{let a,i,n;for(;;){if(t.lookahead=3&&(t.ins_h=yt(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),t.prev_length=t.match_length,t.prev_match=t.match_start,t.match_length=2,0!==a&&t.prev_length4096)&&(t.match_length=2)),t.prev_length>=3&&t.match_length<=t.prev_length){n=t.strstart+t.lookahead-3,i=X(t,t.strstart-1-t.prev_match,t.prev_length-3),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=n&&(t.ins_h=yt(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart)}while(0!=--t.prev_length);if(t.match_available=0,t.match_length=2,t.strstart++,i&&(zt(t,!1),0===t.strm.avail_out))return 1}else if(t.match_available){if(i=X(t,0,t.window[t.strstart-1]),i&&zt(t,!1),t.strstart++,t.lookahead--,0===t.strm.avail_out)return 1}else t.match_available=1,t.strstart++,t.lookahead--}return t.match_available&&(i=X(t,0,t.window[t.strstart-1]),t.match_available=0),t.insert=t.strstart<2?t.strstart:2,e===V?(zt(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(zt(t,!1),0===t.strm.avail_out)?1:2};function Ot(t,e,a,i,n){this.good_length=t,this.max_lazy=e,this.nice_length=a,this.max_chain=i,this.func=n}const It=[new Ot(0,0,0,0,St),new Ot(4,4,8,4,Dt),new Ot(4,5,16,8,Dt),new Ot(4,6,32,32,Dt),new Ot(4,4,16,16,Tt),new Ot(8,16,32,32,Tt),new Ot(8,16,128,128,Tt),new Ot(8,32,128,256,Tt),new Ot(32,128,258,1024,Tt),new Ot(32,258,258,4096,Tt)];function Ft(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=ft,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new Uint16Array(1146),this.dyn_dtree=new Uint16Array(122),this.bl_tree=new Uint16Array(78),kt(this.dyn_ltree),kt(this.dyn_dtree),kt(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new Uint16Array(16),this.heap=new Uint16Array(573),kt(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new Uint16Array(573),kt(this.depth),this.sym_buf=0,this.lit_bufsize=0,this.sym_next=0,this.sym_end=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}const Lt=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||e.status!==wt&&57!==e.status&&69!==e.status&&73!==e.status&&91!==e.status&&103!==e.status&&e.status!==mt&&e.status!==bt?1:0},Nt=t=>{if(Lt(t))return gt(t,at);t.total_in=t.total_out=0,t.data_type=_t;const e=t.state;return e.pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=2===e.wrap?57:e.wrap?wt:mt,t.adler=2===e.wrap?0:1,e.last_flush=-2,P(e),tt},Bt=t=>{const e=Nt(t);var a;return e===tt&&((a=t.state).window_size=2*a.w_size,kt(a.head),a.max_lazy_match=It[a.level].max_lazy,a.good_match=It[a.level].good_length,a.nice_match=It[a.level].nice_length,a.max_chain_length=It[a.level].max_chain,a.strstart=0,a.block_start=0,a.lookahead=0,a.insert=0,a.match_length=a.prev_length=2,a.match_available=0,a.ins_h=0),e},Ct=(t,e,a,i,n,s)=>{if(!t)return at;let r=1;if(e===st&&(e=6),i<0?(r=0,i=-i):i>15&&(r=2,i-=16),n<1||n>9||a!==ft||i<8||i>15||e<0||e>9||s<0||s>ht||8===i&&1!==r)return gt(t,at);8===i&&(i=9);const o=new Ft;return t.state=o,o.strm=t,o.status=wt,o.wrap=r,o.gzhead=null,o.w_bits=i,o.w_size=1<Ct(t,e,ft,15,8,dt),deflateInit2:Ct,deflateReset:Bt,deflateResetKeep:Nt,deflateSetHeader:(t,e)=>Lt(t)||2!==t.state.wrap?at:(t.state.gzhead=e,tt),deflate:(t,e)=>{if(Lt(t)||e>$||e<0)return t?gt(t,at):at;const a=t.state;if(!t.output||0!==t.avail_in&&!t.input||a.status===bt&&e!==V)return gt(t,0===t.avail_out?nt:at);const i=a.last_flush;if(a.last_flush=e,0!==a.pending){if(xt(t),0===t.avail_out)return a.last_flush=-1,tt}else if(0===t.avail_in&&pt(e)<=pt(i)&&e!==V)return gt(t,nt);if(a.status===bt&&0!==t.avail_in)return gt(t,nt);if(a.status===wt&&0===a.wrap&&(a.status=mt),a.status===wt){let e=ft+(a.w_bits-8<<4)<<8,i=-1;if(i=a.strategy>=ot||a.level<2?0:a.level<6?1:6===a.level?2:3,e|=i<<6,0!==a.strstart&&(e|=32),e+=31-e%31,Et(a,e),0!==a.strstart&&(Et(a,t.adler>>>16),Et(a,65535&t.adler)),t.adler=1,a.status=mt,xt(t),0!==a.pending)return a.last_flush=-1,tt}if(57===a.status)if(t.adler=0,At(a,31),At(a,139),At(a,8),a.gzhead)At(a,(a.gzhead.text?1:0)+(a.gzhead.hcrc?2:0)+(a.gzhead.extra?4:0)+(a.gzhead.name?8:0)+(a.gzhead.comment?16:0)),At(a,255&a.gzhead.time),At(a,a.gzhead.time>>8&255),At(a,a.gzhead.time>>16&255),At(a,a.gzhead.time>>24&255),At(a,9===a.level?2:a.strategy>=ot||a.level<2?4:0),At(a,255&a.gzhead.os),a.gzhead.extra&&a.gzhead.extra.length&&(At(a,255&a.gzhead.extra.length),At(a,a.gzhead.extra.length>>8&255)),a.gzhead.hcrc&&(t.adler=H(t.adler,a.pending_buf,a.pending,0)),a.gzindex=0,a.status=69;else if(At(a,0),At(a,0),At(a,0),At(a,0),At(a,0),At(a,9===a.level?2:a.strategy>=ot||a.level<2?4:0),At(a,3),a.status=mt,xt(t),0!==a.pending)return a.last_flush=-1,tt;if(69===a.status){if(a.gzhead.extra){let e=a.pending,i=(65535&a.gzhead.extra.length)-a.gzindex;for(;a.pending+i>a.pending_buf_size;){let n=a.pending_buf_size-a.pending;if(a.pending_buf.set(a.gzhead.extra.subarray(a.gzindex,a.gzindex+n),a.pending),a.pending=a.pending_buf_size,a.gzhead.hcrc&&a.pending>e&&(t.adler=H(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex+=n,xt(t),0!==a.pending)return a.last_flush=-1,tt;e=0,i-=n}let n=new Uint8Array(a.gzhead.extra);a.pending_buf.set(n.subarray(a.gzindex,a.gzindex+i),a.pending),a.pending+=i,a.gzhead.hcrc&&a.pending>e&&(t.adler=H(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex=0}a.status=73}if(73===a.status){if(a.gzhead.name){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=H(t.adler,a.pending_buf,a.pending-i,i)),xt(t),0!==a.pending)return a.last_flush=-1,tt;i=0}e=a.gzindexi&&(t.adler=H(t.adler,a.pending_buf,a.pending-i,i)),a.gzindex=0}a.status=91}if(91===a.status){if(a.gzhead.comment){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=H(t.adler,a.pending_buf,a.pending-i,i)),xt(t),0!==a.pending)return a.last_flush=-1,tt;i=0}e=a.gzindexi&&(t.adler=H(t.adler,a.pending_buf,a.pending-i,i))}a.status=103}if(103===a.status){if(a.gzhead.hcrc){if(a.pending+2>a.pending_buf_size&&(xt(t),0!==a.pending))return a.last_flush=-1,tt;At(a,255&t.adler),At(a,t.adler>>8&255),t.adler=0}if(a.status=mt,xt(t),0!==a.pending)return a.last_flush=-1,tt}if(0!==t.avail_in||0!==a.lookahead||e!==q&&a.status!==bt){let i=0===a.level?St(a,e):a.strategy===ot?((t,e)=>{let a;for(;;){if(0===t.lookahead&&(Ut(t),0===t.lookahead)){if(e===q)return 1;break}if(t.match_length=0,a=X(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,a&&(zt(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===V?(zt(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(zt(t,!1),0===t.strm.avail_out)?1:2})(a,e):a.strategy===lt?((t,e)=>{let a,i,n,s;const r=t.window;for(;;){if(t.lookahead<=ct){if(Ut(t),t.lookahead<=ct&&e===q)return 1;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=3&&t.strstart>0&&(n=t.strstart-1,i=r[n],i===r[++n]&&i===r[++n]&&i===r[++n])){s=t.strstart+ct;do{}while(i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&nt.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=3?(a=X(t,1,t.match_length-3),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(a=X(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),a&&(zt(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===V?(zt(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(zt(t,!1),0===t.strm.avail_out)?1:2})(a,e):It[a.level].func(a,e);if(3!==i&&4!==i||(a.status=bt),1===i||3===i)return 0===t.avail_out&&(a.last_flush=-1),tt;if(2===i&&(e===J?W(a):e!==$&&(Y(a,0,0,!1),e===Q&&(kt(a.head),0===a.lookahead&&(a.strstart=0,a.block_start=0,a.insert=0))),xt(t),0===t.avail_out))return a.last_flush=-1,tt}return e!==V?tt:a.wrap<=0?et:(2===a.wrap?(At(a,255&t.adler),At(a,t.adler>>8&255),At(a,t.adler>>16&255),At(a,t.adler>>24&255),At(a,255&t.total_in),At(a,t.total_in>>8&255),At(a,t.total_in>>16&255),At(a,t.total_in>>24&255)):(Et(a,t.adler>>>16),Et(a,65535&t.adler)),xt(t),a.wrap>0&&(a.wrap=-a.wrap),0!==a.pending?tt:et)},deflateEnd:t=>{if(Lt(t))return at;const e=t.state.status;return t.state=null,e===mt?gt(t,it):tt},deflateSetDictionary:(t,e)=>{let a=e.length;if(Lt(t))return at;const i=t.state,n=i.wrap;if(2===n||1===n&&i.status!==wt||i.lookahead)return at;if(1===n&&(t.adler=C(t.adler,e,a,0)),i.wrap=0,a>=i.w_size){0===n&&(kt(i.head),i.strstart=0,i.block_start=0,i.insert=0);let t=new Uint8Array(i.w_size);t.set(e.subarray(a-i.w_size,a),0),e=t,a=i.w_size}const s=t.avail_in,r=t.next_in,o=t.input;for(t.avail_in=a,t.next_in=0,t.input=e,Ut(i);i.lookahead>=3;){let t=i.strstart,e=i.lookahead-2;do{i.ins_h=yt(i,i.ins_h,i.window[t+3-1]),i.prev[t&i.w_mask]=i.head[i.ins_h],i.head[i.ins_h]=t,t++}while(--e);i.strstart=t,i.lookahead=2,Ut(i)}return i.strstart+=i.lookahead,i.block_start=i.strstart,i.insert=i.lookahead,i.lookahead=0,i.match_length=i.prev_length=2,i.match_available=0,t.next_in=r,t.input=o,t.avail_in=s,i.wrap=n,tt},deflateInfo:"pako deflate (from Nodeca project)"};const Ht=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var jt=function(t){const e=Array.prototype.slice.call(arguments,1);for(;e.length;){const a=e.shift();if(a){if("object"!=typeof a)throw new TypeError(a+"must be non-object");for(const e in a)Ht(a,e)&&(t[e]=a[e])}}return t},Kt=t=>{let e=0;for(let a=0,i=t.length;a=252?6:t>=248?5:t>=240?4:t>=224?3:t>=192?2:1;Yt[254]=Yt[254]=1;var Gt=t=>{if("function"==typeof TextEncoder&&TextEncoder.prototype.encode)return(new TextEncoder).encode(t);let e,a,i,n,s,r=t.length,o=0;for(n=0;n>>6,e[s++]=128|63&a):a<65536?(e[s++]=224|a>>>12,e[s++]=128|a>>>6&63,e[s++]=128|63&a):(e[s++]=240|a>>>18,e[s++]=128|a>>>12&63,e[s++]=128|a>>>6&63,e[s++]=128|63&a);return e},Xt=(t,e)=>{const a=e||t.length;if("function"==typeof TextDecoder&&TextDecoder.prototype.decode)return(new TextDecoder).decode(t.subarray(0,e));let i,n;const s=new Array(2*a);for(n=0,i=0;i4)s[n++]=65533,i+=r-1;else{for(e&=2===r?31:3===r?15:7;r>1&&i1?s[n++]=65533:e<65536?s[n++]=e:(e-=65536,s[n++]=55296|e>>10&1023,s[n++]=56320|1023&e)}}return((t,e)=>{if(e<65534&&t.subarray&&Pt)return String.fromCharCode.apply(null,t.length===e?t:t.subarray(0,e));let a="";for(let i=0;i{(e=e||t.length)>t.length&&(e=t.length);let a=e-1;for(;a>=0&&128==(192&t[a]);)a--;return a<0||0===a?e:a+Yt[t[a]]>e?a:e};var qt=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0};const Jt=Object.prototype.toString,{Z_NO_FLUSH:Qt,Z_SYNC_FLUSH:Vt,Z_FULL_FLUSH:$t,Z_FINISH:te,Z_OK:ee,Z_STREAM_END:ae,Z_DEFAULT_COMPRESSION:ie,Z_DEFAULT_STRATEGY:ne,Z_DEFLATED:se}=K;function re(t){this.options=jt({level:ie,method:se,chunkSize:16384,windowBits:15,memLevel:8,strategy:ne},t||{});let e=this.options;e.raw&&e.windowBits>0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new qt,this.strm.avail_out=0;let a=Mt.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(a!==ee)throw new Error(j[a]);if(e.header&&Mt.deflateSetHeader(this.strm,e.header),e.dictionary){let t;if(t="string"==typeof e.dictionary?Gt(e.dictionary):"[object ArrayBuffer]"===Jt.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,a=Mt.deflateSetDictionary(this.strm,t),a!==ee)throw new Error(j[a]);this._dict_set=!0}}function oe(t,e){const a=new re(e);if(a.push(t,!0),a.err)throw a.msg||j[a.err];return a.result}re.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize;let n,s;if(this.ended)return!1;for(s=e===~~e?e:!0===e?te:Qt,"string"==typeof t?a.input=Gt(t):"[object ArrayBuffer]"===Jt.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;)if(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),(s===Vt||s===$t)&&a.avail_out<=6)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else{if(n=Mt.deflate(a,s),n===ae)return a.next_out>0&&this.onData(a.output.subarray(0,a.next_out)),n=Mt.deflateEnd(this.strm),this.onEnd(n),this.ended=!0,n===ee;if(0!==a.avail_out){if(s>0&&a.next_out>0)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else if(0===a.avail_in)break}else this.onData(a.output)}return!0},re.prototype.onData=function(t){this.chunks.push(t)},re.prototype.onEnd=function(t){t===ee&&(this.result=Kt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};var le={Deflate:re,deflate:oe,deflateRaw:function(t,e){return(e=e||{}).raw=!0,oe(t,e)},gzip:function(t,e){return(e=e||{}).gzip=!0,oe(t,e)},constants:K};const he=16209;var de=function(t,e){let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z,A;const E=t.state;a=t.next_in,z=t.input,i=a+(t.avail_in-5),n=t.next_out,A=t.output,s=n-(e-t.avail_out),r=n+(t.avail_out-257),o=E.dmax,l=E.wsize,h=E.whave,d=E.wnext,_=E.window,f=E.hold,c=E.bits,u=E.lencode,w=E.distcode,m=(1<>>24,f>>>=p,c-=p,p=g>>>16&255,0===p)A[n++]=65535&g;else{if(!(16&p)){if(0==(64&p)){g=u[(65535&g)+(f&(1<>>=p,c-=p),c<15&&(f+=z[a++]<>>24,f>>>=p,c-=p,p=g>>>16&255,!(16&p)){if(0==(64&p)){g=w[(65535&g)+(f&(1<o){t.msg="invalid distance too far back",E.mode=he;break t}if(f>>>=p,c-=p,p=n-s,v>p){if(p=v-p,p>h&&E.sane){t.msg="invalid distance too far back",E.mode=he;break t}if(y=0,x=_,0===d){if(y+=l-p,p2;)A[n++]=x[y++],A[n++]=x[y++],A[n++]=x[y++],k-=3;k&&(A[n++]=x[y++],k>1&&(A[n++]=x[y++]))}else{y=n-v;do{A[n++]=A[y++],A[n++]=A[y++],A[n++]=A[y++],k-=3}while(k>2);k&&(A[n++]=A[y++],k>1&&(A[n++]=A[y++]))}break}}break}}while(a>3,a-=k,c-=k<<3,f&=(1<{const l=o.bits;let h,d,_,f,c,u,w=0,m=0,b=0,g=0,p=0,k=0,v=0,y=0,x=0,z=0,A=null;const E=new Uint16Array(16),R=new Uint16Array(16);let Z,U,S,D=null;for(w=0;w<=_e;w++)E[w]=0;for(m=0;m=1&&0===E[g];g--);if(p>g&&(p=g),0===g)return n[s++]=20971520,n[s++]=20971520,o.bits=1,0;for(b=1;b0&&(0===t||1!==g))return-1;for(R[1]=0,w=1;w<_e;w++)R[w+1]=R[w]+E[w];for(m=0;m852||2===t&&x>592)return 1;for(;;){Z=w-v,r[m]+1=u?(U=D[r[m]-u],S=A[r[m]-u]):(U=96,S=0),h=1<>v)+d]=Z<<24|U<<16|S|0}while(0!==d);for(h=1<>=1;if(0!==h?(z&=h-1,z+=h):z=0,m++,0==--E[w]){if(w===g)break;w=e[a+r[m]]}if(w>p&&(z&f)!==_){for(0===v&&(v=p),c+=b,k=w-v,y=1<852||2===t&&x>592)return 1;_=z&f,n[_]=p<<24|k<<16|c-s|0}}return 0!==z&&(n[c+z]=w-v<<24|64<<16|0),o.bits=p,0};const{Z_FINISH:be,Z_BLOCK:ge,Z_TREES:pe,Z_OK:ke,Z_STREAM_END:ve,Z_NEED_DICT:ye,Z_STREAM_ERROR:xe,Z_DATA_ERROR:ze,Z_MEM_ERROR:Ae,Z_BUF_ERROR:Ee,Z_DEFLATED:Re}=K,Ze=16180,Ue=16190,Se=16191,De=16192,Te=16194,Oe=16199,Ie=16200,Fe=16206,Le=16209,Ne=t=>(t>>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24);function Be(){this.strm=null,this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Uint16Array(320),this.work=new Uint16Array(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}const Ce=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||e.mode16211?1:0},Me=t=>{if(Ce(t))return xe;const e=t.state;return t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=Ze,e.last=0,e.havedict=0,e.flags=-1,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new Int32Array(852),e.distcode=e.distdyn=new Int32Array(592),e.sane=1,e.back=-1,ke},He=t=>{if(Ce(t))return xe;const e=t.state;return e.wsize=0,e.whave=0,e.wnext=0,Me(t)},je=(t,e)=>{let a;if(Ce(t))return xe;const i=t.state;return e<0?(a=0,e=-e):(a=5+(e>>4),e<48&&(e&=15)),e&&(e<8||e>15)?xe:(null!==i.window&&i.wbits!==e&&(i.window=null),i.wrap=a,i.wbits=e,He(t))},Ke=(t,e)=>{if(!t)return xe;const a=new Be;t.state=a,a.strm=t,a.window=null,a.mode=Ze;const i=je(t,e);return i!==ke&&(t.state=null),i};let Pe,Ye,Ge=!0;const Xe=t=>{if(Ge){Pe=new Int32Array(512),Ye=new Int32Array(32);let e=0;for(;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(me(1,t.lens,0,288,Pe,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;me(2,t.lens,0,32,Ye,0,t.work,{bits:5}),Ge=!1}t.lencode=Pe,t.lenbits=9,t.distcode=Ye,t.distbits=5},We=(t,e,a,i)=>{let n;const s=t.state;return null===s.window&&(s.wsize=1<=s.wsize?(s.window.set(e.subarray(a-s.wsize,a),0),s.wnext=0,s.whave=s.wsize):(n=s.wsize-s.wnext,n>i&&(n=i),s.window.set(e.subarray(a-i,a-i+n),s.wnext),(i-=n)?(s.window.set(e.subarray(a-i,a),0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whaveKe(t,15),inflateInit2:Ke,inflate:(t,e)=>{let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z=0;const A=new Uint8Array(4);let E,R;const Z=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);if(Ce(t)||!t.output||!t.input&&0!==t.avail_in)return xe;a=t.state,a.mode===Se&&(a.mode=De),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,_=o,f=l,x=ke;t:for(;;)switch(a.mode){case Ze:if(0===a.wrap){a.mode=De;break}for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>>8&255,a.check=H(a.check,A,2,0),h=0,d=0,a.mode=16181;break}if(a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&h)<<8)+(h>>8))%31){t.msg="incorrect header check",a.mode=Le;break}if((15&h)!==Re){t.msg="unknown compression method",a.mode=Le;break}if(h>>>=4,d-=4,y=8+(15&h),0===a.wbits&&(a.wbits=y),y>15||y>a.wbits){t.msg="invalid window size",a.mode=Le;break}a.dmax=1<>8&1),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=H(a.check,A,2,0)),h=0,d=0,a.mode=16182;case 16182:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<>>8&255,A[2]=h>>>16&255,A[3]=h>>>24&255,a.check=H(a.check,A,4,0)),h=0,d=0,a.mode=16183;case 16183:for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>8),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=H(a.check,A,2,0)),h=0,d=0,a.mode=16184;case 16184:if(1024&a.flags){for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>>8&255,a.check=H(a.check,A,2,0)),h=0,d=0}else a.head&&(a.head.extra=null);a.mode=16185;case 16185:if(1024&a.flags&&(c=a.length,c>o&&(c=o),c&&(a.head&&(y=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Uint8Array(a.head.extra_len)),a.head.extra.set(i.subarray(s,s+c),y)),512&a.flags&&4&a.wrap&&(a.check=H(a.check,i,c,s)),o-=c,s+=c,a.length-=c),a.length))break t;a.length=0,a.mode=16186;case 16186:if(2048&a.flags){if(0===o)break t;c=0;do{y=i[s+c++],a.head&&y&&a.length<65536&&(a.head.name+=String.fromCharCode(y))}while(y&&c>9&1,a.head.done=!0),t.adler=a.check=0,a.mode=Se;break;case 16189:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<>>=7&d,d-=7&d,a.mode=Fe;break}for(;d<3;){if(0===o)break t;o--,h+=i[s++]<>>=1,d-=1,3&h){case 0:a.mode=16193;break;case 1:if(Xe(a),a.mode=Oe,e===pe){h>>>=2,d-=2;break t}break;case 2:a.mode=16196;break;case 3:t.msg="invalid block type",a.mode=Le}h>>>=2,d-=2;break;case 16193:for(h>>>=7&d,d-=7&d;d<32;){if(0===o)break t;o--,h+=i[s++]<>>16^65535)){t.msg="invalid stored block lengths",a.mode=Le;break}if(a.length=65535&h,h=0,d=0,a.mode=Te,e===pe)break t;case Te:a.mode=16195;case 16195:if(c=a.length,c){if(c>o&&(c=o),c>l&&(c=l),0===c)break t;n.set(i.subarray(s,s+c),r),o-=c,s+=c,l-=c,r+=c,a.length-=c;break}a.mode=Se;break;case 16196:for(;d<14;){if(0===o)break t;o--,h+=i[s++]<>>=5,d-=5,a.ndist=1+(31&h),h>>>=5,d-=5,a.ncode=4+(15&h),h>>>=4,d-=4,a.nlen>286||a.ndist>30){t.msg="too many length or distance symbols",a.mode=Le;break}a.have=0,a.mode=16197;case 16197:for(;a.have>>=3,d-=3}for(;a.have<19;)a.lens[Z[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,E={bits:a.lenbits},x=me(0,a.lens,0,19,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid code lengths set",a.mode=Le;break}a.have=0,a.mode=16198;case 16198:for(;a.have>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=m,d-=m,a.lens[a.have++]=g;else{if(16===g){for(R=m+2;d>>=m,d-=m,0===a.have){t.msg="invalid bit length repeat",a.mode=Le;break}y=a.lens[a.have-1],c=3+(3&h),h>>>=2,d-=2}else if(17===g){for(R=m+3;d>>=m,d-=m,y=0,c=3+(7&h),h>>>=3,d-=3}else{for(R=m+7;d>>=m,d-=m,y=0,c=11+(127&h),h>>>=7,d-=7}if(a.have+c>a.nlen+a.ndist){t.msg="invalid bit length repeat",a.mode=Le;break}for(;c--;)a.lens[a.have++]=y}}if(a.mode===Le)break;if(0===a.lens[256]){t.msg="invalid code -- missing end-of-block",a.mode=Le;break}if(a.lenbits=9,E={bits:a.lenbits},x=me(1,a.lens,0,a.nlen,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid literal/lengths set",a.mode=Le;break}if(a.distbits=6,a.distcode=a.distdyn,E={bits:a.distbits},x=me(2,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,E),a.distbits=E.bits,x){t.msg="invalid distances set",a.mode=Le;break}if(a.mode=Oe,e===pe)break t;case Oe:a.mode=Ie;case Ie:if(o>=6&&l>=258){t.next_out=r,t.avail_out=l,t.next_in=s,t.avail_in=o,a.hold=h,a.bits=d,de(t,f),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,a.mode===Se&&(a.back=-1);break}for(a.back=0;z=a.lencode[h&(1<>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=p,d-=p,a.back+=p}if(h>>>=m,d-=m,a.back+=m,a.length=g,0===b){a.mode=16205;break}if(32&b){a.back=-1,a.mode=Se;break}if(64&b){t.msg="invalid literal/length code",a.mode=Le;break}a.extra=15&b,a.mode=16201;case 16201:if(a.extra){for(R=a.extra;d>>=a.extra,d-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=16202;case 16202:for(;z=a.distcode[h&(1<>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=p,d-=p,a.back+=p}if(h>>>=m,d-=m,a.back+=m,64&b){t.msg="invalid distance code",a.mode=Le;break}a.offset=g,a.extra=15&b,a.mode=16203;case 16203:if(a.extra){for(R=a.extra;d>>=a.extra,d-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){t.msg="invalid distance too far back",a.mode=Le;break}a.mode=16204;case 16204:if(0===l)break t;if(c=f-l,a.offset>c){if(c=a.offset-c,c>a.whave&&a.sane){t.msg="invalid distance too far back",a.mode=Le;break}c>a.wnext?(c-=a.wnext,u=a.wsize-c):u=a.wnext-c,c>a.length&&(c=a.length),w=a.window}else w=n,u=r-a.offset,c=a.length;c>l&&(c=l),l-=c,a.length-=c;do{n[r++]=w[u++]}while(--c);0===a.length&&(a.mode=Ie);break;case 16205:if(0===l)break t;n[r++]=a.length,l--,a.mode=Ie;break;case Fe:if(a.wrap){for(;d<32;){if(0===o)break t;o--,h|=i[s++]<{if(Ce(t))return xe;let e=t.state;return e.window&&(e.window=null),t.state=null,ke},inflateGetHeader:(t,e)=>{if(Ce(t))return xe;const a=t.state;return 0==(2&a.wrap)?xe:(a.head=e,e.done=!1,ke)},inflateSetDictionary:(t,e)=>{const a=e.length;let i,n,s;return Ce(t)?xe:(i=t.state,0!==i.wrap&&i.mode!==Ue?xe:i.mode===Ue&&(n=1,n=C(n,e,a,0),n!==i.check)?ze:(s=We(t,e,a,a),s?(i.mode=16210,Ae):(i.havedict=1,ke)))},inflateInfo:"pako inflate (from Nodeca project)"};var Je=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1};const Qe=Object.prototype.toString,{Z_NO_FLUSH:Ve,Z_FINISH:$e,Z_OK:ta,Z_STREAM_END:ea,Z_NEED_DICT:aa,Z_STREAM_ERROR:ia,Z_DATA_ERROR:na,Z_MEM_ERROR:sa}=K;function ra(t){this.options=jt({chunkSize:65536,windowBits:15,to:""},t||{});const e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&0==(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new qt,this.strm.avail_out=0;let a=qe.inflateInit2(this.strm,e.windowBits);if(a!==ta)throw new Error(j[a]);if(this.header=new Je,qe.inflateGetHeader(this.strm,this.header),e.dictionary&&("string"==typeof e.dictionary?e.dictionary=Gt(e.dictionary):"[object ArrayBuffer]"===Qe.call(e.dictionary)&&(e.dictionary=new Uint8Array(e.dictionary)),e.raw&&(a=qe.inflateSetDictionary(this.strm,e.dictionary),a!==ta)))throw new Error(j[a])}function oa(t,e){const a=new ra(e);if(a.push(t),a.err)throw a.msg||j[a.err];return a.result}ra.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize,n=this.options.dictionary;let s,r,o;if(this.ended)return!1;for(r=e===~~e?e:!0===e?$e:Ve,"[object ArrayBuffer]"===Qe.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;){for(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),s=qe.inflate(a,r),s===aa&&n&&(s=qe.inflateSetDictionary(a,n),s===ta?s=qe.inflate(a,r):s===na&&(s=aa));a.avail_in>0&&s===ea&&a.state.wrap>0&&0!==t[a.next_in];)qe.inflateReset(a),s=qe.inflate(a,r);switch(s){case ia:case na:case aa:case sa:return this.onEnd(s),this.ended=!0,!1}if(o=a.avail_out,a.next_out&&(0===a.avail_out||s===ea))if("string"===this.options.to){let t=Wt(a.output,a.next_out),e=a.next_out-t,n=Xt(a.output,t);a.next_out=e,a.avail_out=i-e,e&&a.output.set(a.output.subarray(t,t+e),0),this.onData(n)}else this.onData(a.output.length===a.next_out?a.output:a.output.subarray(0,a.next_out));if(s!==ta||0!==o){if(s===ea)return s=qe.inflateEnd(this.strm),this.onEnd(s),this.ended=!0,!0;if(0===a.avail_in)break}}return!0},ra.prototype.onData=function(t){this.chunks.push(t)},ra.prototype.onEnd=function(t){t===ta&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=Kt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};var la={Inflate:ra,inflate:oa,inflateRaw:function(t,e){return(e=e||{}).raw=!0,oa(t,e)},ungzip:oa,constants:K};const{Deflate:ha,deflate:da,deflateRaw:_a,gzip:fa}=le,{Inflate:ca,inflate:ua,inflateRaw:wa,ungzip:ma}=la;var ba=ha,ga=da,pa=_a,ka=fa,va=ca,ya=ua,xa=wa,za=ma,Aa=K,Ea={Deflate:ba,deflate:ga,deflateRaw:pa,gzip:ka,Inflate:va,inflate:ya,inflateRaw:xa,ungzip:za,constants:Aa};t.Deflate=ba,t.Inflate=va,t.constants=Aa,t.default=Ea,t.deflate=ga,t.deflateRaw=pa,t.gzip=ka,t.inflate=ya,t.inflateRaw=xa,t.ungzip=za,Object.defineProperty(t,"__esModule",{value:!0})})); diff --git a/js/tinySSB-for-Chrome/pinch-zoom.min.js b/js/tinySSB-for-Chrome/pinch-zoom.min.js new file mode 100644 index 0000000..07633a1 --- /dev/null +++ b/js/tinySSB-for-Chrome/pinch-zoom.min.js @@ -0,0 +1 @@ +"function"!=typeof Object.assign&&Object.defineProperty(Object,"assign",{value:function(a){if(null==a)throw new TypeError("Cannot convert undefined or null to object");for(var b,c=Object(a),d=1;db-.01&&ac&&(b=this.getCurrentZoomCenter()),this.animate(this.options.animationDuration,e,this.swing),triggerEvent(this.el,this.options.doubleTapEventName),"function"==typeof this.options.onDoubleTap&&this.options.onDoubleTap(this,a))},computeInitialOffset:function(){this.initialOffset={x:-c(this.el.offsetWidth*this.getInitialZoomFactor()-this.container.offsetWidth)/2,y:-c(this.el.offsetHeight*this.getInitialZoomFactor()-this.container.offsetHeight)/2}},resetOffset:function(){this.offset.x=this.initialOffset.x,this.offset.y=this.initialOffset.y},isImageLoaded:function(a){return"IMG"===a.nodeName?a.complete&&0!==a.naturalHeight:Array.from(a.querySelectorAll("img")).every(this.isImageLoaded)},setupOffsets:function(){this.options.setOffsetsOnce&&this._isOffsetsSet||(this._isOffsetsSet=!0,this.computeInitialOffset(),this.resetOffset())},sanitizeOffset:function(c){var d=this.el.offsetWidth*this.getInitialZoomFactor()*this.zoomFactor,e=this.el.offsetHeight*this.getInitialZoomFactor()*this.zoomFactor,f=d-this.getContainerX()+this.options.horizontalPadding,g=e-this.getContainerY()+this.options.verticalPadding,h=b(f,0),i=b(g,0),j=a(f,0)-this.options.horizontalPadding,k=a(g,0)-this.options.verticalPadding;return{x:a(b(c.x,j),h),y:a(b(c.y,k),i)}},scaleTo:function(a,b){this.scale(a/this.zoomFactor,b)},scale:function(a,b){a=this.scaleZoomFactor(a),this.addOffset({x:(a-1)*(b.x+this.offset.x),y:(a-1)*(b.y+this.offset.y)}),triggerEvent(this.el,this.options.zoomUpdateEventName),"function"==typeof this.options.onZoomUpdate&&this.options.onZoomUpdate(this,event)},scaleZoomFactor:function(c){var d=this.zoomFactor;return this.zoomFactor*=c,this.zoomFactor=a(this.options.maxZoom,b(this.zoomFactor,this.options.minZoom)),this.zoomFactor/d},canDrag:function(){return this.options.draggableUnzoomed||!f(this.zoomFactor,1)},drag:function(a,b){b&&(this.options.lockDragAxis?c(a.x-b.x)>c(a.y-b.y)?this.addOffset({x:-(a.x-b.x),y:0}):this.addOffset({y:-(a.y-b.y),x:0}):this.addOffset({y:-(a.y-b.y),x:-(a.x-b.x)}),triggerEvent(this.el,this.options.dragUpdateEventName),"function"==typeof this.options.onDragUpdate&&this.options.onDragUpdate(this,event))},getTouchCenter:function(a){return this.getVectorAvg(a)},getVectorAvg:function(a){return{x:a.map(function(a){return a.x}).reduce(e)/a.length,y:a.map(function(a){return a.y}).reduce(e)/a.length}},addOffset:function(a){this.offset={x:this.offset.x+a.x,y:this.offset.y+a.y}},sanitize:function(){this.zoomFactor=a?(b(1),d&&d(),this.update(),this.stopAnimation(),this.update()):(c&&(h=c(h)),b(h),this.update(),requestAnimationFrame(f))}}.bind(this);this.inAnimation=!0,requestAnimationFrame(f)},stopAnimation:function(){this.inAnimation=!1},swing:function(a){return-Math.cos(a*Math.PI)/2+.5},getContainerX:function(){return this.container.offsetWidth},getContainerY:function(){return this.container.offsetHeight},setContainerY:function(a){return this.container.style.height=a+"px"},unsetContainerY:function(){this.container.style.height=null},setupMarkup:function(){this.container=buildElement("
"),this.el.parentNode.insertBefore(this.container,this.el),this.container.appendChild(this.el),this.container.style.overflow="hidden",this.container.style.position="relative",this.el.style.webkitTransformOrigin="0% 0%",this.el.style.mozTransformOrigin="0% 0%",this.el.style.msTransformOrigin="0% 0%",this.el.style.oTransformOrigin="0% 0%",this.el.style.transformOrigin="0% 0%",this.el.style.position="absolute"},end:function(){this.hasInteraction=!1,this.sanitize(),this.update()},bindEvents:function(){var a=this;g(this.container,this),window.addEventListener("resize",this.update.bind(this)),Array.from(this.el.querySelectorAll("img")).forEach(function(b){b.addEventListener("load",a.update.bind(a))}),"IMG"===this.el.nodeName&&this.el.addEventListener("load",this.update.bind(this))},update:function(a){this.updatePlaned||(this.updatePlaned=!0,window.setTimeout(function(){this.updatePlaned=!1,a&&"resize"===a.type&&(this.updateAspectRatio(),this.setupOffsets()),a&&"load"===a.type&&(this.updateAspectRatio(),this.setupOffsets());var b=this.getInitialZoomFactor()*this.zoomFactor,c=-this.offset.x/b,d=-this.offset.y/b,e="scale3d("+b+", "+b+",1) translate3d("+c+"px,"+d+"px,0px)",f="scale("+b+", "+b+") translate("+c+"px,"+d+"px)",g=function(){this.clone&&(this.clone.parentNode.removeChild(this.clone),delete this.clone)}.bind(this);!this.options.use2d||this.hasInteraction||this.inAnimation?(this.is3d=!0,g(),this.el.style.webkitTransform=e,this.el.style.mozTransform=f,this.el.style.msTransform=f,this.el.style.oTransform=f,this.el.style.transform=e):(this.is3d&&(this.clone=this.el.cloneNode(!0),this.clone.style.pointerEvents="none",this.container.appendChild(this.clone),window.setTimeout(g,200)),this.el.style.webkitTransform=f,this.el.style.mozTransform=f,this.el.style.msTransform=f,this.el.style.oTransform=f,this.el.style.transform=f,this.is3d=!1)}.bind(this),0))},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1}};var g=function(a,b){var c=null,d=0,e=null,f=null,g=function(a,d){c!==a&&(c&&!a&&("zoom"===c?b.handleZoomEnd(d):"drag"===c?b.handleDragEnd(d):void 0),"zoom"===a?b.handleZoomStart(d):"drag"===a?b.handleDragStart(d):void 0);c=a},h=function(a){2===d?g("zoom"):1===d&&b.canDrag()?g("drag",a):g(null,a)},i=function(a){return Array.from(a).map(function(a){return{x:a.pageX,y:a.pageY}})},j=function(c,a){var d,e,b=Math.sqrt;return d=c.x-a.x,e=c.y-a.y,b(d*d+e*e)},k=function(a,b){var c=j(a[0],a[1]),d=j(b[0],b[1]);return d/c},l=function(a){a.stopPropagation(),a.preventDefault()},m=function(a){var f=new Date().getTime();1f-e?(l(a),b.handleDoubleTap(a),"zoom"===c?b.handleZoomEnd(a):"drag"===c?b.handleDragEnd(a):void 0):b.isDoubleTap=!1;1===d&&(e=f)},n=!0;a.addEventListener("touchstart",function(a){b.enabled&&(n=!0,d=a.touches.length,m(a))}),a.addEventListener("touchmove",function(a){b.enabled&&!b.isDoubleTap&&(n?(h(a),c&&l(a),f=i(a.touches)):("zoom"===c?2==f.length&&2==a.touches.length&&b.handleZoom(a,k(f,i(a.touches))):"drag"===c?b.handleDrag(a):void 0,c&&(l(a),b.update())),n=!1)}),a.addEventListener("touchend",function(a){b.enabled&&(d=a.touches.length,h(a))})};return d},PinchZoom=definePinchZoom(); \ No newline at end of file diff --git a/js/tinySSB-for-Chrome/qrcode.min.js b/js/tinySSB-for-Chrome/qrcode.min.js new file mode 100644 index 0000000..a97923d --- /dev/null +++ b/js/tinySSB-for-Chrome/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.width="100%",this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); diff --git a/js/tinySSB-for-Chrome/scuttlesort.js b/js/tinySSB-for-Chrome/scuttlesort.js new file mode 100644 index 0000000..23c8960 --- /dev/null +++ b/js/tinySSB-for-Chrome/scuttlesort.js @@ -0,0 +1,332 @@ +// + +// scuttlesort.js - convergent incremental topological sort +// 2022-05-14, 2022-09-09 ins(Z,nm) + // mov X Y, mov Y Z etc --> mov X Z + if (this.notify) { + var base = null; + for (let c of this.cmds) { + if (base) { + if (c[0] == 'mov' && base[2] == c[1]) { + base[2] = c[2]; + continue; + } + this.notify(base); + } + base = c; + } + if (base) + this.notify(base); + } + } + + index(nm) { + return this.name2p[nm].indx; + } + + get_tips() { + var lst = [] + for (let t of this.tips) + lst.push(t.name); + return lst; + } + + toJSON() { + var json = {} + json.cmds = this.cmds + json.linear = this.linear + json.notify = this.notify + json.pending = {} + for (let p in this.pending) + json.pending[p] = Object.values(Array.from(this.pending[p])).map(x => x.name) + json.tips = Object.values(Array.from(this.tips)).map(x => x.name) + return json + } + + static fromJSON(json) { + var t = new Timeline() + var nodes = [] + // name, cycl, indx, rank, vstd, prev, succ + for (let n of json.linear) { + var prev = [] + for (let p of n.prev) { + let prevNode = nodes.find((key, indx) => { + return key.name == p + }) + if (prevNode) { + prev.push(prevNode) + } else { + prev.push(p) + } + } + var sortNode = ScuttleSortNode.createSortNode(n.name, n.cycl, n.indx, n.rank, n.vstd, prev, []) + nodes.push(sortNode) + for (let p of prev) { + if (typeof p != "string") + p.succ.push(sortNode) + } + + } + t.linear = nodes + + t.name2p = {} + for (let n of nodes) { + t.name2p[n.name] = n + } + + t.pending = {} + for (let p in json.pending) { + t.pending[p] = [] + for (let n of json.pending[p]) { + let node = nodes.find((key, indx) => { + return key.name == n + }) + if (node) + t.pending[p].push(node) + } + } + + t.notify = json.notify + t.cmds = json.cmds + + t.tips = new Set() + for (let tip of json.tips) { + let node = nodes.find((key, indx) => { + return key.name == tip + }) + if (node) + t.tips.add(node) + } + return t + } +} + +class ScuttleSortNode { + + constructor(name, timeline, after) { + if (!name) // should only be true if called from createSortNode() + return + if (name in timeline.name2p) // can add a name only once, must be unique + throw new Error("KeyError"); + this.name = name; + this.prev = after.map(x => { + return x; + }); // copy of the causes we depend on + // hack alert: these are str/bytes, will be replaced by nodes + // --- internal fields for insertion algorithm: + this.cycl = false; // cycle detection, could be removed for SSB + this.succ = []; // my future successors (="outgoing") + this.vstd = false; // visited + this.rank = 0; // 0 for a start, we will soon know better + + timeline.name2p[name] = this + for (let i = 0; i < this.prev.length; i++) { + let c = this.prev[i]; + let p = timeline.name2p[c] + if (p) { + p.succ.push(this); + this.prev[i] = p; // replace str/bytes by respective node + if (timeline.tips.has(p)) + timeline.tips.delete(p); + } else { + if (!timeline.pending[c]) + timeline.pending[c] = []; + let a = timeline.pending[c]; + if (!a.includes(this)) + a.splice(a.length, 0, this); + } + } + + var pos = 0; + for (let i = 0; i < this.prev.length; i++) { + let p = this.prev[i]; + if (typeof (p) != "string" && p.indx > pos) + pos = p.indx; + } + for (let i = pos; i < timeline.linear.length; i++) + timeline.linear[i].indx += 1; + this.indx = pos; + timeline._insert(pos, this); + + var no_anchor = true; + for (let p of this.prev) { + if (typeof (p) != "string") { + this.add_edge_to_the_past(timeline, p); + no_anchor = false; + } + } + if (no_anchor && timeline.linear.length > 1) { + // there was already at least one feed, hence + // insert us lexicographically at time t=0 + this._rise(timeline); + } + + let s = timeline.pending[this.name]; + if (s) { + for (let e of s) { + for (let i = 0; i < e.prev.length; i++) { + if (e.prev[i] != this.name) + continue; + e.add_edge_to_the_past(timeline, this); + this.succ.push(e); + if (timeline.tips.has(this)) + timeline.tips.delete(this); + e.prev[i] = this; + } + } + delete timeline.pending[this.name]; + } + if (this.succ.length == 0) + timeline.tips.add(this); + + // FIXME: should undo the changes in case of a cycle exception ... + } + + add_edge_to_the_past(timeline, cause) { + // insert causality edge (self-to-cause) into topologically sorted graph + let visited = new Set(); + cause.cycl = true; + this._visit(cause.rank, visited) + cause.cycl = false; + + let si = this.indx; + let ci = cause.indx; + if (si < ci) + this._jump(timeline, ci); + else + this._rise(timeline) + + let a = Array.from(visited); + a.sort((x, y) => { + return y.indx - x.indx; + }); + for (let v of a) { + v._rise(timeline); // bubble up towards the future + v.vstd = false; + } + } + + _visit(rnk, visited) { // "affected" wave towards the future + let out = [[this]]; + while (out.length > 0) { + let o = out[out.length - 1]; + if (o.length == 0) { + out.pop(); + continue + } + let c = o.pop(); + c.vstd = true; + visited.add(c); + if (c.cycl) + throw new Error('cycle'); + if (c.rank <= (rnk + out.length - 1)) { + c.rank = rnk + out.length; + out.push(Array.from(c.succ)); + } + } + } + + _jump(timeline, pos) { + // this.indx pos + // v v + // before .. | e | f | g | h | ... -> future + // + // after .. | f | g | h | e | ... -> future + let si = this.indx + for (let i = si + 1; i < pos + 1; i++) + timeline.linear[i].indx -= 1; + timeline._move(si, pos); + this.indx = pos + } + + _rise(timeline) { + let len1 = timeline.linear.length - 1; + let si = this.indx; + var pos = si + while (pos < len1 && this.rank > timeline.linear[pos + 1].rank) + pos += 1; + while (pos < len1 && this.rank == timeline.linear[pos + 1].rank + && timeline.linear[pos + 1].name < this.name) + pos += 1; + if (si < pos) + this._jump(timeline, pos); + } + + static createSortNode(name, cycl, indx, rank, vstd, prev, succ) { + var node = new ScuttleSortNode() + node.name = name + node.cycl = cycl + node.indx = indx + node.rank = rank + node.vstd = vstd + node.prev = prev + node.succ = succ + return node + } + + toJSON() { + var json = {} + json.cycl = this.cycl + json.indx = this.indx + json.name = this.name + json.rank = this.rank + json.vstd = this.vstd + + json.prev = [] + for (let p of this.prev) { + if (typeof p != "string") { + json.prev.push(p.name) + } else { + json.prev.push(p) + } + } + + json.succ = [] + for (let s of this.succ) { + if (typeof s != "string") { + json.succ.push(s.name) + } else { + json.succ.push(s) + } + } + return json + } +} + +// module.exports = Timeline + +// eof diff --git a/js/tinySSB-for-Chrome/sketch.js b/js/tinySSB-for-Chrome/sketch.js new file mode 100644 index 0000000..630e51e --- /dev/null +++ b/js/tinySSB-for-Chrome/sketch.js @@ -0,0 +1,404 @@ +"use strict"; + +// the maximal dimensions of the sketches in px. +const SKETCH_MAX_HEIGHT = 500 +const SKETCH_MAX_WIDTH = 500 + +const SKETCH_SIZE_UPDATE_INTERVAL = 5000 + +var sketch_size_update_timer = null // reference to size update interval + +function chat_openSketch() { + closeOverlay() + // Create a canvas element + var canvas = document.createElement('canvas'); + canvas.id = 'sketchCanvas'; + canvas.style.position = 'fixed'; + canvas.style.top = '0'; + canvas.style.left = '0'; + canvas.width = window.innerWidth; // Full screen width + canvas.height = window.innerHeight; // Full screen height + + + canvas.style.backgroundColor = '#ffffff'; + document.body.appendChild(canvas); + + var currSize = 0; + var sizeDiv = document.createElement('div') + sizeDiv.id = 'div:sketch_size' + sizeDiv.style.position = 'fixed'; + sizeDiv.style.left = '10px'; + sizeDiv.style.top = '18px'; + sizeDiv.innerHTML = 'Size: ' + + async function sketch_updateSize() { + var new_size = await sketch_get_current_size() + if (new_size == currSize) { + return + } + currSize = new_size + var sizeKB = new_size / 1000 + sizeDiv.innerHTML = 'Size: ' + sizeKB + ' kB' + } + + sketch_updateSize() + sketch_size_update_timer = setInterval(async () => { + await sketch_updateSize() + }, SKETCH_SIZE_UPDATE_INTERVAL); + document.body.appendChild(sizeDiv) + + // Create a close button and style it + var closeButton = document.createElement('button'); + closeButton.id = 'btn:closeSketch'; + closeButton.innerHTML = 'Cancel'; + closeButton.style.position = 'fixed'; + closeButton.style.top = '10px'; + closeButton.style.right = '10px'; + closeButton.style.padding = '10px'; + closeButton.style.backgroundColor = '#ff0000'; + closeButton.style.color = '#ffffff'; + closeButton.style.border = 'none'; + closeButton.style.borderRadius = '5px'; + closeButton.style.cursor = 'pointer'; + closeButton.onclick = chat_closeSketch; + document.body.appendChild(closeButton); + + // Do the same with a submit button + var submitButton = document.createElement('button'); + submitButton.id = 'btn:submitSketch'; + submitButton.innerHTML = 'Submit'; + submitButton.style.position = 'fixed'; + submitButton.style.top = '10px'; + submitButton.style.right = '100px'; + submitButton.style.padding = '10px'; + submitButton.style.backgroundColor = '#008000'; + submitButton.style.color = '#ffffff'; + submitButton.style.border = 'none'; + submitButton.style.borderRadius = '5px'; + submitButton.style.cursor = 'pointer'; + submitButton.onclick = chat_sendDrawing; + document.body.appendChild(submitButton); + + //Create a color Palette, add it only with setColorPaletteButton function + var colorPalette = document.createElement('div'); + colorPalette.id = 'colorPalette'; + colorPalette.style.position = 'fixed'; + colorPalette.style.bottom = '10px'; + colorPalette.style.left = '45px'; + //document.body.appendChild(colorPalette); + + //Color ring addition + var colorChoiceButton = document.createElement('img'); + colorChoiceButton.id = 'colorChoiceButton'; + colorChoiceButton.src = 'img/color-wheel.png'; + colorChoiceButton.style.position = 'fixed'; + colorChoiceButton.style.bottom = '10px'; + colorChoiceButton.style.borderRadius = '50%'; + colorChoiceButton.style.left = '10px'; + colorChoiceButton.style.width = '30px'; + colorChoiceButton.style.height = '30px'; + colorChoiceButton.style.display = 'inline-block'; + colorChoiceButton.style.backgroundColor = 'red'; + colorChoiceButton.onclick = setColorPaletteButton; + document.body.appendChild(colorChoiceButton); + + //Array of colors we want to include + var colors = ['#000000', '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff']; + + // Add color buttons to the color palette + colors.forEach(function(color) { + var colorSwatch = document.createElement('div'); + colorSwatch.style.backgroundColor = color; + colorSwatch.style.width = '20px'; + colorSwatch.style.height = '20px'; + colorSwatch.style.borderRadius = '50%'; + colorSwatch.style.display = 'inline-block'; + colorSwatch.style.marginRight = '5px'; + colorSwatch.style.cursor = 'pointer'; + colorSwatch.onclick = function() { + setStrokeColor(color); + }; + colorPalette.appendChild(colorSwatch); + }); + + //Create and style small thickness button + var changeSmallLine = document.createElement('div'); + changeSmallLine.id = 'changeSmallLine'; + changeSmallLine.style.position = 'fixed'; + changeSmallLine.style.bottom = '10px'; + changeSmallLine.style.right = '50px'; + changeSmallLine.style.width = '10px'; + changeSmallLine.style.height = '10px'; + changeSmallLine.style.display = 'inline-block'; + changeSmallLine.style.backgroundColor = 'black'; + changeSmallLine.onclick = () => {changeThickness(2);}; + document.body.appendChild(changeSmallLine); + + //Do the same for medium thickness + var changeMediumLine = document.createElement('div'); + changeMediumLine.id = 'changeMediumLine'; + changeMediumLine.style.position = 'fixed'; + changeMediumLine.style.bottom = '10px'; + changeMediumLine.style.right = '65px'; + changeMediumLine.style.width = '15px'; + changeMediumLine.style.height = '15px'; + changeMediumLine.style.display = 'inline-block'; + changeMediumLine.style.backgroundColor = 'black'; + changeMediumLine.onclick = () => {changeThickness(5);}; + document.body.appendChild(changeMediumLine); + + //Do the same for large thickness + var changeLargeLine = document.createElement('div'); + changeLargeLine.id = 'changeLargeLine'; + changeLargeLine.style.position = 'fixed'; + changeLargeLine.style.bottom = '10px'; + changeLargeLine.style.right = '85px'; + changeLargeLine.style.width = '20px'; + changeLargeLine.style.height = '20px'; + changeLargeLine.style.display = 'inline-block'; + changeLargeLine.style.backgroundColor = 'black'; + changeLargeLine.onclick = () => {changeThickness(10);}; + document.body.appendChild(changeLargeLine); + + //Add an eraser + var eraserSign = document.createElement('img'); + eraserSign.id = 'eraserSign'; + eraserSign.src = 'img/eraser.png'; + eraserSign.style.position = 'fixed'; + eraserSign.style.bottom = '10px'; + eraserSign.style.right = '10px'; + eraserSign.style.width = '20px'; + eraserSign.style.height = '20px'; + eraserSign.style.cursor = 'pointer'; + eraserSign.onclick = toggleEraser; + document.body.appendChild(eraserSign); + + //get the context of the canvas and set initial drawing settings + var ctx = canvas.getContext('2d'); + //ctx.fillStyle = "white"; + //ctx.fillRect(0 , 0, canvas.width, canvas.height) + var currentWidth = 2; + + var strokeColor = '#000000'; + var isDrawing = false; + var isEraserEnabled = false; + var lastX = 0; + var lastY = 0; + var colChoice = true; + var currentColor = '#000000' + + + + + canvas.addEventListener('touchstart', startDrawing); + canvas.addEventListener('touchmove', draw); + canvas.addEventListener('touchend', endDrawing); + canvas.addEventListener('touchcancel', endDrawing); + + //Drawing function when user starts drawing (on touchstart) + function startDrawing(e) { + e.preventDefault(); + isDrawing = true; + var rect = e.target.getBoundingClientRect(); + [lastX, lastY] = [e.touches[0].clientX - rect.left, e.touches[0].clientY - rect.top]; + } + + + //Function when users move their finger to continue drawing + function draw(e) { + if (!isDrawing) return; + e.preventDefault(); + var rect = e.target.getBoundingClientRect(); + ctx.beginPath(); + ctx.strokeStyle = strokeColor; + ctx.moveTo(lastX, lastY); + ctx.lineTo(e.touches[0].clientX - rect.left, e.touches[0].clientY - rect.top); + ctx.stroke(); + [lastX, lastY] = [e.touches[0].clientX - rect.left, e.touches[0].clientY - rect.top]; + } + + //set isDrawing to false when users remove their finger + function endDrawing() { + isDrawing = false; + } + + //function to decide the drawing color, called by the color buttons above + function setStrokeColor(color) { + ctx.globalCompositeOperation = 'source-over'; + strokeColor = color; + currentColor = color + } + + //function to either add or remove the color Palette depending on if it exists + //Called when the color ring is clicked + function setColorPaletteButton () { + if (colChoice == true) { + document.body.appendChild(colorPalette); + colChoice = false; + } else { + colorPalette.parentNode.removeChild(colorPalette); + colChoice = true; + } + + } + + //function to change thickness of the pinsel, called by the thickness buttons above + function changeThickness(x) { + ctx.lineWidth = x; + currentWidth = x; + } + + //eraser function + function toggleEraser() { + isEraserEnabled = !isEraserEnabled; + if (isEraserEnabled) { + ctx.globalCompositeOperation = 'destination-out'; + ctx.strokeStyle = "rgba(255,255,255,1)"; + ctx.lineWidth = 30; + eraserSign.style.border = '1px solid red'; + } else { + setStrokeColor(currentColor) + ctx.lineWidth = currentWidth; + eraserSign.style.border = ''; + } + } +} + +//function called by the close button to end the sketch +function chat_closeSketch() { + if (sketch_size_update_timer) { + clearInterval(sketch_size_update_timer) // stop updating size + sketch_size_update_timer = null + } + + // Remove the canvas element + var canvas = document.getElementById('sketchCanvas'); + canvas.parentNode.removeChild(canvas); + + var sizeDiv = document.getElementById('div:sketch_size') + sizeDiv.parentNode.removeChild(sizeDiv) + + // Remove the close button + var closeButton = document.getElementById('btn:closeSketch'); + closeButton.parentNode.removeChild(closeButton); + + var submitButton = document.getElementById('btn:submitSketch'); + submitButton.parentNode.removeChild(submitButton); + + // Remove the color Choice Button + var colorChoiceButton = document.getElementById('colorChoiceButton'); + colorChoiceButton.parentNode.removeChild(colorChoiceButton); + + // Remove the eraser sign + var eraserSign = document.getElementById('eraserSign'); + eraserSign.parentNode.removeChild(eraserSign); + + //Remove the changeSmallLine Button + var changeSmallLine = document.getElementById('changeSmallLine'); + changeSmallLine.parentNode.removeChild(changeSmallLine); + + //Remove the changeMediumLine Button + var changeMediumLine = document.getElementById('changeMediumLine'); + changeMediumLine.parentNode.removeChild(changeMediumLine); + + //Remove the changeLargeLine Button + var changeLargeLine = document.getElementById('changeLargeLine'); + changeLargeLine.parentNode.removeChild(changeLargeLine); + + // Remove the color palette if it exists (is open) + var colorPalette = document.getElementById('colorPalette'); + if (colorPalette) { + colorPalette.parentNode.removeChild(colorPalette); + } +} + +function sketch_reduceResolution(base64String, reductionFactor) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = base64String; + + // Wait for the image to load + img.onload = function () { + const canvas = document.createElement("canvas"); + + const reducedWidth = img.width / reductionFactor; + const reducedHeight = img.height / reductionFactor; + canvas.width = reducedWidth; + canvas.height = reducedHeight; + + const ctx = canvas.getContext("2d"); + + ctx.drawImage(img, 0, 0, reducedWidth, reducedHeight); + const resultBase64String = canvas.toDataURL("image/png"); + resolve(resultBase64String); + }; + + img.onerror = function () { + reject(new Error("Sketch - Failed to reduce resolution")); + }; + }); +} + +// returns the size of the base64 string that represents the sketch +async function sketch_get_current_size() { + let sketch = await sketch_getImage() + return sketch.length +} + +// return the current sketch as a base64 string (including the preceding data type descriptor) +async function sketch_getImage() { + let canvas = document.getElementById("sketchCanvas") + var drawingUrl = canvas.toDataURL('image/png'); + var reductionFactor = Math.max(canvas.width / SKETCH_MAX_WIDTH, canvas.height / SKETCH_MAX_HEIGHT) + if (reductionFactor > 1) { + drawingUrl = await sketch_reduceResolution(drawingUrl, reductionFactor) + } + + var data = drawingUrl.split(',')[1]; + + // We Convert the data to a Uint8Array + var byteArray = atob(data) + .split('') + .map(function (char) { + return char.charCodeAt(0); + }); + var uint8Array = new Uint8Array(byteArray); + + // We Use pako to compress the Uint8Array + var compressedData = pako.deflate(uint8Array); + + // We Convert the compressed data back to a base64 string + var compressedBase64 = btoa(String.fromCharCode.apply(null, compressedData)); + + // We Create a new data URL with the compressed data + var shortenedDataURL = 'data:image/png;base64,' + compressedBase64; + + + return shortenedDataURL +} + +//function called by the drawing submit button +async function chat_sendDrawing() { + var sketch = await sketch_getImage() + if (sketch.length == 0) { + return; + } + + // send to backend + var recps; + if (curr_chat == "ALL") { + recps = "ALL"; + backend("publ:post [] " + btoa(base64Image) + " null"); // + recps) + } else { + recps = tremola.chats[curr_chat].members.join(' '); + backend("priv:post [] " + btoa(base64Image) + " null " + recps); + } + closeOverlay(); + setTimeout(function () { // let image rendering (fetching size) take place before we scroll + var c = document.getElementById('core'); + c.scrollTop = c.scrollHeight; + }, 100); + + // close sketch + chat_closeSketch(); +} diff --git a/js/tinySSB-for-Chrome/tremola.css b/js/tinySSB-for-Chrome/tremola.css new file mode 100644 index 0000000..36214b2 --- /dev/null +++ b/js/tinySSB-for-Chrome/tremola.css @@ -0,0 +1,690 @@ +:root { // note: Chrome <49 (bundled with SDK23) does not support var() + --background: #ebf4fa; // never used? + --passive: #51a4d2; + --active: #a4cbe2; + --light: #ebf4fa; + --neutral: #cbd4da; + --gray: #d0d0d0; + --red: #e85132; + --lightGreen: #bddb88; +} + + html, body { + height: 100%; + width: 100%; + margin: 0px; + font-family: "Helvetica Neue", sans-serif; + font-size: medium; + } + table { + border-spacing: 0px; + } + button { + border-radius: 5px; + } + .active { + background-color: #a4cbe2; // var(--active); + } + .passive { + background-color: #51a4d2; // var(--passive); + } + .neutral { + background-color: var(--neutral); + } + .light { + background-color: #ebf4fa; // var(--light); + } + .green { + background-color: #9dbb68; // var(--lightGreen); + } + .red { + background-color: #ff3300; + } + .gray { + background-color: var(--gray); + } + .flat { + border: none; + font-size: small; + user-select: none; + height: 100%; + } + .flat:focus { + outline: none; + } + .flat:active { + color: red; + } + .w100 { + width: 100%; + } + .buttontext { + font-size: small; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + height: 50px; + padding: 3px; + } +.item { + border: none; + text-align: left; + vertical-align: top; + height: 4em; + font-size: medium; +} + +textarea { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + font-size: medium; + resize: none; + width: 100%; +} + +.menu-overlay { + display: none; + position: absolute; + right: 10px; + top: 10px; + background: #fff; + padding: 0.5em; + z-index: 10002; /* high z-index */ + border-radius: 5px; + box-shadow: 0 0 25px rgba(0,0,0,0.9); +} + +.qr-overlay { + display: none; + background: #fff; + padding: 0.5em; + width: calc(80% - 16px); + position: fixed; + top: 10%; + left: 10%; + cursor: default; + z-index: 10001; + border-radius: 4px; + box-shadow: 0 0 25px rgba(0,0,0,0.9); + overflow-wrap: break-word; +} + +.overlay-bg { + display: none; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + cursor: pointer; + z-index: 1000; /* high z-index */ + background: rgba(1,1,1,0.3); + -webkit-tap-highlight-color: transparent; +} + +.overlay-trans { + display: none; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + cursor: pointer; + z-index: 1000; /* high z-index */ + background: trans; + -webkit-tap-highlight-color: transparent; +} + +.overlay-trans-core { + display: none; + position: absolute; + top: 20; + left: 0; + height: 85%; + width: 100%; + cursor: pointer; + z-index: 1000; /* high z-index */ + background: trans; + -webkit-tap-highlight-color: transparent; +} + +.overlay-bg-core { + display: none; + position: absolute; + top: 20; + left: 0; + height: 85%; + width: 100%; + cursor: pointer; + z-index: 1000; /* high z-index */ + background: rgba(1,1,1,0.3); + -webkit-tap-highlight-color: transparent; +} + +.float_button { + position: absolute; + top: calc(100% - 110pt); + left: calc(100% - 50pt); + height: 50px; + width: 50px; + cursor: pointer; + z-index: 999; /* high z-index */ + background: #000; /* fallback */ + background-color: #fff; + border: solid 1px #000; + border-radius: 50%; + font-size: xx-large; + line-height: 36pt; + font-family: HelveticaNeue-Light; + box-shadow: 0 0 30px rgba(0,0,0,0.7); +} + +.chat_item_button { + border: none; + text-align: left; + vertical-align: top; + height: 3em; + font-size: medium; + border-radius: 4pt; + box-shadow: 0 0 5px rgba(0,0,0,0.7); +} + +.chat_item_div { + padding: 0px 5px 10px 5px; + margin: 3px 3px 6px 3px; +} + +.contact_picture { + height: 3em; + width: 3em; + border: none; + border-radius: 50%; + background: transparent; + font-size: medium; + box-shadow: 0 0 5px rgba(0,0,0,0.7); +} + +.contact_item_button { + border: none; + text-align: left; + vertical-align: top; + width: calc(100%-10em); + height: 3em; + font-size: medium; + border-radius: 4pt; + box-shadow: 0 0 5px rgba(0,0,0,0.7); + text-overflow: ellipis; + overflow: hidden; + display: inline-block; +} + +.menu_item_button { + border: none; + text-align: left; + vertical-align: top; + height: 3em; + width: 100%; + font-size: medium; + background-color: white; +} + +.attach-menu-item-button { + border: none; + text-align: left; + vertical-align: top; + height: 3em; + width: 100%; + font-size: medium; + background-color: white; +} + +.attach-menu { + border: none; + text-align: center; + vertical-align: bottom; + height: 3em; + width: 100%; + font-size: medium; + background-color: white; +} + +.attach-menu-overlay { + display: none; + position: absolute; + right: 10px; + bottom: 70px; + background: #fff; + padding: 0.5em; + z-index: 1001; /* high z-index */ + border-radius: 5px; + box-shadow: 0 0 25px rgba(0,0,0,0.9); +} + +.settings { + height: 2em; + padding: 6px; + background: rgba(255,255,255,0.75); + display: flex; + justify-content: space-between; +} + +.settingsText { + white-space: nowrap; + overflow: hidden; + text-overflow: clip; + margin-top: 6px; + font-weight: 500; +} + +/* --------------------------------------------------------------------------- */ + +#snackbar { + visibility: hidden; /* Hidden by default. Visible on click */ + min-width: 250px; /* Set a default minimum width */ + margin-left: -125px; /* Divide value of min-width by 2 */ + background-color: #333; /* Black background color */ + color: #fff; /* White text color */ + text-align: center; /* Centered text */ + border-radius: 2px; /* Rounded borders */ + padding: 16px; /* Padding */ + position: fixed; /* Sit on top of the screen */ + z-index: 1; /* Add a z-index if needed */ + left: 50%; /* Center the snackbar */ + bottom: 30px; /* 30px from the bottom */ +} + +/* Show the snackbar when clicking on a button (class added with JavaScript) */ +#snackbar.show { + visibility: visible; /* Show the snackbar */ + /* Add animation: Take 0.5 seconds to fade in and out the snackbar. + However, delay the fade out process for 2.5 seconds */ + -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; + animation: fadein 0.5s, fadeout 0.5s 2.5s; +} + +/* Animations to fade the snackbar in and out */ +@-webkit-keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@-webkit-keyframes fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} +} + +@keyframes fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} +} + +/* --------------------------------------------------------------------------- */ +/* https://www.w3schools.com/howto/howto_css_switch.asp */ + + /* The switch - the box around the slider */ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +/* Hide default HTML checkbox */ +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +/* The slider */ +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #a4cbe2; // var(--active); + -webkit-transition: .4s; + transition: .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .slider { + background-color: #51a4d2; // var(--passive); +} + +input:focus + .slider { + /* box-shadow: 0 0 1px #2196F3; */ + box-shadow: transparent; +} + +input:checked + .slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +/* Rounded sliders */ +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} + +.connection_entry_container { + display: grid; + /*grid-template-columns: 1fr 1fr 1fr;*/ + /*grid-template-rows: 1fr 1fr 1fr;*/ + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + gap: 0px 0px; + /*grid-template-areas: + "name name ." + ". . remaining" + "type type ."; + */ + grid-template-areas: + "info remaining"; + width:100%; + box-shadow: 0 0 5px rgba(0,0,0,0.7); + border-radius: 4pt; + height: 3em; + margin-bottom: 10px; +} + +.connection_entry_info_container { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + grid-template-areas: + "name" + "type"; + grid-area: info; +} + +.connection-overlay-progressbar { + width: 100%; + height: 25px; + background-color: lightgray; + border-radius: 5px; +} + +.connection-overlay-progressbar::-webkit-progress-bar { + border-radius: 5px; +} + +.connection-overlay-progressbar::-webkit-progress-value { + transition: width 1s ease; + border-radius: 5px; +} + +#connection-overlay-progressbar-want::-webkit-progress-value { + background: #2ecc71; +} + +#connection-overlay-progressbar-gift::-webkit-progress-value { + background: #7fd9eb; +} + +#connection-overlay-progressbar-goset::-webkit-progress-value { + background: #2ecc71; +} + +#connection-overlay-progressbar-chnk::-webkit-progress-value { + background: #f2a641; +} + +.connection-overlay-progressbar-label { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 15px; + color: black; + white-space: nowrap; +} + +.progress-icon { + position: absolute; + left: 2px; + top: 50%; + transform: translateY(-50%); + width: 24px; + height: 24px; +} + +.invalid { + border: 2px solid red; +} + +.valid { + border: 2px solid green; + transition: border-color 0.2s ease-in-out; +} + +.websocket_url_settings { + background: rgba(255,255,255,0.75); + padding:6px; + display: flex; + align-items: center; +} + +.img-gray-filter { + filter: invert(62%) sepia(8%) saturate(116%) hue-rotate(145deg) brightness(89%) contrast(89%); +} + +/* --------------------------------------------------------------------------- */ +/* Kanban Board */ +/* --------------------------------------------------------------------------- */ + +.board_item_button { + border: none; + text-align: left; + vertical-align: top; + height: 3em; + font-size: medium; + border-radius: 4pt; + box-shadow: 0 0 5px rgba(0,0,0,0.7); + /*background-color: #c1e1c1;*/ +} + +.columns_container { + position: relative; + width: 100%; + /*height: 100%;*/ + display: flex; + flex-direction: row; + justify-content: flex-start; + gap: 10px; +} + +.column { + display: flex; + justify-content: flex-start; + flex-direction: column; + border-radius: 5pt; +} + +.column_wrapper{ + flex: 0 0 36vw; + width: 36vw; +} + +.column_content{ + display: flex; + justify-content: flex-start; + flex-direction: column; + gap: 10px; + padding-bottom: 10px; + padding-top: 10px; +} + +.column_hdr { + width: 100%; + margin: 0 auto; + padding-top:5px; + padding-bottom:5px; + overflow-wrap: break-word; + background-color: #c1e1c1; + border-bottom-color: gray; + border-bottom-style: solid; + border-bottom-width: 2px; + border-radius: 5pt; +} + +/* +.column_options{ + display: flex; + flex-direction: row; + justify-content: space-between; +} +*/ + +.context_options_btn { + border: none; + text-align: left; + height: 3em; + width: 100%; + font-size: medium; + background-color: white; +} + + +.context_menu { + display: none; + width: 10%; + max-height: 80vh; + position: absolute; + background-color: #f9f9f9; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1001; + overflow-x: hidden; + overflow-y:scroll; +} + +.column_item { + width:95%; + margin: auto; + font-size: medium; + border-radius: 4pt; + box-shadow: 0 0 5px rgb(0 0 0 / 70%); + min-height: 3em; +} + +.item_button { + width:100%; + border: none; + text-align: left; + vertical-align: top; + font-size: medium; + border-radius: 4pt; + box-shadow: 0 0 5px rgba(0,0,0,0.7); + white-space: normal; + word-wrap: break-word; + min-height: 3em; +} + +.item_menu_content { + padding-top: 10px; + float: left; + width: 70%; +} + +.item_menu_desc { + margin-top: 10px; + border: none; + outline: none; + background-color: rgb(211,211,211); +} + +.item_menu_buttons{ + padding-top: 10px; + float: right; + width: 30%; +} + +.item_menu_button { + width: 90%; + float: right; + margin:5px auto; + display:block; +} + +.div:item_menu_assignees_container { + padding-top: 5px; + width: 100%; + display: flex; + justify-content: flex-start; + flex-direction: column; +} + +.column_footer { + width: 100%; + padding-top: 5px; + padding-bottom: 5px; + border-top-color: gray; + border-top-style: solid; + border-top-width: 2px; +} + + +.kanban_invitation_container { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + gap: 0px 0px; + grid-template-areas: + "text btns"; + width:100%; + box-shadow: 0 0 5px rgba(0,0,0,0.7); + border-radius: 4pt; + height: 3em; + margin-top: 5px; +} + +.kanban_invitation_text_container { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + gap: 0px 0px; + grid-template-areas: + "name" + "author"; + grid-area: text; +} + +.kanban_create_personal_btn { + background: none; + border: none; + cursor: pointer; + font-size: 16px; + margin: 0 10px; + background-color: #51a4d2; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + height: 40px; + width: 35px +} + diff --git a/js/tinySSB-for-Chrome/tremola.html b/js/tinySSB-for-Chrome/tremola.html new file mode 100644 index 0000000..39b5d25 --- /dev/null +++ b/js/tinySSB-for-Chrome/tremola.html @@ -0,0 +1,566 @@ + + + + + + + + + + + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ +
t i n y S S B
+ +
+
+
+ +
+ +
+ + + + + + + + + + + +
+
+
+ Would you like to create your own personal kanban board?
+ (You can also generate new kanban boards using the + icon) +
+
+ + +
+
+
+ +
+
+ Invitations +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ Name of new chat:
 
+   + +
+ + +   + +   +
+ Enter desired alias:
+   + +
+
+
+ + + +
+
Replication Status
+
+
+ Icon + +
+
+
+ Icon + +
+
+
+ Icon + +
+
+
+ + + +
Connected Devices
+
+
+ +
+
+
did
+
+ +
+
to_be_filled
+
+ +
+ +
+
+
+ +
+
to_be_filled
+ +
+ +
+ +
+
+
+ +
+
Edit X:
+   +
+
+ +
+
Import new ID
+

Caution: Ensure you avoid utilizing the same identity simultaneously on multiple devices, as this could result in concurrent actions, thus rendering your log invalid.

+
Import via QR Code
+
+
+
+
Import via copied secret key
+ + +
+
+ +
the msg
+ +
+
+ About
+ +

tinySSB, August 2023 + +


+

Software (c) 2021-2023:
+ Jannick Heisch
+ Etienne Mettaz
+ Cedrik Schimschar
+ Christian Tschudin
+   + +

Icons
+ https://www.flaticon.com/authors/kiranshastry
+ https://icon-icons.com/icon/kanban-board/120442
+ https://icon-icons.com/icon/paper-clip/102692
+ +


+ +

MIT License for tinySSB (previously known as tinyTremola and VoSSBoL) + +

Copyright (c) 2021-23 Computer Networks Group, University of Basel + +

Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +

The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +


+ +

MIT License for QR code generation library +

Copyright (c) 2012 davidshimjs + +

Permission is hereby granted, free of charge, + to any person obtaining a copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + +

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ + +
+ +

MIT License for pinch-zoom library +

Copyright (c) 2013-2019 Manuel Stofer + +

Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +

The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + +

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + + +

+
+ + + +
+ +
+ + +
+ + +
+ +
+ + + + + + + + + +
+ + diff --git a/js/tinySSB-for-Chrome/tremola.js b/js/tinySSB-for-Chrome/tremola.js new file mode 100644 index 0000000..5a01d7b --- /dev/null +++ b/js/tinySSB-for-Chrome/tremola.js @@ -0,0 +1,1335 @@ +// tremola.js + +"use strict"; + +var tremola; +var curr_chat; +var qr; +var myId; +var localPeers = {}; // feedID ~ [isOnline, isConnected] - TF, TT, FT - FF means to remove this entry +var must_redraw = false; +var edit_target = ''; +var new_contact_id = ''; +var colors = ["#d9ceb2", "#99b2b7", "#e6cba5", "#ede3b4", "#8b9e9b", "#bd7578", "#edc951", + "#ffd573", "#c2a34f", "#fbb829", "#ffab03", "#7ab317", "#a0c55f", "#8ca315", + "#5191c1", "#6493a7", "#bddb88"] +var curr_img_candidate = null; +var pubs = [] +var wants = {} + +var restream = false // whether the backend is currently restreaming all posts + +// --- menu callbacks + +/* +function menu_sync() { + if (localPeers.length == 0) + launch_snackbar("no local peer to sync with"); + else { + for (var i in localPeers) { + backend("sync " + i); + launch_snackbar("sync launched"); + break + } + } + closeOverlay(); +} +*/ + +function menu_new_conversation() { + fill_members(); + prev_scenario = 'chats'; + setScenario("members"); + document.getElementById("div:textarea").style.display = 'none'; + document.getElementById("div:confirm-members").style.display = 'flex'; + document.getElementById("tremolaTitle").style.display = 'none'; + var c = document.getElementById("conversationTitle"); + c.style.display = null; + c.innerHTML = "Create New Conversation
Select up to 7 members"; + document.getElementById('plus').style.display = 'none'; + closeOverlay(); +} + +function menu_new_contact() { + document.getElementById('new_contact-overlay').style.display = 'initial'; + document.getElementById('overlay-bg').style.display = 'initial'; + // document.getElementById('chat_name').focus(); + overlayIsActive = true; +} + +function menu_new_pub() { + menu_edit('new_pub_target', "Enter address of trustworthy pub

Format:
net:IP_ADDR:PORT~shs:ID_OF_PUB", ""); +} + +function menu_invite() { + menu_edit('new_invite_target', "Enter invite code

Format:
IP_ADDR:PORT:@ID_OF_PUB.ed25519~INVITE_CODE", ""); +} + +function menu_redraw() { + closeOverlay(); + + load_chat_list() + + document.getElementById("lst:contacts").innerHTML = ''; + load_contact_list(); + + if (curr_scenario == "posts") + load_chat(curr_chat); +} + +function menu_edit(target, title, text) { + closeOverlay() + document.getElementById('edit-overlay').style.display = 'initial'; + document.getElementById('overlay-bg').style.display = 'initial'; + document.getElementById('edit_title').innerHTML = title; + document.getElementById('edit_text').value = text; + document.getElementById('edit_text').focus(); + overlayIsActive = true; + edit_target = target; +} + +function onEnter(ev) { + + if (ev.key == "Enter") { + switch(ev.target.id) { + case 'edit_text': + edit_confirmed() + break + case 'settings_urlInput': + btn_setWebsocketUrl() + break + case 'import-id-input': + btn_import_id() + break + } + } +} + +function menu_edit_convname() { + menu_edit('convNameTarget', "Edit conversation name:
(only you can see this name)", tremola.chats[curr_chat].alias); +} + +// function menu_edit_new_contact_alias() { +// menu_edit('new_contact_alias', "Assign alias to new contact:", ""); +// } + +function edit_confirmed() { + closeOverlay() + console.log("edit confirmed: " + edit_target) + var val = document.getElementById('edit_text').value; + if (edit_target == 'convNameTarget') { + var ch = tremola.chats[curr_chat]; + ch.alias = val; + persist(); + load_chat_title(ch); // also have to update entry in chats + menu_redraw(); + } else if (edit_target == 'new_contact_alias' || edit_target == 'trust_wifi_peer') { + document.getElementById('contact_id').value = ''; + if (val == '') + val = id2b32(new_contact_id); + tremola.contacts[new_contact_id] = { + "alias": val, "initial": val.substring(0, 1).toUpperCase(), + "color": colors[Math.floor(colors.length * Math.random())], + "iam": "", "forgotten": false + }; + var recps = [myId, new_contact_id]; + var nm = recps2nm(recps); + // TODO reactivate when encrypted chats are implemented + /* + tremola.chats[nm] = { + "alias": "Chat w/ " + val, "posts": {}, "members": recps, + "touched": Date.now(), "lastRead": 0, "timeline": new Timeline() + }; + */ + persist(); + backend("add:contact " + new_contact_id + " " + btoa(val)) + menu_redraw(); + } else if (edit_target == 'new_pub_target') { + console.log("action for new_pub_target") + } else if (edit_target == 'new_invite_target') { + backend("invite:redeem " + val) + } else if (edit_target == 'new_board') { + console.log("action for new_board") + if (val == '') { + console.log('empty') + return + } + //create new board with name = val + createBoard(val) + } else if (edit_target == 'board_rename') { + var board = tremola.board[curr_board] + if (val == '') { + menu_edit('board_rename', 'Enter a new name for this board', board.name) + launch_snackbar("Enter a name") + return + } + if (val == board.name) { + menu_edit('board_rename', 'Enter a new name for this board', board.name) + launch_snackbar('This board already have this name') + return + } + renameBoard(curr_board, val) + } else if (edit_target == 'board_new_column') { + if (val == '') { + menu_edit('board_new_column', 'Enter name of new List: ', '') + launch_snackbar("Enter a name") + return + } + createColumn(curr_board, val) + + } else if (edit_target == 'board_new_item') { + if (val == '') { + menu_edit('board_new_item', 'Enter name of new Card: ', '') + launch_snackbar("Enter a name") + return + } + createColumnItem(curr_board, curr_column, val) + } else if (edit_target == 'board_rename_column') { + if (val == '') { + menu_rename_column(curr_column) + launch_snackbar("Please enter a new Name") + return + } + + if (val == tremola.board[curr_board].columns[curr_column].name) + return + + renameColumn(curr_board, curr_column, val) + } else if (edit_target == 'board_rename_item') { + + if (val != tremola.board[curr_board].items[curr_rename_item].name && val != '') { + renameItem(curr_board, curr_rename_item, val) + } + item_menu(curr_rename_item) + } +} + +function members_confirmed() { + if (prev_scenario == 'chats') { + new_conversation() + } else if (prev_scenario == 'kanban') { + menu_new_board_name() + } +} + +function menu_forget_conv() { + // toggles the forgotten flag of a conversation + if (curr_chat == recps2nm([myId])) { + launch_snackbar("cannot be applied to own notes"); + return; + } + tremola.chats[curr_chat].forgotten = !tremola.chats[curr_chat].forgotten; + persist(); + load_chat_list() // refresh list of conversations + closeOverlay(); + if (curr_scenario == 'posts' /* should always be true */ && tremola.chats[curr_chat].forgotten) + setScenario('chats'); + else + load_chat(curr_chat) // refresh currently displayed list of posts +} + +function menu_import_id() { + closeOverlay(); + document.getElementById('import-id-overlay').style.display = 'initial' + document.getElementById('overlay-bg').style.display = 'initial' +} + +function btn_import_id() { + var str = document.getElementById('import-id-input').value + if(str == "") + return + var r = import_id(str) + if(r) { + launch_snackbar("Successfully imported, restarting...") + } else { + launch_snackbar("wrong format") + } +} + +function menu_process_msgs() { + backend('process.msg'); + closeOverlay(); +} + +function menu_add_pub() { + // ... + closeOverlay(); +} + +function menu_dump() { + backend('dump:'); + closeOverlay(); +} + +function menu_take_picture() { + disabled6676863(); // breakpoint using a non-existing fct,in case + closeOverlay(); + var draft = unicodeStringToTypedArray(document.getElementById('draft').value); // escapeHTML( + if (draft.length == 0) + draft = null; + else + draft = atob(draft); + console.log("getVoice" + document.getElementById('draft').value); + backend('get:voice ' + atob(draft)); +} + +function menu_pick_image() { + closeOverlay(); + backend('get:media'); +} + +// --- + +function new_text_post(s) { + if (s.length == 0) { + return; + } + var draft = unicodeStringToTypedArray(document.getElementById('draft').value); // escapeHTML( + var recps; + if (curr_chat == "ALL") { + recps = "ALL"; + backend("publ:post [] " + btoa(draft) + " null"); // + recps) + } else { + recps = tremola.chats[curr_chat].members.join(' '); + backend("priv:post [] " + btoa(draft) + " null " + recps); + } + document.getElementById('draft').value = ''; + closeOverlay(); + setTimeout(function () { // let image rendering (fetching size) take place before we scroll + var c = document.getElementById('core'); + c.scrollTop = c.scrollHeight; + }, 100); +} + +function new_voice_post(voice_b64) { + var draft = unicodeStringToTypedArray(document.getElementById('draft').value); // escapeHTML( + if (draft.length == 0) + draft = "null" + else + draft = btoa(draft) + if (curr_chat == "ALL") { + // recps = "ALL"; + backend("publ:post [] " + draft + " " + voice_b64); // + recps) + } else { + recps = tremola.chats[curr_chat].members.join(' '); + backend("priv:post [] " + draft + " " + voice_b64 + " " + recps); + } + document.getElementById('draft').value = ''; +} + +function play_voice(nm, ref) { + var p = tremola.chats[nm].posts[ref]; + var d = new Date(p["when"]); + d = d.toDateString() + ' ' + d.toTimeString().substring(0, 5); + backend("play:voice " + p["voice"] + " " + btoa(fid2display(p["from"])) + " " + btoa(d)); +} + +function new_image_post() { + if (curr_img_candidate == null) { + return; + } + var draft = "![](" + curr_img_candidate + ")\n"; + var caption = document.getElementById('image-caption').value; + if (caption && caption.length > 0) + draft += caption; + var recps = tremola.chats[curr_chat].members.join(' ') + backend("priv:post " + btoa(draft) + " " + recps); + curr_img_candidate = null; + closeOverlay(); + setTimeout(function () { // let image rendering (fetching size) take place before we scroll + var c = document.getElementById('core'); + c.scrollTop = c.scrollHeight; + }, 100); +} + +function load_post_item(p) { // { 'key', 'from', 'when', 'body', 'to' (if group or public)> + var pl = document.getElementById('lst:posts'); + var is_other = p["from"] != myId; + var box = "
" + fid2display(p["from"]) + "
"; + var txt = "" + if (p["body"] != null) { + txt = escapeHTML(p["body"]).replace(/\n/g, "
\n"); + // Sketch app + if (txt.startsWith("data:image/png;base64")) { // check if the string is a data url + var compressedBase64 = txt.split(',')[1]; + // We Convert the compressed data from a base64 string to a Uint8Array + var compressedData = atob(compressedBase64) + .split('') + .map(function (char) { + return char.charCodeAt(0); + }); + var uint8Array = new Uint8Array(compressedData); + + // We to decompress the Uint8Array + var decompressedData = pako.inflate(uint8Array); + // We Convert the decompressed data back to a base64 string + var decompressedBase64 = btoa(String.fromCharCode.apply(null, decompressedData)); + // We Create a new data URL with the decompressed data + var decompressedDataURL = 'data:image/png;base64,' + decompressedBase64; + //display the data url as an image element + box += "Drawing"; + txt = ""; + } + var re = /!\[.*?\]\((.*?)\)/g; + txt = txt.replace(re, "    "); + // txt = txt + "    (!)"; + // console.log(txt); + } + if (p.voice != null) + box += "🔊  " + box += txt + var d = new Date(p["when"]); + d = d.toDateString() + ' ' + d.toTimeString().substring(0, 5); + box += "
"; + box += d + "
"; + var row; + if (is_other) { + var c = tremola.contacts[p.from] + row = "" + // row = ">" + row += "" + box + ""; + } else { + row = "" + box; + row += "<" + } + pl.insertRow(pl.rows.length).innerHTML = row; +} + +function load_chat(nm) { + var ch, pl, e; + ch = tremola.chats[nm] + if (ch.timeline == null) + ch["timeline"] = new Timeline(); + pl = document.getElementById("lst:posts"); + while (pl.rows.length) { + pl.deleteRow(0); + } + pl.insertRow(0).innerHTML = "     "; + curr_chat = nm; + var lop = []; // list of posts + for (var p in ch.posts) lop.push(p) + lop.sort(function (a, b) { + return ch.posts[a].when - ch.posts[b].when + }) + lop.forEach(function (p) { + load_post_item(ch.posts[p]) + }) + load_chat_title(ch); + setScenario("posts"); + document.getElementById("tremolaTitle").style.display = 'none'; + // update unread badge: + ch["lastRead"] = Date.now(); + persist(); + document.getElementById(nm + '-badge').style.display = 'none' // is this necessary? + setTimeout(function () { // let image rendering (fetching size) take place before we scroll + var c = document.getElementById('core'); + c.scrollTop = c.scrollHeight; + }, 100); + /* + // scroll to bottom: + var c = document.getElementById('core'); + c.scrollTop = c.scrollHeight; + document.getElementById('lst:posts').scrollIntoView(false) + // console.log("did scroll down, but did it do it?") + */ +} + +function load_chat_title(ch) { + var c = document.getElementById("conversationTitle"), bg, box; + c.style.display = null; + c.setAttribute('classList', ch.forgotten ? 'gray' : '') // old JS (SDK 23) + box = "
" + escapeHTML(ch.alias) + "
"; + box += "
" + escapeHTML(recps2display(ch.members)) + "
"; + c.innerHTML = box; +} + +function load_chat_list() { + var meOnly = recps2nm([myId]) + // console.log('meOnly', meOnly) + document.getElementById('lst:chats').innerHTML = ''; + // load_chat_item(meOnly) TODO reactivate when encrypted chats are implemented + var lop = []; + for (var p in tremola.chats) { + if (p != meOnly && !tremola.chats[p]['forgotten']) + lop.push(p) + } + lop.sort(function (a, b) { + return tremola.chats[b]["touched"] - tremola.chats[a]["touched"] + }) + lop.forEach(function (p) { + load_chat_item(p) + }) + // forgotten chats: unsorted + if (!tremola.settings.hide_forgotten_conv) + for (var p in tremola.chats) + if (p != meOnly && tremola.chats[p]['forgotten']) + load_chat_item(p) +} + +function load_chat_item(nm) { // appends a button for conversation with name nm to the conv list + var cl, mem, item, bg, row, badge, badgeId, cnt; + cl = document.getElementById('lst:chats'); + // console.log(nm) + if (nm == "ALL") + mem = "ALL"; + else + mem = recps2display(tremola.chats[nm].members); + item = document.createElement('div'); + // item.style = "padding: 0px 5px 10px 5px; margin: 3px 3px 6px 3px;"; + item.setAttribute('class', 'chat_item_div'); // old JS (SDK 23) + if (tremola.chats[nm].forgotten) bg = ' gray'; else bg = ' light'; + row = ""; + row += "" + item.innerHTML = row; + cl.appendChild(item); + set_chats_badge(nm) +} + +function load_contact_list() { + document.getElementById("lst:contacts").innerHTML = ''; + for (var id in tremola.contacts) + if (!tremola.contacts[id].forgotten) + load_contact_item([id, tremola.contacts[id]]); + if (!tremola.settings.hide_forgotten_contacts) + for (var id in tremola.contacts) { + var c = tremola.contacts[id] + if (c.forgotten) + load_contact_item([id, c]); + } +} + +function load_contact_item(c) { // [ id, { "alias": "thealias", "initial": "T", "color": "#123456" } ] } + var row, item = document.createElement('div'), bg; + item.setAttribute('style', 'padding: 0px 5px 10px 5px;'); // old JS (SDK 23) + if (!("initial" in c[1])) { + c[1]["initial"] = c[1].alias.substring(0, 1).toUpperCase(); + persist(); + } + if (!("color" in c[1])) { + c[1]["color"] = colors[Math.floor(colors.length * Math.random())]; + persist(); + } + // console.log("load_c_i", JSON.stringify(c[1])) + bg = c[1].forgotten ? ' gray' : ' light'; + row = ""; + row += ""; + // var row = ""; + // console.log(row); + item.innerHTML = row; + document.getElementById('lst:contacts').appendChild(item); +} + +function fill_members() { + var choices = ''; + for (var m in tremola.contacts) { + choices += '
\n'; + } + document.getElementById('lst:members').innerHTML = choices + /* + + */ + document.getElementById(myId).checked = true; + document.getElementById(myId).disabled = true; +} + +function show_contact_details(id) { + if (id == myId) { + document.getElementById('old_contact_alias_hdr').innerHTML = "Alias: (own name, visible to others)" + } else { + document.getElementById('old_contact_alias_hdr').innerHTML = "Alias: (only you can see this alias)" + } + var c = tremola.contacts[id]; + new_contact_id = id; + document.getElementById('old_contact_alias').value = c['alias']; + var details = ''; + details += '
IAM-Alias:  ' + (c.iam != "" ? c.iam : "—") + '
\n'; + details += '
Shortname:  ' + id2b32(id) + '
\n'; + details += '
SSB identity:  ' + id + '
\n'; + details += '
Forget this contact
' + document.getElementById('old_contact_details').innerHTML = details; + document.getElementById('old_contact-overlay').style.display = 'initial'; + document.getElementById('overlay-bg').style.display = 'initial'; + document.getElementById('hide_contact').checked = c.forgotten; + + document.getElementById('old_contact_alias').focus(); + overlayIsActive = true; +} + +function toggle_forget_contact(e) { + var c = tremola.contacts[new_contact_id]; + c.forgotten = !c.forgotten; + persist(); + closeOverlay(); + load_contact_list(); +} + +function save_content_alias() { + var c = tremola.contacts[new_contact_id]; + var val = document.getElementById('old_contact_alias').value; + var deleteAlias = false + + val.trim() + + if (val == '') { + deleteAlias = true + if (c.iam != "" && new_contact_id != myId) { + val = c.iam + } else { + val = id2b32(new_contact_id); + } + } + var old_alias = c.alias + c.alias = val; + c.initial = val.substring(0, 1).toUpperCase(); + c.color = colors[Math.floor(colors.length * Math.random())]; + + // update names in connected devices menu + for (var l in localPeers) { + if (localPeers[l].alias == old_alias) { + localPeers[l].alias = val + refresh_connection_entry(l) + } + } + + // share new alias with others via IAM message + if(new_contact_id == myId) { + if(deleteAlias) { + backend("iam " + btoa("")) + c.iam = "" + } else { + backend("iam " + btoa(val)) + c.iam = val + } + } + + persist(); + menu_redraw(); + closeOverlay(); +} + +function new_conversation() { + // { "alias":"local notes (for my eyes only)", "posts":{}, "members":[myId], "touched": millis } + var recps = [] + for (var m in tremola.contacts) { + if (document.getElementById(m).checked) + recps.push(m); + } + if (recps.indexOf(myId) < 0) + recps.push(myId); + if (recps.length > 7) { + launch_snackbar("Too many recipients"); + return; + } + var cid = recps2nm(recps) + if (cid in tremola.chats) { + if (tremola.chats[cid].forgotten) { + tremola.chats[cid].forgotten = false; + load_chat_list(); // refresh + } else + launch_snackbar("Conversation already exists"); + return; + } + var nm = recps2nm(recps); + if (!(nm in tremola.chats)) { + tremola.chats[nm] = { + "alias": "Unnamed conversation", "posts": {}, + "members": recps, "touched": Date.now(), "timeline": new Timeline() + }; + persist(); + } else + tremola.chats[nm]["touched"] = Date.now() + load_chat_list(); + setScenario("chats") + curr_chat = nm + menu_edit_convname() +} + +function load_peer_list() { + var i, lst = '', row; + for (i in localPeers) { + var x = localPeers[i], color, row, nm, tmp; + if (x[1]) color = ' background: var(--lightGreen);'; else color = ''; + tmp = i.split('~'); + nm = '@' + tmp[1].split(':')[1] + '.ed25519' + if (nm in tremola.contacts) + nm = ' / ' + tremola.contacts[nm].alias + else + nm = '' + row = ""; + row += ""; + lst += '
' + row + '
'; + // console.log(row) + } + document.getElementById('the:connex').innerHTML = lst; +} + +function show_peer_details(id) { + new_contact_id = "@" + id.split('~')[1].substring(4) + ".ed25519"; + // if (new_contact_id in tremola.constacts) + // return; + menu_edit("trust_wifi_peer", "Trust and Autoconnect
 
" + new_contact_id + "
 
Should this WiFi peer be trusted (and autoconnected to)? Also enter an alias for the peer - only you will see this alias", "?") +} + +function getUnreadCnt(nm) { + var c = tremola.chats[nm], cnt = 0; + for (var p in c.posts) { + if (c.posts[p].when > c.lastRead) + cnt++; + } + return cnt; +} + +function set_chats_badge(nm) { + var e = document.getElementById(nm + '-badge'), cnt; + cnt = getUnreadCnt(nm) + if (cnt == 0) { + e.style.display = 'none'; + return + } + e.style.display = null; + if (cnt > 9) cnt = ">9"; else cnt = "" + cnt; + e.innerHTML = cnt +} + +// --- util + +function unicodeStringToTypedArray(s) { + var escstr = encodeURIComponent(s); + var binstr = escstr.replace(/%([0-9A-F]{2})/g, function (match, p1) { + return String.fromCharCode('0x' + p1); + }); + return binstr; +} + +function toHex(s) { + return Array.from(s, function (c) { + return ('0' + (c.charCodeAt(0) & 0xFF).toString(16)).slice(-2); + }).join('') +} + +var b32enc_map = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + +function b32enc_do40bits(b40) { + var long = 0, s = ''; + for (var i = 0; i < 5; i++) long = long * 256 + b40[i]; + for (var i = 0; i < 8; i++, long /= 32) s = b32enc_map[long & 0x1f] + s; + return s; +} + +function b32encode(bytes) { + var b32 = '', cnt = bytes.length % 5, buf; + if (cnt == 0) buf = new Uint8Array(bytes.length); + else buf = new Uint8Array(bytes.length + 5 - cnt); + for (var i = 0; i < bytes.length; i++) { + buf[i] = bytes.charCodeAt(i); + } + while (buf.length > 0) { + b32 += b32enc_do40bits(buf.slice(0, 5)); + buf = buf.slice(5, buf.length); + } + if (cnt != 0) { + cnt = Math.floor(8 * (5 - cnt) / 5); + b32 = b32.substring(0, b32.length - cnt) + '======'.substring(0, cnt) + } + return b32; +} + +function id2b32(str) { // derive a shortname from the SSB id + try { + var b = atob(str.slice(1, -9)); // atob(str.substr(1, str.length-9)); + b = b32encode(b.slice(0, 7)).substr(0, 10); + return b.substring(0, 5) + '-' + b.substring(5); + } catch (err) { + } + return '??' +} + +function escapeHTML(str) { + return new Option(str).innerHTML; +} + +function recps2nm(rcps) { // use concat of sorted FIDs as internal name for conversation + // return "ALL"; + return rcps.sort().join('').replace(/.ed25519/g, ''); +} + +function recps2display(rcps) { + if (rcps == null) return 'ALL'; + var lst = rcps.map(function (fid) { + return fid2display(fid) + }); + return '[' + lst.join(', ') + ']'; +} + +function fid2display(fid) { + var a = ''; + if (fid in tremola.contacts) + a = tremola.contacts[fid].alias; + if (a == '') + a = fid.substring(0, 9); + return a; +} + +function import_id(json_str) { + var json + try { + json = JSON.parse(json_str) + } catch (e) { + return false // argument is not a valid json string + } + if (Object.keys(json).length != 2 || !('curve' in json) || !('secret' in json)) { + return false // wrong format + } + + backend("importSecret " + json['secret']) + return true +} + + +// --- Interface to Kotlin side and local (browser) storage + +function backend(cmdStr) { // send this to Kotlin (or simulate in case of browser-only testing) + if (typeof Android != 'undefined') { + Android.onFrontendRequest(cmdStr); + return; + } + // >> BEGIN INSERT VIRTBACKEND + if (cmdStr == 'ready' && window.location.search == '?virtualBackend=true') + return; + if (virtBackEnd != null) { + virtBackEnd.postMessage(['f2b', cmdStr], '*'); + return; + } + // << END INSERT VIRTBACKEND + cmdStr = cmdStr.split(' ') + if (cmdStr[0] == 'ready') + b2f_initialize('@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=.ed25519') + else if (cmdStr[0] == 'exportSecret') + b2f_showSecret('secret_of_id_which_is@AAAA==.ed25519') + else if (cmdStr[0] == "wipe") { + resetTremola() + location.reload() + } else if (cmdStr[0] == 'publ:post') { + var draft = atob(cmdStr[2]) + cmdStr.splice(0, 2) + console.log("CMD STRING", cmdStr) + var e = { + 'header': { + 'tst': Date.now(), + 'ref': Math.floor(1000000 * Math.random()), + 'fid': myId + }, + 'confid': {}, + 'public': ["TAV", atob(cmdStr[0]), null, Date.now()].concat(args) + } + b2f_new_event(e) + } else if (cmdStr[0] == 'kanban') { + console.log("backend:", cmdStr); + var prev = cmdStr[2] //== "null" ? null : cmdStr[2] + if (prev != "null") { + prev = atob(cmdStr[2]) + prev = prev.split(",").map(atob) + } + var args = cmdStr[4] + if (args != "null") { + args = atob(cmdStr[4]) + args = args.split(",").map(atob) + } + /* + var data = { + 'bid': cmdStr[1], + 'prev': prev, + 'op': cmdStr[3], + 'args': args + } + */ + var e = { + 'header': { + 'tst': Date.now(), + 'ref': Math.floor(1000000 * Math.random()), + 'fid': myId + }, + 'confid': {}, + 'public': ["KAN", cmdStr[1], prev, cmdStr[3]].concat(args) + } + console.log('KAN e=', JSON.stringify(e)) + b2f_new_in_order_event(e) + // console.log(e) + } else { + console.log('fake backend, not implemented:', JSON.stringify(cmdStr)) + } +} + +function resetTremola() { // wipes browser-side content + tremola = { + "chats": {}, + "contacts": {}, + "profile": {}, + "id": myId, + "settings": get_default_settings(), + "board": {} + } + var n = recps2nm([myId]) + + //TODO reactivate when encrypted chats are implemented + /* + tremola.chats[n] = { + "alias": "local notes (for my eyes only)", "posts": {}, "forgotten": false, + "members": [myId], "touched": Date.now(), "lastRead": 0, + "timeline": new Timeline() + }; + */ + + tremola.chats["ALL"] = { + "alias": "Public channel", "posts": {}, + "members": ["ALL"], "touched": Date.now(), "lastRead": 0, + "timeline": new Timeline() + }; + tremola.contacts[myId] = {"alias": "me", "initial": "M", "color": "#bd7578", "iam": "", "forgotten": false}; + persist(); +} + +function persist() { + // console.log(tremola); + window.localStorage.setItem("tremola", JSON.stringify(tremola)); +} + +/* +function b2f_local_peer(p, status) { // wireless peer: online, offline, connected, disconnected + console.log("local peer", p, status); + if (!(p in localPeers)) + localPeers[p] = [false, false] + if (status == 'online') localPeers[p][0] = true + if (status == 'offline') localPeers[p][0] = false + if (status == 'connected') localPeers[p][1] = true + if (status == 'disconnected') localPeers[p][1] = false + if (!localPeers[p][0] && !localPeers[p][1]) + delete localPeers[p] + load_peer_list() +} +*/ + + +// type: 'udp' or 'ble' +// identifier: unique identifier of the peer +// displayname +// status: 'connected', 'disconnected' + +function b2f_ble_enabled() { + //ble_status = "enabled" + //TODO update ui +} + +function b2f_ble_disabled() { + + for(var p in localPeers) { + if(localPeers[p].type == "ble") { + delete localPeers[p] + refresh_connection_entry(p) + } + } + //ble_status = "disabled" +} + +/* +var want = {} // all received want vectors, id: [[want vector], timestamp], want vectors older than 90 seconds are discarded +var max_want = [] // current max vector +var old_curr = [] // own want vector at the time when the maximum want vector was last updated + +function b2f_want_update(identifier, wantVector) { + + console.log("b2f received want:", wantVector, "from: ", identifier) + + // remove old want vectors + var deleted = false; + for (var id in want) { + var ts = want[id][1] + if(Date.now() - ts > 90000) { + console.log("removed want of", id) + delete want[id] + deleted = true + } + + } + + // if the want vector didn't change, no further updates are required + if(identifier in want) { + if( equalArrays(want[identifier][0], wantVector)) { + console.log("update only") + want[identifier][1] = Date.now() + if(!deleted) //if a want vector was previously removed, the max_want needs to be recalculated otherwise it is just an update without an effect + return + } + } + + want[identifier] = [wantVector, Date.now()] + + // calculate new max want vector + var all_vectors = Object.values(want).map(val => val[0]) + var new_max_want = all_vectors.reduce((accumulator, curr) => accumulator.len >= curr.len ? accumulator : curr) //return want vector with most entries + + for (var vec of all_vectors) { + for(var i in vec) { + if (vec[i] > new_max_want[i]) + new_max_want[i] = vec[i] + } + } + + // update + if (!equalArrays(max_want,new_max_want)) { + old_curr = want['me'][0] + max_want = new_max_want + console.log("new max") + } + + refresh_connection_progressbar() + + console.log("max:", max_want) +} +*/ + +function b2f_local_peer_remaining_updates(identifier, remaining) { + //TODO +} + +function b2f_update_progress(min_entries, old_min_entries, old_want_entries, curr_want_entries, max_entries) { + refresh_connection_progressbar(min_entries, old_min_entries, old_want_entries, curr_want_entries, max_entries) +} + +function b2f_local_peer(type, identifier, displayname, status) { + console.log("incoming displayname:", displayname) + if (displayname == "null") { + displayname = identifier + } + + localPeers[identifier] = { + 'type' : type, + 'name': displayname, + 'status': status, + 'alias': null, + 'remaining': null + } + + + if (tremola != null) // can be the case during the first initialisation + for (var c in tremola["contacts"]) { + if (id2b32(c) == displayname) { + console.log("FOUND ALIAS") + localPeers[identifier].alias = tremola.contacts[c].alias + } + } + + + console.log("local_peer:", type, identifier, displayname, status) + + if (status == "offline") { + delete localPeers[identifier] + //refresh_connection_progressbar() + } + + + if (document.getElementById('connection-overlay').style.display != 'none') + refresh_connection_entry(identifier) +} + +/** + * This function is called, when the backend received a new log entry and successfully completed the corresponding sidechain. + * The backend assures, that the log entries are sent to the frontend in the exact same sequential order as in the append-only log. + * + * @param {Object} e Object containing all information of the log_entry. + * @param {Object} e.hdr Contains basic information about the log entry. + * @param {number} e.hdr.tst Timestamp at which the message was created. (Number of milliseconds elapsed since midnight at the beginning of January 1970 00:00 UTC) + * @param {string} e.hdr.ref The message ID of this log entry. + * @param {string} e.hdr.fid The public key of the author encoded in base64. + * @param {[]} e.public The payload of the message. The first entry is a String that represents the application to which the message belongs. All additional entries are application-specific parameters. + * + */ +function b2f_new_in_order_event(e) { + + console.log("b2f inorder event:", JSON.stringify(e.public)) + + if (!(e.header.fid in tremola.contacts)) { + var a = id2b32(e.header.fid); + tremola.contacts[e.header.fid] = { + "alias": a, "initial": a.substring(0, 1).toUpperCase(), + "color": colors[Math.floor(colors.length * Math.random())], + "iam": "", "forgotten": false + } + load_contact_list() + } + + switch (e.public[0]) { + case "KAN": + console.log("New kanban event") + kanban_new_event(e) + break + default: + return + } + persist(); + must_redraw = true; +} + +/** + * This function is invoked whenever the backend receives a new log entry, regardless of whether the associated sidechain is fully loaded or not. + * + * @param {Object} e Object containing all information of the log_entry. + * @param {Object} e.hdr Contains basic information about the log entry. + * @param {number} e.hdr.tst Timestamp at which the message was created. (Number of milliseconds elapsed since midnight at the beginning of January 1970 00:00 UTC) + * @param {string} e.hdr.ref The message ID of this log entry. + * @param {string} e.hdr.fid The public key of the author encoded in base64. + * @param {[]} e.public The payload of the logentry, without the content of the sidechain + * + */ +function b2f_new_incomplete_event(e) { + + if (!(e.header.fid in tremola.contacts)) { + var a = id2b32(e.header.fid); + tremola.contacts[e.header.fid] = { + "alias": a, "initial": a.substring(0, 1).toUpperCase(), + "color": colors[Math.floor(colors.length * Math.random())], + "iam": "", "forgotten": false + } + load_contact_list() + } + + switch (e.public[0]) { + default: + return + } + persist(); + must_redraw = true; + + +} + +/** + * This function is called, when the backend received a new log entry and successfully completed the corresponding sidechain. + * This callback does not ensure any specific order; the log entries are forwarded in the order they are received. + * + * @param {Object} e Object containing all information of the log_entry. + * @param {Object} e.hdr Contains basic information about the log entry. + * @param {number} e.hdr.tst Timestamp at which the message was created. (Number of milliseconds elapsed since midnight at the beginning of January 1970 00:00 UTC) + * @param {string} e.hdr.ref The message ID of this log entry. + * @param {string} e.hdr.fid The public key of the author encoded in base64. + * @param {[]} e.public The payload of the message. The first entry is a String that represents the application to which the message belongs. All additional entries are application-specific parameters. + * + */ +function b2f_new_event(e) { // incoming SSB log event: we get map with three entries + // console.log('hdr', JSON.stringify(e.header)) + console.log('pub', JSON.stringify(e.public)) + // console.log('cfd', JSON.stringify(e.confid)) + console.log("New frontend event, hdr=" + JSON.stringify(e.header)) + + //add + if (!(e.header.fid in tremola.contacts)) { + var a = id2b32(e.header.fid); + tremola.contacts[e.header.fid] = { + "alias": a, "initial": a.substring(0, 1).toUpperCase(), + "color": colors[Math.floor(colors.length * Math.random())], + "iam": "", "forgotten": false + } + load_contact_list() + } + + if (e.public) { + if (e.public[0] == 'TAV') { // text and voice + console.log("new post 0 ", tremola) + var conv_name = "ALL"; + if (!(conv_name in tremola.chats)) { // create new conversation if needed + console.log("xx") + tremola.chats[conv_name] = { + "alias": "Public channel X", "posts": {}, + "members": ["ALL"], "touched": Date.now(), "lastRead": 0, + "timeline": new Timeline() + }; + load_chat_list() + } + console.log("new post 1") + var ch = tremola.chats[conv_name]; + if (ch.timeline == null) + ch["timeline"] = new Timeline(); + console.log("new post 1 ", ch) + if (!(e.header.ref in ch.posts)) { // new post + var a = e.public; + // var d = new Date(e.header.tst); + // d = d.toDateString() + ' ' + d.toTimeString().substring(0,5); + // var txt = null; + // if (a[1] != null) + // txt = a[1]; + var p = { + "key": e.header.ref, "from": e.header.fid, "body": a[1], + "voice": a[2], "when": a[3] * 1000 + }; + console.log("new post 2 ", p) + console.log("time: ", a[3]) + ch["posts"][e.header.ref] = p; + if (ch["touched"] < e.header.tst) + ch["touched"] = e.header.tst + if (curr_scenario == "posts" && curr_chat == conv_name) { + load_chat(conv_name); // reload all messages (not very efficient ...) + ch["lastRead"] = Date.now(); + } + set_chats_badge(conv_name) + } else { + console.log("known already?") + } + // if (curr_scenario == "chats") // the updated conversation could bubble up + load_chat_list(); + } else if (e.public[0] == "KAN") { // Kanban board event + b2f_new_in_order_event(e) + } else if (e.public[0] == "IAM") { + var contact = tremola.contacts[e.header.fid] + var old_iam = contact.iam + var old_alias = contact.alias + + contact.iam = e.public[1] + + if ((contact.alias == id2b32(e.header.fid) || contact.alias == old_iam)) { + contact.alias = e.public[1] == "" ? id2b32(e.header.fid) : e.public[1] + contact.initial = contact.alias.substring(0, 1).toUpperCase() + load_contact_list() + load_board_list() + + // update names in connected devices menu + for (var l in localPeers) { + if (localPeers[l].alias == old_alias) { + + localPeers[l].alias = contact.alias + refresh_connection_entry(l) + } + } + } + + } + persist(); + must_redraw = true; + } +} + +function b2f_new_contact(fid) { + if ((fid in tremola.contacts)) // do not overwrite existing entry + return + var id = id2b32(fid); + tremola.contacts[fid] = { + "alias": id, "initial": id.substring(0, 1).toUpperCase(), + "color": colors[Math.floor(colors.length * Math.random())], + "iam": "", "forgotten": false + }; + persist() + load_contact_list(); +} + +function b2f_new_voice(voice_b64) { + new_voice_post(voice_b64) +} + +function b2f_showSecret(json) { + //setScenario(prev_scenario); + generateQR(json) +} + +function b2f_new_image_blob(ref) { + console.log("new image: ", ref); + curr_img_candidate = ref; + ref = ref.replace(new RegExp('/'), "_"); + ref = "http://appassets.androidplatform.net/blobs/" + ref; + ref = "" + document.getElementById('image-preview').innerHTML = ref + document.getElementById('image-caption').value = ''; + var s = document.getElementById('image-overlay').style; + s.display = 'initial'; + s.height = '80%'; // 0.8 * docHeight; + document.getElementById('overlay-bg').style.display = 'initial'; + overlayIsActive = true; +} + +// >> BEGIN INSERT VIRTBACKEND +function b2f_reset(id) { + console.log('reset'); + myId = id; + resetTremola(); +} +// << END INSERT VIRTBACKEND + +function b2f_initialize(id) { + myId = id + if (window.localStorage.tremola) { + tremola = JSON.parse(window.localStorage.getItem('tremola')); + + if (tremola != null && id != tremola.id) // check for clash of IDs, erase old state if new + tremola = null; + } else + tremola = null; + if (tremola == null) { + resetTremola(); + console.log("reset tremola") + } + if (typeof Android == 'undefined') + console.log("loaded ", JSON.stringify(tremola)) + if (!('settings' in tremola)) + tremola.settings = {} + var nm, ref; + for (nm in tremola.settings) + setSetting(nm, tremola.settings[nm]) + load_chat_list() + load_contact_list() + load_board_list() + + closeOverlay(); + setScenario('chats'); + // load_chat("ALL"); +} + +// >> BEGIN INSERT VIRTBACKEND +var virtBackEnd = null; +var cnt = 0; + +window.addEventListener('message', function(event) { + if (virtBackEnd == null) + virtBackEnd = event.source; + if (event.data[0] != 'b2f') // event.data[0] == 'start' || + return false; + console.log('tremola: event data ', event.data); + + let cmd = event.data[1]; + let dat = event.data[2]; + + if (cmd == 'reset') + b2f_reset(dat); + if (cmd == 'initialize') + b2f_initialize(dat); + if (cmd == 'exportSecret') + b2f_showSecret(dat); + if (cmd == 'new_event') + b2f_new_event(dat); + if (cmd == 'new_contact') + b2f_new_contact(dat); + return false; +}, false); + +// << END INSERT VIRTBACKEND + +// --- eof diff --git a/js/tinySSB-for-Chrome/tremola_settings.js b/js/tinySSB-for-Chrome/tremola_settings.js new file mode 100644 index 0000000..91fc78f --- /dev/null +++ b/js/tinySSB-for-Chrome/tremola_settings.js @@ -0,0 +1,125 @@ +// tremola_settings.js + +"use strict"; + +function get_default_settings() { + return { + 'enable_preview': false, + 'background_map': true, + 'websocket': true, + 'show_shortnames': true, + 'hide_forgotten_conv': true, + 'hide_forgotten_contacts': true, + 'udp_multicast': true, + 'ble': true, + 'websocket_url': "ws://meet.dmi.unibas.ch:8989" + } +} + +function toggle_changed(e) { + // console.log("toggle ", e.id); + tremola.settings[e.id] = e.checked; + backend("settings:set " + e.id + " " + e.checked) + persist() + applySetting(e.id, e.checked); +} + +function getSetting(nm) { + return document.getElementById(nm).checked +} + +function applySetting(nm, val) { + if (nm == 'background_map') { + if (val) + document.body.style.backgroundImage = "url('img/splash-as-background.jpg')"; + else + document.body.style.backgroundImage = null; + } else if (nm == 'hide_forgotten_conv') { + load_chat_list(); + } else if (nm == 'hide_forgotten_contacts') { + load_contact_list(); + } else if (nm == 'websocket') { + if (val) + document.getElementById("container:settings_ws_url").style.display = 'flex' + else + document.getElementById("container:settings_ws_url").style.display = 'none' + } +} + +function setSetting(nm, val) { + // console.log("setting", nm, val) + if (nm == "websocket_url") { + document.getElementById("settings_urlInput").value = val + return + } + applySetting(nm, val); + document.getElementById(nm).checked = val; +} + +/* async */ +function settings_wipe() { + closeOverlay(); + backend("wipe"); // will not return + /* + window.localStorage.setItem("tremola", "null"); + backend("ready"); // will call initialize() + await new Promise(resolve => setTimeout(resolve, 500)); + // resetTremola(); + menu_redraw(); + setScenario('chats'); + */ +} + +function btn_setWebsocketUrl() { + var new_url = document.getElementById("settings_urlInput").value + + if(!(new_url.startsWith("ws://") || new_url.startsWith("wss://"))) { + launch_snackbar("Invalid Websocket Url") + document.getElementById("settings_urlInput").classList.add("invalid") + return + } + + document.getElementById("settings_urlInput").classList.remove("invalid") + + document.getElementById("settings_urlInput").classList.add("valid") + setTimeout(function() { + document.getElementById("settings_urlInput").classList.remove("valid"); + }, 700); + document.getElementById("settings_urlInput").blur(); + backend("settings:set websocket_url " + new_url) + tremola.settings["websocket_url"] = new_url + persist() + launch_snackbar("New Websocket Url saved") +} + +function enter_setWebsocketUrl(ev) { + console.log(ev.target) + if (ev.key == "Enter") { + btn_setWebsocketUrl() + } +} + +function settings_restream_posts() { + // closeOverlay(); + setScenario('chats') + launch_snackbar("DB restreaming launched"); + backend("restream"); +} + +function settings_reset_ui() { + closeOverlay(); + resetTremola(); + setScenario('chats'); + menu_redraw(); + launch_snackbar("reloading DB"); + backend("reset"); +} + +function settings_clear_other_feeds() { + backend("wipe:others") + closeOverlay() + settings_reset_ui() + +} + +// eof diff --git a/js/tinySSB-for-Chrome/tremola_ui.js b/js/tinySSB-for-Chrome/tremola_ui.js new file mode 100644 index 0000000..e8d2193 --- /dev/null +++ b/js/tinySSB-for-Chrome/tremola_ui.js @@ -0,0 +1,569 @@ +// tremola_ui.js + +"use strict"; + +var overlayIsActive = false; + +var display_or_not = [ + 'div:qr', 'div:back', + 'core', 'lst:chats', 'div:posts', 'lst:contacts', 'lst:members', 'the:connex', + 'lst:kanban', 'div:footer', 'div:textarea', 'div:confirm-members', 'plus', + 'div:settings', 'div:board' +]; + +var prev_scenario = 'chats'; +var curr_scenario = 'chats'; + +var scenarioDisplay = { + 'chats': ['div:qr', 'core', 'lst:chats', 'div:footer'], // 'plus' TODO reactivate when encrypted chats are implemented + 'contacts': ['div:qr', 'core', 'lst:contacts', 'div:footer', 'plus'], + 'posts': ['div:back', 'core', 'div:posts', 'div:textarea'], + 'connex': ['div:qr', 'core', 'the:connex', 'div:footer', 'plus'], + 'members': ['div:back', 'core', 'lst:members', 'div:confirm-members'], + 'settings': ['div:back', 'div:settings', 'core'], + 'kanban': ['div:qr', 'core', 'lst:kanban', 'div:footer', 'plus'], + 'board': ['div:back', 'core', 'div:board'] +} + +var scenarioMenu = { + 'chats': [['Connected Devices', 'menu_connection'], // '['New conversation', 'menu_new_conversation'],' TODO reactivate when encrypted chats are implemented + ['Settings', 'menu_settings'], + ['About', 'menu_about']], + 'contacts': [['New contact', 'menu_new_contact'], + ['Connected Devices', 'menu_connection'], + ['Settings', 'menu_settings'], + ['About', 'menu_about']], + 'connex': [['New SSB pub', 'menu_new_pub'], + ['Redeem invite code', 'menu_invite'], + ['Connected Devices', 'menu_connection'], + // ['Force sync', 'menu_sync'], + ['Settings', 'menu_settings'], + ['About', 'menu_about']], + /* + ['Redraw', 'menu_redraw'], + ['Sync', 'menu_sync'], + ['Redraw', 'menu_redraw'], + ['Restream', 'menu_stream_all_posts'], + ['Import ID', 'menu_import_id'], + ['Process msgs', 'menu_process_msgs'], + ['Add pub', 'menu_add_pub'], + ['Dump', 'menu_dump'], + ['Reset', 'menu_reset']] + */ + 'posts': [/* ['Take picture', 'menu_take_picture'], + ['Pick image', 'menu_pick_image'], */ + ['Rename this chat', 'menu_edit_convname'], + ['(un)Forget', 'menu_forget_conv'], + ['Settings', 'menu_settings'], + ['About', 'menu_about']], + 'members': [['Settings', 'menu_settings'], + ['About', 'menu_about']], + + 'settings': [], + + 'kanban': [['New Kanban board', 'menu_new_board'], + ['Invitations', 'menu_board_invitations'], + ['Connected Devices', 'menu_connection'], + ['Settings', 'menu_settings'], + ['About', 'menu_about']], + + 'board': [['Add list', 'menu_new_column'], + ['Rename Kanban Board', 'menu_rename_board'], + ['Invite Users', 'menu_invite'], + ['History', 'menu_history'], + ['Reload', 'reload_curr_board'], + ['Leave', 'leave_curr_board'], + ['(un)Forget', 'board_toggle_forget'], + ['Debug', 'ui_debug']] +} + +const QR_SCAN_TARGET = { + ADD_CONTACT: 0, + IMPORT_ID: 1 +} + +var curr_qr_scan_target = QR_SCAN_TARGET.ADD_CONTACT + +function onBackPressed() { + if (overlayIsActive) { + closeOverlay(); + return; + } + if (['chats', 'contacts', 'connex', 'board'].indexOf(curr_scenario) >= 0) { + if (curr_scenario == 'chats') + backend("onBackPressed"); + else if (curr_scenario == 'board') + setScenario('kanban') + else + setScenario('chats') + } else { + if (curr_scenario == 'settings') { + document.getElementById('div:settings').style.display = 'none'; + document.getElementById('core').style.display = null; + document.getElementById('div:footer').style.display = null; + } + setScenario(prev_scenario); + } +} + +function setScenario(s) { + // console.log('setScenario ' + s) + closeOverlay(); + var lst = scenarioDisplay[s]; + if (lst) { + // if (s != 'posts' && curr_scenario != "members" && curr_scenario != 'posts') { + if (['chats', 'contacts', 'connex', 'kanban'].indexOf(curr_scenario) >= 0) { + var cl = document.getElementById('btn:' + curr_scenario).classList; + cl.toggle('active', false); + cl.toggle('passive', true); + } + // console.log(' l: ' + lst) + display_or_not.forEach(function (d) { + // console.log(' l+' + d); + if (lst.indexOf(d) < 0) { + document.getElementById(d).style.display = 'none'; + } else { + document.getElementById(d).style.display = null; + // console.log(' l=' + d); + } + }) + // console.log('s: ' + s) + if (s != "board") { + document.getElementById('tremolaTitle').style.position = null; + } + + if (s == "posts" || s == "settings" || s == "board") { + document.getElementById('tremolaTitle').style.display = 'none'; + document.getElementById('conversationTitle').style.display = null; + // document.getElementById('plus').style.display = 'none'; + } else { + document.getElementById('tremolaTitle').style.display = null; + // if (s == "connex") { /* document.getElementById('plus').style.display = 'none'; */} + // else { /* document.getElementById('plus').style.display = null; */} + document.getElementById('conversationTitle').style.display = 'none'; + } + if (lst.indexOf('div:qr') >= 0) { + prev_scenario = s; + } + curr_scenario = s; + if (['chats', 'contacts', 'connex', 'kanban'].indexOf(curr_scenario) >= 0) { + var cl = document.getElementById('btn:' + curr_scenario).classList; + cl.toggle('active', true); + cl.toggle('passive', false); + } + if (s == 'board') + document.getElementById('core').style.height = 'calc(100% - 60px)'; + else + document.getElementById('core').style.height = 'calc(100% - 118px)'; + + if (s == 'kanban') { + var personalBoardAlreadyExists = false + for (var b in tremola.board) { + var board = tremola.board[b] + if (board.flags.indexOf(FLAG.PERSONAL) >= 0 && board.members.length == 1 && board.members[0] == myId) { + personalBoardAlreadyExists = true + break + } + } + if(!personalBoardAlreadyExists && display_create_personal_board) { + menu_create_personal_board() + } + } + + } +} + +function btnBridge(e) { + var e = e.id, m = ''; + if (['btn:chats', 'btn:posts', 'btn:contacts', 'btn:connex', 'btn:kanban'].indexOf(e) >= 0) { + setScenario(e.substring(4)); + } + if (e == 'btn:menu') { + if (scenarioMenu[curr_scenario].length == 0) + return; + document.getElementById("menu").style.display = 'initial'; + document.getElementById("overlay-trans").style.display = 'initial'; + scenarioMenu[curr_scenario].forEach(function (e) { + m += "
"; + }) + m = m.substring(0, m.length - 4); + // console.log(curr_scenario + ' menu! ' + m); + document.getElementById("menu").innerHTML = m; + return; + } + if (e == 'btn:attach') { + if (scenarioMenu[curr_scenario].length == 0) + return; + backend('get:voice'); // + btoa(document.getElementById('draft').value)); + return; + } + + // if (typeof Android != "undefined") { Android.onFrontendRequest(e); } +} + +function menu_settings() { + closeOverlay(); + setScenario('settings') + document.getElementById("settings_urlInput").classList.remove("invalid") + document.getElementById("settings_urlInput").value = tremola.settings["websocket_url"] + if (tremola.settings["websocket"]) + document.getElementById("container:settings_ws_url").style.display = 'flex' + /* + prev_scenario = curr_scenario; + curr_scenario = 'settings'; + document.getElementById('core').style.display = 'none'; + document.getElementById('div:footer').style.display = 'none'; + document.getElementById('div:settings').style.display = null; + + document.getElementById("tremolaTitle").style.display = 'none'; + */ + var c = document.getElementById("conversationTitle"); + c.style.display = null; + c.innerHTML = "
Settings
"; +} + +function closeOverlay() { + document.getElementById('menu').style.display = 'none'; + document.getElementById('qr-overlay').style.display = 'none'; + document.getElementById('preview-overlay').style.display = 'none'; + document.getElementById('image-overlay').style.display = 'none'; + document.getElementById('new_chat-overlay').style.display = 'none'; + document.getElementById('new_contact-overlay').style.display = 'none'; + document.getElementById('confirm_contact-overlay').style.display = 'none'; + document.getElementById('overlay-bg').style.display = 'none'; + document.getElementById('overlay-trans').style.display = 'none'; + document.getElementById('overlay-bg-core').style.display = 'none'; + document.getElementById('overlay-trans-core').style.display = 'none'; + document.getElementById('about-overlay').style.display = 'none'; + document.getElementById('edit-overlay').style.display = 'none'; + document.getElementById('new_contact-overlay').style.display = 'none'; + document.getElementById('old_contact-overlay').style.display = 'none'; + document.getElementById('attach-menu').style.display = 'none'; + document.getElementById('div:modal_img').style.display = 'none'; + document.getElementById('connection-overlay').style.display = 'none'; + document.getElementById('import-id-overlay').style.display = 'none'; + + // kanban overlays + document.getElementById('div:menu_history').style.display = 'none'; + document.getElementById('div:item_menu').style.display = 'none'; + document.getElementById("kanban-invitations-overlay").style.display = 'none'; + document.getElementById('kanban-create-personal-board-overlay').style.display = 'none'; + curr_item = null + close_board_context_menu() + document.getElementById('btn:item_menu_description_save').style.display = 'none' + document.getElementById('btn:item_menu_description_cancel').style.display = 'none' + document.getElementById('div:debug').style.display = 'none' + document.getElementById("div:invite_menu").style.display = 'none' + + overlayIsActive = false; + + if (curr_img_candidate != null) { + backend('del:blob ' + curr_img_candidate); + curr_img_candidate = null; + } +} + +function showPreview() { + var draft = escapeHTML(document.getElementById('draft').value); + if (draft.length == 0) return; + if (!getSetting("enable_preview")) { + new_text_post(draft); + return; + } + var draft2 = draft.replace(/\n/g, "
\n"); + var to = recps2display(tremola.chats[curr_chat].members) + document.getElementById('preview').innerHTML = "To: " + to + "
" + draft2 + " 
"; + var s = document.getElementById('preview-overlay').style; + s.display = 'initial'; + s.height = '80%'; // 0.8 * docHeight; + document.getElementById('overlay-bg').style.display = 'initial'; + overlayIsActive = true; +} + +function menu_about() { + closeOverlay() + document.getElementById('about-overlay').style.display = 'initial'; + document.getElementById('overlay-bg').style.display = 'initial'; + overlayIsActive = true; +} + +function plus_button() { + closeOverlay(); + if (curr_scenario == 'chats') { + menu_new_conversation(); + } else if (curr_scenario == 'contacts') { + menu_new_contact(); + } else if (curr_scenario == 'connex') { + menu_new_pub(); + } else if (curr_scenario == 'kanban') { + menu_new_board(); + } +} + +function launch_snackbar(txt) { + var sb = document.getElementById("snackbar"); + sb.innerHTML = txt; + sb.className = "show"; + setTimeout(function () { + sb.className = sb.className.replace("show", ""); + }, 3000); +} + +// --- QR display and scan + +function showQR() { + generateQR('did:ssb:ed25519:' + myId.substring(1).split('.')[0]) +} + +function generateQR(s) { + document.getElementById('qr-overlay').style.display = 'initial'; + document.getElementById('overlay-bg').style.display = 'initial'; + document.getElementById('qr-text').innerHTML = s; + if (!qr) { + var w, e, arg; + w = window.getComputedStyle(document.getElementById('qr-overlay')).width; + w = parseInt(w, 10); + e = document.getElementById('qr-code'); + arg = { + height: w, + width: w, + text: s, + correctLevel: QRCode.CorrectLevel.M // L, M, Q, H + }; + qr = new QRCode(e, arg); + } else { + qr.clear(); + qr.makeCode(s); + } + overlayIsActive = true; +} + + +function qr_scan_start(target) { + // test if Android is defined ... + curr_qr_scan_target = target + backend("qrscan.init"); + closeOverlay(); +} + +function qr_scan_success(s) { + closeOverlay(); + switch (curr_qr_scan_target) { + case QR_SCAN_TARGET.ADD_CONTACT: + var t = "did:ssb:ed25519:"; + if (s.substring(0, t.length) == t) { + s = '@' + s.substring(t.length) + '.ed25519'; + } + var b = ''; + try { + b = atob(s.substr(1, s.length - 9)); + // FIXME we should also test whether it is a valid ed25519 public key ... + } catch (err) { + } + if (b.length != 32) { + launch_snackbar("unknown format or invalid identity"); + return; + } + new_contact_id = s; + // console.log("tremola:", tremola) + if (new_contact_id in tremola.contacts) { + launch_snackbar("This contact already exists"); + return; + } + // FIXME: do sanity tests + menu_edit('new_contact_alias', "Assign alias to new contact:
(only you can see this alias)", ""); + break + case QR_SCAN_TARGET.IMPORT_ID: + r = import_id(s) + if (r) { + launch_snackbar("Successfully imported, restarting...") + } else { + launch_snackbar("wrong format") + } + break + } +} + + + + +function qr_scan_failure() { + launch_snackbar("QR scan failed") +} + +function qr_scan_confirmed() { + var a = document.getElementById('alias_text').value; + var s = document.getElementById('alias_id').innerHTML; + // c = {alias: a, id: s}; + var i = (a + "?").substring(0, 1).toUpperCase() + var c = {"alias": a, "initial": i, "color": colors[Math.floor(colors.length * Math.random())], "iam": "", "forgotten": false}; + tremola.contacts[s] = c; + persist(); + backend("add:contact " + s + " " + btoa(a)) + load_contact_item([s, c]); + closeOverlay(); +} + +function modal_img(img) { + var modalImg = document.getElementById("modal_img"); + modalImg.src = img.data; + var modal = document.getElementById('div:modal_img'); + modal.style.display = "block"; + overlayIsActive = true; + let pz = new PinchZoom(modalImg, + { + onDoubleTap: function () { + closeOverlay(); + }, maxZoom: 8 + } + ); +} + +function menu_connection() { + closeOverlay(); + //refresh_connection_progressbar() + + document.getElementById('connection-overlay-content').innerHTML = ''; + + for (var peer in localPeers) { + refresh_connection_entry(peer); + } + + document.getElementById('overlay-bg').style.display = 'initial'; + document.getElementById('connection-overlay').style.display = 'initial'; + overlayIsActive = true; +} + +function refresh_connection_entry(id) { + var content = document.getElementById('connection-overlay-content') + + // only update existing entry + if (document.getElementById('connection_' + id)) { + if(id in localPeers) { + var name = localPeers[id].alias != null ? localPeers[id].alias : localPeers[id].name + if (name.length > 28) + name = name.slice(0,27) + document.getElementById('connection_name_' + id).innerHTML = name + document.getElementById('connection_type_' + id).innerHTML = "via " + localPeers[id].type + document.getElementById('connection_remaining_' + id).innerHTML = localPeers[id].remaining + } else { + document.getElementById('connection_' + id).outerHTML = "" + } + return + } + + if(!(id in localPeers)) + return + + // create new entry + + var peer = localPeers[id] + var name = localPeers[id].alias != null ? peer.alias : peer.name + if (name.length > 28) + name = name.slice(0,27) + var remaining = peer.remaining != null ? peer.remaining : ""//"Remaining: "+ peer.remaining + " messages" : "Remaining messages unknown" + var type = (peer.type != null) && (peer.type != "") ? peer.type : "" + + var entryHTML = "
" + entryHTML += "
" + entryHTML += "
" + name + "
" + entryHTML += "
via " + type + "
" + entryHTML += "
" + entryHTML += "
" + remaining + "
" + entryHTML += "
" + + document.getElementById('connection-overlay-content').innerHTML += entryHTML + +} + +function refresh_goset_progressbar(curr, max) { + + console.log("refresh_goset_progressbar", curr, max) + + var delta = max - curr + + document.getElementById('connection-overlay-progressbar-goset').value = (curr / max) * 100 + document.getElementById('connection-overlay-progressbar-label-goset').textContent = "GoSet - " + delta + " key" + (delta > 1 ? "s" : "") + " left" + if (delta > 0) { + console.log("display progress") + document.getElementById('goset-progress-container').style.display = "initial" + document.getElementById('progress-container').style.display = "none" + } else { + document.getElementById('goset-progress-container').style.display = "none" + document.getElementById('progress-container').style.display = "initial" + } + +} + +var max_chnks = 0 +function refresh_chunk_progressbar(remaining) { + + if(remaining != 0) { + max_chnks = Math.max(max_chnks, remaining) + } else { + max_chnks = 0 // reset + } + + console.log("refresh_chunk_progressbar", remaining, max_chnks) + + + if(remaining > 0) { + var percentage = (1 - ((remaining - 0) / (max_chnks - 0))) * 100 + document.getElementById('connection-overlay-progressbar-chnk').value = percentage + document.getElementById('connection-overlay-progressbar-label-chnk').textContent = remaining + " Chunks left" + } else { + document.getElementById('connection-overlay-progressbar-chnk').value = 100 + document.getElementById('connection-overlay-progressbar-label-chnk').textContent = "Chunks — Synchronized" + } + +} + +function refresh_connection_progressbar(min_entries, old_min_entries, old_want_entries, curr_want_entries, max_entries) { + + console.log("min:", min_entries) + console.log("old_min:", old_min_entries) + console.log("old_curr:", old_want_entries) + console.log("curr:", curr_want_entries) + console.log("max:", max_entries) + + if(curr_want_entries == 0) + return + + // update want progress + + if(curr_want_entries >= max_entries || old_want_entries == max_entries) { + document.getElementById('connection-overlay-progressbar-want').value = 100 + document.getElementById('connection-overlay-progressbar-label-want').textContent = "Missing — Synchronized" + } else { + var newPosReq = (curr_want_entries - old_want_entries) / (max_entries - old_want_entries) * 100 + + console.log("newPosMax:", newPosReq) + + document.getElementById('connection-overlay-progressbar-want').value = newPosReq + document.getElementById('connection-overlay-progressbar-label-want').textContent = "Missing - " + (max_entries - curr_want_entries) + " entries left" + + } + + // update gift progress + if (curr_want_entries <= min_entries || old_min_entries == curr_want_entries) { + document.getElementById('connection-overlay-progressbar-gift').value = 100 + document.getElementById('connection-overlay-progressbar-label-gift').textContent = "Ahead — Synchronized" + } else { + var newPosOff = (min_entries - old_min_entries) / (curr_want_entries - old_min_entries) * 100 + + document.getElementById('connection-overlay-progressbar-gift').value = newPosOff + document.getElementById('connection-overlay-progressbar-label-gift').textContent = "Ahead - " + (curr_want_entries - min_entries) + " entries left" + } +} + +function chat_open_attachments_menu() { + // >> DISABLED FOR VIRTBACKEND + /* + closeOverlay() + document.getElementById('overlay-bg').style.display = 'initial' + document.getElementById('attach-menu').style.display = 'initial' + */ +} + +// --- diff --git a/js/tinySSB-for-Chrome/user_alice.html b/js/tinySSB-for-Chrome/user_alice.html new file mode 100644 index 0000000..f5ebed8 --- /dev/null +++ b/js/tinySSB-for-Chrome/user_alice.html @@ -0,0 +1,12 @@ + + + + + + + +
+ + diff --git a/js/tinySSB-for-Chrome/user_bob.html b/js/tinySSB-for-Chrome/user_bob.html new file mode 100644 index 0000000..5b4e99d --- /dev/null +++ b/js/tinySSB-for-Chrome/user_bob.html @@ -0,0 +1,12 @@ + + + + + + + +
+ + diff --git a/js/tinySSB-for-Chrome/user_carol.html b/js/tinySSB-for-Chrome/user_carol.html new file mode 100644 index 0000000..2f4b7b1 --- /dev/null +++ b/js/tinySSB-for-Chrome/user_carol.html @@ -0,0 +1,12 @@ + + + + + + + +
+ + diff --git a/js/tinySSB-for-Chrome/virtual-backend.js b/js/tinySSB-for-Chrome/virtual-backend.js new file mode 100644 index 0000000..529e763 --- /dev/null +++ b/js/tinySSB-for-Chrome/virtual-backend.js @@ -0,0 +1,271 @@ + +// virtual-backend.js +// 2022-04-28 christian.tschudin@unibas.ch + +'use strict' + +var ether = new BroadcastChannel('log-exchange') +var myname, myseqno, vectorClock, inqueue, outqueue, offline + +var IDs = { + 'Alice': '@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=.ed25519', + 'Bob' : '@CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=.ed25519', + 'Carol': '@ECCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=.ed25519' +}; +var frontEnd = null; + +function setup(name) { + myname = name + myseqno = 0 + + vectorClock = {} + inqueue = [] + outqueue = [] + offline = false + + var hdr = document.createElement("h2") + hdr.innerHTML= "  " + + "This is " + name + + ", status= " + + "(
0
" + + "/
0
queued)" + + var tre = document.createElement("div") + tre.innerHTML = '
\n' + + '
\n' + + var vec = document.createElement("p") + vec.id = 'vec' + + /* + var inp = document.createElement("input") + inp.id = "inp" + inp.type = "text" + inp.placeholder = "value to broadcast" + inp.addEventListener("keyup", evt => { if (evt.key == 'Enter') sendit() }) + + var btn = document.createElement('button') + btn.onclick = sendit + btn.innerHTML = "SEND!" + */ + + var lst = document.createElement("ul") + lst.id = 'lst' + + document.body.innerHTML = null + document.body.style = "font-family: monospace;"; + [hdr, tre, vec, /* inp, btn, */ lst].forEach( e => { + document.body.append(e) + } ) + + document.getElementById('rst').onclick = evt => { + ether.postMessage(null) // we defined this to restart all apps + reset(); + } + document.getElementById('lbl').onclick = evt => { toggle() } + + for (var nm in IDs) + vectorClock[IDs[nm]] = 0; + ether.onmessage = msg => { + if (msg.data == null) reset(myname); + else if (!offline) incoming(msg.data); + else { + inqueue.push(msg.data) + document.getElementById('icnt').innerHTML = inqueue.length + } + } + + console.log('started as:', name) +} + +function reset(name) { + document.getElementById('vec').innerHTML = ''; + for (var nm in IDs) + vectorClock[IDs[nm]] = 0; + myseqno = 0; + inqueue = [] + outqueue = [] + offline = false + + document.getElementById('lst').innerHTML = ''; + frontEnd.postMessage(['b2f', 'reset', IDs[myname]], '*'); + frontEnd.postMessage(['b2f', 'initialize', IDs[myname]], '*'); + add_peers(); +} + +/* +function sendit() { + myseqno += 1 + var txt = document.getElementById("inp") + var data = {fid: IDs[myname], seq: myseqno, val: txt.value} + if (offline) { + outqueue.push(data) + document.getElementById('ocnt').innerHTML = outqueue.length + } else { + ether.postMessage(data) + ether.onmessage({'data':data}) // send also to ourself + } + txt.value = null +} +*/ + +function broadcast(e) { + if (offline) { + outqueue.push(e) + document.getElementById('ocnt').innerHTML = outqueue.length + } else { + ether.postMessage(e) + ether.onmessage({'data':e}) // send also to ourself + } +} + +function incoming(data) { + console.log("incoming", data); + var hdr = data.header; + if (vectorClock[hdr.fid] >= hdr.seq) + alert(myname + ", did you forget to restart your client?"); + else if (hdr.seq != (vectorClock[hdr.fid] + 1)) + alert(myname + ", you should have started at the same time as the others!"); + vectorClock[hdr.fid] = hdr.seq; + var v = {}; + var sender = null; + for (var nm in IDs) { + v[nm] = vectorClock[IDs[nm]]; + if (IDs[nm] == hdr.fid) + sender = nm; + } + // console.log('v', v); + document.getElementById('vec').innerHTML = + "vectorClock = " + JSON.stringify(v) + var li = document.createElement('li') + li.innerHTML = (new Date()).toLocaleTimeString().split(' ')[0] + ' ' + + JSON.stringify({'from':sender, 'seq': hdr.seq, 'app': data.public[0]}); + document.getElementById('lst').prepend(li) + + frontEnd.postMessage(['b2f', 'new_event', data], '*') +} + +function toggle() { + var lbl = document.getElementById('lbl') + lbl.innerHTML = offline ? 'ONLINE' : 'OFFLINE' + lbl.style = 'background-color: ' + (offline ? 'green;' : 'red;') + + 'border: 2mm outset;padding: 3px;' + offline = offline == false + if (!offline) { + while (inqueue.length > 0) incoming(inqueue.shift()) + while (outqueue.length > 0) { + var data = outqueue.shift() + ether.postMessage(data) + ether.onmessage({'data':data}) // send also to ourself + } + document.getElementById('icnt').innerHTML = 0 + document.getElementById('ocnt').innerHTML = 0 + } +} + +// ---------------------------------------------------------------------- + + +function we_can_start_now() { + window.addEventListener('message', virtualBackend, false); + frontEnd = window.frames['tre'].contentWindow; + frontEnd.postMessage(['b2f', 'initialize', IDs[myname]], '*'); + add_peers(); +} + +function add_peers() { + for (var nm in IDs) { + if (nm != myname) + frontEnd.postMessage(['b2f', 'new_contact', IDs[nm]], '*'); + // [IDs[nm], {'alias': nm}] ], '*'); + } +} + +function virtualBackend(event) { + console.log('virtBE1: ', event.data); + if (event.data[0] != 'f2b') + return; + let cmd = event.data[1].split(' '); + console.log('virtBE2: ', cmd); + + if (cmd[0] == 'wipe') { + frontEnd.postMessage(['b2f', 'reset', IDs[myname]], '*'); + add_peers(); + } + if (cmd[0] == 'exportSecret') + event.source.postMessage(['b2f', 'exportSecret', + 'secret_of_id_which_is@AAAA==.ed25519'], '*'); + if (cmd[0] == 'publ:post') { + console.log('cmd1 =', cmd) + var draft = atob(cmd[2]) + // cmd.splice(0,2) + myseqno += 1; + var e = { 'header': { + 'tst': Date.now(), + 'ref': Math.floor(1000000*Math.random()), + 'fid': IDs[myname], + 'seq' : myseqno}, + 'public': ['TAV', draft, null, Math.floor(Date.now() / 1000)] + } + frontEnd.postMessage(['b2f', 'new_event', e], '*') + broadcast(e); + } + if (cmd[0] == 'priv:post') { + var draft = atob(cmd[1]) + cmd.splice(0,2) + myseqno += 1; + var e = { 'header': { + 'tst': Date.now(), + 'ref': Math.floor(1000000*Math.random()), + 'fid': IDs[myname], + 'seq' : myseqno}, + 'confid': {'type': 'post', 'text': draft, 'recps': cmd }, + 'public': {} + } + frontEnd.postMessage(['b2f', 'new_event', e], '*') + broadcast(e); + } + if (cmd[0] == 'kanban') { + console.log('kanban', cmd[1], cmd[4]) + // var operation = JSON.parse(atob(cmd[1])); + // console.log(operation); + // cmd.splice(0,2) + cmd[0] = 'KAN'; + var prev = cmd[2] //== "null" ? null : cmdStr[2] + if (prev != "null") { + prev = atob(cmd[2]) + cmd[2] = prev.split(",").map(atob) + } + if (cmd[4] != "null") { + var args = atob(cmd[4]).split(',').map(atob) + cmd = cmd.splice(0,4).concat(args) + } + myseqno += 1; + var e = { 'header': { + 'tst': Date.now(), + 'ref': Math.floor(1000000*Math.random()), + 'fid': IDs[myname], + 'seq': myseqno}, + 'public': cmd + } + frontEnd.postMessage(['b2f', 'new_event', e], '*') + broadcast(e); + } + if (cmd[0] == 'iam') { + console.log('iam', atob(cmd[1])); + cmd[0] = 'IAM'; + cmd[1] = atob(cmd[1]); + myseqno += 1; + var e = { 'header': { + 'tst': Date.now(), + 'ref': Math.floor(1000000*Math.random()), + 'fid': IDs[myname], + 'seq': myseqno}, + 'public': cmd + } + frontEnd.postMessage(['b2f', 'new_event', e], '*') + broadcast(e); + } +} + +// eof