mirror of
https://github.com/Andre-Bzk/AnyClip.git
synced 2026-11-15 15:43:07 +01:00
Erster Blueprint
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
# Anytype Web Clipper fuer Firefox
|
||||||
|
|
||||||
|
Dieses Projekt stellt ein Firefox-WebExtension-Addon bereit, das Webseiten nach Anytype speichert.
|
||||||
|
|
||||||
|
## Was das Addon kann
|
||||||
|
|
||||||
|
- Aktuelle Seite oder nur markierte Auswahl aus Firefox nach Anytype senden
|
||||||
|
- Titel, URL, Beschreibung und extrahierten Inhalt als Anytype-Objekt speichern
|
||||||
|
- Relevante Bilder direkt im Anytype-Inhalt mitfuehren, soweit die Anytype-API diese uebernimmt
|
||||||
|
- Spaces ueber die lokale Anytype-API laden und direkt im Addon auswaehlen
|
||||||
|
- Ueber das Toolbar-Popup oder per Rechtsklick auf die Webseite clippen
|
||||||
|
|
||||||
|
## Einrichtung
|
||||||
|
|
||||||
|
1. Anytype Desktop starten.
|
||||||
|
2. In Anytype unter `Settings -> API Keys` einen API-Key erzeugen.
|
||||||
|
3. In Firefox `about:debugging` oeffnen.
|
||||||
|
4. `This Firefox` waehlen.
|
||||||
|
5. `Load Temporary Add-on...` anklicken.
|
||||||
|
6. Die Datei `manifest.json` aus diesem Projekt auswaehlen.
|
||||||
|
7. Im Firefox-Erweiterungsmenue das Addon oeffnen und mit dem Pin-Symbol an die Symbolleiste anheften.
|
||||||
|
8. Im Addon Popup API-URL und API-Key eintragen.
|
||||||
|
9. Spaces laden und einen Ziel-Space auswaehlen.
|
||||||
|
|
||||||
|
Standard-API-URL: `http://localhost:31009`
|
||||||
|
|
||||||
|
## Bedienung
|
||||||
|
|
||||||
|
- Klick auf das Addon-Icon in der Firefox-Symbolleiste oder im Erweiterungsmenue
|
||||||
|
- Rechtsklick auf die Seite: `In Anytype sichern`
|
||||||
|
- Rechtsklick auf markierten Text: `Auswahl in Anytype sichern`
|
||||||
|
|
||||||
|
## Hinweis zu Firefox
|
||||||
|
|
||||||
|
Ob ein Addon direkt sichtbar in der Symbolleiste sitzt, entscheidet Firefox. Das Addon kann sich nicht selbst anheften. Falls es nicht sichtbar ist, findest du es im Erweiterungsmenue und kannst es dort anpinnen.
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
- `manifest.json`
|
||||||
|
- `src/background.js`
|
||||||
|
- `src/content-script.js`
|
||||||
|
- `src/lib/anytype.js`
|
||||||
|
- `src/popup/popup.html`
|
||||||
|
- `src/options/options.html`
|
||||||
|
|
||||||
|
## Weiterer Ausbau
|
||||||
|
|
||||||
|
- Bessere Lesbarkeits-Extraktion mit Mozilla Readability
|
||||||
|
- Noch robustere domainspezifische Bild-/Abschnitts-Extractor fuer komplexe Websites
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 2,
|
||||||
|
"name": "Anytype Web Clipper",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Speichert Webseiten aus Firefox in Anytype.",
|
||||||
|
"permissions": [
|
||||||
|
"activeTab",
|
||||||
|
"storage",
|
||||||
|
"tabs",
|
||||||
|
"menus",
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
"icons": {
|
||||||
|
"48": "src/assets/icon.svg",
|
||||||
|
"96": "src/assets/icon.svg"
|
||||||
|
},
|
||||||
|
"browser_action": {
|
||||||
|
"default_title": "Anytype Web Clipper",
|
||||||
|
"default_popup": "src/popup/popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"48": "src/assets/icon.svg",
|
||||||
|
"96": "src/assets/icon.svg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"scripts": [
|
||||||
|
"src/lib/anytype.js",
|
||||||
|
"src/background.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options_ui": {
|
||||||
|
"page": "src/options/options.html",
|
||||||
|
"open_in_tab": true
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
"js": [
|
||||||
|
"src/content-script.js"
|
||||||
|
],
|
||||||
|
"run_at": "document_idle"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"applications": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "anytype-web-clipper@example.local"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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