mirror of
https://github.com/Andre-Bzk/AnyClip.git
synced 2026-11-15 09:33:08 +01:00
Erster Blueprint
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#1d5f55"/>
|
||||
<stop offset="100%" stop-color="#9f4b22"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="22" fill="url(#bg)"/>
|
||||
<path d="M27 24h42c4.4 0 8 3.6 8 8v39c0 4.4-3.6 8-8 8H40l-13 10V32c0-4.4 3.6-8 8-8Z" fill="#fff6e7"/>
|
||||
<path d="M40 38h20M40 49h18M40 60h12" stroke="#1f1a14" stroke-linecap="round" stroke-width="6"/>
|
||||
<path d="M63 18l3 9 9 3-9 3-3 9-3-9-9-3 9-3 3-9Z" fill="#ffd66b"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 591 B |
@@ -0,0 +1,268 @@
|
||||
const DEFAULT_SETTINGS = {
|
||||
apiBase: self.AnytypeApi.DEFAULT_API_BASE,
|
||||
apiKey: "",
|
||||
spaceId: "",
|
||||
objectType: "page"
|
||||
};
|
||||
|
||||
browser.runtime.onInstalled.addListener(async () => {
|
||||
const current = await browser.storage.local.get(Object.keys(DEFAULT_SETTINGS));
|
||||
const nextValues = {};
|
||||
|
||||
for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) {
|
||||
if (typeof current[key] === "undefined") {
|
||||
nextValues[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(nextValues).length > 0) {
|
||||
await browser.storage.local.set(nextValues);
|
||||
}
|
||||
|
||||
createContextMenus();
|
||||
});
|
||||
|
||||
browser.runtime.onStartup.addListener(() => {
|
||||
createContextMenus();
|
||||
});
|
||||
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
switch (message?.type) {
|
||||
case "load-settings":
|
||||
return loadSettings();
|
||||
case "save-settings":
|
||||
return saveSettings(message.payload || {});
|
||||
case "list-spaces":
|
||||
return listSpaces();
|
||||
case "save-active-tab":
|
||||
return saveActiveTab(message.payload || {});
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
browser.menus.onClicked.addListener(async (info, tab) => {
|
||||
try {
|
||||
if (info.menuItemId === "clip-page") {
|
||||
await clipTabFromContext(tab, "full", "clip");
|
||||
}
|
||||
|
||||
if (info.menuItemId === "clip-selection") {
|
||||
await clipTabFromContext(tab, "selection", "clip");
|
||||
}
|
||||
|
||||
if (info.menuItemId === "bookmark-page") {
|
||||
await clipTabFromContext(tab, "full", "bookmark");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Anytype clip failed", error);
|
||||
}
|
||||
});
|
||||
|
||||
async function loadSettings() {
|
||||
return browser.storage.local.get(DEFAULT_SETTINGS);
|
||||
}
|
||||
|
||||
async function saveSettings(payload) {
|
||||
const current = await loadSettings();
|
||||
const next = {
|
||||
...current,
|
||||
...payload
|
||||
};
|
||||
|
||||
next.apiBase = self.AnytypeApi.normalizeApiBase(next.apiBase);
|
||||
|
||||
await browser.storage.local.set(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
async function listSpaces() {
|
||||
const settings = await loadSettings();
|
||||
const spaces = await self.AnytypeApi.listSpaces(settings);
|
||||
|
||||
return { spaces };
|
||||
}
|
||||
|
||||
async function saveActiveTab(payload) {
|
||||
const settings = await saveSettings(payload.settings || {});
|
||||
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
if (!tab?.id || !tab.url || /^about:|^moz-extension:/.test(tab.url)) {
|
||||
throw new Error("Diese Seite kann nicht gecaptured werden.");
|
||||
}
|
||||
|
||||
const capture = await browser.tabs.sendMessage(tab.id, {
|
||||
type: "capture-page",
|
||||
mode: payload.mode || "full"
|
||||
});
|
||||
|
||||
return createClipObjects({ capture, settings, intent: "clip" });
|
||||
}
|
||||
|
||||
async function clipTabFromContext(tab, mode, intent) {
|
||||
const settings = await loadSettings();
|
||||
|
||||
if (!settings.apiKey || !settings.spaceId) {
|
||||
throw new Error("Im Addon fehlen API-Key oder Space. Bitte einmal das Popup oder die Optionen ausfuellen.");
|
||||
}
|
||||
|
||||
if (!tab?.id || !tab.url || /^about:|^moz-extension:/.test(tab.url)) {
|
||||
throw new Error("Diese Seite kann nicht gecaptured werden.");
|
||||
}
|
||||
|
||||
const capture = await browser.tabs.sendMessage(tab.id, {
|
||||
type: "capture-page",
|
||||
mode
|
||||
});
|
||||
|
||||
return createClipObjects({ capture, settings, intent });
|
||||
}
|
||||
|
||||
async function createClipObjects({ capture, settings, intent = "clip" }) {
|
||||
const payload = intent === "bookmark"
|
||||
? buildBookmarkPayload(capture)
|
||||
: buildAnytypePayload({ capture, settings });
|
||||
|
||||
const created = await self.AnytypeApi.createObject({
|
||||
apiBase: settings.apiBase,
|
||||
apiKey: settings.apiKey,
|
||||
spaceId: settings.spaceId,
|
||||
payload
|
||||
});
|
||||
|
||||
return { created };
|
||||
}
|
||||
|
||||
function createContextMenus() {
|
||||
browser.menus.removeAll().then(() => {
|
||||
browser.menus.create({
|
||||
id: "clip-page",
|
||||
title: "In Anytype sichern",
|
||||
contexts: ["page", "tab"]
|
||||
});
|
||||
|
||||
browser.menus.create({
|
||||
id: "bookmark-page",
|
||||
title: "In Anytype bookmarken",
|
||||
contexts: ["page", "tab"]
|
||||
});
|
||||
|
||||
browser.menus.create({
|
||||
id: "clip-selection",
|
||||
title: "Auswahl in Anytype sichern",
|
||||
contexts: ["selection"]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildAnytypePayload({ capture, settings }) {
|
||||
const isSelectionCapture = Boolean(capture.isSelectionCapture);
|
||||
const baseTitle = capture.title || "Unbenannte Webseite";
|
||||
const title = isSelectionCapture ? `${baseTitle} (Textauszug)` : baseTitle;
|
||||
const description = truncate(capture.description || capture.selectionText || capture.mainText, 240);
|
||||
const savedAt = formatSavedAt(new Date());
|
||||
const primaryMarkdown = capture.sectionMarkdown || capture.markdown || "_Kein extrahierbarer Inhalt gefunden._";
|
||||
const primaryHasInlineImages = /!\[/.test(primaryMarkdown);
|
||||
const imageMarkdown = primaryHasInlineImages ? "" : buildImageMarkdown(capture.images || []);
|
||||
const sourceLink = `[${capture.url}](${capture.url})`;
|
||||
const contentHeading = isSelectionCapture ? "## Textauszug" : "## Inhalt";
|
||||
const body = [
|
||||
`> Hinweis Quelle: ${sourceLink}`,
|
||||
`> Hinweis Gespeichert am: ${savedAt}`,
|
||||
"",
|
||||
contentHeading,
|
||||
"",
|
||||
primaryMarkdown,
|
||||
imageMarkdown ? "## Bilder" : "",
|
||||
imageMarkdown
|
||||
].filter(Boolean).join("\n\n");
|
||||
|
||||
return {
|
||||
name: title,
|
||||
type_key: settings.objectType || "page",
|
||||
body,
|
||||
properties: compactProperties([
|
||||
buildUrlProperty("source", capture.url),
|
||||
buildTextProperty("description", description)
|
||||
]),
|
||||
...(settings.objectType === "bookmark" ? { source_url: capture.url } : {})
|
||||
};
|
||||
}
|
||||
|
||||
function buildBookmarkPayload(capture) {
|
||||
const title = capture.title || capture.url || "Quelle";
|
||||
const description = truncate(capture.description || capture.selectionText || capture.mainText, 240);
|
||||
|
||||
return {
|
||||
name: title,
|
||||
type_key: "bookmark",
|
||||
source_url: capture.url,
|
||||
body: [
|
||||
`[${capture.url}](${capture.url})`,
|
||||
"",
|
||||
description || ""
|
||||
].filter(Boolean).join("\n\n"),
|
||||
properties: compactProperties([
|
||||
buildUrlProperty("source", capture.url),
|
||||
buildTextProperty("description", description)
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
function buildImageMarkdown(images) {
|
||||
if (!Array.isArray(images) || images.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return images
|
||||
.map((image) => ``)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function compactProperties(properties) {
|
||||
return properties.filter(Boolean);
|
||||
}
|
||||
|
||||
function buildTextProperty(key, value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
text: value
|
||||
};
|
||||
}
|
||||
|
||||
function buildUrlProperty(key, value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
url: value
|
||||
};
|
||||
}
|
||||
|
||||
function formatSavedAt(date) {
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const year = String(date.getFullYear());
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
|
||||
return `${day}.${month}.${year} um ${hours}:${minutes} Uhr`;
|
||||
}
|
||||
|
||||
function truncate(value, maxLength) {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}...` : value;
|
||||
}
|
||||
|
||||
function escapeMarkdownInline(value) {
|
||||
return String(value || "").replace(/([\\`*_{}[\]()#+\-.!])/g, "\\$1");
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,88 @@
|
||||
(function () {
|
||||
const DEFAULT_API_BASE = "http://localhost:31009";
|
||||
const API_VERSION = "2025-11-08";
|
||||
|
||||
function normalizeApiBase(apiBase) {
|
||||
const value = (apiBase || DEFAULT_API_BASE).trim().replace(/\/+$/, "");
|
||||
return value || DEFAULT_API_BASE;
|
||||
}
|
||||
|
||||
async function anytypeRequest({ apiBase, apiKey, path, method = "GET", body }) {
|
||||
if (!apiKey) {
|
||||
throw new Error("Es fehlt ein Anytype API-Key.");
|
||||
}
|
||||
|
||||
const response = await fetch(`${normalizeApiBase(apiBase)}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Anytype-Version": API_VERSION,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let details = "";
|
||||
|
||||
try {
|
||||
const payload = await response.json();
|
||||
details = payload.message || payload.error || JSON.stringify(payload);
|
||||
} catch (error) {
|
||||
details = await response.text();
|
||||
}
|
||||
|
||||
throw new Error(`Anytype API Fehler (${response.status}): ${details || "Unbekannter Fehler"}`);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function listSpaces({ apiBase, apiKey }) {
|
||||
const result = await anytypeRequest({
|
||||
apiBase,
|
||||
apiKey,
|
||||
path: "/v1/spaces"
|
||||
});
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (Array.isArray(result?.data)) {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
if (Array.isArray(result?.spaces)) {
|
||||
return result.spaces;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function createObject({ apiBase, apiKey, spaceId, payload }) {
|
||||
if (!spaceId) {
|
||||
throw new Error("Es wurde kein Anytype-Space ausgewählt.");
|
||||
}
|
||||
|
||||
return anytypeRequest({
|
||||
apiBase,
|
||||
apiKey,
|
||||
path: `/v1/spaces/${encodeURIComponent(spaceId)}/objects`,
|
||||
method: "POST",
|
||||
body: payload
|
||||
});
|
||||
}
|
||||
|
||||
self.AnytypeApi = {
|
||||
DEFAULT_API_BASE,
|
||||
API_VERSION,
|
||||
normalizeApiBase,
|
||||
listSpaces,
|
||||
createObject
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,88 @@
|
||||
:root {
|
||||
--bg: #f2ede2;
|
||||
--panel: #fff9f0;
|
||||
--text: #211910;
|
||||
--muted: #6a6158;
|
||||
--accent: #1d5f55;
|
||||
--accent-strong: #11443c;
|
||||
--border: #d8ccb7;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
linear-gradient(160deg, rgba(29, 95, 85, 0.11), transparent 28%),
|
||||
linear-gradient(180deg, #fbf7ef, var(--bg));
|
||||
color: var(--text);
|
||||
font: 16px/1.55 "Segoe UI", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.settings {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px 64px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.lead {
|
||||
color: var(--muted);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.field,
|
||||
.toggle {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.row,
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
button {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.status {
|
||||
min-height: 24px;
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-top: 24px;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 249, 240, 0.8);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Anytype Web Clipper Einstellungen</title>
|
||||
<link rel="stylesheet" href="options.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="settings">
|
||||
<h1>Anytype Web Clipper</h1>
|
||||
<p class="lead">Firefox speichert Webseiten nach Anytype und übernimmt dabei Text, Struktur und erkannte Bilder direkt in das Zielobjekt.</p>
|
||||
|
||||
<label class="field">
|
||||
<span>Anytype API-URL</span>
|
||||
<input id="apiBase" type="url" placeholder="http://localhost:31009">
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Anytype API-Key</span>
|
||||
<input id="apiKey" type="password" placeholder="API-Key">
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Standard-Space</span>
|
||||
<div class="row">
|
||||
<select id="spaceId"></select>
|
||||
<button id="refreshSpaces" type="button" class="ghost">Neu laden</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Standard-Objekttyp</span>
|
||||
<select id="objectType">
|
||||
<option value="page">Page</option>
|
||||
<option value="bookmark">Bookmark</option>
|
||||
<option value="note">Note</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button id="save" type="button">Speichern</button>
|
||||
</div>
|
||||
|
||||
<p id="status" class="status"></p>
|
||||
</main>
|
||||
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,81 @@
|
||||
const optionsForm = {
|
||||
apiBase: document.getElementById("apiBase"),
|
||||
apiKey: document.getElementById("apiKey"),
|
||||
spaceId: document.getElementById("spaceId"),
|
||||
objectType: document.getElementById("objectType")
|
||||
};
|
||||
|
||||
const optionsStatus = document.getElementById("status");
|
||||
|
||||
document.getElementById("refreshSpaces").addEventListener("click", refreshSpaces);
|
||||
document.getElementById("save").addEventListener("click", saveSettings);
|
||||
|
||||
init().catch(showError);
|
||||
|
||||
async function init() {
|
||||
const settings = await browser.runtime.sendMessage({ type: "load-settings" });
|
||||
optionsForm.apiBase.value = settings.apiBase || "";
|
||||
optionsForm.apiKey.value = settings.apiKey || "";
|
||||
optionsForm.objectType.value = settings.objectType || "page";
|
||||
await refreshSpaces(settings.spaceId);
|
||||
}
|
||||
|
||||
async function refreshSpaces(selectedSpaceId = optionsForm.spaceId.value) {
|
||||
setStatus("Lade Spaces ...");
|
||||
await persist();
|
||||
|
||||
try {
|
||||
const response = await browser.runtime.sendMessage({ type: "list-spaces" });
|
||||
const spaces = response?.spaces || [];
|
||||
optionsForm.spaceId.innerHTML = "";
|
||||
|
||||
spaces.forEach((space) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = space.id || space.space_id || "";
|
||||
option.textContent = space.name || option.value;
|
||||
option.selected = option.value === selectedSpaceId;
|
||||
optionsForm.spaceId.appendChild(option);
|
||||
});
|
||||
|
||||
if (!optionsForm.spaceId.value && spaces.length > 0) {
|
||||
optionsForm.spaceId.selectedIndex = 0;
|
||||
}
|
||||
|
||||
setStatus(spaces.length > 0 ? "Spaces geladen." : "Keine Spaces gefunden.");
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
try {
|
||||
await persist();
|
||||
setStatus("Einstellungen gespeichert.");
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function persist() {
|
||||
await browser.runtime.sendMessage({
|
||||
type: "save-settings",
|
||||
payload: readOptions()
|
||||
});
|
||||
}
|
||||
|
||||
function readOptions() {
|
||||
return {
|
||||
apiBase: optionsForm.apiBase.value.trim(),
|
||||
apiKey: optionsForm.apiKey.value.trim(),
|
||||
spaceId: optionsForm.spaceId.value,
|
||||
objectType: optionsForm.objectType.value
|
||||
};
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
optionsStatus.textContent = message;
|
||||
}
|
||||
|
||||
function showError(error) {
|
||||
setStatus(error?.message || String(error));
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f4efe4;
|
||||
--panel: #fffaf0;
|
||||
--text: #1f1a14;
|
||||
--muted: #6b6258;
|
||||
--accent: #9f4b22;
|
||||
--accent-strong: #7e3715;
|
||||
--border: #d7c6ae;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 360px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(159, 75, 34, 0.12), transparent 34%),
|
||||
linear-gradient(135deg, #f7f2e8, var(--bg));
|
||||
color: var(--text);
|
||||
font: 14px/1.45 "Segoe UI", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.popup {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 14px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.field,
|
||||
.toggle {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.row,
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
button {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--accent-strong);
|
||||
}
|
||||
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.actions button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status {
|
||||
min-height: 20px;
|
||||
margin: 12px 0 0;
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 10px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Anytype Web Clipper</title>
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="popup">
|
||||
<h1>Anytype Web Clipper</h1>
|
||||
|
||||
<label class="field">
|
||||
<span>API-URL</span>
|
||||
<input id="apiBase" type="url" placeholder="http://localhost:31009">
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>API-Key</span>
|
||||
<input id="apiKey" type="password" placeholder="Anytype API Key">
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Space</span>
|
||||
<div class="row">
|
||||
<select id="spaceId"></select>
|
||||
<button id="refreshSpaces" type="button" class="ghost">Aktualisieren</button>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Objekttyp</span>
|
||||
<select id="objectType">
|
||||
<option value="page">Page</option>
|
||||
<option value="bookmark">Bookmark</option>
|
||||
<option value="note">Note</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button id="savePage" type="button">Ganze Seite sichern</button>
|
||||
<button id="saveSelection" type="button" class="ghost">Auswahl sichern</button>
|
||||
</div>
|
||||
|
||||
<p id="status" class="status"></p>
|
||||
</main>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,109 @@
|
||||
const form = {
|
||||
apiBase: document.getElementById("apiBase"),
|
||||
apiKey: document.getElementById("apiKey"),
|
||||
spaceId: document.getElementById("spaceId"),
|
||||
objectType: document.getElementById("objectType")
|
||||
};
|
||||
|
||||
const status = document.getElementById("status");
|
||||
|
||||
document.getElementById("refreshSpaces").addEventListener("click", refreshSpaces);
|
||||
document.getElementById("savePage").addEventListener("click", () => saveClip("full"));
|
||||
document.getElementById("saveSelection").addEventListener("click", () => saveClip("selection"));
|
||||
|
||||
bootstrap().catch(showError);
|
||||
|
||||
async function bootstrap() {
|
||||
const settings = await browser.runtime.sendMessage({ type: "load-settings" });
|
||||
populateForm(settings);
|
||||
await refreshSpaces(settings.spaceId);
|
||||
}
|
||||
|
||||
function populateForm(settings) {
|
||||
form.apiBase.value = settings.apiBase || "";
|
||||
form.apiKey.value = settings.apiKey || "";
|
||||
form.objectType.value = settings.objectType || "page";
|
||||
}
|
||||
|
||||
async function refreshSpaces(selectedSpaceId = form.spaceId.value) {
|
||||
setStatus("Lade Spaces ...");
|
||||
await persistForm();
|
||||
|
||||
try {
|
||||
const response = await browser.runtime.sendMessage({ type: "list-spaces" });
|
||||
const spaces = response?.spaces || [];
|
||||
|
||||
form.spaceId.innerHTML = "";
|
||||
|
||||
if (spaces.length === 0) {
|
||||
const option = document.createElement("option");
|
||||
option.value = "";
|
||||
option.textContent = "Keine Spaces gefunden";
|
||||
form.spaceId.appendChild(option);
|
||||
setStatus("Keine Spaces verfügbar.");
|
||||
return;
|
||||
}
|
||||
|
||||
spaces.forEach((space) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = space.id || space.space_id || "";
|
||||
option.textContent = space.name || option.value;
|
||||
option.selected = option.value === selectedSpaceId;
|
||||
form.spaceId.appendChild(option);
|
||||
});
|
||||
|
||||
if (!form.spaceId.value) {
|
||||
form.spaceId.selectedIndex = 0;
|
||||
}
|
||||
|
||||
await persistForm();
|
||||
setStatus("Spaces geladen.");
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveClip(mode) {
|
||||
setStatus("Sichere Webseite ...");
|
||||
|
||||
try {
|
||||
const result = await browser.runtime.sendMessage({
|
||||
type: "save-active-tab",
|
||||
payload: {
|
||||
mode,
|
||||
settings: readForm()
|
||||
}
|
||||
});
|
||||
|
||||
const objectId = result?.created?.id || result?.created?.object?.id;
|
||||
const suffix = objectId ? ` Objekt-ID: ${objectId}` : "";
|
||||
setStatus(`In Anytype gespeichert.${suffix}`);
|
||||
window.close();
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function persistForm() {
|
||||
await browser.runtime.sendMessage({
|
||||
type: "save-settings",
|
||||
payload: readForm()
|
||||
});
|
||||
}
|
||||
|
||||
function readForm() {
|
||||
return {
|
||||
apiBase: form.apiBase.value.trim(),
|
||||
apiKey: form.apiKey.value.trim(),
|
||||
spaceId: form.spaceId.value,
|
||||
objectType: form.objectType.value
|
||||
};
|
||||
}
|
||||
|
||||
function showError(error) {
|
||||
setStatus(error?.message || String(error));
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
status.textContent = message;
|
||||
}
|
||||
Reference in New Issue
Block a user