// ==UserScript== // @name TG任务助手前台面板 // @namespace tg-task-monitor-ui // @version 2.0.1 // @description 读取 TG任务状态后台扫描器 的共享结果,在 tg.zcst.edu.cn 页面右下角显示任务助手抽屉 // @author CODEX // @match https://tg.zcst.edu.cn/* // @match https://www.educoder.net/* // @storageName tg-exam-monitor-shared // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant unsafeWindow // @run-at document-idle // ==/UserScript== (function () { "use strict"; const MANUAL_LOGIN = ""; const STORE_KEY = "TG_EXAM_MONITOR_RESULT"; const STORE_KEY_TG_RESULT = "TG_EXAM_MONITOR_TG_RESULT"; const STORE_KEY_EDUCODER_RESULT = "TG_EXAM_MONITOR_EDUCODER_RESULT"; const STORE_KEY_LAST_ERROR = "TG_EXAM_MONITOR_LAST_ERROR"; const STORE_KEY_LAST_RUNNING = "TG_EXAM_MONITOR_LAST_RUNNING"; const STORE_KEY_FILTER_YEAR_MONTH = "TG_TASK_FILTER_YEAR_MONTH"; const STORE_KEY_AUTO_LOGIN = "TG_TASK_MONITOR_AUTO_LOGIN"; const STORE_KEY_COURSE_COLLAPSE = "TG_TASK_COURSE_COLLAPSE"; const STORE_KEY_SECTION_COLLAPSE = "TG_TASK_SECTION_COLLAPSE"; const STORE_KEY_PANEL_STATE = "TG_TASK_PANEL_STATE"; const STORE_KEY_REFRESH_REQUEST = "TG_TASK_ASSISTANT_REFRESH_REQUEST"; const STORE_KEY_REFRESH_STATUS = "TG_TASK_ASSISTANT_REFRESH_STATUS"; const STORE_KEY_REFRESH_HANDLED = "TG_TASK_REFRESH_HANDLED"; const DANGER_DAYS_THRESHOLD = 10; const DANGER_NOTIFY_KEY = "TG_TASK_ASSISTANT_DANGER_NOTIFY_KEY"; const ROOT_ID = "__tg_task_assistant_root__"; const BUTTON_ID = "__tg_task_assistant_button__"; const DRAWER_ID = "__tg_task_assistant_drawer__"; const SVG_FILTER_ID = "__tg_liquid_glass_filter__"; const OPEN_KEY = "TG_TASK_ASSISTANT_OPEN"; const FILTER_KEY = "TG_TASK_ASSISTANT_FILTER"; let countdownIntervalId = null; let pollTimerId = null; let resizeObserverAttached = false; let cacheDeletedNotice = ""; let latestResultLogin = ""; let jumpTaskRegistry = new Map(); let jumpTaskSeq = 0; let pollWasRunning = false; let latestRenderedScanTimestamp = 0; let resizeApplyTimer = null; let activeRefreshRequestId = ""; let activeRefreshStartedAt = 0; let lastLoggedRefreshStatusKey = ""; let panelAutoRefreshTimerId = null; let panelAutoRefreshRequested = false; const PANEL_AUTO_REFRESH_DELAY_MS = 2000; const css = ` #${BUTTON_ID}, #${DRAWER_ID} { --tg-cyan: #67e8f9; --tg-cyan-soft: rgba(103, 232, 249, .22); --tg-violet: #8b5cf6; --tg-violet-soft: rgba(139, 92, 246, .2); --tg-pink: #f0abfc; --tg-pink-soft: rgba(240, 171, 252, .18); --tg-red: #fb7185; --tg-amber: #fbbf24; --tg-green: #86efac; --tg-text: rgba(244, 247, 251, .94); --tg-muted: rgba(203, 213, 225, .68); --tg-faint: rgba(148, 163, 184, .48); --tg-glass: rgba(12, 18, 30, .64); --tg-glass-strong: rgba(15, 23, 42, .76); --tg-line: rgba(148, 227, 255, .18); font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, "Microsoft YaHei", sans-serif; color: var(--tg-text); box-sizing: border-box; letter-spacing: 0; } #${BUTTON_ID} *, #${DRAWER_ID} * { box-sizing: border-box; letter-spacing: 0; } #${BUTTON_ID} { position: fixed; right: 22px; bottom: 24px; z-index: 2147483646; width: 62px; height: 62px; border: 1px solid rgba(103, 232, 249, .34); border-radius: 50%; background: radial-gradient(circle at 32% 24%, rgba(255,255,255,.34), transparent 24%), radial-gradient(circle at 70% 76%, rgba(139,92,246,.34), transparent 34%), linear-gradient(145deg, rgba(12,18,30,.82), rgba(8,12,22,.66)); color: rgba(242, 251, 255, .96); box-shadow: 0 22px 52px rgba(0, 0, 0, .44), 0 0 34px rgba(103, 232, 249, .18), inset 0 1px 0 rgba(255,255,255,.24), inset 0 -18px 42px rgba(139,92,246,.12); backdrop-filter: blur(22px) saturate(145%); -webkit-backdrop-filter: blur(22px) saturate(145%); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 760; line-height: 1.16; text-align: center; overflow: visible; user-select: none; touch-action: none; transition: transform .38s cubic-bezier(.16, 1, .3, 1), border-color .38s ease, box-shadow .38s ease; animation: tgFloatButton 7s ease-in-out infinite; } #${BUTTON_ID}::before { content: ""; position: absolute; inset: -9px; border-radius: inherit; background: conic-gradient(from 140deg, transparent, rgba(103,232,249,.22), transparent, rgba(240,171,252,.16), transparent); filter: blur(12px); opacity: .7; z-index: -1; } #${BUTTON_ID}:hover { transform: translateY(-4px) scale(1.035); border-color: rgba(103, 232, 249, .62); box-shadow: 0 28px 70px rgba(0, 0, 0, .52), 0 0 48px rgba(103, 232, 249, .26), 0 0 58px rgba(139, 92, 246, .16), inset 0 1px 0 rgba(255,255,255,.32); } #${BUTTON_ID} .tg-task-button-badge { position: absolute; right: -4px; top: -5px; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,.44); background: linear-gradient(135deg, rgba(251,113,133,.94), rgba(240,171,252,.74)); color: #fff; font-size: 11px; line-height: 20px; box-shadow: 0 0 20px rgba(251,113,133,.34); } #${DRAWER_ID} { position: fixed; right: 18px; top: 18px; z-index: 2147483647; width: min(520px, calc(100vw - 28px)); height: min(760px, calc(100vh - 48px)); min-width: 390px; min-height: 480px; max-width: calc(100vw - 40px); max-height: calc(100vh - 40px); resize: both; display: flex; flex-direction: column; overflow: hidden; border-radius: 30px; color: var(--tg-text); background: linear-gradient(145deg, rgba(14, 21, 36, .72), rgba(6, 10, 18, .68)), radial-gradient(circle at 18% 0%, rgba(103,232,249,.12), transparent 38%), radial-gradient(circle at 92% 8%, rgba(139,92,246,.16), transparent 42%); border: 1px solid rgba(160, 231, 255, .2); box-shadow: -34px 28px 90px rgba(0,0,0,.56), 0 0 0 1px rgba(255,255,255,.035) inset, 0 0 56px rgba(103,232,249,.12), 0 0 88px rgba(139,92,246,.1); backdrop-filter: blur(34px) saturate(150%); -webkit-backdrop-filter: blur(34px) saturate(150%); transform: translateX(calc(100% + 38px)) scale(.985); opacity: .42; transition: transform .58s cubic-bezier(.16, 1, .3, 1), opacity .38s ease, box-shadow .45s ease; } #${DRAWER_ID}.tg-open { transform: translateX(0) scale(1); opacity: 1; } #${DRAWER_ID}::before { content: ""; position: absolute; inset: 0; pointer-events: none; background-image: radial-gradient(circle at 14% 16%, rgba(103,232,249,.22) 0 1px, transparent 2px), radial-gradient(circle at 84% 22%, rgba(240,171,252,.18) 0 1px, transparent 2px), radial-gradient(circle at 64% 74%, rgba(139,92,246,.16) 0 1px, transparent 2px), linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.024) 1px, transparent 1px); background-size: 128px 128px, 184px 184px, 148px 148px, 44px 44px, 44px 44px; mask-image: linear-gradient(to bottom, rgba(0,0,0,.92), rgba(0,0,0,.26)); opacity: .42; animation: tgParticleDrift 22s linear infinite; } #${DRAWER_ID}::after { content: ""; position: absolute; inset: 1px; pointer-events: none; border-radius: 29px; background: linear-gradient(135deg, rgba(255,255,255,.18), transparent 24%), linear-gradient(315deg, rgba(103,232,249,.1), transparent 28%), radial-gradient(circle at 78% 14%, rgba(240,171,252,.12), transparent 34%); mix-blend-mode: screen; opacity: .55; } #${DRAWER_ID} .tg-ambient { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; } #${DRAWER_ID} .tg-orb, #${DRAWER_ID} .tg-ring { position: absolute; display: block; border-radius: 999px; filter: blur(.2px); opacity: .7; transform: translate3d(0,0,0); } #${DRAWER_ID} .tg-orb-a { width: 150px; height: 150px; right: -42px; top: 70px; background: radial-gradient(circle at 34% 30%, rgba(255,255,255,.38), rgba(103,232,249,.18) 28%, rgba(103,232,249,.04) 68%, transparent 74%); filter: blur(1px); animation: tgSlowFloatA 15s ease-in-out infinite; } #${DRAWER_ID} .tg-orb-b { width: 92px; height: 92px; left: 32px; bottom: 92px; background: radial-gradient(circle at 34% 28%, rgba(255,255,255,.25), rgba(240,171,252,.18) 38%, transparent 72%); filter: blur(1.5px); animation: tgSlowFloatB 18s ease-in-out infinite; } #${DRAWER_ID} .tg-ring-a { width: 178px; height: 178px; left: -78px; top: 178px; border: 1px solid rgba(103,232,249,.16); box-shadow: inset 0 0 34px rgba(103,232,249,.08), 0 0 30px rgba(139,92,246,.08); transform: rotate(-18deg); animation: tgRingDrift 21s ease-in-out infinite; } #${DRAWER_ID} .tg-header, #${DRAWER_ID} .tg-body, #${DRAWER_ID} .tg-actions { position: relative; z-index: 1; } #${DRAWER_ID} .tg-header { flex: 0 0 auto; min-height: 78px; padding: 18px 18px 14px; border-bottom: 1px solid rgba(148, 227, 255, .13); background: linear-gradient(180deg, rgba(255,255,255,.055), rgba(255,255,255,.018)); display: flex; align-items: center; justify-content: space-between; gap: 14px; } #${DRAWER_ID} .tg-title { min-width: 0; } #${DRAWER_ID} .tg-title-main { font-size: 22px; font-weight: 760; line-height: 1.15; color: rgba(248, 250, 252, .98); text-shadow: 0 0 22px rgba(103,232,249,.12); } #${DRAWER_ID} .tg-title-sub { margin-top: 6px; color: var(--tg-muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #${DRAWER_ID} .tg-header-actions { display: flex; gap: 8px; flex: 0 0 auto; } #${DRAWER_ID} .tg-icon-btn { width: 34px; height: 34px; border: 1px solid rgba(148, 227, 255, .18); background: rgba(255,255,255,.055); color: rgba(235, 245, 255, .9); border-radius: 12px; cursor: pointer; font-size: 13px; line-height: 32px; text-align: center; box-shadow: inset 0 1px 0 rgba(255,255,255,.12); transition: transform .28s cubic-bezier(.16, 1, .3, 1), border-color .28s ease, background .28s ease, box-shadow .28s ease; } #${DRAWER_ID} .tg-icon-btn:hover { transform: translateY(-2px); border-color: rgba(103,232,249,.44); background: rgba(103,232,249,.09); box-shadow: 0 0 22px rgba(103,232,249,.12), inset 0 1px 0 rgba(255,255,255,.18); } #${DRAWER_ID} .tg-top-refresh { width: auto; min-width: 74px; padding: 0 11px; font-size: 12px; white-space: nowrap; } #${DRAWER_ID} .tg-body { flex: 1 1 auto; overflow-y: auto; padding: 16px; scrollbar-width: thin; scrollbar-color: rgba(103,232,249,.28) transparent; } #${DRAWER_ID} .tg-body::-webkit-scrollbar { width: 9px; } #${DRAWER_ID} .tg-body::-webkit-scrollbar-thumb { background: rgba(103,232,249,.2); border: 3px solid transparent; border-radius: 999px; background-clip: padding-box; } #${DRAWER_ID} .tg-meta, #${DRAWER_ID} .tg-alert, #${DRAWER_ID} .tg-empty, #${DRAWER_ID} .tg-date-filter, #${DRAWER_ID} .tg-summary-item, #${DRAWER_ID} .tg-card { position: relative; overflow: hidden; border: 1px solid rgba(148, 227, 255, .15); background: linear-gradient(145deg, rgba(255,255,255,.09), rgba(255,255,255,.035)), rgba(8, 13, 24, .42); box-shadow: 0 18px 42px rgba(0,0,0,.25), inset 0 1px 0 rgba(255,255,255,.09), inset 0 -1px 0 rgba(255,255,255,.035); backdrop-filter: blur(20px) saturate(145%); -webkit-backdrop-filter: blur(20px) saturate(145%); } #${DRAWER_ID} .tg-meta::before, #${DRAWER_ID} .tg-alert::before, #${DRAWER_ID} .tg-empty::before, #${DRAWER_ID} .tg-date-filter::before, #${DRAWER_ID} .tg-summary-item::before, #${DRAWER_ID} .tg-card::before { content: ""; position: absolute; inset: 0; pointer-events: none; background-image: radial-gradient(circle at 18% 0%, rgba(255,255,255,.12), transparent 34%), repeating-linear-gradient(0deg, rgba(255,255,255,.025) 0 1px, transparent 1px 4px); opacity: .38; mix-blend-mode: screen; } #${DRAWER_ID} .tg-meta, #${DRAWER_ID} .tg-alert, #${DRAWER_ID} .tg-empty { border-radius: 22px; padding: 14px; margin-bottom: 12px; color: var(--tg-muted); font-size: 12px; line-height: 1.65; } #${DRAWER_ID} .tg-meta { padding: 15px; } #${DRAWER_ID} .tg-refresh-status { border-radius: 20px; padding: 12px; margin-bottom: 12px; border: 1px solid rgba(148, 227, 255, .14); background: linear-gradient(145deg, rgba(255,255,255,.075), rgba(255,255,255,.025)), rgba(8, 13, 24, .38); box-shadow: 0 16px 36px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.08); backdrop-filter: blur(18px) saturate(140%); -webkit-backdrop-filter: blur(18px) saturate(140%); } #${DRAWER_ID} .tg-refresh-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 9px; } #${DRAWER_ID} .tg-refresh-title { color: rgba(245,250,255,.92); font-size: 12px; font-weight: 760; } #${DRAWER_ID} .tg-refresh-stage { color: var(--tg-muted); font-size: 11px; line-height: 1.5; overflow-wrap: anywhere; } #${DRAWER_ID} .tg-progress-track { height: 7px; overflow: hidden; border-radius: 999px; border: 1px solid rgba(148, 227, 255, .12); background: rgba(255,255,255,.045); box-shadow: inset 0 1px 4px rgba(0,0,0,.28); } #${DRAWER_ID} .tg-progress-fill { height: 100%; width: 0%; border-radius: inherit; background: linear-gradient(90deg, rgba(103,232,249,.72), rgba(139,92,246,.62), rgba(240,171,252,.52)); box-shadow: 0 0 18px rgba(103,232,249,.22); transition: width .42s cubic-bezier(.16, 1, .3, 1); } #${DRAWER_ID} .tg-meta-kicker { color: rgba(103,232,249,.86); font-size: 11px; font-weight: 720; margin-bottom: 12px; } #${DRAWER_ID} .tg-meta-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 9px; } #${DRAWER_ID} .tg-meta-chip { border: 1px solid rgba(148, 227, 255, .12); background: rgba(255,255,255,.045); border-radius: 16px; padding: 9px 10px; min-width: 0; } #${DRAWER_ID} .tg-meta-label { display: block; color: var(--tg-faint); font-size: 11px; margin-bottom: 4px; } #${DRAWER_ID} .tg-meta-value { display: block; color: var(--tg-text); font-size: 12px; font-weight: 680; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #${DRAWER_ID} .tg-alert { color: rgba(255, 228, 230, .95); background: linear-gradient(145deg, rgba(251,113,133,.16), rgba(255,255,255,.035)), rgba(40, 9, 20, .34); border-color: rgba(251,113,133,.26); box-shadow: 0 18px 44px rgba(77, 10, 30, .25), inset 0 1px 0 rgba(255,255,255,.09); } #${DRAWER_ID} .tg-empty { color: rgba(203, 213, 225, .74); text-align: center; padding: 18px; } #${DRAWER_ID} .tg-summary { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 9px; margin-bottom: 12px; } #${DRAWER_ID} .tg-summary-item { min-width: 0; width: 100%; border-radius: 20px; padding: 12px 8px 11px; text-align: left; cursor: pointer; color: inherit; font: inherit; appearance: none; -webkit-appearance: none; transition: transform .36s cubic-bezier(.16, 1, .3, 1), border-color .36s ease, box-shadow .36s ease, background .36s ease; } #${DRAWER_ID} .tg-summary-item:hover { transform: translateY(-4px); border-color: rgba(103,232,249,.34); background: linear-gradient(145deg, rgba(103,232,249,.1), rgba(255,255,255,.04)), rgba(9, 16, 28, .54); box-shadow: 0 24px 54px rgba(0,0,0,.3), 0 0 26px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.13); } #${DRAWER_ID} .tg-summary-item.tg-selected { border-color: rgba(103,232,249,.48); background: linear-gradient(145deg, rgba(103,232,249,.16), rgba(139,92,246,.09)), rgba(9, 16, 28, .62); box-shadow: 0 24px 58px rgba(0,0,0,.34), 0 0 32px rgba(103,232,249,.18), 0 0 34px rgba(139,92,246,.11), inset 0 1px 0 rgba(255,255,255,.15); } #${DRAWER_ID} .tg-summary-item.tg-selected .tg-summary-num { color: rgba(165,243,252,.98); text-shadow: 0 0 18px rgba(103,232,249,.18); } #${DRAWER_ID} .tg-summary-item[data-tooltip]::after { content: attr(data-tooltip); position: absolute; left: 50%; bottom: calc(100% + 10px); width: min(230px, 72vw); transform: translate(-50%, 8px); opacity: 0; pointer-events: none; color: rgba(235,245,255,.94); background: linear-gradient(145deg, rgba(18, 27, 44, .94), rgba(8, 13, 24, .9)), rgba(8, 13, 24, .92); border: 1px solid rgba(148, 227, 255, .18); border-radius: 12px; padding: 8px 10px; font-size: 11px; line-height: 1.45; box-shadow: 0 16px 36px rgba(0,0,0,.38), 0 0 24px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.08); backdrop-filter: blur(18px) saturate(145%); -webkit-backdrop-filter: blur(18px) saturate(145%); transition: opacity .22s ease, transform .22s cubic-bezier(.16, 1, .3, 1); z-index: 6; } #${DRAWER_ID} .tg-summary-item[data-tooltip]:hover::after { opacity: 1; transform: translate(-50%, 0); } #${DRAWER_ID} .tg-summary-num { position: relative; font-size: 24px; font-weight: 780; line-height: 1; margin-bottom: 8px; color: rgba(248,250,252,.96); } #${DRAWER_ID} .tg-summary-label { position: relative; color: var(--tg-muted); font-size: 11px; white-space: nowrap; } #${DRAWER_ID} .tg-current-filter { margin: -2px 2px 12px; color: rgba(203, 213, 225, .76); font-size: 12px; font-weight: 650; } #${DRAWER_ID} .tg-current-filter strong { color: rgba(165,243,252,.94); font-weight: 760; } #${DRAWER_ID} .tg-date-filter { border-radius: 22px; padding: 13px; margin-bottom: 12px; } #${DRAWER_ID} .tg-date-filter-row { position: relative; display: flex; align-items: center; gap: 8px; color: rgba(235,245,255,.9); font-size: 12px; font-weight: 650; } #${DRAWER_ID} .tg-time-mark { position: relative; width: 28px; height: 28px; flex: 0 0 auto; border-radius: 10px; border: 1px solid rgba(103,232,249,.22); background: linear-gradient(180deg, rgba(103,232,249,.14), rgba(139,92,246,.08)), rgba(255,255,255,.04); box-shadow: 0 0 18px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.16); } #${DRAWER_ID} .tg-time-mark::before, #${DRAWER_ID} .tg-time-mark::after { content: ""; position: absolute; display: block; } #${DRAWER_ID} .tg-time-mark::before { left: 7px; right: 7px; top: 8px; height: 2px; border-radius: 999px; background: rgba(103,232,249,.62); box-shadow: 0 7px 0 rgba(103,232,249,.2); } #${DRAWER_ID} .tg-time-mark::after { left: 8px; top: 6px; width: 12px; height: 14px; border: 1px solid rgba(245,250,255,.28); border-radius: 4px; } #${DRAWER_ID} .tg-date-filter select { height: 34px; min-width: 84px; border: 1px solid rgba(148, 227, 255, .18); background: rgba(2, 6, 23, .42); color: rgba(245, 250, 255, .94); border-radius: 12px; padding: 0 10px; font-size: 12px; outline: none; box-shadow: inset 0 1px 0 rgba(255,255,255,.08); transition: border-color .24s ease, box-shadow .24s ease, background .24s ease; } #${DRAWER_ID} .tg-date-filter select:focus, #${DRAWER_ID} .tg-date-filter select:hover { border-color: rgba(103,232,249,.42); box-shadow: 0 0 24px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.12); } #${DRAWER_ID} .tg-date-filter select option { background: #0f172a; color: rgba(245,250,255,.94); } #${DRAWER_ID} .tg-date-filter-hint { position: relative; margin-top: 9px; color: var(--tg-muted); font-size: 12px; } #${DRAWER_ID} .tg-action-btn, #${DRAWER_ID} .tg-detail-link { min-height: 34px; border: 1px solid rgba(148, 227, 255, .14); background: rgba(255,255,255,.045); color: rgba(226, 232, 240, .86); border-radius: 14px; cursor: pointer; padding: 0 9px; font-size: 12px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; box-shadow: inset 0 1px 0 rgba(255,255,255,.07); transition: transform .28s cubic-bezier(.16, 1, .3, 1), border-color .28s ease, background .28s ease, box-shadow .28s ease, color .28s ease; } #${DRAWER_ID} .tg-action-btn:hover, #${DRAWER_ID} .tg-detail-link:hover { transform: translateY(-2px); border-color: rgba(103,232,249,.42); color: rgba(245,250,255,.98); background: linear-gradient(135deg, rgba(103,232,249,.14), rgba(139,92,246,.1)), rgba(255,255,255,.055); box-shadow: 0 0 24px rgba(103,232,249,.12), inset 0 1px 0 rgba(255,255,255,.12); } #${DRAWER_ID} .tg-section { margin-bottom: 16px; } #${DRAWER_ID} .tg-course-group { margin-bottom: 16px; border: 1px solid rgba(148, 227, 255, .13); background: linear-gradient(145deg, rgba(255,255,255,.06), rgba(255,255,255,.025)), rgba(8, 13, 24, .34); border-radius: 24px; padding: 12px; box-shadow: 0 18px 42px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.07); backdrop-filter: blur(18px) saturate(135%); -webkit-backdrop-filter: blur(18px) saturate(135%); } #${DRAWER_ID} .tg-course-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: center; color: rgba(248,250,252,.96); font-size: 14px; font-weight: 760; margin: 2px 2px 10px; overflow-wrap: anywhere; cursor: pointer; } #${DRAWER_ID} .tg-course-title { min-width: 0; overflow-wrap: anywhere; } #${DRAWER_ID} .tg-course-stats { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } #${DRAWER_ID} .tg-course-stat { color: rgba(203,213,225,.76); background: rgba(255,255,255,.045); border: 1px solid rgba(148, 227, 255, .1); border-radius: 999px; padding: 3px 7px; font-size: 11px; font-weight: 620; } #${DRAWER_ID} .tg-course-toggle { width: 34px; height: 34px; border-radius: 13px; border: 1px solid rgba(148, 227, 255, .16); background: rgba(255,255,255,.045); color: rgba(235,245,255,.9); cursor: pointer; box-shadow: inset 0 1px 0 rgba(255,255,255,.08); transition: transform .28s cubic-bezier(.16, 1, .3, 1), border-color .28s ease, box-shadow .28s ease; } #${DRAWER_ID} .tg-course-toggle:hover { transform: translateY(-2px); border-color: rgba(103,232,249,.4); box-shadow: 0 0 22px rgba(103,232,249,.12), inset 0 1px 0 rgba(255,255,255,.12); } #${DRAWER_ID} .tg-course-content { display: grid; grid-template-rows: 1fr; transition: grid-template-rows .34s cubic-bezier(.16, 1, .3, 1), opacity .28s ease; opacity: 1; } #${DRAWER_ID} .tg-course-content-inner { overflow: hidden; } #${DRAWER_ID} .tg-course-group.tg-course-collapsed .tg-course-content { grid-template-rows: 0fr; opacity: 0; } #${DRAWER_ID} .tg-course-subgroup { margin-top: 10px; border: 1px solid rgba(148, 227, 255, .09); border-radius: 18px; background: rgba(255,255,255,.025); padding: 9px; } #${DRAWER_ID} .tg-subgroup-title { display: flex; align-items: center; justify-content: space-between; gap: 10px; color: rgba(203,213,225,.76); font-size: 12px; font-weight: 720; margin: 0 2px; cursor: pointer; user-select: none; } #${DRAWER_ID} .tg-subgroup-title-main { min-width: 0; color: rgba(235,245,255,.88); } #${DRAWER_ID} .tg-subgroup-count { flex: 0 0 auto; color: var(--tg-muted); border: 1px solid rgba(148, 227, 255, .1); background: rgba(255,255,255,.04); border-radius: 999px; padding: 2px 7px; font-size: 11px; } #${DRAWER_ID} .tg-subgroup-content { display: grid; grid-template-rows: 1fr; margin-top: 8px; transition: grid-template-rows .32s cubic-bezier(.16, 1, .3, 1), opacity .26s ease; opacity: 1; } #${DRAWER_ID} .tg-subgroup-content-inner { overflow: hidden; } #${DRAWER_ID} .tg-section-collapsed .tg-subgroup-content { grid-template-rows: 0fr; opacity: 0; } #${DRAWER_ID} .tg-section-title { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; color: rgba(245,250,255,.92); font-size: 13px; font-weight: 760; margin: 15px 3px 9px; } #${DRAWER_ID} .tg-section-title::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--tg-cyan); box-shadow: 0 0 14px rgba(103,232,249,.52); margin-right: 1px; } #${DRAWER_ID} .tg-section-title span:first-child { flex: 1 1 auto; } #${DRAWER_ID} .tg-section-count { color: var(--tg-muted); font-size: 12px; font-weight: 560; } #${DRAWER_ID} .tg-section-urgent .tg-section-title::before { background: var(--tg-red); box-shadow: 0 0 16px rgba(251,113,133,.42); } #${DRAWER_ID} .tg-card { border-radius: 22px; padding: 13px; margin-bottom: 10px; line-height: 1.55; transition: transform .36s cubic-bezier(.16, 1, .3, 1), border-color .36s ease, box-shadow .36s ease, background .36s ease; } #${DRAWER_ID} .tg-card:hover { transform: translateY(-4px); border-color: rgba(103,232,249,.32); background: linear-gradient(145deg, rgba(255,255,255,.105), rgba(255,255,255,.045)), rgba(10, 17, 30, .54); box-shadow: 0 28px 64px rgba(0,0,0,.34), 0 0 34px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.13); } #${DRAWER_ID} .tg-card.tg-state-ok { border-color: rgba(134,239,172,.18); } #${DRAWER_ID} .tg-card.tg-state-warn { border-color: rgba(251,191,36,.22); } #${DRAWER_ID} .tg-card.tg-state-bad { border-color: rgba(251,113,133,.28); box-shadow: 0 20px 48px rgba(54, 8, 24, .26), 0 0 32px rgba(251,113,133,.08), inset 0 1px 0 rgba(255,255,255,.1); } #${DRAWER_ID} .tg-card.tg-state-muted { border-color: rgba(148,163,184,.14); } #${DRAWER_ID} .tg-card-head { position: relative; display: flex; gap: 10px; justify-content: space-between; align-items: flex-start; margin-bottom: 9px; } #${DRAWER_ID} .tg-card-title { min-width: 0; color: rgba(248,250,252,.96); font-size: 14px; font-weight: 720; line-height: 1.45; overflow-wrap: anywhere; } #${DRAWER_ID} .tg-card-title a { color: inherit; text-decoration: none; } #${DRAWER_ID} .tg-card-title a:hover { color: rgba(165,243,252,.98); } #${DRAWER_ID} .tg-type { flex: 0 0 auto; border: 1px solid rgba(148, 227, 255, .18); border-radius: 999px; padding: 3px 8px; color: rgba(225, 245, 255, .82); font-size: 11px; font-weight: 680; background: rgba(255,255,255,.045); box-shadow: inset 0 1px 0 rgba(255,255,255,.08); } #${DRAWER_ID} .tg-status { position: relative; font-size: 13px; font-weight: 760; margin: 7px 0 6px; } #${DRAWER_ID} .tg-status-row { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; } #${DRAWER_ID} .tg-helper-countdown { display: inline-flex; align-items: center; justify-content: center; min-height: 26px; border-radius: 999px; border: 1px solid rgba(251,191,36,.34); background: linear-gradient(135deg, rgba(251,191,36,.16), rgba(251,113,133,.1)), rgba(255,255,255,.045); color: rgba(255, 237, 213, .96); padding: 3px 9px; font-size: 11px; font-weight: 760; box-shadow: 0 0 24px rgba(251,191,36,.13), inset 0 1px 0 rgba(255,255,255,.1); white-space: nowrap; animation: tgCountdownPulse 2.8s ease-in-out infinite; } #${DRAWER_ID} .tg-ok { color: var(--tg-green); } #${DRAWER_ID} .tg-warn { color: var(--tg-amber); } #${DRAWER_ID} .tg-bad { color: var(--tg-red); } #${DRAWER_ID} .tg-muted { color: var(--tg-muted); } #${DRAWER_ID} .tg-small, #${DRAWER_ID} .tg-task-meta { position: relative; color: var(--tg-muted); font-size: 12px; line-height: 1.7; overflow-wrap: anywhere; } #${DRAWER_ID} .tg-task-meta { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; margin-top: 10px; } #${DRAWER_ID} .tg-task-meta span { display: block; border: 1px solid rgba(148, 227, 255, .1); background: rgba(255,255,255,.035); border-radius: 13px; padding: 7px 8px; } #${DRAWER_ID} .tg-card-footer { position: relative; display: flex; justify-content: flex-end; margin-top: 11px; } #${DRAWER_ID} .tg-detail-link { display: inline-flex; align-items: center; justify-content: center; min-height: 30px; text-decoration: none; padding: 0 12px; border: 1px solid rgba(103,232,249,.22); border-radius: 999px; color: rgba(225,245,255,.92); background: rgba(255,255,255,.045); cursor: pointer; font: inherit; font-size: 12px; transition: transform .25s cubic-bezier(.16, 1, .3, 1), border-color .25s ease, box-shadow .25s ease; } #${DRAWER_ID} .tg-detail-link:hover { transform: translateY(-1px); border-color: rgba(103,232,249,.45); box-shadow: 0 0 18px rgba(103,232,249,.12); } #${DRAWER_ID} .tg-actions { flex: 0 0 auto; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 10px; padding: 12px 16px 16px; border-top: 1px solid rgba(148, 227, 255, .13); background: linear-gradient(180deg, rgba(8,13,24,.44), rgba(8,13,24,.78)); backdrop-filter: blur(24px) saturate(145%); -webkit-backdrop-filter: blur(24px) saturate(145%); } #${DRAWER_ID} .tg-action-btn { min-height: 40px; border-radius: 16px; } #${DRAWER_ID} [data-copy-all-json] { border-color: rgba(103,232,249,.28); background: linear-gradient(135deg, rgba(103,232,249,.13), rgba(139,92,246,.11)), rgba(255,255,255,.055); color: rgba(245,250,255,.96); } #${DRAWER_ID} [data-delete-cache] { border-color: rgba(251,113,133,.22); background: linear-gradient(135deg, rgba(251,113,133,.1), rgba(251,191,36,.06)), rgba(255,255,255,.045); color: rgba(255,228,230,.94); } #${DRAWER_ID} textarea.tg-json-box { width: 100%; height: 260px; margin-top: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, Monaco, monospace; font-size: 12px; line-height: 1.5; border: 1px solid rgba(148, 227, 255, .16); border-radius: 18px; padding: 12px; resize: vertical; color: rgba(235,245,255,.94); background: rgba(2,6,23,.58); box-shadow: inset 0 1px 0 rgba(255,255,255,.08); } @keyframes tgFloatButton { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } } @keyframes tgParticleDrift { from { background-position: 0 0, 0 0, 0 0, 0 0, 0 0; } to { background-position: 128px 70px, -184px 120px, 148px -90px, 44px 44px, -44px 44px; } } @keyframes tgSlowFloatA { 0%, 100% { transform: translate3d(0,0,0) scale(1); opacity: .58; } 50% { transform: translate3d(-18px,24px,0) scale(1.06); opacity: .76; } } @keyframes tgSlowFloatB { 0%, 100% { transform: translate3d(0,0,0) scale(1); opacity: .44; } 50% { transform: translate3d(22px,-18px,0) scale(1.08); opacity: .66; } } @keyframes tgRingDrift { 0%, 100% { transform: rotate(-18deg) translate3d(0,0,0); opacity: .36; } 50% { transform: rotate(8deg) translate3d(18px,10px,0); opacity: .52; } } @keyframes tgCountdownPulse { 0%, 100% { box-shadow: 0 0 20px rgba(251,191,36,.11), inset 0 1px 0 rgba(255,255,255,.1); } 50% { box-shadow: 0 0 30px rgba(251,191,36,.22), 0 0 18px rgba(251,113,133,.12), inset 0 1px 0 rgba(255,255,255,.14); } } /* iOS 26 Liquid Glass practical skin */ #${BUTTON_ID}, #${DRAWER_ID} { --tg-liquid-bg: rgba(255,255,255,.16); --tg-liquid-bg-strong: rgba(20, 25, 36, .72); --tg-liquid-card: rgba(255,255,255,.13); --tg-liquid-card-strong: rgba(255,255,255,.2); --tg-liquid-border: rgba(255,255,255,.35); --tg-liquid-border-soft: rgba(255,255,255,.22); --tg-liquid-shadow: 0 20px 60px rgba(0,0,0,.25); --tg-text: rgba(255,255,255,.96); --tg-muted: rgba(232,238,247,.76); --tg-faint: rgba(220,228,240,.58); --tg-cyan: #8ee8ff; --tg-violet: #b8a7ff; --tg-pink: #ffc5e8; --tg-red: #ff6b7f; --tg-amber: #ffd166; --tg-green: #8ce99a; text-shadow: 0 1px 1px rgba(0,0,0,.24); } #${BUTTON_ID} { width: auto; min-width: 132px; height: 48px; padding: 0 18px; border-radius: 999px; border: 1px solid var(--tg-liquid-border); background: linear-gradient(135deg, rgba(255,255,255,.32), rgba(255,255,255,.12)), rgba(255,255,255,.16); color: rgba(255,255,255,.96); box-shadow: var(--tg-liquid-shadow), inset 0 1px 0 rgba(255,255,255,.42), inset 0 -1px 0 rgba(255,255,255,.14); backdrop-filter: blur(22px) saturate(180%); -webkit-backdrop-filter: blur(22px) saturate(180%); font-size: 13px; font-weight: 780; line-height: 1; animation: none; transition: transform .18s cubic-bezier(.2, .8, .2, 1), border-color .18s ease, box-shadow .18s ease, background .18s ease; } #${BUTTON_ID}::before { inset: 1px; border-radius: 999px; background: linear-gradient(120deg, transparent 12%, rgba(255,255,255,.36) 32%, transparent 56%); filter: none; opacity: .34; transform: translateX(-42%); transition: transform .22s ease, opacity .18s ease; } #${BUTTON_ID}:hover { transform: translateY(-2px); border-color: rgba(255,255,255,.58); background: linear-gradient(135deg, rgba(255,255,255,.4), rgba(255,255,255,.18)), rgba(255,255,255,.2); box-shadow: 0 24px 64px rgba(0,0,0,.3), inset 0 1px 0 rgba(255,255,255,.5); } #${BUTTON_ID}:hover::before { transform: translateX(36%); opacity: .55; } #${BUTTON_ID} .tg-task-button-badge { right: 4px; top: -8px; min-width: 24px; height: 24px; border-radius: 999px; border: 1px solid rgba(255,255,255,.62); background: rgba(255, 78, 112, .84); color: #fff; line-height: 22px; box-shadow: 0 10px 24px rgba(255,78,112,.24), inset 0 1px 0 rgba(255,255,255,.38); backdrop-filter: blur(14px) saturate(180%); -webkit-backdrop-filter: blur(14px) saturate(180%); } #${DRAWER_ID} { border-radius: 28px; color: var(--tg-text); background: linear-gradient(180deg, rgba(22,27,38,.72), rgba(10,14,22,.68)), rgba(255,255,255,.16); border: 1px solid var(--tg-liquid-border); box-shadow: var(--tg-liquid-shadow), inset 0 1px 0 rgba(255,255,255,.32), inset 0 0 0 1px rgba(255,255,255,.08); backdrop-filter: blur(22px) saturate(180%); -webkit-backdrop-filter: blur(22px) saturate(180%); transition: transform .2s cubic-bezier(.2, .8, .2, 1), opacity .18s ease, box-shadow .18s ease; } #${DRAWER_ID}.tg-open { background: linear-gradient(180deg, rgba(20,25,36,.78), rgba(8,12,20,.74)), rgba(255,255,255,.18); } #${DRAWER_ID}::before { background: linear-gradient(115deg, rgba(255,255,255,.2), transparent 24%), radial-gradient(circle at 18% 0%, rgba(255,255,255,.16), transparent 30%); opacity: .55; animation: none; mask-image: none; } #${DRAWER_ID}::after { border-radius: 27px; background: linear-gradient(135deg, rgba(255,255,255,.2), transparent 28%), linear-gradient(315deg, rgba(255,255,255,.08), transparent 34%); opacity: .45; } #${DRAWER_ID} .tg-ambient { display: none; } #${DRAWER_ID} .tg-header { min-height: 76px; border-bottom: 1px solid rgba(255,255,255,.18); background: rgba(255,255,255,.08); backdrop-filter: blur(16px) saturate(160%); -webkit-backdrop-filter: blur(16px) saturate(160%); } #${DRAWER_ID} .tg-title-main, #${DRAWER_ID} .tg-card-title, #${DRAWER_ID} .tg-course-head, #${DRAWER_ID} .tg-section-title { color: rgba(255,255,255,.98); text-shadow: 0 1px 2px rgba(0,0,0,.28); } #${DRAWER_ID} .tg-icon-btn, #${DRAWER_ID} .tg-action-btn, #${DRAWER_ID} .tg-detail-link, #${DRAWER_ID} .tg-course-toggle, #${DRAWER_ID} .tg-subgroup-count, #${DRAWER_ID} .tg-course-stat, #${DRAWER_ID} .tg-type, #${DRAWER_ID} .tg-date-filter select { border-radius: 999px; border-color: var(--tg-liquid-border-soft); background: rgba(255,255,255,.13); color: rgba(255,255,255,.92); box-shadow: inset 0 1px 0 rgba(255,255,255,.22); backdrop-filter: blur(14px) saturate(170%); -webkit-backdrop-filter: blur(14px) saturate(170%); transition: transform .16s ease, border-color .16s ease, background .16s ease, box-shadow .16s ease; } #${DRAWER_ID} .tg-top-refresh { min-width: 78px; } #${DRAWER_ID} .tg-icon-btn:hover, #${DRAWER_ID} .tg-action-btn:hover, #${DRAWER_ID} .tg-detail-link:hover, #${DRAWER_ID} .tg-course-toggle:hover, #${DRAWER_ID} .tg-date-filter select:hover, #${DRAWER_ID} .tg-date-filter select:focus { transform: translateY(-1px); border-color: rgba(255,255,255,.48); background: rgba(255,255,255,.2); box-shadow: 0 10px 26px rgba(0,0,0,.18), inset 0 1px 0 rgba(255,255,255,.34); } #${DRAWER_ID} .tg-meta, #${DRAWER_ID} .tg-alert, #${DRAWER_ID} .tg-empty, #${DRAWER_ID} .tg-date-filter, #${DRAWER_ID} .tg-summary-item, #${DRAWER_ID} .tg-refresh-status, #${DRAWER_ID} .tg-course-group, #${DRAWER_ID} .tg-course-subgroup, #${DRAWER_ID} .tg-card { border: 1px solid var(--tg-liquid-border-soft); background: linear-gradient(180deg, rgba(255,255,255,.16), rgba(255,255,255,.08)), rgba(255,255,255,.1); box-shadow: 0 12px 34px rgba(0,0,0,.18), inset 0 1px 0 rgba(255,255,255,.24); backdrop-filter: blur(18px) saturate(170%); -webkit-backdrop-filter: blur(18px) saturate(170%); } #${DRAWER_ID} .tg-meta::before, #${DRAWER_ID} .tg-alert::before, #${DRAWER_ID} .tg-empty::before, #${DRAWER_ID} .tg-date-filter::before, #${DRAWER_ID} .tg-summary-item::before, #${DRAWER_ID} .tg-card::before { background: linear-gradient(120deg, rgba(255,255,255,.18), transparent 38%); opacity: .42; mix-blend-mode: normal; } #${DRAWER_ID} .tg-summary-item, #${DRAWER_ID} .tg-card { transition: transform .18s cubic-bezier(.2, .8, .2, 1), border-color .18s ease, box-shadow .18s ease, background .18s ease; } #${DRAWER_ID} .tg-summary-item:hover, #${DRAWER_ID} .tg-card:hover { transform: translateY(-2px); border-color: rgba(255,255,255,.46); background: linear-gradient(180deg, rgba(255,255,255,.22), rgba(255,255,255,.1)), rgba(255,255,255,.14); box-shadow: 0 16px 42px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.3); } #${DRAWER_ID} .tg-summary-item.tg-selected { border-color: rgba(142,232,255,.62); background: linear-gradient(180deg, rgba(142,232,255,.2), rgba(255,255,255,.12)), rgba(255,255,255,.16); box-shadow: 0 16px 42px rgba(0,0,0,.22), 0 0 0 1px rgba(142,232,255,.18), inset 0 1px 0 rgba(255,255,255,.32); } #${DRAWER_ID} .tg-summary-item[data-tooltip]::after, #${DRAWER_ID} .tg-status[data-tooltip]::after { color: rgba(255,255,255,.96); background: linear-gradient(180deg, rgba(34,40,54,.86), rgba(12,16,25,.82)), rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.32); box-shadow: 0 16px 42px rgba(0,0,0,.24), inset 0 1px 0 rgba(255,255,255,.22); backdrop-filter: blur(18px) saturate(180%); -webkit-backdrop-filter: blur(18px) saturate(180%); } #${DRAWER_ID} .tg-status[data-tooltip] { cursor: help; } #${DRAWER_ID} .tg-status[data-tooltip]::after { content: attr(data-tooltip); position: absolute; left: 0; bottom: calc(100% + 8px); width: min(240px, 72vw); transform: translateY(6px); opacity: 0; pointer-events: none; border-radius: 14px; padding: 8px 10px; font-size: 11px; line-height: 1.45; font-weight: 560; transition: opacity .16s ease, transform .16s ease; z-index: 8; } #${DRAWER_ID} .tg-status[data-tooltip]:hover::after { opacity: 1; transform: translateY(0); } #${DRAWER_ID} .tg-progress-track { height: 5px; border-radius: 999px; border: 1px solid rgba(255,255,255,.2); background: rgba(255,255,255,.12); } #${DRAWER_ID} .tg-progress-fill { background: linear-gradient(90deg, rgba(142,232,255,.92), rgba(184,167,255,.82), rgba(255,197,232,.76)); box-shadow: 0 0 16px rgba(142,232,255,.18); transition: width .18s ease, opacity .18s ease; } #${DRAWER_ID} .tg-refresh-status.tg-refresh-done { animation: tgProgressFade .9s ease .8s forwards; } #${DRAWER_ID} .tg-task-meta span { border-color: rgba(255,255,255,.16); background: rgba(255,255,255,.08); color: rgba(238,244,252,.82); backdrop-filter: blur(10px) saturate(150%); -webkit-backdrop-filter: blur(10px) saturate(150%); } #${DRAWER_ID} .tg-actions { border-top: 1px solid rgba(255,255,255,.18); background: rgba(10,14,22,.52); backdrop-filter: blur(18px) saturate(180%); -webkit-backdrop-filter: blur(18px) saturate(180%); } #${DRAWER_ID} textarea.tg-json-box { color: rgba(255,255,255,.94); background: rgba(12,16,25,.68); border-color: rgba(255,255,255,.22); } @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { #${BUTTON_ID} { background: rgba(32, 38, 50, .94); } #${DRAWER_ID} { background: rgba(20, 24, 34, .96); } #${DRAWER_ID} .tg-meta, #${DRAWER_ID} .tg-alert, #${DRAWER_ID} .tg-empty, #${DRAWER_ID} .tg-date-filter, #${DRAWER_ID} .tg-summary-item, #${DRAWER_ID} .tg-refresh-status, #${DRAWER_ID} .tg-course-group, #${DRAWER_ID} .tg-course-subgroup, #${DRAWER_ID} .tg-card { background: rgba(35, 42, 56, .92); } } @keyframes tgProgressFade { to { opacity: .42; transform: translateY(-1px); } } /* Liquid Glass Pro light skin: CSS + SVG filter, optimized for white web pages */ #${BUTTON_ID}, #${DRAWER_ID} { --tg-liquid-surface: rgba(255,255,255,.58); --tg-liquid-card: rgba(255,255,255,.42); --tg-liquid-card-hover: rgba(255,255,255,.58); --tg-liquid-border: rgba(255,255,255,.75); --tg-liquid-line: rgba(15,23,42,.08); --tg-liquid-shadow: 0 20px 60px rgba(15,23,42,.18); --tg-liquid-ease: cubic-bezier(.22, 1, .36, 1); --tg-text: #111827; --tg-muted: rgba(31,41,55,.72); --tg-faint: rgba(75,85,99,.58); --tg-cyan: #0891b2; --tg-violet: #6d28d9; --tg-pink: #be185d; --tg-red: #dc2626; --tg-amber: #b45309; --tg-green: #15803d; color: var(--tg-text); text-shadow: none; } #${BUTTON_ID} { min-width: 136px; height: 50px; border-radius: 999px; border: 1px solid var(--tg-liquid-border); background: radial-gradient(circle at var(--tg-mouse-x, 28%) var(--tg-mouse-y, 18%), rgba(255,255,255,.96), transparent 36%), linear-gradient(135deg, rgba(255,255,255,.78), rgba(255,255,255,.38)), rgba(255,255,255,.58); color: #111827; box-shadow: 0 18px 46px rgba(15,23,42,.16), inset 0 1px 0 rgba(255,255,255,.86), inset 0 -1px 0 rgba(255,255,255,.42); backdrop-filter: blur(26px) saturate(180%); -webkit-backdrop-filter: blur(26px) saturate(180%); transition: transform .18s var(--tg-liquid-ease), border-color .18s ease, box-shadow .18s ease, background .18s ease; } #${BUTTON_ID}::before { inset: -1px; border-radius: 999px; background: linear-gradient(120deg, transparent 18%, rgba(255,255,255,.82) 38%, transparent 58%), radial-gradient(circle at 50% 0%, rgba(255,255,255,.74), transparent 44%); filter: url(#${SVG_FILTER_ID}); opacity: .48; transform: translateX(-28%); transition: transform .22s var(--tg-liquid-ease), opacity .18s ease; } #${BUTTON_ID}:hover { transform: translateY(-2px); border-color: rgba(255,255,255,.92); background: radial-gradient(circle at var(--tg-mouse-x, 28%) var(--tg-mouse-y, 18%), rgba(255,255,255,1), transparent 38%), linear-gradient(135deg, rgba(255,255,255,.86), rgba(255,255,255,.5)), rgba(255,255,255,.66); box-shadow: 0 22px 54px rgba(15,23,42,.2), inset 0 1px 0 rgba(255,255,255,.95); } #${BUTTON_ID}:active { transform: translateY(0) scale(.985); } #${BUTTON_ID}:hover::before { transform: translateX(22%); opacity: .68; } #${BUTTON_ID} .tg-task-button-badge { background: rgba(239, 68, 68, .88); color: #fff; border-color: rgba(255,255,255,.9); box-shadow: 0 8px 20px rgba(239,68,68,.24), inset 0 1px 0 rgba(255,255,255,.42); } #${DRAWER_ID} { color: #111827; background: radial-gradient(circle at var(--tg-mouse-x, 72%) var(--tg-mouse-y, 12%), rgba(255,255,255,.92), transparent 30%), linear-gradient(180deg, rgba(255,255,255,.68), rgba(255,255,255,.48)), rgba(255,255,255,.58); border: 1px solid var(--tg-liquid-border); box-shadow: var(--tg-liquid-shadow), inset 0 1px 0 rgba(255,255,255,.8), inset 0 -1px 0 rgba(255,255,255,.32); backdrop-filter: blur(26px) saturate(180%); -webkit-backdrop-filter: blur(26px) saturate(180%); transition: transform .28s var(--tg-liquid-ease), opacity .22s ease, box-shadow .22s ease; } #${DRAWER_ID}.tg-open { background: radial-gradient(circle at var(--tg-mouse-x, 72%) var(--tg-mouse-y, 12%), rgba(255,255,255,.98), transparent 32%), linear-gradient(180deg, rgba(255,255,255,.72), rgba(255,255,255,.54)), rgba(255,255,255,.58); } #${DRAWER_ID}::before { background: linear-gradient(125deg, rgba(255,255,255,.92), transparent 24%, rgba(255,255,255,.22) 58%, transparent 76%), radial-gradient(circle at var(--tg-mouse-x, 80%) var(--tg-mouse-y, 8%), rgba(255,255,255,.72), transparent 26%); filter: url(#${SVG_FILTER_ID}); opacity: .48; mix-blend-mode: screen; animation: none; mask-image: none; } #${DRAWER_ID}::after { border-radius: 27px; background: linear-gradient(135deg, rgba(255,255,255,.72), transparent 26%), linear-gradient(315deg, rgba(255,255,255,.32), transparent 34%); opacity: .58; mix-blend-mode: screen; } #${DRAWER_ID} .tg-header { border-bottom: 1px solid rgba(15,23,42,.08); background: rgba(255,255,255,.36); backdrop-filter: blur(18px) saturate(180%); -webkit-backdrop-filter: blur(18px) saturate(180%); } #${DRAWER_ID} .tg-title-main, #${DRAWER_ID} .tg-card-title, #${DRAWER_ID} .tg-course-head, #${DRAWER_ID} .tg-section-title, #${DRAWER_ID} .tg-meta-value, #${DRAWER_ID} .tg-refresh-title { color: #111827; text-shadow: none; } #${DRAWER_ID} .tg-title-sub, #${DRAWER_ID} .tg-meta-label, #${DRAWER_ID} .tg-refresh-stage, #${DRAWER_ID} .tg-small, #${DRAWER_ID} .tg-task-meta, #${DRAWER_ID} .tg-summary-label, #${DRAWER_ID} .tg-date-filter-hint, #${DRAWER_ID} .tg-current-filter, #${DRAWER_ID} .tg-section-count, #${DRAWER_ID} .tg-course-stat, #${DRAWER_ID} .tg-subgroup-title, #${DRAWER_ID} .tg-subgroup-count { color: var(--tg-muted); text-shadow: none; } #${DRAWER_ID} .tg-meta, #${DRAWER_ID} .tg-alert, #${DRAWER_ID} .tg-empty, #${DRAWER_ID} .tg-date-filter, #${DRAWER_ID} .tg-summary-item, #${DRAWER_ID} .tg-refresh-status, #${DRAWER_ID} .tg-course-group, #${DRAWER_ID} .tg-course-subgroup, #${DRAWER_ID} .tg-card { color: #111827; border: 1px solid var(--tg-liquid-line); background: var(--tg-liquid-card); box-shadow: 0 10px 30px rgba(15,23,42,.08), inset 0 1px 0 rgba(255,255,255,.72); backdrop-filter: none; -webkit-backdrop-filter: none; transition: transform .18s var(--tg-liquid-ease), border-color .18s ease, box-shadow .18s ease, background .18s ease; } #${DRAWER_ID} .tg-meta, #${DRAWER_ID} .tg-alert, #${DRAWER_ID} .tg-empty, #${DRAWER_ID} .tg-date-filter, #${DRAWER_ID} .tg-summary-item, #${DRAWER_ID} .tg-refresh-status, #${DRAWER_ID} .tg-course-group { backdrop-filter: blur(12px) saturate(160%); -webkit-backdrop-filter: blur(12px) saturate(160%); } #${DRAWER_ID} .tg-meta::before, #${DRAWER_ID} .tg-alert::before, #${DRAWER_ID} .tg-empty::before, #${DRAWER_ID} .tg-date-filter::before, #${DRAWER_ID} .tg-summary-item::before, #${DRAWER_ID} .tg-card::before { background: linear-gradient(135deg, rgba(255,255,255,.68), transparent 38%); opacity: .45; mix-blend-mode: normal; } #${DRAWER_ID} .tg-summary-item:hover, #${DRAWER_ID} .tg-card:hover, #${DRAWER_ID} .tg-course-group:hover { transform: translateY(-2px); border-color: rgba(15,23,42,.13); background: var(--tg-liquid-card-hover); box-shadow: 0 14px 34px rgba(15,23,42,.12), inset 0 1px 0 rgba(255,255,255,.86); } #${DRAWER_ID} .tg-summary-item:active, #${DRAWER_ID} .tg-card:active { transform: translateY(0) scale(.995); } #${DRAWER_ID} .tg-summary-item.tg-selected { border-color: rgba(8,145,178,.28); background: rgba(236,254,255,.66); box-shadow: 0 14px 34px rgba(8,145,178,.12), inset 0 1px 0 rgba(255,255,255,.88); } #${DRAWER_ID} .tg-summary-item.tg-selected .tg-summary-num { color: #075985; text-shadow: none; } #${DRAWER_ID} .tg-icon-btn, #${DRAWER_ID} .tg-action-btn, #${DRAWER_ID} .tg-detail-link, #${DRAWER_ID} .tg-course-toggle, #${DRAWER_ID} .tg-type, #${DRAWER_ID} .tg-date-filter select { position: relative; overflow: hidden; color: #111827; border: 1px solid rgba(15,23,42,.1); background: rgba(255,255,255,.48); box-shadow: 0 8px 22px rgba(15,23,42,.07), inset 0 1px 0 rgba(255,255,255,.78); backdrop-filter: blur(14px) saturate(170%); -webkit-backdrop-filter: blur(14px) saturate(170%); transition: transform .18s var(--tg-liquid-ease), border-color .18s ease, background .18s ease, box-shadow .18s ease; } #${DRAWER_ID} .tg-icon-btn::before, #${DRAWER_ID} .tg-action-btn::before, #${DRAWER_ID} .tg-detail-link::before, #${DRAWER_ID} .tg-course-toggle::before { content: ""; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(120deg, transparent, rgba(255,255,255,.72), transparent); transform: translateX(-120%); transition: transform .22s var(--tg-liquid-ease); } #${DRAWER_ID} .tg-icon-btn:hover, #${DRAWER_ID} .tg-action-btn:hover, #${DRAWER_ID} .tg-detail-link:hover, #${DRAWER_ID} .tg-course-toggle:hover, #${DRAWER_ID} .tg-date-filter select:hover, #${DRAWER_ID} .tg-date-filter select:focus { transform: translateY(-1px); border-color: rgba(15,23,42,.16); background: rgba(255,255,255,.68); box-shadow: 0 12px 28px rgba(15,23,42,.11), inset 0 1px 0 rgba(255,255,255,.92); } #${DRAWER_ID} .tg-icon-btn:hover::before, #${DRAWER_ID} .tg-action-btn:hover::before, #${DRAWER_ID} .tg-detail-link:hover::before, #${DRAWER_ID} .tg-course-toggle:hover::before { transform: translateX(120%); } #${DRAWER_ID} .tg-icon-btn:active, #${DRAWER_ID} .tg-action-btn:active, #${DRAWER_ID} .tg-detail-link:active, #${DRAWER_ID} .tg-course-toggle:active { transform: translateY(0) scale(.98); } #${DRAWER_ID} .tg-liquid-ripple { position: absolute; width: 10px; height: 10px; border-radius: 999px; pointer-events: none; background: rgba(8,145,178,.16); transform: translate(-50%, -50%) scale(1); animation: tgLiquidRipple .55s var(--tg-liquid-ease) forwards; z-index: 0; } #${DRAWER_ID} .tg-progress-track { height: 5px; border: 1px solid rgba(15,23,42,.08); background: rgba(255,255,255,.48); box-shadow: inset 0 1px 2px rgba(15,23,42,.05); } #${DRAWER_ID} .tg-progress-fill { background: linear-gradient(90deg, rgba(8,145,178,.68), rgba(99,102,241,.58), rgba(190,24,93,.42)); box-shadow: 0 0 16px rgba(8,145,178,.16); filter: url(#${SVG_FILTER_ID}); transition: width .18s ease, opacity .18s ease; } #${DRAWER_ID} .tg-summary-item[data-tooltip]::after, #${DRAWER_ID} .tg-status[data-tooltip]::after { color: #111827; background: linear-gradient(180deg, rgba(255,255,255,.88), rgba(255,255,255,.72)), rgba(255,255,255,.78); border: 1px solid rgba(15,23,42,.1); box-shadow: 0 16px 40px rgba(15,23,42,.14), inset 0 1px 0 rgba(255,255,255,.88); backdrop-filter: blur(18px) saturate(180%); -webkit-backdrop-filter: blur(18px) saturate(180%); text-shadow: none; } #${DRAWER_ID} .tg-summary-item[data-tooltip]::before, #${DRAWER_ID} .tg-status[data-tooltip]::before { content: ""; position: absolute; width: 9px; height: 9px; background: rgba(255,255,255,.78); border-right: 1px solid rgba(15,23,42,.08); border-bottom: 1px solid rgba(15,23,42,.08); transform: rotate(45deg); opacity: 0; pointer-events: none; transition: opacity .16s ease; z-index: 7; } #${DRAWER_ID} .tg-summary-item[data-tooltip]::before { left: 50%; bottom: calc(100% + 5px); margin-left: -4px; } #${DRAWER_ID} .tg-status[data-tooltip]::before { left: 18px; bottom: calc(100% + 3px); } #${DRAWER_ID} .tg-summary-item[data-tooltip]:hover::before, #${DRAWER_ID} .tg-status[data-tooltip]:hover::before { opacity: 1; } #${DRAWER_ID} .tg-alert { color: #7f1d1d; background: rgba(254,242,242,.72); border-color: rgba(220,38,38,.14); } #${DRAWER_ID} .tg-empty { color: #374151; } #${DRAWER_ID} .tg-task-meta span, #${DRAWER_ID} .tg-course-stat, #${DRAWER_ID} .tg-subgroup-count { border-color: rgba(15,23,42,.06); background: rgba(255,255,255,.38); color: rgba(31,41,55,.72); backdrop-filter: none; -webkit-backdrop-filter: none; } #${DRAWER_ID} .tg-actions { border-top: 1px solid rgba(15,23,42,.08); background: rgba(255,255,255,.38); backdrop-filter: blur(18px) saturate(180%); -webkit-backdrop-filter: blur(18px) saturate(180%); } #${DRAWER_ID} textarea.tg-json-box { color: #111827; background: rgba(255,255,255,.72); border-color: rgba(15,23,42,.1); } @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { #${BUTTON_ID}, #${DRAWER_ID} { background: rgba(255,255,255,.94); } #${DRAWER_ID} .tg-meta, #${DRAWER_ID} .tg-alert, #${DRAWER_ID} .tg-empty, #${DRAWER_ID} .tg-date-filter, #${DRAWER_ID} .tg-summary-item, #${DRAWER_ID} .tg-refresh-status, #${DRAWER_ID} .tg-course-group, #${DRAWER_ID} .tg-course-subgroup, #${DRAWER_ID} .tg-card { background: rgba(255,255,255,.9); } } @keyframes tgLiquidRipple { to { opacity: 0; transform: translate(-50%, -50%) scale(34); } } /* Readable dark console skin: higher contrast, less white haze */ #${BUTTON_ID}, #${DRAWER_ID} { --tg-liquid-surface: rgba(10, 14, 24, .78); --tg-liquid-card: rgba(17, 24, 39, .74); --tg-liquid-card-hover: rgba(24, 34, 52, .86); --tg-liquid-border: rgba(125, 211, 252, .24); --tg-liquid-line: rgba(148, 163, 184, .2); --tg-liquid-shadow: 0 22px 70px rgba(0, 0, 0, .46); --tg-text: rgba(248, 250, 252, .96); --tg-muted: rgba(203, 213, 225, .78); --tg-faint: rgba(148, 163, 184, .7); --tg-cyan: #22d3ee; --tg-violet: #8b5cf6; --tg-pink: #f472b6; --tg-red: #fb7185; --tg-amber: #fbbf24; --tg-green: #4ade80; color: var(--tg-text); } #${BUTTON_ID} { border: 1px solid rgba(125, 211, 252, .32); background: radial-gradient(circle at var(--tg-mouse-x, 28%) var(--tg-mouse-y, 18%), rgba(34,211,238,.18), transparent 36%), linear-gradient(135deg, rgba(31,41,55,.92), rgba(8,13,24,.82)), rgba(10,14,24,.82); color: rgba(248,250,252,.96); box-shadow: 0 18px 48px rgba(0,0,0,.42), 0 0 0 1px rgba(255,255,255,.04) inset, inset 0 1px 0 rgba(255,255,255,.16); backdrop-filter: blur(24px) saturate(170%); -webkit-backdrop-filter: blur(24px) saturate(170%); } #${BUTTON_ID}::before { background: linear-gradient(120deg, transparent 18%, rgba(125,211,252,.28) 38%, transparent 58%), radial-gradient(circle at 50% 0%, rgba(167,139,250,.18), transparent 44%); opacity: .36; } #${BUTTON_ID}:hover { border-color: rgba(125, 211, 252, .56); background: radial-gradient(circle at var(--tg-mouse-x, 28%) var(--tg-mouse-y, 18%), rgba(34,211,238,.26), transparent 38%), linear-gradient(135deg, rgba(38,50,72,.96), rgba(12,18,31,.9)), rgba(12,18,31,.9); box-shadow: 0 24px 60px rgba(0,0,0,.5), 0 0 28px rgba(34,211,238,.14), inset 0 1px 0 rgba(255,255,255,.2); } #${DRAWER_ID} { color: var(--tg-text); background: radial-gradient(circle at var(--tg-mouse-x, 72%) var(--tg-mouse-y, 12%), rgba(34,211,238,.13), transparent 30%), radial-gradient(circle at 100% 0%, rgba(139,92,246,.12), transparent 34%), linear-gradient(145deg, rgba(17,24,39,.84), rgba(3,7,18,.82)), rgba(10,14,24,.78); border: 1px solid rgba(125, 211, 252, .26); box-shadow: var(--tg-liquid-shadow), 0 0 42px rgba(34,211,238,.08), 0 0 74px rgba(139,92,246,.07), inset 0 1px 0 rgba(255,255,255,.12); backdrop-filter: blur(24px) saturate(170%); -webkit-backdrop-filter: blur(24px) saturate(170%); } #${DRAWER_ID}.tg-open { background: radial-gradient(circle at var(--tg-mouse-x, 72%) var(--tg-mouse-y, 12%), rgba(34,211,238,.15), transparent 32%), radial-gradient(circle at 100% 0%, rgba(139,92,246,.14), transparent 35%), linear-gradient(145deg, rgba(17,24,39,.88), rgba(3,7,18,.86)), rgba(10,14,24,.84); } #${DRAWER_ID}::before { background: linear-gradient(125deg, rgba(255,255,255,.14), transparent 24%, rgba(34,211,238,.08) 58%, transparent 76%), radial-gradient(circle at var(--tg-mouse-x, 80%) var(--tg-mouse-y, 8%), rgba(125,211,252,.16), transparent 26%); opacity: .42; mix-blend-mode: screen; } #${DRAWER_ID}::after { background: linear-gradient(135deg, rgba(255,255,255,.13), transparent 28%), linear-gradient(315deg, rgba(34,211,238,.08), transparent 36%); opacity: .42; } #${DRAWER_ID} .tg-header { border-bottom: 1px solid rgba(148, 163, 184, .18); background: rgba(15, 23, 42, .58); backdrop-filter: blur(18px) saturate(160%); -webkit-backdrop-filter: blur(18px) saturate(160%); } #${DRAWER_ID} .tg-title-main, #${DRAWER_ID} .tg-card-title, #${DRAWER_ID} .tg-course-head, #${DRAWER_ID} .tg-section-title, #${DRAWER_ID} .tg-meta-value, #${DRAWER_ID} .tg-refresh-title, #${DRAWER_ID} .tg-summary-num { color: rgba(248,250,252,.98); text-shadow: none; } #${DRAWER_ID} .tg-title-sub, #${DRAWER_ID} .tg-meta-label, #${DRAWER_ID} .tg-refresh-stage, #${DRAWER_ID} .tg-small, #${DRAWER_ID} .tg-task-meta, #${DRAWER_ID} .tg-summary-label, #${DRAWER_ID} .tg-date-filter-hint, #${DRAWER_ID} .tg-current-filter, #${DRAWER_ID} .tg-section-count, #${DRAWER_ID} .tg-course-stat, #${DRAWER_ID} .tg-subgroup-title, #${DRAWER_ID} .tg-subgroup-count { color: var(--tg-muted); text-shadow: none; } #${DRAWER_ID} .tg-current-filter strong, #${DRAWER_ID} .tg-meta-kicker { color: #67e8f9; } #${DRAWER_ID} .tg-meta, #${DRAWER_ID} .tg-alert, #${DRAWER_ID} .tg-empty, #${DRAWER_ID} .tg-date-filter, #${DRAWER_ID} .tg-summary-item, #${DRAWER_ID} .tg-refresh-status, #${DRAWER_ID} .tg-course-group, #${DRAWER_ID} .tg-course-subgroup, #${DRAWER_ID} .tg-card { color: var(--tg-text); border: 1px solid var(--tg-liquid-line); background: linear-gradient(180deg, rgba(30,41,59,.72), rgba(15,23,42,.66)), rgba(15, 23, 42, .72); box-shadow: 0 12px 34px rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.08); backdrop-filter: none; -webkit-backdrop-filter: none; } #${DRAWER_ID} .tg-meta, #${DRAWER_ID} .tg-alert, #${DRAWER_ID} .tg-empty, #${DRAWER_ID} .tg-date-filter, #${DRAWER_ID} .tg-summary-item, #${DRAWER_ID} .tg-refresh-status, #${DRAWER_ID} .tg-course-group { backdrop-filter: blur(10px) saturate(145%); -webkit-backdrop-filter: blur(10px) saturate(145%); } #${DRAWER_ID} .tg-meta::before, #${DRAWER_ID} .tg-alert::before, #${DRAWER_ID} .tg-empty::before, #${DRAWER_ID} .tg-date-filter::before, #${DRAWER_ID} .tg-summary-item::before, #${DRAWER_ID} .tg-card::before { background: linear-gradient(135deg, rgba(255,255,255,.1), transparent 38%); opacity: .32; } #${DRAWER_ID} .tg-summary-item:hover, #${DRAWER_ID} .tg-card:hover, #${DRAWER_ID} .tg-course-group:hover { border-color: rgba(125,211,252,.34); background: linear-gradient(180deg, rgba(38,50,72,.84), rgba(17,24,39,.78)), rgba(17,24,39,.82); box-shadow: 0 16px 42px rgba(0,0,0,.36), 0 0 26px rgba(34,211,238,.08), inset 0 1px 0 rgba(255,255,255,.12); } #${DRAWER_ID} .tg-summary-item.tg-selected { border-color: rgba(34,211,238,.5); background: linear-gradient(180deg, rgba(14,116,144,.32), rgba(17,24,39,.82)), rgba(17,24,39,.86); box-shadow: 0 16px 42px rgba(0,0,0,.36), 0 0 28px rgba(34,211,238,.16), inset 0 1px 0 rgba(255,255,255,.14); } #${DRAWER_ID} .tg-summary-item.tg-selected .tg-summary-num { color: #67e8f9; } #${DRAWER_ID} .tg-icon-btn, #${DRAWER_ID} .tg-action-btn, #${DRAWER_ID} .tg-detail-link, #${DRAWER_ID} .tg-course-toggle, #${DRAWER_ID} .tg-type, #${DRAWER_ID} .tg-date-filter select { color: rgba(248,250,252,.94); border: 1px solid rgba(148,163,184,.22); background: rgba(15,23,42,.7); box-shadow: 0 8px 22px rgba(0,0,0,.24), inset 0 1px 0 rgba(255,255,255,.08); backdrop-filter: blur(12px) saturate(145%); -webkit-backdrop-filter: blur(12px) saturate(145%); } #${DRAWER_ID} .tg-icon-btn::before, #${DRAWER_ID} .tg-action-btn::before, #${DRAWER_ID} .tg-detail-link::before, #${DRAWER_ID} .tg-course-toggle::before { background: linear-gradient(120deg, transparent, rgba(125,211,252,.2), transparent); } #${DRAWER_ID} .tg-icon-btn:hover, #${DRAWER_ID} .tg-action-btn:hover, #${DRAWER_ID} .tg-detail-link:hover, #${DRAWER_ID} .tg-course-toggle:hover, #${DRAWER_ID} .tg-date-filter select:hover, #${DRAWER_ID} .tg-date-filter select:focus { border-color: rgba(125,211,252,.42); background: rgba(30,41,59,.82); box-shadow: 0 12px 30px rgba(0,0,0,.32), 0 0 22px rgba(34,211,238,.08), inset 0 1px 0 rgba(255,255,255,.12); } #${DRAWER_ID} .tg-date-filter select option { background: #0f172a; color: rgba(248,250,252,.94); } #${DRAWER_ID} [data-copy-all-json] { border-color: rgba(34,211,238,.36); background: rgba(8,47,73,.72); color: rgba(236,254,255,.98); } #${DRAWER_ID} [data-delete-cache] { border-color: rgba(251,113,133,.36); background: rgba(76, 29, 38, .72); color: rgba(255,228,230,.96); } #${DRAWER_ID} .tg-task-meta span, #${DRAWER_ID} .tg-course-stat, #${DRAWER_ID} .tg-subgroup-count { border-color: rgba(148,163,184,.14); background: rgba(15,23,42,.48); color: rgba(203,213,225,.82); } #${DRAWER_ID} .tg-progress-track { border: 1px solid rgba(148,163,184,.18); background: rgba(15,23,42,.72); } #${DRAWER_ID} .tg-progress-fill { background: linear-gradient(90deg, rgba(34,211,238,.9), rgba(99,102,241,.78), rgba(139,92,246,.68)); box-shadow: 0 0 18px rgba(34,211,238,.18); } #${DRAWER_ID} .tg-summary-item[data-tooltip]::after, #${DRAWER_ID} .tg-status[data-tooltip]::after { color: rgba(248,250,252,.96); background: linear-gradient(180deg, rgba(30,41,59,.94), rgba(15,23,42,.92)), rgba(15,23,42,.92); border: 1px solid rgba(125,211,252,.22); box-shadow: 0 16px 40px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.1); } #${DRAWER_ID} .tg-summary-item[data-tooltip]::before, #${DRAWER_ID} .tg-status[data-tooltip]::before { background: rgba(30,41,59,.94); border-right: 1px solid rgba(125,211,252,.18); border-bottom: 1px solid rgba(125,211,252,.18); } #${DRAWER_ID} .tg-alert { color: #fecdd3; background: linear-gradient(180deg, rgba(127,29,29,.42), rgba(15,23,42,.72)), rgba(76,29,38,.66); border-color: rgba(251,113,133,.25); } #${DRAWER_ID} .tg-empty { color: rgba(203,213,225,.84); } #${DRAWER_ID} .tg-actions { border-top: 1px solid rgba(148,163,184,.18); background: rgba(3,7,18,.72); backdrop-filter: blur(18px) saturate(150%); -webkit-backdrop-filter: blur(18px) saturate(150%); } #${DRAWER_ID} textarea.tg-json-box { color: rgba(248,250,252,.94); background: rgba(2,6,23,.82); border-color: rgba(148,163,184,.2); } #${DRAWER_ID} .tg-ok { color: #4ade80; } #${DRAWER_ID} .tg-warn { color: #fbbf24; } #${DRAWER_ID} .tg-bad { color: #fb7185; } #${DRAWER_ID} .tg-muted { color: rgba(203,213,225,.78); } #${DRAWER_ID} .tg-state-ok { border-color: rgba(74,222,128,.22); } #${DRAWER_ID} .tg-state-warn { border-color: rgba(251,191,36,.28); } #${DRAWER_ID} .tg-state-bad { border-color: rgba(251,113,133,.34); } @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { #${BUTTON_ID}, #${DRAWER_ID} { background: rgba(15,23,42,.96); } #${DRAWER_ID} .tg-meta, #${DRAWER_ID} .tg-alert, #${DRAWER_ID} .tg-empty, #${DRAWER_ID} .tg-date-filter, #${DRAWER_ID} .tg-summary-item, #${DRAWER_ID} .tg-refresh-status, #${DRAWER_ID} .tg-course-group, #${DRAWER_ID} .tg-course-subgroup, #${DRAWER_ID} .tg-card { background: rgba(17,24,39,.96); } } @media (max-width: 520px) { #${BUTTON_ID} { right: 16px; bottom: 18px; } #${DRAWER_ID} { right: 8px; top: 8px; bottom: 8px; width: calc(100vw - 16px); border-radius: 24px; } #${DRAWER_ID} .tg-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } #${DRAWER_ID} .tg-meta-grid, #${DRAWER_ID} .tg-task-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); } #${DRAWER_ID} .tg-date-filter-row { flex-wrap: wrap; } } `; if (typeof GM_addStyle === "function") { GM_addStyle(css); } else { const style = document.createElement("style"); style.textContent = css; document.head.appendChild(style); } async function getValue(key, defaultValue) { if (typeof GM !== "undefined" && GM.getValue) { return await GM.getValue(key, defaultValue); } return GM_getValue(key, defaultValue); } async function setValue(key, value) { if (typeof GM !== "undefined" && GM.setValue) { await GM.setValue(key, value); return; } GM_setValue(key, value); } async function deleteValue(key) { if (typeof GM !== "undefined" && GM.deleteValue) { await GM.deleteValue(key); return; } GM_deleteValue(key); } function extractLoginFromText(text) { if (!text) return ""; const patterns = [ /user_\d+/i, /"login"\s*:\s*"([^"]+)"/i, /"username"\s*:\s*"([^"]+)"/i, /zzud=([^&"'\s]+)/i, /username=([^&"'\s]+)/i ]; for (const pattern of patterns) { const match = String(text).match(pattern); if (match) { const value = match[1] || match[0]; if (value && /^user_\d+$/i.test(value)) { return value; } } } return ""; } function detectLoginFromPage() { const manualLogin = String(MANUAL_LOGIN || "").trim(); if (manualLogin) return manualLogin; const candidates = []; candidates.push(location.href); if (document && document.documentElement) { candidates.push(document.documentElement.innerHTML); } try { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); candidates.push(key); candidates.push(localStorage.getItem(key)); } } catch (e) {} try { for (let i = 0; i < sessionStorage.length; i++) { const key = sessionStorage.key(i); candidates.push(key); candidates.push(sessionStorage.getItem(key)); } } catch (e) {} for (const text of candidates) { const login = extractLoginFromText(text); if (login) return login; } return ""; } function saveAutoLoginIfDetected() { if (location.hostname !== "tg.zcst.edu.cn") { return ""; } const login = detectLoginFromPage(); if (login) { GM_setValue(STORE_KEY_AUTO_LOGIN, login); return login; } return ""; } function escapeHtml(text) { return String(text ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function cleanStatusMessage(message) { return String(message || "").replace(/^\[DEBUG\]\s*/, ""); } function getCurrentPageSite() { if (location.hostname === "www.educoder.net") { return { siteKey: "educoder", siteName: "Educoder主站", pageOrigin: "https://www.educoder.net", apiOrigin: "https://data.educoder.net", resultKey: STORE_KEY_EDUCODER_RESULT, login: "pchtkff9y", courseId: 109348, courseIdentifier: "MOAPGNLO", courseName: "操作系统2026", viewHint: "当前为 Educoder 主站视图,仅展示本平台任务。" }; } return { siteKey: "tg-zcst", backendSiteKey: "tg", siteName: "TG校内站", pageOrigin: "https://tg.zcst.edu.cn", apiOrigin: "https://tg.zcst.edu.cn", resultKey: STORE_KEY_TG_RESULT, login: "", viewHint: "当前为 TG 校内站视图,仅展示本平台任务。" }; } function getCurrentBackendSiteKey() { const site = getCurrentPageSite(); return site.backendSiteKey || site.siteKey; } function isTaskForCurrentSite(task) { const current = getCurrentPageSite(); return getTaskSiteKey(task) === current.siteKey || String(task?.siteKey || "") === getCurrentBackendSiteKey(); } function isValueForCurrentSite(value) { const current = getCurrentPageSite(); if (!value?.siteKey) return current.siteKey === "tg-zcst"; return value.siteKey === getCurrentBackendSiteKey() || value.siteKey === current.siteKey || (value.siteKey === "tg" && current.siteKey === "tg-zcst"); } function filterResultForCurrentSite(result) { if (!result || typeof result !== "object") return result; const currentSite = getCurrentPageSite(); const backendSiteKey = getCurrentBackendSiteKey(); const matchesCurrentSite = item => { const key = String(item?.siteKey || item?.key || "").trim(); return key === backendSiteKey || key === currentSite.siteKey || (key === "tg" && currentSite.siteKey === "tg-zcst"); }; const courses = Array.isArray(result.courses) ? result.courses.filter(course => { const key = String(course?.siteKey || "").trim(); return key === backendSiteKey || (key === "tg" && currentSite.siteKey === "tg-zcst"); }) : []; const tasks = Array.isArray(result.tasks) ? result.tasks.filter(isTaskForCurrentSite) : []; const debug = result.debug && typeof result.debug === "object" ? { ...result.debug, siteLogin: Array.isArray(result.debug.siteLogin) ? result.debug.siteLogin.filter(matchesCurrentSite) : result.debug.siteLogin, siteScan: Array.isArray(result.debug.siteScan) ? result.debug.siteScan.filter(matchesCurrentSite) : result.debug.siteScan, requestLog: Array.isArray(result.debug.requestLog) ? result.debug.requestLog.filter(matchesCurrentSite) : result.debug.requestLog, courseScanFlow: Array.isArray(result.debug.courseScanFlow) ? result.debug.courseScanFlow.filter(matchesCurrentSite) : result.debug.courseScanFlow } : result.debug; return { ...result, courses, tasks, debug, summary: { ...(result.summary || {}), courseCount: courses.length, taskCount: tasks.length } }; } function isResultForCurrentSite(result) { if (!result || typeof result !== "object") return false; const current = getCurrentPageSite(); const backendSiteKey = getCurrentBackendSiteKey(); const keys = [ result.siteKey, result.currentSite?.key, ...(Array.isArray(result.sites) ? result.sites.map(site => site?.key) : []), ...(Array.isArray(result.courses) ? result.courses.map(course => course?.siteKey) : []), ...(Array.isArray(result.tasks) ? result.tasks.map(task => task?.siteKey) : []) ].filter(Boolean).map(value => String(value).trim()); if (!keys.length) { return current.siteKey === "tg-zcst"; } return keys.some(key => key === current.siteKey || key === backendSiteKey || (key === "tg" && current.siteKey === "tg-zcst") ); } function getTaskSiteKey(task) { const raw = String(task?.siteKey || "").trim(); if (!raw || raw === "tg") return "tg-zcst"; return raw; } function getTaskSiteName(task) { return String(task?.siteName || (getTaskSiteKey(task) === "educoder" ? "Educoder主站" : "TG校内站")).trim(); } function getCourseKey(task) { const siteKey = getTaskSiteKey(task); const courseId = task?.courseId || task?.courseIdentifier || task?.courseName || "unknown"; return `${siteKey}:${courseId}`; } function formatDateTime(value) { if (!value) return "--"; try { const normalized = String(value).replace(/-/g, "/"); const d = new Date(normalized); if (Number.isNaN(d.getTime())) return String(value); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); const h = String(d.getHours()).padStart(2, "0"); const min = String(d.getMinutes()).padStart(2, "0"); return `${y}-${m}-${day} ${h}:${min}`; } catch (e) { return String(value); } } function normalizeTasks(data) { if (Array.isArray(data?.tasks)) return data.tasks; const tasks = []; for (const course of data?.courses || []) { for (const exam of course.exams || []) tasks.push({ ...exam, taskType: "exercise", taskTypeText: "考试/小测试" }); for (const homework of course.homeworks || []) tasks.push({ ...homework, taskType: "common_homework", taskTypeText: "图文作业" }); for (const experiment of course.experiments || []) tasks.push({ ...experiment, taskType: "classroom_experiment", taskTypeText: "课堂实验" }); } return tasks; } function getTaskDate(task) { const candidates = [ task?.endTime, task?.publishTime, task?.scanTime, task?.startAt, task?.endAt ]; for (const v of candidates) { if (!v) continue; const d = new Date(String(v).replace(/-/g, "/")); if (!Number.isNaN(d.getTime())) return d; } return null; } function getTaskEndDate(task) { const candidates = [ task?.endTime, task?.endAt ]; for (const v of candidates) { if (!v) continue; const d = new Date(String(v).replace(/-/g, "/")); if (!Number.isNaN(d.getTime())) return d; } return null; } function parseChineseRemainingToMs(text) { if (!text) return null; const str = String(text); let ms = 0; const day = str.match(/(\d+)\s*天/); const hour = str.match(/(\d+)\s*小时/); const minute = str.match(/(\d+)\s*分/); const second = str.match(/(\d+)\s*秒/); if (day) ms += Number(day[1]) * 24 * 60 * 60 * 1000; if (hour) ms += Number(hour[1]) * 60 * 60 * 1000; if (minute) ms += Number(minute[1]) * 60 * 1000; if (second) ms += Number(second[1]) * 1000; return ms || null; } function getTaskDeadlineDate(task) { const timestamp = Number(task?.deadlineTimestamp ?? task?.raw?.deadlineTimestamp); if (Number.isFinite(timestamp) && timestamp > 0) { const normalizedTimestamp = timestamp < 10000000000 ? timestamp * 1000 : timestamp; const d = new Date(normalizedTimestamp); if (!Number.isNaN(d.getTime())) return d; } const raw = task?.raw || {}; const candidates = [ task?.endTime, task?.endAt, task?.deadline, task?.deadlineTime, task?.closeTime, task?.end_time, task?.end_time_s, task?.deadline_time, task?.close_time, raw.end_time, raw.end_time_s, raw.endAt, raw.deadline, raw.deadlineTime, raw.deadline_time, raw.closeTime, raw.close_time, raw["截止时间"], raw.exam?.end_time, raw.exam?.end_time_s, raw.exam?.deadline, raw.exam?.deadlineTime, raw.exam?.closeTime ]; for (const value of candidates) { if (!value) continue; const d = new Date(String(value).replace(/-/g, "/")); if (!Number.isNaN(d.getTime())) return d; } return null; } function getTaskRemainingMs(task, now = new Date()) { const remainingMs = Number(task?.remainingMs ?? task?.raw?.remainingMs); if (Number.isFinite(remainingMs)) return remainingMs; const deadline = getTaskDeadlineDate(task); if (deadline) return deadline.getTime() - now.getTime(); const relativeCandidates = [ task?.deadlineRemaining, task?.deadline, task?.deadlineTime, task?.closeTime, task?.exerciseLeftTime, task?.exercise_left_time, task?.remainingTime, task?.remainTime, task?.status_time, task?.raw?.deadlineRemaining, task?.raw?.deadline, task?.raw?.deadlineTime, task?.raw?.deadline_time, task?.raw?.closeTime, task?.raw?.close_time, task?.raw?.["截止时间"], task?.raw?.exercise_left_time, task?.raw?.status_time, task?.raw?.exam?.exercise_left_time, task?.raw?.exam?.deadline, task?.raw?.exam?.deadlineTime, task?.raw?.exam?.closeTime ]; for (const value of relativeCandidates) { const ms = parseChineseRemainingToMs(value); if (ms != null) return ms; } return null; } function parseTaskDeadline(task) { if (task?.endTime) { const d = new Date(String(task.endTime).replace(/-/g, "/")); if (!Number.isNaN(d.getTime())) return d; } if (task?.deadlineAt) { const d = new Date(String(task.deadlineAt).replace(/-/g, "/")); if (!Number.isNaN(d.getTime())) return d; } if (task?.deadlineRemaining && task?.scanTime) { const base = new Date(String(task.scanTime).replace(/-/g, "/")); const ms = parseChineseRemainingToMs(task.deadlineRemaining); if (!Number.isNaN(base.getTime()) && ms != null) { return new Date(base.getTime() + ms); } } return null; } function formatCountdown(deadline) { const diff = deadline.getTime() - Date.now(); if (diff <= 0) return "已截止"; const totalSeconds = Math.floor(diff / 1000); const days = Math.floor(totalSeconds / 86400); const hours = Math.floor((totalSeconds % 86400) / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; const pad = n => String(n).padStart(2, "0"); if (days > 0) { return `剩余 ${days}天 ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; } return `剩余 ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; } function updateCountdowns() { document.querySelectorAll(".tg-helper-countdown[data-deadline]").forEach(el => { const ts = Number(el.dataset.deadline); if (!ts) return; el.textContent = formatCountdown(new Date(ts)); }); } function updateCountdownsOnly() { updateCountdowns(); } function ensureCountdownTimer() { if (countdownIntervalId) return; countdownIntervalId = setInterval(updateCountdownsOnly, 1000); } function stopCountdownTimer() { if (!countdownIntervalId) return; clearInterval(countdownIntervalId); countdownIntervalId = null; } function getFilterStartDate(year, month) { return new Date(year, month - 1, 1, 0, 0, 0); } function filterTasksByYearMonth(tasks, year, month) { const startDate = getFilterStartDate(year, month); return tasks.filter(task => { const d = getTaskDate(task); if (!d) return true; return d >= startDate; }); } function getDefaultFilterYearMonth() { const d = new Date(); d.setDate(1); d.setHours(0, 0, 0, 0); d.setMonth(d.getMonth() - 5); return { year: d.getFullYear(), month: d.getMonth() + 1 }; } async function loadFilterYearMonth() { const saved = await getValue(STORE_KEY_FILTER_YEAR_MONTH, null); const year = Number(saved?.year); const month = Number(saved?.month); if (year && month >= 1 && month <= 12) { return { year, month }; } return getDefaultFilterYearMonth(); } async function loadCourseCollapseState() { const saved = await getValue(STORE_KEY_COURSE_COLLAPSE, {}); return saved && typeof saved === "object" ? saved : {}; } async function loadSectionCollapseState() { const saved = await getValue(STORE_KEY_SECTION_COLLAPSE, {}); return saved && typeof saved === "object" ? saved : {}; } function isDangerTask(task) { if (task?.completed || task?.submitted) return false; if (task?.ended) return false; const remainingMs = getTaskRemainingMs(task); if (remainingMs !== null) { if (remainingMs <= 0) return false; if (remainingMs <= DANGER_DAYS_THRESHOLD * 86400000) return true; } return false; } function taskPriority(task) { if (isDangerTask(task)) return 1; if (!task.completed && task.ended) return 2; if (!task.completed) return 3; return 5; } function sortTasks(tasks) { return [...tasks].sort((a, b) => taskPriority(a) - taskPriority(b)); } function calculateSummaryFromTasks(tasks) { const siteCount = new Set(tasks.map(task => getTaskSiteKey(task)).filter(Boolean)).size; const courseCount = new Set(tasks.map(task => getCourseKey(task)).filter(Boolean)).size; const exerciseCount = tasks.filter(task => task.taskType === "exercise").length; const homeworkCount = tasks.filter(task => task.taskType === "common_homework").length; const experimentCount = tasks.filter(task => task.taskType === "classroom_experiment").length; const unfinishedCount = tasks.filter(task => !task.completed).length; const dangerCount = tasks.filter(task => !task.completed && isDangerTask(task)).length; return { siteCount, courseCount, taskCount: tasks.length, exerciseCount, homeworkCount, experimentCount, unfinishedCount, dangerCount }; } function statusTone(task) { const text = task.statusText || ""; if (task.completed) return { card: "tg-state-ok", text: "tg-ok" }; if (task.ended || text.includes("已截止")) return { card: "tg-state-bad", text: "tg-bad" }; if (isDangerTask(task) || text.includes("快截止") || text.includes("进行中")) return { card: "tg-state-warn", text: "tg-warn" }; return { card: "tg-state-muted", text: "tg-muted" }; } function currentFilter() { const filter = localStorage.getItem(FILTER_KEY) || "all"; if (filter === "homework") return "common_homework"; if (filter === "experiment") return "classroom_experiment"; return filter; } function setCurrentFilter(filter) { localStorage.setItem(FILTER_KEY, filter); } function isOpen() { return localStorage.getItem(OPEN_KEY) === "1"; } function setOpen(value) { localStorage.setItem(OPEN_KEY, value ? "1" : "0"); } function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } function getDefaultPanelState() { return { buttonX: Math.max(18, window.innerWidth - 84), buttonY: Math.max(18, window.innerHeight - 86), panelWidth: 520, panelHeight: Math.min(760, Math.max(480, window.innerHeight - 48)) }; } async function loadPanelState() { const saved = await getValue(STORE_KEY_PANEL_STATE, {}); return { ...getDefaultPanelState(), ...(saved && typeof saved === "object" ? saved : {}) }; } async function savePanelStatePatch(patch) { const current = await loadPanelState(); await setValue(STORE_KEY_PANEL_STATE, { ...current, ...patch }); } async function applyPanelState(panelState) { const button = document.getElementById(BUTTON_ID); const drawer = document.getElementById(DRAWER_ID); const state = panelState || await loadPanelState(); if (button) { const x = clamp(Number(state.buttonX) || 22, -42, window.innerWidth - 20); const y = clamp(Number(state.buttonY) || 24, -42, window.innerHeight - 20); button.style.left = `${x}px`; button.style.top = `${y}px`; button.style.right = "auto"; button.style.bottom = "auto"; } if (drawer) { const maxWidth = Math.min(760, window.innerWidth - 40); const maxHeight = window.innerHeight - 40; const width = clamp(Number(state.panelWidth) || 520, 390, maxWidth); const height = clamp(Number(state.panelHeight) || Math.min(760, window.innerHeight - 48), 480, maxHeight); drawer.style.width = `${width}px`; drawer.style.height = `${height}px`; drawer.style.maxWidth = `${maxWidth}px`; drawer.style.maxHeight = `${maxHeight}px`; drawer.style.right = "18px"; drawer.style.top = "18px"; } } async function loadData() { const site = getCurrentPageSite(); const currentResult = await getValue(site.resultKey, null); if (currentResult) { if (site.siteKey === "educoder" && currentResult.source !== "educoder-page-frontend") { return null; } return filterResultForCurrentSite(currentResult); } if (site.siteKey === "educoder") { return null; } const fallback = await getValue(STORE_KEY, null); return filterResultForCurrentSite(fallback); } async function loadRunningState() { return await getValue(STORE_KEY_LAST_RUNNING, null); } async function loadLastError() { return await getValue(STORE_KEY_LAST_ERROR, null); } async function loadRefreshRequest() { return await getValue(STORE_KEY_REFRESH_REQUEST, null); } async function loadRefreshHandled() { return await getValue(STORE_KEY_REFRESH_HANDLED, 0); } function normalizeFilterYearMonth(saved) { const year = Number(saved?.year); const month = Number(saved?.month); if (year && month >= 1 && month <= 12) { return { year, month }; } return getDefaultFilterYearMonth(); } function normalizeObject(value, fallback = {}) { return value && typeof value === "object" ? value : fallback; } async function loadFrontendState() { const [ result, lastRunning, lastError, filterYearMonthRaw, panelStateRaw, courseCollapse, sectionCollapse, autoLogin, refreshRequest, refreshHandled, refreshStatus ] = await Promise.all([ loadData(), getValue(STORE_KEY_LAST_RUNNING, null), getValue(STORE_KEY_LAST_ERROR, null), getValue(STORE_KEY_FILTER_YEAR_MONTH, null), getValue(STORE_KEY_PANEL_STATE, null), getValue(STORE_KEY_COURSE_COLLAPSE, {}), getValue(STORE_KEY_SECTION_COLLAPSE, {}), getValue(STORE_KEY_AUTO_LOGIN, ""), getValue(STORE_KEY_REFRESH_REQUEST, null), getValue(STORE_KEY_REFRESH_HANDLED, 0), getValue(STORE_KEY_REFRESH_STATUS, null) ]); return { result, lastRunning, lastError: isValueForCurrentSite(lastError) ? lastError : null, filterYearMonth: normalizeFilterYearMonth(filterYearMonthRaw), panelState: { ...getDefaultPanelState(), ...normalizeObject(panelStateRaw) }, courseCollapse: normalizeObject(courseCollapse), sectionCollapse: normalizeObject(sectionCollapse), autoLogin: String(autoLogin || "").trim(), refreshRequest: isValueForCurrentSite(refreshRequest) ? refreshRequest : null, refreshHandled, refreshStatus: isValueForCurrentSite(refreshStatus) ? refreshStatus : null, currentFilter: currentFilter() }; } function applyCurrentFilter(tasks, filter) { return tasks.filter(task => taskMatchesFilter(task, filter)); } function groupTasksByCourseAndType(tasks) { const groups = new Map(); for (const task of tasks) { const key = getCourseKey(task); if (!groups.has(key)) { groups.set(key, { key, siteKey: getTaskSiteKey(task), siteName: getTaskSiteName(task), courseName: task.courseName || "未识别课程", tasks: [], exams: [], homeworks: [], experiments: [], unfinished: 0, danger: 0 }); } const group = groups.get(key); group.tasks.push(task); if (!task.completed) group.unfinished += 1; if (isDangerTask(task)) group.danger += 1; if (task.taskType === "exercise") { group.exams.push(task); } else if (task.taskType === "common_homework") { group.homeworks.push(task); } else if (task.taskType === "classroom_experiment") { group.experiments.push(task); } } return Array.from(groups.values()); } function buildViewModel(result, state) { const allTasks = sortTasks(normalizeTasks(result)); const filteredByMonth = filterTasksByYearMonth(allTasks, state.filterYearMonth.year, state.filterYearMonth.month); const stats = calculateSummaryFromTasks(filteredByMonth); const visibleTasks = applyCurrentFilter(filteredByMonth, state.currentFilter); const dangerTasks = filteredByMonth.filter(isDangerTask); const regularTasks = visibleTasks.filter(task => !isDangerTask(task)); return { allTasks, filteredTasks: filteredByMonth, visibleTasks, dangerTasks, regularTasks, stats, courseGroups: groupTasksByCourseAndType(regularTasks) }; } function getDangerTaskKey(task) { return [ getTaskSiteKey(task), task.courseId || task.courseIdentifier || task.courseName || "", task.taskType || "", task.exerciseId || task.homeworkId || task.id || task.title || "" ].join(":"); } function getDangerNotifySignature(dangerList, result) { const keys = dangerList.map(getDangerTaskKey).sort().join("|"); return `${result?.scanTimestamp || result?.scanTime || ""}:${keys}`; } function requestDangerNotification(dangerList, result) { console.log("[TG任务助手] dangerList", dangerList); if (!dangerList.length || typeof Notification === "undefined") return; const signature = getDangerNotifySignature(dangerList, result); if (localStorage.getItem(DANGER_NOTIFY_KEY) === signature) return; const showNotification = () => { if (Notification.permission !== "granted") return; const first = dangerList[0]; const moreText = dangerList.length > 1 ? `等 ${dangerList.length} 项` : ""; new Notification("TG任务助手:发现危险任务", { body: `${first.courseName || "课程"}:${getTaskTitle(first)} ${moreText}`, tag: "tg-task-danger" }); localStorage.setItem(DANGER_NOTIFY_KEY, signature); }; if (Notification.permission === "granted") { showNotification(); } else if (Notification.permission !== "denied") { Notification.requestPermission().then(permission => { if (permission === "granted") showNotification(); }).catch(() => {}); } } function createRoot() { let root = document.getElementById(ROOT_ID); if (root) return root; root = document.createElement("div"); root.id = ROOT_ID; root.innerHTML = ` `; document.body.appendChild(root); const button = root.querySelector(`#${BUTTON_ID}`); root.addEventListener("click", handlePanelClick); root.addEventListener("change", handlePanelChange); root.addEventListener("mousemove", handleLiquidPointerMove, { passive: true }); attachButtonDrag(button); attachDrawerResize(root.querySelector(`#${DRAWER_ID}`)); applyPanelState(); setDrawerOpen(isOpen()); return root; } async function handlePanelClick(event) { const actionEl = event.target.closest("[data-action], [data-course-toggle], [data-section-toggle], [data-jump-id], [data-summary-filter]"); if (!actionEl) return; const action = actionEl.dataset.action || ""; createLiquidRipple(actionEl, event); if (action === "open-panel") { const button = document.getElementById(BUTTON_ID); if (button?.dataset.dragged === "1") { button.dataset.dragged = "0"; return; } setDrawerOpen(true); render(); return; } if (action === "close-panel") { setDrawerOpen(false); return; } if (action === "request-refresh") { await requestBackendRefresh("frontend-header-button", "manual"); startRunningPoll(); render(); return; } if (action === "delete-cache") { deleteTaskCache(); return; } if (action === "copy-json") { copyAllJson(); return; } if (action === "set-filter" || actionEl.dataset.summaryFilter) { const nextFilter = actionEl.dataset.summaryFilter === "site" ? "all" : actionEl.dataset.summaryFilter; setCurrentFilter(nextFilter); render(); return; } if (action === "open-detail" || actionEl.dataset.jumpId) { event.preventDefault(); event.stopPropagation(); const task = jumpTaskRegistry.get(actionEl.dataset.jumpId); if (!task) { alert("跳转失败:任务数据已失效,请刷新面板后重试"); return; } jumpToTask(task); return; } if (action === "toggle-course" || actionEl.hasAttribute("data-course-toggle")) { event.preventDefault(); event.stopPropagation(); const group = actionEl.closest(".tg-course-group"); if (!group) return; const key = group.dataset.courseKey; const collapseState = await loadCourseCollapseState(); collapseState[key] = !group.classList.contains("tg-course-collapsed"); await setValue(STORE_KEY_COURSE_COLLAPSE, collapseState); render(); return; } if (action === "toggle-section" || actionEl.hasAttribute("data-section-toggle")) { event.preventDefault(); event.stopPropagation(); const section = actionEl.closest(".tg-course-subgroup"); if (!section) return; const key = section.dataset.sectionKey; const collapseState = await loadSectionCollapseState(); collapseState[key] = !section.classList.contains("tg-section-collapsed"); await setValue(STORE_KEY_SECTION_COLLAPSE, collapseState); render(); } } async function handlePanelChange(event) { const target = event.target; if (!target?.matches?.("[data-filter-year], [data-filter-month]")) return; const root = document.getElementById(ROOT_ID); const year = Number(root?.querySelector("[data-filter-year]")?.value); const month = Number(root?.querySelector("[data-filter-month]")?.value); if (!year || !month) return; await setValue(STORE_KEY_FILTER_YEAR_MONTH, { year, month }); render(); } function handleLiquidPointerMove(event) { const target = event.target.closest(`#${BUTTON_ID}, #${DRAWER_ID}`); if (!target) return; const rect = target.getBoundingClientRect(); const x = clamp(((event.clientX - rect.left) / Math.max(rect.width, 1)) * 100, 0, 100); const y = clamp(((event.clientY - rect.top) / Math.max(rect.height, 1)) * 100, 0, 100); target.style.setProperty("--tg-mouse-x", `${x}%`); target.style.setProperty("--tg-mouse-y", `${y}%`); } function createLiquidRipple(target, event) { if (!target || !target.matches("button, .tg-detail-link, .tg-summary-item, .tg-course-toggle, .tg-action-btn, .tg-icon-btn")) return; if (target.dataset.action === "open-panel") return; const rect = target.getBoundingClientRect(); const ripple = document.createElement("span"); ripple.className = "tg-liquid-ripple"; ripple.style.left = `${event.clientX - rect.left}px`; ripple.style.top = `${event.clientY - rect.top}px`; target.appendChild(ripple); setTimeout(() => ripple.remove(), 560); } function setDrawerOpen(open) { const drawer = document.getElementById(DRAWER_ID); if (!drawer) return; drawer.classList.toggle("tg-open", open); setOpen(open); if (open) { applyPanelState(); requestBackendRefresh("frontend-panel-open", "auto"); startRunningPoll(); } else { if (panelAutoRefreshTimerId) { clearTimeout(panelAutoRefreshTimerId); panelAutoRefreshTimerId = null; } stopRunningPoll(); stopCountdownTimer(); } } function attachButtonDrag(button) { if (!button || button.dataset.dragReady === "1") return; button.dataset.dragReady = "1"; let startX = 0; let startY = 0; let originX = 0; let originY = 0; let dragging = false; const onMove = event => { if (!dragging) return; const nextX = clamp(originX + event.clientX - startX, -42, window.innerWidth - 20); const nextY = clamp(originY + event.clientY - startY, -42, window.innerHeight - 20); button.style.left = `${nextX}px`; button.style.top = `${nextY}px`; button.style.right = "auto"; button.style.bottom = "auto"; if (Math.abs(event.clientX - startX) + Math.abs(event.clientY - startY) > 5) { button.dataset.dragged = "1"; } }; const onUp = async () => { if (!dragging) return; dragging = false; document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); await savePanelStatePatch({ buttonX: Math.round(button.getBoundingClientRect().left), buttonY: Math.round(button.getBoundingClientRect().top) }); setTimeout(() => { if (button.dataset.dragged === "1") button.dataset.dragged = "0"; }, 80); }; button.addEventListener("mousedown", event => { if (event.button !== 0 || isOpen()) return; const rect = button.getBoundingClientRect(); startX = event.clientX; startY = event.clientY; originX = rect.left; originY = rect.top; dragging = true; document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp); event.preventDefault(); }); } function attachDrawerResize(drawer) { if (!drawer || resizeObserverAttached || typeof ResizeObserver === "undefined") return; resizeObserverAttached = true; let saveTimer = null; const observer = new ResizeObserver(entries => { const entry = entries[0]; if (!entry || !isOpen()) return; clearTimeout(saveTimer); saveTimer = setTimeout(() => { const rect = entry.target.getBoundingClientRect(); savePanelStatePatch({ panelWidth: Math.round(rect.width), panelHeight: Math.round(rect.height) }); }, 260); }); observer.observe(drawer); } async function requestBackendRefresh(source, type = "manual") { if (location.hostname === "www.educoder.net") { try { await scanEducoderInPage(source); } catch (err) { console.error("Educoder 页面内扫描失败:", err); } return; } const requestId = Date.now() + "-" + Math.random().toString(36).slice(2, 8); const requestedAt = Date.now(); const site = getCurrentPageSite(); const login = site.login || latestResultLogin || saveAutoLoginIfDetected() || parseLoginFromCurrentUrl() || ""; await setValue(STORE_KEY_REFRESH_STATUS, { status: "waiting", requestId, progress: 8, message: "等待后台响应", siteKey: getCurrentBackendSiteKey(), siteName: site.siteName, time: new Date().toLocaleString(), timestamp: Date.now() }); await setValue(STORE_KEY_REFRESH_REQUEST, { type, login, siteKey: getCurrentBackendSiteKey(), siteName: site.siteName, pageHost: location.hostname, requestId, requestedAt, requestedAtText: new Date(requestedAt).toLocaleString(), source }); } function startRunningPoll() { if (pollTimerId) return; pollWasRunning = false; pollTimerId = setInterval(async () => { if (!isOpen()) { stopRunningPoll(); return; } const refreshStatus = await getValue(STORE_KEY_REFRESH_STATUS, null); if (refreshStatus?.status === "running" || refreshStatus?.status === "waiting") { pollWasRunning = true; render(); return; } if (refreshStatus?.status === "done" || refreshStatus?.status === "error") { render(); stopRunningPoll(); return; } const runningState = await loadRunningState(); const isRunning = runningState?.running === true; if (isRunning) { pollWasRunning = true; render(); return; } if (pollWasRunning) { pollWasRunning = false; render(); stopRunningPoll(); return; } render(); stopRunningPoll(); }, 1000); } function stopRunningPoll() { if (!pollTimerId) return; clearInterval(pollTimerId); pollTimerId = null; } async function deleteTaskCache() { if (!confirm("确定要删除 TG任务助手缓存吗?这会清空当前扫描结果、运行状态、错误信息和复制用 JSON。")) return; await deleteValue(getCurrentPageSite().resultKey); await deleteValue(STORE_KEY); await deleteValue(STORE_KEY_LAST_RUNNING); await deleteValue(STORE_KEY_LAST_ERROR); cacheDeletedNotice = "缓存已删除"; render(); } function updateFloatingBadge(summary) { const badge = document.querySelector(`#${BUTTON_ID} .tg-task-button-badge`); if (!badge) return; const count = summary?.dangerCount || summary?.unfinishedCount || 0; badge.textContent = String(count > 99 ? "99+" : count); badge.style.display = count > 0 ? "block" : "none"; } function getFilterLabel(filter) { const labels = { all: "总任务", site: "站点", unfinished: "未完成", danger: "危险", exercise: "考试", common_homework: "图文", homework: "图文", classroom_experiment: "实验", experiment: "实验" }; return labels[filter] || "全部任务"; } function taskMatchesFilter(task, filter) { if (filter === "site") return true; if (filter === "unfinished") return !task.completed; if (filter === "exercise") return task.taskType === "exercise"; if (filter === "common_homework" || filter === "homework") return task.taskType === "common_homework"; if (filter === "classroom_experiment" || filter === "experiment") return task.taskType === "classroom_experiment"; if (filter === "danger") return isDangerTask(task); return true; } function renderSummaryCard(filter, numberHtml, label, selectedFilter) { const selected = selectedFilter === filter; return ` `; } function getEducoderPageFetch() { if (typeof unsafeWindow !== "undefined" && unsafeWindow?.fetch) { return unsafeWindow.fetch.bind(unsafeWindow); } return window.fetch.bind(window); } async function fetchEducoderPageJson(url) { const pageFetch = getEducoderPageFetch(); const res = await pageFetch(url, { method: "GET", mode: "cors", credentials: "include", cache: "no-store", headers: { accept: "application/json", "content-type": "application/json; charset=utf-8" } }); const text = await res.text(); let data; try { data = JSON.parse(text); } catch (err) { const error = new Error("Educoder JSON解析失败:" + text.slice(0, 300)); error.detail = { url, httpStatus: res.status, statusText: res.statusText, textPreview: text.slice(0, 500), keys: [], parseError: err?.message || String(err) }; throw error; } return { ok: res.ok, status: res.status, statusText: res.statusText, data, textPreview: text.slice(0, 500), keys: data && typeof data === "object" ? Object.keys(data) : [] }; } async function fetchAndLogEducoderPageJson(url, requestLog, type) { try { const resp = await fetchEducoderPageJson(url); requestLog.push({ siteKey: "educoder", siteName: "Educoder主站", type, url, httpStatus: resp.status, statusText: resp.statusText, ok: resp.ok, textPreview: resp.textPreview, keys: resp.keys, status: resp.data?.status, message: resp.data?.message, rawCount: Array.isArray(resp.data?.exercises) ? resp.data.exercises.length : Array.isArray(resp.data?.homeworks) ? resp.data.homeworks.length : undefined }); return resp; } catch (err) { const detail = err?.detail || {}; requestLog.push({ siteKey: "educoder", siteName: "Educoder主站", type, url, httpStatus: detail.httpStatus || 0, statusText: detail.statusText || "", ok: false, textPreview: detail.textPreview || "", keys: detail.keys || [], error: err?.message || String(err) }); throw err; } } function parseEducoderExerciseStatus(user) { if (!user) { return { commitStatus: null, statusText: "未提交", completed: false }; } const commitStatus = user.commit_status; if (commitStatus === 0) return { commitStatus, statusText: "未提交", completed: false }; if (commitStatus === 1) return { commitStatus, statusText: "提交中/待评阅", completed: false }; if (commitStatus === 2) return { commitStatus, statusText: "已完成/已提交", completed: true }; return { commitStatus, statusText: user.end_at || user.score != null ? "已完成/已提交" : "未知状态", completed: !!user.end_at || user.score != null }; } function parseEducoderHomeworkItem(course, item) { const homeworkStatus = Array.isArray(item?.status) ? item.status : []; const workStatus = Array.isArray(item?.work_status) ? item.work_status : []; const workStatusText = workStatus.join(" "); const ended = homeworkStatus.includes("已截止") || item?.time_status === 5; let notSubmitted = false; let submitted = false; if (typeof item?.un_commit_work === "boolean") { notSubmitted = item.un_commit_work === true; submitted = item.un_commit_work === false; } else { notSubmitted = workStatusText.includes("提交作品") || workStatusText.includes("未提交"); submitted = workStatusText.includes("查看作品") || workStatusText.includes("已提交") || workStatusText.includes("已完成"); } let statusText = "未知状态"; if (notSubmitted) statusText = ended ? "已截止/未提交" : "未提交"; else if (submitted) statusText = ended ? "已截止/已提交" : "已提交"; const homeworkId = item?.homework_id || item?.id; return { taskType: "common_homework", taskTypeText: "图文作业", siteKey: "educoder", siteName: "Educoder主站", courseId: course.courseId, courseIdentifier: course.courseIdentifier, courseName: course.courseName, homeworkId, workId: item?.work_id || item?.student_work_id, studentWorkId: item?.student_work_id, title: item?.name || item?.homework_name || "未命名图文作业", homeworkStatus, workStatus, timeStatus: item?.time_status, statusText, completed: submitted, submitted, notSubmitted, ended, publishTime: item?.publish_time || "", endTime: item?.end_time || item?.end_time_s || "", lateTime: item?.late_time || "", score: "--", source: "educoder-page-homework_commons_1", detailUrl: `${course.pageOrigin}/classrooms/${course.courseIdentifier}/common_homework/${homeworkId}/detail?tabs=0`, scanTime: new Date().toLocaleString(), raw: item }; } function parseEducoderExperimentItem(course, item) { const finishedStatus = Number(item?.shixun_finished_status); const completed = finishedStatus === 1; const ended = item?.time_status === 5 || (Array.isArray(item?.status) && item.status.includes("已截止")); const homeworkId = item?.homework_id || item?.id; return { taskType: "classroom_experiment", taskTypeText: "课堂实验", siteKey: "educoder", siteName: "Educoder主站", courseId: course.courseId, courseIdentifier: course.courseIdentifier, courseName: course.courseName, homeworkId, title: item?.name || item?.shixun_name || "未命名课堂实验", statusText: completed ? "已完成" : ended ? "已截止/未完成" : "进行中/未完成", completed, submitted: completed, notSubmitted: !completed, ended, publishTime: item?.publish_time || "", endTime: item?.end_time_s || item?.end_time || "", lateTime: item?.late_time || "", challengeCount: item?.challenge_count ?? null, finishedChallengeCount: item?.finished_challenge_count ?? null, passedTime: item?.student_passed_time || "", detailUrl: `${course.pageOrigin}/classrooms/${course.courseIdentifier}/shixun_homework/${homeworkId}/detail?tabs=1`, source: "educoder-page-homework_commons_4", scanTime: new Date().toLocaleString(), raw: item }; } async function scanEducoderInPage(source = "frontend-header-button") { const site = getCurrentPageSite(); const requestId = Date.now() + "-" + Math.random().toString(36).slice(2, 8); const requestLog = []; const errors = []; const scanTime = new Date().toLocaleString(); const scanTimestamp = Date.now(); const course = { courseId: site.courseId, courseIdentifier: site.courseIdentifier, courseName: site.courseName, pageOrigin: site.pageOrigin, apiOrigin: site.apiOrigin }; await setValue(STORE_KEY_REFRESH_STATUS, { status: "running", requestId, progress: 18, message: "正在扫描 Educoder 主站任务", siteKey: "educoder", siteName: site.siteName, source, time: new Date().toLocaleString(), timestamp: Date.now() }); try { const homepageUrl = `${site.apiOrigin}/api/users/${encodeURIComponent(site.login)}/homepage_info.json?zzud=${encodeURIComponent(site.login)}`; const homepageResp = await fetchAndLogEducoderPageJson(homepageUrl, requestLog, "homepage_info"); const homepage = homepageResp.data; if (!homepage || homepage.is_logged_user !== true) { throw new Error("Educoder 主站未登录或登录态失效:" + JSON.stringify({ status: homepage?.status, message: homepage?.message, keys: homepageResp.keys, preview: homepageResp.textPreview })); } const exerciseUrl = `${site.apiOrigin}/api/v2/courses/${course.courseIdentifier}/exercises.json?coursesId=${course.courseIdentifier}&limit=20&type=&id=${course.courseIdentifier}&zzud=${encodeURIComponent(site.login)}`; const exerciseResp = await fetchAndLogEducoderPageJson(exerciseUrl, requestLog, "exercise_list"); if (!Array.isArray(exerciseResp.data?.exercises)) { throw new Error("Educoder 考试列表返回结构异常:" + JSON.stringify({ status: exerciseResp.data?.status, message: exerciseResp.data?.message, keys: exerciseResp.keys, preview: exerciseResp.textPreview })); } const rawExams = exerciseResp.data.exercises; const exams = []; for (const exam of rawExams) { const exerciseId = exam?.id; let user = null; let totalScore = "--"; let status = { commitStatus: null, statusText: "未提交", completed: false }; if (exerciseId) { try { const userUrl = `${site.apiOrigin}/api/exercises/${encodeURIComponent(exerciseId)}/exercise_users.json?page=1&limit=20&coursesId=${course.courseIdentifier}&categoryId=${encodeURIComponent(exerciseId)}&zzud=${encodeURIComponent(site.login)}`; const userResp = await fetchAndLogEducoderPageJson(userUrl, requestLog, "exercise_user"); const userPayload = userResp.data; user = userPayload?.current_answer_user || userPayload?.data?.current_answer_user || null; totalScore = userPayload?.total_score ?? userPayload?.data?.total_score ?? "--"; status = parseEducoderExerciseStatus(user); } catch (err) { errors.push({ stage: "exercise_user", siteKey: "educoder", siteName: site.siteName, courseName: course.courseName, exerciseId, message: err?.message || String(err) }); } } exams.push({ taskType: "exercise", taskTypeText: "考试/小测试", siteKey: "educoder", siteName: site.siteName, courseId: course.courseId, courseIdentifier: course.courseIdentifier, courseName: course.courseName, exerciseId, title: exam?.exercise_name || exam?.name || exam?.title || "未命名考试", detailUrl: `${site.pageOrigin}/classrooms/${course.courseIdentifier}/exercisenotice/${exerciseId}/users/${encodeURIComponent(site.login)}`, deadlineRemaining: exam?.exercise_left_time || "--", durationMinutes: exam?.time ?? null, exerciseStatus: exam?.exercise_status ?? null, currentStatus: exam?.current_status ?? null, wholeExerciseStatus: exam?.whole_exercise_status ?? null, exerciseUserId: exam?.exercise_user_id ?? user?.exercise_user_id ?? null, ...status, startAt: user?.start_at || null, endAt: user?.end_at || null, score: user?.score ?? "--", totalScore, objectiveScore: user?.objective_score ?? "--", subjectiveScore: user?.subjective_score ?? "--", reviewStatus: user?.review_status ?? false, userName: user?.user_name || "", studentId: user?.student_id || "", scanTime, raw: { exam, user } }); } const homeworkUrl = `${site.apiOrigin}/api/courses/${course.courseIdentifier}/homework_commons.json?limit=100&status=0&id=${course.courseIdentifier}&type=1&sort_by=updated_at&sort_direction=asc&order=0&zzud=${encodeURIComponent(site.login)}`; const homeworkResp = await fetchAndLogEducoderPageJson(homeworkUrl, requestLog, "homework_commons_1"); if (!Array.isArray(homeworkResp.data?.homeworks)) { throw new Error("Educoder 图文作业返回结构异常:" + JSON.stringify({ status: homeworkResp.data?.status, message: homeworkResp.data?.message, keys: homeworkResp.keys, preview: homeworkResp.textPreview })); } const homeworks = homeworkResp.data.homeworks.map(item => parseEducoderHomeworkItem(course, item)); const experimentUrl = `${site.apiOrigin}/api/courses/${course.courseIdentifier}/homework_commons.json?limit=100&status=0&id=${course.courseIdentifier}&type=4&sort_by=updated_at&sort_direction=asc&order=0&zzud=${encodeURIComponent(site.login)}`; const experimentResp = await fetchAndLogEducoderPageJson(experimentUrl, requestLog, "homework_commons_4"); if (!Array.isArray(experimentResp.data?.homeworks)) { throw new Error("Educoder 课堂实验返回结构异常:" + JSON.stringify({ status: experimentResp.data?.status, message: experimentResp.data?.message, keys: experimentResp.keys, preview: experimentResp.textPreview })); } const experiments = experimentResp.data.homeworks.map(item => parseEducoderExperimentItem(course, item)); const tasks = sortTasks([...exams, ...homeworks, ...experiments]); const result = { login: site.login, currentSite: { key: "educoder", name: site.siteName, pageOrigin: site.pageOrigin, apiOrigin: site.apiOrigin }, scanTime, scanTimestamp, source: "educoder-page-frontend", courses: [ { courseId: course.courseId, courseIdentifier: course.courseIdentifier, courseName: course.courseName, siteKey: "educoder", siteName: site.siteName, pageOrigin: site.pageOrigin, apiOrigin: site.apiOrigin, login: site.login, exams, homeworks, experiments } ], tasks, errors, warnings: [], debug: { requestLog }, summary: calculateSummaryFromTasks(tasks) }; await setValue(STORE_KEY_EDUCODER_RESULT, result); await setValue(STORE_KEY_REFRESH_STATUS, { status: "done", requestId, progress: 100, message: "刷新完成", siteKey: "educoder", siteName: site.siteName, time: new Date().toLocaleString(), timestamp: Date.now() }); render(); return result; } catch (err) { const message = err?.message || String(err); await setValue(STORE_KEY_LAST_ERROR, { siteKey: "educoder", siteName: site.siteName, time: new Date().toLocaleString(), message: "Educoder 页面内刷新失败,请复制全部 JSON 排查。", error: message, stack: err?.stack || "", stage: "educoder_page_frontend", debug: { requestLog, errorDetail: err?.detail || null } }); await setValue(STORE_KEY_REFRESH_STATUS, { status: "error", requestId, progress: 100, message: "Educoder 页面内刷新失败,请复制全部 JSON 排查。", error: message, siteKey: "educoder", siteName: site.siteName, time: new Date().toLocaleString(), timestamp: Date.now() }); render(); throw err; } } function renderYearMonthFilter(year, month) { const now = new Date(); const currentYear = now.getFullYear(); const years = []; for (let y = currentYear - 3; y <= currentYear + 1; y += 1) years.push(y); const yearOptions = years.map(y => ``).join(""); const monthOptions = Array.from({ length: 12 }, (_, i) => i + 1).map(m => ``).join(""); return `
起始时间:
显示任务起点:${escapeHtml(year)} 年 ${escapeHtml(month)} 月之后
`; } function renderRefreshStatus(runningState, lastError, refreshRequest, refreshHandled, refreshStatus) { const isRunning = runningState?.running === true || refreshStatus?.status === "running"; const hasError = !isRunning && refreshStatus?.status !== "running" && lastError?.message; const percent = clamp(Number(refreshStatus?.progress ?? runningState?.percent) || (isRunning ? 12 : 100), 0, 100); const requestedAt = Number(refreshRequest?.requestedAt) || 0; const handledAt = Number(refreshHandled) || 0; const pending = requestedAt && requestedAt > handledAt && !isRunning; const statusClass = isRunning ? "tg-refresh-running" : hasError || refreshStatus?.status === "error" ? "tg-refresh-failed" : refreshStatus?.status === "done" || runningState?.stage === "done" ? "tg-refresh-done" : pending ? "tg-refresh-pending" : ""; let title = "刷新状态"; let stage = location.hostname === "www.educoder.net" ? "展开面板后会自动进行页面内刷新。" : "展开面板后会自动请求后台刷新。"; let barPercent = pending ? 18 : 0; if (isRunning) { title = "正在刷新"; stage = cleanStatusMessage(refreshStatus?.message || runningState?.stageText || "后台正在扫描任务"); barPercent = percent; } else if (refreshStatus?.status === "done") { title = "最近刷新成功"; stage = cleanStatusMessage(refreshStatus.message || "刷新完成"); barPercent = 100; } else if (refreshStatus?.status === "error") { title = "最近刷新失败"; stage = cleanStatusMessage(refreshStatus.error || refreshStatus.message || "刷新失败"); barPercent = 100; } else if (hasError) { title = "最近刷新失败"; stage = cleanStatusMessage(lastError.message); barPercent = 100; } else if (runningState?.stage === "done") { title = "最近刷新完成"; stage = cleanStatusMessage(runningState.endTime || runningState.stageText || "扫描完成"); barPercent = 100; } else if (pending) { title = "已请求刷新"; } else if (runningState?.stageText) { title = "最近刷新状态"; stage = cleanStatusMessage(runningState.stageText); barPercent = percent; } return `
${escapeHtml(title)} ${Math.round(barPercent)}%
${escapeHtml(stage)}
`; } function appendLog(text) { const boxWrap = document.querySelector(`#${DRAWER_ID} .tg-json-container`); const box = document.querySelector(`#${DRAWER_ID} .tg-json-box`); if (!boxWrap || !box) return; const line = `[${new Date().toLocaleString()}] ${text}`; box.value = box.value ? `${box.value}\n${line}` : line; boxWrap.style.display = "block"; } function writeOutput(text) { const boxWrap = document.querySelector(`#${DRAWER_ID} .tg-json-container`); const box = document.querySelector(`#${DRAWER_ID} .tg-json-box`); if (!boxWrap || !box) return; box.value = text; boxWrap.style.display = "block"; } function parseCourseIdFromCurrentUrl() { const match = location.href.match(/\/classrooms\/([^/?#]+)/); return match && match[1] ? decodeURIComponent(match[1]) : ""; } function parseLoginFromCurrentUrl() { const userMatch = location.href.match(/\/users\/([^/?#]+)/); if (userMatch && userMatch[1]) return decodeURIComponent(userMatch[1]); const zzudMatch = location.href.match(/[?&]zzud=([^&]+)/); if (zzudMatch && zzudMatch[1]) return decodeURIComponent(zzudMatch[1]); const usernameMatch = location.href.match(/[?&]username=([^&]+)/); if (usernameMatch && usernameMatch[1]) return decodeURIComponent(usernameMatch[1]); return ""; } function resolveJumpCourseId(task) { const direct = task?.courseIdentifier || task?.course_id || task?.coursesId; if (direct) return String(direct); if (task?.courseId && !/^\d+$/.test(String(task.courseId))) { return String(task.courseId); } const raw = task?.raw || {}; const rawCourse = raw.courseIdentifier || raw.course_identifier || raw.coursesId || raw.course_id; if (rawCourse && !/^\d+$/.test(String(rawCourse))) { return String(rawCourse); } return parseCourseIdFromCurrentUrl(); } function resolveJumpLogin(task) { return String( task?.login || task?.userLogin || task?.raw?.user_login || latestResultLogin || parseLoginFromCurrentUrl() || "" ).trim(); } function getJumpType(task) { if (task?.taskType === "classroom_experiment") return "shixun_homework"; if (task?.taskType === "exercise") return "exercise"; if (task?.taskType === "common_homework") return "common_homework"; return ""; } function getJumpItem(task) { if (task?.taskType === "exercise") { return task?.raw?.exam || { id: task?.exerciseId || task?.id }; } if (task?.taskType === "classroom_experiment" || task?.taskType === "common_homework") { return task?.raw || { homework_id: task?.homeworkId || task?.id }; } return null; } function buildJumpUrl(type, courseId, item, login) { if (type === "shixun_homework") { if (!item || !item.homework_id) throw new Error("课堂实验缺少 homework_id"); return `${location.origin}/classrooms/${encodeURIComponent(courseId)}/shixun_homework/${encodeURIComponent(item.homework_id)}/detail?tabs=1`; } if (type === "exercise") { if (!item || !item.id) throw new Error("考试缺少 id"); if (!login) throw new Error("考试缺少 login"); return `${location.origin}/classrooms/${encodeURIComponent(courseId)}/exercisenotice/${encodeURIComponent(item.id)}/users/${encodeURIComponent(login)}`; } if (type === "common_homework") { if (!item || !item.homework_id) throw new Error("图文作业缺少 homework_id"); return `${location.origin}/classrooms/${encodeURIComponent(courseId)}/common_homework/${encodeURIComponent(item.homework_id)}/detail?tabs=0`; } throw new Error(`未知跳转类型:${type}`); } function buildTaskJumpUrl(task) { const type = getJumpType(task); const courseId = resolveJumpCourseId(task); const item = getJumpItem(task); const login = resolveJumpLogin(task); if (!courseId) throw new Error("缺少课程 ID"); return buildJumpUrl(type, courseId, item, login); } function jumpToTask(task) { try { const url = task?.detailUrl || buildTaskJumpUrl(task); writeOutput(url); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(url).catch(() => {}); } window.open(url, "_blank"); } catch (err) { console.error("跳转失败:", err, task); alert(`跳转失败:${err.message}`); } } function getTaskTitle(task) { return task.title || task.name || task.exerciseName || task.homeworkName || task.experimentName || task.homeworkTitle || "未命名任务"; } function getTaskTypeLabel(task) { if (task.taskType === "exercise") return "考试 / 小测试"; if (task.taskType === "common_homework") return "图文作业"; if (task.taskType === "classroom_experiment") return "课堂实验"; return task.taskTypeText || "任务"; } function renderDetailLink(task) { const id = `task-${jumpTaskSeq++}`; jumpTaskRegistry.set(id, task); return ``; } function renderTaskCard(task) { const tone = statusTone(task); const status = task.statusText || task.commitStatusText || (task.completed ? "已完成" : "未完成"); const deadline = task.deadlineRemaining || task.endTime || task.endAt || task.end_at || "--"; const scoreText = task.score !== undefined && task.score !== null && task.score !== "" ? task.score : "--"; const totalText = task.totalScore !== undefined && task.totalScore !== null && task.totalScore !== "" ? task.totalScore : "--"; const siteLine = `${getTaskSiteName(task)} · ${task.courseName || "未识别课程"}`; return `
${escapeHtml(getTaskTitle(task))}
${escapeHtml(getTaskTypeLabel(task))}
${escapeHtml(status)}
${isDangerTask(task) ? `请关注截止时间` : ""}
来源:${escapeHtml(siteLine)} 截止:${escapeHtml(deadline)} 成绩:${escapeHtml(scoreText)} / ${escapeHtml(totalText)} 扫描:${escapeHtml(task.scanTime || "--")}
`; } function renderTaskList(tasks) { return tasks.map(renderTaskCard).join(""); } function renderSection(title, tasks, className = "") { if (!tasks.length) return ""; return `
${escapeHtml(title)}${tasks.length}
${renderTaskList(tasks)}
`; } function renderCourseSubgroup(groupKey, typeKey, title, tasks, sectionCollapseState) { if (!tasks.length) return ""; const key = `${groupKey}:${typeKey}`; const collapsed = sectionCollapseState[key] === true; return `
${escapeHtml(title)} ${tasks.length}
${renderTaskList(tasks)}
`; } function renderCourseGroups(groups, courseCollapseState, sectionCollapseState) { return groups.map(group => { const collapsed = courseCollapseState[group.key] === true; const stats = [ `站点 ${group.siteName || "TG校内站"}`, `总任务 ${group.tasks?.length || 0}`, `考试 ${group.exams?.length || 0}`, `图文 ${group.homeworks?.length || 0}`, `实验 ${group.experiments?.length || 0}`, `危险 ${group.danger || 0}` ]; return `
${escapeHtml(group.courseName || "未识别课程")} ${escapeHtml(group.siteName || "TG校内站")}
${stats.map(x => `${escapeHtml(x)}`).join("")}
${renderCourseSubgroup(group.key, "exercise", "考试 / 小测试", group.exams || [], sectionCollapseState)} ${renderCourseSubgroup(group.key, "common_homework", "图文作业", group.homeworks || [], sectionCollapseState)} ${renderCourseSubgroup(group.key, "classroom_experiment", "课堂实验", group.experiments || [], sectionCollapseState)}
`; }).join(""); } async function render() { createRoot(); const body = document.querySelector(`#${DRAWER_ID} .tg-body`); if (!body) return; try { await renderPanelContent(body); } catch (err) { console.error("TG任务助手渲染失败", err); body.innerHTML = `
前台面板渲染失败:${escapeHtml(cleanStatusMessage(err?.message || String(err)))}
`; } } async function renderPanelContent(body) { jumpTaskRegistry = new Map(); jumpTaskSeq = 0; const state = await loadFrontendState(); const data = state.result; const runningState = state.lastRunning; const lastError = state.lastError; const refreshRequest = state.refreshRequest; const refreshHandled = state.refreshHandled; const refreshStatus = state.refreshStatus; const currentPageSite = getCurrentPageSite(); const isRunning = runningState?.running === true || refreshStatus?.status === "running"; const currentRefreshRunning = refreshStatus?.status === "running"; const currentRefreshError = refreshStatus?.status === "error"; const refreshStatusHtml = renderRefreshStatus(runningState, lastError, refreshRequest, refreshHandled, refreshStatus); if (isRunning && !data) { updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 }); body.innerHTML = `${refreshStatusHtml}
后台正在运行
当前阶段:${escapeHtml(cleanStatusMessage(refreshStatus?.message || runningState?.stageText || "正在刷新"))}
`; return; } if (!data && currentRefreshError) { updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 }); body.innerHTML = `${refreshStatusHtml}
最近刷新失败
错误:${escapeHtml(cleanStatusMessage(refreshStatus?.error || refreshStatus?.message || "未知错误"))}
`; return; } if (!data && lastError?.message && !currentRefreshRunning) { updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 }); body.innerHTML = `${refreshStatusHtml}
上次后台错误:${escapeHtml(cleanStatusMessage(lastError.message))}
时间:${escapeHtml(lastError.time || "--")}
`; return; } if (!data) { updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 }); const emptyText = currentPageSite.siteKey === "educoder" ? "当前为 Educoder 主站视图,暂无本平台缓存。请点击“请求刷新”获取任务状态。" : "当前为 TG 校内站视图,暂无本平台缓存。请点击“请求刷新”获取任务状态。"; body.innerHTML = `${refreshStatusHtml}${cacheDeletedNotice ? `
${escapeHtml(cacheDeletedNotice)}
` : ""}
${escapeHtml(emptyText)}
`; return; } const manualLogin = String(MANUAL_LOGIN || "").trim(); const autoLogin = state.autoLogin; const expectedLogin = currentPageSite.login || manualLogin || autoLogin; const resultLogin = String(data?.login || "").trim(); latestResultLogin = resultLogin; const accountText = expectedLogin || "未识别"; const accountSource = currentPageSite.login ? currentPageSite.siteName : manualLogin ? "手动配置" : autoLogin ? "自动识别" : "未识别"; let accountWarningHtml = ""; if (!isResultForCurrentSite(data)) { updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 }); body.innerHTML = `
当前缓存数据属于另一个账号,请重新运行后台扫描。
`; return; } if (!expectedLogin) { accountWarningHtml = `
账号:未识别
提示:请先打开 TG 页面,或在后台脚本顶部填写 MANUAL_LOGIN。
`; } const filterYearMonth = state.filterYearMonth; const courseCollapseState = state.courseCollapse; const sectionCollapseState = state.sectionCollapse; const viewModel = buildViewModel(data, state); const summary = viewModel.stats; updateFloatingBadge(summary); const errors = data.errors || []; const warnings = data.warnings || []; const failedSiteLogins = Array.isArray(data?.debug?.siteLogin) ? data.debug.siteLogin.filter(item => item && item.ok === false) : []; const filter = state.currentFilter; const filteredTasks = viewModel.visibleTasks; const dangerTasks = viewModel.dangerTasks; const dangerList = dangerTasks; requestDangerNotification(dangerList, data); let html = accountWarningHtml + refreshStatusHtml; if (isRunning) { html += `
后台正在运行
当前阶段:${escapeHtml(cleanStatusMessage(refreshStatus?.message || runningState?.stageText || "正在刷新"))}
`; } else if (currentRefreshError) { html += `
最近刷新失败
错误:${escapeHtml(cleanStatusMessage(refreshStatus?.error || refreshStatus?.message || "未知错误"))}
`; } else if (lastError && lastError.message && !currentRefreshRunning) { html += `
上次后台错误:${escapeHtml(cleanStatusMessage(lastError.message))}
时间:${escapeHtml(lastError.time || "--")}
`; } if (failedSiteLogins.length) { html += failedSiteLogins.map(item => { const siteName = item.siteName || "站点"; const loginUrl = item.siteKey === "educoder" ? "https://www.educoder.net" : "https://tg.zcst.edu.cn"; return `
${escapeHtml(siteName)}未登录或登录态失效,请先打开 ${escapeHtml(loginUrl)} 并登录后重新刷新。
原因:${escapeHtml(cleanStatusMessage(item.error || "登录态预检失败"))}
`; }).join(""); } html += `
${escapeHtml(currentPageSite.viewHint)}
任务状态总览 / 实时监控
当前账号${escapeHtml(accountText)}
账号来源${escapeHtml(accountSource)}
上次扫描${escapeHtml(data.scanTime || "--")}
当前范围${escapeHtml(filterYearMonth.year)} 年 ${escapeHtml(filterYearMonth.month)} 月之后
${renderSummaryCard("site", `
${summary.siteCount}
`, "站点", filter)} ${renderSummaryCard("all", `
${summary.taskCount}
`, "总任务", filter)} ${renderSummaryCard("exercise", `
${summary.exerciseCount}
`, "考试", filter)} ${renderSummaryCard("common_homework", `
${summary.homeworkCount}
`, "图文", filter)} ${renderSummaryCard("classroom_experiment", `
${summary.experimentCount}
`, "实验", filter)} ${renderSummaryCard("danger", `
${summary.dangerCount}
`, "危险", filter)}
${renderYearMonthFilter(filterYearMonth.year, filterYearMonth.month)} ${filter !== "all" ? `
正在查看:${escapeHtml(getFilterLabel(filter))}
` : ""} `; if (dangerList.length) { html += `
危险提醒:发现 ${dangerList.length} 个 10 天内截止的未完成任务,请优先处理。
`; html += renderSection("紧急任务", dangerList, "tg-section-urgent"); } if (!filteredTasks.length) { html += `
当前筛选下没有任务。
`; } else { html += renderCourseGroups(viewModel.courseGroups, courseCollapseState, sectionCollapseState); } if (errors.length || warnings.length) { html += `
本次扫描:${errors.length} 个错误,${warnings.length} 个提醒。
需要排查时可以复制全部 JSON。
`; } html += ``; body.innerHTML = html; updateCountdowns(); ensureCountdownTimer(); latestRenderedScanTimestamp = Number(data.scanTimestamp) || latestRenderedScanTimestamp; } async function getAllJsonText() { const state = await loadFrontendState(); return JSON.stringify( { result: state.result, lastRunning: state.lastRunning, lastError: state.lastError, refreshRequest: state.refreshRequest, refreshHandled: state.refreshHandled, refreshStatus: state.refreshStatus, panelState: state.panelState, courseCollapse: state.courseCollapse, sectionCollapse: state.sectionCollapse, copiedAt: new Date().toLocaleString(), pageUrl: location.href, pageMode: isOpen() ? "panel-open" : "button-only" }, null, 2 ); } async function copyAllJson() { const btn = document.querySelector("[data-copy-all-json]"); const text = await getAllJsonText(); try { await navigator.clipboard.writeText(text); btn.textContent = "已复制"; setTimeout(() => { btn.textContent = "全部 JSON"; }, 1200); } catch (e) { showJsonBox(text); btn.textContent = "手动复制"; setTimeout(() => { btn.textContent = "全部 JSON"; }, 1500); } } function showJsonBox(text) { setDrawerOpen(true); const boxWrap = document.querySelector(`#${DRAWER_ID} .tg-json-container`); const box = document.querySelector(`#${DRAWER_ID} .tg-json-box`); if (!boxWrap || !box) return; box.value = text; boxWrap.style.display = boxWrap.style.display === "none" ? "block" : "none"; box.focus(); box.select(); } window.__tgTaskAssistant = { render, loadData, loadRunningState, loadLastError }; saveAutoLoginIfDetected(); if (typeof GM_registerMenuCommand === "function") { GM_registerMenuCommand("显示/请求刷新 TG任务助手", () => { setDrawerOpen(true); render(); }); } window.addEventListener("resize", () => { clearTimeout(resizeApplyTimer); resizeApplyTimer = setTimeout(() => { applyPanelState(); }, 120); }); setTimeout(() => { createRoot(); render(); }, 1000); })();