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"
},
{