├── .npmrc ├── .eslintignore ├── versions.json ├── src ├── main.css ├── utils │ ├── helpers.ts │ ├── use.ts │ ├── frontmatter.ts │ └── style.ts ├── store.ts ├── lang │ ├── helper.ts │ └── locale │ │ ├── zh-TW.ts │ │ ├── zh.ts │ │ └── en.ts ├── dictionary │ ├── uses.ts │ ├── youdao │ │ ├── YDCollins.vue │ │ ├── View.vue │ │ └── engine.ts │ ├── deepl │ │ ├── engine.ts │ │ └── View.vue │ ├── list.ts │ ├── jukuu │ │ ├── View.vue │ │ └── engine.ts │ ├── hjdict │ │ ├── View.vue │ │ └── engine.ts │ ├── cambridge │ │ ├── View.vue │ │ └── engine.ts │ └── helpers.ts ├── constant.ts ├── db │ ├── idb.ts │ ├── interface.ts │ ├── base.ts │ ├── web_db.ts │ └── local_db.ts ├── views │ ├── DataPanelView.ts │ ├── StatView.ts │ ├── SearchPanelView.ts │ ├── LearnPanelView.ts │ ├── Global.vue │ ├── CountBar.vue │ ├── PDFView.ts │ ├── Stat.vue │ ├── PopupSearch.vue │ ├── SearchPanel.vue │ ├── DictItem.vue │ ├── DataPanel.vue │ ├── parser.ts │ ├── ReadingView.ts │ ├── ReadingArea.vue │ └── LearnPanel.vue ├── component │ └── WordMore.vue ├── modals.ts └── api │ └── server.ts ├── public ├── alipay.jpg ├── reading.png ├── review.png ├── table.png ├── wechat.jpg ├── tutorial.pdf ├── complement1.png └── complement2.png ├── .github ├── FUNDING.yml └── workflows │ └── release.yml ├── .editorconfig ├── shims.d.ts ├── manifest.json ├── .gitignore ├── version-bump.mjs ├── .eslintrc ├── publish.mjs ├── tsconfig.json ├── LICENSE ├── package.json ├── esbuild.config.mjs └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.0.1": "0.12.17" 3 | } 4 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | @import "./stalin.css"; 2 | @import "../main.css"; -------------------------------------------------------------------------------- /src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | export function playAudio(src: string) { 2 | new Audio(src).play(); 3 | } -------------------------------------------------------------------------------- /public/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guopenghui/obsidian-language-learner/HEAD/public/alipay.jpg -------------------------------------------------------------------------------- /public/reading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guopenghui/obsidian-language-learner/HEAD/public/reading.png -------------------------------------------------------------------------------- /public/review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guopenghui/obsidian-language-learner/HEAD/public/review.png -------------------------------------------------------------------------------- /public/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guopenghui/obsidian-language-learner/HEAD/public/table.png -------------------------------------------------------------------------------- /public/wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guopenghui/obsidian-language-learner/HEAD/public/wechat.jpg -------------------------------------------------------------------------------- /public/tutorial.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guopenghui/obsidian-language-learner/HEAD/public/tutorial.pdf -------------------------------------------------------------------------------- /public/complement1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guopenghui/obsidian-language-learner/HEAD/public/complement1.png -------------------------------------------------------------------------------- /public/complement2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guopenghui/obsidian-language-learner/HEAD/public/complement2.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | custom: ['https://www.buymeacoffee.com/thtree'] 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | tab_width = 4 10 | line_width = 120 -------------------------------------------------------------------------------- /shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { ComponentOptions, DefineComponent } from 'vue'; 3 | const componentOptions: ComponentOptions; 4 | export default componentOptions; 5 | // const defineComponent: DefineComponent; 6 | // export default defineComponent; 7 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-language-learner", 3 | "name": "Language Learner", 4 | "version": "0.2.7", 5 | "minAppVersion": "0.12.17", 6 | "description": "阅读、查词、复习、使用、统计全部合一的语言学习插件", 7 | "author": "the_tree", 8 | "authorUrl": "", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | 3 | const store = reactive({ 4 | text: "", 5 | dark: false, 6 | themeChange: false, 7 | fontSize: "", 8 | fontFamily: "", 9 | lineHeight: "", 10 | popupSearch: true, 11 | searchPinned: false, 12 | dictsChange: false, 13 | dictHeight: "", 14 | }); 15 | 16 | export default store; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | /main.css 15 | 16 | # Exclude sourcemaps 17 | *.map 18 | 19 | # obsidian 20 | data.json 21 | 22 | # Exclude macOS Finder (System Explorer) View States 23 | .DS_Store 24 | 25 | # pdf 26 | pdf -------------------------------------------------------------------------------- /src/lang/helper.ts: -------------------------------------------------------------------------------- 1 | import zh from "./locale/zh"; 2 | import en from "./locale/en"; 3 | import zh_TW from "./locale/zh-TW"; 4 | 5 | const localeMap: { [k: string]: Partial; } = { 6 | en, 7 | zh, 8 | "zh-TW": zh_TW, 9 | }; 10 | 11 | 12 | const lang = window.localStorage.getItem("language"); 13 | const locale = localeMap[lang || "en"]; 14 | 15 | export function t(text: keyof typeof en): string { 16 | return (locale && locale[text]) || en[text]; 17 | } -------------------------------------------------------------------------------- /src/utils/use.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onBeforeUnmount, unref } from "vue"; 2 | import type { Ref } from "vue"; 3 | import type { EventMap } from "@/constant"; 4 | 5 | function useEvent( 6 | elRef: Ref | EventTarget, 7 | type: T, 8 | listener: (ev: EventMap[T]) => void 9 | ) { 10 | onMounted(() => { 11 | unref(elRef).addEventListener(type, listener); 12 | }); 13 | onBeforeUnmount(() => { 14 | unref(elRef).removeEventListener(type, listener); 15 | }); 16 | } 17 | 18 | export { useEvent }; -------------------------------------------------------------------------------- /src/dictionary/uses.ts: -------------------------------------------------------------------------------- 1 | import { watch } from "vue"; 2 | 3 | type Emit = (event: "loading", status: { id: string, loading: boolean, result: boolean }) => void; 4 | 5 | function useLoading(watchee: () => any, id: string, search: () => Promise, emit: Emit) { 6 | watch( 7 | watchee, 8 | async () => { 9 | emit("loading", { id, loading: true, result: false }); 10 | try { 11 | emit("loading", { id, loading: false, result: await search() }); 12 | } catch (e) { 13 | emit("loading", { id, loading: false, result: false }); 14 | } 15 | } 16 | ) 17 | } 18 | 19 | export { useLoading }; -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /src/constant.ts: -------------------------------------------------------------------------------- 1 | import { t } from "./lang/helper"; 2 | 3 | const dict = { 4 | NAME: "Language Learner" 5 | }; 6 | 7 | type Position = { 8 | x: number; 9 | y: number; 10 | }; 11 | 12 | interface EventMap extends GlobalEventHandlersEventMap { 13 | "obsidian-langr-search": CustomEvent<{ 14 | selection: string, 15 | target?: HTMLElement, 16 | evtPosition?: Position, 17 | }>; 18 | "obsidian-langr-refresh": CustomEvent<{ 19 | expression: string, 20 | type: string, 21 | status: number, 22 | }>; 23 | "obsidian-langr-refresh-stat": CustomEvent<{}>; 24 | } 25 | 26 | 27 | 28 | export { dict }; 29 | export type { EventMap, Position } 30 | 31 | 32 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /publish.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import process from "process"; 3 | import child from "child_process"; 4 | import path from "path"; 5 | 6 | const version = process.argv[2]; 7 | const root = process.cwd(); 8 | 9 | // write version 10 | let manifest = JSON.parse(fs.readFileSync(path.join(root, "manifest.json"), "utf8")); 11 | if (manifest.version !== version) { 12 | manifest.version = version; 13 | fs.writeFileSync(path.join(root, "manifest.json"), JSON.stringify(manifest, null, 4)); 14 | // message must use " instead of ' on windows 15 | child.execSync('git commit -am "update manifest"'); 16 | } 17 | 18 | child.execSync("git push"); 19 | child.execSync(`git tag ${version}`); 20 | child.execSync("git push --tags"); 21 | 22 | console.log("> Publish succeeded."); 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths":{ 5 | "@/*": ["./src/*"], 6 | "@dict/*": ["./src/dictionary/*"], 7 | "@comp/*": ["./src/component/*"] 8 | }, 9 | "inlineSourceMap": true, 10 | "inlineSources": true, 11 | "module": "ESNext", 12 | "target": "ES6", 13 | "allowJs": true, 14 | "noImplicitAny": true, 15 | "moduleResolution": "node", 16 | "importHelpers": true, 17 | "isolatedModules": true, 18 | "allowSyntheticDefaultImports": true, 19 | "jsx":"preserve", 20 | "lib": [ 21 | "DOM", 22 | "ES5", 23 | "ES6", 24 | "ES7", 25 | "dom.iterable" 26 | ] 27 | }, 28 | "include": [ 29 | "**/*.ts", 30 | "**/*.tsx", 31 | "**/*.vue" 32 | ] 33 | // "vueCompilerOptions": { 34 | // "target": 3, 35 | // "jsxTemplates": true, 36 | // "experimentalRfc436": true, 37 | // } 38 | } 39 | -------------------------------------------------------------------------------- /src/db/idb.ts: -------------------------------------------------------------------------------- 1 | import Dexie from "dexie"; 2 | import Plugin from "@/plugin"; 3 | 4 | export default class WordDB extends Dexie { 5 | expressions: Dexie.Table; 6 | sentences: Dexie.Table; 7 | plugin: Plugin; 8 | dbName: string; 9 | constructor(plugin: Plugin) { 10 | super(plugin.settings.db_name); 11 | this.plugin = plugin; 12 | this.dbName = plugin.settings.db_name; 13 | this.version(1).stores({ 14 | expressions: "++id, &expression, status, t, date, *tags", 15 | sentences: "++id, &text" 16 | }); 17 | } 18 | } 19 | 20 | interface Expression { 21 | id?: number, 22 | expression: string, 23 | meaning: string, 24 | status: number, 25 | t: string, 26 | date: number, 27 | notes: string[], 28 | tags: Set, 29 | sentences: Set, 30 | connections: Map, 31 | } 32 | interface Sentence { 33 | id?: number; 34 | text: string, 35 | trans: string, 36 | origin: string, 37 | } -------------------------------------------------------------------------------- /src/dictionary/youdao/YDCollins.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /src/dictionary/deepl/engine.ts: -------------------------------------------------------------------------------- 1 | import { requestUrl, RequestUrlParam } from "obsidian" 2 | 3 | const langMap: Record = { 4 | zh: "ZH", 5 | en: "EN", 6 | jp: "JA", 7 | fr: "FR", 8 | de: "DE", 9 | es: "ES", 10 | }; 11 | 12 | export async function search(text: string, lang: string = ""): Promise { 13 | let target = (/[\u4e00-\u9fa5]/.test(text) && !/[\u0800-\u4e00]/.test(text)) // chinese 14 | ? langMap[lang] || "ZH" 15 | : "ZH"; 16 | const payload = { 17 | text, 18 | source_lang: "auto", 19 | target_lang: target, 20 | }; 21 | 22 | const data: RequestUrlParam = { 23 | url: "https://deeplx.vercel.app/translate", 24 | method: "POST", 25 | body: JSON.stringify(payload), 26 | contentType: "application/json" 27 | }; 28 | 29 | try { 30 | let res = (await requestUrl(data)).json; 31 | if (res.code !== 200) throw new Error("Deeplx api source error."); 32 | 33 | return res.data; 34 | } catch (err) { 35 | console.error(err.message) 36 | } 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 the_tree 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/dictionary/list.ts: -------------------------------------------------------------------------------- 1 | import { t } from "@/lang/helper"; 2 | import Youdao from "./youdao/View.vue"; 3 | import Cambridge from "./cambridge/View.vue"; 4 | import Jukuu from "./jukuu/View.vue"; 5 | import HJdict from "./hjdict/View.vue"; 6 | import DeepL from "./deepl/View.vue"; 7 | 8 | const dicts = { 9 | "youdao": { 10 | name: t("Youdao"), 11 | description: `${t("English")} <=> ${t("Chinese")}`, 12 | Cp: Youdao 13 | }, 14 | "cambridge": { 15 | name: t("Cambridge"), 16 | description: `${t("English")} => ${t("Chinese")}`, 17 | Cp: Cambridge 18 | }, 19 | "jukuu": { 20 | name: t("Jukuu"), 21 | description: `${t("English")} <=> ${t("Chinese")}`, 22 | Cp: Jukuu 23 | }, 24 | "hjdict": { 25 | name: t("Hujiang"), 26 | description: `${t("English")},${t("Japanese")}, ${t("Korean")}, ${t("Spanish")}, ${t("French")}, ${t("Deutsch")} <=> ${t("Chinese")}`, 27 | Cp: HJdict 28 | }, 29 | "deepl": { 30 | name: "DeepL", 31 | description: `All <=> ${t("Chinese")}`, 32 | Cp: DeepL 33 | } 34 | }; 35 | 36 | export { dicts }; -------------------------------------------------------------------------------- /src/views/DataPanelView.ts: -------------------------------------------------------------------------------- 1 | import { ItemView, WorkspaceLeaf } from "obsidian"; 2 | import { createApp, App } from "vue"; 3 | import PluginType from "@/plugin"; 4 | import { t } from "@/lang/helper"; 5 | import DataPanel from "./DataPanel.vue"; 6 | 7 | export const DATA_ICON: string = "database"; 8 | export const DATA_PANEL_VIEW: string = "langr-data-panel"; 9 | 10 | export class DataPanelView extends ItemView { 11 | plugin: PluginType; 12 | vueapp: App; 13 | 14 | constructor(leaf: WorkspaceLeaf, plugin: PluginType) { 15 | super(leaf); 16 | this.plugin = plugin; 17 | } 18 | getViewType(): string { 19 | return DATA_PANEL_VIEW; 20 | } 21 | getDisplayText(): string { 22 | return t("Data Panel"); 23 | } 24 | getIcon(): string { 25 | return DATA_ICON; 26 | } 27 | async onOpen() { 28 | const container = this.containerEl.children[1]; 29 | container.empty(); 30 | // const contentEl = container.createEl("div", { 31 | // cls: "langr-search" 32 | // }) 33 | 34 | this.vueapp = createApp(DataPanel); 35 | this.vueapp.config.globalProperties.plugin = this.plugin; 36 | this.vueapp.mount(container); 37 | } 38 | async onClose() { 39 | this.vueapp.unmount(); 40 | this.vueapp = null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/views/StatView.ts: -------------------------------------------------------------------------------- 1 | import { ItemView, WorkspaceLeaf, } from 'obsidian'; 2 | import { createApp, App } from 'vue'; 3 | 4 | import MainPlugin from "@/plugin"; 5 | import { t } from "@/lang/helper"; 6 | import Stat from './Stat.vue'; 7 | 8 | export const STAT_ICON: string = "bar-chart-4"; 9 | export const STAT_VIEW_TYPE: string = 'langr-stat'; 10 | 11 | export class StatView extends ItemView { 12 | vueApp: App; 13 | plugin: MainPlugin; 14 | constructor(leaf: WorkspaceLeaf, plugin: MainPlugin) { 15 | super(leaf); 16 | this.plugin = plugin; 17 | } 18 | getViewType(): string { 19 | return STAT_VIEW_TYPE; 20 | } 21 | getDisplayText(): string { 22 | return t("Statistics"); 23 | } 24 | getIcon(): string { 25 | return STAT_ICON; 26 | } 27 | async onOpen() { 28 | const container = this.containerEl.children[1]; // view-content 29 | let content = container.createDiv({ cls: "langr-stat" }); 30 | 31 | this.vueApp = createApp(Stat); 32 | this.vueApp.config.globalProperties.container = content; 33 | this.vueApp.config.globalProperties.plugin = this.plugin; 34 | this.vueApp.mount(content); 35 | } 36 | async onClose() { 37 | this.vueApp.unmount(); 38 | } 39 | } -------------------------------------------------------------------------------- /src/dictionary/deepl/View.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 42 | 43 | -------------------------------------------------------------------------------- /src/views/SearchPanelView.ts: -------------------------------------------------------------------------------- 1 | import { ItemView, WorkspaceLeaf, } from 'obsidian'; 2 | import LanguageLearner from '@/plugin'; 3 | import { t } from "@/lang/helper"; 4 | import { createApp, App } from 'vue'; 5 | import SearchPanel from './SearchPanel.vue'; 6 | 7 | export const SEARCH_ICON: string = "book"; 8 | export const SEARCH_PANEL_VIEW: string = 'langr-search-panel'; 9 | 10 | export class SearchPanelView extends ItemView { 11 | plugin: LanguageLearner; 12 | vueapp: App; 13 | 14 | constructor(leaf: WorkspaceLeaf, plugin: LanguageLearner) { 15 | super(leaf); 16 | this.plugin = plugin; 17 | } 18 | getViewType(): string { 19 | return SEARCH_PANEL_VIEW; 20 | } 21 | getDisplayText(): string { 22 | return t("Search Panel"); 23 | } 24 | getIcon(): string { 25 | return SEARCH_ICON; 26 | } 27 | async onOpen(this: SearchPanelView) { 28 | const container = this.containerEl.children[1]; 29 | container.empty(); 30 | // const contentEl = container.createEl("div", { 31 | // cls: "langr-search" 32 | // }) 33 | 34 | this.vueapp = createApp(SearchPanel); 35 | this.vueapp.config.globalProperties.plugin = this.plugin; 36 | this.vueapp.mount(container); 37 | } 38 | async onClose() { 39 | this.vueapp.unmount(); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/db/interface.ts: -------------------------------------------------------------------------------- 1 | interface ArticleWords { 2 | article: string; 3 | words: string[]; 4 | } 5 | 6 | interface Word { 7 | text: string; 8 | status: number; 9 | } 10 | 11 | interface Phrase { 12 | text: string; 13 | status: number; 14 | offset: number; 15 | } 16 | 17 | interface WordsPhrase { 18 | words: Word[]; 19 | phrases: Phrase[]; 20 | } 21 | 22 | interface Sentence { 23 | text: string; 24 | trans: string; 25 | origin: string; 26 | } 27 | 28 | interface ExpressionInfo { 29 | expression: string; 30 | meaning: string; 31 | status: number; 32 | t: string; 33 | tags: string[]; 34 | notes: string[]; 35 | sentences: Sentence[]; 36 | } 37 | 38 | interface ExpressionInfoSimple { 39 | expression: string; 40 | meaning: string; 41 | status: number; 42 | t: string; 43 | tags: string[]; 44 | note_num: number; 45 | sen_num: number; 46 | date: number; 47 | } 48 | 49 | interface CountInfo { 50 | word_count: number[]; 51 | phrase_count: number[]; 52 | } 53 | 54 | 55 | interface Span { 56 | from: number; 57 | to: number; 58 | } 59 | 60 | interface WordCount { 61 | today: number[]; 62 | accumulated: number[]; 63 | } 64 | 65 | 66 | export type { 67 | ArticleWords, Word, Phrase, WordsPhrase, Sentence, 68 | ExpressionInfo, ExpressionInfoSimple, CountInfo, WordCount, Span 69 | }; -------------------------------------------------------------------------------- /src/views/LearnPanelView.ts: -------------------------------------------------------------------------------- 1 | import { ItemView, WorkspaceLeaf, } from 'obsidian'; 2 | import LanguageLearner from '@/plugin'; 3 | import { t } from "@/lang/helper"; 4 | import { createApp, App } from 'vue'; 5 | import LearnPanel from './LearnPanel.vue'; 6 | 7 | export const LEARN_ICON: string = "reading-glasses"; 8 | export const LEARN_PANEL_VIEW: string = 'langr-learn-panel'; 9 | 10 | export class LearnPanelView extends ItemView { 11 | plugin: LanguageLearner; 12 | vueapp: App; 13 | 14 | constructor(leaf: WorkspaceLeaf, plugin: LanguageLearner) { 15 | super(leaf); 16 | this.plugin = plugin; 17 | } 18 | getViewType(): string { 19 | return LEARN_PANEL_VIEW; 20 | } 21 | getDisplayText(): string { 22 | return t("Learning New Words"); 23 | } 24 | getIcon(): string { 25 | return LEARN_ICON; 26 | } 27 | async onOpen() { 28 | const container = this.containerEl.children[1]; 29 | container.empty(); 30 | // const contentEl = container.createEl("div", { 31 | // cls: "langr-learn" 32 | // }) 33 | this.vueapp = createApp(LearnPanel); 34 | this.vueapp.config.globalProperties.view = this; 35 | this.vueapp.config.globalProperties.plugin = this.plugin; 36 | this.vueapp.mount(container); 37 | } 38 | async onClose() { 39 | this.vueapp.unmount(); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/db/base.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArticleWords, Word, Phrase, WordsPhrase, Sentence, 3 | ExpressionInfo, ExpressionInfoSimple, CountInfo, WordCount, Span 4 | } from "./interface"; 5 | 6 | 7 | abstract class DbProvider { 8 | abstract open(): Promise; 9 | abstract close(): void; 10 | // 在文章中寻找之前记录过的单词和词组 11 | abstract getStoredWords(payload: ArticleWords): Promise; 12 | // 查询单个单词/词组的全部信息 13 | abstract getExpression(expression: string): Promise; 14 | //获取一批单词的简略信息 15 | abstract getExpressionsSimple(expressions: string[]): Promise; 16 | // 某一时间之后添加的全部单词 17 | abstract getExpressionAfter(time: string): Promise; 18 | // 获取全部单词的简略信息 19 | abstract getAllExpressionSimple(ignores?: boolean): Promise; 20 | // 发送单词信息到数据库保存 21 | abstract postExpression(payload: ExpressionInfo): Promise; 22 | // 获取所有tag 23 | abstract getTags(): Promise; 24 | // 批量发送单词,全部标记为ignore 25 | abstract postIgnoreWords(payload: string[]): Promise; 26 | // 查询一个例句是否已经记录过 27 | abstract tryGetSen(text: string): Promise; 28 | // 获取各类单词的个数 29 | abstract getCount(): Promise; 30 | // 获取7天内的统计信息 31 | abstract countSeven(): Promise; 32 | // 销毁数据库 33 | abstract destroyAll(): Promise; 34 | // 导入数据库 35 | abstract importDB(data: any): Promise; 36 | // 导出数据库 37 | abstract exportDB(): Promise; 38 | } 39 | 40 | 41 | export default DbProvider; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-sample-plugin", 3 | "version": "1.0.1", 4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "pub": "node publish.mjs", 10 | "version": "node version-bump.mjs && git add manifest.json versions.json" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "MIT", 15 | "dependencies": { 16 | "@vueuse/core": "^9.6.0", 17 | "ac-auto": "^2.0.0", 18 | "dexie": "^3.2.2", 19 | "dexie-export-import": "^1.0.3", 20 | "downloadjs": "^1.4.7", 21 | "echarts": "^5.3.2", 22 | "monkey-around": "^2.3.0", 23 | "nlcst-to-string": "^3.1.0", 24 | "parse-english": "^5.0.0", 25 | "retext-english": "^4.1.0", 26 | "unified": "^10.1.2", 27 | "unist-util-modify-children": "^3.0.0", 28 | "unist-util-visit": "^4.1.0", 29 | "vue": "^3.2.31" 30 | }, 31 | "devDependencies": { 32 | "@the_tree/esbuild-plugin-vue3": "^0.3.1", 33 | "@types/downloadjs": "^1.4.3", 34 | "@types/node": "^16.11.6", 35 | "@typescript-eslint/eslint-plugin": "^5.2.0", 36 | "@typescript-eslint/parser": "^5.2.0", 37 | "builtin-modules": "^3.2.0", 38 | "esbuild": "0.13.12", 39 | "hash-sum": "^2.0.0", 40 | "naive-ui": "^2.33.0", 41 | "obsidian": "latest", 42 | "sass": "^1.56.1", 43 | "tslib": "2.3.1", 44 | "typescript": "4.4.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/dictionary/jukuu/View.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | 39 | -------------------------------------------------------------------------------- /src/utils/frontmatter.ts: -------------------------------------------------------------------------------- 1 | import { App, TFile, parseYaml, stringifyYaml } from "obsidian"; 2 | 3 | type FrontMatter = { [K in string]: string }; 4 | 5 | export class FrontMatterManager { 6 | app: App; 7 | 8 | constructor(app: App) { 9 | this.app = app; 10 | } 11 | 12 | // 解析 13 | async loadFrontMatter(file: TFile): Promise { 14 | let res = {} as FrontMatter; 15 | let text = await this.app.vault.read(file); 16 | 17 | let match = text.match(/^\n*---\n([\s\S]+)\n---/); 18 | if (match) { 19 | res = parseYaml(match[1]); 20 | } 21 | 22 | return res; 23 | } 24 | 25 | async storeFrontMatter(file: TFile, fm: FrontMatter) { 26 | if (Object.keys(fm).length === 0) { 27 | return; 28 | } 29 | 30 | let text = await this.app.vault.read(file); 31 | let match = text.match(/^\n*---\n([\s\S]+)\n---/); 32 | 33 | let newText = ""; 34 | let newFront = stringifyYaml(fm); 35 | if (match) { 36 | newText = text.replace(/^\n*---\n([\s\S]+)\n---/, `---\n${newFront}---`); 37 | } else { 38 | newText = `---\n${newFront}---\n\n` + text; 39 | } 40 | 41 | this.app.vault.modify(file, newText); 42 | } 43 | 44 | // 读取值 45 | async getFrontMatter(file: TFile, key: string): Promise { 46 | let frontmatter = await this.loadFrontMatter(file); 47 | 48 | return frontmatter[key]; 49 | } 50 | 51 | // 修改 52 | async setFrontMatter(file: TFile, key: string, value: string) { 53 | let fm = await this.loadFrontMatter(file); 54 | 55 | fm[key] = value; 56 | 57 | this.storeFrontMatter(file, fm); 58 | } 59 | } -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from 'builtin-modules'; 4 | import vue from "@the_tree/esbuild-plugin-vue3"; 5 | 6 | const banner = 7 | `/* 8 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 9 | if you want to view the source, please visit the github repository of this plugin 10 | */ 11 | `; 12 | 13 | const prod = (process.argv[2] === 'production'); 14 | 15 | await esbuild.build({ 16 | banner: { 17 | js: banner, 18 | }, 19 | plugins: [ 20 | vue({ isProd: true }), 21 | ], 22 | entryPoints: ['./src/plugin.ts'], 23 | bundle: true, 24 | external: [ 25 | 'obsidian', 26 | 'electron', 27 | '@codemirror/autocomplete', 28 | '@codemirror/closebrackets', 29 | '@codemirror/collab', 30 | '@codemirror/commands', 31 | '@codemirror/comment', 32 | '@codemirror/fold', 33 | '@codemirror/gutter', 34 | '@codemirror/highlight', 35 | '@codemirror/history', 36 | '@codemirror/language', 37 | '@codemirror/lint', 38 | '@codemirror/matchbrackets', 39 | '@codemirror/panel', 40 | '@codemirror/rangeset', 41 | '@codemirror/rectangular-selection', 42 | '@codemirror/search', 43 | '@codemirror/state', 44 | '@codemirror/stream-parser', 45 | '@codemirror/text', 46 | '@codemirror/tooltip', 47 | '@codemirror/view', 48 | ...builtins], 49 | format: 'cjs', 50 | watch: !prod, 51 | target: 'es2016', 52 | logLevel: "info", 53 | sourcemap: prod ? false : 'inline', 54 | minify: prod ? true : false, 55 | treeShaking: true, 56 | outfile: 'main.js', 57 | }).catch(() => process.exit(1)); 58 | 59 | await esbuild.build({ 60 | entryPoints: ["./src/main.css"], 61 | outfile: "styles.css", 62 | watch: !prod, 63 | bundle: true, 64 | allowOverwrite: true, 65 | minify: false, 66 | }); 67 | 68 | // if (!prod) { 69 | // fs.rm("./main.css", () => { 70 | // console.log("Build completed successfully.") 71 | // }) 72 | // } -------------------------------------------------------------------------------- /src/views/Global.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 70 | 71 | -------------------------------------------------------------------------------- /src/views/CountBar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 52 | 53 | 82 | -------------------------------------------------------------------------------- /src/component/WordMore.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 43 | 44 | -------------------------------------------------------------------------------- /src/utils/style.ts: -------------------------------------------------------------------------------- 1 | function getRGB(selector: string, property: string): { R: number, G: number, B: number; } { 2 | let elem = document.querySelector(selector); 3 | if (!elem) return { R: 0, G: 0, B: 0 }; 4 | 5 | let rgb = window.getComputedStyle(elem, null) 6 | .getPropertyValue(property) 7 | .match(/\d+/g) 8 | .map(v => parseInt(v)); 9 | return { R: rgb[0], G: rgb[1], B: rgb[2] }; 10 | } 11 | 12 | function getPageSize(): { pageW: number, pageH: number; } { 13 | let w = window.getComputedStyle(document.body, null).getPropertyValue("width"); 14 | let h = window.getComputedStyle(document.body, null).getPropertyValue("height"); 15 | return { 16 | pageW: parseInt(w.match(/\d+/)[0]), 17 | pageH: parseInt(h.match(/\d+/)[0]), 18 | }; 19 | } 20 | 21 | function optimizedPos( 22 | containerSize: { h: number, w: number; }, 23 | elemSize: { h: number, w: number, }, 24 | eventPos: { x: number, y: number, }, 25 | xPadding: number, 26 | yPadding: number, 27 | ): { x: number; y: number; } { 28 | let x: number = 0, y: number = 0; 29 | if (containerSize.w - eventPos.x - xPadding - elemSize.w > 0) { // 右 30 | x = eventPos.x + xPadding; 31 | y = eventPos.y < containerSize.h / 2 ? 32 | Math.min(eventPos.y, (containerSize.h - elemSize.h) / 2) : 33 | Math.max(eventPos.y - elemSize.h, (containerSize.h - elemSize.h) / 2); 34 | } else if (eventPos.x - xPadding - elemSize.w > 0) { // 左 35 | x = eventPos.x - xPadding - elemSize.w; 36 | y = eventPos.y < containerSize.h / 2 ? 37 | Math.min(eventPos.y, (containerSize.h - elemSize.h) / 2) : 38 | Math.max(eventPos.y - elemSize.h, (containerSize.h - elemSize.h) / 2); 39 | } else if (eventPos.y - elemSize.h - yPadding > 0) { // 上 40 | x = eventPos.x < containerSize.w / 2 ? 41 | Math.min(eventPos.x, (containerSize.w - elemSize.w) / 2) : 42 | Math.max(eventPos.x - elemSize.w, (containerSize.w - elemSize.w) / 2); 43 | y = eventPos.y - yPadding - elemSize.h; 44 | } else { // 下 45 | x = eventPos.x < containerSize.w / 2 ? 46 | Math.min(eventPos.x, (containerSize.w - elemSize.w) / 2) : 47 | Math.max(eventPos.x - elemSize.w, (containerSize.w - elemSize.w) / 2); 48 | y = eventPos.y + yPadding; 49 | } 50 | return { x, y }; 51 | } 52 | 53 | 54 | export { getRGB, getPageSize, optimizedPos }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 用Obsidian来学习语言! 2 | 3 | 公告:已经开放discussion区,一般的问题可以先在这里进行讨论。 4 | 5 | ### 早期阶段 6 | 当前插件还处在早期开发阶段,因此有以下事情需要注意: 7 | + **目前仅支持中文母语者学习英文**。 8 | + 因为还在不断的扩充新功能和重构旧功能,所以可能某次更新会带来与之前**不兼容的改变**(比如笔记的格式,数据库的结构等)。所以在更新新版本前请仔细查看release的说明。 9 | 10 | 11 | ### 使用指南 12 | + [文字教程](https://github.com/guopenghui/obsidian-language-learner/blob/master/public/tutorial.pdf) 13 | + [视频教程](https://www.bilibili.com/video/BV1914y1Y7mT) 14 | + [一些做好的文本](https://github.com/guopenghui/language-learner-texts) 15 | + @emisjerry 制作的使用教程: [Youtube](https://www.youtube.com/watch?v=lK3oFpUg7-o), [Bilibili](https://www.bilibili.com/video/BV1N24y1k7SL/) 16 | 17 | 18 | 19 | ### 本插件功能 20 | 21 | + **查词功能**。直接在笔记中划词查词,词典为有道词典,支持柯林斯例句、近义词辨析。 22 | + **添加笔记**。数据被保存在obsidain的indexDB数据库中。每个单词/短语支持多条笔记、多条例句(包括文本、翻译和出处) 23 | + **解析页面**。将每个单词变成一个按钮,通过点击就可以边读边查边记笔记。如果有音频链接的话可以边听边读。 24 | + **统计页面**。目前支持显示7天内每天自己记的单词数和总的单词数。 25 | 26 | 联动其他插件功能: 27 | + 联动various complements插件,将数据库中的单词保存在本地的一个note中。这样就可以在写作时得到自己之前记过的单词/短语的**自动提示和补全** 28 | + 联动spaced repetition插件,将数据库中的单词保存在本地的note中。这样就可以制作成卡片,进行**间隔复习**。 29 | 30 | ### 外观展示 31 | 阅读: 32 | 33 | ![阅读界面](https://github.com/guopenghui/obsidian-language-learner/blob/master/public/reading.png) 34 | 35 | 单词列表: 36 | ![单词列表](https://github.com/guopenghui/obsidian-language-learner/blob/master/public/table.png) 37 | 38 | 自动补全/提示: 39 | 40 | ![自动补全-英中](https://github.com/guopenghui/obsidian-language-learner/blob/master/public/complement1.png) 41 | ![自动补全-中英](https://github.com/guopenghui/obsidian-language-learner/blob/master/public/complement2.png) 42 | 43 | 间隔复习: 44 | 45 | ![间隔复习](https://github.com/guopenghui/obsidian-language-learner/blob/master/public/review.png) 46 | 47 | 48 | 49 | 50 | ## 安装 51 | 52 | + 从realease下载压缩包`obsidian-language-leaner.zip` 53 | + 解压到obsidian库的`.obsidian/plugins/`文件夹下,即保证`main.js`的路径为`.obsidian/plugins/obsidian-language-learner/main.js` 54 | + 打开obsidian,在插件中启用本插件`Language Learner`. 55 | + 配置见[使用指南](#使用指南) 56 | ## 自行构建 57 | 58 | 下载源码到本地 59 | ```shell 60 | git clone https://github.com/guopenghui/obsidian-language-learner.git 61 | ``` 62 | 63 | 进入文件夹,运行 64 | ```shell 65 | cd obsidian-language-learner 66 | # 安装依赖 67 | npm install 68 | # 构建 会自动压缩代码体积 69 | npm run build 70 | ``` 71 | 72 | ## 问题或建议 73 | 欢迎大家提交issue: 74 | + bug反馈 75 | + 对新功能的想法 76 | + 对已有功能的优化 77 | 78 | 可能有时作者暂时比较忙,或是对于提出的功能需求暂时没想到好的实现方法而没有一一回复。 79 | 80 | 但是只要提了issue都会看的,所以大家有想法或反馈直接发到issue就行。 81 | 82 | 83 | ## 新鼠标 84 | 在鼠标寿命到头,左键时灵时不灵的艰难的环境下完成了0.0.1版的发布。😭 85 | 86 | 觉得这款插件好用的朋友,或是想鼓励一下作者,可以赞助孩子买个新鼠标!!🖱 87 | 88 | ![微信](https://github.com/guopenghui/obsidian-language-learner/blob/master/public/wechat.jpg) 89 | ![支付宝](https://github.com/guopenghui/obsidian-language-learner/blob/master/public/alipay.jpg) -------------------------------------------------------------------------------- /src/dictionary/jukuu/engine.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HTMLString, 3 | getText, 4 | getInnerHTML, 5 | handleNoResult, 6 | handleNetWorkError, 7 | SearchFunction, 8 | GetSrcPageFunction, 9 | removeChildren, 10 | fetchDirtyDOM, 11 | DictSearchResult 12 | } from '../helpers'; 13 | 14 | export type JukuuLang = 'engjp' | 'zhjp' | 'zheng'; 15 | 16 | function getUrl(text: string, lang: JukuuLang) { 17 | text = encodeURIComponent(text.replace(/\s+/g, '+')); 18 | 19 | switch (lang) { 20 | case 'engjp': 21 | return 'http://www.jukuu.com/jsearch.php?q=' + text; 22 | case 'zhjp': 23 | return 'http://www.jukuu.com/jcsearch.php?q=' + text; 24 | // case 'zheng': 25 | default: 26 | return 'http://www.jukuu.com/search.php?q=' + text; 27 | } 28 | } 29 | 30 | export const getSrcPage: GetSrcPageFunction = (text) => { 31 | return getUrl(text, "zheng"); 32 | }; 33 | 34 | interface JukuuTransItem { 35 | trans: HTMLString; 36 | original: string; 37 | src: string; 38 | } 39 | 40 | export interface JukuuResult { 41 | lang: JukuuLang; 42 | sens: JukuuTransItem[]; 43 | } 44 | 45 | export interface JukuuPayload { 46 | lang?: JukuuLang; 47 | } 48 | 49 | type JukuuSearchResult = DictSearchResult; 50 | 51 | export const search: SearchFunction = ( 52 | text: string, 53 | ) => { 54 | const lang = "zheng"; 55 | return fetchDirtyDOM(getSrcPage(text)) 56 | .catch(handleNetWorkError) 57 | .then(handleDOM) 58 | .then(sens => 59 | sens.length > 0 ? { result: { lang, sens } } : handleNoResult() 60 | ); 61 | }; 62 | 63 | function handleDOM(doc: DocumentFragment): JukuuTransItem[] { 64 | return [...doc.querySelectorAll('tr.e')] 65 | .map($e => { 66 | const $trans = $e.lastElementChild; 67 | if (!$trans) { 68 | return; 69 | } 70 | removeChildren($trans, 'img'); 71 | 72 | const $original = $e.nextElementSibling; 73 | if (!$original || !$original.classList.contains('c')) { 74 | return; 75 | } 76 | 77 | const $src = $original.nextElementSibling; 78 | 79 | return { 80 | trans: getInnerHTML('http://www.jukuu.com', $trans), 81 | original: getText($original), 82 | src: 83 | $src && $src.classList.contains('s') 84 | ? getText($src).replace(/^[\s-]*/, '') 85 | : '' 86 | }; 87 | }) 88 | .filter((item): item is JukuuTransItem => Boolean(item && item.trans)); 89 | } 90 | -------------------------------------------------------------------------------- /src/views/PDFView.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WorkspaceLeaf, 3 | FileView, 4 | TFile, 5 | Menu, 6 | Notice, 7 | normalizePath, 8 | Platform, 9 | } from "obsidian"; 10 | import Plugin from "@/plugin"; 11 | 12 | export const PDF_FILE_EXTENSION = "pdf"; 13 | export const VIEW_TYPE_PDF = "langr-pdf"; 14 | export const ICON_EPUB = "pdf"; 15 | 16 | export class PDFView extends FileView { 17 | constructor(leaf: WorkspaceLeaf) { 18 | super(leaf); 19 | } 20 | 21 | onPaneMenu(menu: Menu): void { 22 | // menu.addItem((item) => { 23 | // item.setTitle("Create new epub note") 24 | // .setIcon("document") 25 | // .onClick(async () => { 26 | // const fileName = this.getFileName(); 27 | // let file = this.app.vault.getAbstractFileByPath(fileName); 28 | // if (file == null || !(file instanceof TFile)) { 29 | // file = await this.app.vault.create(fileName, this.getFileContent()); 30 | // } 31 | // const fileLeaf = this.app.workspace.createLeafBySplit(this.leaf); 32 | // fileLeaf.openFile(file as TFile, { 33 | // active: true 34 | // }); 35 | // }); 36 | // }); 37 | // menu.addSeparator(); 38 | super.onPaneMenu(menu, ""); 39 | } 40 | 41 | getFileName() { 42 | return this.file.name; 43 | } 44 | 45 | getFileContent() { 46 | 47 | } 48 | 49 | async onLoadFile(file: TFile): Promise { 50 | this.contentEl.empty(); 51 | 52 | // const contents = await this.app.vault.adapter.readBinary(file.path); 53 | // console.log(file) 54 | 55 | let basePath = normalizePath((this.app.vault.adapter as any).basePath); 56 | const prefix = Platform.isDesktopApp ? "app://local/" : "http://localhost/_capacitor_file_"; 57 | const viewerPath = 58 | `${prefix}${basePath}/%2Eobsidian/plugins/obsidian-language-learner/pdf/web/viewer.html`; 59 | let content = 60 | '