import QtQuick 2.15 import QtQuick.Layouts 1.15 import QtQuick.Controls 2.15 import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents3 import org.kde.plasma.extras 2.0 as PlasmaExtras Item { width: PlasmaCore.Units.gridUnit * 20 height: PlasmaCore.Units.gridUnit * 40 clip: true readonly property string canvasUrl: plasmoid.configuration.canvasUrl readonly property string apiEndpoint: `${canvasUrl.replace(/\/$/, "")}/api/v1` readonly property string oauth2Token: plasmoid.configuration.oauth2Token readonly property string authHeader: `Bearer ${oauth2Token}` function callApi(path, perPage, callback) { let xhr = new XMLHttpRequest() let apiUrl = `${apiEndpoint}${path}` if (perPage >= 1) { // add pagination parameter apiUrl += `${path.includes("?") ? "&" : "?"}per_page=${perPage}` } xhr.open("GET", apiUrl) xhr.setRequestHeader("Authorization", authHeader) xhr.onload = () => { if (xhr.status == 200) { try { let json = JSON.parse(xhr.responseText) if (callback) { callback(json) } } catch (e) { if (e instanceof SyntaxError) { console.error(`Cannot parse response for ${path} as JSON:\n${xhr.responseText}`) } else { throw e } } } else { console.error(`XHR failed when retrieving ${path} (status ${xhr.status}):\n${xhr.responseText}`) } } xhr.send() } function syncCanvas() { const courses = plasmoid.configuration.courses.split("\n").map( // each line in the "courses" config consists of // a numeric course id, a space, and a course code line => { const spaceIndex = line.indexOf(" ") return {id: line.slice(0, spaceIndex), code: line.slice(spaceIndex + 1)} } ) const showSubmittedAssignments = plasmoid.configuration.showSubmittedAssignments // we need user id to check submission status callApi("/users/self", 0, user => { syncCourses(courses, showSubmittedAssignments, user.id) }) } function syncCourses(courses, showSubmittedAssignments, userId) { announcementsModel.clear() assignmentsModel.clear() let importantCount = {announcements: 0, assignments: 0} for (let course of courses) { callApi(`/announcements?context_codes[]=course_${course.id}`, 10, announcements => { announcements.forEach(announcement => { const info = { type: "announcement", activityId: announcement.id, course: course.code, title: announcement.title, url: announcement.html_url, important: plasmoid.configuration.importantAnnouncements.includes(announcement.id.toString()), finished: plasmoid.configuration.finishedAnnouncements.includes(announcement.id.toString()), } if (info.important) { announcementsModel.insert(importantCount.announcements++, info) // place above unimportant ones } else { announcementsModel.append(info) } }) }) callApi(`/courses/${course.id}/assignments`, 10, assignments => { assignments.forEach(assignment => { callApi(`/courses/${course.id}/assignments/${assignment.id}/submissions/${userId}`, 1, submission => { const info = { type: "assignment", activityId: assignment.id, course: course.code, title: assignment.name, dueAt: assignment.due_at || "", // if null, use empty string to suppress errors submitted: submission.workflow_state != "unsubmitted", // "graded" counts as submitted url: assignment.html_url, important: plasmoid.configuration.importantAssignments.includes(assignment.id.toString()), finished: plasmoid.configuration.finishedAssignments.includes(assignment.id.toString()), } if (!info.submitted || showSubmittedAssignments) { if (info.important) { assignmentsModel.insert(importantCount.assignments++, info) } else { assignmentsModel.append(info) } } }) }) }) } } // sync on initialization Component.onCompleted: syncCanvas() // update every refreshInterval minutes Timer { interval: plasmoid.configuration.refreshInterval * 60 * 1000 running: true repeat: true onTriggered: syncCanvas() } // top level layout ColumnLayout { id: main anchors.fill: parent PlasmaExtras.Heading { level: 1 text: "Kanvas" } PlasmaExtras.Heading { level: 2 text: "Announcements" } ListModel { id: announcementsModel /* Uncomment when debugging layout ListElement { type: "announcement" course: "CS101" title: "Code quality" url: "https://xkcd.com/1513" important: true finished: false activityId: 0 } */ } ScrollView { implicitHeight: PlasmaCore.Units.gridUnit * 20 Layout.margins: PlasmaCore.Units.smallSpacing Layout.fillWidth: true Layout.fillHeight: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ListView { id: announcementsListView Layout.fillWidth: true spacing: PlasmaCore.Units.smallSpacing delegate: ActivityView {} model: announcementsModel } } PlasmaExtras.Heading { level: 2 text: "Assignments" } ListModel { id: assignmentsModel /* Uncomment when debugging layout ListElement { type: "assignment" course: "EE201" title: "Circuit diagram" dueAt: "2022-04-10T15:59:59Z" submitted: true url: "https://xkcd.com/730" important: true finished: true activityId: 1 } */ } ScrollView { implicitHeight: PlasmaCore.Units.gridUnit * 20 Layout.margins: PlasmaCore.Units.smallSpacing Layout.fillWidth: true Layout.fillHeight: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ListView { id: assignmentsListView Layout.fillWidth: true spacing: PlasmaCore.Units.gridUnit delegate: ActivityView {} model: assignmentsModel } } PlasmaComponents3.Button { icon.name: "view-refresh" text: i18n("Refresh") onClicked: syncCanvas() } } }