vanspod 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -75
- package/bin/register.cjs +11 -11
- package/bin/vanspod.cjs +2 -2
- package/dist/classroomUpdate.js +142 -142
- package/dist/connectionMascot.js +215 -215
- package/dist/installProgress.js +167 -167
- package/dist/lifecyclePage.js +33 -33
- package/dist/modelStatus.js +38 -38
- package/dist/version.js +1 -1
- package/package.json +30 -29
- package/scripts/uninstall.ps1 +133 -133
package/README.md
CHANGED
|
@@ -1,75 +1,81 @@
|
|
|
1
|
-
# VansPod
|
|
2
|
-
|
|
3
|
-
VansPod 是 Windows 學生使用的本機課堂入口。提供含 Node runtime 的單檔 `VansPod.exe`,以及給已有 Node.js 24 以上環境的 npm 套件;兩者共用同一個背景服務和學生設定。
|
|
4
|
-
|
|
5
|
-
## 學生操作
|
|
6
|
-
|
|
7
|
-
雙擊 exe 或執行 `vanspod start`,會啟動背景服務並開啟瀏覽器。再開一次會回到既有畫面;關閉瀏覽器或啟動用終端機不會停止課堂代理。Windows 右下角的 VansPod logo 可重新開啟畫面,右鍵「結束 VansPod」可恢復客戶端個人設定後退出。電腦重新啟動後須手動開啟,不設定開機自動啟動。
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```powershell
|
|
12
|
-
npm install -g vanspod
|
|
13
|
-
vanspod start
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
| 指令 | 用途 |
|
|
17
|
-
| --- | --- |
|
|
18
|
-
| `vanspod` / `vanspod start` | 啟動並開啟瀏覽器,或開啟既有畫面 |
|
|
19
|
-
| `vanspod status` | 顯示是否執行中、執行版本、PID 與本機網址 |
|
|
20
|
-
| `vanspod stop` | 恢復客戶端設定後結束,保留課堂連線與模式記憶 |
|
|
21
|
-
| `vanspod uninstall` | 列出刪除內容,確認後完整移除 |
|
|
22
|
-
| `vanspod --version` / `vanspod --help` | 顯示安裝版本/說明,不啟動服務 |
|
|
23
|
-
|
|
24
|
-
安裝環境或課程專案時不能結束或移除。模型回答中須確認中斷;命令列無法互動確認時保持執行。設定恢復失敗會保留服務與備份,顯示失敗的客戶端並允許重試。
|
|
25
|
-
|
|
26
|
-
完整移除請用頁面的「移除 VansPod」或 `vanspod uninstall`。這會先恢復客戶端設定,再清理 VansPod 憑證、偏好、日誌、暫存與已登記的 exe/npm 安裝;保留學生作品、課程專案、Node、Git、VS Code 和客戶端個人設定。結果以 Windows 訊息顯示,刪除失敗會保留移除工具和失敗項目供重試;未登記的其他 exe 副本須自行刪除。直接刪 exe 或 `npm uninstall -g vanspod` 無法保證完整清理。
|
|
27
|
-
|
|
28
|
-
更新前先結束 VansPod。exe 下載指定版本替換,npm 使用 `npm install -g vanspod@<老師指定版本>`;兩者使用相同版本號,上課途中不自動更新。開啟入口與執行版本不同時會提示,先結束才能換版。
|
|
29
|
-
|
|
30
|
-
## 開發與本機打包
|
|
31
|
-
|
|
32
|
-
```powershell
|
|
33
|
-
npm install
|
|
34
|
-
npm run typecheck
|
|
35
|
-
npm test
|
|
36
|
-
npm start
|
|
37
|
-
npm run dist
|
|
38
|
-
npm pack --pack-destination dist
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
exe 產物為 `dist/VansPod.exe`,本機套件為 `dist/vanspod-0.1.
|
|
42
|
-
|
|
43
|
-
exe 發佈管道為本專案 GitHub Releases,npm 為公開 registry
|
|
44
|
-
|
|
45
|
-
## GitHub Actions 發佈
|
|
46
|
-
|
|
47
|
-
一般 push 不發佈;推送 `v*` tag 會在 Windows runner 安裝 Node.js 24、檢查型別、執行測試、打包並驗證 exe/npm 生命週期,全部通過後才建立 GitHub Release
|
|
48
|
-
|
|
49
|
-
發版前同步 `package.json`、`package-lock.json` 與 `src/version.ts` 的版本;tag 必須等於 `v` 加版本號,否則流程失敗。以 `0.1.
|
|
50
|
-
|
|
51
|
-
```powershell
|
|
52
|
-
git push origin master
|
|
53
|
-
git tag -a v0.1.
|
|
54
|
-
git push origin v0.1.
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
新版本使用新的版本號與 tag。相同 tag 的 Actions 重跑會更新該 Release 的產物。
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
1
|
+
# VansPod
|
|
2
|
+
|
|
3
|
+
VansPod 是 Windows 學生使用的本機課堂入口。提供含 Node runtime 的單檔 `VansPod.exe`,以及給已有 Node.js 24 以上環境的 npm 套件;兩者共用同一個背景服務和學生設定。
|
|
4
|
+
|
|
5
|
+
## 學生操作
|
|
6
|
+
|
|
7
|
+
雙擊 exe 或執行 `vanspod start`,會啟動背景服務並開啟瀏覽器。再開一次會回到既有畫面;關閉瀏覽器或啟動用終端機不會停止課堂代理。Windows 右下角的 VansPod logo 可重新開啟畫面,右鍵「結束 VansPod」可恢復客戶端個人設定後退出。電腦重新啟動後須手動開啟,不設定開機自動啟動。
|
|
8
|
+
|
|
9
|
+
已有 Node.js 24 以上的學生可從 npm 安裝:
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
npm install -g vanspod
|
|
13
|
+
vanspod start
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| 指令 | 用途 |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `vanspod` / `vanspod start` | 啟動並開啟瀏覽器,或開啟既有畫面 |
|
|
19
|
+
| `vanspod status` | 顯示是否執行中、執行版本、PID 與本機網址 |
|
|
20
|
+
| `vanspod stop` | 恢復客戶端設定後結束,保留課堂連線與模式記憶 |
|
|
21
|
+
| `vanspod uninstall` | 列出刪除內容,確認後完整移除 |
|
|
22
|
+
| `vanspod --version` / `vanspod --help` | 顯示安裝版本/說明,不啟動服務 |
|
|
23
|
+
|
|
24
|
+
安裝環境或課程專案時不能結束或移除。模型回答中須確認中斷;命令列無法互動確認時保持執行。設定恢復失敗會保留服務與備份,顯示失敗的客戶端並允許重試。
|
|
25
|
+
|
|
26
|
+
完整移除請用頁面的「移除 VansPod」或 `vanspod uninstall`。這會先恢復客戶端設定,再清理 VansPod 憑證、偏好、日誌、暫存與已登記的 exe/npm 安裝;保留學生作品、課程專案、Node、Git、VS Code 和客戶端個人設定。結果以 Windows 訊息顯示,刪除失敗會保留移除工具和失敗項目供重試;未登記的其他 exe 副本須自行刪除。直接刪 exe 或 `npm uninstall -g vanspod` 無法保證完整清理。
|
|
27
|
+
|
|
28
|
+
更新前先結束 VansPod。exe 下載指定版本替換,npm 使用 `npm install -g vanspod@<老師指定版本>`;兩者使用相同版本號,上課途中不自動更新。開啟入口與執行版本不同時會提示,先結束才能換版。
|
|
29
|
+
|
|
30
|
+
## 開發與本機打包
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
npm install
|
|
34
|
+
npm run typecheck
|
|
35
|
+
npm test
|
|
36
|
+
npm start
|
|
37
|
+
npm run dist
|
|
38
|
+
npm pack --pack-destination dist
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
exe 產物為 `dist/VansPod.exe`,本機套件為 `dist/vanspod-0.1.3.tgz`;可在 Windows 用 `npm install -g <套件路徑>` 安裝測試。圖片、favicon、系統匣與移除腳本均包含在發佈產物中。更新 logo 後執行 `powershell.exe -NoProfile -File scripts/build-icon.ps1` 重建多尺寸 Windows icon。
|
|
42
|
+
|
|
43
|
+
exe 發佈管道為本專案 GitHub Releases,npm 為公開 registry 的 [`vanspod`](https://www.npmjs.com/package/vanspod) 套件。更新時不要執行完整移除,否則會清除課堂資料。
|
|
44
|
+
|
|
45
|
+
## GitHub Actions 發佈
|
|
46
|
+
|
|
47
|
+
一般 push 不發佈;推送 `v*` tag 會在 Windows runner 安裝 Node.js 24、檢查型別、執行測試、打包並驗證 exe/npm 生命週期,全部通過後才建立 GitHub Release,最後發佈 npm 套件。學生從 Release 下載 `VansPod.exe`,同頁提供 `SHA256SUMS.txt`。
|
|
48
|
+
|
|
49
|
+
發版前同步 `package.json`、`package-lock.json` 與 `src/version.ts` 的版本;tag 必須等於 `v` 加版本號,否則流程失敗。以 `0.1.3` 為例:
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
git push origin master
|
|
53
|
+
git tag -a v0.1.3 -m "VansPod 0.1.3"
|
|
54
|
+
git push origin v0.1.3
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
新版本使用新的版本號與 tag。相同 tag 的 Actions 重跑會更新該 Release 的產物。
|
|
58
|
+
|
|
59
|
+
## 發佈 npm
|
|
60
|
+
|
|
61
|
+
GitHub Release 建立後,同一流程以 npm Trusted Publishing(OIDC)發佈 `vanspod`,不在 GitHub 保存 npm token,並附上來源證明(provenance)。npm 上須將 `mz038197/VansPod` 的 `release.yml` 設為 Trusted Publisher。npm 已有該版本時略過發佈,因此重跑同一 tag 只更新 Release 產物。
|
|
62
|
+
|
|
63
|
+
可用 `npm view vanspod dist-tags` 確認 `latest`。npm 同一版本號只能發佈一次,發佈 72 小時後幾乎無法撤下。若 Actions 的 npm 步驟失敗,修正後重跑該 tag,或由維護者在本機 PowerShell 執行 `npm publish` 並完成 2FA。
|
|
64
|
+
|
|
65
|
+
本機課堂 app。規格是 [classroom-one-click-install#8](https://github.com/mz038197/classroom-one-click-install/issues/8)。
|
|
66
|
+
|
|
67
|
+
不放進凡思課堂安裝擴充,也不從 opencodex fork。領域詞以該 repo 的 CONTEXT.md 為準。視窗決定見該 repo 的 ADR 0017。
|
|
68
|
+
|
|
69
|
+
設計與操作邊界見 [ADR-0008](docs/adr/0008-student-distribution-and-lifecycle.md)。
|
|
70
|
+
|
|
71
|
+
## Classroom 上游 API
|
|
72
|
+
|
|
73
|
+
模型區提供各客戶端的上游 API 選項:Codex 可選 Responses/Chat,Claude Code 可選 Responses/Chat/Messages,兩者均預設 Responses。選擇保存在使用者目錄的 `.vanspod/settings.json`,與課堂憑證分開;重啟、切換模型、清除或重新兌換課堂不會移除偏好。此選項只影響 Classroom 模式的下一個請求。
|
|
74
|
+
|
|
75
|
+
共用 Adapter 介面下的三個實作負責請求、上游呼叫與回應轉換;所有程式都在 VansPod 內,不需要 opencodex 專案。支援多輪文字、圖片輸入、函式工具往返與串流/非串流;Chat 路徑沿用 Codex 自訂工具的文字包裝及 `apply_patch` 修復。
|
|
76
|
+
|
|
77
|
+
跨協定不能對等保留的資料會明確報錯,例如 Claude 的 thinking/簽章、工具結果的 `is_error`、快取控制或服務端工具,以及 Codex 的加密 reasoning 或工具 namespace。Chat 自訂工具將完整的 Lark/regex grammar 定義保留在 input schema 的描述中,作為模型指令;Chat 不提供 Responses 原生 grammar 的受限解碼保證。原協定直通可保留其原有資料;轉換失敗不會自動換 adapter 重送。使用者取消或客戶端斷線會中止本機上游請求,串流缺少完成事件會回報失敗。
|
|
78
|
+
|
|
79
|
+
協定轉換參考官方 [Chat API](https://developers.openai.com/api/reference/resources/chat)、[Responses API](https://developers.openai.com/api/reference/resources/responses) 與 [Claude 串流事件](https://platform.claude.com/docs/en/build-with-claude/streaming)。
|
|
80
|
+
|
|
81
|
+
驗證指令:`npm run typecheck`、`npm test`、`npm run test:windows-exe`。Windows 驗證使用隔離的合成 USERPROFILE/APPDATA 與模擬 Router,涵蓋三個 Client Route、47821~47823/自訂 base port、port collision、獨立模式與 adapter、client-native catalog、失敗恢復,以及五條 JSON/SSE adapter 路徑;測試前後亦確認真實使用者設定未變。通過不表示實際 Router 或模型已完成端到端驗證。
|
package/bin/register.cjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// Register a packaged install without starting the classroom service. A source
|
|
2
|
-
// checkout has not been compiled during npm install and needs no registration.
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const entry = path.join(__dirname, '../dist/installations.js');
|
|
6
|
-
if (fs.existsSync(entry)) {
|
|
7
|
-
require(entry).registerInstallation().catch(error => {
|
|
8
|
-
process.stderr.write(`VansPod 安裝紀錄無法保存:${error.message}\n`);
|
|
9
|
-
process.exitCode = 1;
|
|
10
|
-
});
|
|
11
|
-
}
|
|
1
|
+
// Register a packaged install without starting the classroom service. A source
|
|
2
|
+
// checkout has not been compiled during npm install and needs no registration.
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const entry = path.join(__dirname, '../dist/installations.js');
|
|
6
|
+
if (fs.existsSync(entry)) {
|
|
7
|
+
require(entry).registerInstallation().catch(error => {
|
|
8
|
+
process.stderr.write(`VansPod 安裝紀錄無法保存:${error.message}\n`);
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
});
|
|
11
|
+
}
|
package/bin/vanspod.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
require('../dist/entry.js');
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('../dist/entry.js');
|
package/dist/classroomUpdate.js
CHANGED
|
@@ -9,147 +9,147 @@ function classroomUpdateControl(view, restart) {
|
|
|
9
9
|
const escaped = warning.replace(/[&<>"']/g, char => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
10
10
|
return `<div class="classroom-update-control"><p><span data-classroom-update-status data-update-warning="${escaped}">課堂內容</span><small>模型・課程安裝・課程片段</small></p><form method="post" action="/reload"><button type="submit" aria-label="更新課堂內容"${view.commandRunning ? " disabled" : ""}>↻ 更新</button></form></div><template data-classroom-restart>${restart}</template>`;
|
|
11
11
|
}
|
|
12
|
-
exports.classroomUpdateStyles = `
|
|
13
|
-
.classroom-update-home { min-height: 224px; padding-bottom: 16px; position: relative; }
|
|
14
|
-
.classroom-update { display: flex; flex-direction: column; gap: 8px; }
|
|
15
|
-
.classroom-update-home .connection-mascot { height: 144px; }
|
|
16
|
-
.classroom-update-home .connection-mascot.mascot-speaking { height: auto; min-height: 144px; flex-basis: auto; padding: 12px 0 144px; overflow: visible; }
|
|
17
|
-
.classroom-update-home .mascot-speaking .mascot-speech::after { top: auto; bottom: -7px; right: 48px; transform: rotate(135deg); }
|
|
18
|
-
.classroom-update-control { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border: 1px solid var(--line); border-radius: 16px; background: var(--tile); color: var(--text); box-shadow: 0 4px 24px #0000000a; }
|
|
19
|
-
.classroom-update-control p { margin: 0; flex: 1; min-width: 0; font-size: 13px; overflow-wrap: anywhere; }
|
|
20
|
-
.classroom-update-control small { display: block; color: var(--muted); font-size: 11px; margin-top: 3px; }
|
|
21
|
-
.classroom-update-control button { flex-shrink: 0; min-height: 40px; padding: 8px 16px; border: 0; border-radius: 24px; background: var(--accent); color: var(--on-accent); font: inherit; font-size: 13px; }
|
|
22
|
-
body.has-classroom-update { --classroom-update-bottom-gap: 24px; }
|
|
23
|
-
body.has-classroom-update main { padding-bottom: var(--classroom-update-bottom-gap); }
|
|
24
|
-
.classroom-update.docked { position: fixed; z-index: 20; right: 24px; bottom: var(--classroom-update-bottom-gap); width: 288px; }
|
|
25
|
-
.classroom-update.docked .connection-mascot { flex: none; width: 90px; height: 84px; min-height: 84px; margin-left: auto; overflow: visible; padding: 0; }
|
|
26
|
-
.classroom-update.docked .mascot-traveler { width: 84px; max-width: none; left: 0; right: auto; bottom: -4px; animation: none; transform: none; }
|
|
27
|
-
.classroom-update.docked .mascot-facing, .classroom-update.docked .mascot-logo, .classroom-update.docked .mascot-foot { animation: none; }
|
|
28
|
-
.classroom-update.docked .mascot-pause { display: none; }
|
|
29
|
-
.classroom-update.docked button.mascot-play { display: block; width: 136px; max-width: 100%; min-height: 0; padding: 0; border: 0; border-radius: 16px; background: transparent; color: inherit; }
|
|
30
|
-
.classroom-update.docked .mascot-speech { position: absolute; right: 96px; bottom: 4px; width: 184px; }
|
|
31
|
-
.classroom-update.docked .mascot-quote { max-height: min(40dvh, 280px); overflow-y: auto; overflow-x: hidden; }
|
|
32
|
-
.classroom-update.docked button.mascot-speech-close { background: transparent; color: var(--muted); padding: 4px 0; border: 0; font: inherit; font-size: 11px; }
|
|
33
|
-
@media (max-width: 640px) {
|
|
34
|
-
body.has-classroom-update main { padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); }
|
|
35
|
-
.classroom-update.docked { left: 0; right: 0; bottom: 0; width: auto; gap: 0; padding: 4px 12px max(4px, env(safe-area-inset-bottom)); background: var(--tile); border-top: 1px solid var(--line); }
|
|
36
|
-
.classroom-update.docked .connection-mascot { position: absolute; right: 16px; bottom: 100%; width: 88px; height: 88px; min-height: 88px; }
|
|
37
|
-
.classroom-update.docked .mascot-traveler { width: 88px; }
|
|
38
|
-
.classroom-update.docked .classroom-update-control { min-height: 52px; border: 0; border-radius: 0; padding: 0; box-shadow: none; }
|
|
39
|
-
.classroom-update.docked .classroom-update-control small { display: none; }
|
|
40
|
-
.classroom-update.docked .mascot-speech { right: 96px; width: min(184px, calc(100vw - 128px)); }
|
|
41
|
-
}
|
|
12
|
+
exports.classroomUpdateStyles = `
|
|
13
|
+
.classroom-update-home { min-height: 224px; padding-bottom: 16px; position: relative; }
|
|
14
|
+
.classroom-update { display: flex; flex-direction: column; gap: 8px; }
|
|
15
|
+
.classroom-update-home .connection-mascot { height: 144px; }
|
|
16
|
+
.classroom-update-home .connection-mascot.mascot-speaking { height: auto; min-height: 144px; flex-basis: auto; padding: 12px 0 144px; overflow: visible; }
|
|
17
|
+
.classroom-update-home .mascot-speaking .mascot-speech::after { top: auto; bottom: -7px; right: 48px; transform: rotate(135deg); }
|
|
18
|
+
.classroom-update-control { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border: 1px solid var(--line); border-radius: 16px; background: var(--tile); color: var(--text); box-shadow: 0 4px 24px #0000000a; }
|
|
19
|
+
.classroom-update-control p { margin: 0; flex: 1; min-width: 0; font-size: 13px; overflow-wrap: anywhere; }
|
|
20
|
+
.classroom-update-control small { display: block; color: var(--muted); font-size: 11px; margin-top: 3px; }
|
|
21
|
+
.classroom-update-control button { flex-shrink: 0; min-height: 40px; padding: 8px 16px; border: 0; border-radius: 24px; background: var(--accent); color: var(--on-accent); font: inherit; font-size: 13px; }
|
|
22
|
+
body.has-classroom-update { --classroom-update-bottom-gap: 24px; }
|
|
23
|
+
body.has-classroom-update main { padding-bottom: var(--classroom-update-bottom-gap); }
|
|
24
|
+
.classroom-update.docked { position: fixed; z-index: 20; right: 24px; bottom: var(--classroom-update-bottom-gap); width: 288px; }
|
|
25
|
+
.classroom-update.docked .connection-mascot { flex: none; width: 90px; height: 84px; min-height: 84px; margin-left: auto; overflow: visible; padding: 0; }
|
|
26
|
+
.classroom-update.docked .mascot-traveler { width: 84px; max-width: none; left: 0; right: auto; bottom: -4px; animation: none; transform: none; }
|
|
27
|
+
.classroom-update.docked .mascot-facing, .classroom-update.docked .mascot-logo, .classroom-update.docked .mascot-foot { animation: none; }
|
|
28
|
+
.classroom-update.docked .mascot-pause { display: none; }
|
|
29
|
+
.classroom-update.docked button.mascot-play { display: block; width: 136px; max-width: 100%; min-height: 0; padding: 0; border: 0; border-radius: 16px; background: transparent; color: inherit; }
|
|
30
|
+
.classroom-update.docked .mascot-speech { position: absolute; right: 96px; bottom: 4px; width: 184px; }
|
|
31
|
+
.classroom-update.docked .mascot-quote { max-height: min(40dvh, 280px); overflow-y: auto; overflow-x: hidden; }
|
|
32
|
+
.classroom-update.docked button.mascot-speech-close { background: transparent; color: var(--muted); padding: 4px 0; border: 0; font: inherit; font-size: 11px; }
|
|
33
|
+
@media (max-width: 640px) {
|
|
34
|
+
body.has-classroom-update main { padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px)); }
|
|
35
|
+
.classroom-update.docked { left: 0; right: 0; bottom: 0; width: auto; gap: 0; padding: 4px 12px max(4px, env(safe-area-inset-bottom)); background: var(--tile); border-top: 1px solid var(--line); }
|
|
36
|
+
.classroom-update.docked .connection-mascot { position: absolute; right: 16px; bottom: 100%; width: 88px; height: 88px; min-height: 88px; }
|
|
37
|
+
.classroom-update.docked .mascot-traveler { width: 88px; }
|
|
38
|
+
.classroom-update.docked .classroom-update-control { min-height: 52px; border: 0; border-radius: 0; padding: 0; box-shadow: none; }
|
|
39
|
+
.classroom-update.docked .classroom-update-control small { display: none; }
|
|
40
|
+
.classroom-update.docked .mascot-speech { right: 96px; width: min(184px, calc(100vw - 128px)); }
|
|
41
|
+
}
|
|
42
42
|
`;
|
|
43
|
-
exports.classroomUpdateScript = `
|
|
44
|
-
var classroomUpdateDisabled = [];
|
|
45
|
-
var classroomRestartSeen = '';
|
|
46
|
-
// Remember the reminder Fadou already read in this tab so reloads stay quiet until it changes.
|
|
47
|
-
var classroomRestartSpokenKey = 'vanspod-classroom-restart-spoken';
|
|
48
|
-
function classroomRestartSpoken() {
|
|
49
|
-
try { return sessionStorage.getItem(classroomRestartSpokenKey) || ''; } catch (error) { return ''; }
|
|
50
|
-
}
|
|
51
|
-
function rememberClassroomRestart(message) {
|
|
52
|
-
try {
|
|
53
|
-
if (message) sessionStorage.setItem(classroomRestartSpokenKey, message);
|
|
54
|
-
else sessionStorage.removeItem(classroomRestartSpokenKey);
|
|
55
|
-
} catch (error) {}
|
|
56
|
-
}
|
|
57
|
-
function showClassroomSpeech(message) {
|
|
58
|
-
var stage = document.querySelector('.classroom-update .connection-mascot');
|
|
59
|
-
if (!stage) return;
|
|
60
|
-
stage.querySelector('.mascot-speech')?.remove();
|
|
61
|
-
var speech = document.createElement('div');
|
|
62
|
-
speech.className = 'mascot-speech';
|
|
63
|
-
speech.setAttribute('role', 'status');
|
|
64
|
-
speech.setAttribute('aria-live', 'polite');
|
|
65
|
-
speech.setAttribute('data-classroom-speech', '');
|
|
66
|
-
var quote = document.createElement('p');
|
|
67
|
-
quote.className = 'mascot-quote';
|
|
68
|
-
quote.textContent = message;
|
|
69
|
-
var close = document.createElement('button');
|
|
70
|
-
close.type = 'button';
|
|
71
|
-
close.className = 'mascot-speech-close';
|
|
72
|
-
close.setAttribute('aria-label', '收起法鬥台詞');
|
|
73
|
-
close.textContent = '知道啦';
|
|
74
|
-
speech.append(quote, close);
|
|
75
|
-
stage.prepend(speech);
|
|
76
|
-
stage.classList.add('mascot-waiting', 'mascot-speaking');
|
|
77
|
-
mascotSpeechDismissed = false;
|
|
78
|
-
}
|
|
79
|
-
function classroomRestartText() {
|
|
80
|
-
return document.querySelector('[data-classroom-restart]')?.content.textContent.trim() || '';
|
|
81
|
-
}
|
|
82
|
-
function refreshClassroomRestart(html) {
|
|
83
|
-
var source = document.querySelector('[data-classroom-restart]');
|
|
84
|
-
if (!source) { classroomRestartSeen = ''; return; }
|
|
85
|
-
if (typeof html === 'string') source.innerHTML = html;
|
|
86
|
-
var message = classroomRestartText();
|
|
87
|
-
if (message === classroomRestartSeen) {
|
|
88
|
-
if (message && !mascotSpeechDismissed && !document.querySelector('.classroom-update .mascot-speech')) showClassroomSpeech(message);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
classroomRestartSeen = message;
|
|
92
|
-
var alreadySpoken = message !== '' && message === classroomRestartSpoken();
|
|
93
|
-
rememberClassroomRestart(message);
|
|
94
|
-
if (alreadySpoken) mascotSpeechDismissed = true;
|
|
95
|
-
else if (message) showClassroomSpeech(message);
|
|
96
|
-
else {
|
|
97
|
-
var stage = document.querySelector('.classroom-update .connection-mascot');
|
|
98
|
-
if (stage?.querySelector('[data-classroom-speech]') && !settingsBusy) hideMascotSpeech(stage);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
function returnClassroomUpdateHome() {
|
|
102
|
-
var home = document.querySelector('.classroom-update-home');
|
|
103
|
-
var companion = document.querySelector('.classroom-update');
|
|
104
|
-
if (home && companion) { companion.classList.remove('docked'); home.appendChild(companion); }
|
|
105
|
-
}
|
|
106
|
-
function positionClassroomUpdate() {
|
|
107
|
-
var card = document.querySelector('[data-setting="連線"]');
|
|
108
|
-
var home = card?.querySelector('.classroom-update-home');
|
|
109
|
-
var companion = document.querySelector('.classroom-update');
|
|
110
|
-
document.body.classList.toggle('has-classroom-update', Boolean(companion));
|
|
111
|
-
if (!home || !companion) return;
|
|
112
|
-
// Portal out of the card's CSS container so fixed positioning uses the viewport.
|
|
113
|
-
var docked = card.getBoundingClientRect().bottom < 24;
|
|
114
|
-
if (docked !== companion.classList.contains('docked')) {
|
|
115
|
-
var top = window.scrollY;
|
|
116
|
-
if (docked) home.style.minHeight = companion.offsetHeight + parseFloat(getComputedStyle(home).paddingBottom) + 'px';
|
|
117
|
-
companion.classList.toggle('docked', docked);
|
|
118
|
-
(docked ? document.body : home).appendChild(companion);
|
|
119
|
-
if (!docked) home.style.minHeight = '';
|
|
120
|
-
window.scrollTo({ top: top, behavior: 'instant' });
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
function classroomUpdateBusy(busy) {
|
|
124
|
-
var companion = document.querySelector('.classroom-update');
|
|
125
|
-
if (!companion) return;
|
|
126
|
-
if (busy) {
|
|
127
|
-
classroomUpdateDisabled = Array.from(companion.querySelectorAll('button'), function (button) {
|
|
128
|
-
var previous = { button: button, disabled: button.disabled };
|
|
129
|
-
button.disabled = true;
|
|
130
|
-
return previous;
|
|
131
|
-
});
|
|
132
|
-
} else {
|
|
133
|
-
classroomUpdateDisabled.forEach(function (previous) { previous.button.disabled = previous.disabled; });
|
|
134
|
-
classroomUpdateDisabled = [];
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function classroomUpdateFeedback(message) {
|
|
138
|
-
var companion = document.querySelector('.classroom-update');
|
|
139
|
-
var status = companion?.querySelector('[data-classroom-update-status]');
|
|
140
|
-
if (!status) return;
|
|
141
|
-
var warning = status.getAttribute('data-update-warning');
|
|
142
|
-
var result = message;
|
|
143
|
-
if (!message) {
|
|
144
|
-
classroomRestartSeen = classroomRestartText();
|
|
145
|
-
rememberClassroomRestart(classroomRestartSeen);
|
|
146
|
-
result = warning ? '有些內容還沒追到,先別急著怪披風。' + warning + (classroomRestartSeen ? ' ' + classroomRestartSeen : '')
|
|
147
|
-
: classroomRestartSeen || '課堂內容幫你更新好了,手速可以不用拿來爬回頁首了。';
|
|
148
|
-
}
|
|
149
|
-
showClassroomSpeech(result);
|
|
150
|
-
}
|
|
151
|
-
window.addEventListener('scroll', positionClassroomUpdate, { passive: true });
|
|
152
|
-
window.addEventListener('resize', positionClassroomUpdate);
|
|
153
|
-
positionClassroomUpdate();
|
|
154
|
-
refreshClassroomRestart();
|
|
43
|
+
exports.classroomUpdateScript = `
|
|
44
|
+
var classroomUpdateDisabled = [];
|
|
45
|
+
var classroomRestartSeen = '';
|
|
46
|
+
// Remember the reminder Fadou already read in this tab so reloads stay quiet until it changes.
|
|
47
|
+
var classroomRestartSpokenKey = 'vanspod-classroom-restart-spoken';
|
|
48
|
+
function classroomRestartSpoken() {
|
|
49
|
+
try { return sessionStorage.getItem(classroomRestartSpokenKey) || ''; } catch (error) { return ''; }
|
|
50
|
+
}
|
|
51
|
+
function rememberClassroomRestart(message) {
|
|
52
|
+
try {
|
|
53
|
+
if (message) sessionStorage.setItem(classroomRestartSpokenKey, message);
|
|
54
|
+
else sessionStorage.removeItem(classroomRestartSpokenKey);
|
|
55
|
+
} catch (error) {}
|
|
56
|
+
}
|
|
57
|
+
function showClassroomSpeech(message) {
|
|
58
|
+
var stage = document.querySelector('.classroom-update .connection-mascot');
|
|
59
|
+
if (!stage) return;
|
|
60
|
+
stage.querySelector('.mascot-speech')?.remove();
|
|
61
|
+
var speech = document.createElement('div');
|
|
62
|
+
speech.className = 'mascot-speech';
|
|
63
|
+
speech.setAttribute('role', 'status');
|
|
64
|
+
speech.setAttribute('aria-live', 'polite');
|
|
65
|
+
speech.setAttribute('data-classroom-speech', '');
|
|
66
|
+
var quote = document.createElement('p');
|
|
67
|
+
quote.className = 'mascot-quote';
|
|
68
|
+
quote.textContent = message;
|
|
69
|
+
var close = document.createElement('button');
|
|
70
|
+
close.type = 'button';
|
|
71
|
+
close.className = 'mascot-speech-close';
|
|
72
|
+
close.setAttribute('aria-label', '收起法鬥台詞');
|
|
73
|
+
close.textContent = '知道啦';
|
|
74
|
+
speech.append(quote, close);
|
|
75
|
+
stage.prepend(speech);
|
|
76
|
+
stage.classList.add('mascot-waiting', 'mascot-speaking');
|
|
77
|
+
mascotSpeechDismissed = false;
|
|
78
|
+
}
|
|
79
|
+
function classroomRestartText() {
|
|
80
|
+
return document.querySelector('[data-classroom-restart]')?.content.textContent.trim() || '';
|
|
81
|
+
}
|
|
82
|
+
function refreshClassroomRestart(html) {
|
|
83
|
+
var source = document.querySelector('[data-classroom-restart]');
|
|
84
|
+
if (!source) { classroomRestartSeen = ''; return; }
|
|
85
|
+
if (typeof html === 'string') source.innerHTML = html;
|
|
86
|
+
var message = classroomRestartText();
|
|
87
|
+
if (message === classroomRestartSeen) {
|
|
88
|
+
if (message && !mascotSpeechDismissed && !document.querySelector('.classroom-update .mascot-speech')) showClassroomSpeech(message);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
classroomRestartSeen = message;
|
|
92
|
+
var alreadySpoken = message !== '' && message === classroomRestartSpoken();
|
|
93
|
+
rememberClassroomRestart(message);
|
|
94
|
+
if (alreadySpoken) mascotSpeechDismissed = true;
|
|
95
|
+
else if (message) showClassroomSpeech(message);
|
|
96
|
+
else {
|
|
97
|
+
var stage = document.querySelector('.classroom-update .connection-mascot');
|
|
98
|
+
if (stage?.querySelector('[data-classroom-speech]') && !settingsBusy) hideMascotSpeech(stage);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function returnClassroomUpdateHome() {
|
|
102
|
+
var home = document.querySelector('.classroom-update-home');
|
|
103
|
+
var companion = document.querySelector('.classroom-update');
|
|
104
|
+
if (home && companion) { companion.classList.remove('docked'); home.appendChild(companion); }
|
|
105
|
+
}
|
|
106
|
+
function positionClassroomUpdate() {
|
|
107
|
+
var card = document.querySelector('[data-setting="連線"]');
|
|
108
|
+
var home = card?.querySelector('.classroom-update-home');
|
|
109
|
+
var companion = document.querySelector('.classroom-update');
|
|
110
|
+
document.body.classList.toggle('has-classroom-update', Boolean(companion));
|
|
111
|
+
if (!home || !companion) return;
|
|
112
|
+
// Portal out of the card's CSS container so fixed positioning uses the viewport.
|
|
113
|
+
var docked = card.getBoundingClientRect().bottom < 24;
|
|
114
|
+
if (docked !== companion.classList.contains('docked')) {
|
|
115
|
+
var top = window.scrollY;
|
|
116
|
+
if (docked) home.style.minHeight = companion.offsetHeight + parseFloat(getComputedStyle(home).paddingBottom) + 'px';
|
|
117
|
+
companion.classList.toggle('docked', docked);
|
|
118
|
+
(docked ? document.body : home).appendChild(companion);
|
|
119
|
+
if (!docked) home.style.minHeight = '';
|
|
120
|
+
window.scrollTo({ top: top, behavior: 'instant' });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function classroomUpdateBusy(busy) {
|
|
124
|
+
var companion = document.querySelector('.classroom-update');
|
|
125
|
+
if (!companion) return;
|
|
126
|
+
if (busy) {
|
|
127
|
+
classroomUpdateDisabled = Array.from(companion.querySelectorAll('button'), function (button) {
|
|
128
|
+
var previous = { button: button, disabled: button.disabled };
|
|
129
|
+
button.disabled = true;
|
|
130
|
+
return previous;
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
classroomUpdateDisabled.forEach(function (previous) { previous.button.disabled = previous.disabled; });
|
|
134
|
+
classroomUpdateDisabled = [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function classroomUpdateFeedback(message) {
|
|
138
|
+
var companion = document.querySelector('.classroom-update');
|
|
139
|
+
var status = companion?.querySelector('[data-classroom-update-status]');
|
|
140
|
+
if (!status) return;
|
|
141
|
+
var warning = status.getAttribute('data-update-warning');
|
|
142
|
+
var result = message;
|
|
143
|
+
if (!message) {
|
|
144
|
+
classroomRestartSeen = classroomRestartText();
|
|
145
|
+
rememberClassroomRestart(classroomRestartSeen);
|
|
146
|
+
result = warning ? '有些內容還沒追到,先別急著怪披風。' + warning + (classroomRestartSeen ? ' ' + classroomRestartSeen : '')
|
|
147
|
+
: classroomRestartSeen || '課堂內容幫你更新好了,手速可以不用拿來爬回頁首了。';
|
|
148
|
+
}
|
|
149
|
+
showClassroomSpeech(result);
|
|
150
|
+
}
|
|
151
|
+
window.addEventListener('scroll', positionClassroomUpdate, { passive: true });
|
|
152
|
+
window.addEventListener('resize', positionClassroomUpdate);
|
|
153
|
+
positionClassroomUpdate();
|
|
154
|
+
refreshClassroomRestart();
|
|
155
155
|
`;
|