feat: 添加贡献内容更新服务,优化侧边栏贡献模式按钮及相关提示功能
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
(() => {
|
||||
const PORTAL_BASE_URL = 'https://apikey.qzz.io';
|
||||
const CONTENT_SUMMARY_API_URL = `${PORTAL_BASE_URL}/api/content-summary`;
|
||||
const CACHE_KEY = 'multipage-contribution-content-summary-v1';
|
||||
const FETCH_TIMEOUT_MS = 6000;
|
||||
|
||||
function sanitizeItem(item = {}) {
|
||||
return {
|
||||
slug: String(item?.slug || '').trim(),
|
||||
title: String(item?.title || '').trim(),
|
||||
isEnabled: Boolean(item?.is_enabled),
|
||||
hasContent: Boolean(item?.has_content),
|
||||
isVisible: Boolean(item?.is_visible),
|
||||
updatedAt: String(item?.updated_at || '').trim(),
|
||||
updatedAtDisplay: String(item?.updated_at_display || '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSnapshot(payload = {}) {
|
||||
const items = Array.isArray(payload?.items)
|
||||
? payload.items.map(sanitizeItem).filter((item) => item.slug)
|
||||
: [];
|
||||
const promptVersion = String(payload?.prompt_version || '').trim();
|
||||
const latestUpdatedAt = String(payload?.latest_updated_at || '').trim();
|
||||
const latestUpdatedAtDisplay = String(payload?.latest_updated_at_display || '').trim();
|
||||
const hasVisibleUpdates = Boolean(payload?.has_visible_updates) && Boolean(promptVersion);
|
||||
|
||||
return {
|
||||
status: hasVisibleUpdates ? 'update-available' : 'idle',
|
||||
promptVersion,
|
||||
hasVisibleUpdates,
|
||||
latestUpdatedAt,
|
||||
latestUpdatedAtDisplay,
|
||||
items,
|
||||
portalUrl: PORTAL_BASE_URL,
|
||||
apiUrl: CONTENT_SUMMARY_API_URL,
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function readCache() {
|
||||
try {
|
||||
const raw = localStorage.getItem(CACHE_KEY);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const snapshot = buildSnapshot({
|
||||
items: parsed.items,
|
||||
prompt_version: parsed.promptVersion,
|
||||
has_visible_updates: parsed.hasVisibleUpdates,
|
||||
latest_updated_at: parsed.latestUpdatedAt,
|
||||
latest_updated_at_display: parsed.latestUpdatedAtDisplay,
|
||||
});
|
||||
if (!Number.isFinite(parsed.checkedAt)) {
|
||||
return snapshot;
|
||||
}
|
||||
snapshot.checkedAt = parsed.checkedAt;
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(snapshot) {
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(snapshot));
|
||||
} catch (error) {
|
||||
// Ignore cache write failures.
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContentSummary() {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(CONTENT_SUMMARY_API_URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`内容摘要请求失败:${response.status}`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (!payload || payload.ok !== true) {
|
||||
throw new Error('内容摘要返回格式异常');
|
||||
}
|
||||
|
||||
const snapshot = buildSnapshot(payload);
|
||||
writeCache(snapshot);
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('内容摘要请求超时');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
async function getContentUpdateSnapshot() {
|
||||
try {
|
||||
return await fetchContentSummary();
|
||||
} catch (error) {
|
||||
const cached = readCache();
|
||||
if (cached) {
|
||||
return {
|
||||
...cached,
|
||||
fromCache: true,
|
||||
errorMessage: error?.message || '内容摘要获取失败',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
promptVersion: '',
|
||||
hasVisibleUpdates: false,
|
||||
latestUpdatedAt: '',
|
||||
latestUpdatedAtDisplay: '',
|
||||
items: [],
|
||||
portalUrl: PORTAL_BASE_URL,
|
||||
apiUrl: CONTENT_SUMMARY_API_URL,
|
||||
checkedAt: Date.now(),
|
||||
errorMessage: error?.message || '内容摘要获取失败',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
window.SidepanelContributionContentService = {
|
||||
getContentUpdateSnapshot,
|
||||
portalUrl: PORTAL_BASE_URL,
|
||||
apiUrl: CONTENT_SUMMARY_API_URL,
|
||||
};
|
||||
})();
|
||||
+71
-1
@@ -243,6 +243,76 @@ header {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.contribution-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.contribution-update-hint {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
max-width: 220px;
|
||||
margin-left: 6px;
|
||||
padding: 8px 10px;
|
||||
background: color-mix(in srgb, var(--amber) 10%, var(--bg-base));
|
||||
border: 1px solid color-mix(in srgb, var(--amber) 34%, var(--border));
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.contribution-update-hint::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 14px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: inherit;
|
||||
border-top: inherit;
|
||||
border-left: inherit;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.contribution-update-hint-text {
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.contribution-update-hint-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background var(--transition), color var(--transition);
|
||||
}
|
||||
|
||||
.contribution-update-hint-close:hover {
|
||||
background: color-mix(in srgb, var(--amber) 16%, transparent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.contribution-update-hint-close:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--amber) 34%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Theme Toggle
|
||||
============================================================ */
|
||||
@@ -366,7 +436,7 @@ header {
|
||||
|
||||
.run-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -34,8 +34,17 @@
|
||||
</div>
|
||||
<div class="header-btns">
|
||||
<div class="run-group">
|
||||
<button id="btn-contribution-mode" class="btn btn-outline btn-sm btn-contribution-mode" type="button"
|
||||
aria-pressed="false" title="进入贡献模式并打开上传页">贡献/使用</button>
|
||||
<div class="contribution-entry">
|
||||
<button id="btn-contribution-mode" class="btn btn-outline btn-sm btn-contribution-mode" type="button"
|
||||
aria-pressed="false" title="进入贡献模式并打开上传页">贡献/使用</button>
|
||||
<div id="contribution-update-hint" class="contribution-update-hint" aria-live="polite" hidden>
|
||||
<p id="contribution-update-hint-text" class="contribution-update-hint-text">
|
||||
公告 / 使用教程有更新了,可点上方“贡献/使用”查看。
|
||||
</p>
|
||||
<button id="btn-dismiss-contribution-update-hint" class="contribution-update-hint-close" type="button"
|
||||
aria-label="关闭更新提示" title="关闭更新提示">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="number" id="input-run-count" class="run-count-input" value="1" min="1" max="50" title="运行次数" />
|
||||
<button id="btn-auto-run" class="btn btn-success" title="自动执行全部步骤">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||
@@ -710,6 +719,7 @@
|
||||
<script src="../luckmail-utils.js"></script>
|
||||
<script src="../data/step-definitions.js"></script>
|
||||
<script src="update-service.js"></script>
|
||||
<script src="contribution-content-update-service.js"></script>
|
||||
<script src="hotmail-manager.js"></script>
|
||||
<script src="mail-2925-manager.js"></script>
|
||||
<script src="icloud-manager.js"></script>
|
||||
|
||||
+117
-54
@@ -57,6 +57,9 @@ const btnSaveSettings = document.getElementById('btn-save-settings');
|
||||
const btnStop = document.getElementById('btn-stop');
|
||||
const btnReset = document.getElementById('btn-reset');
|
||||
const btnContributionMode = document.getElementById('btn-contribution-mode');
|
||||
const contributionUpdateHint = document.getElementById('contribution-update-hint');
|
||||
const contributionUpdateHintText = document.getElementById('contribution-update-hint-text');
|
||||
const btnDismissContributionUpdateHint = document.getElementById('btn-dismiss-contribution-update-hint');
|
||||
const stepsProgress = document.getElementById('steps-progress');
|
||||
const btnAutoRun = document.getElementById('btn-auto-run');
|
||||
const btnAutoContinue = document.getElementById('btn-auto-continue');
|
||||
@@ -240,6 +243,7 @@ const MAIL_2925_MODE_RECEIVE = 'receive';
|
||||
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
|
||||
const AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-skip-failures-prompt-dismissed';
|
||||
const AUTO_RUN_FALLBACK_RISK_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-run-fallback-risk-prompt-dismissed';
|
||||
const CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY = 'multipage-contribution-content-prompt-dismissed-version';
|
||||
const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 15;
|
||||
const AUTO_RUN_FALLBACK_RISK_RECOMMENDED_THREAD_INTERVAL_MINUTES = 5;
|
||||
const HOTMAIL_SERVICE_MODE_REMOTE = 'remote';
|
||||
@@ -494,6 +498,8 @@ let scheduledCountdownTimer = null;
|
||||
let configMenuOpen = false;
|
||||
let configActionInFlight = false;
|
||||
let currentReleaseSnapshot = null;
|
||||
let currentContributionContentSnapshot = null;
|
||||
let contributionContentSnapshotRequestInFlight = null;
|
||||
|
||||
const EYE_OPEN_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
|
||||
const EYE_CLOSED_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-6.94"/><path d="M9.9 4.24A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.86 21.86 0 0 1-2.16 3.19"/><path d="M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
|
||||
@@ -511,6 +517,7 @@ const normalizeLuckmailTimestampValue = window.LuckMailUtils?.normalizeTimestamp
|
||||
return Number.isFinite(timestamp) ? timestamp : 0;
|
||||
});
|
||||
const sidepanelUpdateService = window.SidepanelUpdateService;
|
||||
const contributionContentService = window.SidepanelContributionContentService;
|
||||
const DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME = window.LuckMailUtils?.DEFAULT_LUCKMAIL_PRESERVE_TAG_NAME || '保留';
|
||||
const normalizeIcloudHost = window.IcloudUtils?.normalizeIcloudHost
|
||||
|| ((value) => {
|
||||
@@ -821,6 +828,19 @@ function setPromptDismissed(storageKey, dismissed) {
|
||||
}
|
||||
}
|
||||
|
||||
function getDismissedContributionContentPromptVersion() {
|
||||
return String(localStorage.getItem(CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY) || '').trim();
|
||||
}
|
||||
|
||||
function setDismissedContributionContentPromptVersion(version) {
|
||||
const normalized = String(version || '').trim();
|
||||
if (normalized) {
|
||||
localStorage.setItem(CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY, normalized);
|
||||
} else {
|
||||
localStorage.removeItem(CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
function isAutoSkipFailuresPromptDismissed() {
|
||||
return isPromptDismissed(AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY);
|
||||
}
|
||||
@@ -2192,6 +2212,81 @@ async function initializeReleaseInfo() {
|
||||
renderReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
function getContributionUpdateHintMessage(snapshot = currentContributionContentSnapshot) {
|
||||
if (!snapshot?.promptVersion) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '公告 / 使用教程有更新了,可点上方“贡献/使用”查看。';
|
||||
}
|
||||
|
||||
function shouldShowContributionUpdateHint(snapshot = currentContributionContentSnapshot) {
|
||||
const promptVersion = String(snapshot?.promptVersion || '').trim();
|
||||
if (!contributionUpdateHint || !contributionUpdateHintText || !btnContributionMode) {
|
||||
return false;
|
||||
}
|
||||
if (!promptVersion) {
|
||||
return false;
|
||||
}
|
||||
if (promptVersion === getDismissedContributionContentPromptVersion()) {
|
||||
return false;
|
||||
}
|
||||
if (latestState?.contributionMode) {
|
||||
return false;
|
||||
}
|
||||
return !btnContributionMode.disabled;
|
||||
}
|
||||
|
||||
function renderContributionUpdateHint(snapshot = currentContributionContentSnapshot) {
|
||||
if (!contributionUpdateHint) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visible = shouldShowContributionUpdateHint(snapshot);
|
||||
contributionUpdateHint.hidden = !visible;
|
||||
if (!visible || !contributionUpdateHintText) {
|
||||
return;
|
||||
}
|
||||
|
||||
contributionUpdateHintText.textContent = getContributionUpdateHintMessage(snapshot);
|
||||
}
|
||||
|
||||
function dismissContributionUpdateHint() {
|
||||
const promptVersion = String(currentContributionContentSnapshot?.promptVersion || '').trim();
|
||||
if (promptVersion) {
|
||||
setDismissedContributionContentPromptVersion(promptVersion);
|
||||
}
|
||||
renderContributionUpdateHint();
|
||||
}
|
||||
|
||||
async function refreshContributionContentHint() {
|
||||
if (!contributionContentService?.getContentUpdateSnapshot) {
|
||||
currentContributionContentSnapshot = null;
|
||||
renderContributionUpdateHint();
|
||||
return null;
|
||||
}
|
||||
if (contributionContentSnapshotRequestInFlight) {
|
||||
return contributionContentSnapshotRequestInFlight;
|
||||
}
|
||||
|
||||
contributionContentSnapshotRequestInFlight = contributionContentService.getContentUpdateSnapshot()
|
||||
.then((snapshot) => {
|
||||
currentContributionContentSnapshot = snapshot;
|
||||
renderContributionUpdateHint(snapshot);
|
||||
return snapshot;
|
||||
})
|
||||
.catch((error) => {
|
||||
currentContributionContentSnapshot = null;
|
||||
renderContributionUpdateHint(null);
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
contributionContentSnapshotRequestInFlight = null;
|
||||
});
|
||||
|
||||
return contributionContentSnapshotRequestInFlight;
|
||||
}
|
||||
|
||||
function syncPasswordField(state) {
|
||||
inputPassword.value = state?.contributionMode ? '' : (state.customPassword || state.password || '');
|
||||
}
|
||||
@@ -3270,12 +3365,16 @@ const contributionModeManager = window.SidepanelContributionMode?.createContribu
|
||||
sendMessage: (message) => chrome.runtime.sendMessage(message),
|
||||
},
|
||||
constants: {
|
||||
contributionOauthUrl: 'https://apikey.qzz.io/oauth/',
|
||||
contributionUploadUrl: 'https://apikey.qzz.io',
|
||||
contributionOauthUrl: `${contributionContentService?.portalUrl || 'https://apikey.qzz.io'}/oauth/`,
|
||||
contributionUploadUrl: contributionContentService?.portalUrl || 'https://apikey.qzz.io',
|
||||
},
|
||||
});
|
||||
const renderContributionMode = contributionModeManager?.render
|
||||
const baseRenderContributionMode = contributionModeManager?.render
|
||||
|| (() => { });
|
||||
const renderContributionMode = () => {
|
||||
baseRenderContributionMode();
|
||||
renderContributionUpdateHint();
|
||||
};
|
||||
const bindContributionModeEvents = contributionModeManager?.bindEvents
|
||||
|| (() => { });
|
||||
bindContributionModeEvents();
|
||||
@@ -3596,6 +3695,11 @@ extensionUpdateStatus?.addEventListener('click', () => {
|
||||
openReleaseListPage();
|
||||
});
|
||||
|
||||
btnDismissContributionUpdateHint?.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
dismissContributionUpdateHint();
|
||||
});
|
||||
|
||||
configMenu?.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
@@ -3631,6 +3735,12 @@ autoStartModal?.addEventListener('click', (event) => {
|
||||
btnAutoStartClose?.addEventListener('click', () => resolveModalChoice(null));
|
||||
|
||||
async function startAutoRunFromCurrentSettings() {
|
||||
try {
|
||||
await refreshContributionContentHint();
|
||||
} catch (error) {
|
||||
console.warn('Failed to refresh contribution content hint before auto run:', error);
|
||||
}
|
||||
|
||||
const totalRuns = getRunCountValue();
|
||||
let mode = 'restart';
|
||||
const autoRunSkipFailures = inputAutoSkipFailures.checked;
|
||||
@@ -3692,57 +3802,7 @@ async function startAutoRunFromCurrentSettings() {
|
||||
// Auto Run
|
||||
btnAutoRun.addEventListener('click', async () => {
|
||||
try {
|
||||
return await startAutoRunFromCurrentSettings();
|
||||
const totalRuns = getRunCountValue();
|
||||
let mode = 'restart';
|
||||
const autoRunSkipFailures = inputAutoSkipFailures.checked;
|
||||
const fallbackThreadIntervalMinutes = normalizeAutoRunThreadIntervalMinutes(
|
||||
inputAutoSkipFailuresThreadIntervalMinutes.value
|
||||
);
|
||||
inputAutoSkipFailuresThreadIntervalMinutes.value = String(fallbackThreadIntervalMinutes);
|
||||
|
||||
if (shouldOfferAutoModeChoice()) {
|
||||
const startStep = getFirstUnfinishedStep();
|
||||
const runningStep = getRunningSteps()[0] ?? null;
|
||||
const choice = await openAutoStartChoiceDialog(startStep, { runningStep });
|
||||
if (!choice) {
|
||||
return;
|
||||
}
|
||||
mode = choice;
|
||||
}
|
||||
|
||||
if (shouldWarnAutoRunFallbackRisk(totalRuns, autoRunSkipFailures)
|
||||
&& !isAutoRunFallbackRiskPromptDismissed()) {
|
||||
const result = await openAutoRunFallbackRiskConfirmModal(totalRuns, fallbackThreadIntervalMinutes);
|
||||
if (!result.confirmed) {
|
||||
return;
|
||||
}
|
||||
if (result.dismissPrompt) {
|
||||
setAutoRunFallbackRiskPromptDismissed(true);
|
||||
}
|
||||
}
|
||||
|
||||
btnAutoRun.disabled = true;
|
||||
inputRunCount.disabled = true;
|
||||
const delayEnabled = inputAutoDelayEnabled.checked;
|
||||
const delayMinutes = normalizeAutoDelayMinutes(inputAutoDelayMinutes.value);
|
||||
inputAutoDelayMinutes.value = String(delayMinutes);
|
||||
btnAutoRun.innerHTML = delayEnabled
|
||||
? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 计划中...'
|
||||
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> 运行中...';
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: delayEnabled ? 'SCHEDULE_AUTO_RUN' : 'AUTO_RUN',
|
||||
source: 'sidepanel',
|
||||
payload: {
|
||||
totalRuns,
|
||||
delayMinutes,
|
||||
autoRunSkipFailures,
|
||||
mode,
|
||||
},
|
||||
});
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
await startAutoRunFromCurrentSettings();
|
||||
} catch (err) {
|
||||
setDefaultAutoRunButton();
|
||||
inputRunCount.disabled = false;
|
||||
@@ -4654,4 +4714,7 @@ restoreState().then(() => {
|
||||
updatePanelModeUI();
|
||||
updateButtonStates();
|
||||
updateStatusDisplay(latestState);
|
||||
return refreshContributionContentHint();
|
||||
}).catch((err) => {
|
||||
console.error('Failed to initialize sidepanel state:', err);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user