From f5d482975106efd100d55fb7eacb6ef7f9bc94df Mon Sep 17 00:00:00 2001 From: Toran Sharma Date: Mon, 18 May 2020 21:24:56 +0100 Subject: [PATCH] Add extension scripts, manifest and placeholder icons --- CHANGELOG.md | 23 + background.js | 198 +++++++++ icons/icon_128.png | Bin 0 -> 2531 bytes icons/icon_16.png | Bin 0 -> 467 bytes icons/icon_32.png | Bin 0 -> 1010 bytes icons/icon_48.png | Bin 0 -> 1824 bytes icons/icon_64.png | Bin 0 -> 1811 bytes manifest.json | 45 ++ options.html | 11 + script.js | 1017 ++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 1294 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 background.js create mode 100644 icons/icon_128.png create mode 100644 icons/icon_16.png create mode 100644 icons/icon_32.png create mode 100644 icons/icon_48.png create mode 100644 icons/icon_64.png create mode 100644 manifest.json create mode 100644 options.html create mode 100644 script.js diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c696243 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +Changelog +========= + +[Unreleased] +------------ +- + +[v0.0.1] - 2020-05-18 +--------------------- +[GitHub Release Page](https://github.com/ToranSharma/Xporcle-Extension/releases/tag/v0.0.1) +### Added +- README +- GNU GPLv3 License +- This changelog +- Manifest +- Content script. This handles the addition of the user interface and the + processing of messages from the server. +- Background script. This handles to connection to the server and relays + message sending between the content script and the server. +- Options page popup placeholder. + +[Unreleased]: https://github.com/ToranSharma/Xporcle-Extension/compare/master...develop +[v0.0.1]: https://github.com/ToranSharma/Xporcle-Extension/releases/tag/v1.0.0 diff --git a/background.js b/background.js new file mode 100644 index 0000000..ceeeeb4 --- /dev/null +++ b/background.js @@ -0,0 +1,198 @@ +// Open page tracking and options page request handling +const pages = { + openedTabs: [] +}; + +chrome.runtime.onMessage.addListener( + (message, sender, sendResponse) => + { + if (message.type == "showPageAction") + { + chrome.pageAction.show(sender.tab.id); + if (!pages.openedTabs.includes(sender.tab.id)) + pages.openedTabs.push(sender.tab.id); + } + if (message.type == "pageClosed") + { + const index = pages.openedTabs.indexOf(sender.tab.id); + if (index != -1) + pages.openedTabs.splice(index, 1); + } + if (message.type == "tabsRequest") + { + sendResponse(pages); + } + } +); + +// Web Socket Handling + +let ws = null; +let roomCode = null; +let username = null; +let host = null; +let messagePort = null; +let scores = {}; +let urls = {}; +let playing = null; + +chrome.runtime.onConnect.addListener( + (port) => + { + console.log("Page connecting"); + if (port.name === "messageRelay") + { + messagePort = port; + port.onMessage.addListener( + (message) => + { + console.log("Page sent a message"); + console.log(message); + if (message.type === "connectionStatus") + { + console.log("Page is asking about the connection status"); + // Request to see if we are still connected to a room + if (ws !== null) + { + port.postMessage( + { + type: "connectionStatus", + connected: true, + room_code: roomCode, + username: username, + host: host, + scores: scores, + urls: urls + } + ); + ws.send(JSON.stringify({type: "url_update", url: message["url"]})) + } + else + { + port.postMessage({type: "connectionStatus", connected: false}); + } + + } + else if (message.type === "startConnection") + { + startConnection(message.initialMessage); + } + else + { + // This is a message to forward on to the server + ws.send(JSON.stringify(message)); + + if (message.type === "live_scores_update") + { + playing = message["playing"] + } + } + } + ); + + // Handle port disconnect + port.onDisconnect.addListener( + () => + { + console.log("page disconnected"); + messagePort = null; + + if (playing) + { + playing = false; + // Send message to server that the player's playing state is false + ws.send(JSON.stringify({type: "page_disconnect"})); + } + } + ); + } + } +); + + +function startConnection(initialMessage) +{ + console.log("Connecting to server"); + + username = initialMessage.username; + + if (initialMessage["code"] !== undefined) + { + roomCode = initialMessage["code"]; + } + + ws = new WebSocket("wss://toransharma.com/sporcle") + + ws.onerror = (error) => + { + throw error; + }; + ws.onclose = (event) => + { + console.log("connection closed"); + reset(); + }; + ws.onopen = (event) => + { + ws.send(JSON.stringify(initialMessage)); + }; + + ws.onmessage = forwardMessage; +} + +function forwardMessage(event) +{ + const message = JSON.parse(event.data); + console.log(message); + + messageType = message["type"]; + + if (messageType === "new_room_code") + { + host = true; + roomCode = message["room_code"]; + } + else if (messageType === "join_room" && !message["success"]) + { + ws.close(); + reset(); + } + else if (messageType === "scores_update") + { + playing = false; + Object.assign(scores, message["scores"]); + } + else if (messageType === "start_quiz") + { + playing = true; + } + else if (messageType === "url_update") + { + urls[message["username"]] = message["url"]; + } + else if (messageType === "removed_from_room") + { + const removedUser = message["username"]; + if (removedUser === username) + { + ws.close(); + reset(); + } + else + { + delete urls[removedUser]; + } + } + + if (messagePort !== null) + { + messagePort.postMessage(message); + } +} + +function reset() +{ + ws = null; + username = null; + roomCode = null; +} diff --git a/icons/icon_128.png b/icons/icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..781c4ce8563b2b62f64175595dd51bc401353952 GIT binary patch literal 2531 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 literal 0 HcmV?d00001 diff --git a/icons/icon_16.png b/icons/icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..ab00abcd7930d28a864ad08bd3aafc5f1af5bd52 GIT binary patch literal 467 zcmV;^0WAKBP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0cc4?K~y+T%}}{Y z15p&cZ+0h)TM%O*_zwXa!L8FuFr}cCg&>OH2MD&>jUqz81uP_mtq{~g3sXcv#m+`i z(8@x|jOKdY%yS8tkTM4j=Pu`7-n-OTFb(P!f-)-ohNiSWV8Jc8W*F3@Y1E52E9(hY z=*XwQ>Bv|O7+8fcSP(?_GDd9$yh8i|?SWPY3?m>9r7a!=XEH9kj$qFfa%nDh{SSe` zIcPyH$ouX5dfdsuS&EChNydHTOE%DNj@$~m(g-m0LngPET4-#*A=?zBd literal 0 HcmV?d00001 diff --git a/icons/icon_32.png b/icons/icon_32.png new file mode 100644 index 0000000000000000000000000000000000000000..7ad3cf1054860c075ab853a57496bd71c977f3cc GIT binary patch literal 1010 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1BXdOK~z{r?UqYO zR8bhmzxx+8CjkZ&s5&Cm5!8Itx1<86;mD0rkwhw*_G{ZFwdPv7ijPdW#AB^z)K*BvxVgXzv zqG3#2peR%w5SM7be`3F}5=cbwM&(WDOJ*fVIxs6SU+f6_C?0lX7$s)1L@X&z_9aM7 zl3Cd?htHoaXxjCI^iWLjqm%U*92W0|W}mXoR5hGaqJ z51xD}l9%F1-oX`WZp&_73_pgT~F9xUbrkBJdKf*!?QM!mQZ zg`nNsS5=9M4#kjyP7KDnl`5S)97|biRcdVW5GrVr3gP|m8q&uDCLzBCpbdSHi*jc4 z4{G%Frm%fuFoM`M%u#_kcr`xip&5d0fxsRmW#Gnb+~yCy zcWdZlpZ7*hlbc4zc^`@f6-`ankdd` zz4lQwX=~S{u#x--u6-|@AXsjwBU(xv^DzDzW$KYYDzTU|M;(TzbEjdP#yyNa4QX`i z;kbyQYSrkaQsIOPzHkoQ*a_1-Oy)>^c|7ZDUd9gYunRruRq#`IWG4?hfkuz5E z**K!Jp~EKZ@@QV>`_sXj5V&os^pqQB*P~LJdxE_Y&ff-;--9xrbO7b$|8iuUm=FBo zAv-=I&;zYd{NE36P=2zR@K@RVTt5<#1>!9d@G4lHMm+WeRl2OZ3qX`A6&q$SVq&AE g9K&8@kuV|p1w2qW}N^07*qoM6N<$g8lTz9smFU literal 0 HcmV?d00001 diff --git a/icons/icon_48.png b/icons/icon_48.png new file mode 100644 index 0000000000000000000000000000000000000000..4db131e955eb517371ed9b707ea1b0a9d05c540c GIT binary patch literal 1824 zcmV+*2jBRKP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2DM21#FhrD}&))M|mQ_Z(IFf2KwA@%hirDHv&*mLOc%O zv1A)eolttr=arM-7L z7`Mds$#CsUL%`>gdvPF!yc2#-LY)Z=_ha-hk4I)P?g#js(ufsuyZ>8Y)MzGYL`*sn zqx~)VOSIpG*2fLdKFAs4^I31gGl20uG5BTu2F(WK??VA=bfUKvzy$r7Y%^lgfh5(i z8qd?$pA{L+xV`CPydF`*tYoUNnlI8m(tHfG3<^T6gQB2Sf}vH2%_mJ#1GEMwzDm$dJ=)nfFi! z8D41%X&^UU8Iq}EL&7S^v@s#+-92$|W4K&9GAI)+ORdv*TvkV8W{2C%8wFhlh*Ak` z5d21>bv=`IN%W`zM(&vqmWfw{q(`||W{fYAjvW&5>DA3Ew$G9tT|ADHw_Q^tkIg+M z2Y$tKz^d(MM|zf^Y;_a`AnOU54vMQABQIux5zwWmdO|6`tvJ;ui=Hl({=IyzCKX3( zjD$m$kV>-g%SIV`%VD?2K{dP6GX!N}ZM~^>b$KD56bcgDxkURpjK7t(9`Py07Z+JF z;%vVJNRO64O*qCYs@~NNECt=8F?pn}-YiF+0(En=c_5z@ielI=IB+?#rce_utn|yn zw-(Dw4|b4o3@Lgu{45T;0|c)QxfaTb zylTK}S^M}<4WhmWnr@bpM+N1MaYa%Z$!8%G2vXo9&kZ<5-=m9H8XAr4J@A(o3RYU@ zsAQeTAooT74$|`@19>?>l%g=bkm(8xbCh7q*HLG8Ijf(o1)euFmHM9Km0^QyDP`3@ z+)yu#Izt+m?`LQ!PHdv70n9)lyO5rqt0=TeR^xPJtAQ)shV6}V3dRgPC42E^gQm^r zI~tvx=*ib=NF_ZAnxMDWp9siXr0sZ&*YhT;@rnZCmW0t0xCy@;jmy?udO6J>7$$c< zb5!1VuQtVawtzShU&O9A-S4wzqWUe-(=rNjfMULm=-qvAX=4j}aIs}d z83$Z*5b-_GT@>yEtniAu2Ge5UHb=fGq_3%ov6#$(mpdA8)rA3>eOJUeZI2!^a-=3M z0oxMN%pK7(N+|p#YwCsV(*coxpC{{V(z2j)AYTXc{rd!@2Urs-PPJwGw^3RCalI^F zTPMrlg9q5swNvhe^JQ6}+1$<1W&2GUZGm#+I-qGoBvWc0;|m=>Du z;EwM6fwtNCcA1$oGh+I5_d2>MT%~s}>teRf@0{}cY^2}h#djuh-e|D~_~Hl-oCt;W zDTXd*Y4xqT0D2XwVzSq9auYc_kaj1CL(M0ntD2o|ovUtL;Mfox*^TH#P#-C5gDFu>(#hqC6-g2oz-!>yi{<}<{vU)B zS>MJ)wCQ!$0`-CZHU7a#{1-@#Vp1#HUm&#@KcDkpsoPEgsiM~s{m-avfsxZ;>5)B% zfqD&4$9%lR-h%6uxcQjRptc96?$fZOQF@Qr0ZrAuBzP+JM7I!8E7%!JTb*iq5b1d^ z$0%xHy=}`#WWV;MLGSb{A!jU3J+8{*0joK6j=`5ex&DyS{?(9PUB{`S%|RqL5&5Uc z^RBKc7Y4G;F814Up4)C)u~#Ji2MmSV^oXMsY_e{ O0000Ta2 literal 0 HcmV?d00001 diff --git a/icons/icon_64.png b/icons/icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..cee3c438b095099ff81823282583c12a54836994 GIT binary patch literal 1811 zcmV+u2kiKXP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2B=9yK~#8N?V4F^ zR8<(qzdI|#(3TwmLR11qz{CeZd=WH&FX97ndnhg#jSDe?VT%ut zXq13;17gskh-g_fLK6ZJwIrIdS4!L2j{on>najO1cjj)CcEbIo|9t11xp#WMvwUZ{ zLjeH+0RaI40bZy{Y7wRSOy9uT z4hiltp%A5z(7=B+MDMr?459Uj+Fp0P}4aQUfEI4**SWKINbU7ON8d!O~ zrcpPnqc~!8T&au82Ms*T1sL6Vqsny6TIdjDx;7ruw!oIf!f~nE5n2oEpt2D%J(N)x z<3Gddfet<6YZ%LGV-amCtch}t8#K5xP+>Z(B-P#LBR;Gw{;WvcGvbgF$5~wYgAR9T ztB3O>^pG_U9G6ltXz;;wco4?&AqPr+r%xuNtZ!6{xn{{D!zmusRL&jQ9IaK!!KZG7 zHW|j$LU2sXx+iWbGit%Cgb=@s6HDhMEkCa$%oyikht~32&E=*C-Tq#^ShcKHR)(>B z%EYMX+dWoR0NaN8km>V+%9TT_pdm-ebjYpCP9FMrv}kG!%QCiIJI2WJf?D6IH{HjM zzw=CutUzPAK~2z*p#d@A1w`*oq)iXX&K8_J|J_(Ie?~%9hSB+gc(@cCFZ2$pA%y?6 z3-+?$E&3=hx6jVy=n2>vuu)Z9!m%x*si`rda^*R8B1_kg2@%G(%%41)qcPJfRnbn&ws+1WxzZM&*6-fFH zf^K^NFL?Gt%`#^Lx-+566QVucdXs$*D27{~dw~s`pbb!)%(=w%5Exw_y5;n_+&&?w zMirGXeM($9jqz)0Q8yg^BSTXcpK($Fl2}yRmr9Edk>tv7*5!bwr{fJvDkHvCSrZ$V z*U47D9CY^x+O(~g*0=W3!6Ru^TA65s&ZaZ;5t6(^JSht+;H`P`EP+bq;@YG}dp2C- zsmoWQZGTF}gim+&sS;a`L9-z~u_}w7Q{i?5Ld;dfD?puoG}Mm0{WSN11n&}6sef*` zHjM8UwEMR-UF_r8&>B2b8q?$o1m?NO{+ll?kMAeM*xQM*SF$6+N{`w;809$z7&(CM zzwp&=+hG95zszQ7F_I_pCHDB<48i$Fxz<%yXYcQ6dUoZ1D(4N30Z+f#O_Lr!ZRh-9 zEW&2O`G=5p>^$SU0;k5_w%a2#`Ni**+tEI^kVszxM40RmO$XL+c zk(RA^*Tk5*$~m>k5beSg_~$?7Mix2h%?a81zVH&F=R>X!B6(Q!{WwY_8)6=r0b*Ay6LGR-qW zG0})NC!GA*JEiuEtnq1$JvWgUODJ+TP&q6YA%{e+=#pDG?Q@bqMAsS2mN zx^5pIEh$6n;VG97VyXwM%m0QcamJ(Wg{lh`oUEMDRs5cyf`-=14+l7TQF@eT7F0sn z*W@e&)E?NXV*+I95G#z8Eo`AxfgtfdD+5=S=aA*#Ul`}j|u7)ip9tq1IP*~lw z9vV=5p9d6^h|`TKsRPIPG-Dyw?tG3|Oh2}@_ro5@XD@*va9ky;3O002ovPDHLkV1ggj BYJ30y literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..0a2d6e1 --- /dev/null +++ b/manifest.json @@ -0,0 +1,45 @@ +{ + "name" : "Xporcle", + "description" : "Adds real-time multiplayer abilities to Sporcle.com", + "version" : "0.0.1", + "manifest_version" : 2, + + "icons" : { + "16" : "icons/icon_16.png", + "32" : "icons/icon_32.png", + "48" : "icons/icon_48.png", + "64" : "icons/icon_64.png", + "128" : "icons/icon_128.png" + }, + + "background" : { + "scripts" : ["background.js"] + }, + + "page_action" : { + "show_matches" : ["https://www.sporcle.com/*"], + "default_icon" : { + "16" : "icons/icon_16.png", + "32" : "icons/icon_32.png", + "48" : "icons/icon_48.png", + "64" : "icons/icon_64.png", + "128" : "icons/icon_128.png" + }, + "default_title" : "Xporcle", + "default_popup" : "options.html" + }, + + "content_scripts" : [ + { + "matches" : ["https://www.sporcle.com/*"], + "js" : ["script.js"] + } + ], + + "permissions" : ["storage"], + + "options_ui" : { + "page" : "options.html", + "open_in_tab" : false + } +} diff --git a/options.html b/options.html new file mode 100644 index 0000000..d2b8acf --- /dev/null +++ b/options.html @@ -0,0 +1,11 @@ + + + + + Xporcle Options + + +

Xporcle v0.0.1

+

Enable or Disable Features Here

+ + diff --git a/script.js b/script.js new file mode 100644 index 0000000..8a946f2 --- /dev/null +++ b/script.js @@ -0,0 +1,1017 @@ +let port = null; + +let onQuizPage = null; +let interfaceBox = null; +let roomCode = null; +let username = null; +let host = null; +let urls = {}; + +let quizStartTime = null; +let quizRunning = false; + +const quizStartObserver = new MutationObserver(quizStarted); +const scoreObserver = new MutationObserver(() => sendLiveScore()); +const quizFinishObserver = new MutationObserver(quizFinished); + + +if (document.readyState === "complete" || document.readyState === "interactive") +{ + run() +} +else +{ + document.addEventListener("DOMContentLoaded", run); +} + + +let con = true; // To stop debug erros on reloading extension + +window.onunload = function () +{ + if (con) chrome.runtime.sendMessage({type: "pageClosed"}); +} + +function run() +{ + chrome.runtime.sendMessage({type: "showPageAction"}); + chrome.runtime.onMessage.addListener( + (message) => + { + if (message.type == "optionsChanged" && /^\/games\/.*/.test(window.location.pathname)) + { + init(); + } + } + ); + port = chrome.runtime.connect({name: "messageRelay"}); + port.onDisconnect.addListener( + () => + { + con = false; + chrome.runtime.sendMessage({type: "pageClosed"}); + } + ); + if (/^\/games\//.test(window.location.pathname)) + { + onQuizPage = true; + } + else + { + onQuizPage = false; + } + init(); +} + +async function init() +{ + // Add UI Container + const interfaceBox = addInterfaceBox(); + + // Check to see if we are still connected to a room + const statusResponse = await new Promise( + (resolve, reject) => + { + let statusChecker; + port.onMessage.addListener( + statusChecker = (message) => + { + port.onMessage.removeListener(statusChecker); + resolve(message); + } + ); + port.postMessage({type: "connectionStatus", url: window.location.href}); + } + ); + + if (statusResponse.connected) + { + // We are already connected to a room + roomCode = statusResponse.room_code; + username = statusResponse.username; + host = statusResponse.host; + urls = statusResponse.urls; + + onRoomConnect(statusResponse.scores); + if (host) + { + updateLeaderboardUrls(); + } + if (!host && onQuizPage) + { + toggleQuizStartProvention(true); + } + } + else + { + // Not connected so add room forms + addCreateRoomForm(); + addJoinRoomForm(); + } +} + +function addInterfaceBox() +{ + const centerContent = document.querySelector(`#CenterContent`); + + const gameHeader = document.querySelector(`.game-header`); + const staffPicks = document.querySelector(`#staff-picks-wrapper`); + + if (document.querySelector(`#interfaceContainer`) === null) + { + const interfaceContainer = document.createElement("div"); + interfaceContainer.id = "interfaceContainer"; + interfaceContainer.style = + ` + position: sticky; + top: 67px; + margin-left: calc(100% + ${window.getComputedStyle(centerContent).paddingRight}); + height: 0; + width: 0; + overflow: visible; + z-index: 999; + `; + + if (gameHeader !== null) + { + gameHeader.parentNode.insertBefore(interfaceContainer, gameHeader.nextElementSibling); + } + else if (staffPicks !== null) + { + staffPicks.parentNode.insertBefore(interfaceContainer, staffPicks.nextElementSibling); + } + else + { + centerContent.insertBefore(interfaceContainer, centerContent.firstElementChild); + } + } + + if (document.querySelector(`#interfaceBox`) === null) + { + interfaceBox = document.createElement("div"); + interfaceBox.id = "interfaceBox"; + interfaceBox.style = + ` + width: calc(((100vw - 960px) / 2 - 4px)); + padding: 0.5em; + box-sizing: border-box; + max-width: 400px; + list-style: none; + border-width: 1px; + border-style: solid; + border-color: darkgrey; + border-radius: 0.25em; + background-color: white; + + display: grid; + row-gap: 1em; + grid-template-columns: 100%; + `; + + interfaceContainer.appendChild(interfaceBox); + } +} + +function resetInterface() +{ + quizStartObserver.disconnect(); + scoreObserver.disconnect(); + + port.onMessage.removeListener(processMessage); + + code = null; + username = null; + host = null; + urls = {}; + + Array.from(interfaceBox.childNodes).forEach( + (element) => element.remove() + ); + + init(); +} + +function processMessage(message) +{ + console.log(message); + + messageType = message["type"]; + + switch (messageType) + { + case "scores_update": + updateLeaderboard(message["scores"]); + break; + case "room_closed": + console.log("room closed"); + resetInterface(); + break; + case "rooms_list": + console.log(message["rooms"]); + break; + case "error": + console.error(message["error"]); + break; + case "removed_from_room": + const removedUser = message["username"]; + console.log(`${removedUser} removed from room`); + if (removedUser === username) + { + resetInterface(); + } + else + { + delete urls[removedUser]; + } + break; + case "start_quiz": + // Start the quiz! + + // First remove the quiz start provention + toggleQuizStartProvention(false); + document.querySelector(`#button-play`).click(); + break; + case "live_scores_update": + updateLiveScores(message["live_scores"]); + break; + case "change_quiz": + newUrl = message["url"]; + currentUrl = window.location.href; + if (currentUrl !== newUrl) + { + window.location = newUrl; + } + break; + case "url_update": + urls[message["username"]] = message["url"] + updateLeaderboardUrls() + break; + } + +} + +async function createRoom(event, form) +{ + event.preventDefault(); + + username = form.querySelector(`input[type="text"]`).value.trim(); + const button = form.querySelector(`input[type="submit"]`); + button.disabled = true; + button.value = "..."; + + const message = { + type: "create_room", + username: username, + url: window.location.href + } + try + { + port.postMessage({type: "startConnection", initialMessage: message}); + + await new Promise((resolve, reject) => { + let connectListener; + port.onMessage.addListener( + connectListener = (message) => + { + port.onMessage.removeListener(connectListener); + if (message.type === "new_room_code") + { + roomCode = message.room_code; + resolve(); + } + else + { + reject(message); + } + } + ); + }); + } + catch (error) + { + console.error(error); + return; + } + + console.log("connection established."); + + await navigator.permissions.query({name: "clipboard-write"}).then( + (result) => + { + if (result.state == "granted" || result.state == "prompt") + { + // Copy roomCode to clipboard + navigator.clipboard.writeText(roomCode).then((success) => true, (failure) => false); + } + } + ); + + interfaceBox.querySelectorAll(`form`).forEach( + (form) => form.remove() + ); + + host = true; + + onRoomConnect(); +} + +async function joinRoom(event, form) +{ + event.preventDefault(); + + username = form.querySelector(`#joinRoomUsernameInput`).value.trim(); + roomCode = form.querySelector(`#joinRoomCodeInput`).value.trim(); + + const button = form.querySelector(`input[type="submit"]`); + button.disabled = true; + button.value = "..."; + + console.log(`Trying to join room ${roomCode}`); + + const message = { + type: "join_room", + username: username, + code: roomCode, + url: window.location.href + }; + + try + { + port.postMessage({type: "startConnection", initialMessage: message}); + + let connectListener; + await new Promise((resolve, reject) => { + port.onMessage.addListener( + connectListener = (message) => + { + port.onMessage.removeListener(connectListener); + if (message.type === "join_room") + { + if (message.success) + { + resolve(); + } + else + { + reject(message.fail_reason); + } + } + else + { + reject(message); + } + } + ); + }); + + } + catch (error) + { + console.error(error); + resetInterface(); + return; + } + + console.log("connection established"); + onRoomConnect(); +} + +function onRoomConnect(existingScores) +{ + // Set up message handing + port.onMessage.addListener(processMessage); + + // Clear the interface box of the forms + interfaceBox.querySelectorAll(`form`).forEach( + (form) => form.remove() + ); + + // Display the room code + interfaceBox.appendChild(document.createElement("h4")); + interfaceBox.lastChild.textContent = `Room code: ${roomCode}`; + interfaceBox.lastChild.style.margin = "0"; + + // If the user is a host and is on a quiz, + // add a button to send the quiz to the rest of the room + if (host && onQuizPage) + { + const changeQuizButton = document.createElement("button"); + changeQuizButton.id = "changeQuizButton"; + changeQuizButton.textContent = "Send Quiz to Room"; + changeQuizButton.addEventListener("click", + (event) => + { + port.postMessage( + { + type: "change_quiz", + url: window.location.href + } + ); + } + ); + + interfaceBox.appendChild(changeQuizButton); + } + + // Make the leaderboard + let scores; + if (existingScores !== undefined) + { + scores = existingScores; + } + else + { + scores = {}; + scores[username] = 0; + } + + updateLeaderboard(scores); + + // Add a button to leave the room + interfaceBox.appendChild(document.createElement("button")); + interfaceBox.lastChild.textContent = "Leave Room"; + interfaceBox.lastChild.addEventListener("click", + (event) => { + port.postMessage({type: "leave_room"}); + } + ); + + // If on a quiz page, observe for the start of the quiz + if (onQuizPage) + { + // add observer for quiz starting + const startButtons = document.querySelector(`#playPadding`); + quizStartObserver.observe(startButtons, {attributes: true}); + } + + // If not a host and on a quiz, stop the user from starting any quizzes + if (!host && onQuizPage) + { + toggleQuizStartProvention(true); + } +} + +function updateLeaderboard(scores) +{ + const scoresCopy = JSON.parse(JSON.stringify(scores)); + let leaderboard = interfaceBox.querySelector(`#leaderboard`); + if (leaderboard === null) + leaderboard = addLeaderboard(scores); + + const rows = leaderboard.querySelectorAll(`li`); + rows.forEach( + (row) => { + const nameElem = row.firstChild; + const pointsElem = row.lastChild; + if (scores[nameElem.textContent] !== undefined) + { + pointsElem.textContent = scores[nameElem.textContent]; + delete scores[nameElem.textContent]; + } + else + { + row.remove(); + } + } + ); + + for (const [name, points] of Object.entries(scores)) + { + const row = rows[0].cloneNode(true); + row.firstChild.textContent = name; + row.lastChild.textContent = points; + leaderboard.appendChild(row); + } + + // Now sort the by score, falling back to alphabetically + // First restart scores back to it's unmodified state + scores = scoresCopy; + + const sortedRows = Array.from(leaderboard.querySelectorAll('li')); + + const alphabetically = (a, b) => + { + if (a.firstChild.textContent.toLowerCase() < b.firstChild.textContent.toLowerCase()) + return -1; + else + return 1; + }; + + const byScore = (a, b) => + { + const aName = a.firstChild.textContent; + const bName = b.firstChild.textContent; + const aScore = scores[aName]; + const bScore = scores[bName]; + + if (aScore < bScore) + return 1; + else if (aScore > bScore) + return -1; + else + return 0; + }; + + sortedRows.sort(alphabetically); + sortedRows.sort(byScore); + + sortedRows.forEach( + (row) => + { + leaderboard.appendChild(row); + } + ); +} + +function updateLeaderboardUrls() +{ + const leaderboard = interfaceBox.querySelector(`#leaderboard`); + if (leaderboard === null) + return; + + const rows = leaderboard.querySelectorAll(`li`); + rows.forEach( + (row) => + { + const name = row.firstChild.textContent; + if (name !== username && urls[name] !== window.location.href) + { + row.style.backgroundColor = "LightGrey"; + } + else + { + row.style.backgroundColor = "unset"; + } + } + ); + + if (onQuizPage) + { + const playButton = document.querySelector("#button-play"); + const playButtonContainer = playButton.parentNode.parentNode; + + const allPlayersOnSamePage = ! Object.entries(urls).some(entry => entry[1] !== window.location.href); + + toggleQuizStartProvention(allPlayersOnSamePage === false) + } +} + +function toggleQuizStartProvention(prevent) +{ + const playPadding = document.querySelector(`#playPadding`); + if (prevent) + { + playPadding.addEventListener("click", stopQuizStart, true); + } + else + { + playPadding.removeEventListener("click", stopQuizStart, true); + } + +} +function stopQuizStart(event) +{ + event.stopPropagation(); +} + +function updateLiveScores(scores) +{ + let liveScores = interfaceBox.querySelector(`#liveScores`); + if (liveScores === null) + liveScores = addLiveScores(scores); + + const rows = liveScores.querySelectorAll(`li`); + rows.forEach( + (row) => + { + const nameElem = row.firstChild; + const pointsElem = row.lastChild; + if (scores[nameElem.textContent] !== undefined) + { + pointsElem.textContent = scores[nameElem.textContent]["score"]; + if (scores[nameElem.textContent]["finished"]) + { + // This player has finished the quiz + row.style.backgroundColor = "LightGreen"; + } + else + { + row.style.backgroundColor = "unset"; + } + } + else + { + row.remove(); + } + } + ); + + /* Probaly won't have extra people in the live scores once the quiz has started + for (const [name, data] of Object.entries(scores)) + { + const row = rows[0].cloneNode(true); + row.firstChild.textContent = name; + row.lastChild.textContent = data["score"]; + liveScores.appendChild(row); + } + */ + + // Now sort them into order, by score, then by time to break ties, then alphabetically if still tied + let sortedRows = Array.from(rows); + + const alphabetically = (a, b) => + { + if (a.firstChild.textContent.toLowerCase() < b.firstChild.textContent.toLowerCase()) + return -1; + else + return 1; + }; + + const byTime = (a, b) => + { + const aName = a.firstChild.textContent; + const bName = b.firstChild.textContent; + const aTime = scores[aName]["quiz_time"]; + const bTime = scores[bName]["quiz_time"]; + + if (aTime < bTime) + return -1; + else if (aTime > bTime) + return 1; + else + return 0; // Highly unlikely + }; + + const byScore = (a, b) => + { + const aName = a.firstChild.textContent; + const bName = b.firstChild.textContent; + const aScore = scores[aName]["score"]; + const bScore = scores[bName]["score"]; + + if (aScore < bScore) + return 1; + else if (aScore > bScore) + return -1; + else + return 0; + }; + + + sortedRows.sort(alphabetically); + sortedRows.sort(byTime); + sortedRows.sort(byScore); + + sortedRows.forEach( + (row) => + { + liveScores.appendChild(row); + } + ); +} + +function addLeaderboard(scores) +{ + const leaderboard = document.createElement("ol"); + leaderboard.id = "leaderboard"; + leaderboard.style = + ` + width: 100%; + max-width: 10em; + padding: 0; + margin: auto; + `; + + const columnHeaders = document.createElement("h3"); + columnHeaders.style = + ` + margin: 0 0 1em 0; + display: grid; + grid-template-columns: 1fr 1fr; + `; + columnHeaders.appendChild(document.createElement("span")); + columnHeaders.lastChild.textContent = "Name"; + columnHeaders.appendChild(document.createElement("span")); + columnHeaders.lastChild.textContent = "Points"; + columnHeaders.lastChild.style = `text-align: right;`; + + leaderboard.appendChild(columnHeaders); + + for (const [name, points] of Object.entries(scores)) + { + const row = document.createElement("li"); + row.style = + ` + display: grid; + grid-template-columns: auto max-content; + `; + row.appendChild(document.createTextNode(name)); + + const pointsContainer = document.createElement("span"); + pointsContainer.textContent = points; + pointsContainer.style = + ` + text-align: right; + `; + row.appendChild(pointsContainer); + + leaderboard.appendChild(row); + } + + const leaderboardHeader = document.createElement("h2"); + leaderboardHeader.id = "leaderboardHeader"; + leaderboardHeader.style.margin = "0"; + leaderboardHeader.textContent = "Overall Rankings"; + interfaceBox.appendChild(leaderboardHeader); + + interfaceBox.appendChild(leaderboard); + return leaderboard; +} + +function addLiveScores(scores) +{ + const liveScores = document.createElement("ol"); + liveScores.id = "liveScores"; + liveScores.style = + ` + width: 100%; + max-width: 10em; + padding: 0; + margin: auto; + `; + + const columnHeaders = document.createElement("h3"); + columnHeaders.style = + ` + margin: 0 0 1em 0; + display: grid; + grid-template-columns: 1fr 1fr; + `; + + columnHeaders.appendChild(document.createElement("span")); + columnHeaders.lastChild.textContent = "Name"; + columnHeaders.appendChild(document.createElement("span")); + columnHeaders.lastChild.textContent = "Points"; + columnHeaders.lastChild.style = `text-align: right;`; + + liveScores.appendChild(columnHeaders); + + for (const [name, data] of Object.entries(scores)) + { + const row = document.createElement("li"); + row.style = + ` + display: grid; + grid-template-columns: auto max-content; + background-color: inherit; + `; + row.appendChild(document.createTextNode(name)); + + const pointsContainer = document.createElement("span"); + pointsContainer.textContent = data["score"]; + pointsContainer.style = + ` + text-align: right; + + `; + row.appendChild(pointsContainer); + + liveScores.appendChild(row); + } + + const liveScoresHeader = document.createElement("h2"); + liveScoresHeader.id = "liveScoresHeader"; + liveScoresHeader.style.margin = "0"; + liveScoresHeader.textContent = "Quiz Scores"; + interfaceBox.insertBefore(liveScoresHeader, interfaceBox.querySelector(`#leaderboardHeader`)); + + interfaceBox.insertBefore(liveScores, liveScoresHeader.nextElementSibling); + return liveScores; +} + +function quizStarted(mutationList) +{ + mutationList.forEach( + (mutation) => + { + if (mutation.target.getAttribute("style") !== null) + { + // Quiz started + quizStartObserver.disconnect(); + console.log("Quiz started"); + + quizRunning = true; + quizStartTime = new Date(); + + port.postMessage({"type": "start_quiz"}); + sendLiveScore(); + + // start observing changes to the score + const scoreElement = document.querySelector(`.currentScore`); + scoreObserver.observe(scoreElement, {childList: true}); + + // start observing quiz end + const resultsBox = document.querySelector(`#reckonBox`); + quizFinishObserver.observe(resultsBox, {attributes: true}); + } + else + { + return false; + } + } + ); +} + +function quizFinished(mutationList) +{ + mutationList.forEach( + (mutation) => + { + if (mutation.target.getAttribute("style") !== null) + { + // Quiz finished + console.log("Quiz finished"); + + quizFinishObserver.disconnect(); + scoreObserver.disconnect(); + + quizRunning = false; + + sendLiveScore(); + } + else + { + return false; + } + } + ); +} + +function getCurrentScore() +{ + const scoreText = document.querySelector(`.currentScore`).textContent; + + return Number(scoreText.split("/")[0]); +} + +function sendLiveScore() +{ + const currentScore = getCurrentScore(); + const elapsedTime = (new Date()) - quizStartTime; + + port.postMessage( + { + type: "live_scores_update", + current_score: currentScore, + quiz_time: elapsedTime, + finished: !quizRunning, + } + ); +} + +function addCreateRoomForm() +{ + const form = document.createElement("form"); + form.id = "createRoomForm"; + form.autocomplete = "off"; + form.addEventListener("submit", (event) => {createRoom(event, form)}); + form.style = + ` + display: grid; + grid-template-columns: min(100%, 10em); + `; + + const heading = document.createElement("h3"); + heading.textContent = "Create a Room"; + form.appendChild(heading); + + const usernameInput = document.createElement("input"); + usernameInput.id = "createRoomUsernameInput"; + usernameInput.setAttribute("type", "text"); + usernameInput.value = "Enter Username"; + + form.appendChild(usernameInput); + + const button = document.createElement("input"); + button.id = "createRoomSubmit"; + button.setAttribute("type", "submit"); + button.value = "Create Room"; + button.disabled = true; + + + usernameInput.addEventListener("keyup", function () + { + this.value = this.value.trimStart(); + button.disabled = this.value === ""; + } + ); + + usernameInput.addEventListener("focus", function () + { + if (this.value === "Enter Username") + this.value = ""; + } + ); + + usernameInput.addEventListener("blur", function () + { + if (this.value === "") + this.value = "Enter Username"; + } + ); + + form.appendChild(button); + + interfaceBox.appendChild(form); +} + +function addJoinRoomForm() +{ + const form = document.createElement("form"); + form.id = "joinRoomForm"; + form.autocomplete = "off"; + form.addEventListener("submit", (event) => {joinRoom(event, form)}); + form.style = + ` + display: grid; + grid-template-columns: min(100%, 10em); + `; + + const heading = document.createElement("h3"); + heading.textContent = "Join a Room"; + form.appendChild(heading); + + const usernameInput = document.createElement("input"); + usernameInput.id = "joinRoomUsernameInput"; + usernameInput.setAttribute("type", "text"); + usernameInput.value = "Enter Username"; + + form.appendChild(usernameInput); + + const codeInput = document.createElement("input"); + codeInput.id = "joinRoomCodeInput"; + codeInput.setAttribute("type", "text"); + codeInput.value = "Enter Room Code"; + + form.appendChild(codeInput); + + const button = document.createElement("input"); + button.id = "joinRoomSubmit"; + button.setAttribute("type", "submit"); + button.value = "Join Room"; + button.disabled = true; + + usernameInput.addEventListener("keyup", function () + { + this.value = this.value.trimStart(); + button.disabled = ( + this.value === "" + || codeInput.value === "" + || codeInput.value === "Enter Room Code" + ); + } + ); + + usernameInput.addEventListener("focus", function () + { + if (this.value === "Enter Username") + this.value = ""; + } + ); + + usernameInput.addEventListener("blur", function () + { + if (this.value === "") + this.value = "Enter Username"; + } + ); + + codeInput.addEventListener("keyup", function () + { + this.value = this.value.trimStart(); + button.disabled = ( + this.value === "" + || usernameInput.value === "" + || usernameInput.value === "Enter Username" + ); + } + ); + + codeInput.addEventListener("focus", function () + { + if (this.value === "Enter Room Code") + this.value = ""; + } + ); + + codeInput.addEventListener("blur", function () + { + if (this.value === "") + this.value = "Enter Room Code"; + } + ); + + form.appendChild(button); + + interfaceBox.appendChild(form); +}