diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 33cf628..c6798e3 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1412,6 +1412,18 @@ "search": { "message": "Search" }, + "search_age_days": { + "message": "{{COUNT}}d ago" + }, + "search_age_hours": { + "message": "{{COUNT}}h ago" + }, + "search_age_minutes": { + "message": "{{COUNT}}m ago" + }, + "search_age_now": { + "message": "just now" + }, "search_building": { "message": "Building search index…" }, @@ -1433,6 +1445,18 @@ "search_hint": { "message": "Start typing to search your modules and assignments. Use @coursename to search a specific course, or @grades to open a course's grades page." }, + "search_index_count": { + "message": "{{COUNT}} items" + }, + "search_index_updated": { + "message": "updated {{AGE}}" + }, + "search_index_working": { + "message": "updating…" + }, + "search_indexing": { + "message": "Indexing your courses…" + }, "search_navigate": { "message": "navigate" }, @@ -1463,6 +1487,15 @@ "search_open": { "message": "open" }, + "search_partial": { + "message": "Still indexing your courses…" + }, + "search_refresh": { + "message": "Refresh" + }, + "search_refresh_title": { + "message": "Rebuild the index from Canvas" + }, "search_placeholder": { "message": "Search modules & assignments… (@coursename filters by course)" }, @@ -1478,6 +1511,9 @@ "search_type_assignment": { "message": "Assignment" }, + "search_type_course": { + "message": "Course" + }, "search_type_discussion": { "message": "Discussion" }, @@ -1487,12 +1523,18 @@ "search_type_file": { "message": "File" }, + "search_type_grades": { + "message": "Grades" + }, "search_type_item": { "message": "Item" }, "search_type_link": { "message": "Link" }, + "search_type_module": { + "message": "Module" + }, "search_type_page": { "message": "Page" }, diff --git a/css/content.css b/css/content.css index 7241945..d27fd76 100644 --- a/css/content.css +++ b/css/content.css @@ -373,6 +373,22 @@ .canvasrefined-gs-type-link, .canvasrefined-gs-type-external-tool { background: #fff4d6; color: #7a5b00; border-color: #f1dfab; } .canvasrefined-gs-type-course { background: #dff3f3; color: #0b6e6e; border-color: #b3dede; } .canvasrefined-gs-type-grades { background: #e8eefc; color: #1d4ed8; border-color: #c3d2f5; } +.canvasrefined-gs-status { + display: flex; align-items: center; gap: 10px; padding: 6px 14px; + border-top: 1px solid var(--bcborders, #e2e6e8); + background: var(--bcbackground-1, #fff); + font-size: 11px; color: var(--bctext-2, #9aa3ab); +} +.canvasrefined-gs-status-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.canvasrefined-gs-refresh { + border: 1px solid var(--bcborders, #c7cdd1); border-radius: 6px; + background: var(--bcbackground-2, #f5f7f8); color: var(--bctext-1, #5b6770); + font: inherit; font-size: 11px; font-weight: 600; padding: 2px 8px; cursor: pointer; + flex-shrink: 0; +} +.canvasrefined-gs-refresh:hover:not(:disabled) { background: var(--bcbackground-2, #ececec); } +.canvasrefined-gs-refresh:disabled { opacity: .55; cursor: default; } +.canvasrefined-gs-partial { color: var(--bctext-2, #9aa3ab); font-size: 12px; } .canvasrefined-gs-footer { display: flex; flex-wrap: wrap; gap: 12px; padding: 8px 14px; font-size: 12px; color: var(--bctext-2, #9aa3ab); border-top: 1px solid var(--bcborders, #e2e6e8); diff --git a/js/content.js b/js/content.js index e5a13e8..0a3c5e4 100644 --- a/js/content.js +++ b/js/content.js @@ -9011,6 +9011,9 @@ function applyAestheticChanges() { // strip Canvas' natural top/left card margins; the grid supplies its own gaps style.textContent += `.ic-DashboardCard__box__container{margin-top:0!important;margin-left:0!important}`; style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{margin-top:0!important;margin-left:0!important}`; + // CanvasRefined's own widgets carry 36px outer margins meant for the flex + // card flow; in the grid every side is spacing the gaps already provide + style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-widget-card,.ic-DashboardCard__box__container > .canvasrefined-dashboard-notes,.ic-DashboardCard__box__container > .canvasrefined-gpa-card,.ic-DashboardCard__box__container > .canvasrefined-gpa{margin:0!important}`; const centerGrid = options.center_cards === true; const gpaOn = options.gpa_calc === true; const gpaPos = gpaCalcPosition(); @@ -9226,18 +9229,47 @@ function injectQuizSafeModeBanner(safeModeOn) { // Global Canvas Search: search every course's modules, items and assignments, then jump // to them. Triggered by the floating button or Ctrl/Cmd+K. -let globalSearchIndex = null; // [{type,title,course,courseId,url}] -let globalSearchIndexPromise = null; // in-flight build so concurrent opens share one fetch -let globalSearchIndexAt = 0; // ms timestamp of last successful build -const GLOBAL_SEARCH_INDEX_TTL = 10 * 60 * 1000; // 10 minutes -const GLOBAL_SEARCH_STORAGE_KEY = "canvasrefined_global_search_index"; +// Cost model: Canvas has no cross-course search API, so the index is assembled from +// /courses -> (/assignments + /modules?include[]=items). Items are returned inline by +// Canvas where it supports it, which keeps a full build at ~2 requests per course +// instead of one per module. The result is cached per host in chrome.storage.local, +// so reopening search costs zero requests; stale courses are topped up a few at a +// time while the tab idles, and "Refresh" forces a full network rebuild. + +const GS_CACHE_VERSION = 2; +const GS_CACHE_KEY_PREFIX = "canvasrefined_gs_cache_"; +const GS_CACHE_MAX_AGE = 14 * 24 * 60 * 60 * 1000; // drop a cache untouched for 2 weeks +const GS_COURSE_TTL = 30 * 60 * 1000; // course content is fresh for 30 min +const GS_COURSELIST_TTL = 15 * 60 * 1000; // enrollment list is fresh for 15 min +const GS_REFRESH_PER_LOAD = 6; // stale courses topped up per page load +const GS_CONCURRENCY = 12; // parallel requests while indexing +const GS_MAX_COURSES = 60; +const GS_MAX_PAGES = 10; // pagination safety net +const GS_BUILD_LOCK = "canvasrefined-gs-build"; + +// Persisted shape (keyed per host): +// { v, updatedAt, courses: { "": { n: name, c: code, at: fetchedAt, +// e: [[kind, title, urlPath], ...] } } } +let gsCache = null; +let gsCacheLoaded = false; +let gsCachePromise = null; +let gsEntries = []; // live flat index, appended per course as it lands +let gsEntriesByCourse = new Map(); // "" -> its entries, so a course can be replaced +let gsSeenUrls = new Set(); // dedupe across courses by normalized URL +let gsBuildPromise = null; +let gsTopUpPromise = null; // background stale-course refresh, if any +let gsBuiltOnce = false; +let gsRequests = 0; // request counter for the debug summary +let gsRateLimitedUntil = 0; // throttle gate shared by all workers +let gsOnIndexUpdated = null; // modal hook: rerun the current query +let _gsPrewarmScheduled = false; let _gsShortcutBound = false; function setupGlobalSearch() { if (options.global_search !== true) return; - invalidateGlobalSearchIndex(); ensureGlobalSearchButton(); ensureGlobalSearchShortcut(); + gsSchedulePrewarm(); } function removeGlobalSearch() { @@ -9406,6 +9438,10 @@ function openGlobalSearchModal() {
+