From 91c842dd40eda35a23e7907d43b8b50721ad5ce9 Mon Sep 17 00:00:00 2001
From: Daniel Scalzi <d_scalzi@yahoo.com>
Date: Wed, 30 May 2018 22:22:17 -0400
Subject: [PATCH] Added UI and implementation for the account settings tab.

Features:
* Add a new account.
* Switch accounts.
* Log out of an account.

Also added a cancel button to the login UI which is only shown when a user is adding an account. In that case, the operation should be and is cancellable.
---
 app/assets/css/launcher.css       | 244 +++++++++++++++++++++++++++++-
 app/assets/js/scripts/landing.js  |   1 +
 app/assets/js/scripts/login.js    |  28 +++-
 app/assets/js/scripts/settings.js | 156 ++++++++++++++++++-
 app/assets/js/scripts/uibinder.js |   1 +
 app/login.ejs                     |   6 +
 app/settings.ejs                  |  11 ++
 7 files changed, 442 insertions(+), 5 deletions(-)

diff --git a/app/assets/css/launcher.css b/app/assets/css/launcher.css
index 5cf3c496..6873d1a8 100644
--- a/app/assets/css/launcher.css
+++ b/app/assets/css/launcher.css
@@ -387,6 +387,68 @@ body, button {
     background: rgba(0, 0, 0, 0.50);
 }
 
+/* Login cancel button styles. */
+#loginCancelContainer {
+    position: absolute;
+    top: 5%;
+    right: 5%;
+}
+
+/* Login cancel button styles. */
+#loginCancelButton {
+    background: none;
+    border: none;
+    outline: none;
+    cursor: pointer;
+    transition: 0.25s ease;
+}
+#loginCancelButton:hover #loginCancelIcon,
+#loginCancelButton:hover #loginCancelText,
+#loginCancelButton:focus #loginCancelIcon,
+#loginCancelButton:focus #loginCancelText {
+    text-shadow: 0px 0px 20px white;
+}
+#loginCancelButton:hover #loginCancelIcon,
+#loginCancelButton:focus #loginCancelIcon {
+    box-shadow: 0px 0px 20px white;
+}
+#loginCancelButton:active #loginCancelIcon,
+#loginCancelButton:active #loginCancelText {
+    text-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75);
+    color: rgba(255, 255, 255, 0.75);
+    border-color: rgba(255, 255, 255, 0.75);
+}
+#loginCancelButton:active #loginCancelIcon {
+    box-shadow: 0px 0px 20px rgba(255, 255, 255, 0.75);
+}
+#loginCancelButton:disabled {
+    pointer-events: none;
+}
+#loginCancelButton:disabled #loginCancelIcon,
+#loginCancelButton:disabled #loginCancelText {
+    color: rgba(255, 255, 255, 0.75);
+    border-color: rgba(255, 255, 255, 0.75);
+}
+
+/* The X in a circle icon for the cancel button. */
+#loginCancelIcon {
+    border-radius: 50%;
+    border: 1px solid white;
+    box-sizing: border-box;
+    height: 30px;
+    width: 30px;
+    font-size: 19px;
+    line-height: 30px;
+    margin: 0 auto;
+    margin-bottom: 5px;
+    transition: 0.25s ease;
+}
+/* Text for the login cancel button. */
+#loginCancelText {
+    font-size: 15px;
+    transition: 0.25s ease;
+}
+
 /* Login content wrapper. */
 #loginContent {
     display: flex;
@@ -816,6 +878,7 @@ body, button {
  *                                                                             *
  ******************************************************************************/
 
+/* Main settings container. */
 #settingsContainer {
     position: relative;
     height: 100%;
@@ -823,6 +886,7 @@ body, button {
     background: rgba(0, 0, 0, 0.50);
 }
 
+/* Left hand side of the settings UI, for navigation. */
 #settingsContainerLeft {
     height: 100%;
     width: 25%;
@@ -830,20 +894,22 @@ body, button {
     box-sizing: border-box;
 }
 
+/* Settings navigation container. */
 #settingsNavContainer {
     display: flex;
     flex-direction: column;
 }
 
+/* Navigation header styles. */
 #settingsNavHeader {
     display: flex;
     justify-content: center;
 }
-
 #settingsNavHeaderText {
     font-size: 20px;
 }
 
+/* Navigation items outer container. */
 #settingsNavItemsContainer {
     height: 100%;
     display: flex;
