|
26 | 26 |
|
27 | 27 | <!-- Left: back arrow or logo --> |
28 | 28 | <div class="flex items-center gap-2 min-w-0"> |
29 | | - <button x-show="['lectures','detail','settings'].includes(view)" |
| 29 | + <button x-show="['lectures','detail','settings','subscriptions'].includes(view)" |
30 | 30 | @click="goBack()" class="p-1 -ml-1 text-gray-600 hover:text-gray-900 flex-shrink-0"> |
31 | 31 | <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
32 | 32 | <path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/> |
|
36 | 36 | view === 'lectures' ? currentCourse?.title : |
37 | 37 | view === 'detail' ? currentLecture?.sub_title : |
38 | 38 | view === 'settings' ? 'Settings' : |
| 39 | + view === 'subscriptions' ? '编辑订阅' : |
39 | 40 | 'iCourse' |
40 | 41 | "></h1> |
41 | 42 | </div> |
|
63 | 64 | </svg> |
64 | 65 | Search |
65 | 66 | </button> |
| 67 | + <button @click="openSubscriptions()" |
| 68 | + class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors" |
| 69 | + :class="view === 'subscriptions' |
| 70 | + ? 'bg-blue-50 text-blue-600' |
| 71 | + : 'text-gray-500 hover:text-gray-800 hover:bg-gray-100'"> |
| 72 | + <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
| 73 | + <path stroke-linecap="round" stroke-linejoin="round" |
| 74 | + d="M19 11H5m14-4H5m14 8H5m14 4H5"/> |
| 75 | + </svg> |
| 76 | + 订阅 |
| 77 | + </button> |
66 | 78 | <button @click="openSettings()" |
67 | 79 | class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors" |
68 | 80 | :class="view === 'settings' |
|
102 | 114 | </header> |
103 | 115 |
|
104 | 116 | <!-- Mobile bottom nav (hidden on desktop) --> |
105 | | -<nav x-show="['courses','lectures','detail','search','settings'].includes(view)" |
| 117 | +<nav x-show="['courses','lectures','detail','search','settings','subscriptions'].includes(view)" |
106 | 118 | class="md:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 safe-bottom z-30"> |
107 | 119 | <div class="flex justify-around py-2"> |
108 | 120 | <button @click="_go('courses')" class="flex flex-col items-center gap-0.5 px-4 py-1" |
|
120 | 132 | </svg> |
121 | 133 | <span class="text-[10px]">Search</span> |
122 | 134 | </button> |
| 135 | + <button @click="openSubscriptions()" class="flex flex-col items-center gap-0.5 px-4 py-1" |
| 136 | + :class="view === 'subscriptions' ? 'text-blue-600' : 'text-gray-400'"> |
| 137 | + <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
| 138 | + <path stroke-linecap="round" stroke-linejoin="round" |
| 139 | + d="M19 11H5m14-4H5m14 8H5m14 4H5"/> |
| 140 | + </svg> |
| 141 | + <span class="text-[10px]">订阅</span> |
| 142 | + </button> |
123 | 143 | <button @click="openSettings()" class="flex flex-col items-center gap-0.5 px-4 py-1" |
124 | 144 | :class="view === 'settings' ? 'text-blue-600' : 'text-gray-400'"> |
125 | 145 | <svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> |
@@ -255,9 +275,17 @@ <h2 class="text-xl font-bold text-gray-800 mb-1">iCourse Subscriber</h2> |
255 | 275 | <button @click="openCourse(c.course_id)" |
256 | 276 | class="w-full text-left bg-white rounded-xl shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow active:bg-gray-50"> |
257 | 277 | <div class="flex items-start justify-between gap-3"> |
258 | | - <div class="min-w-0"> |
259 | | - <h3 class="font-semibold text-gray-800 truncate" x-text="c.title"></h3> |
260 | | - <p class="text-sm text-gray-500 mt-0.5" x-text="c.teacher"></p> |
| 278 | + <div class="min-w-0 flex items-start gap-2"> |
| 279 | + <button @click.stop="toggleStar(c.course_id)" |
| 280 | + class="flex-shrink-0 mt-0.5 text-lg leading-none" |
| 281 | + :class="isStarred(c.course_id) ? 'text-amber-400' : 'text-gray-300 hover:text-amber-400'" |
| 282 | + :title="isStarred(c.course_id) ? '取消置顶' : '置顶'"> |
| 283 | + <span x-text="isStarred(c.course_id) ? '★' : '☆'"></span> |
| 284 | + </button> |
| 285 | + <div class="min-w-0"> |
| 286 | + <h3 class="font-semibold text-gray-800 truncate" x-text="c.title"></h3> |
| 287 | + <p class="text-sm text-gray-500 mt-0.5" x-text="c.teacher"></p> |
| 288 | + </div> |
261 | 289 | </div> |
262 | 290 | <div class="flex flex-col items-end flex-shrink-0"> |
263 | 291 | <span class="text-xs font-medium bg-blue-50 text-blue-700 px-2 py-0.5 rounded-full" |
@@ -348,6 +376,27 @@ <h4 class="font-medium text-gray-800 text-sm truncate" x-text="lec.sub_title || |
348 | 376 | </template> |
349 | 377 | </div> |
350 | 378 |
|
| 379 | + <!-- Prev / next lecture nav (foot of detail view). Wraps within the |
| 380 | + current course's lecture list, in the same order the lectures |
| 381 | + page renders. Hidden on the only/last/first lecture as |
| 382 | + appropriate; respects sub_id ordering. --> |
| 383 | + <nav class="mt-8 pt-4 border-t border-gray-100 flex items-stretch gap-2 text-sm"> |
| 384 | + <button @click="gotoPrevLecture()" |
| 385 | + :disabled="!prevLecture()" |
| 386 | + class="flex-1 text-left px-3 py-2 rounded-lg border border-gray-200 hover:bg-gray-50 disabled:opacity-40 disabled:hover:bg-transparent"> |
| 387 | + <div class="text-xs text-gray-400">← 上一节</div> |
| 388 | + <div class="truncate text-gray-700" |
| 389 | + x-text="prevLecture()?.sub_title || '(无)'"></div> |
| 390 | + </button> |
| 391 | + <button @click="gotoNextLecture()" |
| 392 | + :disabled="!nextLecture()" |
| 393 | + class="flex-1 text-right px-3 py-2 rounded-lg border border-gray-200 hover:bg-gray-50 disabled:opacity-40 disabled:hover:bg-transparent"> |
| 394 | + <div class="text-xs text-gray-400">下一节 →</div> |
| 395 | + <div class="truncate text-gray-700" |
| 396 | + x-text="nextLecture()?.sub_title || '(无)'"></div> |
| 397 | + </button> |
| 398 | + </nav> |
| 399 | + |
351 | 400 | <!-- Mobile cycle button (FAB-style) --> |
352 | 401 | <button x-show="currentLecture" |
353 | 402 | @click="cycleDetailView()" |
@@ -377,14 +426,104 @@ <h4 class="font-medium text-gray-800 text-sm truncate" x-text="lec.sub_title || |
377 | 426 | <template x-for="r in searchResults" :key="r.sub_id"> |
378 | 427 | <button @click="openLecture(r.sub_id)" |
379 | 428 | class="w-full text-left bg-white rounded-lg border border-gray-100 p-3 hover:shadow-md transition-shadow active:bg-gray-50"> |
380 | | - <p class="text-xs text-blue-600 font-medium" x-text="r.course_title"></p> |
| 429 | + <div class="flex items-center justify-between gap-2"> |
| 430 | + <p class="text-xs text-blue-600 font-medium truncate" x-text="r.course_title"></p> |
| 431 | + <span class="text-[10px] uppercase tracking-wide text-gray-400 flex-shrink-0" |
| 432 | + x-text="r.hit_field === 'transcript' ? '转录命中' |
| 433 | + : r.hit_field === 'sub_title' ? '标题命中' |
| 434 | + : '摘要命中'"></span> |
| 435 | + </div> |
381 | 436 | <h4 class="font-medium text-gray-800 text-sm mt-0.5" x-text="r.sub_title || 'Untitled'"></h4> |
382 | | - <p class="text-xs text-gray-400 mt-1 line-clamp-2" x-html="highlight(r.summary, searchQuery)"></p> |
| 437 | + <p class="text-xs text-gray-400 mt-1 line-clamp-2" |
| 438 | + x-html="highlight(r.hit_field === 'transcript' ? r.transcript : r.summary, searchQuery)"></p> |
383 | 439 | </button> |
384 | 440 | </template> |
385 | 441 | </div> |
386 | 442 | </div> |
387 | 443 |
|
| 444 | + <!-- ─── SUBSCRIPTIONS (course list + checkbox + save to GitHub Secret) ─── --> |
| 445 | + <div x-show="view === 'subscriptions'" class="view-enter max-w-2xl mx-auto p-4"> |
| 446 | + |
| 447 | + <!-- Empty catalog message --> |
| 448 | + <div x-show="allCourses.length === 0" |
| 449 | + class="bg-yellow-50 border border-yellow-200 text-yellow-800 rounded-lg p-4 text-sm leading-relaxed"> |
| 450 | + 课程目录为空。请在仓库的 Actions secrets 里设置 <code class="bg-yellow-100 px-1 rounded">CRAWL_TERM</code> |
| 451 | + (例如 <code class="bg-yellow-100 px-1 rounded">25</code>)并跑一次 workflow 来抓取学期课程目录。 |
| 452 | + </div> |
| 453 | + |
| 454 | + <template x-if="allCourses.length > 0"> |
| 455 | + <div> |
| 456 | + <!-- Term selector + search --> |
| 457 | + <div class="flex flex-col sm:flex-row gap-2 mb-3"> |
| 458 | + <select x-model="subsTerm" @change="rebuildSubsFiltered()" |
| 459 | + class="border rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-300 focus:outline-none"> |
| 460 | + <template x-for="t in allCoursesTerms" :key="t"> |
| 461 | + <option :value="t" x-text="'Term ' + t"></option> |
| 462 | + </template> |
| 463 | + </select> |
| 464 | + <input x-model="subsSearch" @input="rebuildSubsFiltered()" type="text" |
| 465 | + placeholder="搜索课程名 / 老师 / 院系 / ID…" |
| 466 | + class="flex-1 border rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-300 focus:outline-none"> |
| 467 | + </div> |
| 468 | + |
| 469 | + <p class="text-xs text-gray-500 mb-3"> |
| 470 | + 已选 <span class="font-semibold text-blue-600" x-text="subsSelectedIds.length"></span> |
| 471 | + / 目录共 <span x-text="allCoursesForTerm.length"></span> 门课。 |
| 472 | + <span class="text-gray-400">(点击复选框切换订阅;保存后会写入 GitHub Secret <code class="bg-gray-100 px-1 rounded">COURSE_IDS</code>。)</span> |
| 473 | + </p> |
| 474 | + |
| 475 | + <!-- Course list --> |
| 476 | + <div class="bg-white border border-gray-100 rounded-lg overflow-hidden divide-y divide-gray-100 max-h-[60vh] overflow-y-auto"> |
| 477 | + <template x-for="c in subsFiltered" :key="c.course_id + '@' + c.term"> |
| 478 | + <label class="flex items-start gap-3 px-3 py-2 hover:bg-gray-50 cursor-pointer"> |
| 479 | + <input type="checkbox" class="mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-400" |
| 480 | + :checked="subsSelectedIds.includes(c.course_id)" |
| 481 | + @change="toggleSubscription(c.course_id, $event.target.checked)"> |
| 482 | + <div class="min-w-0 flex-1"> |
| 483 | + <p class="text-sm font-medium text-gray-800 truncate" x-text="c.title || '(no title)'"></p> |
| 484 | + <p class="text-xs text-gray-500 mt-0.5"> |
| 485 | + <span x-text="c.teacher || '—'"></span> |
| 486 | + <span x-show="c.dept" class="text-gray-400"> · <span x-text="c.dept"></span></span> |
| 487 | + <span class="text-gray-400"> · ID <span x-text="c.course_id"></span></span> |
| 488 | + </p> |
| 489 | + </div> |
| 490 | + </label> |
| 491 | + </template> |
| 492 | + <p x-show="subsFiltered.length === 0" class="text-center text-gray-400 text-sm py-6"> |
| 493 | + 没有匹配的课程。 |
| 494 | + </p> |
| 495 | + </div> |
| 496 | + |
| 497 | + <!-- Save error --> |
| 498 | + <p x-show="subsError" x-text="subsError" |
| 499 | + class="text-sm text-red-600 bg-red-50 rounded-lg p-3 mt-3"></p> |
| 500 | + |
| 501 | + <!-- Action buttons --> |
| 502 | + <div class="mt-4 flex flex-col sm:flex-row sm:items-center gap-2"> |
| 503 | + <button @click="saveSubscriptions()" :disabled="subsSaving" |
| 504 | + class="flex-1 bg-blue-500 text-white py-2.5 rounded-lg font-medium hover:bg-blue-600 disabled:opacity-50 flex items-center justify-center gap-2"> |
| 505 | + <div x-show="subsSaving" class="spinner !w-4 !h-4 !border-2 !border-white/30 !border-t-white"></div> |
| 506 | + <span x-text="subsSaving ? '加密上传中…' : '保存到 GitHub Secret'"></span> |
| 507 | + </button> |
| 508 | + <button @click="resetSubscriptionsToCurrent()" :disabled="subsSaving" |
| 509 | + class="text-sm text-gray-500 underline disabled:opacity-50"> |
| 510 | + 重置为当前已订阅 |
| 511 | + </button> |
| 512 | + </div> |
| 513 | + <button @click="triggerCheckRun()" :disabled="triggeringCheck" |
| 514 | + class="w-full mt-2 bg-gray-100 text-gray-700 py-2 rounded-lg text-sm font-medium hover:bg-gray-200 disabled:opacity-50 flex items-center justify-center gap-2"> |
| 515 | + <div x-show="triggeringCheck" class="spinner !w-4 !h-4 !border-2 !border-gray-400/30 !border-t-gray-700"></div> |
| 516 | + <span x-text="triggeringCheck ? '触发中…' : '立即跑一次 workflow(使用当前 secret)'"></span> |
| 517 | + </button> |
| 518 | + <p class="text-xs text-gray-400 mt-2 leading-relaxed"> |
| 519 | + 保存需要你的 GitHub PAT 具备 <strong>Secrets: Read and write</strong> 权限; |
| 520 | + 立即运行需要 <strong>Actions: Read and write</strong>。 |
| 521 | + COURSE_IDS 会被 libsodium sealed box 加密后才离开浏览器。 |
| 522 | + </p> |
| 523 | + </div> |
| 524 | + </template> |
| 525 | + </div> |
| 526 | + |
388 | 527 | <!-- ─── SETTINGS ─── --> |
389 | 528 | <div x-show="view === 'settings'" class="view-enter max-w-md mx-auto p-6"> |
390 | 529 | <h2 class="text-lg font-bold text-gray-800 mb-4">Credentials</h2> |
@@ -525,6 +664,8 @@ <h3 class="font-semibold text-gray-800">导出课程摘要</h3> |
525 | 664 | <!-- KaTeX + auto-render --> |
526 | 665 | <script src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js"></script> |
527 | 666 | <script src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/contrib/auto-render.min.js"></script> |
| 667 | +<!-- libsodium-wrappers (sealed-box encryption for GitHub Secrets PUT) --> |
| 668 | +<script src="https://cdn.jsdelivr.net/npm/libsodium-wrappers@0.7.13/dist/browsers/sodium.js"></script> |
528 | 669 | <!-- App scripts (defer, in order, before Alpine) --> |
529 | 670 | <script defer src="js/crypto.js"></script> |
530 | 671 | <script defer src="js/github.js"></script> |
|
0 commit comments