Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: display unsubmitted assignments on the manaba home page #561

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions public/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
"message": "Description",
"description": "Description"
},
"test": {
"message": "Test",
"description": "Test"
},
"assignment": {
"message": "Assignment",
"description": "Assignment"
},
"report_template_introduction_section_title": {
"message": "Introduction"
},
Expand Down
8 changes: 8 additions & 0 deletions public/_locales/ja/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
"message": "概要",
"description": "概要"
},
"test": {
"message": "小テスト",
"description": "小テスト"
},
"assignment": {
"message": "レポート",
"description": "レポート"
},
"report_template_introduction_section_title": {
"message": "はじめに"
},
Expand Down
17 changes: 17 additions & 0 deletions public/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,23 @@ <h2>Enabled Features</h2>
<div class="checkbox-style" />
</div>
</li>
<li>
<label
class="checkboxLabel"
for="featuresUnsubmittedAssignmentsOnHome"
>
Display unsubmitted assignments on the manaba home page
</label>
<div class="checkbox">
<input
type="checkbox"
checked
id="featuresUnsubmittedAssignmentsOnHome"
class="checkbox-features"
/>
<div class="checkbox-style" />
</div>
</li>
</ul>
</section>
<section class="section-keys">
Expand Down
7 changes: 6 additions & 1 deletion src/background.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use strict"

import { getStorage, setStorage, onStorageChanged } from "./network/storage"
import { Feature } from "./types/feature"

