├── .env.development ├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── CHANGELOG_ZH.md ├── LICENSE ├── README.md ├── biome.json ├── design └── img │ ├── 512.png │ ├── bg.ai │ └── bg.svg ├── entrypoints ├── about.html ├── background.ts └── index.html ├── lingui.config.ts ├── package.json ├── pnpm-lock.yaml ├── public ├── _locales │ ├── en_US │ │ └── messages.json │ └── zh_CN │ │ └── messages.json └── icons │ ├── 128.png │ ├── 16.png │ ├── 19.png │ └── 48.png ├── src ├── about.tsx ├── components │ ├── CardList.tsx │ ├── ContentCard.tsx │ ├── ContentDetail.tsx │ ├── CourseList.tsx │ ├── IframeWrapper.tsx │ ├── SettingList.tsx │ ├── SummaryList.tsx │ └── dialogs │ │ ├── ChangeSemesterDialog.tsx │ │ ├── ClearDataDialog.tsx │ │ ├── LoginDialog.tsx │ │ ├── LogoutDialog.tsx │ │ ├── NetworkErrorDialog.tsx │ │ ├── NewSemesterDialog.tsx │ │ └── index.ts ├── constants │ ├── index.ts │ └── ui.tsx ├── css │ ├── card.module.css │ ├── doc.module.css │ ├── list.module.css │ ├── main.module.css │ ├── page.module.css │ └── scrollbar.css ├── i18n.ts ├── image │ ├── bg.png │ ├── bg_dark.png │ ├── creampaper.png │ ├── creampaper_black.png │ ├── file.png │ ├── homework_expired.png │ ├── homework_submitted.png │ ├── new_item.png │ ├── settings_1.png │ ├── settings_2.png │ ├── starred_item.png │ ├── switch_course.png │ ├── switch_filter.png │ └── title_filter.png ├── init.ts ├── locales │ ├── en.po │ └── zh.po ├── main.tsx ├── pages │ ├── _app.tsx │ ├── content.tsx │ ├── doc │ │ ├── _doc.tsx │ │ ├── about │ │ │ ├── _en.mdx │ │ │ ├── _zh.mdx │ │ │ └── index.tsx │ │ ├── changelog.tsx │ │ └── readme │ │ │ ├── _en.mdx │ │ │ ├── _zh.mdx │ │ │ └── index.tsx │ ├── settings.tsx │ ├── web.tsx │ └── welcome.tsx ├── redux │ ├── actions.ts │ ├── hooks.ts │ ├── reducers │ │ ├── data.ts │ │ ├── helper.ts │ │ └── ui.ts │ ├── selectors.ts │ └── store.ts ├── theme.ts ├── types │ ├── data.ts │ └── ui.ts ├── utils │ ├── console.ts │ ├── crypto.ts │ ├── csrf.ts │ ├── download.ts │ ├── format.ts │ ├── permission.ts │ └── storage.ts └── vite-env.d.ts ├── tsconfig.json └── wxt.config.ts /.env.development: -------------------------------------------------------------------------------- 1 | # username & password for auto login when developing locally 2 | # copy this file to .env.development.local 3 | VITE_USERNAME= 4 | VITE_PASSWORD= -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Install pnpm 12 | uses: pnpm/action-setup@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 'lts/*' 16 | cache: 'pnpm' 17 | - name: Install dependencies 18 | run: pnpm install 19 | - name: Build Chrome version 20 | run: pnpm zip:chrome 21 | - name: Upload Chrome version 22 | uses: actions/upload-artifact@v4 23 | with: 24 | name: chrome 25 | path: | 26 | dist/chrome-mv3/ 27 | stats.html 28 | - name: Build Firefox version 29 | run: pnpm zip:firefox 30 | - name: Upload Firefox version 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: firefox 34 | path: | 35 | dist/firefox-mv3/ 36 | stats.html 37 | - name: Upload Zip files 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: zip-files 41 | path: | 42 | dist/*.zip 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist/ 12 | dist-zip/ 13 | stats.html 14 | stats-*.json 15 | .wxt 16 | web-ext.config.ts 17 | *.local 18 | .swc 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.7.0 4 | 5 | - FIX The "Load more" button is invisible in dark mode 6 | - ADD Introduce basic page routing, allow the use of the browser's forward and backward buttons to switch content, and retaining the previous content when refreshing or reopening the page 7 | - FIX Prevent multiple page openings to avoid data conflicts ([#160](https://github.com/Harry-Chen/Learn-Helper/issues/160)) 8 | - FIX The dropdown symbol is not aligned when the course name is too long ([#165](https://github.com/Harry-Chen/Learn-Helper/issues/165)) 9 | - FIX Escape issue with the display of homeworks content ([#167](https://github.com/Harry-Chen/Learn-Helper/issues/167)) 10 | - FIX The "Submission content" of homeworks is always displayed 11 | - ADD Use [WXT](https://wxt.dev/) as the development framework 12 | - ADD Upgrade thu-learn-lib to v4.1.0 and add support for the following: 13 | - Deadline for late submission, completion types and submission types of homeworks 14 | - Expiration time of notifications 15 | - File category (currently only supports displaying the category of files. Filtering files by category will be implemented in future versions) 16 | 17 | ## 4.6.2 18 | 19 | - FIX empty course time and location ([#145](https://github.com/Harry-Chen/Learn-Helper/issues/145)) 20 | - FIX only inject csrf token in extension tab 21 | - ADD sort homework by submission status ([#148](https://github.com/Harry-Chen/Learn-Helper/issues/148)) 22 | 23 | ## 4.6.1 24 | 25 | - Change i18n backend to [Lingui](https://lingui.dev/) and support language switching ([#134](https://github.com/Harry-Chen/Learn-Helper/issues/134)) 26 | - FIX csrf injection ([#135](https://github.com/Harry-Chen/Learn-Helper/issues/135)) 27 | - Try to FIX PKU course compatibility ([#139](https://github.com/Harry-Chen/Learn-Helper/issues/139)) 28 | - FIX open in new window ([#140](https://github.com/Harry-Chen/Learn-Helper/issues/140)) 29 | - FIX card list scroll height reset after open item ([AsakuraMizu/Learn-Helper#2](https://github.com/AsakuraMizu/Learn-Helper/issues/2)) 30 | - FIX card filter not reset after switching semester ([AsakuraMizu/Learn-Helper#3](https://github.com/AsakuraMizu/Learn-Helper/issues/3)) 31 | - Remove fontawesome and use [unplugin-icons](https://github.com/unplugin/unplugin-icons) to render icons, reduce package size 32 | 33 | ## v4.6.0 34 | 35 | (Contributed by @AsakuraMizu as part of OSPP'2023 project) 36 | 37 | - REFACTOR React components as functional component and Redux logic with Redux Toolkit, change build toolchain from Webpack to Vite 38 | - MIGRATE to Manifest V3 39 | - ADD dark mode and switcher 40 | - ADD i18n and partial English translations Due to technical restrictions language can only follow browser config 41 | - Upgrade thu-learn-lib to v3.1.0 42 | - FIX snackbar conflict 43 | 44 | ## v4.5.1 45 | 46 | - Upgrade to latest dependencies (TypeScript 4.6.1, MUI 5.5.2) 47 | 48 | ## v4.5.0 49 | 50 | Note: User data (except configuration) will be **automatically cleared** after upgrade to `v4.5.0` from any previous version due to breaking changes of `thu-learn-lib`. 51 | 52 | - FIX problem that notifications might be (occasionally) changed to incorrect publish time (Harry-Chen/thu-learn-lib#36) 53 | - ADD preview frame to homework and notifications (if applicable) 54 | - ADD login status check before triggering bulk download to avoid garbage artifacts 55 | - Upgrade to latest dependencies 56 | 57 | ## v4.4.3 58 | 59 | - FIX problem when opening notification or submitting homework from detail panel (#103) 60 | - FIX problem when certain course has disabled some functionalities of Web Learning (#104) 61 | 62 | ## v4.4.2 63 | 64 | - FIX login error (update to `thu-learn-helper` 2.5.0 to support CSRF token) 65 | 66 | ## v4.4.1 67 | 68 | - FIX possible missing current semester in semester choice dialog 69 | - Upgrade dependencies to mitigate security issues 70 | 71 | ## v4.4.0 72 | 73 | - FIX error when reading data from disabled functionalities (#90) 74 | - ADD more version info on welcome page 75 | - Deploy self-host version of FireFox Addons (see https://harrychen.xyz/learn/) 76 | - Upgrade to TypeScript v4.1, Webpack v5.15, React v17, Material-UI v4.11 77 | 78 | ## v4.3.1 79 | 80 | - FIX login error when not saving credentials 81 | - FIX error on first use after upgrade 82 | - ADD refresh button on error page to avoid unnecessary data cleaning 83 | - FIX iframe same-origin problem (probably a Chrome bug) (by Starrah) 84 | - FIX alignment of icons on content cards (bt Starrah) 85 | 86 | ## v4.3.0 87 | 88 | - ADD dialog to change current semester 89 | - ADD link for file preview in detail page (workaround for a Chrome bug) 90 | - FIX sorting function of cards, showing undue homework in deadline order 91 | 92 | ## v4.2.1 93 | 94 | - ADD preview window to the detail page of files 95 | - REMOVE permission of learn2018.tsinghua.edu.cn 96 | - Rewrite welcome page using modern CSS 97 | 98 | ## v4.2.0 99 | 100 | - ADD switch to `preact` and adjust bundle configuration to reduce code size (but also increase the minimal browser version) 101 | - ADD more information in error recovery page 102 | - FIX crash after dropping a course (again) 103 | 104 | ## v4.1.7 105 | 106 | - FIX wrong UI logic when login failed 107 | - ADD more specific error recovery message 108 | 109 | ## v4.1.6 110 | 111 | - FIX inability to login after login failed 112 | - FIX crash triggered by wrong badge counting 113 | 114 | ## v4.1.5 115 | 116 | - FIX scrolling problem of sidebar 117 | 118 | ## v4.1.4 119 | 120 | - FIX crash after dropping courses 121 | - FIX wrong order of undue homework in card list 122 | - FIX ignored homework counting in badges 123 | 124 | ## v4.1.3 125 | 126 | - FIX wrong logic in version migration which clears data at each start 127 | 128 | ## v4.1.2 129 | 130 | - FIX error when loading content from disabled modules of courses 131 | - ADD general handler to clear all data when fatal error occurs 132 | 133 | ## v4.1.1 134 | 135 | - FIX wrong encoding of notifications (caused by `thu-learn-lib`) 136 | 137 | ## v4.1.0 138 | 139 | - ADD conversion to non-numerical grades (A+/B/C/...) 140 | - ADD ignoring of single item, refine the logic of course module hiding 141 | - ADD bulk downloading of unread files 142 | - ADD detailed time in detail pane 143 | - FIX state storing problem in FireFox 144 | - FIX homework not marked as unread after being graded 145 | - FIX garbled text shown in detail pane when notification has empty body 146 | - FIX wrong notice when certain module of course is disabled 147 | 148 | ## v4.0.3 149 | 150 | - FIX bugs in `ContentDetail` and `ContentIgnoreSettings` 151 | - Avoid opening too many instances when clicking on extension icon 152 | - Switch to WebExtension API for file downloading 153 | - Release Firefox version! 154 | 155 | ## v4.0.2 156 | 157 | - FIX whitespace warping problem in detail page 158 | - ADD timeout judgement for login process 159 | - ADD detail page for files 160 | 161 | ## v4.0.1 162 | 163 | - FIX url error in attachment of notification 164 | - FIX too wide detail pane on narrow-screen devices 165 | 166 | ## v4.0.0 167 | 168 | - Rewrite use React (with Material-UI) + Redux 169 | - ADD support for learn2018 170 | - REMOVE support for all other versions 171 | - ADD chrome badge to remind unread message count 172 | - ADD card title filter 173 | 174 | ## v3.3.1 175 | 176 | - FIX discusstion reply bug 177 | 178 | ## v3.3.0 179 | 180 | - ADD discusstion collection 181 | - FIX icon error 182 | - rebuild project, using grunt build completely 183 | 184 | ## v3.2.3 185 | 186 | - FIX display items repeatly when refresh 187 | 188 | ## v3.2.2 189 | 190 | - add installation checking code 191 | 192 | ## v3.2.0 193 | 194 | - hide term-model when first time open 195 | - move to Chrome Web Store 196 | 197 | ## v3.1.0 198 | 199 | - FIX clear div when clearing data 200 | - automaticaly switch to new term 201 | 202 | ## v3.0.1 203 | 204 | - css fix 205 | 206 | ## v3.0.0 207 | 208 | - move all logic to `background.iced` 209 | - auto login after long time no-operation 210 | - do not refresh data when reopen in 5min 211 | - add off-line mode for homework and announcement by adding a cache 212 | 213 | ## v2.2.1 214 | 215 | - bug fixed 216 | 217 | ## v2.2.0 218 | 219 | - FIX wrong href target 220 | - ignore unfinished homework that exceed the time limit 221 | 222 | ## v2.1.0 223 | 224 | - ADD file collection function 225 | - add function to update database smoothly 226 | - ADD changelog page 227 | - FIX bug of displaying course name #4 228 | - FIX unread message number 229 | 230 | ## v2.0.1 231 | 232 | - FIX refresh button bug 233 | - FIX security problem of saved password 234 | - show version info in index page 235 | 236 | ## v2.0.0 237 | 238 | - evernote-like UI 239 | - collect Homework and Announcement 240 | -------------------------------------------------------------------------------- /CHANGELOG_ZH.md: -------------------------------------------------------------------------------- 1 | # 更新记录 2 | 3 | ## 4.7.0 4 | 5 | - [FIX] 暗色模式下“加载更多”按钮不可见 6 | - [ADD] 初步实现页面路由化,允许使用浏览器的前进后退功能切换内容,刷新或重新打开页面时保留之前的内容 7 | - [FIX] 禁止多开页面以避免出现数据冲突 ([#160](https://github.com/Harry-Chen/Learn-Helper/issues/160)) 8 | - [FIX] 课程名过长时下拉符号显示位置未对齐 ([#165](https://github.com/Harry-Chen/Learn-Helper/issues/165)) 9 | - [FIX] 作业内容显示转义问题 ([#167](https://github.com/Harry-Chen/Learn-Helper/issues/167)) 10 | - [FIX] 作业“提交内容”总是被显示 11 | - [ADD] 使用 [WXT](https://wxt.dev/) 作为开发框架 12 | - [ADD] 升级 thu-learn-lib v4.1.0,并新增以下内容的支持: 13 | - 作业补交截止时间、作业完成方式、作业提交方式 14 | - 通知过期时间 15 | - 文件分类(目前仅支持显示文件所属分类,按分类查看等功能请期待后续版本) 16 | 17 | ## 4.6.2 18 | 19 | - [FIX] 修复课程时间地点为空时无法获取课程信息 ([#145](https://github.com/Harry-Chen/Learn-Helper/issues/145)) 20 | - [FIX] 只在插件页面注入 csrf 以免影响网络学堂正常使用 21 | - [ADD] 作业列表优先显示未完成作业 ([#148](https://github.com/Harry-Chen/Learn-Helper/issues/148)) 22 | 23 | ## 4.6.1 24 | 25 | - [FIX] 更换 i18n 后端,支持切换语言 ([#134](https://github.com/Harry-Chen/Learn-Helper/issues/134)) 26 | - [FIX] csrf 注入失败导致无法提交作业等问题 ([#135](https://github.com/Harry-Chen/Learn-Helper/issues/135)) 27 | - [FIX] 尝试修复北大课程兼容 ([#139](https://github.com/Harry-Chen/Learn-Helper/issues/139)) 28 | - [FIX] 无法正确在新窗口打开 ([#140](https://github.com/Harry-Chen/Learn-Helper/issues/140)) 29 | - [FIX] 打开项目后滚动重置 ([AsakuraMizu/Learn-Helper#2](https://github.com/AsakuraMizu/Learn-Helper/issues/2)) 30 | - [FIX] 切换学期后过滤未重置 ([AsakuraMizu/Learn-Helper#3](https://github.com/AsakuraMizu/Learn-Helper/issues/3)) 31 | - [FIX] 移除 fontawesome,使用 [unplugin-icons](https://github.com/unplugin/unplugin-icons) 渲染图标,减小包体积 32 | 33 | ## 4.6.0 34 | 35 | (由 @AsakuraMizu 作为 2023 年 OSPP 项目贡献) 36 | 37 | - [REFACTOR] 切换工具链为 Vite,重构了 React 组件和 Redux 逻辑,增加项目可维护性 38 | - [ADD] 全面迁移为 Manifest V3,支持 Chrome / Firefox 39 | - [ADD] 添加暗色模式,支持跟随系统或手动设置 40 | - [ADD] 国际化支持,添加了英文翻译(部分,待校对) 由于技术限制,语言跟随浏览器,暂不可切换 41 | - [ADD] 升级 thu-learn-lib v3.1.0 42 | - [FIX] snackbar 有多条消息时只显示最后一条 43 | 44 | ## 4.5.1 45 | 46 | - [ADD] 升级到 TypeScript v4.3.6,MUI v5.5.2(界面风格略有变化) 47 | 48 | ## 4.5.0 49 | 50 | - [FIX] 升级到最新版本依赖。由于 thu-learn-lib 的改动, **此版本升级将会自动清空数据** ,敬请理解 51 | - [FIX] 修复公告发布时间在刷新后可能错误变化的问题 52 | - [ADD] 为公告和作业添加文件预览(#109) 53 | - [ADD] 在开始批量下载前检查登录状态,防止获得大量垃圾文件 54 | 55 | ## 4.4.3 56 | 57 | - [FIX] 修复无法打开公告页面、无法提交作业的问题(#103),感谢喵喵! 58 | - [FIX] 修复课程关闭部分功能时无法获取数据的问题(#104) 59 | 60 | ## 4.4.2 61 | 62 | - [FIX] 紧急修复无法登录的问题(升级到最新版 thu-learn-lib 以支持网络学堂的 CSRF token) 63 | 64 | ## 4.4.1 65 | 66 | - [FIX] 修复切换学期对话框中可能没有网络学堂当前学期的问题(#92) 67 | - [ADD] 升级依赖以增强安全性 68 | 69 | ## 4.4.0 70 | 71 | - [FIX] 修复读取课程关闭的功能时发生的错误(#90) 72 | - [ADD] 在欢迎页面添加更多版本信息 73 | - [ADD] 由于 FireFox Addons 下架,部署私有版本(见 [此页面](https://harrychen.xyz/learn/) ) 74 | - [ADD] 升级到 TypeScript v4.1, Webpack v5.15, React v17, Material-UI v4.11 75 | 76 | ## 4.3.1 77 | 78 | - [FIX] 修复不选择保存密码无法正常使用的问题 79 | - [FIX] 修复版本升级后首次启动时报错的问题 80 | - [ADD] 在错误页面增加刷新提示,避免在非必要情况下清除数据 81 | - [FIX] 绕过(由于 Chrome bug 导致的)嵌入的网络学堂页面无法正常打开的问题(by Starrah) 82 | - [FIX] 修复作业图标不对齐的问题(by Starrah) 83 | 84 | ## 4.3.0 85 | 86 | - [ADD] 添加切换学期功能 87 | - [ADD] 添加文件预览查看链接(绕过 Chrome bug) 88 | - [FIX] 对于尚未到期的作业,按照 deadline 从早到晚排序 89 | 90 | ## 4.2.1 91 | 92 | - [ADD] 在文件详情中添加预览(仅限于支持的格式) 93 | - [DEL] 移除对 learn2018.tsinghua.edu.cn 的权限要求 94 | - [FIX] 使用现代 HTML 重写欢迎页面 95 | 96 | ## 4.2.0 97 | 98 | - [ADD] 切换到 preact,减少代码体积,提升最低 Chrome 版本 99 | - [ADD] 在错误恢复页面添加更多信息 100 | - [FIX] 修复另一个退课后崩溃的问题 101 | 102 | ## 4.1.7 103 | 104 | - [FIX] 修复登录失败后的 UI 逻辑 105 | - [ADD] 在错误恢复页面中增加更多信息 106 | 107 | ## 4.1.6 108 | 109 | - [FIX] 修复登录失败后登录对话框状态问题 110 | - [FIX] 修复侧边栏计数导致的插件崩溃问题 111 | 112 | ## 4.1.5 113 | 114 | - [FIX] 修复侧边栏无法独立滚动问题 115 | 116 | ## 4.1.4 117 | 118 | - [FIX] 修复退课导致插件崩溃的问题 119 | - [FIX] 修复未截止作业排序混乱的问题 120 | - [FIX] 不再将被忽略的作业计入统计数量中 121 | 122 | ## 4.1.3 123 | 124 | - [FIX] 修复导致每次启动时数据被清除的错误的版本迁移逻辑 125 | 126 | ## 4.1.2 127 | 128 | - [FIX] 修复课程模块被关闭时加载失败的问题 129 | - [FIX] 修复取消切换学期导致插件错误的问题 130 | - [ADD] 增加致命错误恢复功能 131 | 132 | ## 4.1.1 133 | 134 | - [FIX] 紧急修复课程公告编码错误的问题 135 | 136 | ## 4.1.0 137 | 138 | - [ADD] 增加非百分制的作业成绩显示 139 | - [ADD] 允许忽略单个任意项目,完善隐藏课程模块逻辑 140 | - [ADD] 增加未读文件批量下载功能 141 | - [ADD] 在详情面板中显示详细时间 142 | - [FIX] 修复在 FireFox 中无法存储状态的问题 143 | - [FIX] 在作业被评阅后标记为未读,在 DDL 前将未完成作业置顶显示 144 | - [FIX] 修复公告内容为空时显示乱码的问题 145 | - [FIX] 修复错误提示,增加在老师关闭课程模块时的提醒 146 | 147 | ## 4.0.3 148 | 149 | - [FIX] 修复点击图标重复打开页面的问题 150 | - [FIX] 修复打开详情页面崩溃的问题 151 | - [FIX] 修复退课后管理忽略项崩溃的问题 152 | - [FIX] 修复 FireFox 中文件下载问题 153 | 154 | ## 4.0.2 155 | 156 | - [ADD] 为文件添加详情页面(by gjz010) 157 | - [ADD] 添加登录超时判断,减少等待时间 158 | - [FIX] 修复详情页面中的空格问题 159 | 160 | ## 4.0.1 161 | 162 | - [FIX] 修复公告中附件下载链接错误问题(by zhaoxh16) 163 | - [FIX] 修复过窄屏幕上详情面板宽度问题 164 | 165 | ## 4.0.0 166 | 167 | (为 2018 版网络学堂完全重写 by Harry Chen) 168 | 169 | - [ADD] 使用 React 进行前后端分离 170 | - [ADD] 界面部分 Material 化 171 | - [ADD] 增加课程答疑模块 172 | - [ADD] 增加 Chrome 插件徽章显示未读数量 173 | - [ADD] 增加卡片标签过滤功能 174 | 175 | ## 3.5.3 176 | 177 | - [ADD] 可以折叠侧边栏以适应低分辨率屏幕(by yaoht) 178 | 179 | ## 3.5.2 180 | 181 | - [FIX] 新版网络学堂登陆问题(by yaoht) 182 | 183 | ## 3.5.0 184 | 185 | - [ADD] 初步支持新版网络学堂(by moreD) 186 | 187 | ## 3.4.0 188 | 189 | - [FIX] 移除新版学堂课程,保证旧版可以正常使用 190 | 191 | ## 3.3.1 192 | 193 | - [FIX] 讨论区无法回复 194 | 195 | ## 3.3.0 196 | 197 | - [ADD] 添加讨论区提醒功能 198 | - [FIX] 功能板块图标错误 199 | - [FIX] 开发目录结构重构 200 | 201 | ## 3.2.3 202 | 203 | - [FIX] 修复强制刷新后重复显示 item 204 | 205 | ## 3.2.2 206 | 207 | - [ADD] 迁移到 Chrome 商店,添加安装检测代码 208 | 209 | ## 3.2 210 | 211 | - [FIX] 修复首次打开时显示学期切换窗口 212 | - [FIX] 简化前后台交互及弹出方式 213 | - [ADD] 整体迁移至 Chrome Web Store,升级地址修改 214 | 215 | ## 3.1 216 | 217 | - [FIX] 修复清空数据时不清空课程列表问题 218 | - [ADD] 自动判断学期切换 219 | 220 | ## 3.0.1 221 | 222 | - [FIX] 修复网络学堂官方修复标签所带来的副作用,使标签背景有效 223 | 224 | ## 3.0 225 | 226 | - [ADD] 增加对公告页面的格式修正,保证换行能正常显示 227 | - [ADD] 逻辑前后台拆分,提高性能 228 | - [FIX] 若干点击之后的显示更新问题 229 | - [ADD] 长时间未使用后,自动登录功能(无需刷新) 230 | - [ADD] 5 分钟内重复进入系统,不进行刷新(减轻服务器压力) 231 | - [ADD] 通知、作业的离线缓存功能 232 | 233 | ## 2.2.1 234 | 235 | - [FIX] 合并函数时遗留的作业已读无效 bug 236 | 237 | ## 2.2 238 | 239 | - [FIX] 查看批阅打开 Target 错误 240 | - [FIX] 简化显示逻辑,合并函数 241 | - [FIX] 未完成作业算上已过期错误 242 | - [ADD] 增加作业计数上传 243 | 244 | ## 2.1 245 | 246 | - [ADD] 查找课程文件功能 247 | - [FIX] 添加数据库升级函数 248 | - [ADD] Changelog 页面 249 | - [FIX] 含括号课程名显示 bug, ISSUE 4 250 | - [FIX] 未读信息数量显示 bug 251 | 252 | ## 2.0.1 253 | 254 | - [FIX] 修复了更换用户后「刷新」无用,必须「强制刷新」然后再「刷新」方能生效的问题 255 | - [FIX] 修复了 localStroage 的安全性问题 256 | - [ADD] Version 信息 257 | 258 | ## 2.0 259 | 260 | - 新版本推出 261 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2012-2021 Shengqi Chen, Xinran Xu, Brian Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learn Helper 2 | 3 | [![GitHub Action Build](https://github.com/Harry-Chen/Learn-Helper/workflows/Build/badge.svg)](https://github.com/Harry-Chen/Learn-Helper/actions) 4 | [![GitHub release](https://img.shields.io/github/v/release/Harry-Chen/Learn-Helper)](https://github.com/Harry-Chen/Learn-Helper) 5 | [![Chrome Web Store version](https://img.shields.io/chrome-web-store/v/mdehapphdlihjjgkhmoiknmnhcjpjall)](https://chrome.google.com/webstore/detail/learn-helper/mdehapphdlihjjgkhmoiknmnhcjpjall) 6 | ![Chrome Web Store users](https://img.shields.io/chrome-web-store/users/mdehapphdlihjjgkhmoiknmnhcjpjall) 7 | ![Chrome Web Store rating](https://img.shields.io/chrome-web-store/rating/mdehapphdlihjjgkhmoiknmnhcjpjall) 8 | 9 | A browser extension for [Web Learning](https://learn.tsinghua.edu.cn) of Tsinghua University. 10 | 11 | ## Authors & Maintainers 12 | 13 | * [AsakuraMizu](https://github.com/AsakuraMizu) 14 | * current maintainer 15 | * developer from v4.6.0 16 | * [Harry-Chen](https://github.com/Harry-Chen) 17 | * current maintainer 18 | * developer of v4.0.0 - v4.5.1 19 | * [xxr3376](https://github.com/xxr3376) 20 | * original author 21 | * developer till v3.3.1 22 | 23 | ## Install 24 | 25 | [Chrome Store](https://chrome.google.com/webstore/detail/learn-helper/mdehapphdlihjjgkhmoiknmnhcjpjall), [Self-hosted Firefox Add-on](https://harrychen.xyz/learn/), [Edge Addons](https://microsoftedge.microsoft.com/addons/detail/dhddjfhadejlhiaafnbadhaeichbkgil) 26 | 27 | Or you can install the unpacked version from releases. 28 | 29 | ## License 30 | 31 | This project is licensed under the terms of MIT License from version 4.0.3 __EXCLUDING any of the following conditions__: 32 | 33 | * You are working / have worked for *Informatization Office* or *Information Technology Center* of Tsinghua University. 34 | * Your project is funded or supported in any way by an affiliate of Tsinghua University or any other institution associated with Tsinghua University. 35 | 36 | If any of these criteria is met, any use of code, without explicit authorization from the authors, from this project will be considered as infringement of copyright. The word ‘use’ may refer to making copies of, modifying, redistributing of the source code or derivatives (such as browser extension) of this project, whether or not for commercial use. However you can still install and run the browser extension released by the author without being constrained by this exception. 37 | 38 | ## 版权说明 39 | 40 | 本项目从 4.0.3 版本起,依照 MIT License 开源,但 __不包含下列任意情况__: 41 | 42 | * 您过去或者目前为清华大学信息化工作办公室或信息化技术中心工作; 43 | * 您的项目受到清华大学的下属机构或其他任何与清华大学有关的机构的任何形式的资助或支持。 44 | 45 | 如果上述任意条件成立,任何未经授权的对本项目中代码的使用将会被认为是侵权。上文中的“使用”包括对项目的源代码或衍生品(如浏览器插件)制作拷贝、修改、重分发,无论是否用作商业用途。但安装并运行作者发布的浏览器插件的行为不受此例外约束。 46 | 47 | ## Features 48 | 49 | * provide a Evernote-like materialized UI 50 | * collect all data of Web Learning 51 | * Homework 52 | * Notification 53 | * File 54 | * Discussion 55 | * Question 56 | * provide new message reminder and highlight messages 57 | * provide a `Priority Inbox` like Gmail 58 | * provide a off-line mode, cache all message that you have already read 59 | 60 | ## Build 61 | 62 | Learn Helper is built using `yarn`: 63 | 64 | ```bash 65 | yarn --frozen-lockfile 66 | yarn build:chrome # for chrome build 67 | yarn build:firefox # for firefox build 68 | ``` 69 | 70 | The compiled Chrome/Firefox plugin is in `dist/` (unpacked). 71 | 72 | ## Development 73 | 74 | ```bash 75 | yarn dev 76 | # in another terminal window 77 | yarn serve:chrome 78 | ``` 79 | 80 | Due to technical restrictions, dev mode works only for chrome. 81 | 82 | ### Auto login 83 | 84 | Copy `.env.development` to `.env.development.local` and fill in your username & password to login automatically on start. (development mode only) 85 | 86 | ## Revision History 87 | 88 | See [CHANGELOG.md](https://github.com/Harry-Chen/Learn-Helper/blob/master/CHANGELOG.md). 89 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", 3 | "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, 4 | "files": { "ignoreUnknown": false, "ignore": [] }, 5 | "formatter": { 6 | "enabled": true, 7 | "useEditorconfig": true, 8 | "formatWithErrors": false, 9 | "indentStyle": "space", 10 | "lineEnding": "lf", 11 | "lineWidth": 100 12 | }, 13 | "organizeImports": { "enabled": true }, 14 | "linter": { 15 | "enabled": true, 16 | "rules": { 17 | "style": { 18 | "noNonNullAssertion": "off" 19 | }, 20 | "suspicious": { 21 | "noEmptyBlock": "off" 22 | } 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "trailingCommas": "all", 28 | "quoteStyle": "single" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /design/img/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harry-Chen/Learn-Helper/4d538afad1fbb13de0b2b94380b845e11a9bb5aa/design/img/512.png -------------------------------------------------------------------------------- /design/img/bg.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harry-Chen/Learn-Helper/4d538afad1fbb13de0b2b94380b845e11a9bb5aa/design/img/bg.ai -------------------------------------------------------------------------------- /design/img/bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | 31 | 34 | 36 | 38 | 39 | 42 | 44 | 47 | 50 | 52 | 53 | 54 | 致力于帮助你及时了解网络学堂的信息 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /entrypoints/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 关于我们 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /entrypoints/background.ts: -------------------------------------------------------------------------------- 1 | const open = async () => { 2 | const url = browser.runtime.getURL('/index.html'); 3 | const tabs = await browser.tabs.query({ url }); 4 | if (tabs.length) { 5 | await browser.tabs.update(tabs[0].id, { active: true }); 6 | } else { 7 | await browser.tabs.create({ url }); 8 | } 9 | }; 10 | 11 | export default defineBackground({ 12 | type: 'module', 13 | main() { 14 | if (!browser.action.onClicked.hasListener(open)) { 15 | browser.action.onClicked.addListener(open); 16 | } 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /entrypoints/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Learn Helper 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /lingui.config.ts: -------------------------------------------------------------------------------- 1 | import type { LinguiConfig } from '@lingui/conf'; 2 | 3 | const config: LinguiConfig = { 4 | locales: ['en', 'zh'], 5 | sourceLocale: 'zh', 6 | orderBy: 'origin', 7 | catalogs: [ 8 | { 9 | path: '/src/locales/{locale}', 10 | include: ['src'], 11 | }, 12 | ], 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-helper", 3 | "version": "4.7.0.1", 4 | "author": "Harry Chen", 5 | "license": "MIT", 6 | "private": true, 7 | "type": "module", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Harry-Chen/Learn-Helper" 11 | }, 12 | "scripts": { 13 | "dev": "wxt", 14 | "build:chrome": "wxt build", 15 | "build:firefox": "wxt build -b firefox", 16 | "zip:chrome": "wxt zip", 17 | "zip:firefox": "wxt zip -b firefox", 18 | "clean": "wxt clean", 19 | "postinstall": "wxt prepare", 20 | "check": "biome check", 21 | "lingui:extract": "lingui extract" 22 | }, 23 | "devDependencies": { 24 | "@biomejs/biome": "^1.9.4", 25 | "@lingui/cli": "^5.2.0", 26 | "@lingui/swc-plugin": "5.4.0", 27 | "@lingui/vite-plugin": "^5.2.0", 28 | "@mdx-js/rollup": "^3.1.0", 29 | "@svgr/core": "^8.1.0", 30 | "@svgr/plugin-jsx": "^8.1.0", 31 | "@types/chrome": "^0.0.309", 32 | "@types/mdx": "^2.0.13", 33 | "@types/node": "^22.13.10", 34 | "@types/randomstring": "^1.3.0", 35 | "@types/react": "^19.0.11", 36 | "@types/react-dom": "^19.0.4", 37 | "@types/redux-logger": "^3.0.13", 38 | "@vitejs/plugin-react-swc": "^3.8.0", 39 | "randomstring": "^1.3.1", 40 | "rehype-unwrap-images": "^1.0.0", 41 | "rollup-preserve-directives": "^1.1.3", 42 | "terser": "^5.39.0", 43 | "typescript": "^5.8.2", 44 | "unplugin-icons": "^22.1.0", 45 | "vite-plugin-chunk-split": "^0.5.0", 46 | "wxt": "^0.19.29" 47 | }, 48 | "dependencies": { 49 | "@iconify-json/fa6-solid": "^1.2.3", 50 | "@lingui/core": "^5.2.0", 51 | "@lingui/message-utils": "^5.2.0", 52 | "@lingui/react": "^5.2.0", 53 | "@mui/material": "^6.4.8", 54 | "@reduxjs/toolkit": "^2.6.1", 55 | "classnames": "^2.5.1", 56 | "compare-versions": "^6.1.1", 57 | "fake-parse5": "^0.0.1", 58 | "material-ui-popup-state": "^5.3.3", 59 | "notistack": "^3.0.2", 60 | "proxy-memoize": "^3.0.1", 61 | "react": "^19.0.0", 62 | "react-dom": "^19.0.0", 63 | "react-error-boundary": "^5.0.0", 64 | "react-iframe": "^1.8.5", 65 | "react-redux": "^9.2.0", 66 | "redux-logger": "^3.0.6", 67 | "rehype-mdx-import-media": "^1.2.0", 68 | "thu-learn-lib": "^4.1.0", 69 | "wouter": "^3.6.0" 70 | }, 71 | "pnpm": { 72 | "onlyBuiltDependencies": [ 73 | "@biomejs/biome", 74 | "@swc/core", 75 | "dtrace-provider", 76 | "esbuild", 77 | "spawn-sync" 78 | ] 79 | }, 80 | "packageManager": "pnpm@10.6.4+sha512.da3d715bfd22a9a105e6e8088cfc7826699332ded60c423b14ec613a185f1602206702ff0fe4c438cb15c979081ce4cb02568e364b15174503a63c7a8e2a5f6c" 81 | } 82 | -------------------------------------------------------------------------------- /public/_locales/en_US/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appDesc": { 3 | "message": "Helper of Web Learning of Tsinghua University, showing course notices, homeworks, deadlines and other information in one page, providing one-stop service" 4 | }, 5 | "appName": { 6 | "message": "Learn Helper" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appDesc": { 3 | "message": "清华大学网络学堂助手,集中展示课程公告、作业、DDL等信息,提供一站式服务" 4 | }, 5 | "appName": { 6 | "message": "Learn Helper" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harry-Chen/Learn-Helper/4d538afad1fbb13de0b2b94380b845e11a9bb5aa/public/icons/128.png -------------------------------------------------------------------------------- /public/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harry-Chen/Learn-Helper/4d538afad1fbb13de0b2b94380b845e11a9bb5aa/public/icons/16.png -------------------------------------------------------------------------------- /public/icons/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harry-Chen/Learn-Helper/4d538afad1fbb13de0b2b94380b845e11a9bb5aa/public/icons/19.png -------------------------------------------------------------------------------- /public/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harry-Chen/Learn-Helper/4d538afad1fbb13de0b2b94380b845e11a9bb5aa/public/icons/48.png -------------------------------------------------------------------------------- /src/about.tsx: -------------------------------------------------------------------------------- 1 | import { i18n } from '@lingui/core'; 2 | import { I18nProvider } from '@lingui/react'; 3 | import { CssBaseline, ThemeProvider } from '@mui/material'; 4 | import classNames from 'classnames'; 5 | import { createRoot } from 'react-dom/client'; 6 | 7 | import { theme } from './theme'; 8 | import './i18n'; 9 | 10 | import styles from './css/doc.module.css'; 11 | import About from './pages/doc/about'; 12 | 13 | const root = createRoot(document.querySelector('#main')!); 14 | root.render( 15 | 16 | 17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 |
, 25 | ); 26 | -------------------------------------------------------------------------------- /src/components/CardList.tsx: -------------------------------------------------------------------------------- 1 | import { Trans } from '@lingui/react/macro'; 2 | import { Button, List, ListSubheader } from '@mui/material'; 3 | import cn from 'classnames'; 4 | import { memoize } from 'proxy-memoize'; 5 | import { useEffect, useRef, useState } from 'react'; 6 | import { ContentType } from 'thu-learn-lib'; 7 | 8 | import { downloadAllUnreadFiles, loadMoreCard } from '../redux/actions'; 9 | import { useAppDispatch, useAppSelector } from '../redux/hooks'; 10 | 11 | import styles from '../css/list.module.css'; 12 | import ContentCard from './ContentCard'; 13 | 14 | const CardList = () => { 15 | const dispatch = useAppDispatch(); 16 | const threshold = useAppSelector((state) => state.ui.cardVisibilityThreshold); 17 | const originalCardList = useAppSelector((state) => state.ui.cardList); 18 | 19 | const [_onTop, setOnTop] = useState(true); 20 | const scrollRef = useRef(null); 21 | 22 | useEffect(() => { 23 | void originalCardList; 24 | if (scrollRef.current) { 25 | scrollRef.current.scrollTop = 0; 26 | setOnTop(true); 27 | } 28 | }, [originalCardList]); 29 | 30 | const cards = useAppSelector( 31 | memoize((state) => 32 | state.helper.loggedIn 33 | ? originalCardList 34 | .map(({ type, id }) => state.data[`${type}Map`][id]) 35 | .filter( 36 | (c) => 37 | !!c && 38 | c.title 39 | .toLocaleLowerCase() 40 | .includes(state.ui.titleFilter?.toLocaleLowerCase() ?? ''), 41 | ) 42 | : [], 43 | ), 44 | ); 45 | const filtered = cards.slice(0, threshold); 46 | const unreadFileCount = cards.reduce((count, c) => { 47 | if (c.type === ContentType.FILE && !c.hasRead) return count + 1; 48 | return count; 49 | }, 0); 50 | const canLoadMore = threshold < cards.length; 51 | 52 | return ( 53 |
{ 56 | const self = ev.target as HTMLDivElement; 57 | setOnTop(self.scrollTop === 0); 58 | 59 | if (!canLoadMore) return; 60 | const bottomLine = self.scrollTop + self.clientHeight; 61 | if (bottomLine + 180 > self.scrollHeight) { 62 | // 80 px on load more hint 63 | dispatch(loadMoreCard()); 64 | } 65 | }} 66 | ref={scrollRef} 67 | > 68 | 76 | {unreadFileCount !== 0 && ( 77 | 86 | )} 87 | 88 | } 89 | > 90 | {filtered.map((c) => ( 91 | 92 | ))} 93 | 94 | {filtered.length === 0 && ( 95 |
96 | 这里什么也没有 97 |
98 | )} 99 | 100 | {canLoadMore && ( 101 |
dispatch(loadMoreCard())} 104 | onKeyDown={(e) => e.key === 'Enter' && dispatch(loadMoreCard())} 105 | > 106 | 加载更多 107 |
108 | )} 109 |
110 |
111 | ); 112 | }; 113 | 114 | export default CardList; 115 | -------------------------------------------------------------------------------- /src/components/ContentCard.tsx: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/core/macro'; 2 | import { useLingui } from '@lingui/react'; 3 | import classnames from 'classnames'; 4 | import { ContentType } from 'thu-learn-lib'; 5 | import { useLocation } from 'wouter'; 6 | 7 | import { 8 | Avatar, 9 | Badge, 10 | Card, 11 | CardActionArea, 12 | CardActions, 13 | CardContent, 14 | Chip, 15 | IconButton, 16 | Tooltip, 17 | } from '@mui/material'; 18 | 19 | import IconCheck from '~icons/fa6-solid/check'; 20 | import IconClipboard from '~icons/fa6-solid/clipboard'; 21 | import IconClipboardCheck from '~icons/fa6-solid/clipboard-check'; 22 | import IconDownload from '~icons/fa6-solid/download'; 23 | import IconPaperclip from '~icons/fa6-solid/paperclip'; 24 | import IconStar from '~icons/fa6-solid/star'; 25 | import IconTrash from '~icons/fa6-solid/trash'; 26 | import IconTrashCan from '~icons/fa6-solid/trash-can'; 27 | import IconUpload from '~icons/fa6-solid/upload'; 28 | 29 | import { COURSE_MAIN_FUNC } from '../constants/ui'; 30 | import styles from '../css/card.module.css'; 31 | import { toggleIgnoreState, toggleReadState, toggleStarState } from '../redux/actions'; 32 | import { useAppDispatch, useAppSelector } from '../redux/hooks'; 33 | import { initiateFileDownload } from '../utils/download'; 34 | import { formatDate, formatHomeworkGradeLevel } from '../utils/format'; 35 | 36 | interface ContentCardProps { 37 | type: ContentType; 38 | id: string; 39 | } 40 | 41 | const ContentCard = ({ type, id }: ContentCardProps) => { 42 | const { _ } = useLingui(); 43 | const dispatch = useAppDispatch(); 44 | const [_location, navigate] = useLocation(); 45 | 46 | const content = useAppSelector((state) => state.data[`${type}Map`][id]); 47 | 48 | const onTitleClick = () => { 49 | switch (content.type) { 50 | // show details in DetailPane 51 | case ContentType.FILE: 52 | case ContentType.NOTIFICATION: 53 | case ContentType.HOMEWORK: 54 | navigate(`/content/${content.type}/${content.id}`); 55 | break; 56 | // navigate iframe in DetailPane to given url 57 | case ContentType.DISCUSSION: 58 | case ContentType.QUESTION: 59 | navigate(`/web/${encodeURIComponent(content.url)}`); 60 | break; 61 | } 62 | // mark card as read 63 | dispatch(toggleReadState({ type: content.type, id: content.id, state: true })); 64 | }; 65 | 66 | const diffDays = Math.floor((content.date.getTime() - new Date().getTime()) / (1000 * 3600 * 24)); 67 | 68 | return ( 69 | 70 | 71 | 72 |
73 | 74 | 77 | {content.type === ContentType.HOMEWORK && content.submitted ? ( 78 | 79 | ) : ( 80 | COURSE_MAIN_FUNC[content.type].icon 81 | )} 82 | 83 | } 84 | label={ 85 |
86 | {content.type === ContentType.HOMEWORK 87 | ? diffDays > 99 88 | ? '99+' 89 | : diffDays < 0 90 | ? _(COURSE_MAIN_FUNC[content.type].name) 91 | : diffDays.toString() 92 | : _(COURSE_MAIN_FUNC[content.type].name)} 93 |
94 | } 95 | className={classnames( 96 | styles[ 97 | content.type === ContentType.HOMEWORK 98 | ? `chip_homework_${ 99 | diffDays < 0 100 | ? 'due' 101 | : content.submitted 102 | ? 'submitted' 103 | : diffDays >= 10 104 | ? 'far' 105 | : diffDays >= 5 106 | ? 'near' 107 | : diffDays >= 3 108 | ? 'close' 109 | : 'urgent' 110 | }` 111 | : `chip_${content.type}` 112 | ], 113 | styles.card_func_chip, 114 | )} 115 | /> 116 |
117 | {content.title} 118 |
119 | 120 |
121 | 122 | {formatDate(content.date)} 123 | {content.type === ContentType.HOMEWORK 124 | ? ` · ${content.submitted ? t`已提交` : t`未提交`} · ${ 125 | content.graded 126 | ? ( 127 | content.grade 128 | ? content.gradeLevel 129 | ? _(formatHomeworkGradeLevel(content.gradeLevel)) 130 | : t`${content.grade}分` 131 | : t`无评分` 132 | ) + t`(${content.graderName ?? ''})` 133 | : t`未批阅` 134 | }` 135 | : content.type === ContentType.NOTIFICATION || content.type === ContentType.FILE 136 | ? (content.markedImportant ? ` · ${t`重要`}` : '') + 137 | (content.type === ContentType.NOTIFICATION 138 | ? ` · ${t`发布者:${content.publisher}`}` 139 | : ` · ${content.size}${ 140 | content.description.trim() !== '' 141 | ? ` · ${content.description.trim()}` 142 | : '' 143 | }`) 144 | : content.type === ContentType.DISCUSSION || content.type === ContentType.QUESTION 145 | ? ` · ${t`回复:${content.replyCount}`}${ 146 | content.replyCount !== 0 147 | ? ` · ${t`最后回复:${content.lastReplierName}`}` 148 | : '' 149 | }` 150 | : null} 151 | 152 | {_({ id: `course-${content.courseId}` })} 153 |
154 |
155 | 156 | 157 | { 164 | dispatch( 165 | toggleStarState({ type: content.type, id: content.id, state: !content.starred }), 166 | ); 167 | ev.stopPropagation(); 168 | }} 169 | onMouseDown={(ev) => ev.stopPropagation()} 170 | size="small" 171 | > 172 | 173 | 174 | 175 | 176 | { 181 | dispatch( 182 | toggleReadState({ type: content.type, id: content.id, state: !content.hasRead }), 183 | ); 184 | ev.stopPropagation(); 185 | }} 186 | onMouseDown={(ev) => ev.stopPropagation()} 187 | size="small" 188 | > 189 | {content.hasRead ? : } 190 | 191 | 192 | 193 | { 198 | dispatch( 199 | toggleIgnoreState({ 200 | type: content.type, 201 | id: content.id, 202 | state: !content.ignored, 203 | }), 204 | ); 205 | ev.stopPropagation(); 206 | }} 207 | onMouseDown={(ev) => ev.stopPropagation()} 208 | size="small" 209 | > 210 | {content.ignored ? : } 211 | 212 | 213 | {content.type === ContentType.HOMEWORK && ( 214 | 215 | { 220 | navigate(`/web/${encodeURIComponent(content.submitUrl)}`); 221 | ev.stopPropagation(); 222 | }} 223 | onMouseDown={(ev) => ev.stopPropagation()} 224 | size="small" 225 | > 226 | 227 | 228 | 229 | )} 230 | {content.type === ContentType.FILE && ( 231 | 232 | { 237 | initiateFileDownload(content.remoteFile.downloadUrl); 238 | }} 239 | size="small" 240 | > 241 | 242 | 243 | 244 | )} 245 | {(content.type === ContentType.NOTIFICATION || content.type === ContentType.HOMEWORK) && 246 | content.attachment && ( 247 | 248 | { 253 | if (content.attachment) 254 | initiateFileDownload(content.attachment.downloadUrl, content.attachment.name); 255 | }} 256 | size="small" 257 | > 258 | 259 | 260 | 261 | )} 262 | 263 |
264 |
265 | ); 266 | }; 267 | 268 | export default ContentCard; 269 | -------------------------------------------------------------------------------- /src/components/ContentDetail.tsx: -------------------------------------------------------------------------------- 1 | import type { MessageDescriptor } from '@lingui/core'; 2 | import { msg, t } from '@lingui/core/macro'; 3 | import { useLingui } from '@lingui/react'; 4 | import { Trans } from '@lingui/react/macro'; 5 | import { Button, Paper } from '@mui/material'; 6 | import { type ReactNode, useState } from 'react'; 7 | import { ContentType, type RemoteFile } from 'thu-learn-lib'; 8 | import { useLocation } from 'wouter'; 9 | 10 | import styles from '../css/page.module.css'; 11 | import type { ContentInfo, FileInfo, HomeworkInfo, NotificationInfo } from '../types/data'; 12 | import { 13 | formatDateTime, 14 | formatHomeworkCompletionType, 15 | formatHomeworkGradeLevel, 16 | formatHomeworkSubmissionType, 17 | html2text, 18 | } from '../utils/format'; 19 | import IframeWrapper from './IframeWrapper'; 20 | 21 | // const initialState = { 22 | // frameUrl: undefined as string | undefined, 23 | // loadPreview: false, 24 | // }; 25 | 26 | interface LineProps { 27 | title: MessageDescriptor; 28 | children?: ReactNode; 29 | __html?: string; 30 | } 31 | 32 | const Line = ({ title, children, __html }: LineProps) => { 33 | const { _ } = useLingui(); 34 | 35 | return ( 36 | 37 | {_(title)} 38 | {/* biome-ignore lint/security/noDangerouslySetInnerHtml: no reason */} 39 | {__html ? : {children}} 40 | 41 | ); 42 | }; 43 | 44 | interface LinkProps { 45 | url: string; 46 | inApp?: boolean; 47 | children: ReactNode; 48 | } 49 | 50 | const ContentLink = ({ url, inApp, children }: LinkProps) => { 51 | const [_location, navigate] = useLocation(); 52 | 53 | if (inApp) 54 | return ( 55 | // don't use here as we want to keep the behaviour of middle click to open in new tab 56 | { 59 | navigate(`/web/${encodeURIComponent(url)}`); 60 | ev.preventDefault(); 61 | }} 62 | > 63 | {children} 64 | 65 | ); 66 | 67 | return ( 68 | 69 | {children} 70 | 71 | ); 72 | }; 73 | 74 | const canFilePreview = (file: RemoteFile): boolean => { 75 | // black list for files that could not be previewed 76 | const BLACK_LIST = ['.exe', '.zip', '.rar', '.7z']; 77 | return !BLACK_LIST.some((suffix) => file.name.endsWith(suffix)); 78 | }; 79 | 80 | interface FileLinkProps { 81 | downloadTitle: MessageDescriptor; 82 | previewTitle: MessageDescriptor; 83 | file: RemoteFile; 84 | } 85 | 86 | const FileLinks = ({ downloadTitle, previewTitle, file }: FileLinkProps) => ( 87 | <> 88 | 89 | 90 | 91 | {file.name}({file.size}) 92 | 93 | 94 | 95 | {canFilePreview(file) && ( 96 | 97 | 98 | 在新窗口打开 99 | 100 | 101 | )} 102 | 103 | ); 104 | 105 | const translateFileType = (type: string): string => 106 | // TODO: Translate file type to human-readable representation. 107 | type; 108 | 109 | interface ContentDetailProps { 110 | content: T; 111 | } 112 | 113 | const FileDetails = ({ content: file }: ContentDetailProps) => ( 114 | <> 115 | {formatDateTime(file.uploadTime)} 116 | {file.visitCount} 117 | {file.downloadCount} 118 | {file.size} 119 | {translateFileType(file.type)} 120 | {file.category && {file.category.title}} 121 | 126 | 127 | ); 128 | 129 | const HomeworkDetails = ({ content: homework }: ContentDetailProps) => { 130 | const { _ } = useLingui(); 131 | 132 | const submittedContent = html2text(homework.submittedContent ?? ''); 133 | const answerContent = html2text(homework.answerContent ?? ''); 134 | 135 | return ( 136 | <> 137 | {formatDateTime(homework.deadline)} 138 | {homework.lateSubmissionDeadline && ( 139 | {formatDateTime(homework.lateSubmissionDeadline)} 140 | )} 141 | 142 | {_(formatHomeworkCompletionType(homework.completionType))} 143 | 144 | 145 | {_(formatHomeworkSubmissionType(homework.submissionType))} 146 | 147 | {homework.submitted && ( 148 | 149 | {formatDateTime(homework.submitTime)} 150 | 151 | )} 152 | {submittedContent && {submittedContent}} 153 | {homework.submittedAttachment && ( 154 | 159 | )} 160 | {homework.graded && ( 161 | <> 162 | {formatDateTime(homework.gradeTime)} 163 | {homework.graderName} 164 | 165 | {homework.gradeLevel 166 | ? _(formatHomeworkGradeLevel(homework.gradeLevel)) 167 | : (homework.grade ?? 无评分)} 168 | 169 | 170 | {homework.gradeAttachment && ( 171 | 176 | )} 177 | 178 | )} 179 | {answerContent && {answerContent}} 180 | {homework.answerAttachment && ( 181 | 186 | )} 187 | {homework.attachment && ( 188 | 193 | )} 194 | 195 | 196 | 在本窗口打开 197 | 198 | 199 | 200 | ); 201 | }; 202 | 203 | const NotificationDetails = ({ content: notification }: ContentDetailProps) => ( 204 | <> 205 | {formatDateTime(notification.publishTime)} 206 | {notification.expireTime && ( 207 | {formatDateTime(notification.expireTime)} 208 | )} 209 | {notification.publisher} 210 | 211 | {notification.markedImportant ? : 普通} 212 | 213 | {notification.attachment && ( 214 | 219 | )} 220 | 221 | ); 222 | 223 | const ContentDetail = ({ content }: ContentDetailProps) => { 224 | const { _ } = useLingui(); 225 | 226 | const contentDetail = 227 | (content.type === ContentType.HOMEWORK 228 | ? content.description 229 | : content.type === ContentType.FILE 230 | ? content.description 231 | : content.type === ContentType.NOTIFICATION 232 | ? content.content 233 | : undefined 234 | )?.trim() || t`详情为空`; 235 | 236 | const fileToPreview: RemoteFile | undefined = 237 | (content as NotificationInfo | HomeworkInfo).attachment ?? (content as FileInfo).remoteFile; 238 | const showPreviewFrame = fileToPreview && canFilePreview(fileToPreview); 239 | 240 | const [preview, setPreview] = useState(content.type === ContentType.FILE); 241 | 242 | return ( 243 |
244 |

