diff --git a/css/all.css b/css/all.css index 67100519..c2d2c9ff 100644 --- a/css/all.css +++ b/css/all.css @@ -1,14 +1,13 @@ -/* -html, -body { - width: 100% !important; - height: 100% !important; - overflow: hidden !important; +:root { + --color-hue: 210; + --primary-color: hsl(var(--color-hue), 50%, 50%); + --primary-light: hsl(var(--color-hue), 70%, 60%); + --primary-dark: hsl(var(--color-hue), 55%, 40%); + --primary-very-dark: hsl(var(--color-hue), 90%, 50%); } -*/ body #header { - background-color: #3c83ce !important; + background-color: var(--primary-color) !important; } .s-enable-course-dashboard.is-home #nav ul #primary-home a, @@ -21,7 +20,7 @@ body #header { #nav #nav_left li.primary-activities:hover, #nav #nav_left li.primary-activities.active, .s-enable-course-dashboard.is-home #nav ul #primary-home:hover { - background-color: #2d659c !important; + background-color: var(--primary-dark) !important; color: white; } @@ -41,14 +40,14 @@ body #sidebar-left .action-links a:hover, .component-add-link:hover, body .search-toggle:hover, body #primary-settings .unfold:hover { - background-color: #2d659c !important; + background-color: var(--primary-dark) !important; color: white !important; } body .search-toggle, body #primary-settings .unfold { - background-color: #3c83ce !important; - border-color: #5fa2e4 !important; + background-color: var(--primary-color) !important; + border-color: var(--primary-light) !important; } body #nav ul #home a { @@ -60,8 +59,8 @@ body .click-submit, body .submit-btn, body .popups-body .submit-span-wrapper, body #nav .s-notifications-mini .requester-links a:hover { - background-color: #3c83ce !important; - border-color: #0f80e9 !important; + background-color: var(--primary-color) !important; + border-color: var(--primary-very-dark) !important; } video.easter-egg { @@ -96,4 +95,100 @@ video.easter-egg { .schoology-plus-icon:hover { opacity: 1.0 !important; +} + +.modal { + display: none; + position: fixed; + z-index: 1; + padding-top: 100px; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + position: relative; + background-color: #fefefe; + margin: auto; + padding: 0; + border: 1px solid #888; + width: 800px; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); + -webkit-animation-name: animatetop; + -webkit-animation-duration: 0.4s; + animation-name: animatetop; + animation-duration: 0.4s +} + +@-webkit-keyframes animatetop { + from { + top: -300px; + opacity: 0 + } + to { + top: 0; + opacity: 1 + } +} + +@keyframes animatetop { + from { + top: -300px; + opacity: 0 + } + to { + top: 0; + opacity: 1 + } +} + +.close { + color: rgba(255, 255, 255, 0.8); + float: right; + font-size: 32px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: white; + text-decoration: none; + cursor: pointer; +} + +.modal-header { + padding: 2px 16px; + background-color: var(--primary-color); + color: white; +} + +.modal-body { + padding: 16px; +} + +.modal-footer { + padding: 2px 16px; + background-color: var(--primary-color); + color: white; +} + +.modal-title { + font-size: 32px; +} + +.setting-entry { + padding-bottom: 4px; +} + +.setting-modified { + color: red +} + +.modal-button { + margin-left: 0 !important; + margin-top: 4px; } \ No newline at end of file diff --git a/js/all.js b/js/all.js index cdd0e9a0..e6647657 100644 --- a/js/all.js +++ b/js/all.js @@ -1,5 +1,14 @@ +// Page Modifications + let svg = ''; +let modalHTML = ''; document.getElementById("home").innerHTML = svg; +document.body.appendChild(document.createElement("div")).innerHTML = modalHTML; + +let modal = document.getElementById("settings-modal"); + +let modalFooterText = document.querySelector(".modal-footer-text"); +modalFooterText.textContent += ` | Schoology Plus v${chrome.runtime.getManifest().version}`; let video = document.body.appendChild(createElement("video", ["easter-egg"], { onended: function () { @@ -12,6 +21,9 @@ let source = createElement("source", [], { type: "video/webm" }); +let modalBody = document.querySelector(".modal-body"); +modalBody.appendChild(getModalContents()); + let sourceSet = false; document.body.onkeydown = (data) => { @@ -33,26 +45,24 @@ document.body.onkeydown = (data) => { document.querySelector(".user-menu").prepend(createElement("li", ["schoology-plus-icon"], undefined, [ createElement("a", ["nav-icon-button"], { href: "#" }, [ - createElement("img", ["icon-unread-requests"], { src: chrome.runtime.getURL("imgs/plus-icon.png"), width: 24 }) + createElement("img", ["icon-unread-requests"], { src: chrome.runtime.getURL("imgs/plus-icon.png"), width: 24, onclick: openOptionsMenu }) ]) ])); -function createElement(tag, classList, properties, children) { - let element = document.createElement(tag); - if (classList) { - for (let c of classList) { - element.classList.add(c); - } - } - if (properties) { - for (let property in properties) { - element[property] = properties[property]; - } +document.querySelector(".close").onclick = modalClose; + +window.onclick = function (event) { + if (event.target == modal) { + modalClose(); } - if (children) { - for (let child of children) { - element.appendChild(child); - } +} + +function modalClose() { + if(anySettingsModified()){ + if(!confirm("You have unsaved settings.\nAre you sure you want to exit?")) return; + updateSettings(); + modalBody.innerHTML = ""; + modalBody.appendChild(getModalContents()); } - return element; + modal.style.display = "none"; } \ No newline at end of file diff --git a/js/background.js b/js/background.js index d9174506..68d56c65 100644 --- a/js/background.js +++ b/js/background.js @@ -51,6 +51,7 @@ function onAlarm(alarm) { div.innerHTML = response.output; let notifications = div.querySelectorAll(".edge-sentence"); let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + let totalAssignments = 0; for (let notification of Array.from(notifications).reverse()) { if (notification.textContent.includes("new grade")) { let assignments = notification.getElementsByTagName("a"); @@ -76,25 +77,30 @@ function onAlarm(alarm) { if (extraTextElement) { count = +extraTextElement.textContent.match(/\d+/)[0]; } - console.warn("New notification!"); + totalAssignments += count + assignments.length; console.dir(notification); - let n = { - type: "basic", - iconUrl: "imgs/icon@128.png", - title: "New grade posted", - message: `${assignments.length + count} new assignment${assignments.length + count === 1 ? " has a grade" : "s have grades"}`, - eventTime: Date.now(), - isClickable: true - }; - console.dir(n); - chrome.browserAction.getBadgeText({}, x => { - let n = Number.parseInt(x); - chrome.browserAction.setBadgeText({ text: (n ? n + assignments.length + count : assignments.length + count).toString() }); - }); - chrome.notifications.create("gradeNotification", n, null); } } } + + if (totalAssignments > 0) { + console.warn("New notification!"); + let n = { + type: "basic", + iconUrl: "imgs/icon@128.png", + title: "New grade posted", + message: `${totalAssignments} new assignment${totalAssignments === 1 ? " has a grade" : "s have grades"}`, + eventTime: Date.now(), + isClickable: true + }; + console.dir(n); + chrome.browserAction.getBadgeText({}, x => { + let num = Number.parseInt(x); + chrome.browserAction.setBadgeText({ text: (num ? num + totalAssignments : totalAssignments).toString() }); + }); + chrome.notifications.create("gradeNotification", n, null); + } + if (timeModified) { chrome.storage.sync.set({ lastTime: time }, () => { console.log("Set new time " + new Date(time)) }); } else { diff --git a/js/preload.js b/js/preload.js new file mode 100644 index 00000000..6c4f9ea8 --- /dev/null +++ b/js/preload.js @@ -0,0 +1,216 @@ +// Process options +updateSettings(); + +// Functions + +var modalContents; +function getModalContents() { + return modalContents; +} + +function openOptionsMenu() { + document.getElementById("settings-modal").style.display = "block"; +} + +let rainbowInterval = undefined; +let rainbowColor = 0; +function colorLoop() { + document.documentElement.style.setProperty("--color-hue", rainbowColor > 359 ? 0 : rainbowColor++); +} + +function rainbowMode(enable) { + if (rainbowInterval) clearInterval(rainbowInterval); + if (enable) rainbowInterval = setInterval(colorLoop, 100); +} + +/** + * Creates a DOM element + * @returns {HTMLElement} A DOM element + * @param {string} tag - The HTML tag name of the type of DOM element to create + * @param {string[]} classList - CSS classes to apply to the DOM element + * @param {Object} properties - Properties to apply to the DOM element + * @param {HTMLElement[]} children - Elements to append as children to the created element + */ +function createElement(tag, classList, properties, children) { + let element = document.createElement(tag); + if (classList) { + for (let c of classList) { + element.classList.add(c); + } + } + if (properties) { + for (let property in properties) { + element[property] = properties[property]; + } + } + if (children) { + for (let child of children) { + element.appendChild(child); + } + } + return element; +} + +let storage = {}; + +function updateSettings() { + chrome.storage.sync.get(null, storageContents => { + storage = storageContents; + + modalContents = createElement("div", ["modal-contents"], undefined, [ + createSetting( + "color", + "Color Hue", + "A HSL hue to be used as the color for the navigation bar (0-359)", + "number", + { min: 0, max: 359, value: 210 }, + (value, element) => { + document.documentElement.style.setProperty("--color-hue", value || value === 0 ? value : 210); + element.value = value; + }, + event => document.documentElement.style.setProperty("--color-hue", event.target.value), + element => Number.parseInt(element.value) + ), + createSetting( + "rainbow", + "Rainbow Mode", + "Slowly cycles through all possible color hues (overrides Color Hue preference)", + "select", + { + options: [ + { + text: "Disabled", + value: false + }, + { + text: "Enabled", + value: true + } + ] + }, + (value, element) => { + rainbowMode(value); + element.value = value; + }, + event => { + if (event.target.value === "true") { + rainbowMode(true); + } else { + rainbowMode(false); + document.documentElement.style.setProperty("--color-hue", storage["color"] || 210); + } + }, + element => element.value === "true" + ), + createElement("span", ["submit-span-wrapper", "modal-button"], { onclick: saveSettings }, [createElement("input", ["form-submit"], { type: "button", value: "Save Settings", id: "save-settings" })]) + ]); + }); +} + +let settings = {}; + +/** + * Creates a setting, appends it to the settings list, and returns a DOM representation of the setting + * @returns {HTMLElement} + * @param {string} name - The name of the setting, to be stored in extension settings + * @param {string} friendlyName - The display name of the setting + * @param {string} description - A description of the setting and appropriate values + * @param {string} type - Setting control type, one of ["number", "text", "button", "select"] + * @param {Object|Object[]} options Additional options, format dependent on setting **type** + * - **number, text, button**: Directly applied as element properties + * - **select**: *options* property on ***options*** object should be an array of objects containing *text* and *value* properties + * @param {function(any,HTMLElement):void} onLoad Called with the setting's current value and the element used to display the setting value when the page is loaded and when the setting is changed + * - *This function should update the setting's display element appropriately so that the setting value is displayed* + * @param {function(any):void} previewCallback Function called when setting value is changed + * - *Should be used to show how changing the setting affects the page if applicable* + * @param {function(HTMLElement):any} saveCallback Function called when setting is saved + * - First argument is the HTML element containing the setting value set by the user + * - Must return the value to be saved to extension settings + * - Will only be called if user saves settings and setting was modified + */ +function createSetting(name, friendlyName, description, type, options, onLoad, previewCallback, saveCallback) { + + let setting = createElement("div", ["setting-entry"]); + let title = createElement("h2", ["setting-title"], { textContent: friendlyName + ": " }); + let helpText = createElement("p", ["setting-description"], { textContent: description }); + + switch (type) { + case "number": + case "text": + case "button": + let inputElement = createElement("input", undefined, Object.assign({ type: type }, options)); + title.appendChild(inputElement); + if (type == "button") inputElement.onclick = settingModified; + else inputElement.oninput = settingModified; + break; + case "select": + let selectElement = createElement("select"); + for (let option of options.options) { + selectElement.appendChild(createElement("option", undefined, { textContent: option.text, value: option.value })); + } + title.appendChild(selectElement); + selectElement.onchange = settingModified; + break; + } + + setting.appendChild(title); + setting.appendChild(helpText); + + title.firstElementChild.dataset.settingName = name; + onLoad(storage[name], title.firstElementChild); + + settings[name] = { + element: title.firstElementChild, + onmodify: previewCallback, + onsave: saveCallback, + onload: onLoad, + modified: false + }; + + return setting; +} + +function settingModified(event) { + let element = event.target || event; + let parent = element.parentElement; + if (parent && !parent.querySelector(".setting-modified")) { + parent.appendChild(createElement("span", ["setting-modified"], { textContent: " *" })); + } + let setting = settings[element.dataset.settingName]; + setting.modified = true; + setting.onmodify(event); +} + +function anySettingsModified() { + for(let setting in settings) { + if(settings[setting].modified) { + return true; + } + } + return false; +} + +function saveSettings() { + let newValues = {}; + for (let setting in settings) { + let v = settings[setting]; + if (v.modified) { + let value = v.onsave(v.element); + newValues[setting] = value; + v.onload(value, v.element); + v.modified = false; + } + } + chrome.storage.sync.set(newValues, () => { + Object.assign(storage, newValues); + for (let element of document.querySelectorAll(".setting-modified")) { + element.parentElement.removeChild(element); + } + }); + + let settingsSaved = document.getElementById("save-settings"); + settingsSaved.value = "Saved!"; + setTimeout(() => { + settingsSaved.value = "Save Settings"; + }, 2000); +} \ No newline at end of file diff --git a/manifest.json b/manifest.json index 9b9db59c..51dcc234 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "Schoology Plus", "description": "Provides some enhancements to your LAUSD Schoology experience", - "version": "2.1.3", + "version": "3.0", "icons": { "128": "imgs/icon@128.png", "64": "imgs/icon@64.png", @@ -43,6 +43,9 @@ "css": [ "css/all.css" ], + "js": [ + "js/preload.js" + ], "run_at": "document_start" }, {