feat: remove firefox extension
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
# MAL Firefox Extension (dev)
|
||||
|
||||
## Load in Firefox
|
||||
|
||||
1. Open `about:debugging#/runtime/this-firefox`
|
||||
2. Click **Load Temporary Add-on…**
|
||||
3. Select `extensions/mal-firefox/manifest.json`
|
||||
|
||||
## Usage
|
||||
|
||||
- Click the toolbar icon to open the popup and log in.
|
||||
- After login, select text on any page → right click → **MyAnimeList** → **Add to Watchlist** → pick a status.
|
||||
@@ -1,103 +0,0 @@
|
||||
const MENU_ROOT_ID = 'mal-root';
|
||||
const MENU_WATCHLIST_ID = 'mal-watchlist';
|
||||
const MENU_STATUS_PREFIX = 'mal-status:';
|
||||
const STATUSES = [
|
||||
{ value: 'watching', label: 'Watching' },
|
||||
{ value: 'completed', label: 'Completed' },
|
||||
{ value: 'on_hold', label: 'On Hold' },
|
||||
{ value: 'dropped', label: 'Dropped' },
|
||||
{ value: 'plan_to_watch', label: 'Plan to Watch' },
|
||||
];
|
||||
|
||||
async function getSettings() {
|
||||
const { authToken, apiBaseUrl } = await browser.storage.local.get(['authToken', 'apiBaseUrl']);
|
||||
return {
|
||||
authToken: authToken || '',
|
||||
apiBaseUrl: apiBaseUrl || 'https://mal.mkelvers.tech',
|
||||
};
|
||||
}
|
||||
|
||||
async function apiFetch(path, init = {}) {
|
||||
const { authToken, apiBaseUrl } = await getSettings();
|
||||
const url = apiBaseUrl.replace(/\/+$/, '') + path;
|
||||
const headers = new Headers(init.headers || {});
|
||||
if (authToken) headers.set('Authorization', `Bearer ${authToken}`);
|
||||
const res = await fetch(url, { ...init, headers });
|
||||
if (!res.ok) {
|
||||
const msg = await res.text().catch(() => '');
|
||||
throw new Error(msg || `HTTP ${res.status}`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function ensureContextMenu() {
|
||||
const { authToken } = await getSettings();
|
||||
await browser.contextMenus.removeAll();
|
||||
if (!authToken) return;
|
||||
|
||||
browser.contextMenus.create({
|
||||
id: MENU_ROOT_ID,
|
||||
title: 'MyAnimeList',
|
||||
contexts: ['selection'],
|
||||
});
|
||||
|
||||
browser.contextMenus.create({
|
||||
id: MENU_WATCHLIST_ID,
|
||||
parentId: MENU_ROOT_ID,
|
||||
title: 'Add to Watchlist',
|
||||
contexts: ['selection'],
|
||||
});
|
||||
|
||||
for (const s of STATUSES) {
|
||||
browser.contextMenus.create({
|
||||
id: MENU_STATUS_PREFIX + s.value,
|
||||
parentId: MENU_WATCHLIST_ID,
|
||||
title: s.label,
|
||||
contexts: ['selection'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
ensureContextMenu();
|
||||
});
|
||||
|
||||
browser.runtime.onStartup.addListener(() => {
|
||||
ensureContextMenu();
|
||||
});
|
||||
|
||||
browser.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== 'local') return;
|
||||
if (changes.authToken) ensureContextMenu();
|
||||
});
|
||||
|
||||
browser.contextMenus.onClicked.addListener(async info => {
|
||||
if (typeof info.menuItemId !== 'string') return;
|
||||
if (!info.menuItemId.startsWith(MENU_STATUS_PREFIX)) return;
|
||||
|
||||
const status = info.menuItemId.slice(MENU_STATUS_PREFIX.length);
|
||||
const text = (info.selectionText || '').trim().replace(/\s+/g, ' ').slice(0, 120);
|
||||
if (!text) return;
|
||||
|
||||
try {
|
||||
const searchRes = await apiFetch(`/api/search-quick?q=${encodeURIComponent(text)}`);
|
||||
const items = await searchRes.json();
|
||||
const top = items && items[0];
|
||||
if (!top || !top.id) {
|
||||
await browser.notifications?.create?.({
|
||||
type: 'basic',
|
||||
title: 'MyAnimeList',
|
||||
message: `No matches for: ${text}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await apiFetch('/api/watchlist', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ animeId: top.id, status }),
|
||||
});
|
||||
} catch {
|
||||
// Silent failure by default; can be extended with notifications later.
|
||||
}
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="35%" cy="35%" r="75%">
|
||||
<stop offset="0%" style="stop-color: var(--accent, #0466c8)" />
|
||||
<stop offset="100%" style="stop-color: var(--accent-dark, #1d4ed8)" />
|
||||
</radialGradient>
|
||||
<clipPath id="clip">
|
||||
<circle cx="50" cy="50" r="45" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<!-- Base -->
|
||||
<circle cx="50" cy="50" r="45" fill="url(#bg)" />
|
||||
|
||||
<!-- Crescent moon cutout -->
|
||||
<g clip-path="url(#clip)">
|
||||
<path
|
||||
d="M70 50a25 25 0 1 1 -25 -25 20 20 0 1 0 25 25z"
|
||||
fill="#FFF7ED"
|
||||
transform="translate(-2 -2)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 685 B |
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "MyAnimeList",
|
||||
"version": "0.1.0",
|
||||
"description": "Right-click selected anime titles and add them to your watchlist.",
|
||||
"permissions": ["contextMenus", "storage"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"action": {
|
||||
"default_title": "MAL Watchlist",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"icons": {
|
||||
"48": "icon.svg"
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg: #0b0f1a;
|
||||
--card: rgba(255, 255, 255, 0.06);
|
||||
--border: rgba(255, 255, 255, 0.12);
|
||||
--text: rgba(255, 255, 255, 0.92);
|
||||
--muted: rgba(255, 255, 255, 0.65);
|
||||
--accent: #6ea8fe;
|
||||
--danger: #ff6b6b;
|
||||
--ok: #4ade80;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--bg: #f6f7fb;
|
||||
--card: rgba(0, 0, 0, 0.03);
|
||||
--border: rgba(0, 0, 0, 0.1);
|
||||
--text: rgba(0, 0, 0, 0.88);
|
||||
--muted: rgba(0, 0, 0, 0.6);
|
||||
--accent: #1f6feb;
|
||||
--danger: #b42318;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font:
|
||||
14px/1.4 system-ui,
|
||||
-apple-system,
|
||||
Segoe UI,
|
||||
Roboto,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 380px;
|
||||
min-width: 380px;
|
||||
}
|
||||
|
||||
#app {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.brandIcon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.link {
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
border: 0;
|
||||
padding: 6px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: transparent;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.label {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 9px 10px;
|
||||
border-radius: 0;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border: 1px solid var(--border);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
background: rgba(110, 168, 254, 0.18);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
background: rgba(255, 107, 107, 0.18);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.body {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.login {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.statusRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--ok);
|
||||
}
|
||||
|
||||
.statusText {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: grid;
|
||||
grid-template-columns: 44px 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
width: 44px;
|
||||
height: 62px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.metaTitle {
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.metaSub {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.select {
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
color: var(--text);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mini {
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
border: 0;
|
||||
background: rgba(110, 168, 254, 0.18);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>MAL Watchlist</title>
|
||||
<link rel="stylesheet" href="popup.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<section class="panel">
|
||||
<header class="header">
|
||||
<div class="brand">
|
||||
<img class="brandIcon" src="icon.svg" alt="" />
|
||||
<div class="title">MyAnimeList</div>
|
||||
</div>
|
||||
<button id="logoutBtn" class="link" hidden>Log out</button>
|
||||
</header>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="body">
|
||||
Select an anime title on any page, then right click to open the context menu. Under
|
||||
“MyAnimeList”, choose “Add to Watchlist” and pick a status to save it to your watchlist.
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div id="loggedIn" class="statusRow" hidden>
|
||||
<div class="statusDot"></div>
|
||||
<div class="statusText">Signed in — context menu enabled</div>
|
||||
</div>
|
||||
|
||||
<div id="login" class="login" hidden>
|
||||
<label class="label">
|
||||
Username
|
||||
<input id="username" class="input" autocomplete="username" />
|
||||
</label>
|
||||
<label class="label">
|
||||
Password
|
||||
<input id="password" class="input" type="password" autocomplete="current-password" />
|
||||
</label>
|
||||
<button id="loginBtn" class="btn">Log in</button>
|
||||
<div id="loginErr" class="error" hidden></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,74 +0,0 @@
|
||||
function qs(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
async function getSettings() {
|
||||
const { authToken, apiBaseUrl } = await browser.storage.local.get(['authToken', 'apiBaseUrl']);
|
||||
return {
|
||||
authToken: authToken || '',
|
||||
apiBaseUrl: apiBaseUrl || 'https://mal.mkelvers.tech',
|
||||
};
|
||||
}
|
||||
|
||||
async function setSettings(patch) {
|
||||
await browser.storage.local.set(patch);
|
||||
}
|
||||
|
||||
function show(el, on) {
|
||||
el.hidden = !on;
|
||||
}
|
||||
|
||||
async function render() {
|
||||
const settings = await getSettings();
|
||||
document.body.dataset.state = settings.authToken ? 'in' : 'out';
|
||||
|
||||
const logoutBtn = qs('logoutBtn');
|
||||
logoutBtn.addEventListener('click', async () => {
|
||||
await setSettings({ authToken: '' });
|
||||
await render();
|
||||
});
|
||||
|
||||
const hasToken = !!settings.authToken;
|
||||
show(logoutBtn, hasToken);
|
||||
show(qs('login'), !hasToken);
|
||||
show(qs('loggedIn'), hasToken);
|
||||
|
||||
if (!hasToken) {
|
||||
setupLogin();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function setupLogin() {
|
||||
const loginErr = qs('loginErr');
|
||||
show(loginErr, false);
|
||||
|
||||
qs('loginBtn').onclick = async () => {
|
||||
show(loginErr, false);
|
||||
const username = qs('username').value.trim();
|
||||
const password = qs('password').value;
|
||||
if (!username || !password) {
|
||||
loginErr.textContent = 'Missing username or password';
|
||||
show(loginErr, true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { apiBaseUrl } = await getSettings();
|
||||
const res = await fetch(apiBaseUrl.replace(/\/+$/, '') + '/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, name: 'Firefox extension' }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Invalid username or password');
|
||||
const data = await res.json();
|
||||
await setSettings({ authToken: data.token });
|
||||
await render();
|
||||
} catch (e) {
|
||||
loginErr.textContent = e.message || 'Login failed';
|
||||
show(loginErr, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render();
|
||||
@@ -32,9 +32,6 @@ func CORSMiddleware() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
func isAllowedOrigin(origin string) bool {
|
||||
if strings.HasPrefix(origin, "moz-extension://") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(origin, "http://localhost:") || strings.HasPrefix(origin, "https://localhost:") {
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user