diff --git a/.build/build.py b/.build/build.py index f4c31dfe..42504049 100644 --- a/.build/build.py +++ b/.build/build.py @@ -78,7 +78,9 @@ def copyFileOrDirectory(src, dst): try: os.makedirs(f".build/{target}") except FileExistsError: - pass + print("Deleting existing directory") + shutil.rmtree(f".build/{target}") + os.makedirs(f".build/{target}") for path in os.listdir(): if path not in EXCLUDED_FILES and path not in rules.excluded_files: diff --git a/css/all.css b/css/all.css index b2dfdac4..658056b8 100644 --- a/css/all.css +++ b/css/all.css @@ -501,6 +501,101 @@ the selector matches inside notifications in the new notifs dropdown display: var(--upcoming-assignments-display) !important; } +.quick-access-wrapper { + display: var(--quick-access-display) !important; +} + +.quick-link-wrapper { + padding-top: 5px; +} + +.quick-course-link { + max-width: 250px; + display: inline-block; +} + +.quick-access-course .splus-course-icon { + width: 18px; + height: 18px; + margin-right: 4px; + border: none !important; + display: inline-block; + vertical-align: top; +} + +.quick-link:not(:first-of-type)::before, +.quick-link:not(:first-of-type):hover::before { + content: "•"; + padding: 0 4px; + display: inline-block; +} + +.quick-right-link { + float: right; + text-transform: none !important; + font-weight: normal !important; + font-size: 12px !important; +} + +.quick-access-course .icon { + width: 22px; + height: 22px; + padding-left: 3px; + background-image: url(https://asset-cdn.schoology.com/sites/all/themes/schoology_theme/images/icons_sprite_med.png); + background-repeat: no-repeat; + display: inline-block; + vertical-align: middle; +} + +.quick-access-course .icons-container { + float: right; +} + +.quick-access-course .icon.icon-grades { + background-position: 5px -125px; +} + +.quick-access-course .icon.icon-mastery { + background-position: 5px -1325px; +} + +.quick-access-course .icon.icon-settings { + background-position: 5px -475px; +} + +.quick-access-course .icon.icon-grades:hover { + background-position: 5px -100px; +} + +.quick-access-course .icon.icon-mastery:hover { + background-position: 5px -1300px; +} + +.quick-access-course .icon.icon-settings:hover { + background-position: 5px -450px; +} + +.quick-access-no-courses { + padding-top: 5px; +} + +.quick-access-course { + padding-top: 5px; +} + +.splus-logo-inline { + height: 22px; + filter: brightness(0.4); + vertical-align: middle; + padding-right: 3px; +} + +.courses-kabob-menu { + float: right; + font-size: 20px; + cursor: pointer; +} + body a, body .clickable, body .smart-box .filter-block li a, @@ -534,12 +629,14 @@ body #sidebar-left .action-links a:hover, body .search-toggle:hover, #edit-course-switcher-select-nid-menu .ui-selectmenu-group li a:hover, body #primary-settings .unfold:hover, +ul.tabs li a.active, #nav ul li a:hover { background-color: var(--hover-color) !important; color: white !important; } -body a.link-btn { +body a.link-btn, +ul.tabs li a { color: #333333 !important; } diff --git a/css/grades.css b/css/grades.css index f217597f..81ee1460 100644 --- a/css/grades.css +++ b/css/grades.css @@ -191,6 +191,13 @@ input#enable-modify { display: none; } +.grades-kabob-menu { + font-size: 20px; + vertical-align: sub; + padding-left: 10px; + cursor: pointer; +} + /* undo exception's .no-grade styling for unknown .max-grade elements */ .exception-grade-wrapper .max-grade.no-grade { width: unset !important; diff --git a/js/all-idle.js b/js/all-idle.js index 986301f6..e3dc81df 100644 --- a/js/all-idle.js +++ b/js/all-idle.js @@ -343,4 +343,28 @@ document.documentElement.style.overflow = ""; } }).observe(document.getElementById("body"), { attributes: true, attributeFilter: ["aria-hidden"] }); -})(); \ No newline at end of file +})(); + +function parseSettingsHash() { + let hashes = location.hash.split('#'); + if (hashes.length > 1 && hashes[1] === "splus-settings") { + openModal("settings-modal"); + if (hashes.length > 2) { + setTimeout(() => { + location.hash = hashes[2]; + document.getElementById(hashes[2]).parentElement.parentElement.style.backgroundColor = "lightyellow"; + location.hash = ""; + }, 500); + } + else { + location.hash = ""; + } + } +} + +parseSettingsHash(); + +// Handle opening Schoology Plus Settings +window.addEventListener("hashchange", event => { + parseSettingsHash(); +}); \ No newline at end of file diff --git a/js/all.js b/js/all.js index 19399069..1a83df42 100644 --- a/js/all.js +++ b/js/all.js @@ -358,7 +358,7 @@ document.body.onkeydown = (data) => { video.style.visibility = "visible"; video.currentTime = 0; video.play(); - trackEvent("Easter Egg", "play"); + trackEvent("Easter Egg", "play", "Easter Egg"); } else if (data.altKey && data.key === "b") { openModal("beta-modal"); } diff --git a/js/analytics.js b/js/analytics.js index 85af600b..ec329b2d 100644 --- a/js/analytics.js +++ b/js/analytics.js @@ -1,9 +1,9 @@ /** * Tracks an event using Google Analytics if the user did not opt out * NOTE: The Firefox version of the extension has no support for Google Analytics - * @param {string} target The target of the event - * @param {string} action The action of the event - * @param {string} [label] Used to group related events + * @param {string} target (Event Category) The target of the event + * @param {string} action (Event Action) The action of the event + * @param {string} [label] (Event Label) Used to group related events * @param {number} [value] Numeric value associated with the event */ var trackEvent = function (target, action, label = undefined, value = undefined) { @@ -54,7 +54,7 @@ var trackEvent = function (target, action, label = undefined, value = undefined) function trackClick(event) { let target = event.currentTarget || event.target; - trackEvent(target.dataset.splusTrackingTarget || target.id || "Unlabeled Button", "click", target.dataset.splusTrackingLabel, target.dataset.splusTrackingValue); + trackEvent(target.dataset.splusTrackingTarget || target.id || "Unlabeled Button", "click", target.dataset.splusTrackingLabel || "Tracking Link", target.dataset.splusTrackingValue || event.button); } let trackedElements = new Set(); @@ -62,18 +62,15 @@ var trackEvent = function (target, action, label = undefined, value = undefined) for (let m of mutations) { for (let n of m.addedNodes) { if (n.classList && n.classList.contains("splus-track-clicks") && !trackedElements.has(n)) { + Logger.debug("Added node", n); n.addEventListener("click", trackClick); + n.addEventListener("auxclick", trackClick); trackedElements.add(n); } } } }); - observer.observe(document.body, { - childList: true, - subtree: true - }); - var readyStateCheckInterval = setInterval(function () { if (document.readyState === "complete") { clearInterval(readyStateCheckInterval); @@ -82,9 +79,15 @@ var trackEvent = function (target, action, label = undefined, value = undefined) }, 10); function init() { + observer.observe(document.body, { + childList: true, + subtree: true + }); + for (let elem of document.querySelectorAll(".splus-track-clicks")) { if (!trackedElements.has(elem)) { elem.addEventListener("click", trackClick); + elem.addEventListener("auxclick", trackClick); trackedElements.add(elem); } } diff --git a/js/courses.js b/js/courses.js index 4823d34a..64356432 100644 --- a/js/courses.js +++ b/js/courses.js @@ -4,6 +4,14 @@ for (let course of document.querySelectorAll("li.course-item.list-item")) { parent.replaceChild(wrapper, course); wrapper.appendChild(course); course.prepend(createElement("img", ["course-list-icon"], { src: Theme.getIcon(course.querySelector(".course-title").textContent) || chrome.runtime.getURL("imgs/fallback-course-icon.svg") })); + + let kabobMenuButton = createElement("span", ["courses-kabob-menu"], { + textContent: "⠇", + onclick: function (event) { + $(course).contextMenu({ x: event.pageX, y: event.pageY }); + } + }); + course.querySelector("p.course-info").appendChild(kabobMenuButton); } $.contextMenu({ @@ -12,6 +20,7 @@ $.contextMenu({ options: { name: "Course Options", callback: function (key, opt) { + trackEvent("Course Options", "click", "Courses Context Menu"); openModal("course-settings-modal", { courseId: this[0].querySelector(".section-item").id.match(/\d+/)[0], courseName: `${this[0].querySelector(".course-title").textContent}: ${this[0].querySelector(".section-item").textContent}` diff --git a/js/default-icons-page.js b/js/default-icons-page.js index dd05a383..8e3c4bc4 100644 --- a/js/default-icons-page.js +++ b/js/default-icons-page.js @@ -1,67 +1,74 @@ -const MAX_CHARS = 40; - -let container = document.getElementById("icons-container"); -let textbox = document.getElementById("icon-test-text"); -textbox.addEventListener("input", e => displayFilteredIcons(e.target.value)); -textbox.addEventListener("dblclick", e => (e.target.value = "") || displayFilteredIcons()); -let toggle = document.getElementById("toggle"); -toggle.addEventListener("click", e => toggleCondensed()); -let toggleIcon = document.getElementById("toggle-icon"); -displayFilteredIcons(); -M.Tooltip.init(document.querySelectorAll('.tooltipped'), { outDuration: 0, inDuration: 300, enterDelay: 0, exitDelay: 10, transition: 10 }); - -function toggleCondensed() { - if (document.body.classList.contains("condensed")) { - toggleIcon.textContent = "toggle_off"; - } else { - toggleIcon.textContent = "toggle_on"; - } - document.body.classList.toggle("condensed"); -} - -function createIconPreview(icon, i) { - let div = document.createElement("div"); - div.classList.add("icon-preview", "col", "s6", "m3", "l2", "xl1", "center"); - div.title = `#${i}\n${icon.regex}`; - let img = document.createElement("img"); - img.dataset.index = i; - img.classList.add("col", "s12"); - img.src = icon.url; - img.addEventListener("click", iconClick); - let code = document.createElement("code"); - if (icon.regex.length > MAX_CHARS) { - code.textContent = icon.regex.substr(0, MAX_CHARS - 3) + "..."; - } else { - code.textContent = icon.regex; - } - code.classList.add("col", "s12"); - let a = document.createElement("a"); - a.href = `https://www.flaticon.com/free-icon/${icon.source}`; - a.target = "_blank"; - a.textContent = `#${i}`; - a.classList.add("col", "s12"); - div.appendChild(img); - div.appendChild(a); - div.appendChild(code); - return div; -} - -function displayFilteredIcons(text = "") { - container.innerHTML = ""; - let v = Number.parseInt(text) || 0; - let i = 0; - for (let icon of icons) { - i++; - if (v !== i && text !== "" && !text.match(new RegExp(icon.regex, "i"))) continue; - container.appendChild(createIconPreview(icon, i)); - } -} - -function iconClick(e) { - let indx = +e.target.dataset.index; - textbox.value = indx; - displayFilteredIcons(textbox.value); - if (document.body.classList.contains("condensed")) { - toggleCondensed(); - } +const MAX_CHARS = 40; + +let container = document.getElementById("icons-container"); +let textbox = document.getElementById("icon-test-text"); +textbox.addEventListener("input", e => displayFilteredIcons(e.target.value)); +textbox.addEventListener("dblclick", e => (e.target.value = "") || displayFilteredIcons()); +let toggle = document.getElementById("toggle"); +toggle.addEventListener("click", e => toggleCondensed()); +let toggleIcon = document.getElementById("toggle-icon"); +displayFilteredIcons(); +M.Tooltip.init(document.querySelectorAll('.tooltipped'), { outDuration: 0, inDuration: 300, enterDelay: 0, exitDelay: 10, transition: 10 }); + +function toggleCondensed() { + if (document.body.classList.contains("condensed")) { + toggleIcon.textContent = "toggle_off"; + } else { + toggleIcon.textContent = "toggle_on"; + } + document.body.classList.toggle("condensed"); +} + +function createIconPreview(icon, i) { + let div = document.createElement("div"); + div.classList.add("icon-preview", "col", "s6", "m3", "l2", "xl1", "center"); + div.title = `#${i}\n${icon.regex}`; + let img = document.createElement("img"); + img.dataset.index = i; + img.classList.add("col", "s12"); + img.src = icon.url; + img.addEventListener("click", iconClick); + let code = document.createElement("code"); + if (icon.regex.length > MAX_CHARS) { + code.textContent = icon.regex.substr(0, MAX_CHARS - 3) + "..."; + } else { + code.textContent = icon.regex; + } + code.classList.add("col", "s12"); + let a = document.createElement("a"); + a.href = `https://www.flaticon.com/free-icon/${icon.source}`; + a.target = "_blank"; + a.textContent = `#${i}`; + a.classList.add("col", "s12"); + div.appendChild(img); + div.appendChild(a); + div.appendChild(code); + return div; +} + +function displayFilteredIcons(text = "") { + container.innerHTML = ""; + + let m = text.match(/#(\d+)/); + m = m ? m[1] : false; + if (m && m <= icons.length) { + container.appendChild(createIconPreview(icons[m - 1], m)); + return; + } + + let i = 0; + for (let icon of icons) { + i++; + if (text !== "" && !text.match(new RegExp(icon.regex, "i"))) continue; + container.appendChild(createIconPreview(icon, i)); + } +} + +function iconClick(e) { + let indx = +e.target.dataset.index; + textbox.value = `#${indx}`; + displayFilteredIcons(textbox.value); + if (document.body.classList.contains("condensed")) { + toggleCondensed(); + } } \ No newline at end of file diff --git a/js/grades.js b/js/grades.js index 6985c812..306c6bc9 100644 --- a/js/grades.js +++ b/js/grades.js @@ -18,6 +18,17 @@ $.contextMenu({ options: { name: "Course Options", callback: function (key, opt) { + trackEvent("Course Options", "click", "Grades Context Menu"); + openModal("course-settings-modal", { + courseId: this[0].parentElement.id.match(/\d+/)[0], + courseName: this[0].querySelector("a span:nth-child(3)") ? this[0].querySelector("a span:nth-child(2)").textContent : this[0].innerText.split('\n')[0] + }); + } + }, + grades: { + name: "Change Grading Scale", + callback: function (key, opt) { + trackEvent("Change Grading Scale", "click", "Grades Context Menu"); openModal("course-settings-modal", { courseId: this[0].parentElement.id.match(/\d+/)[0], courseName: this[0].querySelector("a span:nth-child(3)") ? this[0].querySelector("a span:nth-child(2)").textContent : this[0].innerText.split('\n')[0] @@ -106,7 +117,16 @@ var fetchQueue = []; createElement("col", ["comments-column"]) ])); + let kabobMenuButton = createElement("span", ["grades-kabob-menu"], { + textContent: "⠇", + onclick: function (event) { + $(title).contextMenu({ x: event.pageX, y: event.pageY }); + // hacky way to prevent the course from expanding + title.click(); + } + }); let grade = createElement("span", ["awarded-grade", "injected-title-grade", courseGrade ? "grade-active-color" : "grade-none-color"], { textContent: "LOADING" }); + title.appendChild(kabobMenuButton); title.appendChild(grade); let invalidatePerTotal = false; diff --git a/js/home.js b/js/home.js index 7c840a52..3433540e 100644 --- a/js/home.js +++ b/js/home.js @@ -2,6 +2,7 @@ let homeFeedContainer = document.getElementById("home-feed-container"); let feed = homeFeedContainer.querySelector(".feed .item-list .s-edge-feed"); +let rightCol = document.getElementById("right-column-inner"); /** * Creates a post from a broadcast @@ -75,6 +76,73 @@ function formatDateAsString(date) { return `${date.toLocaleString("en-US", { weekday: "short" })} ${date.toLocaleString("en-US", { year: "numeric", month: "long", day: "numeric" })} at ${date.toLocaleString("en-US", { hour: "numeric", minute: "2-digit" }).toLowerCase()}`; } +async function createQuickAccess() { + let linkWrap; + + let wrapper = createElement("div", ["quick-access-wrapper"], {}, [ + createElement("h3", ["h3-med"], {}, [ + createElement("img", ["splus-logo-inline"], { src: chrome.runtime.getURL("imgs/plus-icon.png"), title: "Provided by Schoology Plus" }), + createElement("span", [], { textContent: "Quick Access" }), + createElement("a", ["quick-right-link"], { textContent: "Settings", href: "#splus-settings#setting-input-quickAccessVisibility" }) + ]), + createElement("div", ["date-header", "first"], {}, [ + createElement("h4", [], { textContent: "Pages" }) + ]), + (linkWrap = createElement("div", ["quick-link-wrapper"])) + ]); + + const PAGES = [ + { textContent: "Grade Report", href: "/grades/grades", id: "quick-access-grades" }, + { textContent: "Courses", href: "/courses", id: "quick-access-courses" }, + { textContent: "Mastery", href: "/mastery", id: "quick-access-mastery" }, + { textContent: "Groups", href: "/groups", id: "quick-access-groups" }, + { textContent: "Messages", href: "/messages", id: "quick-access-messages" }, + ]; + + for (let page of PAGES) { + let a = linkWrap.appendChild(createElement("a", ["quick-link", "splus-track-clicks"], page)); + a.dataset.splusTrackingLabel = "Quick Access"; + } + + wrapper.appendChild( + createElement("div", ["date-header"], {}, [ + createElement("h4", [], {}, [ + createElement("span", [], { textContent: "Courses" }), + createElement("a", ["quick-right-link"], { textContent: "Reorder", href: "/courses?reorder" }) + ]) + ]) + ); + + let sectionsList = (await fetchApiJson(`users/${getUserId()}/sections`)).section; + + if (!sectionsList || sectionsList.length == 0) { + wrapper.appendChild(createElement("p", ["quick-access-no-courses"], { textContent: "No courses found" })); + } else { + let courseOptionsButton; + let iconImage; + for (let section of sectionsList) { + wrapper.appendChild(createElement("div", ["quick-access-course"], {}, [ + (iconImage = createElement("div", ["splus-course-icon"], { dataset: { courseTitle: `${section.course_title}: ${section.section_title}` } })), + createElement("a", ["splus-track-clicks", "quick-course-link"], { textContent: `${section.course_title}: ${section.section_title}`, href: `/course/${section.id}`, dataset: { splusTrackingTarget: "quick-access-course-link", splusTrackingLabel: "Quick Access" } }), + createElement("div", ["icons-container"], {}, [ + createElement("a", ["icon", "icon-grades", "splus-track-clicks"], { href: `/course/${section.id}/student_grades`, title: "Grades", dataset: { splusTrackingTarget: "quick-access-grades-link", splusTrackingLabel: "Quick Access" } }), + createElement("a", ["icon", "icon-mastery", "splus-track-clicks"], { href: `/course/${section.id}/student_mastery`, title: "Mastery", dataset: { splusTrackingTarget: "quick-access-mastery-link", splusTrackingLabel: "Quick Access" } }), + (courseOptionsButton = createElement("a", ["icon", "icon-settings", "splus-track-clicks"], { href: "#", dataset: { splusTrackingTarget: "quick-access-settings-link", splusTrackingLabel: "Quick Access" } })) + ]) + ])); + + iconImage.style.backgroundImage = `url(${chrome.runtime.getURL("imgs/fallback-course-icon.svg")})`; + + courseOptionsButton.addEventListener("click", () => openModal("course-settings-modal", { + courseId: section.id, + courseName: `${section.course_title}: ${section.section_title}` + })); + } + } + + rightCol.prepend(wrapper); +} + if (Setting.getValue("broadcasts") !== "disabled") { (function () { let observer = new MutationObserver(function (mutations) { @@ -99,4 +167,6 @@ if (Setting.getValue("broadcasts") !== "disabled") { attributeFilter: ["style"] }); })(); -} \ No newline at end of file +} + +createQuickAccess(); \ No newline at end of file diff --git a/js/preload.js b/js/preload.js index 697f7373..45a4d9eb 100644 --- a/js/preload.js +++ b/js/preload.js @@ -612,6 +612,31 @@ function updateSettings(callback) { undefined, element => element.value ).control, + new Setting( + "quickAccessVisibility", + "Quick Access", + "Enables or disables the quick access panel on the home page", + "enabled", + "select", + { + options: [ + { + text: "Enabled", + value: "enabled" + }, + { + text: "Disabled", + value: "disabled" + } + ] + }, + value => { + setCSSVariable("quick-access-display", value === "disabled" ? "none" : "block"); + return value; + }, + function (event) { this.onload(event.target.value) }, + element => element.value + ).control, new Setting( "upcomingOverdueVisibility", "Hide Upcoming and Overdue Assignments", diff --git a/js/theme.js b/js/theme.js index 6aef578b..7ac04864 100644 --- a/js/theme.js +++ b/js/theme.js @@ -261,7 +261,7 @@ class Theme { pictures = pictures.concat(courseImgs); } - let arrows = Array.from(document.querySelectorAll(".gradebook-course-title .arrow")); + let arrows = Array.from(document.querySelectorAll(".gradebook-course-title .arrow, .splus-course-icon")); for (let arrow of arrows) { arrow.themedIconMode = "gradesPageArrow"; @@ -299,7 +299,7 @@ class Theme { arrow.classList.add("icon-modified"); // fallbacks don't work in CSS // implement our own thing for it, based on img and onerror - let sourceUrl = Theme.getIcon(arrow.courseTitle || arrow.parentElement.textContent); + let sourceUrl = Theme.getIcon(arrow.dataset.courseTitle || arrow.courseTitle || arrow.parentElement.textContent); let fallbackUrl = chrome.runtime.getURL("imgs/fallback-course-icon.svg"); let matches = sourceUrl && sourceUrl.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i); let domain = matches && matches[1]; @@ -367,8 +367,8 @@ class Theme { } // ***** Tracking courses missing icons - if ((!missingIconsLastCheck || beforeThisSemester(missingIconsLastCheck)) && !Theme.hasBuiltInIcon(arrow.courseTitle || arrow.parentElement.textContent)) { - coursesMissingDefaultIcons.add(arrow.courseTitle || arrow.parentElement.textContent); + if ((!missingIconsLastCheck || beforeThisSemester(missingIconsLastCheck)) && !Theme.hasBuiltInIcon(arrow.dataset.courseTitle || arrow.courseTitle || arrow.parentElement.textContent)) { + coursesMissingDefaultIcons.add(arrow.dataset.courseTitle || arrow.courseTitle || arrow.parentElement.textContent); } // ***** } diff --git a/manifest.json b/manifest.json index 4a40c5ae..37297703 100644 --- a/manifest.json +++ b/manifest.json @@ -8,7 +8,7 @@ "id": "schoology.plus@aopell.me" } }, - "version": "6.2.2", + "version": "6.3", "icons": { "128": "imgs/icon@128.png", "64": "imgs/icon@64.png", @@ -151,6 +151,7 @@ "https://*.schoology.com/home/recent-activity*" ], "js": [ + "js/course.js", "js/home.js" ], "run_at": "document_end" diff --git a/theme-editor.html b/theme-editor.html index 5ba83424..fce13c2d 100644 --- a/theme-editor.html +++ b/theme-editor.html @@ -29,6 +29,7 @@