Add Opera cache cleaner extension baseline
This commit is contained in:
74
opera-cache-cleaner-extension/.codex/project.md
Normal file
74
opera-cache-cleaner-extension/.codex/project.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Codex Project Notes
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
`Opera Cache Cleaner` is an Opera-compatible Chromium extension for clearing only the browser cache from a toolbar popup. It supports immediate cleanup and an optional timer.
|
||||||
|
|
||||||
|
Repository:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Toxic/Opera-Extensions
|
||||||
|
```
|
||||||
|
|
||||||
|
Gitea URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://git.wilkensxl.de/Toxic/Opera-Extensions.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Use these commands as the source of truth from the repository root:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Syntax check background: node --check background.js
|
||||||
|
Syntax check popup: node --check popup/popup.js
|
||||||
|
Build package: mkdir -p dist && zip -r dist/opera-cache-cleaner-extension.zip manifest.json background.js popup icons -x "*.DS_Store"
|
||||||
|
```
|
||||||
|
|
||||||
|
No install, lint, test, README generation, or dependency audit command exists. The project has no package manifest and no vendored dependencies.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
```text
|
||||||
|
Manifest V3 Chromium extension using plain HTML, CSS, and JavaScript.
|
||||||
|
```
|
||||||
|
|
||||||
|
Package manager or build tool:
|
||||||
|
|
||||||
|
```text
|
||||||
|
None.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Artifacts
|
||||||
|
|
||||||
|
Release artifacts are produced in:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected file:
|
||||||
|
|
||||||
|
```text
|
||||||
|
opera-cache-cleaner-extension.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Rules
|
||||||
|
|
||||||
|
- Keep the extension limited to `browsingData`, `storage`, and `alarms` permissions unless a feature explicitly requires more.
|
||||||
|
- The extension must clear only cache data through `chrome.browsingData.remove(..., { cache: true })`.
|
||||||
|
- Do not request cookie, history, downloads, tabs, or host permissions without a documented user request.
|
||||||
|
- Do not add external network calls.
|
||||||
|
- Do not commit secrets, tokens, `.env` files, certificates, or private keys.
|
||||||
|
|
||||||
|
## Release Rules
|
||||||
|
|
||||||
|
Before a release:
|
||||||
|
|
||||||
|
1. run the syntax checks,
|
||||||
|
2. review `docs/security-review.md`,
|
||||||
|
3. update `CHANGELOG.md`,
|
||||||
|
4. build `dist/opera-cache-cleaner-extension.zip`,
|
||||||
|
5. load the unpacked extension in Opera for a manual smoke test,
|
||||||
|
6. create a tag and release only when the user explicitly asks for it.
|
||||||
37
opera-cache-cleaner-extension/.gitea/workflows/build.yml
Normal file
37
opera-cache-cleaner-extension/.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check background script syntax
|
||||||
|
run: node --check background.js
|
||||||
|
|
||||||
|
- name: Check popup script syntax
|
||||||
|
run: node --check popup/popup.js
|
||||||
|
|
||||||
|
- name: Build extension archive
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
rm -rf dist
|
||||||
|
mkdir -p dist/package
|
||||||
|
cp manifest.json background.js dist/package/
|
||||||
|
cp -R popup icons dist/package/
|
||||||
|
cd dist/package
|
||||||
|
zip -r ../opera-cache-cleaner-extension.zip .
|
||||||
|
|
||||||
|
- name: Upload extension archive
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: opera-cache-cleaner-extension
|
||||||
|
path: dist/opera-cache-cleaner-extension.zip
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: Release Dry Run
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-dry-run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify release documentation
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
test -f README.md
|
||||||
|
test -f CHANGELOG.md
|
||||||
|
test -f SECURITY.md
|
||||||
|
test -f docs/release-checklist.md
|
||||||
|
test -f docs/security-review.md
|
||||||
|
|
||||||
|
- name: Check for unresolved template placeholders
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if grep -RInE 'PROJECT[_]NAME|PROJECT[_]DESCRIPTION|REPOSITORY[_]OWNER|REPOSITORY[_]NAME|PACKAGE[_]NAME|ARTIFACT[_]NAME|ARTIFACT[_]OUTPUT[_]DIRECTORY|BUILD[_]COMMAND|TEST[_]COMMAND|LINT[_]COMMAND|AUDIT[_]COMMAND' . --exclude-dir=.git --exclude-dir=dist; then
|
||||||
|
echo "Unresolved template placeholders found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build extension archive
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
rm -rf dist
|
||||||
|
mkdir -p dist/package
|
||||||
|
cp manifest.json background.js dist/package/
|
||||||
|
cp -R popup icons dist/package/
|
||||||
|
cd dist/package
|
||||||
|
zip -r ../opera-cache-cleaner-extension.zip .
|
||||||
|
test -s ../opera-cache-cleaner-extension.zip
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
name: Repository Cleanup
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 5 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup-report:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Report generated or sensitive tracked files
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
git ls-files | grep -E '(^dist/|^build/|^out/|\.log$|\.tmp$|\.env|\.pem$|\.key$|\.token$)' && {
|
||||||
|
echo "Tracked generated or sensitive-looking files found."
|
||||||
|
exit 1
|
||||||
|
} || true
|
||||||
|
|
||||||
|
- name: Report large tracked files
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
large_files="$(git ls-files -z | xargs -0 du -k | awk '$1 > 1024 { print }')"
|
||||||
|
if [ -n "$large_files" ]; then
|
||||||
|
echo "$large_files"
|
||||||
|
echo "Tracked files above 1 MiB should be reviewed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: Security Scan
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 4 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security-scan:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check JavaScript syntax
|
||||||
|
run: |
|
||||||
|
node --check background.js
|
||||||
|
node --check popup/popup.js
|
||||||
|
|
||||||
|
- name: Scan for risky patterns
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if grep -RInE 'eval\s*\(|new Function|innerHTML\s*=|insertAdjacentHTML|fetch\s*\(|XMLHttpRequest|chrome\.tabs|chrome\.cookies|chrome\.history' background.js popup manifest.json; then
|
||||||
|
echo "Review the matches above before release."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check manifest permissions
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if grep -qE '"(tabs|cookies|history|downloads|<all_urls>)"' manifest.json; then
|
||||||
|
echo "Unexpected broad permission found in manifest.json."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: Template Compliance
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
template-compliance:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify Codex baseline files
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
test -f AGENTS.md
|
||||||
|
test -f .codex/project.md
|
||||||
|
test -f SECURITY.md
|
||||||
|
test -f CHANGELOG.md
|
||||||
|
test -f docs/security-review.md
|
||||||
|
test -f docs/release-checklist.md
|
||||||
|
test -f .gitea/workflows/build.yml
|
||||||
|
test -f .gitea/workflows/security-scan.yml
|
||||||
|
test -f .gitea/workflows/repo-cleanup.yml
|
||||||
|
test -f .gitea/workflows/release-dry-run.yml
|
||||||
|
|
||||||
|
- name: Check for unresolved template placeholders
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if grep -RInE 'PROJECT[_]NAME|PROJECT[_]DESCRIPTION|REPOSITORY[_]OWNER|REPOSITORY[_]NAME|PACKAGE[_]NAME|ARTIFACT[_]NAME|ARTIFACT[_]OUTPUT[_]DIRECTORY|BUILD[_]COMMAND|TEST[_]COMMAND|LINT[_]COMMAND|AUDIT[_]COMMAND' . --exclude-dir=.git --exclude-dir=dist; then
|
||||||
|
echo "Unresolved template placeholders found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
32
opera-cache-cleaner-extension/.gitignore
vendored
Normal file
32
opera-cache-cleaner-extension/.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
release/
|
||||||
|
|
||||||
|
# Logs and temporary files
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Local environment and secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.pfx
|
||||||
|
*.p12
|
||||||
|
*.crt
|
||||||
|
*.cer
|
||||||
|
*.token
|
||||||
|
secrets/
|
||||||
|
|
||||||
|
# OS and editor files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
41
opera-cache-cleaner-extension/AGENTS.md
Normal file
41
opera-cache-cleaner-extension/AGENTS.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
Opera Cache Cleaner is an Opera-compatible Chromium extension that clears only the browser cache for a selected time range and can optionally schedule repeated cache cleanup.
|
||||||
|
|
||||||
|
## Repository Rules
|
||||||
|
|
||||||
|
- Preserve the plain Manifest V3 extension structure. Do not add a framework unless the user explicitly asks for it.
|
||||||
|
- Keep runtime code in `background.js` and `popup/`.
|
||||||
|
- Keep permissions minimal. The extension currently uses only `browsingData`, `storage`, and `alarms`.
|
||||||
|
- Do not add host permissions, network calls, cookie/history access, or broad browser permissions without documenting the reason.
|
||||||
|
- Do not create a release unless the user explicitly asks for one.
|
||||||
|
- Keep `.codex/project.md` aligned when commands, artifact paths, or release rules change.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Use these commands from the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --check background.js
|
||||||
|
node --check popup/popup.js
|
||||||
|
mkdir -p dist && zip -r dist/opera-cache-cleaner-extension.zip manifest.json background.js popup icons -x "*.DS_Store"
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no package manager, dependency install, lint, or dependency audit command for the current project.
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
|
||||||
|
Expected release artifact:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/opera-cache-cleaner-extension.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
## Finish Checklist
|
||||||
|
|
||||||
|
- `git diff --check` passes when the project is inside a Git repository.
|
||||||
|
- `node --check background.js` passes.
|
||||||
|
- `node --check popup/popup.js` passes.
|
||||||
|
- Release documentation is updated when release behavior changes.
|
||||||
8
opera-cache-cleaner-extension/CHANGELOG.md
Normal file
8
opera-cache-cleaner-extension/CHANGELOG.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project are documented here.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Added Codex repository baseline files and Gitea workflow checks.
|
||||||
|
- Documented release, security, and maintenance expectations.
|
||||||
25
opera-cache-cleaner-extension/CONTRIBUTING.md
Normal file
25
opera-cache-cleaner-extension/CONTRIBUTING.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
This extension has no package manager or dependency install step. Edit the plain JavaScript, HTML, CSS, and manifest files directly.
|
||||||
|
|
||||||
|
Run the syntax checks before submitting changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --check background.js
|
||||||
|
node --check popup/popup.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Testing
|
||||||
|
|
||||||
|
1. Open `opera://extensions`.
|
||||||
|
2. Enable developer mode.
|
||||||
|
3. Load this folder as an unpacked extension.
|
||||||
|
4. Open the popup and verify cache clearing, timer save, timer disable, and status display.
|
||||||
|
|
||||||
|
## Security Expectations
|
||||||
|
|
||||||
|
- Do not add host permissions unless required by a documented feature.
|
||||||
|
- Do not add cookie, history, downloads, or tabs permissions without explicit review.
|
||||||
|
- Do not add external network calls.
|
||||||
66
opera-cache-cleaner-extension/README.md
Normal file
66
opera-cache-cleaner-extension/README.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Opera Cache Cleaner Extension
|
||||||
|
|
||||||
|
Eine Opera-kompatible Chromium-Erweiterung zum Loeschen des Browser-Caches ueber ein Toolbar-Popup. Zusaetzlich kann ein einmaliger oder wiederholter Timer eingerichtet werden.
|
||||||
|
|
||||||
|
## Funktionen
|
||||||
|
|
||||||
|
- Cache fuer einen auswaehlbaren Zeitraum loeschen.
|
||||||
|
- Zeitraeume: letzte Stunde, letzte 24 Stunden, letzte 7 Tage, letzte 4 Wochen, gesamter Zeitraum.
|
||||||
|
- Timer mit Intervall in Minuten, Stunden oder Tagen.
|
||||||
|
- Optionaler Repeat-Modus fuer wiederholtes Loeschen.
|
||||||
|
- Statusanzeige fuer letzte Cache-Loeschung, Timer-Status und naechsten geplanten Lauf.
|
||||||
|
|
||||||
|
## Datenschutz
|
||||||
|
|
||||||
|
Die Erweiterung loescht ausschliesslich den Browser-Cache ueber `chrome.browsingData.remove({ since }, { cache: true })`.
|
||||||
|
|
||||||
|
Sie loescht keine Cookies, keinen Verlauf, keine Downloads und keine Passwoerter.
|
||||||
|
|
||||||
|
## Installation in Opera
|
||||||
|
|
||||||
|
1. Oeffne `opera://extensions`.
|
||||||
|
2. Aktiviere den Entwicklermodus.
|
||||||
|
3. Klicke auf `Entpackte Erweiterung laden`.
|
||||||
|
4. Waehle diesen Ordner aus:
|
||||||
|
|
||||||
|
```text
|
||||||
|
D:\Codex\Opera-Extensions\opera-cache-cleaner-extension
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Oeffne die Erweiterung ueber das Toolbar-Icon.
|
||||||
|
|
||||||
|
## Entwicklung
|
||||||
|
|
||||||
|
Die Extension verwendet plain HTML, CSS und JavaScript ohne Paketmanager. Fuehre vor Aenderungen oder Releases diese Checks aus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --check background.js
|
||||||
|
node --check popup/popup.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Ein Release-Archiv kann so gebaut werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p dist && zip -r dist/opera-cache-cleaner-extension.zip manifest.json background.js popup icons -x "*.DS_Store"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
1. Waehle im Popup den gewuenschten Zeitraum aus.
|
||||||
|
2. Klicke auf `Cache jetzt leeren`, um den Cache sofort zu loeschen.
|
||||||
|
3. Fuer einen Timer:
|
||||||
|
- Intervall eingeben.
|
||||||
|
- Einheit auswaehlen.
|
||||||
|
- Optional `Wiederholen` aktivieren.
|
||||||
|
- `Timer speichern` klicken.
|
||||||
|
4. Mit `Timer deaktivieren` wird der aktive Timer entfernt.
|
||||||
|
|
||||||
|
## Berechtigungen
|
||||||
|
|
||||||
|
Die Erweiterung verwendet nur diese Permissions:
|
||||||
|
|
||||||
|
- `browsingData`
|
||||||
|
- `storage`
|
||||||
|
- `alarms`
|
||||||
|
|
||||||
|
Es werden keine Host-Permissions verwendet. Die Erweiterung fordert keine `tabs`, `cookies` oder `history` Permission an.
|
||||||
21
opera-cache-cleaner-extension/SECURITY.md
Normal file
21
opera-cache-cleaner-extension/SECURITY.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| --- | --- |
|
||||||
|
| Latest | Yes |
|
||||||
|
|
||||||
|
## Reporting A Vulnerability
|
||||||
|
|
||||||
|
Please report security issues privately to the project owner.
|
||||||
|
|
||||||
|
Do not include secrets, production data, or private credentials in public issues.
|
||||||
|
|
||||||
|
## Project Security Principles
|
||||||
|
|
||||||
|
- Keep browser permissions minimal.
|
||||||
|
- Clear only browser cache data.
|
||||||
|
- Do not add external network calls.
|
||||||
|
- Do not persist sensitive browsing data.
|
||||||
|
- Keep release artifacts reproducible from tracked files.
|
||||||
373
opera-cache-cleaner-extension/background.js
Normal file
373
opera-cache-cleaner-extension/background.js
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
const ALARM_NAME = "cacheCleanerTimer";
|
||||||
|
const DEFAULT_RANGE_KEY = "last_24h";
|
||||||
|
const MAX_INTERVAL_MINUTES = 30 * 24 * 60;
|
||||||
|
const INTERVAL_UNITS = new Set(["minutes", "hours", "days"]);
|
||||||
|
|
||||||
|
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 DEFAULT_TIMER_CONFIG = {
|
||||||
|
enabled: false,
|
||||||
|
repeat: false,
|
||||||
|
rangeKey: DEFAULT_RANGE_KEY,
|
||||||
|
intervalValue: 1,
|
||||||
|
intervalUnit: "hours",
|
||||||
|
intervalMinutes: 60,
|
||||||
|
nextRun: null
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSinceByRange(rangeKey) {
|
||||||
|
const option = RANGE_OPTIONS[rangeKey];
|
||||||
|
|
||||||
|
if (!option) {
|
||||||
|
throw new Error("Unbekannter Zeitraum.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return option.sinceMs === null ? 0 : Date.now() - option.sinceMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 createAlarm(details) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const result = chrome.alarms.create(ALARM_NAME, details);
|
||||||
|
const error = chrome.runtime.lastError;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reject(new Error(error.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result && typeof result.then === "function") {
|
||||||
|
result.then(resolve).catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAlarmOnly() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.alarms.clear(ALARM_NAME, () => {
|
||||||
|
const error = chrome.runtime.lastError;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reject(new Error(error.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateTimerConfig(config) {
|
||||||
|
if (!config || typeof config !== "object") {
|
||||||
|
throw new Error("Timer-Konfiguration fehlt.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RANGE_OPTIONS[config.rangeKey]) {
|
||||||
|
throw new Error("Unbekannter Zeitraum.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!INTERVAL_UNITS.has(config.intervalUnit)) {
|
||||||
|
throw new Error("Unbekannte Timer-Einheit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(config.intervalValue)) {
|
||||||
|
throw new Error("Das Intervall muss eine Zahl sein.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.intervalValue <= 0) {
|
||||||
|
throw new Error("Das Intervall muss groesser als 0 sein.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(config.intervalMinutes)) {
|
||||||
|
throw new Error("Das Intervall konnte nicht berechnet werden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.intervalMinutes < 1) {
|
||||||
|
throw new Error("Das Mindestintervall betraegt 1 Minute.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.intervalMinutes > MAX_INTERVAL_MINUTES) {
|
||||||
|
throw new Error("Das Maximalintervall betraegt 30 Tage.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearCacheByRange(rangeKey) {
|
||||||
|
const resolvedRangeKey = RANGE_OPTIONS[rangeKey] ? rangeKey : DEFAULT_RANGE_KEY;
|
||||||
|
const since = getSinceByRange(resolvedRangeKey);
|
||||||
|
|
||||||
|
// Only the cache flag is set so cookies, history, downloads, and passwords stay untouched.
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
chrome.browsingData.remove(
|
||||||
|
{ since },
|
||||||
|
{ cache: true },
|
||||||
|
() => {
|
||||||
|
const error = chrome.runtime.lastError;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
reject(new Error(error.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastRun = Date.now();
|
||||||
|
await setStorage({
|
||||||
|
selectedRange: resolvedRangeKey,
|
||||||
|
lastRun
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
lastRun,
|
||||||
|
rangeKey: resolvedRangeKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupAlarm(config) {
|
||||||
|
validateTimerConfig(config);
|
||||||
|
|
||||||
|
const intervalMinutes = config.intervalMinutes;
|
||||||
|
const nextRun = Date.now() + intervalMinutes * 60 * 1000;
|
||||||
|
const timerConfig = {
|
||||||
|
enabled: true,
|
||||||
|
repeat: Boolean(config.repeat),
|
||||||
|
rangeKey: config.rangeKey,
|
||||||
|
intervalValue: config.intervalValue,
|
||||||
|
intervalUnit: config.intervalUnit,
|
||||||
|
intervalMinutes,
|
||||||
|
nextRun
|
||||||
|
};
|
||||||
|
const alarmDetails = timerConfig.repeat
|
||||||
|
? {
|
||||||
|
delayInMinutes: intervalMinutes,
|
||||||
|
periodInMinutes: intervalMinutes
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
delayInMinutes: intervalMinutes
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replacing the alarm avoids duplicate timers after repeated saves.
|
||||||
|
await clearAlarmOnly();
|
||||||
|
await createAlarm(alarmDetails);
|
||||||
|
await setStorage({
|
||||||
|
selectedRange: timerConfig.rangeKey,
|
||||||
|
timerConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
return timerConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearAlarm() {
|
||||||
|
const { timerConfig } = await getStorage(["timerConfig"]);
|
||||||
|
const disabledConfig = {
|
||||||
|
...DEFAULT_TIMER_CONFIG,
|
||||||
|
...(timerConfig || {}),
|
||||||
|
enabled: false,
|
||||||
|
nextRun: null
|
||||||
|
};
|
||||||
|
|
||||||
|
await clearAlarmOnly();
|
||||||
|
await setStorage({ timerConfig: disabledConfig });
|
||||||
|
|
||||||
|
return disabledConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreAlarmOnStartup() {
|
||||||
|
const { timerConfig } = await getStorage(["timerConfig"]);
|
||||||
|
|
||||||
|
if (!timerConfig || timerConfig.enabled !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateTimerConfig(timerConfig);
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
let nextRun = Number(timerConfig.nextRun);
|
||||||
|
let delayInMinutes;
|
||||||
|
|
||||||
|
// Extension restarts can miss an alarm, so restart from a future run instead of firing immediately.
|
||||||
|
if (Number.isFinite(nextRun) && nextRun > now) {
|
||||||
|
delayInMinutes = Math.max(1, Math.ceil((nextRun - now) / 60000));
|
||||||
|
nextRun = now + delayInMinutes * 60000;
|
||||||
|
} else {
|
||||||
|
delayInMinutes = timerConfig.intervalMinutes;
|
||||||
|
nextRun = now + timerConfig.intervalMinutes * 60000;
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoredConfig = {
|
||||||
|
...timerConfig,
|
||||||
|
nextRun
|
||||||
|
};
|
||||||
|
const alarmDetails = restoredConfig.repeat
|
||||||
|
? {
|
||||||
|
delayInMinutes,
|
||||||
|
periodInMinutes: restoredConfig.intervalMinutes
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
delayInMinutes
|
||||||
|
};
|
||||||
|
|
||||||
|
await createAlarm(alarmDetails);
|
||||||
|
await setStorage({ timerConfig: restoredConfig });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTimerAlarm(alarm) {
|
||||||
|
if (!alarm || alarm.name !== ALARM_NAME) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { timerConfig } = await getStorage(["timerConfig"]);
|
||||||
|
|
||||||
|
if (!timerConfig || timerConfig.enabled !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeKey = RANGE_OPTIONS[timerConfig.rangeKey] ? timerConfig.rangeKey : DEFAULT_RANGE_KEY;
|
||||||
|
await clearCacheByRange(rangeKey);
|
||||||
|
|
||||||
|
if (timerConfig.repeat) {
|
||||||
|
const nextRun = Date.now() + timerConfig.intervalMinutes * 60 * 1000;
|
||||||
|
await setStorage({
|
||||||
|
timerConfig: {
|
||||||
|
...timerConfig,
|
||||||
|
rangeKey,
|
||||||
|
nextRun
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await setStorage({
|
||||||
|
timerConfig: {
|
||||||
|
...timerConfig,
|
||||||
|
rangeKey,
|
||||||
|
enabled: false,
|
||||||
|
nextRun: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendError(sendResponse, error) {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: error && error.message ? error.message : "Unbekannter Fehler."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
|
(async () => {
|
||||||
|
if (!message || typeof message.type !== "string") {
|
||||||
|
throw new Error("Unbekannte Nachricht.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "CLEAR_CACHE") {
|
||||||
|
const result = await clearCacheByRange(message.rangeKey);
|
||||||
|
sendResponse({
|
||||||
|
ok: true,
|
||||||
|
...result
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "SET_TIMER") {
|
||||||
|
const timerConfig = await setupAlarm(message.config);
|
||||||
|
sendResponse({
|
||||||
|
ok: true,
|
||||||
|
timerConfig
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "CLEAR_TIMER") {
|
||||||
|
const timerConfig = await clearAlarm();
|
||||||
|
sendResponse({
|
||||||
|
ok: true,
|
||||||
|
timerConfig
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unbekannter Nachrichtentyp.");
|
||||||
|
})().catch((error) => {
|
||||||
|
sendError(sendResponse, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||||
|
handleTimerAlarm(alarm).catch((error) => {
|
||||||
|
console.error("Timer konnte den Cache nicht loeschen.", error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
restoreAlarmOnStartup().catch((error) => {
|
||||||
|
console.error("Timer konnte nicht wiederhergestellt werden.", error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onStartup.addListener(() => {
|
||||||
|
restoreAlarmOnStartup().catch((error) => {
|
||||||
|
console.error("Timer konnte nicht wiederhergestellt werden.", error);
|
||||||
|
});
|
||||||
|
});
|
||||||
17
opera-cache-cleaner-extension/docs/agent-handoff.md
Normal file
17
opera-cache-cleaner-extension/docs/agent-handoff.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Agent Handoff
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
The Codex repository baseline has been applied to the local Opera Cache Cleaner extension.
|
||||||
|
|
||||||
|
## Notes For Next Agent
|
||||||
|
|
||||||
|
- The project is not currently a Git repository in this workspace.
|
||||||
|
- Expected Gitea repository: `https://git.wilkensxl.de/Toxic/Opera-Extensions.git`.
|
||||||
|
- The project has no package manager and no external dependencies.
|
||||||
|
- Use syntax checks and manual Opera extension testing as the main verification path.
|
||||||
|
|
||||||
|
## Open Items
|
||||||
|
|
||||||
|
- Initialize or connect a Git repository if the project should be pushed to Gitea.
|
||||||
|
- Confirm whether this extension should live at the repository root or inside `opera-cache-cleaner-extension/` before enabling package publishing or Gitea API polling.
|
||||||
34
opera-cache-cleaner-extension/docs/release-checklist.md
Normal file
34
opera-cache-cleaner-extension/docs/release-checklist.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Release Checklist
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
- [ ] Version number updated in `manifest.json`.
|
||||||
|
- [ ] `CHANGELOG.md` updated.
|
||||||
|
- [ ] README checked for current installation and usage instructions.
|
||||||
|
|
||||||
|
## Quality
|
||||||
|
|
||||||
|
- [ ] Working tree is clean.
|
||||||
|
- [ ] `node --check background.js` passes.
|
||||||
|
- [ ] `node --check popup/popup.js` passes.
|
||||||
|
- [ ] Manual unpacked-extension smoke test in Opera passes.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- [ ] `docs/security-review.md` is current.
|
||||||
|
- [ ] No new permissions were added without review.
|
||||||
|
- [ ] No secrets are committed.
|
||||||
|
- [ ] Release artifact does not contain local config files.
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
|
||||||
|
- [ ] `dist/opera-cache-cleaner-extension.zip` exists.
|
||||||
|
- [ ] Zip contains `manifest.json`, `background.js`, `popup/`, and `icons/`.
|
||||||
|
- [ ] Zip does not contain `dist/`, `.git/`, `.codex/`, `.gitea/`, or docs-only files.
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
- [ ] Git tag created.
|
||||||
|
- [ ] Release notes written.
|
||||||
|
- [ ] Release published.
|
||||||
|
- [ ] Post-release download smoke test completed.
|
||||||
54
opera-cache-cleaner-extension/docs/security-review.md
Normal file
54
opera-cache-cleaner-extension/docs/security-review.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Security Review
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Project:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Opera Cache Cleaner
|
||||||
|
```
|
||||||
|
|
||||||
|
Reviewed version or commit:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1.0.0 local workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Patterns Checked
|
||||||
|
|
||||||
|
- [x] No `eval`.
|
||||||
|
- [x] No dynamic `Function` constructor.
|
||||||
|
- [x] No unsafe HTML injection found in the reviewed code.
|
||||||
|
- [x] No shell execution.
|
||||||
|
- [x] No external network calls.
|
||||||
|
- [x] No secrets committed in the current source files.
|
||||||
|
- [x] No unsafe file writes. Browser data changes are limited to cache removal.
|
||||||
|
|
||||||
|
## Dependency Review
|
||||||
|
|
||||||
|
Command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
No dependency audit command exists because the project has no package manifest or external dependencies.
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Not applicable.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime Review
|
||||||
|
|
||||||
|
- [x] Manifest uses least-privilege permissions for the current feature set.
|
||||||
|
- [x] No host permissions are declared.
|
||||||
|
- [x] Local storage is used only for selected range, timer settings, and last run timestamp.
|
||||||
|
- [x] Cache clearing uses `chrome.browsingData.remove({ since }, { cache: true })`.
|
||||||
|
|
||||||
|
## Release Notes
|
||||||
|
|
||||||
|
Known residual risks:
|
||||||
|
|
||||||
|
```text
|
||||||
|
No automated browser-extension integration tests exist. Perform an unpacked-extension smoke test in Opera before release.
|
||||||
|
```
|
||||||
BIN
opera-cache-cleaner-extension/icons/icon128.png
Normal file
BIN
opera-cache-cleaner-extension/icons/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
opera-cache-cleaner-extension/icons/icon16.png
Normal file
BIN
opera-cache-cleaner-extension/icons/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 504 B |
BIN
opera-cache-cleaner-extension/icons/icon48.png
Normal file
BIN
opera-cache-cleaner-extension/icons/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
28
opera-cache-cleaner-extension/manifest.json
Normal file
28
opera-cache-cleaner-extension/manifest.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Opera Cache Cleaner",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Clears only the browser cache for a selected time range and can run on a configurable timer.",
|
||||||
|
"permissions": [
|
||||||
|
"browsingData",
|
||||||
|
"storage",
|
||||||
|
"alarms"
|
||||||
|
],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"default_title": "Opera Cache Cleaner",
|
||||||
|
"default_popup": "popup/popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
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