Add Opera cache cleaner extension baseline
This commit is contained in:
195
opera-cache-cleaner-extension/popup/popup.css
Normal file
195
opera-cache-cleaner-extension/popup/popup.css
Normal file
@@ -0,0 +1,195 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--background: #f7f8fa;
|
||||
--surface: #ffffff;
|
||||
--border: #d7dce2;
|
||||
--text: #20242a;
|
||||
--muted: #5f6875;
|
||||
--primary: #cc0f2f;
|
||||
--primary-hover: #a90d28;
|
||||
--secondary: #edf0f4;
|
||||
--secondary-hover: #dfe4ea;
|
||||
--error: #9f1d22;
|
||||
--success: #176b3a;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
max-width: 320px;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.popup-shell {
|
||||
width: 320px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin: 0;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.section,
|
||||
.status-box {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
padding: 7px 9px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.field input:focus,
|
||||
.field select:focus,
|
||||
.button:focus {
|
||||
outline: 2px solid rgba(204, 15, 47, 0.28);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-height: 38px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
width: 100%;
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.button-primary:hover:not(:disabled) {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
background: var(--secondary);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.button-secondary:hover:not(:disabled) {
|
||||
background: var(--secondary-hover);
|
||||
}
|
||||
|
||||
.timer-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 2px 0 12px;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.checkbox-row input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-box dl {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-box div {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.status-box dt {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-box dd {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.message-error {
|
||||
color: var(--error);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.message-success {
|
||||
color: var(--success);
|
||||
font-weight: 700;
|
||||
}
|
||||
78
opera-cache-cleaner-extension/popup/popup.html
Normal file
78
opera-cache-cleaner-extension/popup/popup.html
Normal file
@@ -0,0 +1,78 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Opera Cache Cleaner</title>
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="popup-shell">
|
||||
<header class="header">
|
||||
<img class="header-icon" src="../icons/icon48.png" alt="" width="32" height="32">
|
||||
<h1>Opera Cache Cleaner</h1>
|
||||
</header>
|
||||
|
||||
<section class="section" aria-labelledby="range-heading">
|
||||
<h2 id="range-heading">Cache löschen</h2>
|
||||
<label class="field">
|
||||
<span>Zeitraum</span>
|
||||
<select id="rangeSelect" aria-label="Zeitraum"></select>
|
||||
</label>
|
||||
<button id="clearNowButton" class="button button-primary" type="button">Cache jetzt leeren</button>
|
||||
</section>
|
||||
|
||||
<section class="section" aria-labelledby="timer-heading">
|
||||
<h2 id="timer-heading">Timer</h2>
|
||||
<div class="timer-grid">
|
||||
<label class="field">
|
||||
<span>Intervall</span>
|
||||
<input id="intervalValue" type="number" min="1" step="1" inputmode="decimal" value="1">
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Einheit</span>
|
||||
<select id="intervalUnit">
|
||||
<option value="minutes">Minuten</option>
|
||||
<option value="hours">Stunden</option>
|
||||
<option value="days">Tage</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="checkbox-row">
|
||||
<input id="repeatCheckbox" type="checkbox">
|
||||
<span>Wiederholen</span>
|
||||
</label>
|
||||
|
||||
<div class="button-row">
|
||||
<button id="saveTimerButton" class="button button-primary" type="button">Timer speichern</button>
|
||||
<button id="clearTimerButton" class="button button-secondary" type="button">Timer deaktivieren</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="status-box" aria-live="polite" aria-labelledby="status-heading">
|
||||
<h2 id="status-heading">Status</h2>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Letzte Cache-Löschung</dt>
|
||||
<dd id="lastRunValue">Noch nie</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Timer</dt>
|
||||
<dd id="timerStateValue">Inaktiv</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Nächster Lauf</dt>
|
||||
<dd id="nextRunValue">Nicht geplant</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Meldung</dt>
|
||||
<dd id="messageValue">Keine Fehler.</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
312
opera-cache-cleaner-extension/popup/popup.js
Normal file
312
opera-cache-cleaner-extension/popup/popup.js
Normal file
@@ -0,0 +1,312 @@
|
||||
const DEFAULT_RANGE_KEY = "last_24h";
|
||||
const MAX_INTERVAL_MINUTES = 30 * 24 * 60;
|
||||
|
||||
const RANGE_OPTIONS = {
|
||||
last_hour: {
|
||||
label: "Letzte Stunde",
|
||||
sinceMs: 60 * 60 * 1000
|
||||
},
|
||||
last_24h: {
|
||||
label: "Letzte 24 Stunden",
|
||||
sinceMs: 24 * 60 * 60 * 1000
|
||||
},
|
||||
last_7d: {
|
||||
label: "Letzte 7 Tage",
|
||||
sinceMs: 7 * 24 * 60 * 60 * 1000
|
||||
},
|
||||
last_4w: {
|
||||
label: "Letzte 4 Wochen",
|
||||
sinceMs: 28 * 24 * 60 * 60 * 1000
|
||||
},
|
||||
all_time: {
|
||||
label: "Gesamter Zeitraum",
|
||||
sinceMs: null
|
||||
}
|
||||
};
|
||||
|
||||
const UNIT_FACTORS = {
|
||||
minutes: 1,
|
||||
hours: 60,
|
||||
days: 24 * 60
|
||||
};
|
||||
|
||||
const elements = {};
|
||||
|
||||
function getStorage(keys) {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.storage.local.get(keys, (result) => {
|
||||
const error = chrome.runtime.lastError;
|
||||
|
||||
if (error) {
|
||||
reject(new Error(error.message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(result || {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setStorage(values) {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.storage.local.set(values, () => {
|
||||
const error = chrome.runtime.lastError;
|
||||
|
||||
if (error) {
|
||||
reject(new Error(error.message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessage(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage(message, (response) => {
|
||||
const error = chrome.runtime.lastError;
|
||||
|
||||
if (error) {
|
||||
reject(new Error(error.message));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response || response.ok !== true) {
|
||||
reject(new Error(response && response.error ? response.error : "Die Aktion konnte nicht ausgefuehrt werden."));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function populateRangeOptions() {
|
||||
elements.rangeSelect.textContent = "";
|
||||
|
||||
Object.entries(RANGE_OPTIONS).forEach(([value, option]) => {
|
||||
const item = document.createElement("option");
|
||||
item.value = value;
|
||||
item.textContent = option.label;
|
||||
elements.rangeSelect.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp, fallbackText) {
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
return fallbackText;
|
||||
}
|
||||
|
||||
const date = new Date(timestamp);
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return fallbackText;
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat("de-DE", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "medium"
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function setMessage(text, state) {
|
||||
elements.messageValue.textContent = text;
|
||||
elements.messageValue.classList.remove("message-error", "message-success");
|
||||
|
||||
if (state === "error") {
|
||||
elements.messageValue.classList.add("message-error");
|
||||
}
|
||||
|
||||
if (state === "success") {
|
||||
elements.messageValue.classList.add("message-success");
|
||||
}
|
||||
}
|
||||
|
||||
function setBusy(isBusy) {
|
||||
elements.clearNowButton.disabled = isBusy;
|
||||
elements.saveTimerButton.disabled = isBusy;
|
||||
elements.clearTimerButton.disabled = isBusy;
|
||||
}
|
||||
|
||||
function renderStatus(lastRun, timerConfig) {
|
||||
const timer = timerConfig || {};
|
||||
|
||||
elements.lastRunValue.textContent = formatTimestamp(Number(lastRun), "Noch nie");
|
||||
elements.timerStateValue.textContent = timer.enabled
|
||||
? `Aktiv (${timer.repeat ? "Wiederholung" : "einmalig"})`
|
||||
: "Inaktiv";
|
||||
elements.nextRunValue.textContent = timer.enabled
|
||||
? formatTimestamp(Number(timer.nextRun), "Nicht geplant")
|
||||
: "Nicht geplant";
|
||||
}
|
||||
|
||||
function applyState(state) {
|
||||
const selectedRange = RANGE_OPTIONS[state.selectedRange] ? state.selectedRange : DEFAULT_RANGE_KEY;
|
||||
const timerConfig = state.timerConfig || {};
|
||||
|
||||
elements.rangeSelect.value = selectedRange;
|
||||
|
||||
if (Number.isFinite(timerConfig.intervalValue) && timerConfig.intervalValue > 0) {
|
||||
elements.intervalValue.value = String(timerConfig.intervalValue);
|
||||
}
|
||||
|
||||
if (UNIT_FACTORS[timerConfig.intervalUnit]) {
|
||||
elements.intervalUnit.value = timerConfig.intervalUnit;
|
||||
}
|
||||
|
||||
elements.repeatCheckbox.checked = Boolean(timerConfig.repeat);
|
||||
renderStatus(state.lastRun, timerConfig);
|
||||
}
|
||||
|
||||
async function loadState() {
|
||||
const state = await getStorage(["selectedRange", "lastRun", "timerConfig"]);
|
||||
applyState(state);
|
||||
}
|
||||
|
||||
function validateTimerInput() {
|
||||
const rangeKey = elements.rangeSelect.value;
|
||||
const intervalText = elements.intervalValue.value.trim();
|
||||
const intervalValue = Number(intervalText);
|
||||
const intervalUnit = elements.intervalUnit.value;
|
||||
|
||||
if (!RANGE_OPTIONS[rangeKey]) {
|
||||
throw new Error("Bitte einen gueltigen Zeitraum auswaehlen.");
|
||||
}
|
||||
|
||||
if (intervalText === "" || !Number.isFinite(intervalValue)) {
|
||||
throw new Error("Das Intervall muss eine Zahl sein.");
|
||||
}
|
||||
|
||||
if (intervalValue <= 0) {
|
||||
throw new Error("Das Intervall muss groesser als 0 sein.");
|
||||
}
|
||||
|
||||
if (!UNIT_FACTORS[intervalUnit]) {
|
||||
throw new Error("Bitte eine gueltige Einheit auswaehlen.");
|
||||
}
|
||||
|
||||
const intervalMinutes = intervalValue * UNIT_FACTORS[intervalUnit];
|
||||
|
||||
if (intervalMinutes < 1) {
|
||||
throw new Error("Das Mindestintervall betraegt 1 Minute.");
|
||||
}
|
||||
|
||||
if (intervalMinutes > MAX_INTERVAL_MINUTES) {
|
||||
throw new Error("Das Maximalintervall betraegt 30 Tage.");
|
||||
}
|
||||
|
||||
return {
|
||||
rangeKey,
|
||||
repeat: elements.repeatCheckbox.checked,
|
||||
intervalValue,
|
||||
intervalUnit,
|
||||
intervalMinutes
|
||||
};
|
||||
}
|
||||
|
||||
async function saveSelectedRange() {
|
||||
try {
|
||||
await setStorage({ selectedRange: elements.rangeSelect.value });
|
||||
} catch (error) {
|
||||
setMessage(error.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClearNow() {
|
||||
setBusy(true);
|
||||
setMessage("Cache wird geloescht.", "success");
|
||||
|
||||
try {
|
||||
const rangeKey = elements.rangeSelect.value;
|
||||
await sendMessage({
|
||||
type: "CLEAR_CACHE",
|
||||
rangeKey
|
||||
});
|
||||
await loadState();
|
||||
setMessage("Cache wurde geleert.", "success");
|
||||
} catch (error) {
|
||||
setMessage(error.message, "error");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveTimer() {
|
||||
setBusy(true);
|
||||
|
||||
try {
|
||||
const config = validateTimerInput();
|
||||
await sendMessage({
|
||||
type: "SET_TIMER",
|
||||
config
|
||||
});
|
||||
await loadState();
|
||||
setMessage("Timer gespeichert.", "success");
|
||||
} catch (error) {
|
||||
setMessage(error.message, "error");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClearTimer() {
|
||||
setBusy(true);
|
||||
|
||||
try {
|
||||
await sendMessage({ type: "CLEAR_TIMER" });
|
||||
await loadState();
|
||||
setMessage("Timer deaktiviert.", "success");
|
||||
} catch (error) {
|
||||
setMessage(error.message, "error");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function cacheElements() {
|
||||
elements.rangeSelect = document.getElementById("rangeSelect");
|
||||
elements.clearNowButton = document.getElementById("clearNowButton");
|
||||
elements.intervalValue = document.getElementById("intervalValue");
|
||||
elements.intervalUnit = document.getElementById("intervalUnit");
|
||||
elements.repeatCheckbox = document.getElementById("repeatCheckbox");
|
||||
elements.saveTimerButton = document.getElementById("saveTimerButton");
|
||||
elements.clearTimerButton = document.getElementById("clearTimerButton");
|
||||
elements.lastRunValue = document.getElementById("lastRunValue");
|
||||
elements.timerStateValue = document.getElementById("timerStateValue");
|
||||
elements.nextRunValue = document.getElementById("nextRunValue");
|
||||
elements.messageValue = document.getElementById("messageValue");
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
elements.rangeSelect.addEventListener("change", saveSelectedRange);
|
||||
elements.clearNowButton.addEventListener("click", handleClearNow);
|
||||
elements.saveTimerButton.addEventListener("click", handleSaveTimer);
|
||||
elements.clearTimerButton.addEventListener("click", handleClearTimer);
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||
if (areaName !== "local") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes.lastRun || changes.timerConfig || changes.selectedRange) {
|
||||
loadState().catch((error) => {
|
||||
setMessage(error.message, "error");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
cacheElements();
|
||||
populateRangeOptions();
|
||||
bindEvents();
|
||||
|
||||
try {
|
||||
await loadState();
|
||||
} catch (error) {
|
||||
setMessage(error.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
Reference in New Issue
Block a user