Initial commit: AutoTrader Marker Firefox extension

Mark AutoTrader listings as 'not wanted' on both search results and detail
pages. Includes the unlisted signing / auto-update pipeline (web-ext +
dist/updates.json polled by Firefox).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 12:33:45 +00:00
commit eabab39210
13 changed files with 5094 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
const api = typeof browser !== "undefined" ? browser : chrome;
const STORAGE_KEY = "dismissedListings";
const SETTINGS_KEY = "settings";
const els = {
count: document.getElementById("count"),
hideToggle: document.getElementById("hideToggle"),
exportBtn: document.getElementById("exportBtn"),
importBtn: document.getElementById("importBtn"),
importFile: document.getElementById("importFile"),
clearBtn: document.getElementById("clearBtn"),
status: document.getElementById("status"),
};
function setStatus(msg) {
els.status.textContent = msg;
if (msg) setTimeout(() => (els.status.textContent = ""), 2500);
}
async function refresh() {
const res = await api.storage.local.get([STORAGE_KEY, SETTINGS_KEY]);
const dismissed = res[STORAGE_KEY] || {};
const settings = res[SETTINGS_KEY] || {};
const n = Object.keys(dismissed).length;
els.count.textContent = n === 1 ? "1 listing dismissed" : `${n} listings dismissed`;
els.hideToggle.checked = !!settings.hideDismissed;
}
els.hideToggle.addEventListener("change", async () => {
const res = await api.storage.local.get(SETTINGS_KEY);
const settings = res[SETTINGS_KEY] || {};
settings.hideDismissed = els.hideToggle.checked;
await api.storage.local.set({ [SETTINGS_KEY]: settings });
});
els.clearBtn.addEventListener("click", async () => {
const res = await api.storage.local.get(STORAGE_KEY);
const n = Object.keys(res[STORAGE_KEY] || {}).length;
if (n === 0) {
setStatus("Nothing to clear.");
return;
}
const ok = confirm(`Clear all ${n} dismissals? This can't be undone.`);
if (!ok) return;
await api.storage.local.set({ [STORAGE_KEY]: {} });
await refresh();
setStatus("Cleared.");
});
els.exportBtn.addEventListener("click", async () => {
const res = await api.storage.local.get(STORAGE_KEY);
const data = res[STORAGE_KEY] || {};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
const stamp = new Date().toISOString().slice(0, 10);
a.href = url;
a.download = `autotrader-marker-${stamp}.json`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
setStatus("Exported.");
});
els.importBtn.addEventListener("click", () => els.importFile.click());
els.importFile.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const text = await file.text();
const parsed = JSON.parse(text);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Expected an object keyed by listing id");
}
const res = await api.storage.local.get(STORAGE_KEY);
const merged = { ...(res[STORAGE_KEY] || {}), ...parsed };
await api.storage.local.set({ [STORAGE_KEY]: merged });
await refresh();
setStatus(`Imported ${Object.keys(parsed).length} entries.`);
} catch (err) {
setStatus(`Import failed: ${err.message}`);
} finally {
els.importFile.value = "";
}
});
api.storage.onChanged.addListener((changes, area) => {
if (area === "local" && (changes[STORAGE_KEY] || changes[SETTINGS_KEY])) {
refresh();
}
});
refresh();