diff --git a/README.md b/README.md index 9437edf..ef7cb09 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,10 @@ Actually Better Canvas adds more with more to come! - Popup UI revamp - NEW Better todo list - better sidebar +- simplified UI ## Planned Features (by priority) - widgets (music, timer) -- better notes - auto rotate theme + theme history + fix theme submissions - mail assistent + ui revamp - better calender (+ calender sync) @@ -73,8 +73,6 @@ Actually Better Canvas adds more with more to come! - fix darkmode fixer - make sidebar and todo list work on all pages that need them - grade history with graph -- preview font -- button to remove all card images ## Extra features that might be added: - card grade position, card outline @@ -89,7 +87,8 @@ Actually Better Canvas adds more with more to come! - flashcards - goals - Scheduled Reminder Popups -- simplified UI +- preview font +- button to remove all card images and undo ## Community suggestions (maybe will be done at some point) - when opening assignments it will show you "if you get a 0 on this your grade will be _" diff --git a/css/content.css b/css/content.css index 55f34a8..05169d6 100644 --- a/css/content.css +++ b/css/content.css @@ -47,7 +47,20 @@ } .bettercanvas-add-assignment {max-height: 0; overflow:hidden;transition: .3s max-height;} .bettercanvas-custom-open {max-height: 250px} +#better-todo-add-task-menu.bettercanvas-custom-open {max-height: 420px;} .bettercanvas-custom-input {box-sizing: border-box!important; width: 100%!important; height: 32px!important} +#better-todo-new-task-date, +#better-todo-new-task-time { + flex: 1 1 0; + min-width: 0; + width: auto !important; + font-size: 12px; + padding-inline: 6px; +} +#better-todo-new-task-date::-webkit-calendar-picker-indicator { + filter: invert(1) brightness(2); + opacity: .95; +} .bettercanvas-custom-btn {background: #f5f5f5; border: 1px solid #c7cdd1; border-radius:6px; padding: 2px 8px;} .bettercanvas-viewmore-btn {display: block;margin:0 auto;margin-top: 8px;} .bettercanvas-course-percent, .bettercanvas-course-credit {border: 1px solid #ccc; color: var(--ic-brand-font-color-dark)} diff --git a/js/content.js b/js/content.js index fe7b864..3deeeaa 100644 --- a/js/content.js +++ b/js/content.js @@ -1229,6 +1229,241 @@ function updateIndicator(element) { // better todo html betterTodoFilter = "tasks"; let domContainers = {}; + +function formatDateForInput(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +} + +function formatTimeForInput(date) { + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + return `${hours}:${minutes}`; +} + +function buildPlannerNotePayload(form) { + const title = form.querySelector("#better-todo-new-task-title")?.value?.trim(); + const details = form.querySelector("#better-todo-new-task-details")?.value?.trim(); + const courseIdRaw = form.querySelector("#better-todo-new-task-course")?.value; + const dateValue = form.querySelector("#better-todo-new-task-date")?.value; + const timeValue = form.querySelector("#better-todo-new-task-time")?.value; + + if (!title) { + throw new Error("Task title is required."); + } + + if (!dateValue || !timeValue) { + throw new Error("Please choose both a date and time."); + } + + const localDateTime = new Date(`${dateValue}T${timeValue}:00`); + if (Number.isNaN(localDateTime.getTime())) { + throw new Error("Invalid task date."); + } + + return { + title, + details, + courseId: courseIdRaw ? parseInt(courseIdRaw) : null, + // Canvas accepts local timestamp strings more reliably than UTC ISO strings for planner notes. + todoDate: `${dateValue}T${timeValue}:00`, + }; +} + +async function createCanvasPlannerNote(payload) { + const csrfToken = CSRFtoken(); + const plannerNote = { + title: payload.title, + todo_date: payload.todoDate, + }; + if (payload.details) plannerNote.details = payload.details; + if (payload.courseId) plannerNote.course_id = payload.courseId; + + const attempts = [ + { + headers: { + "content-type": "application/json", + "accept": "application/json", + "X-CSRF-Token": csrfToken, + }, + body: JSON.stringify({ planner_note: plannerNote }), + }, + { + headers: { + "content-type": "application/json", + "accept": "application/json", + "X-CSRF-Token": csrfToken, + }, + body: JSON.stringify(plannerNote), + }, + { + headers: { + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + "accept": "application/json", + "X-CSRF-Token": csrfToken, + }, + body: (() => { + const formBody = new URLSearchParams(); + formBody.set("planner_note[title]", plannerNote.title); + formBody.set("planner_note[todo_date]", plannerNote.todo_date); + if (plannerNote.details) formBody.set("planner_note[details]", plannerNote.details); + if (plannerNote.course_id) formBody.set("planner_note[course_id]", plannerNote.course_id); + return formBody.toString(); + })(), + }, + ]; + + let lastError = "Canvas rejected task creation."; + for (const attempt of attempts) { + const response = await fetch(domain + "/api/v1/planner_notes", { + method: "POST", + headers: attempt.headers, + body: attempt.body, + }); + + if (response.status === 200 || response.status === 201) { + return response.json(); + } + + try { + const errData = await response.json(); + if (errData?.errors?.length) { + lastError = errData.errors.join(" "); + } else if (errData?.message) { + lastError = errData.message; + } + } catch (_) { + // Keep prior error text when body is not JSON. + } + } + + throw new Error(lastError || "Canvas rejected task creation."); +} + +function fillTaskCourseOptions(courseSelect) { + const cards = options.custom_cards || {}; + const courseColors = options.custom_cards_3 || {}; + const currentCourseId = getCurrentCourseId(); + const entries = Object.entries(cards) + .map(([id, card]) => ({ + id, + label: card?.default || `Course ${id}`, + color: + courseColors?.[String(id)]?.color ?? + courseColors?.[id]?.color ?? + "#c7cdd1", + })) + .sort((a, b) => a.label.localeCompare(b.label)); + + courseSelect.innerHTML = ''; + courseSelect.options[0].dataset.color = "#c7cdd1"; + entries.forEach(entry => { + const option = makeElement("option", courseSelect, { + value: entry.id, + textContent: entry.label, + }); + option.dataset.color = entry.color; + option.style.color = entry.color; + if (currentCourseId && String(currentCourseId) === String(entry.id)) { + option.selected = true; + } + }); +} + +function updateTaskCourseSelectColor(courseSelect) { + const selectedOption = courseSelect?.options?.[courseSelect.selectedIndex]; + const color = selectedOption?.dataset?.color || "#c7cdd1"; + courseSelect.style.borderLeft = `4px solid ${color}`; + courseSelect.style.paddingLeft = "8px"; +} + +function ensureTodoTaskMenu(location, feedbackElement) { + let actionsRow = location.querySelector("#better-todo-actions-row"); + + if (!actionsRow) { + actionsRow = makeElement("div", location, { + id: "better-todo-actions-row", + style: "display:flex;flex-direction:column;gap:8px;margin-top:14px;", + }); + + const addTaskButton = makeElement("button", actionsRow, { + id: "better-todo-add-task-btn", + className: "bettercanvas-custom-btn", + textContent: "+ Add Task", + style: "width:100%;padding:6px 8px;cursor:pointer;", + }); + + const menu = makeElement("div", actionsRow, { + id: "better-todo-add-task-menu", + className: "bettercanvas-add-assignment", + }); + + menu.innerHTML = ` +
+ + + +
+ + +
+
+ + +
+
+ `; + + const today = new Date(); + menu.querySelector("#better-todo-new-task-date").value = formatDateForInput(today); + menu.querySelector("#better-todo-new-task-time").value = formatTimeForInput(today); + const courseSelect = menu.querySelector("#better-todo-new-task-course"); + fillTaskCourseOptions(courseSelect); + updateTaskCourseSelectColor(courseSelect); + courseSelect.addEventListener("change", () => updateTaskCourseSelectColor(courseSelect)); + + addTaskButton.addEventListener("click", () => { + menu.classList.toggle("bettercanvas-custom-open"); + }); + + menu.querySelector("#better-todo-add-task-submit").addEventListener("click", async () => { + const status = menu.querySelector("#better-todo-add-task-status"); + const submitButton = menu.querySelector("#better-todo-add-task-submit"); + status.textContent = ""; + submitButton.disabled = true; + + try { + const payload = buildPlannerNotePayload(menu); + await createCanvasPlannerNote(payload); + status.textContent = "Task created."; + status.style.color = "#198754"; + menu.querySelector("#better-todo-new-task-title").value = ""; + menu.querySelector("#better-todo-new-task-details").value = ""; + menu.classList.remove("bettercanvas-custom-open"); + + getAssignments(); + clearTodoList(); + createTodoSections(location); + } catch (e) { + status.textContent = e?.message || "Could not create task."; + status.style.color = "#db3754"; + } finally { + submitButton.disabled = false; + } + }); + } + + if (feedbackElement) { + if (actionsRow.nextSibling !== feedbackElement) { + location.insertBefore(actionsRow, feedbackElement); + } + } else if (actionsRow.parentElement !== location) { + location.append(actionsRow); + } +} + async function createTodoSections(location) { if (!location.querySelector("#better-todo-header")) { let header = makeElement("div", location, { id: "better-todo-header" }); @@ -1376,7 +1611,8 @@ async function createTodoSections(location) { populateAssignments(true); } - const feedbackElement = document.querySelector(".recent_feedback"); + const feedbackElement = location.querySelector(".recent_feedback"); + ensureTodoTaskMenu(location, feedbackElement); if (feedbackElement) { if (options.todo_hide_feedback == true) { feedbackElement.style.display = "none"; @@ -1443,7 +1679,17 @@ function clearTodoList() { function populateAssignments(iscompleted = false) { const today = new Date(); today.setHours(0,0,0,0); - let assignments = iscompleted ? completed : assignmentsDue; + let assignments = (iscompleted ? completed : assignmentsDue).slice(); + if (iscompleted) { + assignments.sort((a, b) => { + const aIsGraded = Boolean(a.submissions?.graded); + const bIsGraded = Boolean(b.submissions?.graded); + if (aIsGraded !== bIsGraded) { + return aIsGraded - bIsGraded; + } + return new Date(b.plannable_date) - new Date(a.plannable_date); + }); + } let assignmentCount = 0; const maxElements = options.num_todo_items; @@ -1499,17 +1745,26 @@ function populateAssignments(iscompleted = false) { options.custom_cards_3?.[item.plannable.course_id]?.color ?? "#cccccc"; + const isCustomTask = item.plannable_type == "planner_note" || item.planner_override?.custom === true; + const iconSize = isCustomTask ? 26 : 20; + const iconLeftOffset = isCustomTask ? 2 : 5; + const taskIcon = isCustomTask + ? ` + + ` + : ` + + + + + `; + assignment.style.overflowX = "hidden"; assignment.innerHTML = `
-
- - - - - - +
+ ${taskIcon}
@@ -1617,7 +1872,7 @@ function populateAnnouncements() { function markAs(item, element) { const csrfToken = CSRFtoken(); const completeState = item.planner_override ? !item.planner_override.marked_complete : true; - fetch(domain + "/api/v1/planner/overrides/" + (item.planner_override ? "/" + item.planner_override.id : ""), { + fetch(domain + "/api/v1/planner/overrides" + (item.planner_override ? "/" + item.planner_override.id : ""), { method: item.planner_override ? "PUT" : "POST", headers: { "content-type":"application/json", @@ -1632,7 +1887,7 @@ function markAs(item, element) { }) }) .then(resp => { - if (resp.status == 200 || resp.status == 201) { + if (resp.status == 200 || resp.status == 201 || resp.status == 204) { console.log("marked as complete"); item.planner_override = item.planner_override || {}; item.planner_override.marked_complete = completeState; @@ -2663,6 +2918,7 @@ function loadCardAssignments() { }); return; } + setupCardAssignments(); cardAssignments.then(els => { try { let cards = document.querySelectorAll('.ic-DashboardCard'); @@ -2675,8 +2931,11 @@ function loadCardAssignments() { if (!link) return; let course_id = link.href.split("courses/")[1]; let cardContainer = card.querySelector('.bettercanvas-card-container'); + if (!cardContainer) return; cardContainer.textContent = ""; - cardContainer.parentElement.style.display = "block"; + if (cardContainer.parentElement) { + cardContainer.parentElement.style.display = "block"; + } if (els[course_id]) { els[course_id].forEach(assignment => {