@@ -852,11 +918,13 @@ body, button {
     box-sizing: border-box;
 }
 
+/* Navigation items content container. */
 #settingsNavItemsContent {
     display: flex;
     flex-direction: column;
 }
 
+/* Navigation item shared styles. */
 .settingsNavItem {
     background: none;
     border: none;
@@ -880,23 +948,36 @@ body, button {
     text-shadow: none;
 }
 
+/* Right hand side of the settings container, for tabs. */
 #settingsContainerRight {
     height: 100%;
     width: 75%;
-    padding: 5% 0%;
+    padding-top: 5%;
     box-sizing: border-box;
 }
 
+/* Settings tab shared styles. */
 .settingsTab {
     width: 100%;
     height: 100%;
+    overflow-y: auto;
+}
+.settingsTab::-webkit-scrollbar {
+    width: 2px;
+}
+.settingsTab::-webkit-scrollbar-track {
+    display: none;
+}
+.settingsTab::-webkit-scrollbar-thumb {
+    border-radius: 10px;
+    box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50);
 }
 
+/* Tab header shared styles. */
 .settingsTabHeader {
     display: flex;
     flex-direction: column;
 }
-
 .settingsTabHeaderText {
     font-size: 20px;
     font-family: 'Avenir Medium';
@@ -905,6 +986,163 @@ body, button {
     font-size: 12px;
 }
 
+/* * *
+* Settings View (Account Tab)
+* * */
+
+/* Add account button styles. */
+#settingsAddAccountContainer {
+    margin-top: 20px;
+}
+#settingsAddAccount {
+    background: rgba(0, 0, 0, 0.25);
+    border: 1px solid rgba(126, 126, 126, 0.57);
+    border-radius: 3px;
+    height: 50px;
+    width: 75%;
+    text-align: left;
+    padding: 0px 50px;
+    cursor: pointer;
+    outline: none;
+    transition: 0.25s ease;
+}
+#settingsAddAccount:hover,
+#settingsAddAccount:focus {
+    background: rgba(54, 54, 54, 0.25);
+    text-shadow: 0px 0px 20px white;
+}
+
+/* Settings auth accounts header. */
+#settingsCurrentAccountsHeader {
+    margin: 20px 0px;
+}
+#settingsCurrentAccountsHeaderText {
+    font-size: 16px;
+}
+
+/* Auth account list container styles. */
+#settingsCurrentAccounts {
+    margin-bottom: 5%;
+}
+#settingsCurrentAccounts > .settingsAuthAccount:not(:last-child) {
+    margin-bottom: 10px;
+}
+#settingsCurrentAccounts > .settingsAuthAccount:not(:first-child) {
+    margin-top: 10px;
+}
+
+/* Auth account shared styles. */
+.settingsAuthAccount {
+    display: flex;
+    width: 75%;
+    background: rgba(0, 0, 0, 0.25);
+    border-radius: 3px;
+    border: 1px solid rgba(126, 126, 126, 0.57);
+}
+
+/* Left hand side of an auth account element, for the skin image. */
+.settingsAuthAccountLeft {
+    padding: 5px 5px 5px 20px;
+}
+
+/* Image of the auth account's skin. */
+.settingsAuthAccountImage {
+    height: 115px;
+}
+
+/* Right hand side of the auth account, for info + actions. */
+.settingsAuthAccountRight {
+    display: flex;
+    width: 100%;
+}
+
+/* Account details container. */
+.settingsAuthAccountDetails {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    margin-left: 20px;
+    width: 100%;
+}
+.settingsAuthAccountDetails > *:not(:last-child) {
+    margin-bottom: 20px;
+}
+
+/* Account detail element styles. */
+.settingsAuthAccountDetailPane {
+    display: flex;
+    flex-direction: column;
+}
+.settingsAuthAccountDetailTitle {
+    font-size: 12px;
+    color: grey;
+    font-weight: bold;
+    font-family: 'Avenir Medium';
+}
+.settingsAuthAccountDetailValue {
+    font-size: 14px;
+}
+
+/* Account actions container. */
+.settingsAuthAccountActions {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    align-items: flex-end;
+    padding: 10px;
+}
+
+/* Account select button shared styles. */
+.settingsAuthAccountSelect {
+    opacity: 0;
+    border: none;
+    white-space: nowrap;
+    background: none;
+    font-family: 'Avenir Medium';
+    outline: none;
+    transition: 0.25s ease;
+}
+.settingsAuthAccountSelect:hover:not([selected]),
+.settingsAuthAccountSelect:focus:not([selected]) {
+    text-shadow: 0px 0px 20px white, 0px 0px 20px white;
+    cursor: pointer;
+}
+.settingsAuthAccount:hover .settingsAuthAccountSelect:not([selected]),
+.settingsAuthAccountSelect[selected] {
+    opacity: 1;
+}
+.settingsAuthAccountSelect[selected] {
+    pointer-events: none;
+}
+
+/* Account logout button shared styles. */
+.settingsAuthAccountLogOut {
+    opacity: 0;
+    border: 1px solid rgb(241, 55, 55);
+    color: rgb(241, 55, 55);
+    background: none;
+    font-size: 12px;
+    border-radius: 3px;
+    font-family: 'Avenir Medium';
+    transition: 0.25s ease;
+    cursor: pointer;
+    outline: none;
+}
+.settingsAuthAccountLogOut:hover,
+.settingsAuthAccountLogOut:focus {
+    box-shadow: 0px 0px 20px rgb(241, 55, 55);
+    background: rgba(241, 55, 55, 0.25);
+}
+.settingsAuthAccountLogOut:active {
+    box-shadow: 0px 0px 20px rgb(185, 47, 47);
+    background: rgba(185, 47, 47, 0.25);
+    border: 1px solid rgb(185, 47, 47);
+    color: rgb(185, 47, 47);
+}
+.settingsAuthAccount:hover .settingsAuthAccountLogOut {
+    opacity: 1;
+}
+
 /*******************************************************************************
  *                                                                             *
  * Landing View (Structural Styles)                                            *
diff --git a/app/assets/js/scripts/landing.js b/app/assets/js/scripts/landing.js
index dce50401..4d00c007 100644
--- a/app/assets/js/scripts/landing.js
+++ b/app/assets/js/scripts/landing.js
@@ -116,6 +116,7 @@ document.getElementById('launch_button').addEventListener('click', function(e){
 
 // Bind settings button
 document.getElementById('settingsMediaButton').onclick = (e) => {
+    prepareSettings()
     switchView(getCurrentView(), VIEWS.settings)
 }
 
diff --git a/app/assets/js/scripts/login.js b/app/assets/js/scripts/login.js
index ac96a08f..61ea765b 100644
--- a/app/assets/js/scripts/login.js
+++ b/app/assets/js/scripts/login.js
@@ -7,6 +7,8 @@ const basicEmail            = /^\S+@\S+\.\S+$/
 //const validEmail          = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
 
 // Login Elements
+const loginCancelContainer  = document.getElementById('loginCancelContainer')
+const loginCancelButton     = document.getElementById('loginCancelButton')
 const loginEmailError       = document.getElementById('loginEmailError')
 const loginUsername         = document.getElementById('loginUsername')
 const loginPasswordError    = document.getElementById('loginPasswordError')
@@ -139,6 +141,7 @@ function loginLoading(v){
  */
 function formDisabled(v){
     loginDisabled(v)
+    loginCancelButton.disabled = v
     loginUsername.disabled = v
     loginPassword.disabled = v
     if(v){
@@ -215,6 +218,23 @@ function resolveError(err){
     }
 }
 
+let loginViewOnSuccess = VIEWS.landing
+let loginViewOnCancel = VIEWS.settings
+
+function loginCancelEnabled(val){
+    if(val){
+        $(loginCancelContainer).show()
+    } else {
+        $(loginCancelContainer).hide()
+    }
+}
+
+loginCancelButton.onclick = (e) => {
+    switchView(getCurrentView(), loginViewOnCancel, 500, 500, () => {
+        loginCancelEnabled(false)
+    })
+}
+
 // Disable default form behavior.
 loginForm.onsubmit = () => { return false }
 
@@ -233,7 +253,13 @@ loginButton.addEventListener('click', () => {
         $('.checkmark').toggle()
         //console.log(value)
         setTimeout(() => {
-            switchView(VIEWS.login, VIEWS.landing, 500, 500, () => {
+            switchView(VIEWS.login, loginViewOnSuccess, 500, 500, () => {
+                // Temporary workaround
+                if(loginViewOnSuccess === VIEWS.settings){
+                    prepareSettings()
+                }
+                loginViewOnSuccess = VIEWS.landing // Reset this for good measure.
+                loginCancelEnabled(false) // Reset this for good measure.
                 loginUsername.value = ''
                 loginPassword.value = ''
                 $('.circle-loader').toggleClass('load-complete')
diff --git a/app/assets/js/scripts/settings.js b/app/assets/js/scripts/settings.js
index f9e7e468..f6b7f687 100644
--- a/app/assets/js/scripts/settings.js
+++ b/app/assets/js/scripts/settings.js
@@ -1,5 +1,15 @@
+const settingsAddAccount      = document.getElementById('settingsAddAccount')
+const settingsCurrentAccounts = document.getElementById('settingsCurrentAccounts')
+
+/**
+ * General Settings Functions
+ */
+
 let selectedTab = 'settingsTabAccount'
 
+/**
+ * Bind functionality for the settings navigation items.
+ */
 function setupSettingsTabs(){
     Array.from(document.getElementsByClassName('settingsNavItem')).map((val) => {
         val.onclick = (e) => {
@@ -22,4 +32,148 @@ function setupSettingsTabs(){
     })
 }
 
-setupSettingsTabs()
\ No newline at end of file
+/**
+ * Account Management Tab
+ */
+
+// Bind the add account button.
+settingsAddAccount.onclick = (e) => {
+    switchView(getCurrentView(), VIEWS.login, 500, 500, () => {
+        loginViewOnCancel = VIEWS.settings
+        loginViewOnSuccess = VIEWS.settings
+        loginCancelEnabled(true)
+    })
+}
+
+/**
+ * Bind functionality for the account selection buttons. If another account
+ * is selected, the UI of the previously selected account will be updated.
+ */
+function bindAuthAccountSelect(){
+    Array.from(document.getElementsByClassName('settingsAuthAccountSelect')).map((val) => {
+        val.onclick = (e) => {
+            if(val.hasAttribute('selected')){
+                return
+            }
+            const selectBtns = document.getElementsByClassName('settingsAuthAccountSelect')
+            for(let i=0; i<selectBtns.length; i++){
+                if(selectBtns[i].hasAttribute('selected')){
+                    selectBtns[i].removeAttribute('selected')
+                    selectBtns[i].innerHTML = 'Select Account'
+                }
+            }
+            val.setAttribute('selected', '')
+            val.innerHTML = 'Selected Account &#10004;'
+            ConfigManager.setSelectedAccount(val.closest('.settingsAuthAccount').getAttribute('uuid'))
+            ConfigManager.save()
+            updateSelectedAccount(ConfigManager.getSelectedAccount())
+        }
+    })
+}
+
+/**
+ * Bind functionality for the log out button. If the logged out account was
+ * the selected account, another account will be selected and the UI will
+ * be updated accordingly.
+ */
+function bindAuthAccountLogOut(){
+    Array.from(document.getElementsByClassName('settingsAuthAccountLogOut')).map((val) => {
+        val.onclick = (e) => {
+            const parent = val.closest('.settingsAuthAccount')
+            const uuid = parent.getAttribute('uuid')
+            const prevSelAcc = ConfigManager.getSelectedAccount()
+            AuthManager.removeAccount(uuid).then(() => {
+                if(uuid === prevSelAcc.uuid){
+                    const selAcc = ConfigManager.getSelectedAccount()
+                    refreshAuthAccountSelected(selAcc.uuid)
+                    updateSelectedAccount(selAcc)
+                }
+            })
+            $(parent).fadeOut(250, () => {
+                parent.remove()
+            })
+        }
+    })
+}
+
+/**
+ * Refreshes the status of the selected account on the auth account
+ * elements.
+ * 
+ * @param {string} uuid The UUID of the new selected account.
+ */
+function refreshAuthAccountSelected(uuid){
+    Array.from(document.getElementsByClassName('settingsAuthAccount')).map((val) => {
+        const selBtn = val.getElementsByClassName('settingsAuthAccountSelect')[0]
+        if(uuid === val.getAttribute('uuid')){
+            selBtn.setAttribute('selected', '')
+            selBtn.innerHTML = 'Selected Account &#10004;'
+        } else {
+            if(selBtn.hasAttribute('selected')){
+                selBtn.removeAttribute('selected')
+            }
+            selBtn.innerHTML = 'Select Account'
+        }
+    })
+}
+
+/**
+ * Add auth account elements for each one stored in the authentication database.
+ */
+function populateAuthAccounts(){
+    const authAccounts = ConfigManager.getAuthAccounts()
+    const authKeys = Object.keys(authAccounts)
+    const selectedUUID = ConfigManager.getSelectedAccount().uuid
+
+    let authAccountStr = ``
+
+    authKeys.map((val) => {
+        const acc = authAccounts[val]
+        authAccountStr += `<div class="settingsAuthAccount" uuid="${acc.uuid}">
+            <div class="settingsAuthAccountLeft">
+                <img class="settingsAuthAccountImage" alt="${acc.displayName}" src="https://crafatar.com/renders/body/${acc.uuid}?scale=3&default=MHF_Steve&overlay">
+            </div>
+            <div class="settingsAuthAccountRight">
+                <div class="settingsAuthAccountDetails">
+                    <div class="settingsAuthAccountDetailPane">
+                        <div class="settingsAuthAccountDetailTitle">Username</div>
+                        <div class="settingsAuthAccountDetailValue">${acc.displayName}</div>
+                    </div>
+                    <div class="settingsAuthAccountDetailPane">
+                        <div class="settingsAuthAccountDetailTitle">${acc.displayName === acc.username ? 'UUID' : 'Email'}</div>
+                        <div class="settingsAuthAccountDetailValue">${acc.displayName === acc.username ? acc.uuid : acc.username}</div>
+                    </div>
+                </div>
+                <div class="settingsAuthAccountActions">
+                    <button class="settingsAuthAccountSelect" ${selectedUUID === acc.uuid ? 'selected>Selected Account &#10004;' : '>Select Account'}</button>
+                    <div class="settingsAuthAccountWrapper">
+                        <button class="settingsAuthAccountLogOut">Log Out</button>
+                    </div>
+                </div>
+            </div>
+        </div>`
+    })
+
+    settingsCurrentAccounts.innerHTML = authAccountStr
+}
+
+function prepareAccountsTab() {
+    populateAuthAccounts()
+    bindAuthAccountSelect()
+    bindAuthAccountLogOut()
+}
+
+/**
+ * Settings preparation functions.
+ */
+
+ /**
+  * Prepare the entire settings UI.
+  */
+function prepareSettings() {
+    setupSettingsTabs()
+    prepareAccountsTab()
+}
+
+// Prepare the settings UI on startup.
+prepareSettings()
\ No newline at end of file
diff --git a/app/assets/js/scripts/uibinder.js b/app/assets/js/scripts/uibinder.js
index 2e1555ca..ada4bd97 100644
--- a/app/assets/js/scripts/uibinder.js
+++ b/app/assets/js/scripts/uibinder.js
@@ -145,6 +145,7 @@ async function validateSelectedAccount(){
             setOverlayHandler(() => {
                 document.getElementById('loginUsername').value = selectedAcc.username
                 validateEmail(selectedAcc.username)
+                loginViewOnSuccess = getCurrentView()
                 switchView(getCurrentView(), VIEWS.login)
                 toggleOverlay(false)
             })
diff --git a/app/login.ejs b/app/login.ejs
index 12a10d15..552e5b83 100644
--- a/app/login.ejs
+++ b/app/login.ejs
@@ -1,4 +1,10 @@
 <div id="loginContainer" style="display: none;">
+    <div id="loginCancelContainer" style="display: none;">
+        <button id="loginCancelButton">
+            <div id="loginCancelIcon">X</div>
+            <span id="loginCancelText">Cancel</span>
+        </button>
+    </div>
     <div id="loginContent">
         <form id="loginForm">
             <img id="loginImageSeal" src="assets/images/WesterosSealCircle.png"/>
diff --git a/app/settings.ejs b/app/settings.ejs
index f0f7c8dc..4d877f56 100644
--- a/app/settings.ejs
+++ b/app/settings.ejs
@@ -20,6 +20,17 @@
                 <span class="settingsTabHeaderText">Account Settings</span>
                 <span class="settingsTabHeaderDesc">Add new accounts or manage existing ones.</span>
             </div>
+            <div id="settingsAddAccountContainer">
+                <button id="settingsAddAccount">
+                    <span id="settingsAddAccountText">&#43; Add Account</span>
+                </button>
+            </div>
+            <div id="settingsCurrentAccountsHeader">
+                <span id="settingsCurrentAccountsHeaderText">Current Accounts</span>
+            </div>
+            <div id="settingsCurrentAccounts">
+                <!-- Auth accounts populated here. -->
+            </div>
         </div>
         <div id="settingsTabMinecraft" class="settingsTab" style="display: none;">
             <div class="settingsTabHeader">