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
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 @@ -52,6 +52,10 @@ const generateManifest = () => {
include_globs: ["https://manaba.tsukuba.ac.jp/ct/course_*_grade"],
js: ["contentScript/showRelativeGradesPosition.js"],
},
{
matches: ["https://manaba.tsukuba.ac.jp/ct/home"],
js: ["contentScript/unsubmittedAssignmentsOnHome.js"],
},
],
commands: {
"manaba-enhanced:open-in-respon": {
Expand Down
129 changes: 129 additions & 0 deletions src/methods/unsubmittedAssignmentsOnHome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import dayjs from "dayjs"
import ReactDOM from "react-dom"

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 = (
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 tableContainer = document.createElement("div")
const showmoreElement = container.querySelector("div.showmore")
container.insertBefore(tableContainer, showmoreElement)

ReactDOM.render(asgTable({ assignments }), tableContainer)
}

const asgTable = ({ assignments }: { assignments: Assignment[] }) => (
<table className="eventlist">
<tbody>
{assignments.map((asg) => (
<tr className="bordertop">
<td className="center eventlist-day">
{dayjs(asg.deadline.toDateString()).format("MM/DD")}
</td>
<td className="event-title">
<span className="eventlist-day">
{dayjs(asg.deadline.toString()).format("HH:mm")}
</span>
<a href={getAssignmentPath(asg)}>
<img
src={assignmentIcon}
alt="assignment icon"
className="inline"
style={{ marginRight: "0.3em" }}
title={asg.type}
/>
{asg.title}
</a>
<a href={getCoursePath(asg)}>[{asg.course}]</a>
</td>
</tr>
))}
</tbody>
</table>
)

const getAssignmentPath = (asg: Assignment) =>
"course_" + asg.id.split("_").slice(1).join("_")

const getCoursePath = (asg: Assignment) => "course_" + asg.id.split("_")[1]

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
9 changes: 1 addition & 8 deletions src/optionsPage/legacyHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,7 @@ export const startLegacyHandler = () => {
).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 @@ -3,6 +3,7 @@
"extends": "@tsconfig/recommended/tsconfig.json",
"compilerOptions": {
"lib": ["DOM", "ES2021"],
"jsx": "react-jsx"
"jsx": "react-jsx",
"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", "optionsPage", "index.tsx"),
},
Expand Down