{content.title}

245 |
246 | 247 | 248 | {_({ id: `course-${content.courseId}` })} 249 | {content.type === ContentType.FILE && } 250 | {content.type === ContentType.HOMEWORK && } 251 | {content.type === ContentType.NOTIFICATION && } 252 | 253 |
254 |
255 | 260 | {showPreviewFrame && !preview && ( 261 | 264 | )} 265 | {showPreviewFrame && preview && ( 266 | 267 | )} 268 |
269 | ); 270 | }; 271 | 272 | export default ContentDetail; 273 | -------------------------------------------------------------------------------- /src/components/CourseList.tsx: -------------------------------------------------------------------------------- 1 | import { useLingui } from '@lingui/react'; 2 | import { Trans } from '@lingui/react/macro'; 3 | import { useState } from 'react'; 4 | import { useLocation } from 'wouter'; 5 | 6 | import { 7 | Collapse, 8 | List, 9 | ListItemButton, 10 | ListItemIcon, 11 | ListItemText, 12 | ListSubheader, 13 | } from '@mui/material'; 14 | 15 | import IconAngleDown from '~icons/fa6-solid/angle-down'; 16 | import IconAngleUp from '~icons/fa6-solid/angle-up'; 17 | import IconBook from '~icons/fa6-solid/book'; 18 | import IconInbox from '~icons/fa6-solid/inbox'; 19 | 20 | import { COURSE_FUNC_LIST } from '../constants/ui'; 21 | import { refreshCardList, setCardFilter } from '../redux/actions'; 22 | import { useAppDispatch, useAppSelector } from '../redux/hooks'; 23 | import { selectCourseList } from '../redux/selectors'; 24 | 25 | import styles from '../css/list.module.css'; 26 | 27 | const CourseList = () => { 28 | const { _ } = useLingui(); 29 | const [_location, navigate] = useLocation(); 30 | const dispatch = useAppDispatch(); 31 | const courseList = useAppSelector(selectCourseList); 32 | 33 | const [opened, setOpened] = useState(); 34 | 35 | return ( 36 | 41 | 42 | 43 | 本学期课程 44 | 45 | 46 | } 47 | > 48 | {courseList.length === 0 ? ( 49 | 50 | 这里什么也没有,快去选点课吧! 51 | 52 | ) : ( 53 | courseList.map((c) => ( 54 |
55 | { 58 | setOpened(opened === c.id ? undefined : c.id); 59 | }} 60 | > 61 | 62 | 63 | 64 | 68 | 69 | {opened === c.id ? : } 70 | 71 | 72 | 73 | 74 | {COURSE_FUNC_LIST.map((func) => ( 75 | { 79 | if (func.type !== 'homepage') { 80 | // show cards 81 | dispatch(setCardFilter({ type: func.type, courseId: c.id })); 82 | dispatch(refreshCardList()); 83 | } else { 84 | navigate(`/web/${encodeURIComponent(c.url)}`); 85 | } 86 | }} 87 | > 88 | {func.icon} 89 | 90 | 91 | ))} 92 | 93 | 94 |
95 | )) 96 | )} 97 |
98 | ); 99 | }; 100 | 101 | export default CourseList; 102 | -------------------------------------------------------------------------------- /src/components/IframeWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import Iframe from 'react-iframe'; 3 | 4 | interface IframeWrapperProps { 5 | id?: string; 6 | className?: string; 7 | url: string; 8 | } 9 | 10 | const IframeWrapper = ({ id, className, url }: IframeWrapperProps) => { 11 | return ( 12 | 13 |