mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 09:11:06 +00:00
chore: reduce complexity
This commit is contained in:
+94
-90
@@ -513,100 +513,104 @@ function isTrustedMessage(event) {
|
|||||||
return event.source === window && event.origin === window.location.origin;
|
return event.source === window && event.origin === window.location.origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function executeReactFiberAction(fiberInstance, action, payload) {
|
||||||
|
switch (action) {
|
||||||
|
case "getState":
|
||||||
|
return fiberInstance.getState(payload.key);
|
||||||
|
case "setState":
|
||||||
|
if (
|
||||||
|
payload.updateObject &&
|
||||||
|
typeof payload.updateObject === "object" &&
|
||||||
|
!Array.isArray(payload.updateObject)
|
||||||
|
) {
|
||||||
|
fiberInstance.setState(payload.updateObject);
|
||||||
|
} else {
|
||||||
|
console.warn("[pageState] setState rejected: only plain objects are allowed");
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
case "getProp":
|
||||||
|
return fiberInstance.getProp(payload.propName);
|
||||||
|
case "setProp":
|
||||||
|
fiberInstance.setProp(payload.propName, payload.value);
|
||||||
|
return {};
|
||||||
|
case "forceUpdate":
|
||||||
|
fiberInstance.forceUpdate();
|
||||||
|
return {};
|
||||||
|
default:
|
||||||
|
console.warn(`[pageState] Unknown action: ${action}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeResponseForPostMessage(response) {
|
||||||
|
if (response !== null && typeof response === "object") {
|
||||||
|
response = makeSerializable(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
deepFunctionCheck(response);
|
||||||
|
} catch (functionError) {
|
||||||
|
console.warn("[pageState] Function detected in response, cleaning:", functionError.message);
|
||||||
|
return `[Cleaned Response - Function found at: ${functionError.message}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof structuredClone === "function") {
|
||||||
|
structuredClone(response);
|
||||||
|
} else {
|
||||||
|
JSON.parse(JSON.stringify(response));
|
||||||
|
}
|
||||||
|
} catch (cloneError) {
|
||||||
|
console.warn("[pageState] Response not cloneable, fallback:", cloneError.message);
|
||||||
|
return `[Uncloneable Response: ${cloneError.message}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReactFiberRequest(data) {
|
||||||
|
const { selector, action, payload, debug, messageId } = data;
|
||||||
|
const fiberInstance = ReactFiber.find(selector, { debug });
|
||||||
|
const response = sanitizeResponseForPostMessage(
|
||||||
|
executeReactFiberAction(fiberInstance, action, payload),
|
||||||
|
);
|
||||||
|
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: "reactFiberResponse",
|
||||||
|
response,
|
||||||
|
messageId,
|
||||||
|
},
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTriggerKeyboardEvent(data) {
|
||||||
|
const { key, code, altKey, ctrlKey, metaKey, shiftKey, keyCode } = data;
|
||||||
|
const keyboardEvent = new KeyboardEvent("keydown", {
|
||||||
|
key,
|
||||||
|
code,
|
||||||
|
keyCode: keyCode || 0,
|
||||||
|
which: keyCode || 0,
|
||||||
|
altKey: altKey || false,
|
||||||
|
ctrlKey: ctrlKey || false,
|
||||||
|
metaKey: metaKey || false,
|
||||||
|
shiftKey: shiftKey || false,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
document.dispatchEvent(keyboardEvent);
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener("message", (event) => {
|
window.addEventListener("message", (event) => {
|
||||||
if (!isTrustedMessage(event)) return;
|
if (!isTrustedMessage(event)) return;
|
||||||
|
|
||||||
if (event.data.type === "reactFiberRequest") {
|
if (event.data.type === "reactFiberRequest") {
|
||||||
const { selector, action, payload, debug, messageId } = event.data;
|
handleReactFiberRequest(event.data);
|
||||||
const fiberInstance = ReactFiber.find(selector, {
|
return;
|
||||||
debug,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
let response;
|
if (event.data.type === "triggerKeyboardEvent") {
|
||||||
switch (action) {
|
handleTriggerKeyboardEvent(event.data);
|
||||||
case "getState":
|
|
||||||
response = fiberInstance.getState(payload.key);
|
|
||||||
break;
|
|
||||||
case "setState":
|
|
||||||
if (
|
|
||||||
payload.updateObject &&
|
|
||||||
typeof payload.updateObject === "object" &&
|
|
||||||
!Array.isArray(payload.updateObject)
|
|
||||||
) {
|
|
||||||
fiberInstance.setState(payload.updateObject);
|
|
||||||
} else {
|
|
||||||
console.warn("[pageState] setState rejected: only plain objects are allowed");
|
|
||||||
}
|
|
||||||
response = {};
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "getProp":
|
|
||||||
response = fiberInstance.getProp(payload.propName);
|
|
||||||
break;
|
|
||||||
case "setProp":
|
|
||||||
fiberInstance.setProp(payload.propName, payload.value);
|
|
||||||
response = {};
|
|
||||||
break;
|
|
||||||
case "forceUpdate":
|
|
||||||
fiberInstance.forceUpdate();
|
|
||||||
response = {};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn(`[pageState] Unknown action: ${action}`);
|
|
||||||
response = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response !== null && typeof response === "object") {
|
|
||||||
response = makeSerializable(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final safety check before postMessage
|
|
||||||
try {
|
|
||||||
deepFunctionCheck(response);
|
|
||||||
} catch (functionError) {
|
|
||||||
console.warn("[pageState] Function detected in response, cleaning:", functionError.message);
|
|
||||||
response = `[Cleaned Response - Function found at: ${functionError.message}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional structured clone test
|
|
||||||
try {
|
|
||||||
// Test if the object can be cloned (same algorithm as postMessage)
|
|
||||||
if (typeof structuredClone === 'function') {
|
|
||||||
structuredClone(response);
|
|
||||||
} else {
|
|
||||||
// Fallback for older browsers - try JSON round-trip
|
|
||||||
JSON.parse(JSON.stringify(response));
|
|
||||||
}
|
|
||||||
} catch (cloneError) {
|
|
||||||
console.warn("[pageState] Response not cloneable, fallback:", cloneError.message);
|
|
||||||
response = `[Uncloneable Response: ${cloneError.message}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.postMessage(
|
|
||||||
{
|
|
||||||
type: "reactFiberResponse",
|
|
||||||
response,
|
|
||||||
messageId,
|
|
||||||
},
|
|
||||||
window.location.origin,
|
|
||||||
);
|
|
||||||
} else if (event.data.type === "triggerKeyboardEvent") {
|
|
||||||
// Handle keyboard event triggering from content script
|
|
||||||
const { key, code, altKey, ctrlKey, metaKey, shiftKey, keyCode } = event.data;
|
|
||||||
|
|
||||||
const keyboardEvent = new KeyboardEvent('keydown', {
|
|
||||||
key,
|
|
||||||
code,
|
|
||||||
keyCode: keyCode || 0,
|
|
||||||
which: keyCode || 0,
|
|
||||||
altKey: altKey || false,
|
|
||||||
ctrlKey: ctrlKey || false,
|
|
||||||
metaKey: metaKey || false,
|
|
||||||
shiftKey: shiftKey || false,
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true
|
|
||||||
});
|
|
||||||
|
|
||||||
document.dispatchEvent(keyboardEvent);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1040,7 +1040,15 @@ function resolveTabSetClasses(): Record<string, string> {
|
|||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
|
interface WeightingTabContext {
|
||||||
|
assessmentID?: string;
|
||||||
|
autoWeight?: number;
|
||||||
|
override?: number | string;
|
||||||
|
weightingUnavailable: boolean;
|
||||||
|
statusNote: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveWeightingTabContext(api: any): Promise<WeightingTabContext> {
|
||||||
const selectedItem = document.querySelector(
|
const selectedItem = document.querySelector(
|
||||||
"[class*='AssessmentItem__AssessmentItem___'][class*='selected___']",
|
"[class*='AssessmentItem__AssessmentItem___'][class*='selected___']",
|
||||||
) as HTMLElement | null;
|
) as HTMLElement | null;
|
||||||
@@ -1056,14 +1064,11 @@ async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
|
|||||||
? (api.storage.weightings?.[assessmentID] as WeightingEntry | undefined)
|
? (api.storage.weightings?.[assessmentID] as WeightingEntry | undefined)
|
||||||
: undefined;
|
: undefined;
|
||||||
const rawWeight = entry?.weight;
|
const rawWeight = entry?.weight;
|
||||||
|
|
||||||
const weightingUnavailable = rawWeight === "N/A";
|
const weightingUnavailable = rawWeight === "N/A";
|
||||||
|
|
||||||
const autoWeight =
|
const autoWeight =
|
||||||
rawWeight && rawWeight !== "processing" && rawWeight !== "N/A"
|
rawWeight && rawWeight !== "processing" && rawWeight !== "N/A"
|
||||||
? rawWeight
|
? rawWeight
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const override = assessmentID
|
const override = assessmentID
|
||||||
? api.storage.weightingOverrides?.[assessmentID]
|
? api.storage.weightingOverrides?.[assessmentID]
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -1076,6 +1081,22 @@ async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
|
|||||||
? "No weighting was found in the marksheet. Set one manually."
|
? "No weighting was found in the marksheet. Set one manually."
|
||||||
: "Overrides the auto-detected value.";
|
: "Overrides the auto-detected value.";
|
||||||
|
|
||||||
|
return {
|
||||||
|
assessmentID,
|
||||||
|
autoWeight,
|
||||||
|
override,
|
||||||
|
weightingUnavailable,
|
||||||
|
statusNote,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWeightingTabHtml(
|
||||||
|
sheet: HTMLElement,
|
||||||
|
context: WeightingTabContext,
|
||||||
|
) {
|
||||||
|
const { assessmentID, autoWeight, override, weightingUnavailable, statusNote } =
|
||||||
|
context;
|
||||||
|
|
||||||
sheet.innerHTML = `
|
sheet.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
#betterseqta-weight-override::placeholder {
|
#betterseqta-weight-override::placeholder {
|
||||||
@@ -1120,9 +1141,13 @@ async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
|
|||||||
${!assessmentID ? `<p style="font-size:12px;color:rgba(255,80,80,0.8);margin-top:8px">Assessment not yet indexed — try refreshing.</p>` : ""}
|
${!assessmentID ? `<p style="font-size:12px;color:rgba(255,80,80,0.8);margin-top:8px">Assessment not yet indexed — try refreshing.</p>` : ""}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
if (!assessmentID) return;
|
function attachWeightingInputHandlers(
|
||||||
|
sheet: HTMLElement,
|
||||||
|
api: any,
|
||||||
|
assessmentID: string,
|
||||||
|
) {
|
||||||
const input = sheet.querySelector(
|
const input = sheet.querySelector(
|
||||||
"#betterseqta-weight-override",
|
"#betterseqta-weight-override",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
@@ -1132,20 +1157,17 @@ async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
|
|||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
const raw = input.value.trim();
|
const raw = input.value.trim();
|
||||||
if (raw === "") {
|
const result = saveWeightingOverride(api, assessmentID, raw);
|
||||||
const result = saveWeightingOverride(api, assessmentID, "");
|
if (!result.ok) {
|
||||||
if (!result.ok) return;
|
if (raw !== "") {
|
||||||
input.style.borderColor = "rgba(128,128,128,0.3)";
|
|
||||||
} else {
|
|
||||||
const result = saveWeightingOverride(api, assessmentID, raw);
|
|
||||||
if (!result.ok) {
|
|
||||||
input.style.borderColor = "rgba(255,80,80,0.6)";
|
input.style.borderColor = "rgba(255,80,80,0.6)";
|
||||||
statusEl.textContent = result.error ?? "Invalid. Must be 0 or greater";
|
statusEl.textContent = result.error ?? "Invalid. Must be 0 or greater";
|
||||||
statusEl.style.color = "rgba(255,80,80,0.8)";
|
statusEl.style.color = "rgba(255,80,80,0.8)";
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
input.style.borderColor = "rgba(128,128,128,0.3)";
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.style.borderColor = "rgba(128,128,128,0.3)";
|
||||||
statusEl.textContent = "Saved";
|
statusEl.textContent = "Saved";
|
||||||
statusEl.style.color = "";
|
statusEl.style.color = "";
|
||||||
setTimeout(() => (statusEl.textContent = ""), 2000);
|
setTimeout(() => (statusEl.textContent = ""), 2000);
|
||||||
@@ -1165,6 +1187,13 @@ async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
|
||||||
|
const context = await resolveWeightingTabContext(api);
|
||||||
|
renderWeightingTabHtml(sheet, context);
|
||||||
|
if (!context.assessmentID) return;
|
||||||
|
attachWeightingInputHandlers(sheet, api, context.assessmentID);
|
||||||
|
}
|
||||||
|
|
||||||
export function injectWeightingsTab(api: any) {
|
export function injectWeightingsTab(api: any) {
|
||||||
const tabList = document.querySelector(
|
const tabList = document.querySelector(
|
||||||
'[class*="TabSet__tabs___"]',
|
'[class*="TabSet__tabs___"]',
|
||||||
|
|||||||
@@ -128,102 +128,9 @@ export async function mountSearchBar(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateProgressDisplay = () => {
|
const resetIdleProgressUi = () => {
|
||||||
const indexingStoppedThisTick = indexingJustStoppedFlag;
|
|
||||||
indexingJustStoppedFlag = false;
|
|
||||||
|
|
||||||
const active = isIndexing && totalJobs > 0;
|
|
||||||
|
|
||||||
// Stray pulses (missing total, 0 completed, etc.) used to hit the idle
|
|
||||||
// branch and call clearDoneFlashTimer(), killing the Done! hold/fade.
|
|
||||||
if (doneFlashTimer !== null || doneFadeTimer !== null) {
|
|
||||||
if (!active) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearDoneFlashTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
const completionEligible =
|
|
||||||
ranIndexingCycle &&
|
|
||||||
!active &&
|
|
||||||
totalJobs > 0 &&
|
|
||||||
(completedJobs >= totalJobs || indexingStoppedThisTick);
|
|
||||||
|
|
||||||
if (active) {
|
|
||||||
clearDoneFlashTimer();
|
|
||||||
progressBarWrapper.classList.remove("is-rough-complete");
|
|
||||||
progressText.classList.remove(
|
|
||||||
"is-rough",
|
|
||||||
"is-fading-done",
|
|
||||||
"is-done-message",
|
|
||||||
);
|
|
||||||
const percentage = Math.round((completedJobs / totalJobs) * 100);
|
|
||||||
progressBar.style.width = `${Math.max(2, percentage)}%`;
|
|
||||||
progressBarWrapper.classList.add("is-active");
|
|
||||||
searchAnchor.classList.add("is-indexing");
|
|
||||||
searchButton.classList.add("is-indexing");
|
|
||||||
|
|
||||||
if (indexingStatus) {
|
|
||||||
progressText.textContent = `${truncateStatus(indexingStatus)} · ${percentage}%`;
|
|
||||||
} else {
|
|
||||||
progressText.textContent = `Indexing ${completedJobs}/${totalJobs} (${percentage}%)`;
|
|
||||||
}
|
|
||||||
progressText.classList.add("is-active");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (completionEligible) {
|
|
||||||
// Duplicate end-of-run ticks must not reschedule hold/fade timers
|
|
||||||
if (doneFlashTimer !== null || doneFadeTimer !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rough =
|
|
||||||
indexingStatus != null && statusLooksRough(indexingStatus);
|
|
||||||
|
|
||||||
progressBar.style.width = "0%";
|
|
||||||
progressBarWrapper.classList.remove("is-active");
|
|
||||||
searchAnchor.classList.remove("is-indexing");
|
|
||||||
searchButton.classList.remove("is-indexing");
|
|
||||||
progressText.classList.remove("is-fading-done");
|
|
||||||
|
|
||||||
progressText.textContent = rough ? truncateStatus(indexingStatus!, 52) : "Done!";
|
|
||||||
if (rough) {
|
|
||||||
progressText.classList.add("is-rough");
|
|
||||||
progressBarWrapper.classList.add("is-rough-complete");
|
|
||||||
} else {
|
|
||||||
progressText.classList.remove("is-rough");
|
|
||||||
progressBarWrapper.classList.remove("is-rough-complete");
|
|
||||||
}
|
|
||||||
progressText.classList.add("is-active", "is-done-message");
|
|
||||||
|
|
||||||
doneFlashTimer = setTimeout(() => {
|
|
||||||
doneFlashTimer = null;
|
|
||||||
progressText.classList.add("is-fading-done");
|
|
||||||
doneFadeTimer = setTimeout(() => {
|
|
||||||
doneFadeTimer = null;
|
|
||||||
ranIndexingCycle = false;
|
|
||||||
indexingStatus = null;
|
|
||||||
progressBar.style.width = "0%";
|
|
||||||
progressBarWrapper.classList.remove("is-active");
|
|
||||||
progressBarWrapper.classList.remove("is-rough-complete");
|
|
||||||
searchAnchor.classList.remove("is-indexing");
|
|
||||||
searchButton.classList.remove("is-indexing");
|
|
||||||
progressText.classList.remove(
|
|
||||||
"is-active",
|
|
||||||
"is-rough",
|
|
||||||
"is-fading-done",
|
|
||||||
"is-done-message",
|
|
||||||
);
|
|
||||||
progressText.textContent = "";
|
|
||||||
}, DONE_FADE_MS);
|
|
||||||
}, DONE_HOLD_MS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearDoneFlashTimer();
|
clearDoneFlashTimer();
|
||||||
progressBarWrapper.classList.remove("is-active");
|
progressBarWrapper.classList.remove("is-active", "is-rough-complete");
|
||||||
progressBarWrapper.classList.remove("is-rough-complete");
|
|
||||||
searchAnchor.classList.remove("is-indexing");
|
searchAnchor.classList.remove("is-indexing");
|
||||||
searchButton.classList.remove("is-indexing");
|
searchButton.classList.remove("is-indexing");
|
||||||
progressText.classList.remove(
|
progressText.classList.remove(
|
||||||
@@ -238,6 +145,75 @@ export async function mountSearchBar(
|
|||||||
indexingStatus = null;
|
indexingStatus = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showActiveIndexingUi = (percentage: number) => {
|
||||||
|
clearDoneFlashTimer();
|
||||||
|
progressBarWrapper.classList.remove("is-rough-complete");
|
||||||
|
progressText.classList.remove("is-rough", "is-fading-done", "is-done-message");
|
||||||
|
progressBar.style.width = `${Math.max(2, percentage)}%`;
|
||||||
|
progressBarWrapper.classList.add("is-active");
|
||||||
|
searchAnchor.classList.add("is-indexing");
|
||||||
|
searchButton.classList.add("is-indexing");
|
||||||
|
progressText.textContent = indexingStatus
|
||||||
|
? `${truncateStatus(indexingStatus)} · ${percentage}%`
|
||||||
|
: `Indexing ${completedJobs}/${totalJobs} (${percentage}%)`;
|
||||||
|
progressText.classList.add("is-active");
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleCompletionFlash = (rough: boolean) => {
|
||||||
|
progressBar.style.width = "0%";
|
||||||
|
progressBarWrapper.classList.remove("is-active");
|
||||||
|
searchAnchor.classList.remove("is-indexing");
|
||||||
|
searchButton.classList.remove("is-indexing");
|
||||||
|
progressText.classList.remove("is-fading-done");
|
||||||
|
progressText.textContent = rough ? truncateStatus(indexingStatus!, 52) : "Done!";
|
||||||
|
progressText.classList.toggle("is-rough", rough);
|
||||||
|
progressBarWrapper.classList.toggle("is-rough-complete", rough);
|
||||||
|
progressText.classList.add("is-active", "is-done-message");
|
||||||
|
|
||||||
|
doneFlashTimer = setTimeout(() => {
|
||||||
|
doneFlashTimer = null;
|
||||||
|
progressText.classList.add("is-fading-done");
|
||||||
|
doneFadeTimer = setTimeout(() => {
|
||||||
|
doneFadeTimer = null;
|
||||||
|
resetIdleProgressUi();
|
||||||
|
}, DONE_FADE_MS);
|
||||||
|
}, DONE_HOLD_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProgressDisplay = () => {
|
||||||
|
const indexingStoppedThisTick = indexingJustStoppedFlag;
|
||||||
|
indexingJustStoppedFlag = false;
|
||||||
|
|
||||||
|
const active = isIndexing && totalJobs > 0;
|
||||||
|
|
||||||
|
// Stray pulses (missing total, 0 completed, etc.) used to hit the idle
|
||||||
|
// branch and call clearDoneFlashTimer(), killing the Done! hold/fade.
|
||||||
|
if (doneFlashTimer !== null || doneFadeTimer !== null) {
|
||||||
|
if (!active) return;
|
||||||
|
clearDoneFlashTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
const completionEligible =
|
||||||
|
ranIndexingCycle &&
|
||||||
|
!active &&
|
||||||
|
totalJobs > 0 &&
|
||||||
|
(completedJobs >= totalJobs || indexingStoppedThisTick);
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
showActiveIndexingUi(Math.round((completedJobs / totalJobs) * 100));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completionEligible) {
|
||||||
|
if (doneFlashTimer !== null || doneFadeTimer !== null) return;
|
||||||
|
const rough = indexingStatus != null && statusLooksRough(indexingStatus);
|
||||||
|
scheduleCompletionFlash(rough);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetIdleProgressUi();
|
||||||
|
};
|
||||||
|
|
||||||
// Listen for indexing progress events
|
// Listen for indexing progress events
|
||||||
const progressHandler = (event: CustomEvent) => {
|
const progressHandler = (event: CustomEvent) => {
|
||||||
const { completed, total, indexing, status } = event.detail as {
|
const { completed, total, indexing, status } = event.detail as {
|
||||||
|
|||||||
@@ -105,6 +105,78 @@ function appendFolderBadgeContent(badge: HTMLElement, folder: Folder): void {
|
|||||||
badge.appendChild(document.createTextNode(folder.name));
|
badge.appendChild(document.createTextNode(folder.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MESSAGE_LIST_ITEM_SELECTOR =
|
||||||
|
"[class*='MessageList__MessageList___'] ol > li[data-message]";
|
||||||
|
|
||||||
|
function getMessageListItems(): NodeListOf<Element> {
|
||||||
|
return document.querySelectorAll(MESSAGE_LIST_ITEM_SELECTOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMessageListBadges(
|
||||||
|
messageItems: NodeListOf<Element>,
|
||||||
|
restoreSubjectPlain: (subject: Element) => void,
|
||||||
|
): void {
|
||||||
|
for (const li of messageItems) {
|
||||||
|
const subject = li.querySelector("[class*='MessageList__subject___']");
|
||||||
|
if (
|
||||||
|
subject &&
|
||||||
|
(subject.querySelector(".bsplus-msg-badges") ||
|
||||||
|
subject.querySelector(".bsplus-subject-text"))
|
||||||
|
) {
|
||||||
|
restoreSubjectPlain(subject);
|
||||||
|
} else {
|
||||||
|
li.querySelector(".bsplus-msg-badges")?.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAssignedFolderIds(
|
||||||
|
msgId: string,
|
||||||
|
assignments: Record<string, string[]>,
|
||||||
|
): string[] {
|
||||||
|
return Object.entries(assignments)
|
||||||
|
.filter(([, messageIds]) => messageIds.includes(msgId))
|
||||||
|
.map(([folderId]) => folderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureMessageBadgeContainer(li: Element): HTMLElement {
|
||||||
|
const existing = li.querySelector(".bsplus-msg-badges") as HTMLElement | null;
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
const badgeContainer = document.createElement("div");
|
||||||
|
badgeContainer.className = "bsplus-msg-badges";
|
||||||
|
const subject = li.querySelector("[class*='MessageList__subject___']");
|
||||||
|
if (subject) {
|
||||||
|
if (!subject.querySelector(".bsplus-subject-text")) {
|
||||||
|
const textWrap = document.createElement("span");
|
||||||
|
textWrap.className = "bsplus-subject-text";
|
||||||
|
textWrap.textContent = subject.textContent;
|
||||||
|
subject.textContent = "";
|
||||||
|
subject.appendChild(textWrap);
|
||||||
|
}
|
||||||
|
subject.appendChild(badgeContainer);
|
||||||
|
} else {
|
||||||
|
li.appendChild(badgeContainer);
|
||||||
|
}
|
||||||
|
return badgeContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFolderBadge(
|
||||||
|
folder: Folder,
|
||||||
|
onFilter: (folderId: string) => void,
|
||||||
|
): HTMLElement {
|
||||||
|
const badge = document.createElement("span");
|
||||||
|
badge.className = "bsplus-msg-badge";
|
||||||
|
badge.style.background = folder.color;
|
||||||
|
appendFolderBadgeContent(badge, folder);
|
||||||
|
badge.title = `Filter by "${folder.name}"`;
|
||||||
|
badge.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onFilter(folder.id);
|
||||||
|
});
|
||||||
|
return badge;
|
||||||
|
}
|
||||||
|
|
||||||
const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFoldersStorage> = {
|
const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFoldersStorage> = {
|
||||||
id: "messageFolders",
|
id: "messageFolders",
|
||||||
name: "Message Folders",
|
name: "Message Folders",
|
||||||
@@ -799,66 +871,37 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
|
|||||||
};
|
};
|
||||||
|
|
||||||
const applyBadges = () => {
|
const applyBadges = () => {
|
||||||
const messageItems = document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]");
|
const messageItems = getMessageListItems();
|
||||||
if (!shouldShowBadgesInList()) {
|
if (!shouldShowBadgesInList()) {
|
||||||
for (const li of messageItems) {
|
clearMessageListBadges(messageItems, restoreSubjectPlain);
|
||||||
const subject = li.querySelector("[class*='MessageList__subject___']");
|
|
||||||
if (subject && (subject.querySelector(".bsplus-msg-badges") || subject.querySelector(".bsplus-subject-text"))) {
|
|
||||||
restoreSubjectPlain(subject);
|
|
||||||
} else {
|
|
||||||
li.querySelector(".bsplus-msg-badges")?.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const folders = getFolders();
|
const folders = getFolders();
|
||||||
const assignments = getAssignments();
|
const assignments = getAssignments();
|
||||||
|
const selectFolder = (folderId: string) => {
|
||||||
|
activeFolderId = folderId;
|
||||||
|
applyFolderFilter();
|
||||||
|
applyBadges();
|
||||||
|
renderSidebarFolders();
|
||||||
|
};
|
||||||
|
|
||||||
for (const li of messageItems) {
|
for (const li of messageItems) {
|
||||||
const msgId = li.getAttribute("data-message");
|
const msgId = li.getAttribute("data-message");
|
||||||
if (!msgId) continue;
|
if (!msgId) continue;
|
||||||
let badgeContainer = li.querySelector(".bsplus-msg-badges") as HTMLElement | null;
|
|
||||||
const folderIds: string[] = [];
|
const folderIds = getAssignedFolderIds(msgId, assignments);
|
||||||
for (const [fId, mIds] of Object.entries(assignments)) {
|
|
||||||
if (mIds.includes(msgId)) folderIds.push(fId);
|
|
||||||
}
|
|
||||||
if (folderIds.length === 0) {
|
if (folderIds.length === 0) {
|
||||||
badgeContainer?.remove();
|
li.querySelector(".bsplus-msg-badges")?.remove();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!badgeContainer) {
|
|
||||||
badgeContainer = document.createElement("div");
|
const badgeContainer = ensureMessageBadgeContainer(li);
|
||||||
badgeContainer.className = "bsplus-msg-badges";
|
badgeContainer.replaceChildren();
|
||||||
const subject = li.querySelector("[class*='MessageList__subject___']");
|
for (const folderId of folderIds) {
|
||||||
if (subject) {
|
const folder = folders.find((f) => f.id === folderId);
|
||||||
if (!subject.querySelector(".bsplus-subject-text")) {
|
|
||||||
const textWrap = document.createElement("span");
|
|
||||||
textWrap.className = "bsplus-subject-text";
|
|
||||||
textWrap.textContent = subject.textContent;
|
|
||||||
subject.textContent = "";
|
|
||||||
subject.appendChild(textWrap);
|
|
||||||
}
|
|
||||||
subject.appendChild(badgeContainer);
|
|
||||||
} else {
|
|
||||||
li.appendChild(badgeContainer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
badgeContainer.innerHTML = "";
|
|
||||||
for (const fId of folderIds) {
|
|
||||||
const folder = folders.find((f) => f.id === fId);
|
|
||||||
if (!folder) continue;
|
if (!folder) continue;
|
||||||
const badge = document.createElement("span");
|
badgeContainer.appendChild(createFolderBadge(folder, selectFolder));
|
||||||
badge.className = "bsplus-msg-badge";
|
|
||||||
badge.style.background = folder.color;
|
|
||||||
appendFolderBadgeContent(badge, folder);
|
|
||||||
badge.title = `Filter by "${folder.name}"`;
|
|
||||||
badge.addEventListener("click", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
activeFolderId = folder.id;
|
|
||||||
applyFolderFilter();
|
|
||||||
applyBadges();
|
|
||||||
renderSidebarFolders();
|
|
||||||
});
|
|
||||||
badgeContainer.appendChild(badge);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,216 +14,215 @@ function escapeHtmlAttr(value: string): string {
|
|||||||
.replace(/>/g, ">");
|
.replace(/>/g, ">");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OpenMenuOptions() {
|
function syncDefaultMenuOrder(menu: HTMLElement) {
|
||||||
if (MenuOptionsOpen) return;
|
const childnodes = menu.firstChild!.childNodes;
|
||||||
|
if (settingsState.defaultmenuorder.length === 0) {
|
||||||
var container = document.getElementById("container");
|
settingsState.defaultmenuorder = Array.from(childnodes).map(
|
||||||
var menu = document.getElementById("menu");
|
(element) => (element as HTMLElement).dataset.key,
|
||||||
|
);
|
||||||
if (settingsState.defaultmenuorder.length == 0) {
|
return;
|
||||||
let childnodes = menu!.firstChild!.childNodes;
|
|
||||||
let newdefaultmenuorder = [];
|
|
||||||
for (let i = 0; i < childnodes.length; i++) {
|
|
||||||
const element = childnodes[i];
|
|
||||||
newdefaultmenuorder.push((element as HTMLElement).dataset.key);
|
|
||||||
settingsState.defaultmenuorder = newdefaultmenuorder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let childnodes = menu!.firstChild!.childNodes;
|
|
||||||
if (settingsState.defaultmenuorder.length != childnodes.length) {
|
|
||||||
for (let i = 0; i < childnodes.length; i++) {
|
|
||||||
const element = childnodes[i];
|
|
||||||
if (
|
|
||||||
settingsState.defaultmenuorder.indexOf(
|
|
||||||
(element as HTMLElement).dataset.key,
|
|
||||||
) === -1
|
|
||||||
) {
|
|
||||||
let newdefaultmenuorder = settingsState.defaultmenuorder;
|
|
||||||
newdefaultmenuorder.push((element as HTMLElement).dataset.key);
|
|
||||||
settingsState.defaultmenuorder = newdefaultmenuorder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuOptionsOpen = true;
|
if (settingsState.defaultmenuorder.length === childnodes.length) return;
|
||||||
|
|
||||||
var cover = document.createElement("div");
|
const nextOrder = [...settingsState.defaultmenuorder];
|
||||||
|
for (let i = 0; i < childnodes.length; i++) {
|
||||||
|
const key = (childnodes[i] as HTMLElement).dataset.key;
|
||||||
|
if (key && nextOrder.indexOf(key) === -1) nextOrder.push(key);
|
||||||
|
}
|
||||||
|
settingsState.defaultmenuorder = nextOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMenuEditControls(menu: HTMLElement, container: HTMLElement) {
|
||||||
|
const cover = document.createElement("div");
|
||||||
cover.classList.add("notMenuCover");
|
cover.classList.add("notMenuCover");
|
||||||
menu!.style.zIndex = "20";
|
menu.style.zIndex = "20";
|
||||||
menu!.style.setProperty("--menuHidden", "flex");
|
menu.style.setProperty("--menuHidden", "flex");
|
||||||
container!.append(cover);
|
container.append(cover);
|
||||||
|
|
||||||
var menusettings = document.createElement("div");
|
const menusettings = document.createElement("div");
|
||||||
menusettings.classList.add("editmenuoption-container");
|
menusettings.classList.add("editmenuoption-container");
|
||||||
|
|
||||||
var defaultbutton = document.createElement("div");
|
const defaultbutton = document.createElement("div");
|
||||||
defaultbutton.classList.add("editmenuoption");
|
defaultbutton.classList.add("editmenuoption");
|
||||||
defaultbutton.innerText = "Restore Default";
|
defaultbutton.innerText = "Restore Default";
|
||||||
defaultbutton.id = "restoredefaultoption";
|
defaultbutton.id = "restoredefaultoption";
|
||||||
|
|
||||||
var savebutton = document.createElement("div");
|
const savebutton = document.createElement("div");
|
||||||
savebutton.classList.add("editmenuoption");
|
savebutton.classList.add("editmenuoption");
|
||||||
savebutton.innerText = "Save";
|
savebutton.innerText = "Save";
|
||||||
savebutton.id = "savemenuoption";
|
savebutton.id = "savemenuoption";
|
||||||
|
|
||||||
menusettings.appendChild(defaultbutton);
|
menusettings.appendChild(defaultbutton);
|
||||||
menusettings.appendChild(savebutton);
|
menusettings.appendChild(savebutton);
|
||||||
|
menu.appendChild(menusettings);
|
||||||
|
|
||||||
menu!.appendChild(menusettings);
|
return { cover, menusettings, defaultbutton, savebutton };
|
||||||
|
}
|
||||||
|
|
||||||
var ListItems = menu!.firstChild!.childNodes;
|
function prepareMenuItemsForEdit(menu: HTMLElement) {
|
||||||
for (let i = 0; i < ListItems.length; i++) {
|
const listItems = menu.firstChild!.childNodes;
|
||||||
const element1 = ListItems[i];
|
for (let i = 0; i < listItems.length; i++) {
|
||||||
const element = element1 as HTMLElement;
|
const element = listItems[i] as HTMLElement;
|
||||||
|
element.classList.add("draggable");
|
||||||
(element as HTMLElement).classList.add("draggable");
|
if (element.classList.contains("hasChildren")) {
|
||||||
if ((element as HTMLElement).classList.contains("hasChildren")) {
|
element.classList.remove("active");
|
||||||
(element as HTMLElement).classList.remove("active");
|
|
||||||
(element.firstChild as HTMLElement).classList.remove("noscroll");
|
(element.firstChild as HTMLElement).classList.remove("noscroll");
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuKey = escapeHtmlAttr((element as HTMLElement).dataset.key ?? "");
|
const menuKey = escapeHtmlAttr(element.dataset.key ?? "");
|
||||||
let MenuItemToggle = stringToHTML(
|
const menuItemToggle = stringToHTML(
|
||||||
`<div class="onoffswitch" style="margin: auto 0;"><input class="onoffswitch-checkbox notification menuitem" type="checkbox" id="${menuKey}"><label for="${menuKey}" class="onoffswitch-label"></label>`,
|
`<div class="onoffswitch" style="margin: auto 0;"><input class="onoffswitch-checkbox notification menuitem" type="checkbox" id="${menuKey}"><label for="${menuKey}" class="onoffswitch-label"></label>`,
|
||||||
).firstChild;
|
).firstChild;
|
||||||
(element as HTMLElement).append(MenuItemToggle!);
|
element.append(menuItemToggle!);
|
||||||
|
|
||||||
if (!element.dataset.betterseqta) {
|
if (element.dataset.betterseqta) continue;
|
||||||
const a = document.createElement("section");
|
|
||||||
cloneAttributes(a, element);
|
const section = document.createElement("section");
|
||||||
while (element.firstChild) {
|
cloneAttributes(section, element);
|
||||||
a.appendChild(element.firstChild);
|
while (element.firstChild) section.appendChild(element.firstChild);
|
||||||
}
|
menu.firstChild!.insertBefore(section, element);
|
||||||
menu!.firstChild!.insertBefore(a, element);
|
element.remove();
|
||||||
element.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return listItems;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(settingsState.menuitems).length == 0) {
|
function ensureMenuItemSettings(menu: HTMLElement) {
|
||||||
menubuttons = menu!.firstChild!.childNodes;
|
if (Object.keys(settingsState.menuitems).length > 0) return;
|
||||||
let menuItems = {} as any;
|
|
||||||
for (var i = 0; i < menubuttons.length; i++) {
|
const menuItems: Record<string, { toggle: boolean }> = {};
|
||||||
var id = (menubuttons[i] as HTMLElement).dataset.key;
|
const menuButtons = menu.firstChild!.childNodes;
|
||||||
const element: any = {};
|
for (let i = 0; i < menuButtons.length; i++) {
|
||||||
element.toggle = true;
|
const id = (menuButtons[i] as HTMLElement).dataset.key;
|
||||||
(menuItems[id as keyof typeof menuItems] as any) = element;
|
if (id) menuItems[id] = { toggle: true };
|
||||||
}
|
|
||||||
settingsState.menuitems = menuItems;
|
|
||||||
}
|
}
|
||||||
|
settingsState.menuitems = menuItems;
|
||||||
|
}
|
||||||
|
|
||||||
var menubuttons: any = document.getElementsByClassName("menuitem");
|
function applySavedMenuToggleStates() {
|
||||||
|
const menuItems = settingsState.menuitems as Record<
|
||||||
let menuItems = settingsState.menuitems as any;
|
string,
|
||||||
let buttons = document.getElementsByClassName("menuitem");
|
{ toggle: boolean }
|
||||||
|
>;
|
||||||
|
const buttons = document.getElementsByClassName("menuitem");
|
||||||
for (let i = 0; i < buttons.length; i++) {
|
for (let i = 0; i < buttons.length; i++) {
|
||||||
let id = buttons[i].id as string | undefined;
|
const id = buttons[i].id;
|
||||||
if (menuItems[id as keyof typeof menuItems]) {
|
(buttons[i] as HTMLInputElement).checked = id
|
||||||
(buttons[i] as HTMLInputElement).checked =
|
? (menuItems[id]?.toggle ?? true)
|
||||||
menuItems[id as keyof typeof menuItems].toggle;
|
: true;
|
||||||
} else {
|
|
||||||
(buttons[i] as HTMLInputElement).checked = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let sortable: Sortable | undefined;
|
function createMenuSortable(): Sortable | undefined {
|
||||||
try {
|
try {
|
||||||
var el = document.querySelector("#menu > ul");
|
const el = document.querySelector("#menu > ul");
|
||||||
|
if (!el) return undefined;
|
||||||
|
let sortable: Sortable | undefined;
|
||||||
sortable = Sortable.create(el as HTMLElement, {
|
sortable = Sortable.create(el as HTMLElement, {
|
||||||
draggable: ".draggable",
|
draggable: ".draggable",
|
||||||
dataIdAttr: "data-key",
|
dataIdAttr: "data-key",
|
||||||
animation: 150,
|
animation: 150,
|
||||||
easing: "cubic-bezier(.5,0,.5,1)",
|
easing: "cubic-bezier(.5,0,.5,1)",
|
||||||
onEnd: function () {
|
onEnd: () => saveNewOrder(sortable),
|
||||||
saveNewOrder(sortable);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
return sortable;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeMenuItemDisplay(element: HTMLInputElement) {
|
||||||
|
const row = element.parentNode?.parentNode as HTMLElement | null;
|
||||||
|
if (!row) return;
|
||||||
|
if (!element.checked) {
|
||||||
|
row.style.display = "var(--menuHidden)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row.style.setProperty("display", "flex", "important");
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeMenuSettings() {
|
||||||
|
const menu = document.getElementById("menu");
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
const menuItems: Record<string, { toggle: boolean }> = {};
|
||||||
|
const menuButtons = menu.firstChild!.childNodes;
|
||||||
|
const toggles = document.getElementsByClassName("menuitem");
|
||||||
|
for (let i = 0; i < menuButtons.length; i++) {
|
||||||
|
const id = (menuButtons[i] as HTMLElement).dataset.key;
|
||||||
|
if (!id) continue;
|
||||||
|
menuItems[id] = { toggle: (toggles[i] as HTMLInputElement).checked };
|
||||||
|
}
|
||||||
|
settingsState.menuitems = menuItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreMenuItemsFromEditMode(menu: HTMLElement, listItems: NodeListOf<ChildNode>) {
|
||||||
|
for (let i = 0; i < listItems.length; i++) {
|
||||||
|
const element = listItems[i] as HTMLElement;
|
||||||
|
element.classList.remove("draggable");
|
||||||
|
element.setAttribute("draggable", "false");
|
||||||
|
|
||||||
|
if (element.dataset.betterseqta) continue;
|
||||||
|
|
||||||
|
const listItem = document.createElement("li");
|
||||||
|
cloneAttributes(listItem, element);
|
||||||
|
while (element.firstChild) listItem.appendChild(element.firstChild);
|
||||||
|
menu.firstChild!.insertBefore(listItem, element);
|
||||||
|
element.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeDisplayProperty(element: any) {
|
menu.querySelectorAll(".onoffswitch").forEach((toggle) => toggle.remove());
|
||||||
if (!element.checked) {
|
}
|
||||||
element.parentNode.parentNode.style.display = "var(--menuHidden)";
|
|
||||||
}
|
|
||||||
if (element.checked) {
|
|
||||||
element.parentNode.parentNode.style.setProperty(
|
|
||||||
"display",
|
|
||||||
"flex",
|
|
||||||
"important",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function StoreMenuSettings() {
|
export function OpenMenuOptions() {
|
||||||
let menu = document.getElementById("menu");
|
if (MenuOptionsOpen) return;
|
||||||
const menuItems: any = {};
|
|
||||||
let menubuttons = menu!.firstChild!.childNodes;
|
|
||||||
const button = document.getElementsByClassName("menuitem");
|
|
||||||
for (let i = 0; i < menubuttons.length; i++) {
|
|
||||||
const id = (menubuttons[i] as HTMLElement).dataset.key;
|
|
||||||
const element: any = {};
|
|
||||||
element.toggle = (button[i] as HTMLInputElement).checked;
|
|
||||||
|
|
||||||
menuItems[id as keyof typeof menuItems] = element;
|
const container = document.getElementById("container");
|
||||||
}
|
const menu = document.getElementById("menu");
|
||||||
settingsState.menuitems = menuItems;
|
if (!container || !menu) return;
|
||||||
}
|
|
||||||
|
syncDefaultMenuOrder(menu);
|
||||||
|
MenuOptionsOpen = true;
|
||||||
|
|
||||||
|
const { cover, menusettings, defaultbutton, savebutton } = createMenuEditControls(
|
||||||
|
menu,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
const listItems = prepareMenuItemsForEdit(menu);
|
||||||
|
ensureMenuItemSettings(menu);
|
||||||
|
applySavedMenuToggleStates();
|
||||||
|
|
||||||
|
const menubuttons = document.getElementsByClassName("menuitem");
|
||||||
|
const sortable = createMenuSortable();
|
||||||
|
|
||||||
for (let i = 0; i < menubuttons.length; i++) {
|
for (let i = 0; i < menubuttons.length; i++) {
|
||||||
const element = menubuttons[i];
|
const element = menubuttons[i] as HTMLInputElement;
|
||||||
element.addEventListener("change", () => {
|
element.addEventListener("change", () => {
|
||||||
element.parentElement.parentElement.getAttribute("data-key");
|
storeMenuSettings();
|
||||||
StoreMenuSettings();
|
changeMenuItemDisplay(element);
|
||||||
changeDisplayProperty(element);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAll() {
|
const closeAll = () => {
|
||||||
menusettings?.remove();
|
menusettings.remove();
|
||||||
cover?.remove();
|
cover.remove();
|
||||||
MenuOptionsOpen = false;
|
MenuOptionsOpen = false;
|
||||||
menu!.style.setProperty("--menuHidden", "none");
|
menu.style.setProperty("--menuHidden", "none");
|
||||||
|
restoreMenuItemsFromEditMode(menu, listItems);
|
||||||
|
};
|
||||||
|
|
||||||
for (let i = 0; i < ListItems.length; i++) {
|
cover.addEventListener("click", closeAll);
|
||||||
const element1 = ListItems[i];
|
savebutton.addEventListener("click", closeAll);
|
||||||
const element = element1 as HTMLElement;
|
|
||||||
element.classList.remove("draggable");
|
|
||||||
element.setAttribute("draggable", "false");
|
|
||||||
|
|
||||||
if (!element.dataset.betterseqta) {
|
defaultbutton.addEventListener("click", () => {
|
||||||
const a = document.createElement("li");
|
settingsState.menuorder = settingsState.defaultmenuorder;
|
||||||
cloneAttributes(a, element);
|
ChangeMenuItemPositions(settingsState.defaultmenuorder);
|
||||||
while (element.firstChild) {
|
|
||||||
a.appendChild(element.firstChild);
|
|
||||||
}
|
|
||||||
menu!.firstChild!.insertBefore(a, element);
|
|
||||||
element.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let switches = menu!.querySelectorAll(".onoffswitch");
|
|
||||||
for (let i = 0; i < switches.length; i++) {
|
|
||||||
switches[i].remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cover?.addEventListener("click", closeAll);
|
|
||||||
savebutton?.addEventListener("click", closeAll);
|
|
||||||
|
|
||||||
defaultbutton?.addEventListener("click", function () {
|
|
||||||
const options = settingsState.defaultmenuorder;
|
|
||||||
settingsState.menuorder = options;
|
|
||||||
|
|
||||||
ChangeMenuItemPositions(options);
|
|
||||||
|
|
||||||
for (let i = 0; i < menubuttons.length; i++) {
|
for (let i = 0; i < menubuttons.length; i++) {
|
||||||
const element = menubuttons[i];
|
const element = menubuttons[i] as HTMLInputElement;
|
||||||
element.checked = true;
|
element.checked = true;
|
||||||
element.parentNode.parentNode.style.setProperty(
|
changeMenuItemDisplay(element);
|
||||||
"display",
|
|
||||||
"flex",
|
|
||||||
"important",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (sortable) saveNewOrder(sortable);
|
if (sortable) saveNewOrder(sortable);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,28 +5,42 @@ const ALLOWED_HOST_SUFFIXES = [
|
|||||||
"github.com",
|
"github.com",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
function isPrivateOrLocalHost(hostname: string): boolean {
|
function normalizeHost(hostname: string): string {
|
||||||
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
return hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
||||||
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
}
|
||||||
if (host === "127.0.0.1" || host.startsWith("127.")) return true;
|
|
||||||
if (host === "::1" || host === "0.0.0.0") return true;
|
|
||||||
|
|
||||||
|
function isNamedLocalHost(host: string): boolean {
|
||||||
|
return (
|
||||||
|
host === "localhost" ||
|
||||||
|
host.endsWith(".localhost") ||
|
||||||
|
host === "127.0.0.1" ||
|
||||||
|
host.startsWith("127.") ||
|
||||||
|
host === "::1" ||
|
||||||
|
host === "0.0.0.0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrivateIpv4(host: string): boolean {
|
||||||
const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||||
if (ipv4) {
|
if (!ipv4) return false;
|
||||||
const [a, b] = [Number(ipv4[1]), Number(ipv4[2])];
|
|
||||||
if (a === 10) return true;
|
|
||||||
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
||||||
if (a === 192 && b === 168) return true;
|
|
||||||
if (a === 169 && b === 254) return true;
|
|
||||||
if (a === 127 || a === 0) return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (host.includes(":")) {
|
const [a, b] = [Number(ipv4[1]), Number(ipv4[2])];
|
||||||
const h = host.split("%")[0];
|
if (a === 10) return true;
|
||||||
if (h.startsWith("fe80") || h.startsWith("fc") || h.startsWith("fd")) return true;
|
if (a === 172 && b >= 16 && b <= 31) return true;
|
||||||
}
|
if (a === 192 && b === 168) return true;
|
||||||
|
if (a === 169 && b === 254) return true;
|
||||||
|
return a === 127 || a === 0;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
function isPrivateIpv6(host: string): boolean {
|
||||||
|
if (!host.includes(":")) return false;
|
||||||
|
const h = host.split("%")[0];
|
||||||
|
return h.startsWith("fe80") || h.startsWith("fc") || h.startsWith("fd");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrivateOrLocalHost(hostname: string): boolean {
|
||||||
|
const host = normalizeHost(hostname);
|
||||||
|
return isNamedLocalHost(host) || isPrivateIpv4(host) || isPrivateIpv6(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAllowedHost(hostname: string): boolean {
|
function isAllowedHost(hostname: string): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user