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 {
|
func isAllowedOrigin(origin string) bool {
|
||||||
if strings.HasPrefix(origin, "moz-extension://") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(origin, "http://localhost:") || strings.HasPrefix(origin, "https://localhost:") {
|
if strings.HasPrefix(origin, "http://localhost:") || strings.HasPrefix(origin, "https://localhost:") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user