mirror of
https://github.com/procc99/dictcc-sidebar.git
synced 2026-07-22 07:46:05 +00:00
Initial release: dict.cc sidebar plugin
This commit is contained in:
commit
00fcb8a6af
10 changed files with 757 additions and 0 deletions
47
.github/workflows/release.yml
vendored
Normal file
47
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
name: Release Obsidian Plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: dictcc-sidebar
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build plugin
|
||||
run: npm run build
|
||||
|
||||
- name: Attest build artifacts
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-path: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
main.js
|
||||
*.js.map
|
||||
.hotreload
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Craig Irving
|
||||
Portions copyright (c) 2026 Jonas Karneboge (wiktionary-sidebar)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
42
README.md
Normal file
42
README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# dict.cc Sidebar
|
||||
|
||||
An Obsidian plugin that lets you look up any German or English word on [dict.cc](https://www.dict.cc/) without leaving your vault.
|
||||
|
||||
**Right-click any selected word → "dict.cc: word" → bilingual translation table appears in the sidebar.**
|
||||
|
||||
## Features
|
||||
|
||||
- Works in both **editor view** and **reading view**
|
||||
- Shows **grouped translation tables** with section headers (Verbs, Nouns, etc.)
|
||||
- **Misspelling suggestions** — click any suggestion to re-search instantly
|
||||
- **Direction filter** in settings: DE ↔ EN (both), DE → EN, or EN → DE
|
||||
- **Max results** slider to control how many rows are displayed
|
||||
- Respects Obsidian's **light and dark themes** via CSS variables
|
||||
- Desktop only
|
||||
|
||||
## Installation
|
||||
|
||||
### From the Community Plugin Browser (recommended)
|
||||
1. Open Obsidian → Settings → Community Plugins → Browse
|
||||
2. Search for **dict.cc Sidebar**
|
||||
3. Install and enable
|
||||
|
||||
### Manual installation
|
||||
1. Download `main.js`, `manifest.json`, and `styles.css` from the [latest release](../../releases/latest)
|
||||
2. Copy them to `.obsidian/plugins/dictcc-sidebar/` in your vault
|
||||
3. Enable the plugin in Settings → Community Plugins
|
||||
|
||||
## Usage
|
||||
1. Select a word in any note (editor or reading view)
|
||||
2. Right-click → **dict.cc: "word"**
|
||||
3. The sidebar opens with bilingual translation results
|
||||
|
||||
Or use the Command Palette: **Look up selection in dict.cc**
|
||||
|
||||
## Settings
|
||||
Open Settings → dict.cc Sidebar:
|
||||
- **Translation direction**: show both directions, only DE→EN, or only EN→DE
|
||||
- **Max results**: limit how many translation rows are shown
|
||||
|
||||
## License
|
||||
MIT — forked from [wiktionary-sidebar](https://github.com/jonas-karneboge/wiktionary-sidebar) by Jonas Karneboge
|
||||
47
esbuild.config.mjs
Normal file
47
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: { js: banner },
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
10
manifest.json
Normal file
10
manifest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "dictcc-sidebar",
|
||||
"name": "dict.cc Sidebar",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Right-click any selected word to look it up on dict.cc (German ↔ English). Shows bilingual translation tables in a sidebar with misspelling suggestions.",
|
||||
"author": "Craig Irving",
|
||||
"authorUrl": "https://github.com/craigirving",
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
17
package.json
Normal file
17
package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "dictcc-sidebar",
|
||||
"version": "1.0.0",
|
||||
"description": "Obsidian plugin: dict.cc sidebar for German-English translation",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc --noEmit -p tsconfig.json && node esbuild.config.mjs production"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18",
|
||||
"obsidian": "latest",
|
||||
"esbuild": "0.17.3",
|
||||
"builtin-modules": "3.3.0",
|
||||
"typescript": "5.0.0"
|
||||
}
|
||||
}
|
||||
427
src/main.ts
Normal file
427
src/main.ts
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
import {
|
||||
App,
|
||||
Editor,
|
||||
ItemView,
|
||||
Menu,
|
||||
MarkdownView,
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
WorkspaceLeaf,
|
||||
} from "obsidian";
|
||||
|
||||
// ── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
type Direction = "both" | "de-en" | "en-de";
|
||||
|
||||
interface DictEntry {
|
||||
section: string;
|
||||
english: string;
|
||||
german: string;
|
||||
}
|
||||
|
||||
interface DictResult {
|
||||
term: string;
|
||||
entries: DictEntry[];
|
||||
suggestions: string[];
|
||||
notFound: boolean;
|
||||
}
|
||||
|
||||
interface DictCCSettings {
|
||||
direction: Direction;
|
||||
maxResults: number;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: DictCCSettings = {
|
||||
direction: "both",
|
||||
maxResults: 40,
|
||||
};
|
||||
|
||||
const VIEW_TYPE = "dictcc-sidebar";
|
||||
|
||||
// ── HTML helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
function decodeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/<br\s*\/?>/gi, "\n")
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/&#(\d+);/g, (_: string, n: string) => String.fromCharCode(Number(n)))
|
||||
.replace(/&#x([0-9a-f]+);/gi, (_: string, n: string) => String.fromCharCode(parseInt(n, 16)));
|
||||
}
|
||||
|
||||
function stripTags(str: string): string {
|
||||
return decodeHtml(str.replace(/<[^>]*>/g, " ")).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function normalizeCell(cellHtml: string): string {
|
||||
let s = cellHtml
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
||||
.replace(/<a[^>]*>([\s\S]*?)<\/a>/gi, "$1");
|
||||
return stripTags(s)
|
||||
.replace(/\s*\[\s*/g, " [")
|
||||
.replace(/\s*\]\s*/g, "] ")
|
||||
.replace(/\s+([,;:.!?])/g, "$1")
|
||||
.replace(/\(\s+/g, "(")
|
||||
.replace(/\s+\)/g, ")")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
// ── dict.cc fetch + parse ────────────────────────────────────────────────────
|
||||
|
||||
async function fetchDictCC(term: string): Promise<DictResult> {
|
||||
const url = `https://www.dict.cc/?s=${encodeURIComponent(term)}`;
|
||||
|
||||
let html: string;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { "accept-language": "en,de;q=0.9" },
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
html = await res.text();
|
||||
} catch {
|
||||
return { term, entries: [], suggestions: [], notFound: true };
|
||||
}
|
||||
|
||||
// ── Extract suggestions from c1Entry / "Ähnliche Begriffe" / failed_kw ──
|
||||
const suggestions: string[] = [];
|
||||
|
||||
// failed_kw in URL means this is a corrected search — the original bad term
|
||||
const failedKw = html.match(/failed_kw=([^&"'>\s]+)/i);
|
||||
if (failedKw) {
|
||||
const bad = decodeURIComponent(failedKw[1].replace(/\+/g, " "));
|
||||
suggestions.push(bad);
|
||||
}
|
||||
|
||||
// Suggestion links appearing in similar-words blocks
|
||||
const suggLinkRe = /<a[^>]+href="[^"]*\?s=([^"&]+)[^"]*"[^>]*class="[^"]*(?:col1Entry|did-you-mean)[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
|
||||
let sm: RegExpExecArray | null;
|
||||
while ((sm = suggLinkRe.exec(html))) {
|
||||
const t2 = stripTags(sm[2]).replace(/^[•·\-]\s*/, "");
|
||||
if (t2 && !suggestions.includes(t2)) suggestions.push(t2);
|
||||
}
|
||||
|
||||
// Also scan the "Ähnliche Begriffe" / similar section for plain word links
|
||||
const simBlock = html.match(/hnliche Begriffe([\s\S]{0,3000}?)(?:back to top|home|©)/i)?.[1] ?? "";
|
||||
const simRe = /<a[^>]+href="[^"]*\?s=([^"&]+)[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
|
||||
let sg: RegExpExecArray | null;
|
||||
while ((sg = simRe.exec(simBlock))) {
|
||||
const t3 = stripTags(sg[2]).replace(/^[•·\-]\s*/, "");
|
||||
if (t3 && t3.length < 50 && !suggestions.includes(t3)) suggestions.push(t3);
|
||||
}
|
||||
|
||||
// ── Extract translation rows ─────────────────────────────────────────────
|
||||
// dict.cc embeds translations in a JS variable: var c1Arr = [...];
|
||||
// Each entry is: [id, [en_html, de_html], pos, tags]
|
||||
// Fallback: parse HTML table rows
|
||||
const entries: DictEntry[] = [];
|
||||
let currentSection = "Entries";
|
||||
|
||||
// Try JS variable extraction first (most reliable)
|
||||
const jsArr = html.match(/var\s+c1Arr\s*=\s*(\[[\s\S]*?\]);\s*\n/);
|
||||
if (jsArr) {
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
const arr: Array<[number, [string, string], string, string[]]> = JSON.parse(jsArr[1]);
|
||||
for (const row of arr) {
|
||||
if (!Array.isArray(row) || !Array.isArray(row[1])) continue;
|
||||
const [en, de] = row[1];
|
||||
const sec = typeof row[2] === "string" && row[2] ? row[2] : "Entries";
|
||||
const english = normalizeCell(en);
|
||||
const german = normalizeCell(de);
|
||||
if (english && german) entries.push({ section: sec, english, german });
|
||||
}
|
||||
} catch {
|
||||
// fallthrough to HTML table parse
|
||||
}
|
||||
}
|
||||
|
||||
// HTML table fallback
|
||||
if (!entries.length) {
|
||||
const rowRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
||||
let rm: RegExpExecArray | null;
|
||||
while ((rm = rowRe.exec(html))) {
|
||||
const rowHtml = rm[1];
|
||||
// Section header rows (colspan)
|
||||
const sectionOnly = rowHtml.match(/<td[^>]*colspan=["']?\d+["']?[^>]*>([\s\S]*?)<\/td>/i);
|
||||
if (sectionOnly && !/<td[^>]*>[\s\S]*?<td/i.test(rowHtml)) {
|
||||
const st = normalizeCell(sectionOnly[1]);
|
||||
if (st && !/dict\.cc|Impressum|back to top/i.test(st)) currentSection = st;
|
||||
continue;
|
||||
}
|
||||
const cells = [...rowHtml.matchAll(/<td[^>]*>([\s\S]*?)<\/td>/gi)].map((m) => m[1]);
|
||||
if (cells.length < 2) continue;
|
||||
const cleaned = cells.map(normalizeCell);
|
||||
const english = cleaned[0];
|
||||
const german = cleaned[1];
|
||||
if (!english || !german) continue;
|
||||
if (/^English$/i.test(english) || /^German$/i.test(german)) continue;
|
||||
if (/dict\.cc|Impressum|©/.test(english)) continue;
|
||||
entries.push({ section: currentSection, english, german });
|
||||
}
|
||||
}
|
||||
|
||||
const notFound = entries.length === 0;
|
||||
return { term, entries, suggestions: suggestions.slice(0, 10), notFound };
|
||||
}
|
||||
|
||||
// ── ItemView (Sidebar) ────────────────────────────────────────────────────────
|
||||
|
||||
class DictCCSidebarView extends ItemView {
|
||||
private plugin: DictCCPlugin;
|
||||
private currentSuggestions: string[] = [];
|
||||
private currentTerm = "";
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: DictCCPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType(): string { return VIEW_TYPE; }
|
||||
getDisplayText(): string { return "dict.cc"; }
|
||||
getIcon(): string { return "languages"; }
|
||||
|
||||
async onOpen(): Promise<void> { this.renderEmpty(); }
|
||||
async onClose(): Promise<void> {}
|
||||
|
||||
async lookup(term: string): Promise<void> {
|
||||
this.currentTerm = term;
|
||||
this.renderLoading(term);
|
||||
const result = await fetchDictCC(term);
|
||||
this.renderResult(result);
|
||||
}
|
||||
|
||||
private container(): HTMLElement {
|
||||
return this.containerEl.children[1] as HTMLElement;
|
||||
}
|
||||
|
||||
private renderEmpty(): void {
|
||||
const c = this.container();
|
||||
c.empty();
|
||||
c.addClass("dictcc-container");
|
||||
c.createEl("p", { cls: "dictcc-placeholder", text: "Select a word and right-click → dict.cc lookup" });
|
||||
}
|
||||
|
||||
private renderLoading(term: string): void {
|
||||
const c = this.container();
|
||||
c.empty();
|
||||
c.addClass("dictcc-container");
|
||||
c.createEl("p", { cls: "dictcc-loading", text: `Looking up "${term}"…` });
|
||||
}
|
||||
|
||||
private renderResult(result: DictResult): void {
|
||||
const c = this.container();
|
||||
c.empty();
|
||||
c.addClass("dictcc-container");
|
||||
|
||||
// Header
|
||||
const header = c.createEl("div", { cls: "dictcc-header" });
|
||||
header.createEl("h2", { cls: "dictcc-word", text: result.term });
|
||||
header.createEl("a", {
|
||||
cls: "dictcc-source-link",
|
||||
text: "dict.cc ↗",
|
||||
href: `https://www.dict.cc/?s=${encodeURIComponent(result.term)}`,
|
||||
});
|
||||
|
||||
// Suggestions (misspelling / similar terms)
|
||||
if (result.suggestions.length > 0) {
|
||||
const sugBlock = c.createEl("div", { cls: "dictcc-suggestions" });
|
||||
sugBlock.createEl("p", { cls: "dictcc-sug-label", text: "Did you mean:" });
|
||||
const sugList = sugBlock.createEl("div", { cls: "dictcc-sug-list" });
|
||||
for (const sug of result.suggestions) {
|
||||
const btn = sugList.createEl("button", { cls: "dictcc-sug-btn", text: sug });
|
||||
btn.addEventListener("click", () => this.lookup(sug));
|
||||
}
|
||||
}
|
||||
|
||||
if (result.notFound) {
|
||||
c.createEl("p", { cls: "dictcc-not-found", text: `No translations found for "${result.term}".` });
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter by direction
|
||||
let entries = result.entries;
|
||||
const dir = this.plugin.settings.direction;
|
||||
if (dir === "de-en") {
|
||||
entries = entries.filter(
|
||||
(e) => e.german.toLowerCase().includes(result.term.toLowerCase())
|
||||
);
|
||||
} else if (dir === "en-de") {
|
||||
entries = entries.filter(
|
||||
(e) => e.english.toLowerCase().includes(result.term.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
const maxR = this.plugin.settings.maxResults;
|
||||
entries = entries.slice(0, maxR);
|
||||
|
||||
// Group by section
|
||||
const sections = new Map<string, DictEntry[]>();
|
||||
for (const e of entries) {
|
||||
if (!sections.has(e.section)) sections.set(e.section, []);
|
||||
sections.get(e.section)!.push(e);
|
||||
}
|
||||
|
||||
for (const [sec, rows] of sections) {
|
||||
const secEl = c.createEl("div", { cls: "dictcc-section" });
|
||||
secEl.createEl("h4", { cls: "dictcc-section-title", text: sec });
|
||||
const table = secEl.createEl("table", { cls: "dictcc-table" });
|
||||
const thead = table.createEl("thead");
|
||||
const hrow = thead.createEl("tr");
|
||||
hrow.createEl("th", { text: "English" });
|
||||
hrow.createEl("th", { text: "Deutsch" });
|
||||
const tbody = table.createEl("tbody");
|
||||
for (const row of rows) {
|
||||
const tr = tbody.createEl("tr");
|
||||
tr.createEl("td", { text: row.english });
|
||||
tr.createEl("td", { text: row.german });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Settings Tab ─────────────────────────────────────────────────────────────
|
||||
|
||||
class DictCCSettingTab extends PluginSettingTab {
|
||||
private plugin: DictCCPlugin;
|
||||
constructor(app: App, plugin: DictCCPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h2", { text: "dict.cc Sidebar Settings" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Translation direction")
|
||||
.setDesc("Filter which direction to show results for.")
|
||||
.addDropdown((drop) =>
|
||||
drop
|
||||
.addOption("both", "DE ↔ EN (both)")
|
||||
.addOption("de-en", "DE → EN only")
|
||||
.addOption("en-de", "EN → DE only")
|
||||
.setValue(this.plugin.settings.direction)
|
||||
.onChange(async (val) => {
|
||||
this.plugin.settings.direction = val as Direction;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Max results")
|
||||
.setDesc("Maximum number of translation rows to display (5–200).")
|
||||
.addSlider((s) =>
|
||||
s
|
||||
.setLimits(5, 200, 5)
|
||||
.setValue(this.plugin.settings.maxResults)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (val) => {
|
||||
this.plugin.settings.maxResults = val;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main Plugin ───────────────────────────────────────────────────────────────
|
||||
|
||||
export default class DictCCPlugin extends Plugin {
|
||||
settings!: DictCCSettings;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
await this.loadSettings();
|
||||
this.registerView(VIEW_TYPE, (leaf) => new DictCCSidebarView(leaf, this));
|
||||
this.addSettingTab(new DictCCSettingTab(this.app, this));
|
||||
|
||||
// Editor right-click menu
|
||||
this.registerEvent(
|
||||
(this.app.workspace as any).on(
|
||||
"editor-menu",
|
||||
(menu: Menu, editor: Editor, _view: MarkdownView) => {
|
||||
const word = editor.getSelection().trim().split(/\s+/)[0];
|
||||
if (!word) return;
|
||||
menu.addItem((item) =>
|
||||
item
|
||||
.setTitle(`dict.cc: "${word}"`)
|
||||
.setIcon("languages")
|
||||
.onClick(() => this.openAndLookup(word))
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Reading view right-click
|
||||
this.registerDomEvent(document, "contextmenu", (evt: MouseEvent) => {
|
||||
const target = evt.target as HTMLElement;
|
||||
if (!target.closest(".markdown-preview-view")) return;
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.isCollapsed) return;
|
||||
const selectedText = selection.toString().trim();
|
||||
const word = selectedText.split(/\s+/)[0];
|
||||
if (!word) return;
|
||||
evt.preventDefault();
|
||||
const menu = new Menu();
|
||||
menu.addItem((item) =>
|
||||
item
|
||||
.setTitle("Copy")
|
||||
.setIcon("copy")
|
||||
.onClick(() => navigator.clipboard.writeText(selectedText))
|
||||
);
|
||||
menu.addSeparator();
|
||||
menu.addItem((item) =>
|
||||
item
|
||||
.setTitle(`dict.cc: "${word}"`)
|
||||
.setIcon("languages")
|
||||
.onClick(() => this.openAndLookup(word))
|
||||
);
|
||||
menu.showAtMouseEvent(evt);
|
||||
});
|
||||
|
||||
// Command palette
|
||||
this.addCommand({
|
||||
id: "dictcc-lookup-selection",
|
||||
name: "Look up selection in dict.cc",
|
||||
editorCallback: (editor: Editor) => {
|
||||
const word = editor.getSelection().trim().split(/\s+/)[0];
|
||||
if (word) this.openAndLookup(word);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async onunload(): Promise<void> {
|
||||
this.app.workspace.detachLeavesOfType(VIEW_TYPE);
|
||||
}
|
||||
|
||||
async openAndLookup(word: string): Promise<void> {
|
||||
const { workspace } = this.app;
|
||||
let leaf: WorkspaceLeaf | null;
|
||||
const existing = workspace.getLeavesOfType(VIEW_TYPE);
|
||||
if (existing.length > 0) {
|
||||
leaf = existing[0];
|
||||
} else {
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
if (!leaf) return;
|
||||
await leaf.setViewState({ type: VIEW_TYPE, active: true });
|
||||
}
|
||||
workspace.revealLeaf(leaf);
|
||||
if (leaf.view instanceof DictCCSidebarView) {
|
||||
await leaf.view.lookup(word);
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
124
styles.css
Normal file
124
styles.css
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/* dict.cc Sidebar – Obsidian Plugin */
|
||||
|
||||
.dictcc-container {
|
||||
padding: 12px;
|
||||
font-size: var(--font-ui-small);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dictcc-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.dictcc-word {
|
||||
font-size: var(--font-ui-large);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.dictcc-source-link {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-accent);
|
||||
text-decoration: none;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dictcc-source-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dictcc-placeholder,
|
||||
.dictcc-loading,
|
||||
.dictcc-not-found {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* Suggestions / Did you mean */
|
||||
.dictcc-suggestions {
|
||||
background: var(--background-secondary);
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.dictcc-sug-label {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-smaller);
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
.dictcc-sug-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dictcc-sug-btn {
|
||||
background: var(--background-modifier-hover);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
padding: 2px 10px;
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-accent);
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease;
|
||||
}
|
||||
|
||||
.dictcc-sug-btn:hover {
|
||||
background: var(--background-modifier-active-hover);
|
||||
}
|
||||
|
||||
/* Section heading */
|
||||
.dictcc-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dictcc-section-title {
|
||||
font-size: var(--font-ui-small);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.dictcc-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--font-ui-small);
|
||||
}
|
||||
|
||||
.dictcc-table th {
|
||||
text-align: left;
|
||||
padding: 4px 6px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-smaller);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.dictcc-table td {
|
||||
padding: 4px 6px;
|
||||
vertical-align: top;
|
||||
color: var(--text-normal);
|
||||
border-bottom: 1px solid var(--background-modifier-border-hover);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.dictcc-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dictcc-table tr:hover td {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2018",
|
||||
"allowImportingTsExtensions": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": ["ES2018", "DOM"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Loading…
Reference in a new issue