const removeAttachmentHeader = (
details: chrome.webRequest.WebResponseHeadersDetails
Expand Down Expand Up @@ -65,7 +66,9 @@ chrome.runtime.onInstalled.addListener((details) => {
kind: "sync",
keys: null,
callback: (storage) => {
setStorage({
setStorage<{
[key in Feature]: boolean
}>({
kind: "sync",
items: {
featuresAssignmentsColoring:
Expand All @@ -82,6 +85,8 @@ chrome.runtime.onInstalled.addListener((details) => {
storage.featuresDisableForceFileSaving ?? true,
featuresRelativeGradesPosition:
storage.featuresRelativeGradesPosition ?? false,
featuresUnsubmittedAssignmentsOnHome:
storage.featuresUnsubmittedAssignmentsOnHome ?? true,
},
})

Expand Down
19 changes: 19 additions & 0 deletions src/contentScript/unsubmittedAssignmentsOnHome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
fetchAssignments,
renderUnsubmittedAsgsOnHome,
} from "../methods/unsubmittedAssignmentsOnHome"
import { getStorage } from "../network/storage"

getStorage({
kind: "sync",
keys: "featuresUnsubmittedAssignmentsOnHome",
callback: ({ featuresUnsubmittedAssignmentsOnHome }) => {
if (featuresUnsubmittedAssignmentsOnHome)
fetchAssignments().then(
(assignments) =>
assignments &&
assignments.length &&
renderUnsubmittedAsgsOnHome(assignments, document)
)
},
})
4 changes: 4 additions & 0 deletions src/json/uri.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"homeLibraryQuery": "https://manaba.tsukuba.ac.jp/ct/home_library_query",
"assignmentIcon": "/icon_collist_query.png"
}
4 changes: 4 additions & 0 deletions src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ const generateManifest = () => {
persistent: true,
},
content_scripts: [
{
matches: ["https://manaba.tsukuba.ac.jp/ct/home"],
js: ["contentScript/unsubmittedAssignmentsOnHome.js"],
},
{
matches: ["https://manaba.tsukuba.ac.jp/*"],
run_at: "document_start",
Expand Down
130 changes: 130 additions & 0 deletions src/methods/unsubmittedAssignmentsOnHome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import dayjs from "dayjs"

import { homeLibraryQuery, assignmentIcon } from "../json/uri.json"

class Assignment {
constructor(
public id: string,
public type: string,
public title: string,
public course: string,
public deadline: Date
) {}
}

const asgTableColumn = {
type: 0,
title: 1,
course: 2,
start: 3,
end: 4,
} as const

const targetAsgType = new Set([
chrome.i18n.getMessage("test"),
chrome.i18n.getMessage("assignment"),
])

const parseRawDOM = async (url: string) =>
fetch(url)
.then((res) => res.text())
.then((text) => new DOMParser().parseFromString(text, "text/html"))

const asgPageTable = (asgPageDOM: Document) =>
asgPageDOM.querySelector<HTMLTableElement>(".contentbody-l table")

const getAsgsFromTable = (table: HTMLTableElement) =>
Array.from(table.rows)
.filter((row) => targetAsgType.has(getAsgType(row)))
.filter((row) => getAsgDeadline(row))
.map(
(row) =>
new Assignment(
getAsgID(row),
getAsgType(row),
getAsgTitle(row),
getAsgCourse(row),
getAsgDeadline(row)
)
)

/**
* @example course_XXXXXXX_query_XXXXXXX, course_XXXXXXX_report_XXXXXXX
*/
const getAsgID = (row: HTMLTableRowElement) =>
row.children[asgTableColumn.title]
.querySelector<HTMLAnchorElement>("a")
?.href.split("/")
.pop() || ""

const getAsgType = (row: HTMLTableRowElement) =>
row.children[asgTableColumn.type].textContent?.trim() || ""

const getAsgTitle = (row: HTMLTableRowElement) =>
row.children[asgTableColumn.title].textContent?.trim() || ""

const getAsgCourse = (row: HTMLTableRowElement) =>
row.children[asgTableColumn.course].textContent?.trim() || ""

const getAsgDeadline = (row: HTMLTableRowElement) =>
new Date(row.children[asgTableColumn.end].textContent?.trim() || "")

const fetchAssignments = () =>
parseRawDOM(homeLibraryQuery)
.then((DOM) => asgPageTable(DOM))
.then((table) => table && getAsgsFromTable(table))

const renderUnsubmittedAsgsOnHome = (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function renderUnsubmittedAsgsOnHome has 46 lines of code (exceeds 25 allowed). Consider refactoring.

assignments: Assignment[],
document: Document
) => {
const container = document.querySelector(
"#container > div.pagebody > div.my-course.my-courseV2 > div.contentbody-right > div.my-infolist.my-infolist-event.my-infolist-submitlog > div:nth-child(2)"
)
if (!container) return

const asgTable = document.createElement("table")
asgTable.classList.add("eventlist")
const asgTableBody = document.createElement("tbody")
asgTable.appendChild(asgTableBody)
assignments.forEach((asg) => {
const asgTableHeader = document.createElement("tr")
asgTableHeader.classList.add("bordertop")
const asgTableHeaderDate = document.createElement("td")
asgTableHeaderDate.classList.add("center", "eventlist-day")
asgTableHeaderDate.textContent = dayjs(
asg.deadline.toLocaleDateString()
).format("MM/DD")
asgTableHeader.appendChild(asgTableHeaderDate)
const asgTableHeaderTitle = document.createElement("td")
asgTableHeaderTitle.classList.add("event-title")
const asgTableHeaderTitleTime = document.createElement("span")
asgTableHeaderTitleTime.classList.add("eventlist-day")
asgTableHeaderTitleTime.textContent = dayjs(asg.deadline.toString()).format(
"HH:mm"
)
asgTableHeaderTitle.appendChild(asgTableHeaderTitleTime)
const asgTableHeaderTitleLink = document.createElement("a")
asgTableHeaderTitleLink.href =
"course_" + asg.id.split("_").slice(1).join("_")
asgTableHeaderTitleLink.textContent = asg.title
asgTableHeaderTitle.appendChild(asgTableHeaderTitleLink)
const asgTableHeaderTitleIcon = document.createElement("img")
asgTableHeaderTitleIcon.src = assignmentIcon
asgTableHeaderTitleIcon.alt = "assignment icon"
asgTableHeaderTitleIcon.classList.add("inline")
asgTableHeaderTitleIcon.style.marginRight = "0.3em"
asgTableHeaderTitleIcon.title = asg.type
asgTableHeaderTitleLink.prepend(asgTableHeaderTitleIcon)
const asgTableHeaderTitleCourse = document.createElement("a")
asgTableHeaderTitleCourse.href = "course_" + asg.id.split("_")[1]
asgTableHeaderTitleCourse.textContent = `[${asg.course}]`
asgTableHeaderTitle.appendChild(asgTableHeaderTitleCourse)
asgTableHeader.appendChild(asgTableHeaderTitle)
asgTableBody.appendChild(asgTableHeader)
})
const showmoreElement = container.querySelector("div.showmore")
container.insertBefore(asgTable, showmoreElement)
}

export { fetchAssignments, renderUnsubmittedAsgsOnHome }
39 changes: 19 additions & 20 deletions src/network/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,26 +57,25 @@ export function getStorage<
}
}

export const setStorage = ({
kind,
items,
callback,
}:
| {
kind: Extract<StorageKind, "sync">
items: Partial<StorageSync>
callback?: () => void
}
| {
kind: Extract<StorageKind, "local">
items: Partial<StorageLocal>
callback?: () => void
}): void => {
if (kind === "sync") {
chrome.storage.sync.set(items, callback)
} else {
chrome.storage.local.set(items, callback)
}
export function setStorage<T extends StorageSync | StorageLocal>(params: {
kind: Extract<StorageKind, T extends StorageSync ? "sync" : "local">
items: T
callback?: () => void
}): void
export function setStorage(
params:
| {
kind: Extract<StorageKind, "sync">
items: Partial<StorageSync>
callback?: () => void
}
| {
kind: Extract<StorageKind, "local">
items: Partial<StorageLocal>
callback?: () => void
}
): void {
chrome.storage[params.kind].set(params.items, params.callback)
}

export const onStorageChanged = ({
Expand Down
10 changes: 2 additions & 8 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { ReportTemplateFormHandler } from "./methods/handleReportTemplateForm"
import { getStorage, setStorage } from "./network/storage"
import { Feature } from "./types/feature"
import type { StorageSync } from "./types/storage"

import "./style/options.scss"
Expand Down Expand Up @@ -49,14 +50,7 @@ window.onload = () => {
).map((dom) => {
const key = dom.id as keyof Pick<
StorageSync,
| "featuresAssignmentsColoring"
| "featuresAutoSaveReports"
| "featuresDeadlineHighlighting"
| "featuresRemoveConfirmation"
| "featuresFilterCourses"
| "featuresDragAndDrop"
| "featuresReportTemplate"
| "featuresRelativeGradesPosition"
Exclude<Feature, "featuresDisableForceFileSaving">
>

getStorage({
Expand Down
16 changes: 16 additions & 0 deletions src/types/feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const Feature = [
"featuresAssignmentsColoring",
"featuresAutoSaveReports",
"featuresDeadlineHighlighting",
"featuresRemoveConfirmation",
"featuresFilterCourses",
"featuresDragAndDrop",
"featuresReportTemplate",
"featuresRelativeGradesPosition",
"featuresDisableForceFileSaving",
"featuresUnsubmittedAssignmentsOnHome",
] as const

type Feature = typeof Feature[number]

export { Feature }
22 changes: 8 additions & 14 deletions src/types/storage.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import { Feature } from "./feature"
import type { ModuleCode } from "./filterCources"

export type StorageKind = "sync" | "local"

export type StorageSync = Readonly<{
featuresAssignmentsColoring?: boolean
featuresDeadlineHighlighting?: boolean
featuresAutoSaveReports?: boolean
featuresRemoveConfirmation?: boolean
featuresFilterCourses?: boolean
featuresDragAndDrop: boolean
featuresReportTemplate: boolean
featuresDisableForceFileSaving?: boolean
featuresRelativeGradesPosition?: boolean
filterConfigForModule?: ModuleCode
reportTemplate?: string
reportFilename?: string
}>
export type StorageSync = Readonly<
{ [key in Feature]?: boolean } & {
filterConfigForModule?: ModuleCode
reportTemplate?: string
reportFilename?: string
}
>

export type StorageLocal = Readonly<{
reportText?: {
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"include": ["./src/**/*"],
"extends": "@tsconfig/recommended/tsconfig.json",
"compilerOptions": {
"lib": ["DOM", "ES2021"]
"lib": ["DOM", "ES2021"],
"resolveJsonModule": true
}
}
6 changes: 6 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ module.exports = {
"contentScript",
"showRelativeGradesPosition.ts"
),
"contentScript/unsubmittedAssignmentsOnHome": path.resolve(
__dirname,
"src",
"contentScript",
"unsubmittedAssignmentsOnHome.ts"
),
background: path.resolve(__dirname, "src", "background.ts"),
options: path.resolve(__dirname, "src", "options.ts"),
},
Expand Down